diff --git a/composer.lock b/composer.lock index b98610e74..2f3e7358a 100644 --- a/composer.lock +++ b/composer.lock @@ -136,20 +136,20 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -196,7 +196,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -212,7 +212,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "utopia-php/cache", @@ -506,16 +506,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.2", + "version": "v1.17.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" + "reference": "9d77be916e145864f10788bb94531d03e1f7b482" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", - "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482", + "reference": "9d77be916e145864f10788bb94531d03e1f7b482", "shasum": "" }, "require": { @@ -526,13 +526,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.61.1", - "illuminate/view": "^10.48.18", + "friendsofphp/php-cs-fixer": "^3.64.0", + "illuminate/view": "^10.48.20", "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.35.0" + "pestphp/pest": "^2.35.1" }, "bin": [ "builds/pint" @@ -568,7 +568,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-08-06T15:11:54+00:00" + "time": "2024-09-03T15:00:28+00:00" }, { "name": "myclabs/deep-copy", @@ -632,16 +632,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.19.1", + "version": "v4.19.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" + "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2", + "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2", "shasum": "" }, "require": { @@ -650,7 +650,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -682,9 +682,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4" }, - "time": "2024-03-17T08:10:35+00:00" + "time": "2024-09-29T15:01:53+00:00" }, { "name": "pcov/clobber", @@ -1217,16 +1217,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.20", + "version": "9.6.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "49d7820565836236411f5dc002d16dd689cde42f" + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f", - "reference": "49d7820565836236411f5dc002d16dd689cde42f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", "shasum": "" }, "require": { @@ -1241,7 +1241,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.31", + "phpunit/php-code-coverage": "^9.2.32", "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.4", @@ -1300,7 +1300,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" }, "funding": [ { @@ -1316,7 +1316,7 @@ "type": "tidelift" } ], - "time": "2024-07-10T11:45:39+00:00" + "time": "2024-09-19T10:50:18+00:00" }, { "name": "psr/container", diff --git a/phpunit.xml b/phpunit.xml index ccdaa969e..783265d80 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false"> + stopOnFailure="true"> ./tests/unit diff --git a/src/Database/Database.php b/src/Database/Database.php index 437f18881..374f2505d 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -21,7 +21,7 @@ use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\Queries\Document as DocumentValidator; -use Utopia\Database\Validator\Queries\Documents as DocumentsValidator; +use Utopia\Database\Validator\Queries\V2 as DocumentsValidator; use Utopia\Database\Validator\Structure; class Database @@ -5006,11 +5006,15 @@ public function find(string $collection, array $queries = []): array throw new DatabaseException('Collection not found'); } - $attributes = $collection->getAttribute('attributes', []); - $indexes = $collection->getAttribute('indexes', []); - if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes); + $collections = []; + $collections[] = $collection; + $joins = Query::getByType($queries, [Query::TYPE_JOIN]); + foreach ($joins as $join) { + $collections[] = $this->silent(fn () => $this->getCollection($join->getCollection())); + } + + $validator = new DocumentsValidator($collections); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } diff --git a/src/Database/Query.php b/src/Database/Query.php index 6af553415..f62386ed8 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -21,6 +21,7 @@ class Query public const TYPE_BETWEEN = 'between'; public const TYPE_STARTS_WITH = 'startsWith'; public const TYPE_ENDS_WITH = 'endsWith'; + public const TYPE_RELATION = 'relation'; public const TYPE_SELECT = 'select'; @@ -38,6 +39,12 @@ class Query public const TYPE_AND = 'and'; public const TYPE_OR = 'or'; + // Join methods + public const TYPE_JOIN = 'join'; + public const TYPE_INNER_JOIN = 'innerJoin'; + public const TYPE_LEFT_JOIN = 'leftJoin'; + public const TYPE_RIGHT_JOIN = 'rightJoin'; + public const TYPES = [ self::TYPE_EQUAL, self::TYPE_NOT_EQUAL, @@ -69,8 +76,17 @@ class Query ]; protected string $method = ''; + protected string $as = ''; + protected string $collection = ''; + protected string $type = ''; + protected string $function = ''; + protected string $alias = ''; protected string $attribute = ''; + protected string $aliasRight = ''; + protected string $attributeRight = ''; + protected bool $onArray = false; + protected bool $isRelation = false; /** * @var array @@ -84,11 +100,29 @@ class Query * @param string $attribute * @param array $values */ - public function __construct(string $method, string $attribute = '', array $values = []) + protected function __construct( + string $method, + string $attribute = '', + array $values = [], + string $alias = '', + string $attributeRight = '', + string $aliasRight = '', + string $as = '', + string $collection = '', + string $function = '', + string $type = '' + ) { $this->method = $method; + $this->alias = $alias; $this->attribute = $attribute; $this->values = $values; + $this->function = $function; + $this->aliasRight = $aliasRight; + $this->attributeRight = $attributeRight; + $this->as = $as; + $this->collection = $collection; + $this->type = $type; } public function __clone(): void @@ -133,6 +167,31 @@ public function getValue(mixed $default = null): mixed return $this->values[0] ?? $default; } + public function getAlias(): string + { + return $this->alias; + } + + public function getRightAlias(): string + { + return $this->aliasRight; + } + + public function getAttributeRight(): string + { + return $this->attributeRight; + } + + public function getCollection(): string + { + return $this->collection; + } + + public function getType(): string + { + return $this->type; + } + /** * Sets method * @@ -338,9 +397,9 @@ public function toString(): string * @param array $values * @return Query */ - public static function equal(string $attribute, array $values): self + public static function equal(string $attribute, array $values, string $alias = ''): self { - return new self(self::TYPE_EQUAL, $attribute, $values); + return new self(self::TYPE_EQUAL, $attribute, $values, alias: $alias); } /** @@ -457,9 +516,9 @@ public static function select(array $attributes): self * @param string $attribute * @return Query */ - public static function orderDesc(string $attribute = ''): self + public static function orderDesc(string $attribute = '', string $alias = ''): self { - return new self(self::TYPE_ORDER_DESC, $attribute); + return new self(self::TYPE_ORDER_DESC, $attribute, alias: $alias); } /** @@ -567,6 +626,35 @@ public static function and(array $queries): self return new self(self::TYPE_AND, '', $queries); } + /** + * @param string $collection + * @param string $alias + * @param array $conditions + * @return Query + */ + public static function join(string $collection, string $alias, array $queries = []): self + { + //$conditions = Query::groupByType($queries)['filters']; + //$conditions = Query::groupByType($queries)['relations']; + + return new self(self::TYPE_JOIN, '', $queries, alias: $alias, collection: $collection, type: self::TYPE_INNER_JOIN); + } + + /** + * @param string $collection + * @param string $alias + * @param array $conditions + * @return Query + */ + public static function relation($leftAlias, string $leftColumn, string $method, string $rightAlias, string $rightColumn): self + { + $value = [ + 'method' => $method, + ]; + + return new self(self::TYPE_RELATION, $leftColumn, $value, alias: $leftAlias, attributeRight: $rightColumn, aliasRight: $rightAlias); + } + /** * Filters $queries for $types * @@ -605,6 +693,7 @@ public static function getByType(array $queries, array $types): array public static function groupByType(array $queries): array { $filters = []; + $joins = []; $selections = []; $limit = null; $offset = null; @@ -665,6 +754,10 @@ public static function groupByType(array $queries): array $selections[] = clone $query; break; + case Query::TYPE_JOIN: + $joins[] = clone $query; + break; + default: $filters[] = clone $query; break; @@ -680,6 +773,7 @@ public static function groupByType(array $queries): array 'orderTypes' => $orderTypes, 'cursor' => $cursor, 'cursorDirection' => $cursorDirection, + 'join' => $joins, ]; } diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index 2e4aac71a..5072a262d 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -101,14 +101,21 @@ public function isValid($value): bool Query::TYPE_ENDS_WITH, Query::TYPE_AND, Query::TYPE_OR => Base::METHOD_TYPE_FILTER, + Query::TYPE_JOIN => Base::METHOD_TYPE_JOIN, default => '', }; - + var_dump('____________________________________'); $methodIsValid = false; foreach ($this->validators as $validator) { + var_dump('---'); + var_dump($method); + var_dump($methodType); + var_dump($validator->getMethodType()); + var_dump('---'); if ($validator->getMethodType() !== $methodType) { continue; } + if (!$validator->isValid($query)) { $this->message = 'Invalid query: ' . $validator->getDescription(); return false; diff --git a/src/Database/Validator/Queries/V2.php b/src/Database/Validator/Queries/V2.php new file mode 100644 index 000000000..259b17db1 --- /dev/null +++ b/src/Database/Validator/Queries/V2.php @@ -0,0 +1,543 @@ + $collections + * + * @throws Exception + */ + public function __construct(array $collections, int $length = 0, int $maxValuesCount = 100, int $maxLimit = PHP_INT_MAX, int $maxOffset = PHP_INT_MAX) + { + foreach ($collections as $i => $collection) { + if($i === 0){ + $this->aliases[''] = $collection->getId(); + } + + //$this->collections[$collection->getId()] = $collection->getArrayCopy(); + + $attributes = $collection->getAttribute('attributes', []); + foreach ($attributes as $attribute) { + // todo: internal id's? + $this->schema[$collection->getId()][$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); + } + } + + $this->maxLimit = $maxLimit; + $this->maxOffset = $maxOffset; + $this->length = $length; + $this->maxValuesCount = $maxValuesCount; + + // $attributes[] = new Document([ + // '$id' => '$id', + // 'key' => '$id', + // 'type' => Database::VAR_STRING, + // 'array' => false, + // ]); + // $attributes[] = new Document([ + // '$id' => '$internalId', + // 'key' => '$internalId', + // '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($collections), + // new Order($collections), + // new Select($collections), + // new Join($collections), + // ]; + } + + /** + * @param array $value + * + * @throws \Utopia\Database\Exception\Query + */ + public function isValid($value): bool + { + if (! is_array($value)) { + $this->message = 'Queries must be an array'; + + return false; + } + + if ($this->length && \count($value) > $this->length) { + return false; + } + + var_dump('in isValid '); + var_dump($this->aliases); + $queries = []; + + foreach ($value as $query) { + if (!$query instanceof Query) { + try { + $query = Query::parse($query); + } catch (\Throwable $e) { + $this->message = 'Invalid query: ' . $e->getMessage(); + + return false; + } + } + + if($query->getMethod() === Query::TYPE_JOIN) { + $this->aliases[$query->getAlias()] = $query->getCollection(); + } + + var_dump($query); + $queries[] = $query; + } + + foreach ($queries as $query) { + if ($query->isNested()) { + if (! self::isValid($query->getValues())) { + return false; + } + } + + $method = $query->getMethod(); + + switch ($method) { + case Query::TYPE_EQUAL: + case Query::TYPE_CONTAINS: + if ($this->isEmpty($query->getValues())) { + $this->message = \ucfirst($method).' queries require at least one value.'; + return false; + } + + if(!$this->isAttributeExist($query->getAttribute(), $query->getAlias())){ + return false; + } + + if(!$this->isValidValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method)){ + return false; + } + + return true; + + 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($query->getValues()) != 1) { + $this->message = \ucfirst($method).' queries require exactly one value.'; + + return false; + } + + if(!$this->isAttributeExist($query->getAttribute(), $query->getAlias())){ + return false; + } + + if(!$this->isValidValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method)){ + return false; + } + + return true; + + case Query::TYPE_BETWEEN: + if (count($query->getValues()) != 2) { + $this->message = \ucfirst($method).' queries require exactly two values.'; + + return false; + } + + if(!$this->isAttributeExist($query->getAttribute(), $query->getAlias())){ + return false; + } + + if(!$this->isValidValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method)){ + return false; + } + + return true; + + case Query::TYPE_IS_NULL: + case Query::TYPE_IS_NOT_NULL: + if(!$this->isAttributeExist($query->getAttribute(), $query->getAlias())){ + return false; + } + + if(!$this->isValidValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method)){ + return false; + } + + return true; + + case Query::TYPE_OR: + case Query::TYPE_AND: + $filters = Query::groupByType($query->getValues())['filters']; + + if (count($query->getValues()) !== count($filters)) { + $this->message = \ucfirst($method).' queries can only contain filter queries'; + + return false; + } + + if (count($filters) < 2) { + $this->message = \ucfirst($method).' queries require at least two queries'; + + return false; + } + + return true; + + case Query::TYPE_RELATION: + echo 'Hello TYPE_RELATION'; + break; + + case Query::TYPE_LIMIT: + return $this->isValidLimit($query); + + case Query::TYPE_OFFSET: + return $this->isValidOffset($query); + + case Query::TYPE_SELECT: + return $this->isValidSelect($query); + + default: + return false; + } + } + + return false; + } + + /** + * Get Description. + * + * Returns validator description + */ + public function getDescription(): string + { + return $this->message; + } + + /** + * Is array + * + * Function will return true if object is array. + */ + public function isArray(): bool + { + return true; + } + + /** + * Get Type + * + * Returns validator type. + */ + public function getType(): string + { + return self::TYPE_OBJECT; + } + + /** + * @param array $values + */ + 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; + } + + protected function isAttributeExist(string $attributeId, string $alias): bool + { + var_dump("=== isAttributeExist"); + +// if (\str_contains($attributeId, '.')) { +// // Check for special symbol `.` +// if (isset($this->schema[$attributeId])) { +// return true; +// } +// +// // For relationships, just validate the top level. +// // will validate each nested level during the recursive calls. +// $attributeId = \explode('.', $attributeId)[0]; +// +// if (isset($this->schema[$attributeId])) { +// $this->message = 'Cannot query nested attribute on: '.$attributeId; +// +// return false; +// } +// } + + $collectionId = $this->aliases[$alias]; + var_dump("=== attribute === " . $attributeId); + var_dump("=== alias === " . $alias); + var_dump("=== collectionId === " . $collectionId); + + var_dump($this->schema[$collectionId][$attributeId]); + + if (! isset($this->schema[$collectionId][$attributeId])) { + $this->message = 'Attribute not found in schema: '.$attributeId; + + return false; + } + + return true; + } + + protected function isValidValues(string $attributeId, string $alias, array $values, string $method): bool + { + var_dump("=== isValidValues"); + + if (count($values) > $this->maxValuesCount) { + $this->message = 'Query on attribute has greater than '.$this->maxValuesCount.' values: '.$attributeId; + + return false; + } + + $collectionId = $this->aliases[$alias]; + + $attribute = $this->schema[$collectionId][$attributeId]; + + foreach ($values as $value) { + + $validator = null; + + switch ($attribute['type']) { + case Database::VAR_STRING: + $validator = new Text(0, 0); + break; + + case Database::VAR_INTEGER: + $validator = new Integer; + break; + + case Database::VAR_FLOAT: + $validator = new FloatValidator; + break; + + case Database::VAR_BOOLEAN: + $validator = new Boolean; + break; + + case Database::VAR_DATETIME: + $validator = new DatetimeValidator; + break; + + case Database::VAR_RELATIONSHIP: + $validator = new Text(255, 0); // The query is always on uid + break; + default: + $this->message = 'Unknown Data type'; + + return false; + } + + if (! $validator->isValid($value)) { + $this->message = 'Query value is invalid for attribute "'.$attributeId.'"'; + + return false; + } + } + + if ($attribute['type'] === 'relationship') { + /** + * We can not disable relationship query since we have logic that use it, + * so instead we validate against the relation type + */ + $options = $attribute['options']; + + if ($options['relationType'] === Database::RELATION_ONE_TO_ONE && $options['twoWay'] === false && $options['side'] === Database::RELATION_SIDE_CHILD) { + $this->message = 'Cannot query on virtual relationship attribute'; + + return false; + } + + if ($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { + $this->message = 'Cannot query on virtual relationship attribute'; + + return false; + } + + if ($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { + $this->message = 'Cannot query on virtual relationship attribute'; + + return false; + } + + if ($options['relationType'] === Database::RELATION_MANY_TO_MANY) { + $this->message = 'Cannot query on virtual relationship attribute'; + + return false; + } + } + + $array = $attribute['array'] ?? false; + + if ( + ! $array && + $method === Query::TYPE_CONTAINS && + $attribute['type'] !== Database::VAR_STRING + ) { + $this->message = 'Cannot query contains on attribute "'.$attributeId.'" because it is not an array or string.'; + + return false; + } + + if ( + $array && + ! in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL]) + ) { + $this->message = 'Cannot query '.$method.' on attribute "'.$attributeId.'" because it is an array.'; + + return false; + } + + return true; + } + + public function isValidLimit(Query $query): bool + { + $limit = $query->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 isValidOffset(Query $query): bool + { + $offset = $query->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 isValidSelect(Query $query): bool + { + $internalKeys = \array_map( + fn ($attr) => $attr['$id'], + Database::INTERNAL_ATTRIBUTES + ); + + foreach ($query->getValues() as $attribute) { + + if(is_string()){ + + } + else if($this->isArray()){ + + } + + if($this->isAttributeExist()){ + + } + +// 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 (\in_array($attribute, $internalKeys)) { + continue; + } + + if (!isset($this->schema[$attribute]) && $attribute !== '*') { + $this->message = 'Attribute not found in schema: ' . $attribute; + return false; + } + } + return true; + } + +} diff --git a/src/Database/Validator/Query/Base.php b/src/Database/Validator/Query/Base.php index a37fdd65a..6b40c37af 100644 --- a/src/Database/Validator/Query/Base.php +++ b/src/Database/Validator/Query/Base.php @@ -12,6 +12,7 @@ abstract class Base extends Validator public const METHOD_TYPE_ORDER = 'order'; public const METHOD_TYPE_FILTER = 'filter'; public const METHOD_TYPE_SELECT = 'select'; + public const METHOD_TYPE_JOIN = 'join'; protected string $message = 'Invalid query'; diff --git a/src/Database/Validator/Query/Join.php b/src/Database/Validator/Query/Join.php new file mode 100644 index 000000000..82e2f5543 --- /dev/null +++ b/src/Database/Validator/Query/Join.php @@ -0,0 +1,44 @@ +getMethod(); + + if ($method === Query::TYPE_JOIN) { + if (! in_array($value->getType(), $this->types)) { + $this->message = 'Invalid join type'; + + return false; + } + + return true; + } + + return false; + } + + public function getMethodType(): string + { + return self::METHOD_TYPE_JOIN; + } +} diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 72587d44a..1f40da090 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -2251,6 +2251,26 @@ public function testListDocumentSearch(): void $this->assertEquals(1, count($documents)); } + public function testJoin() + { + $documents = static::getDatabase()->find( + 'documents', + [ + Query::join( + 'users', + 'u', + [ + Query::relation('main', 'id', Query::TYPE_EQUAL, 'u', 'user_id'), + Query::equal('id', ['usa'], 'u'), + ] + ) + ] + ); + + $this->assertEquals('shmuel', 'fogel'); + + } + public function testEmptyTenant(): void { if(static::getDatabase()->getAdapter()->getSharedTables()) { diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index 9666ebf3a..c11fe57cf 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -9,29 +9,27 @@ class QueryTest extends TestCase { - public function setUp(): void - { - } + public function setUp(): void {} - public function tearDown(): void - { - } + public function tearDown(): void {} public function testCreate(): void { - $query = new Query(Query::TYPE_EQUAL, 'title', ['Iron Man']); + $query = Query::equal('title', ['Iron Man'], 'users'); $this->assertEquals(Query::TYPE_EQUAL, $query->getMethod()); $this->assertEquals('title', $query->getAttribute()); $this->assertEquals('Iron Man', $query->getValues()[0]); + $this->assertEquals('users', $query->getAlias()); - $query = new Query(Query::TYPE_ORDER_DESC, 'score'); + $query = Query::orderDesc('score', 'users'); $this->assertEquals(Query::TYPE_ORDER_DESC, $query->getMethod()); $this->assertEquals('score', $query->getAttribute()); $this->assertEquals([], $query->getValues()); + $this->assertEquals('users', $query->getAlias()); - $query = new Query(Query::TYPE_LIMIT, values: [10]); + $query = Query::limit(10); $this->assertEquals(Query::TYPE_LIMIT, $query->getMethod()); $this->assertEquals('', $query->getAttribute()); @@ -67,7 +65,7 @@ public function testCreate(): void $this->assertEquals('', $query->getAttribute()); $this->assertEquals([10], $query->getValues()); - $cursor = new Document(); + $cursor = new Document; $query = Query::cursorAfter($cursor); $this->assertEquals(Query::TYPE_CURSOR_AFTER, $query->getMethod()); @@ -88,7 +86,6 @@ public function testCreate(): void } /** - * @return void * @throws QueryException */ public function testParse(): void @@ -200,7 +197,7 @@ public function testParse(): void $json = Query::or([ Query::equal('actors', ['Brad Pitt']), - Query::equal('actors', ['Johnny Depp']) + Query::equal('actors', ['Johnny Depp']), ])->toString(); $query = Query::parse($json); @@ -275,4 +272,46 @@ public function testIsMethod(): void $this->assertFalse(Query::isMethod('invalid')); $this->assertFalse(Query::isMethod('lte ')); } + + /** + * @throws QueryException + */ + public function testJoins(): void + { + $query = + Query::join( + 'users', + 'u', + [ + Query::relation('main', 'id', Query::TYPE_EQUAL, 'u', 'user_id'), + Query::equal('id', ['usa'], 'u'), + ] + ); + + $this->assertEquals(Query::TYPE_JOIN, $query->getMethod()); + $this->assertEquals(Query::TYPE_INNER_JOIN, $query->getType()); + $this->assertEquals('users', $query->getCollection()); + $this->assertEquals('u', $query->getAlias()); + $this->assertCount(2, $query->getValues()); + + /** + * @var $query0 Query + */ + $query0 = $query->getValues()[0]; + $this->assertEquals(Query::TYPE_RELATION, $query0->getMethod()); + $this->assertEquals('main', $query0->getAlias()); + $this->assertEquals('id', $query0->getAttribute()); + $this->assertEquals('u', $query0->getRightAlias()); + $this->assertEquals('user_id', $query0->getAttributeRight()); + + /** + * @var $query0 Query + */ + $query1 = $query->getValues()[1]; + $this->assertEquals(Query::TYPE_EQUAL, $query1->getMethod()); + $this->assertEquals('u', $query1->getAlias()); + $this->assertEquals('id', $query1->getAttribute()); + $this->assertEquals('', $query1->getRightAlias()); + $this->assertEquals('', $query1->getAttributeRight()); + } }