diff --git a/composer.lock b/composer.lock index 3abe843d8..dff29f735 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "brick/math", - "version": "0.12.3", + "version": "0.13.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", "shasum": "" }, "require": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.3" + "source": "https://github.com/brick/math/tree/0.13.1" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2025-02-28T13:11:00+00:00" + "time": "2025-03-29T13:50:30+00:00" }, { "name": "composer/semver", @@ -149,16 +149,16 @@ }, { "name": "google/protobuf", - "version": "v4.30.2", + "version": "v4.31.1", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "a4c4d8565b40b9f76debc9dfeb221412eacb8ced" + "reference": "2b028ce8876254e2acbeceea7d9b573faad41864" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/a4c4d8565b40b9f76debc9dfeb221412eacb8ced", - "reference": "a4c4d8565b40b9f76debc9dfeb221412eacb8ced", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/2b028ce8876254e2acbeceea7d9b573faad41864", + "reference": "2b028ce8876254e2acbeceea7d9b573faad41864", "shasum": "" }, "require": { @@ -187,9 +187,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.30.2" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.31.1" }, - "time": "2025-03-26T18:01:50+00:00" + "time": "2025-05-28T18:52:35+00:00" }, { "name": "nyholm/psr7", @@ -337,16 +337,16 @@ }, { "name": "open-telemetry/api", - "version": "1.2.3", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "199d7ddda88f5f5619fa73463f1a5a7149ccd1f1" + "reference": "b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/199d7ddda88f5f5619fa73463f1a5a7149ccd1f1", - "reference": "199d7ddda88f5f5619fa73463f1a5a7149ccd1f1", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7", + "reference": "b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7", "shasum": "" }, "require": { @@ -366,7 +366,7 @@ ] }, "branch-alias": { - "dev-main": "1.1.x-dev" + "dev-main": "1.4.x-dev" } }, "autoload": { @@ -403,20 +403,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-03-05T21:42:54+00:00" + "time": "2025-06-19T23:36:51+00:00" }, { "name": "open-telemetry/context", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/context.git", - "reference": "5f553042b951d3fedf47925852c380159dfca801" + "reference": "1eb2b837ee9362db064a6b65d5ecce15a9f9f020" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/5f553042b951d3fedf47925852c380159dfca801", - "reference": "5f553042b951d3fedf47925852c380159dfca801", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/1eb2b837ee9362db064a6b65d5ecce15a9f9f020", + "reference": "1eb2b837ee9362db064a6b65d5ecce15a9f9f020", "shasum": "" }, "require": { @@ -462,20 +462,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-05-02T01:57:57+00:00" + "time": "2025-05-07T23:36:50+00:00" }, { "name": "open-telemetry/exporter-otlp", - "version": "1.2.1", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "b7580440b7481a98da97aceabeb46e1b276c8747" + "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/b7580440b7481a98da97aceabeb46e1b276c8747", - "reference": "b7580440b7481a98da97aceabeb46e1b276c8747", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/196f3a1dbce3b2c0f8110d164232c11ac00ddbb2", + "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2", "shasum": "" }, "require": { @@ -526,7 +526,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-03-06T23:21:56+00:00" + "time": "2025-06-16T00:24:51+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", @@ -593,22 +593,22 @@ }, { "name": "open-telemetry/sdk", - "version": "1.3.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "05d9ceb6773b5bddcf485af6d4a6f543bbeb980b" + "reference": "1c0371794e4c0700afd4a9d4d8511cb5e3f78e6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/05d9ceb6773b5bddcf485af6d4a6f543bbeb980b", - "reference": "05d9ceb6773b5bddcf485af6d4a6f543bbeb980b", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/1c0371794e4c0700afd4a9d4d8511cb5e3f78e6a", + "reference": "1c0371794e4c0700afd4a9d4d8511cb5e3f78e6a", "shasum": "" }, "require": { "ext-json": "*", "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "~1.0 || ~1.1", + "open-telemetry/api": "~1.4.0", "open-telemetry/context": "^1.0", "open-telemetry/sem-conv": "^1.0", "php": "^8.1", @@ -631,6 +631,10 @@ "type": "library", "extra": { "spi": { + "OpenTelemetry\\API\\Configuration\\ConfigEnv\\EnvComponentLoader": [ + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderHttpConfig", + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderPeerConfig" + ], "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" ] @@ -679,20 +683,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-05-01T23:20:43+00:00" + "time": "2025-06-19T23:36:51+00:00" }, { "name": "open-telemetry/sem-conv", - "version": "1.32.0", + "version": "1.32.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sem-conv.git", - "reference": "16585cc0dbc3032a318e274043454679430d2ebf" + "reference": "94daa85ea61a8e2b7e1b0af6be0e875bedda7c22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/16585cc0dbc3032a318e274043454679430d2ebf", - "reference": "16585cc0dbc3032a318e274043454679430d2ebf", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/94daa85ea61a8e2b7e1b0af6be0e875bedda7c22", + "reference": "94daa85ea61a8e2b7e1b0af6be0e875bedda7c22", "shasum": "" }, "require": { @@ -736,7 +740,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-05-05T03:58:53+00:00" + "time": "2025-06-24T02:32:27+00:00" }, { "name": "php-http/discovery", @@ -1158,21 +1162,20 @@ }, { "name": "ramsey/uuid", - "version": "4.7.6", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", - "ext-json": "*", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -1180,26 +1183,23 @@ "rhumsaa/uuid": "self.version" }, "require-dev": { - "captainhook/captainhook": "^5.10", + "captainhook/captainhook": "^5.25", "captainhook/plugin-composer": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "doctrine/annotations": "^1.8", - "ergebnis/composer-normalize": "^2.15", - "mockery/mockery": "^1.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", "paragonie/random-lib": "^2", - "php-mock/php-mock": "^2.2", - "php-mock/php-mock-mockery": "^1.3", - "php-parallel-lint/php-parallel-lint": "^1.1", - "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9", - "ramsey/composer-repl": "^1.4", - "slevomat/coding-standard": "^8.4", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.9" + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" }, "suggest": { "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", @@ -1234,32 +1234,22 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.6" + "source": "https://github.com/ramsey/uuid/tree/4.9.0" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" - } - ], - "time": "2024-04-27T21:32:50+00:00" + "time": "2025-06-25T14:20:11+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -1272,7 +1262,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1297,7 +1287,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -1313,20 +1303,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/http-client", - "version": "v7.2.4", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" + "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "url": "https://api.github.com/repos/symfony/http-client/zipball/4403d87a2c16f33345dca93407a8714ee8c05a64", + "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64", "shasum": "" }, "require": { @@ -1338,6 +1328,7 @@ }, "conflict": { "amphp/amp": "<2.5", + "amphp/socket": "<1.1", "php-http/discovery": "<1.15", "symfony/http-foundation": "<6.4" }, @@ -1350,7 +1341,6 @@ "require-dev": { "amphp/http-client": "^4.2.1|^5.0", "amphp/http-tunnel": "^1.0|^2.0", - "amphp/socket": "^1.1", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", @@ -1392,7 +1382,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.2.4" + "source": "https://github.com/symfony/http-client/tree/v7.3.1" }, "funding": [ { @@ -1408,20 +1398,20 @@ "type": "tidelift" } ], - "time": "2025-02-13T10:27:23+00:00" + "time": "2025-06-28T07:58:39+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.2", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + "reference": "75d7043853a42837e68111812f4d964b01e5101c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", "shasum": "" }, "require": { @@ -1434,7 +1424,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1470,7 +1460,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" }, "funding": [ { @@ -1486,7 +1476,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:49:48+00:00" + "time": "2025-04-29T11:18:49+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -1647,16 +1637,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -1674,7 +1664,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1710,7 +1700,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -1726,20 +1716,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "tbachert/spi", - "version": "v1.0.3", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/Nevay/spi.git", - "reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a" + "reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nevay/spi/zipball/506a79c98e1a51522e76ee921ccb6c62d52faf3a", - "reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a", + "url": "https://api.github.com/repos/Nevay/spi/zipball/e7078767866d0a9e0f91d3f9d42a832df5e39002", + "reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002", "shasum": "" }, "require": { @@ -1757,7 +1747,7 @@ "extra": { "class": "Nevay\\SPI\\Composer\\Plugin", "branch-alias": { - "dev-main": "0.2.x-dev" + "dev-main": "1.0.x-dev" }, "plugin-optional": true }, @@ -1776,22 +1766,22 @@ ], "support": { "issues": "https://github.com/Nevay/spi/issues", - "source": "https://github.com/Nevay/spi/tree/v1.0.3" + "source": "https://github.com/Nevay/spi/tree/v1.0.5" }, - "time": "2025-04-02T19:38:14+00:00" + "time": "2025-06-29T15:42:06+00:00" }, { "name": "utopia-php/cache", - "version": "0.13.0", + "version": "0.13.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "dee01dec33a211644d60f6cfa56b1b8176d3fae3" + "reference": "97220cb3b3822b166ee016d1646e2ae2815dc540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/dee01dec33a211644d60f6cfa56b1b8176d3fae3", - "reference": "dee01dec33a211644d60f6cfa56b1b8176d3fae3", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/97220cb3b3822b166ee016d1646e2ae2815dc540", + "reference": "97220cb3b3822b166ee016d1646e2ae2815dc540", "shasum": "" }, "require": { @@ -1828,9 +1818,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.13.0" + "source": "https://github.com/utopia-php/cache/tree/0.13.1" }, - "time": "2025-04-17T04:20:26+00:00" + "time": "2025-05-09T14:43:52+00:00" }, { "name": "utopia-php/compression", @@ -1880,16 +1870,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.19", + "version": "0.33.20", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0" + "reference": "e1c7ab4e0b5b0a9a70256b1e00912e101e76a131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0", - "reference": "64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0", + "url": "https://api.github.com/repos/utopia-php/http/zipball/e1c7ab4e0b5b0a9a70256b1e00912e101e76a131", + "reference": "e1c7ab4e0b5b0a9a70256b1e00912e101e76a131", "shasum": "" }, "require": { @@ -1921,9 +1911,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.19" + "source": "https://github.com/utopia-php/http/tree/0.33.20" }, - "time": "2025-03-06T11:37:49+00:00" + "time": "2025-05-18T23:51:21+00:00" }, { "name": "utopia-php/pools", @@ -2164,16 +2154,16 @@ }, { "name": "laravel/pint", - "version": "v1.22.0", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36" + "reference": "9ab851dba4faa51a3c3223dd3d07044129021024" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36", - "reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36", + "url": "https://api.github.com/repos/laravel/pint/zipball/9ab851dba4faa51a3c3223dd3d07044129021024", + "reference": "9ab851dba4faa51a3c3223dd3d07044129021024", "shasum": "" }, "require": { @@ -2184,12 +2174,12 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.75.0", - "illuminate/view": "^11.44.2", - "larastan/larastan": "^3.3.1", - "laravel-zero/framework": "^11.36.1", + "friendsofphp/php-cs-fixer": "^3.76.0", + "illuminate/view": "^11.45.1", + "larastan/larastan": "^3.5.0", + "laravel-zero/framework": "^11.45.0", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3", + "nunomaduro/termwind": "^2.3.1", "pestphp/pest": "^2.36.0" }, "bin": [ @@ -2197,6 +2187,9 @@ ], "type": "project", "autoload": { + "files": [ + "overrides/Runner/Parallel/ProcessFactory.php" + ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -2226,20 +2219,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-04-08T22:11:45+00:00" + "time": "2025-07-03T10:37:47+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.13.1", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", "shasum": "" }, "require": { @@ -2278,7 +2271,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" }, "funding": [ { @@ -2286,7 +2279,7 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-07-05T12:25:42+00:00" }, { "name": "nikic/php-parser", @@ -2498,16 +2491,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.25", + "version": "1.12.27", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e310849a19e02b8bfcbb63147f495d8f872dd96f" + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e310849a19e02b8bfcbb63147f495d8f872dd96f", - "reference": "e310849a19e02b8bfcbb63147f495d8f872dd96f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3a6e423c076ab39dfedc307e2ac627ef579db162", + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162", "shasum": "" }, "require": { @@ -2552,7 +2545,7 @@ "type": "github" } ], - "time": "2025-04-27T12:20:45+00:00" + "time": "2025-05-21T20:51:45+00:00" }, { "name": "phpunit/php-code-coverage", 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/Adapter.php b/src/Database/Adapter.php index 9b25dae55..ffe04b577 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -747,19 +747,33 @@ abstract public function deleteDocuments(string $collection, array $internalIds, * * Find data sets using chosen queries * - * @param string $collection + * @param QueryContext $context * @param array $queries * @param int|null $limit * @param int|null $offset - * @param array $orderAttributes - * @param array $orderTypes * @param array $cursor * @param string $cursorDirection * @param string $forPermission + * @param array $selects + * @param array $filters + * @param array $joins + * @param array $orderQueries * * @return array */ - abstract public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array; + abstract public function find( + QueryContext $context, + array $queries = [], + ?int $limit = 25, + ?int $offset = null, + array $cursor = [], + string $cursorDirection = Database::CURSOR_AFTER, + string $forPermission = Database::PERMISSION_READ, + array $selects = [], + array $filters = [], + array $joins = [], + array $orderQueries = [] + ): array; /** * Sum an attribute @@ -1045,13 +1059,10 @@ abstract public function getAttributeWidth(Document $collection): int; abstract public function getKeywords(): array; /** - * Get an attribute projection given a list of selected attributes - * - * @param array $selections - * @param string $prefix - * @return mixed + * @param array $selects + * @return string */ - abstract protected function getAttributeProjection(array $selections, string $prefix = ''): mixed; + abstract protected function getAttributeProjection(array $selects): string; /** * Get all selected attributes from queries diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 9b3d0c126..3327f9422 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -11,11 +11,11 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\NotFound as NotFoundException; -use Utopia\Database\Exception\Order as OrderException; use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; +use Utopia\Database\QueryContext; use Utopia\Database\Validator\Authorization; class MariaDB extends SQL @@ -60,7 +60,7 @@ public function delete(string $name): bool $sql = "DROP DATABASE `{$name}`;"; $sql = $this->trigger(Database::EVENT_DATABASE_DELETE, $sql); - + var_dump($sql); return $this->getPDO() ->prepare($sql) ->execute(); @@ -1011,7 +1011,7 @@ public function createDocuments(string $collection, array $documents): array $columns = []; foreach ($attributeKeys as $key => $attribute) { - $columns[$key] = "`{$this->filter($attribute)}`"; + $columns[$key] = "{$this->quote($this->filter($attribute))}"; } $columns = '(' . \implode(', ', $columns) . ')'; @@ -1673,121 +1673,144 @@ public function deleteDocument(string $collection, string $id): bool /** * Find Documents * - * @param string $collection + * @param QueryContext $context * @param array $queries * @param int|null $limit * @param int|null $offset - * @param array $orderAttributes - * @param array $orderTypes * @param array $cursor * @param string $cursorDirection * @param string $forPermission + * @param array $selects + * @param array $filters + * @param array $joins + * @param array $orderQueries * @return array * @throws DatabaseException * @throws TimeoutException * @throws Exception */ - public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array - { - $name = $this->filter($collection); + public function find( + QueryContext $context, + array $queries = [], + ?int $limit = 25, + ?int $offset = null, + array $cursor = [], + string $cursorDirection = Database::CURSOR_AFTER, + string $forPermission = Database::PERMISSION_READ, + array $selects = [], + array $filters = [], + array $joins = [], + array $orderQueries = [] + ): array { + unset($queries); // remove this since we pass explicit queries + + $alias = Query::DEFAULT_ALIAS; + $binds = []; + + $name = $context->getCollections()[0]->getId(); + $name = $this->filter($name); + $roles = Authorization::getRoles(); $where = []; $orders = []; - $alias = Query::DEFAULT_ALIAS; - $binds = []; - $queries = array_map(fn ($query) => clone $query, $queries); + $filters = array_map(fn ($query) => clone $query, $filters); - $hasIdAttribute = false; - foreach ($orderAttributes as $i => $attribute) { - $originalAttribute = $attribute; + $cursorWhere = []; - $attribute = $this->getInternalKeyForAttribute($attribute); + foreach ($orderQueries as $i => $order) { + $orderAlias = $order->getAlias(); + $attribute = $order->getAttribute(); + $originalAttribute = $attribute; + $attribute = $this->getInternalKeyForAttribute($originalAttribute); $attribute = $this->filter($attribute); - if (\in_array($attribute, ['_uid', '_id'])) { - $hasIdAttribute = true; + + $direction = $order->getOrderDirection(); + + if ($cursorDirection === Database::CURSOR_BEFORE) { + $direction = ($direction === Database::ORDER_ASC) ? Database::ORDER_DESC : Database::ORDER_ASC; } - $orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC); + $orders[] = "{$this->quote($attribute)} {$direction}"; - // Get most dominant/first order attribute - if ($i === 0 && !empty($cursor)) { - $orderMethodInternalId = Query::TYPE_GREATER; // To preserve natural order - $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; + // Build pagination WHERE clause only if we have a cursor + if (!empty($cursor)) { + // Special case: No tie breaks. only 1 attribute and it's a unique primary key + if (count($orderQueries) === 1 && $i === 0 && $originalAttribute === '$sequence') { + $operator = ($direction === Database::ORDER_DESC) + ? Query::TYPE_LESSER + : Query::TYPE_GREATER; - if ($cursorDirection === Database::CURSOR_BEFORE) { - $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - $orderMethodInternalId = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER; - $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; - } + $bindName = ":cursor_pk"; + $binds[$bindName] = $cursor[$originalAttribute]; - if (\is_null($cursor[$originalAttribute] ?? null)) { - throw new OrderException( - message: "Order attribute '{$originalAttribute}' is empty", - attribute: $originalAttribute - ); + $cursorWhere[] = "{$this->quote($orderAlias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}"; + break; } - $binds[':cursor'] = $cursor[$originalAttribute]; - - $where[] = "( - {$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($orderMethod)} :cursor - OR ( - {$this->quote($alias)}.{$this->quote($attribute)} = :cursor - AND - {$this->quote($alias)}._id {$this->getSQLOperator($orderMethodInternalId)} {$cursor['$internalId']} - ) - )"; - } elseif ($cursorDirection === Database::CURSOR_BEFORE) { - $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - } + $conditions = []; - $orders[] = "{$this->quote($attribute)} {$orderType}"; - } + // Add equality conditions for previous attributes + for ($j = 0; $j < $i; $j++) { + $prevQuery = $orderQueries[$j]; + $prevOriginal = $prevQuery->getAttribute(); + $prevAttr = $this->filter($this->getInternalKeyForAttribute($prevOriginal)); - // Allow after pagination without any order - if (empty($orderAttributes) && !empty($cursor)) { - $orderType = $orderTypes[0] ?? Database::ORDER_ASC; + $bindName = ":cursor_{$j}"; + $binds[$bindName] = $cursor[$prevOriginal]; - if ($cursorDirection === Database::CURSOR_AFTER) { - $orderMethod = $orderType === Database::ORDER_DESC + $conditions[] = "{$this->quote($orderAlias)}.{$this->quote($prevAttr)} = {$bindName}"; + } + + // Add comparison for current attribute + $operator = ($direction === Database::ORDER_DESC) ? Query::TYPE_LESSER : Query::TYPE_GREATER; - } else { - $orderMethod = $orderType === Database::ORDER_DESC - ? Query::TYPE_GREATER - : Query::TYPE_LESSER; + + $bindName = ":cursor_{$i}"; + $binds[$bindName] = $cursor[$originalAttribute]; + + $conditions[] = "{$this->quote($orderAlias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}"; + + $cursorWhere[] = '(' . implode(' AND ', $conditions) . ')'; } + } - $where[] = "({$this->quote($alias)}._id {$this->getSQLOperator($orderMethod)} {$cursor['$internalId']})"; + if (!empty($cursorWhere)) { + $where[] = '(' . implode(' OR ', $cursorWhere) . ')'; } - // Allow order type without any order attribute, fallback to the natural order (_id) - if (!$hasIdAttribute) { - if (empty($orderAttributes) && !empty($orderTypes)) { - $order = $orderTypes[0] ?? Database::ORDER_ASC; - if ($cursorDirection === Database::CURSOR_BEFORE) { - $order = $order === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - } + $sqlJoin = ''; + foreach ($joins as $join) { + $permissions = ''; + $collection = $join->getCollection(); + $collection = $this->filter($collection); - $orders[] = "{$this->quote($alias)}._id ".$this->filter($order); - } else { - $orders[] = "{$this->quote($alias)}._id " . ($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC); // Enforce last ORDER by '_id' + $skipAuth = $context->skipAuth($collection, $forPermission); + if (! $skipAuth) { + $permissions = 'AND '.$this->getSQLPermissionsCondition($collection, $roles, $join->getAlias(), $forPermission); } + + $sqlJoin .= "INNER JOIN {$this->getSQLTable($collection)} AS {$this->quote($join->getAlias())} + ON {$this->getSQLConditions($join->getValues(), $binds)} + {$permissions} + {$this->getTenantQuery($collection, $join->getAlias())} + "; } - $conditions = $this->getSQLConditions($queries, $binds); + $conditions = $this->getSQLConditions($filters, $binds); if (!empty($conditions)) { $where[] = $conditions; } - if (Authorization::$status) { + $skipAuth = $context->skipAuth($name, $forPermission); + if (! $skipAuth) { $where[] = $this->getSQLPermissionsCondition($name, $roles, $alias, $forPermission); } if ($this->sharedTables) { $binds[':_tenant'] = $this->tenant; - $where[] = "{$this->getTenantQuery($collection, $alias, condition: '')}"; + $where[] = "{$this->getTenantQuery($name, $alias, condition: '')}"; } $sqlWhere = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; @@ -1804,11 +1827,10 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $sqlLimit .= ' OFFSET :offset'; } - $selections = $this->getAttributeSelections($queries); - $sql = " - SELECT {$this->getAttributeProjection($selections, $alias)} + SELECT {$this->getAttributeProjection($selects)} FROM {$this->getSQLTable($name)} AS {$this->quote($alias)} + {$sqlJoin} {$sqlWhere} {$sqlOrder} {$sqlLimit}; @@ -1823,14 +1845,16 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $stmt->bindValue($key, $value, $this->getPDOType($value)); } + echo $stmt->queryString; + var_dump($binds); $stmt->execute(); + $results = $stmt->fetchAll(); + $stmt->closeCursor(); + } catch (PDOException $e) { throw $this->processException($e); } - $results = $stmt->fetchAll(); - $stmt->closeCursor(); - foreach ($results as $index => $document) { if (\array_key_exists('_uid', $document)) { $results[$index]['$id'] = $document['_uid']; @@ -1956,6 +1980,7 @@ public function sum(string $collection, string $attribute, array $queries = [], $roles = Authorization::getRoles(); $where = []; $alias = Query::DEFAULT_ALIAS; + $alias = Query::DEFAULT_ALIAS; $binds = []; $limit = ''; @@ -2023,11 +2048,14 @@ public function sum(string $collection, string $attribute, array $queries = [], protected function getSQLCondition(Query $query, array &$binds): string { $query->setAttribute($this->getInternalKeyForAttribute($query->getAttribute())); + $query->setAttributeRight($this->getInternalKeyForAttribute($query->getAttributeRight())); $attribute = $query->getAttribute(); $attribute = $this->filter($attribute); $attribute = $this->quote($attribute); - $alias = $this->quote(Query::DEFAULT_ALIAS); + $alias = $query->getAlias(); + $alias = $this->filter($alias); + $alias = $this->quote($alias); $placeholder = ID::unique(); switch ($query->getMethod()) { @@ -2054,6 +2082,12 @@ protected function getSQLCondition(Query $query, array &$binds): string return "{$alias}.{$attribute} BETWEEN :{$placeholder}_0 AND :{$placeholder}_1"; + case Query::TYPE_RELATION_EQUAL: + $attributeRight = $this->quote($this->filter($query->getAttributeRight())); + $aliasRight = $this->quote($query->getRightAlias()); + + return "{$alias}.{$attribute}={$aliasRight}.{$attributeRight}"; + case Query::TYPE_IS_NULL: case Query::TYPE_IS_NOT_NULL: diff --git a/src/Database/Adapter/Pool.php b/src/Database/Adapter/Pool.php index 86521df0a..3426fa14e 100644 --- a/src/Database/Adapter/Pool.php +++ b/src/Database/Adapter/Pool.php @@ -6,6 +6,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; +use Utopia\Database\QueryContext; use Utopia\Pools\Pool as UtopiaPool; class Pool extends Adapter @@ -255,8 +256,19 @@ public function deleteDocuments(string $collection, array $internalIds, array $p return $this->delegate(__FUNCTION__, \func_get_args()); } - public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array - { + public function find( + QueryContext $context, + array $queries = [], + ?int $limit = 25, + ?int $offset = null, + array $cursor = [], + string $cursorDirection = Database::CURSOR_AFTER, + string $forPermission = Database::PERMISSION_READ, + array $selects = [], + array $filters = [], + array $joins = [], + array $orderQueries = [] + ): array { return $this->delegate(__FUNCTION__, \func_get_args()); } @@ -445,7 +457,7 @@ public function getKeywords(): array return $this->delegate(__FUNCTION__, \func_get_args()); } - protected function getAttributeProjection(array $selections, string $prefix = ''): mixed + protected function getAttributeProjection(array $selects): string { return $this->delegate(__FUNCTION__, \func_get_args()); } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 5d94c4ad9..7f960647d 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -16,6 +16,7 @@ use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; +use Utopia\Database\QueryContext; use Utopia\Database\Validator\Authorization; class Postgres extends SQL @@ -1032,6 +1033,7 @@ public function createDocument(string $collection, Document $document): Document * @return array * * @throws DuplicateException + * @throws \Throwable */ public function createDocuments(string $collection, array $documents): array { @@ -1041,12 +1043,13 @@ public function createDocuments(string $collection, array $documents): array try { $name = $this->filter($collection); + $attributeKeys = Database::INTERNAL_ATTRIBUTE_KEYS; $hasInternalId = null; foreach ($documents as $document) { $attributes = $document->getAttributes(); - $attributeKeys = array_merge($attributeKeys, array_keys($attributes)); + $attributeKeys = [...$attributeKeys, ...\array_keys($attributes)]; if ($hasInternalId === null) { $hasInternalId = !empty($document->getInternalId()); @@ -1062,16 +1065,16 @@ public function createDocuments(string $collection, array $documents): array $columns = []; foreach ($attributeKeys as $key => $attribute) { - $columns[$key] = "\"{$this->filter($attribute)}\""; + $columns[$key] = "{$this->quote($this->filter($attribute))}"; } $columns = '(' . \implode(', ', $columns) . ')'; - $internalIds = []; - $bindIndex = 0; $batchKeys = []; $bindValues = []; $permissions = []; + $documentIds = []; + $documentTenants = []; foreach ($documents as $index => $document) { $attributes = $document->getAttributes(); @@ -1081,13 +1084,15 @@ public function createDocuments(string $collection, array $documents): array $attributes['_permissions'] = \json_encode($document->getPermissions()); if (!empty($document->getInternalId())) { - $internalIds[$document->getId()] = true; $attributes['_id'] = $document->getInternalId(); $attributeKeys[] = '_id'; + } else { + $documentIds[] = $document->getId(); } if ($this->sharedTables) { $attributes['_tenant'] = $document->getTenant(); + $documentTenants[] = $document->getTenant(); } $bindKeys = []; @@ -1148,18 +1153,20 @@ public function createDocuments(string $collection, array $documents): array $stmtPermissions?->execute(); } - } catch (PDOException $e) { - throw $this->processException($e); - } - foreach ($documents as $document) { - if (!isset($internalIds[$document->getId()])) { - $document['$internalId'] = $this->getDocument( - $collection, - $document->getId(), - [Query::select(['$internalId'])] - )->getInternalId(); + $internalIds = $this->getInternalIds( + $collection, + $documentIds, + $documentTenants + ); + + foreach ($documents as $document) { + if (isset($internalIds[$document->getId()])) { + $document['$internalId'] = $internalIds[$document->getId()]; + } } + } catch (PDOException $e) { + throw $this->processException($e); } return $documents; @@ -1497,42 +1504,63 @@ public function deleteDocument(string $collection, string $id): bool /** * Find Documents * - * @param string $collection + * @param QueryContext $context * @param array $queries * @param int|null $limit * @param int|null $offset - * @param array $orderAttributes - * @param array $orderTypes * @param array $cursor * @param string $cursorDirection * @param string $forPermission + * @param array $selects + * @param array $filters + * @param array $joins + * @param array $orderQueries * @return array * @throws DatabaseException * @throws TimeoutException * @throws Exception */ - public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array - { - $name = $this->filter($collection); - $roles = Authorization::getRoles(); - $where = []; - $orders = []; + public function find( + QueryContext $context, + array $queries = [], + ?int $limit = 25, + ?int $offset = null, + array $cursor = [], + string $cursorDirection = Database::CURSOR_AFTER, + string $forPermission = Database::PERMISSION_READ, + array $selects = [], + array $filters = [], + array $joins = [], + array $orderQueries = [] + ): array { + unset($queries); + $alias = Query::DEFAULT_ALIAS; $binds = []; - $queries = array_map(fn ($query) => clone $query, $queries); + $name = $context->getCollections()[0]->getId(); + $name = $this->filter($name); + $roles = Authorization::getRoles(); + $where = []; + $orders = []; $hasIdAttribute = false; - foreach ($orderAttributes as $i => $attribute) { - $originalAttribute = $attribute; + //$queries = array_map(fn ($query) => clone $query, $queries); + $filters = array_map(fn ($query) => clone $query, $filters); + //$filters = Query::getFilterQueries($filters); // for cloning if needed + + foreach ($orderQueries as $i => $order) { + $orderAlias = $order->getAlias(); + $attribute = $order->getAttribute(); + $originalAttribute = $attribute; $attribute = $this->getInternalKeyForAttribute($attribute); $attribute = $this->filter($attribute); - if (\in_array($attribute, ['_uid', '_id'])) { + if ($attribute === '_uid' || $attribute === '_id') { $hasIdAttribute = true; } - $orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC); + $orderType = $order->getOrderDirection(); // Get most dominant/first order attribute if ($i === 0 && !empty($cursor)) { @@ -1566,52 +1594,64 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; } - $orders[] = "{$this->quote($attribute)} {$orderType}"; + $orders[] = "{$this->quote($orderAlias)}.{$this->quote($attribute)} {$orderType}"; } // Allow after pagination without any order - if (empty($orderAttributes) && !empty($cursor)) { - $orderType = $orderTypes[0] ?? Database::ORDER_ASC; - + if (empty($orderQueries) && !empty($cursor)) { if ($cursorDirection === Database::CURSOR_AFTER) { - $orderMethod = $orderType === Database::ORDER_DESC - ? Query::TYPE_LESSER - : Query::TYPE_GREATER; + $orderMethod = Query::TYPE_GREATER; } else { - $orderMethod = $orderType === Database::ORDER_DESC - ? Query::TYPE_GREATER - : Query::TYPE_LESSER; + $orderMethod = Query::TYPE_LESSER; } - $where[] = "({$this->quote($alias)}._id {$this->getSQLOperator($orderMethod)} {$cursor['$internalId']})"; + $where[] = "({$this->quote($alias)}.{$this->quote('_id')} {$this->getSQLOperator($orderMethod)} {$cursor['$internalId']})"; } // Allow order type without any order attribute, fallback to the natural order (_id) + // Because if we have 2 movies with same year 2000 order by year, _id for pagination + if (!$hasIdAttribute) { - if (empty($orderAttributes) && !empty($orderTypes)) { - $order = $orderTypes[0] ?? Database::ORDER_ASC; - if ($cursorDirection === Database::CURSOR_BEFORE) { - $order = $order === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - } + $order = Database::ORDER_ASC; - $orders[] = "{$this->quote($alias)}._id ".$this->filter($order); - } else { - $orders[] = "{$this->quote($alias)}._id " . ($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC); // Enforce last ORDER by '_id' + if ($cursorDirection === Database::CURSOR_BEFORE) { + $order = Database::ORDER_DESC; } + + $orders[] = "{$this->quote($alias)}.{$this->quote('_id')} ".$order; } - $conditions = $this->getSQLConditions($queries, $binds); + $sqlJoin = ''; + foreach ($joins as $join) { + $permissions = ''; + $collection = $join->getCollection(); + $collection = $this->filter($collection); + + $skipAuth = $context->skipAuth($collection, $forPermission); + if (! $skipAuth) { + $permissions = 'AND '.$this->getSQLPermissionsCondition($collection, $roles, $join->getAlias(), $forPermission); + } + + $sqlJoin .= "INNER JOIN {$this->getSQLTable($collection)} AS {$this->quote($join->getAlias())} + ON {$this->getSQLConditions($join->getValues(), $binds)} + {$permissions} + {$this->getTenantQuery($collection, $join->getAlias())} + "; + } + + $conditions = $this->getSQLConditions($filters, $binds); if (!empty($conditions)) { $where[] = $conditions; } - if (Authorization::$status) { + $skipAuth = $context->skipAuth($name, $forPermission); + if (! $skipAuth) { $where[] = $this->getSQLPermissionsCondition($name, $roles, $alias, $forPermission); } if ($this->sharedTables) { $binds[':_tenant'] = $this->tenant; - $where[] = "{$this->getTenantQuery($collection, $alias, condition: '')}"; + $where[] = "{$this->getTenantQuery($name, $alias, condition: '')}"; } $sqlWhere = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; @@ -1628,11 +1668,12 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $sqlLimit .= ' OFFSET :offset'; } - $selections = $this->getAttributeSelections($queries); + //$selections = $this->getAttributeSelections($selects); $sql = " - SELECT {$this->getAttributeProjection($selections, $alias)} + SELECT {$this->getAttributeProjection($selects)} FROM {$this->getSQLTable($name)} AS {$this->quote($alias)} + {$sqlJoin} {$sqlWhere} {$sqlOrder} {$sqlLimit}; @@ -1647,14 +1688,16 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $stmt->bindValue($key, $value, $this->getPDOType($value)); } + echo $stmt->queryString; + var_dump($binds); $stmt->execute(); + $results = $stmt->fetchAll(); + $stmt->closeCursor(); + } catch (PDOException $e) { throw $this->processException($e); } - $results = $stmt->fetchAll(); - $stmt->closeCursor(); - foreach ($results as $index => $document) { if (\array_key_exists('_uid', $document)) { $results[$index]['$id'] = $document['_uid']; @@ -1744,7 +1787,6 @@ public function count(string $collection, array $queries = [], ?int $max = null) ) table_count "; - $sql = $this->trigger(Database::EVENT_DOCUMENT_COUNT, $sql); $stmt = $this->getPDO()->prepare($sql); @@ -1781,6 +1823,7 @@ public function sum(string $collection, string $attribute, array $queries = [], $roles = Authorization::getRoles(); $where = []; $alias = Query::DEFAULT_ALIAS; + $alias = Query::DEFAULT_ALIAS; $binds = []; $limit = ''; @@ -1857,10 +1900,13 @@ public function getConnectionId(): string protected function getSQLCondition(Query $query, array &$binds): string { $query->setAttribute($this->getInternalKeyForAttribute($query->getAttribute())); + $query->setAttributeRight($this->getInternalKeyForAttribute($query->getAttributeRight())); $attribute = $this->filter($query->getAttribute()); $attribute = $this->quote($attribute); - $alias = $this->quote(Query::DEFAULT_ALIAS); + $alias = $query->getAlias(); + $alias = $this->filter($alias); + $alias = $this->quote($alias); $placeholder = ID::unique(); $operator = null; @@ -1885,6 +1931,12 @@ protected function getSQLCondition(Query $query, array &$binds): string $binds[":{$placeholder}_1"] = $query->getValues()[1]; return "{$alias}.{$attribute} BETWEEN :{$placeholder}_0 AND :{$placeholder}_1"; + case Query::TYPE_RELATION_EQUAL: + $attributeRight = $this->quote($this->filter($query->getAttributeRight())); + $aliasRight = $this->quote($query->getRightAlias()); + + return "{$alias}.{$attribute}={$aliasRight}.{$attributeRight}"; + case Query::TYPE_IS_NULL: case Query::TYPE_IS_NOT_NULL: return "{$alias}.{$attribute} {$this->getSQLOperator($query->getMethod())}"; @@ -1901,6 +1953,7 @@ protected function getSQLCondition(Query $query, array &$binds): string $value = match ($query->getMethod()) { Query::TYPE_STARTS_WITH => $this->escapeWildcards($value) . '%', Query::TYPE_ENDS_WITH => '%' . $this->escapeWildcards($value), + //Query::TYPE_SEARCH => $this->getFulltextValue($value), Query::TYPE_CONTAINS => $query->onArray() ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%', default => $value }; @@ -1996,16 +2049,16 @@ protected function getSQLSchema(): string return "\"{$this->getDatabase()}\"."; } - /** - * Get SQL table - * - * @param string $name - * @return string - */ - protected function getSQLTable(string $name): string - { - return "\"{$this->getDatabase()}\".\"{$this->getNamespace()}_{$name}\""; - } + // /** + // * Get SQL table + // * + // * @param string $name + // * @return string + // */ + // protected function getSQLTable(string $name): string + // { + // return "\"{$this->getDatabase()}\".\"{$this->getNamespace()}_{$name}\""; + // } /** * Get PDO Type diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index c2907b88d..c5f10871a 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -173,6 +173,7 @@ public function exists(string $database, ?string $collection = null): bool $stmt->execute(); $document = $stmt->fetchAll(); $stmt->closeCursor(); + var_dump($document); } catch (PDOException $e) { $e = $this->processException($e); @@ -213,21 +214,23 @@ public function list(): array public function getDocument(string $collection, string $id, array $queries = [], bool $forUpdate = false): Document { $name = $this->filter($collection); - $selections = $this->getAttributeSelections($queries); + $alias = Query::DEFAULT_ALIAS; + //$selections = $this->getAttributeSelections($queries); $forUpdate = $forUpdate ? 'FOR UPDATE' : ''; + //, _permissions as {$this->quote('$perms')} $sql = " - SELECT {$this->getAttributeProjection($selections)} - FROM {$this->getSQLTable($name)} - WHERE _uid = :_uid - {$this->getTenantQuery($collection)} + SELECT {$this->getAttributeProjection($queries)} + FROM {$this->getSQLTable($name)} AS {$this->quote($alias)} + WHERE {$this->quote($alias)}._uid = :_uid + {$this->getTenantQuery($collection, $alias)} "; if ($this->getSupportForUpdateLock()) { $sql .= " {$forUpdate}"; } - +var_dump($sql); $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $id); @@ -271,6 +274,8 @@ public function getDocument(string $collection, string $id, array $queries = [], unset($document['_permissions']); } + //$document['$perms'] = json_decode($document['$perms'], true); + return new Document($document); } @@ -1476,13 +1481,10 @@ abstract protected function getSQLCondition(Query $query, array &$binds): string */ public function getSQLConditions(array $queries, array &$binds, string $separator = 'AND'): string { + $queries = Query::getFilterQueries($queries); + $conditions = []; foreach ($queries as $query) { - - if ($query->getMethod() === Query::TYPE_SELECT) { - continue; - } - if ($query->isNested()) { $conditions[] = $this->getSQLConditions($query->getValues(), $binds, $query->getMethod()); } else { @@ -1546,6 +1548,97 @@ public function getTenantQuery( return "{$condition} ({$alias}{$dot}_tenant IN ({$bindings}) {$orIsNull})"; } + /** + * Get the SQL projection given the selected attributes + * + * @param array $selects + * @return string + * @throws Exception + */ + protected function addHiddenAttribute(array $selects): string + { + $hash = [Query::DEFAULT_ALIAS => true]; + + foreach ($selects as $select) { + $alias = $select->getAlias(); + if (!isset($hash[$alias])){ + $hash[$alias] = true; + } + } + + $hash = array_keys($hash); + + $strings = []; + + foreach ($hash as $alias) { + $strings[] = $alias.'._uid as '.$this->quote($alias.'::$id'); + $strings[] = $alias.'._id as '.$this->quote($alias.'::$internalId'); + $strings[] = $alias.'._permissions as '.$this->quote($alias.'::$permissions'); + $strings[] = $alias.'._createdAt as '.$this->quote($alias.'::$createdAt'); + $strings[] = $alias.'._updatedAt as '.$this->quote($alias.'::$updatedAt'); + + if ($this->sharedTables) { + $strings[] = $alias.'._tenant as '.$this->quote($alias.'::$tenant'); + } + } + + return ', '.implode(', ', $strings); + } + + /** + * Get the SQL projection given the selected attributes + * + * @param array $selects + * @return string + * @throws Exception + */ + protected function getAttributeProjection(array $selects): string + { + if (empty($selects)) { + return Query::DEFAULT_ALIAS.'.*'.$this->addHiddenAttribute($selects); + } + + $string = ''; + foreach ($selects as $select) { + if ($select->getAttribute() === '$collection') { + continue; + } + + $alias = $select->getAlias(); + $alias = $this->filter($alias); + $attribute = $select->getAttribute(); + + $attribute = match ($attribute) { + '$id' => '_uid', + '$internalId' => '_id', + '$tenant' => '_tenant', + '$createdAt' => '_createdAt', + '$updatedAt' => '_updatedAt', + '$permissions' => '_permissions', + default => $attribute + }; + + if ($attribute !== '*') { + $attribute = $this->filter($attribute); + $attribute = $this->quote($attribute); + } + + $as = $select->getAs(); + + if (!empty($as)){ + $as = ' as '.$this->quote($this->filter($as)); + } + + if (!empty($string)) { + $string .= ', '; + } + + $string .= "{$this->quote($alias)}.{$attribute}{$as}"; + } + + return $string.$this->addHiddenAttribute($selects); + } + /** * Get the SQL projection given the selected attributes * @@ -1554,7 +1647,7 @@ public function getTenantQuery( * @return mixed * @throws Exception */ - protected function getAttributeProjection(array $selections, string $prefix = ''): mixed + protected function getAttributeProjection_original(array $selections, string $prefix = ''): mixed { if (empty($selections) || \in_array('*', $selections)) { if (!empty($prefix)) { diff --git a/src/Database/Database.php b/src/Database/Database.php index 9480ceb5e..99bbeed0f 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -13,6 +13,7 @@ use Utopia\Database\Exception\Index as IndexException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; +use Utopia\Database\Exception\Order as OrderException; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Exception\Relationship as RelationshipException; use Utopia\Database\Exception\Restricted as RestrictedException; @@ -26,8 +27,7 @@ use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator; use Utopia\Database\Validator\PartialStructure; 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 @@ -3018,10 +3018,27 @@ public function getDocument(string $collection, string $id, array $queries = [], throw new NotFoundException('Collection not found'); } - $attributes = $collection->getAttribute('attributes', []); + /** + * Auth requires $permissions + */ + //$selects[] = Query::select('$id'); // Do we need this? + //$selects[] = Query::select('$permissions', system: true); + $queries = Query::addSelect($queries, Query::select('$permissions', system: true)); +// $queries = Query::add($queries, Query::select('$id')); +// $queries = Query::add($queries, Query::select('$createdAt')); +// $queries = Query::add($queries, Query::select('$createdAt')); + + $selects = Query::getSelectQueries($queries); + if (count($selects) !== count($queries)) { + // Do we want this check? + throw new QueryException('Only select queries are allowed'); + } + + $context = new QueryContext(); + $context->add($collection); if ($this->validate) { - $validator = new DocumentValidator($attributes); + $validator = new DocumentsValidator($context); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } @@ -3032,45 +3049,36 @@ public function getDocument(string $collection, string $id, array $queries = [], fn (Document $attribute) => $attribute->getAttribute('type') === self::VAR_RELATIONSHIP ); - $selects = Query::groupByType($queries)['selections']; - $selections = $this->validateSelections($collection, $selects); $nestedSelections = []; - foreach ($queries as $query) { - if ($query->getMethod() == Query::TYPE_SELECT) { - $values = $query->getValues(); - foreach ($values as $valueIndex => $value) { - if (\str_contains($value, '.')) { - // Shift the top level off the dot-path to pass the selection down the chain - // 'foo.bar.baz' becomes 'bar.baz' - $nestedSelections[] = Query::select([ - \implode('.', \array_slice(\explode('.', $value), 1)) - ]); + foreach ($selects as $i => $q) { + if (\str_contains($q->getAttribute(), '.')) { + $key = \explode('.', $q->getAttribute())[0]; - $key = \explode('.', $value)[0]; + foreach ($relationships as $relationship) { + if ($relationship->getAttribute('key') === $key) { + $nestedSelections[] = Query::select( + \implode('.', \array_slice(\explode('.', $q->getAttribute()), 1)) + ); - foreach ($relationships as $relationship) { - if ($relationship->getAttribute('key') === $key) { - switch ($relationship->getAttribute('options')['relationType']) { - case Database::RELATION_MANY_TO_MANY: - case Database::RELATION_ONE_TO_MANY: - unset($values[$valueIndex]); - break; + switch ($relationship->getAttribute('options')['relationType']) { + case Database::RELATION_MANY_TO_MANY: + case Database::RELATION_ONE_TO_MANY: + unset($selects[$i]); + break; - case Database::RELATION_MANY_TO_ONE: - case Database::RELATION_ONE_TO_ONE: - $values[$valueIndex] = $key; - break; - } - } + case Database::RELATION_MANY_TO_ONE: + case Database::RELATION_ONE_TO_ONE: + $q->setAttribute($key); + $selects[$i] = $q; + break; } } } - $query->setValues(\array_values($values)); } } - $queries = \array_values($queries); + $selects = \array_values($selects); // Since we may unset above $validator = new Authorization(self::PERMISSION_READ); $documentSecurity = $collection->getAttribute('documentSecurity', false); @@ -3078,7 +3086,7 @@ public function getDocument(string $collection, string $id, array $queries = [], [$collectionKey, $documentKey, $hashKey] = $this->getCacheKeys( $collection->getId(), $id, - $selections + $selects ); try { @@ -3091,6 +3099,12 @@ public function getDocument(string $collection, string $id, array $queries = [], if ($cached) { $document = new Document($cached); +// $permissions = new Document([ +// '$permissions' => $document->getAttribute('$perms') +// ]); +// +// $document->removeAttribute('$perms'); + if ($collection->getId() !== self::METADATA) { if (!$validator->isValid([ ...$collection->getRead(), @@ -3108,10 +3122,14 @@ public function getDocument(string $collection, string $id, array $queries = [], $document = $this->adapter->getDocument( $collection->getId(), $id, - $queries, + $selects, $forUpdate ); +// $permissions = new Document([ +// '$permissions' => $document->getAttribute('$perms') +// ]); + if ($document->isEmpty()) { return $document; } @@ -3127,8 +3145,8 @@ public function getDocument(string $collection, string $id, array $queries = [], } } - $document = $this->casting($collection, $document); - $document = $this->decode($collection, $document, $selections); + $document = $this->casting($context, $document, $selects); + $document = $this->decode($context, $document, $selects); $this->map = []; if ($this->resolveRelationships && (empty($selects) || !empty($nestedSelections))) { @@ -3153,19 +3171,36 @@ public function getDocument(string $collection, string $id, array $queries = [], // Remove internal attributes if not queried for select query // $id, $permissions and $collection are the default selected attributes for (MariaDB, MySQL, SQLite, Postgres) // All internal attributes are default selected attributes for (MongoDB) - foreach ($queries as $query) { - if ($query->getMethod() === Query::TYPE_SELECT) { - $values = $query->getValues(); - foreach ($this->getInternalAttributes() as $internalAttribute) { - if (!\in_array($internalAttribute['$id'], $values)) { - $document->removeAttribute($internalAttribute['$id']); - } - } - } - } + +// if (!empty($selects)) { +// $selectedAttributes = array_map( +// fn ($q) => $q->getAttribute(), +// array_filter($selects, fn ($q) => $q->isSystem() === false) +// ); +// +// if (!in_array('*', $selectedAttributes)){ +// foreach ($this->getInternalAttributes() as $internalAttribute) { +// if (!in_array($internalAttribute['$id'], $selectedAttributes, true)) { +// $document->removeAttribute($internalAttribute['$id']); +// } +// } +// } +// } + +// if (!empty($selects)){ +// $selectedAttributes = array_map(fn ($q) => $q->getAttribute(), $selects); +// +// if (!in_array('*', $selectedAttributes) && !in_array('$collection', $selectedAttributes)) { +// $document->removeAttribute('$collection'); +// } +// +// var_dump($selectedAttributes); +// } $this->trigger(self::EVENT_DOCUMENT_READ, $document); + $document->removeAttribute('$perms'); + return $document; } @@ -3495,7 +3530,10 @@ public function createDocument(string $collection, Document $document): Document $document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document)); } - $document = $this->decode($collection, $document); + $context = new QueryContext(); + $context->add($collection); + + $document = $this->decode($context, $document); $this->trigger(self::EVENT_DOCUMENT_CREATE, $document); @@ -3538,6 +3576,9 @@ public function createDocuments( } } + $context = new QueryContext(); + $context->add($collection); + $time = DateTime::now(); $modified = 0; @@ -3587,7 +3628,7 @@ public function createDocuments( $document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document)); } - $document = $this->decode($collection, $document); + $document = $this->decode($context, $document); $onNext && $onNext($document); $modified++; } @@ -4116,7 +4157,10 @@ public function updateDocument(string $collection, string $id, Document $documen $document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document)); } - $document = $this->decode($collection, $document); + $context = new QueryContext(); + $context->add($collection); + + $document = $this->decode($context, $document); $this->trigger(self::EVENT_DOCUMENT_UPDATE, $document); @@ -4168,16 +4212,15 @@ public function updateDocuments( throw new AuthorizationException($authorization->getDescription()); } - $attributes = $collection->getAttribute('attributes', []); - $indexes = $collection->getAttribute('indexes', []); + $context = new QueryContext(); + $context->add($collection); if ($this->validate) { $validator = new DocumentsValidator( - $attributes, - $indexes, - $this->maxQueryValues, - $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime(), + $context, + maxValuesCount: $this->maxQueryValues, + minAllowedDate: $this->adapter->getMinDateTime(), + maxAllowedDate: $this->adapter->getMaxDateTime() ); if (!$validator->isValid($queries)) { @@ -4185,12 +4228,15 @@ public function updateDocuments( } } - $grouped = Query::groupByType($queries); - $limit = $grouped['limit']; - $cursor = $grouped['cursor']; + $limit = Query::getLimitQuery($queries); - if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { - throw new DatabaseException("cursor Document must be from the same Collection."); + $cursor = new Document(); + $cursorQuery = Query::getCursorQueries($queries); + if (! is_null($cursorQuery)) { + $cursor = $cursorQuery->getCursorDocument($cursorQuery); + if ($cursor->getCollection() !== $collection->getId()) { + throw new DatabaseException("cursor Document must be from the same Collection."); + } } unset($updates['$id']); @@ -4233,7 +4279,7 @@ public function updateDocuments( Query::limit($batchSize) ]; - if (!empty($last)) { + if (!$last->isEmpty()) { $new[] = Query::cursorAfter($last); } @@ -4280,7 +4326,7 @@ public function updateDocuments( foreach ($batch as $doc) { $this->purgeCachedDocument($collection->getId(), $doc->getId()); - $doc = $this->decode($collection, $doc); + $doc = $this->decode($context, $doc); $onNext && $onNext($doc); $modified++; } @@ -4364,7 +4410,7 @@ private function updateDocumentRelationships(Document $collection, Document $old } if (\is_string($value)) { - $related = $this->skipRelationships(fn () => $this->getDocument($relatedCollection->getId(), $value, [Query::select(['$id'])])); + $related = $this->skipRelationships(fn () => $this->getDocument($relatedCollection->getId(), $value, [Query::select('$id')])); if ($related->isEmpty()) { // If no such document exists in related collection // For one-one we need to update the related key to null if no relation exists @@ -4393,7 +4439,7 @@ private function updateDocumentRelationships(Document $collection, Document $old switch (\gettype($value)) { case 'string': $related = $this->skipRelationships( - fn () => $this->getDocument($relatedCollection->getId(), $value, [Query::select(['$id'])]) + fn () => $this->getDocument($relatedCollection->getId(), $value, [Query::select('$id')]) ); if ($related->isEmpty()) { @@ -4405,7 +4451,7 @@ private function updateDocumentRelationships(Document $collection, Document $old if ( $oldValue?->getId() !== $value && !($this->skipRelationships(fn () => $this->findOne($relatedCollection->getId(), [ - Query::select(['$id']), + Query::select('$id'), Query::equal($twoWayKey, [$value]), ]))->isEmpty()) ) { @@ -4426,7 +4472,7 @@ private function updateDocumentRelationships(Document $collection, Document $old if ( $oldValue?->getId() !== $value->getId() && !($this->skipRelationships(fn () => $this->findOne($relatedCollection->getId(), [ - Query::select(['$id']), + Query::select('$id'), Query::equal($twoWayKey, [$value->getId()]), ]))->isEmpty()) ) { @@ -4507,7 +4553,7 @@ private function updateDocumentRelationships(Document $collection, Document $old foreach ($value as $relation) { if (\is_string($relation)) { $related = $this->skipRelationships( - fn () => $this->getDocument($relatedCollection->getId(), $relation, [Query::select(['$id'])]) + fn () => $this->getDocument($relatedCollection->getId(), $relation, [Query::select('$id')]) ); if ($related->isEmpty()) { @@ -4521,7 +4567,7 @@ private function updateDocumentRelationships(Document $collection, Document $old )); } elseif ($relation instanceof Document) { $related = $this->skipRelationships( - fn () => $this->getDocument($relatedCollection->getId(), $relation->getId(), [Query::select(['$id'])]) + fn () => $this->getDocument($relatedCollection->getId(), $relation->getId(), [Query::select('$id')]) ); if ($related->isEmpty()) { @@ -4550,7 +4596,7 @@ private function updateDocumentRelationships(Document $collection, Document $old if (\is_string($value)) { $related = $this->skipRelationships( - fn () => $this->getDocument($relatedCollection->getId(), $value, [Query::select(['$id'])]) + fn () => $this->getDocument($relatedCollection->getId(), $value, [Query::select('$id')]) ); if ($related->isEmpty()) { @@ -4561,7 +4607,7 @@ private function updateDocumentRelationships(Document $collection, Document $old $this->purgeCachedDocument($relatedCollection->getId(), $value); } elseif ($value instanceof Document) { $related = $this->skipRelationships( - fn () => $this->getDocument($relatedCollection->getId(), $value->getId(), [Query::select(['$id'])]) + fn () => $this->getDocument($relatedCollection->getId(), $value->getId(), [Query::select('$id')]) ); if ($related->isEmpty()) { @@ -4631,11 +4677,11 @@ private function updateDocumentRelationships(Document $collection, Document $old foreach ($value as $relation) { if (\is_string($relation)) { - if (\in_array($relation, $oldIds) || $this->getDocument($relatedCollection->getId(), $relation, [Query::select(['$id'])])->isEmpty()) { + if (\in_array($relation, $oldIds) || $this->getDocument($relatedCollection->getId(), $relation, [Query::select('$id')])->isEmpty()) { continue; } } elseif ($relation instanceof Document) { - $related = $this->getDocument($relatedCollection->getId(), $relation->getId(), [Query::select(['$id'])]); + $related = $this->getDocument($relatedCollection->getId(), $relation->getId(), [Query::select('$id')]); if ($related->isEmpty()) { if (!isset($value['$permissions'])) { @@ -4752,6 +4798,9 @@ public function createOrUpdateDocumentsWithIncrease( $created = 0; $updated = 0; + $context = new QueryContext(); + $context->add($collection); + foreach ($documents as $key => $document) { if ($this->getSharedTables() && $this->getTenantPerDocument()) { $old = Authorization::skip(fn () => $this->withTenant($document->getTenant(), fn () => $this->silent(fn () => $this->getDocument( @@ -4871,7 +4920,7 @@ public function createOrUpdateDocumentsWithIncrease( $doc = $this->silent(fn () => $this->populateDocumentRelationships($collection, $doc)); } - $doc = $this->decode($collection, $doc); + $doc = $this->decode($context, $doc); if ($this->getSharedTables() && $this->getTenantPerDocument()) { $this->withTenant($doc->getTenant(), function () use ($collection, $doc) { @@ -5274,7 +5323,7 @@ private function deleteRestrict( ) { Authorization::skip(function () use ($document, $relatedCollection, $twoWayKey) { $related = $this->findOne($relatedCollection->getId(), [ - Query::select(['$id']), + Query::select('$id'), Query::equal($twoWayKey, [$document->getId()]) ]); @@ -5297,7 +5346,7 @@ private function deleteRestrict( && $side === Database::RELATION_SIDE_CHILD ) { $related = Authorization::skip(fn () => $this->findOne($relatedCollection->getId(), [ - Query::select(['$id']), + Query::select('$id'), Query::equal($twoWayKey, [$document->getId()]) ])); @@ -5335,14 +5384,14 @@ private function deleteSetNull(Document $collection, Document $relatedCollection Authorization::skip(function () use ($document, $value, $relatedCollection, $twoWay, $twoWayKey, $side) { if (!$twoWay && $side === Database::RELATION_SIDE_CHILD) { $related = $this->findOne($relatedCollection->getId(), [ - Query::select(['$id']), + Query::select('$id'), Query::equal($twoWayKey, [$document->getId()]) ]); } else { if (empty($value)) { return; } - $related = $this->getDocument($relatedCollection->getId(), $value->getId(), [Query::select(['$id'])]); + $related = $this->getDocument($relatedCollection->getId(), $value->getId(), [Query::select('$id')]); } if ($related->isEmpty()) { @@ -5383,7 +5432,7 @@ private function deleteSetNull(Document $collection, Document $relatedCollection if (!$twoWay) { $value = $this->find($relatedCollection->getId(), [ - Query::select(['$id']), + Query::select('$id'), Query::equal($twoWayKey, [$document->getId()]), Query::limit(PHP_INT_MAX) ]); @@ -5406,7 +5455,7 @@ private function deleteSetNull(Document $collection, Document $relatedCollection $junction = $this->getJunctionCollection($collection, $relatedCollection, $side); $junctions = $this->find($junction, [ - Query::select(['$id']), + Query::select('$id'), Query::equal($twoWayKey, [$document->getId()]), Query::limit(PHP_INT_MAX) ]); @@ -5476,7 +5525,7 @@ private function deleteCascade(Document $collection, Document $relatedCollection } $value = $this->find($relatedCollection->getId(), [ - Query::select(['$id']), + Query::select('$id'), Query::equal($twoWayKey, [$document->getId()]), Query::limit(PHP_INT_MAX), ]); @@ -5497,7 +5546,8 @@ private function deleteCascade(Document $collection, Document $relatedCollection $junction = $this->getJunctionCollection($collection, $relatedCollection, $side); $junctions = $this->skipRelationships(fn () => $this->find($junction, [ - Query::select(['$id', $key]), + Query::select('$id'), + Query::select($key), Query::equal($twoWayKey, [$document->getId()]), Query::limit(PHP_INT_MAX) ])); @@ -5561,29 +5611,38 @@ public function deleteDocuments( throw new AuthorizationException($authorization->getDescription()); } - $attributes = $collection->getAttribute('attributes', []); - $indexes = $collection->getAttribute('indexes', []); + $context = new QueryContext(); + $context->add($collection); + + $queries = Query::addSelect($queries, Query::select('$id')); + $queries = Query::addSelect($queries, Query::select('$permissions')); + $queries = Query::addSelect($queries, Query::select('$internalId')); + $queries = Query::addSelect($queries, Query::select('$createdAt')); + $queries = Query::addSelect($queries, Query::select('$updatedAt')); if ($this->validate) { $validator = new DocumentsValidator( - $attributes, - $indexes, - $this->maxQueryValues, - $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime() + $context, + maxValuesCount: $this->maxQueryValues, + minAllowedDate: $this->adapter->getMinDateTime(), + maxAllowedDate: $this->adapter->getMaxDateTime() ); + if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } } - $grouped = Query::groupByType($queries); - $limit = $grouped['limit']; - $cursor = $grouped['cursor']; + $limit = Query::getLimitQuery($queries); - if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { - throw new DatabaseException("Cursor document must be from the same Collection."); + $cursor = new Document(); + $cursorQuery = Query::getCursorQueries($queries); + if (! is_null($cursorQuery)) { + $cursor = $cursorQuery->getCursorDocument($cursorQuery); + if ($cursor->getCollection() !== $collection->getId()) { + throw new DatabaseException("cursor Document must be from the same Collection."); + } } $originalLimit = $limit; @@ -5601,7 +5660,7 @@ public function deleteDocuments( Query::limit($batchSize) ]; - if (!empty($last)) { + if (!$last->isEmpty()) { $new[] = Query::cursorAfter($last); } @@ -5750,143 +5809,151 @@ public function find(string $collection, array $queries = [], string $forPermiss throw new NotFoundException('Collection not found'); } - $attributes = $collection->getAttribute('attributes', []); - $indexes = $collection->getAttribute('indexes', []); + $context = new QueryContext(); + $context->add($collection); + + $joins = Query::getJoinQueries($queries); + + foreach ($joins as $join) { + $context->add( + $this->silent(fn () => $this->getCollection($join->getCollection())), + $join->getAlias() + ); + } + + $authorization = new Authorization($forPermission); + + foreach ($context->getCollections() as $_collection) { + $documentSecurity = $_collection->getAttribute('documentSecurity', false); + $skipAuth = $authorization->isValid($_collection->getPermissionsByType($forPermission)); + + if (!$skipAuth && !$documentSecurity && $_collection->getId() !== self::METADATA) { + throw new AuthorizationException($authorization->getDescription()); + } + + $context->addSkipAuth($this->adapter->filter($_collection->getId()), $forPermission, $skipAuth); + } if ($this->validate) { $validator = new DocumentsValidator( - $attributes, - $indexes, - $this->maxQueryValues, - $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime(), + $context, + maxValuesCount: $this->maxQueryValues, + minAllowedDate: $this->adapter->getMinDateTime(), + maxAllowedDate: $this->adapter->getMaxDateTime() ); + if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } } - $authorization = new Authorization($forPermission); - $documentSecurity = $collection->getAttribute('documentSecurity', false); - $skipAuth = $authorization->isValid($collection->getPermissionsByType($forPermission)); - - if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { - throw new AuthorizationException($authorization->getDescription()); - } + /** + * Convert Queries + */ + $queries = self::convertQueries($context, $queries); $relationships = \array_filter( $collection->getAttribute('attributes', []), fn (Document $attribute) => $attribute->getAttribute('type') === self::VAR_RELATIONSHIP ); - $grouped = Query::groupByType($queries); - $filters = $grouped['filters']; - $selects = $grouped['selections']; - $limit = $grouped['limit']; - $offset = $grouped['offset']; - $orderAttributes = $grouped['orderAttributes']; - $orderTypes = $grouped['orderTypes']; - $cursor = $grouped['cursor']; - $cursorDirection = $grouped['cursorDirection']; + $filters = Query::getFilterQueries($queries); + $selects = Query::getSelectQueries($queries); + $limit = Query::getLimitQuery($queries, 25); + $offset = Query::getOffsetQuery($queries, 0); + $orders = Query::getOrderQueries($queries); + + $cursor = []; + $cursorDirection = Database::CURSOR_AFTER; + $cursorQuery = Query::getCursorQueries($queries); + if (! is_null($cursorQuery)) { + $cursor = $cursorQuery->getCursorDocument($cursorQuery); + $cursorDirection = $cursorQuery->getCursorDirection(); - if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { - throw new DatabaseException("cursor Document must be from the same Collection."); + if ($cursor->getCollection() !== $collection->getId()) { + throw new DatabaseException("cursor Document must be from the same Collection."); + } + + $cursor = $this->encode($collection, $cursor)->getArrayCopy(); } - $cursor = empty($cursor) ? [] : $this->encode($collection, $cursor)->getArrayCopy(); + $uniqueOrderBy = false; + foreach ($orders as $order) { + if ($order->getAttribute() === '$id' || $order->getAttribute() === '$sequence') { + $uniqueOrderBy = true; + } + } - /** @var array $queries */ - $queries = \array_merge( - $selects, - self::convertQueries($collection, $filters) - ); + if ($uniqueOrderBy === false) { + $orders[] = Query::orderAsc(); + } - $selections = $this->validateSelections($collection, $selects); - $nestedSelections = []; + foreach ($orders as $order) { + if (!empty($cursor) && !isset($cursor[$order->getAttribute()])) { + throw new OrderException( + message: "Order attribute '{$order->getAttribute()}' is empty", + attribute: $order->getAttribute() + ); + } + } - foreach ($queries as $index => &$query) { - switch ($query->getMethod()) { - case Query::TYPE_SELECT: - $values = $query->getValues(); - foreach ($values as $valueIndex => $value) { - if (\str_contains($value, '.')) { - // Shift the top level off the dot-path to pass the selection down the chain - // 'foo.bar.baz' becomes 'bar.baz' - $nestedSelections[] = Query::select([ - \implode('.', \array_slice(\explode('.', $value), 1)) - ]); + $nestedSelections = []; - $key = \explode('.', $value)[0]; + foreach ($selects as $i => $q) { + if (\str_contains($q->getAttribute(), '.')) { + $key = \explode('.', $q->getAttribute())[0]; + foreach ($relationships as $relationship) { + if ($relationship->getAttribute('key') === $key) { + $nestedSelections[] = Query::select( + \implode('.', \array_slice(\explode('.', $q->getAttribute()), 1)) + ); - foreach ($relationships as $relationship) { - if ($relationship->getAttribute('key') === $key) { - switch ($relationship->getAttribute('options')['relationType']) { - case Database::RELATION_MANY_TO_MANY: - case Database::RELATION_ONE_TO_MANY: - unset($values[$valueIndex]); - break; + switch ($relationship->getAttribute('options')['relationType']) { + case Database::RELATION_MANY_TO_MANY: + case Database::RELATION_ONE_TO_MANY: + unset($selects[$i]); + break; - case Database::RELATION_MANY_TO_ONE: - case Database::RELATION_ONE_TO_ONE: - $values[$valueIndex] = $key; - break; - } - } - } + case Database::RELATION_MANY_TO_ONE: + case Database::RELATION_ONE_TO_ONE: + $q->setAttribute($key); + $selects[$i] = $q; + break; } } - $query->setValues(\array_values($values)); - break; - default: - if (\str_contains($query->getAttribute(), '.')) { - unset($queries[$index]); - } - break; + } } } - $queries = \array_values($queries); + $selects = \array_values($selects); // Since we may unset above - $getResults = fn () => $this->adapter->find( - $collection->getId(), + $results = $this->adapter->find( + $context, $queries, - $limit ?? 25, - $offset ?? 0, - $orderAttributes, - $orderTypes, + $limit, + $offset, $cursor, - $cursorDirection ?? Database::CURSOR_AFTER, - $forPermission + $cursorDirection, + $forPermission, + selects: $selects, + filters: $filters, + joins: $joins, + orderQueries: $orders ); - $results = $skipAuth ? Authorization::skip($getResults) : $getResults(); + foreach ($results as $index => $node) { + $node = $this->casting($context, $node, $selects); + $node = $this->decode($context, $node, $selects); - foreach ($results as &$node) { if ($this->resolveRelationships && (empty($selects) || !empty($nestedSelections))) { $node = $this->silent(fn () => $this->populateDocumentRelationships($collection, $node, $nestedSelections)); } - $node = $this->casting($collection, $node); - $node = $this->decode($collection, $node, $selections); if (!$node->isEmpty()) { $node->setAttribute('$collection', $collection->getId()); } - } - - unset($query); - // Remove internal attributes which are not queried - foreach ($queries as $query) { - if ($query->getMethod() === Query::TYPE_SELECT) { - $values = $query->getValues(); - foreach ($results as $result) { - foreach ($this->getInternalAttributes() as $internalAttribute) { - if (!\in_array($internalAttribute['$id'], $values)) { - $result->removeAttribute($internalAttribute['$id']); - } - } - } - } + $results[$index] = $node; } $this->trigger(self::EVENT_DOCUMENT_FIND, $results); @@ -5907,17 +5974,20 @@ public function find(string $collection, array $queries = [], string $forPermiss */ public function foreach(string $collection, callable $callback, array $queries = [], string $forPermission = Database::PERMISSION_READ): void { - $grouped = Query::groupByType($queries); - $limitExists = $grouped['limit'] !== null; - $limit = $grouped['limit'] ?? 25; - $offset = $grouped['offset']; + $cursorQuery = Query::getCursorQueries($queries); + if (! is_null($cursorQuery)) { + if ($cursorQuery->getCursorDirection() === Database::CURSOR_BEFORE) { + throw new DatabaseException('Cursor ' . Database::CURSOR_BEFORE . ' not supported in this method.'); + } + } - $cursor = $grouped['cursor']; - $cursorDirection = $grouped['cursorDirection']; + $offset = Query::getOffsetQuery($queries); - // Cursor before is not supported - if ($cursor !== null && $cursorDirection === Database::CURSOR_BEFORE) { - throw new DatabaseException('Cursor ' . Database::CURSOR_BEFORE . ' not supported in this method.'); + $limitExists = true; + $limit = Query::getLimitQuery($queries); + if (is_null($limit)) { + $limit = 25; + $limitExists = false; } $sum = $limit; @@ -5992,29 +6062,44 @@ public function findOne(string $collection, array $queries = []): Document public function count(string $collection, array $queries = [], ?int $max = null): int { $collection = $this->silent(fn () => $this->getCollection($collection)); - $attributes = $collection->getAttribute('attributes', []); - $indexes = $collection->getAttribute('indexes', []); + + /** + * @var $collection Document + */ + + if ($collection->isEmpty()) { + throw new NotFoundException('Collection not found'); + } + + $context = new QueryContext(); + $context->add($collection); + + $authorization = new Authorization(self::PERMISSION_READ); + if ($authorization->isValid($collection->getRead())) { + $skipAuth = true; + } if ($this->validate) { $validator = new DocumentsValidator( - $attributes, - $indexes, - $this->maxQueryValues, - $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime(), + $context, + maxValuesCount: $this->maxQueryValues, + minAllowedDate: $this->adapter->getMinDateTime(), + maxAllowedDate: $this->adapter->getMaxDateTime() ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } } - $authorization = new Authorization(self::PERMISSION_READ); - if ($authorization->isValid($collection->getRead())) { - $skipAuth = true; - } + /** + * We allow only filters + */ + $queries = Query::getFilterQueries($queries); - $queries = Query::groupByType($queries)['filters']; - $queries = self::convertQueries($collection, $queries); + /** + * Convert Queries + */ + $queries = self::convertQueries($context, $queries); $getCount = fn () => $this->adapter->count($collection->getId(), $queries, $max); $count = $skipAuth ?? false ? Authorization::skip($getCount) : $getCount(); @@ -6040,25 +6125,47 @@ public function count(string $collection, array $queries = [], ?int $max = null) public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): float|int { $collection = $this->silent(fn () => $this->getCollection($collection)); - $attributes = $collection->getAttribute('attributes', []); - $indexes = $collection->getAttribute('indexes', []); + + /** + * @var $collection Document + */ + + if ($collection->isEmpty()) { + throw new NotFoundException('Collection not found'); + } + + $context = new QueryContext(); + $context->add($collection); + + $authorization = new Authorization(self::PERMISSION_READ); + if ($authorization->isValid($collection->getRead())) { + $skipAuth = true; + } if ($this->validate) { $validator = new DocumentsValidator( - $attributes, - $indexes, - $this->maxQueryValues, - $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime(), + $context, + maxValuesCount: $this->maxQueryValues, + minAllowedDate: $this->adapter->getMinDateTime(), + maxAllowedDate: $this->adapter->getMaxDateTime() ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } } - $queries = self::convertQueries($collection, $queries); + /** + * We allow only filters + */ + $queries = Query::getFilterQueries($queries); - $sum = $this->adapter->sum($collection->getId(), $attribute, $queries, $max); + /** + * Convert Queries + */ + $queries = self::convertQueries($context, $queries); + + $getCount = fn () => $this->adapter->sum($collection->getId(), $attribute, $queries, $max); + $sum = $skipAuth ?? false ? Authorization::skip($getCount) : $getCount(); $this->trigger(self::EVENT_DOCUMENT_SUM, $sum); @@ -6144,108 +6251,180 @@ public function encode(Document $collection, Document $document): Document /** * Decode Document * - * @param Document $collection + * @param QueryContext $context * @param Document $document - * @param array $selections + * @param array $selects * @return Document * @throws DatabaseException */ - public function decode(Document $collection, Document $document, array $selections = []): Document + public function decode(QueryContext $context, Document $document, array $selects = []): Document { - $attributes = \array_filter( - $collection->getAttribute('attributes', []), - fn ($attribute) => $attribute['type'] !== self::VAR_RELATIONSHIP - ); - - $relationships = \array_filter( - $collection->getAttribute('attributes', []), - fn ($attribute) => $attribute['type'] === self::VAR_RELATIONSHIP - ); + $internals = []; + $schema = []; - foreach ($relationships as $relationship) { - $key = $relationship['$id'] ?? ''; + foreach (Database::INTERNAL_ATTRIBUTES as $attribute) { + $internals[$attribute['$id']] = $attribute; + } - if ( - \array_key_exists($key, (array)$document) - || \array_key_exists($this->adapter->filter($key), (array)$document) - ) { - $value = $document->getAttribute($key); - $value ??= $document->getAttribute($this->adapter->filter($key)); - $document->removeAttribute($this->adapter->filter($key)); - $document->setAttribute($key, $value); + foreach ($context->getCollections() as $collection) { + foreach ($collection->getAttribute('attributes', []) as $attribute) { + $key = $attribute->getAttribute('key', $attribute->getAttribute('$id')); + $key = $this->adapter->filter($key); + $schema[$collection->getId()][$key] = $attribute->getArrayCopy(); } } - $attributes = \array_merge($attributes, $this->getInternalAttributes()); + $new = new Document(); - foreach ($attributes as $attribute) { - $key = $attribute['$id'] ?? ''; - $array = $attribute['array'] ?? false; - $filters = $attribute['filters'] ?? []; - $value = $document->getAttribute($key); + foreach ($document as $key => $value) { + if($key === '$perms'){ + $new->setAttribute($key, $value); + continue; + } + + $alias = Query::DEFAULT_ALIAS; + $attributeKey = ''; + + foreach ($selects as $select) { + if ($select->getAs() === $key){ + $attributeKey = $key; + $key = $select->getAttribute(); + $alias = $select->getAlias(); + + break; + } - if (\is_null($value)) { - $value = $document->getAttribute($this->adapter->filter($key)); + if ($select->getAttribute() == $key || $this->adapter->filter($select->getAttribute()) == $key) { + $alias = $select->getAlias(); - if (!\is_null($value)) { - $document->removeAttribute($this->adapter->filter($key)); + break; } } + $collection = $context->getCollectionByAlias($alias); + if ($collection->isEmpty()) { + throw new \Exception('Invalid query: Unknown Alias context'); + } + + $attribute = $internals[$key] ?? null; + + if (is_null($attribute)) { + $attribute = $schema[$collection->getId()][$this->adapter->filter($key)] ?? null; + } + + if (is_null($attribute)) { + continue; + } + + if (empty($attributeKey)){ + $attributeKey = $attribute['$id']; + } + + $array = $attribute['array'] ?? false; + $filters = $attribute['filters'] ?? []; + $value = ($array) ? $value : [$value]; $value = (is_null($value)) ? [] : $value; - foreach ($value as &$node) { - foreach (\array_reverse($filters) as $filter) { - $node = $this->decodeAttribute($filter, $node, $document, $key); + foreach ($value as $index => $node) { + foreach (array_reverse($filters) as $filter) { + $value[$index] = $this->decodeAttribute($filter, $node, $document, $key); } } - if (empty($selections) || \in_array($key, $selections) || \in_array('*', $selections)) { - if ( - empty($selections) - || \in_array($key, $selections) - || \in_array('*', $selections) - || \in_array($key, ['$createdAt', '$updatedAt']) - ) { - // Prevent null values being set for createdAt and updatedAt - if (\in_array($key, ['$createdAt', '$updatedAt']) && $value[0] === null) { - continue; - } else { - $document->setAttribute($key, ($array) ? $value : $value[0]); - } - } - } + $value = ($array) ? $value : $value[0]; + + $new->setAttribute($attributeKey, $value); } - return $document; + return $new; } /** * Casting * - * @param Document $collection + * @param QueryContext $context * @param Document $document - * + * @param array $selects * @return Document + * @throws Exception */ - public function casting(Document $collection, Document $document): Document + public function casting(QueryContext $context, Document $document, array $selects = []): Document { if ($this->adapter->getSupportForCasting()) { return $document; } - $attributes = $collection->getAttribute('attributes', []); + $internals = []; + $schema = []; + + foreach (Database::INTERNAL_ATTRIBUTES as $attribute) { + $internals[$attribute['$id']] = $attribute; + } + + foreach ($context->getCollections() as $collection) { + foreach ($collection->getAttribute('attributes', []) as $attribute) { + $key = $attribute->getAttribute('key', $attribute->getAttribute('$id')); + $key = $this->adapter->filter($key); + $schema[$collection->getId()][$key] = $attribute->getArrayCopy(); + } + } + + $new = new Document(); + + foreach ($document as $key => $value) { + + if($key === '$perms'){ + $new->setAttribute($key, $value); + continue; + } + + $alias = Query::DEFAULT_ALIAS; + $attributeKey = ''; + + foreach ($selects as $select) { + if ($select->getAs() === $key){ + $attributeKey = $key; + $key = $select->getAttribute(); + $alias = $select->getAlias(); + + break; + } + + if ($select->getAttribute() == $key || $this->adapter->filter($select->getAttribute()) == $key) { + $alias = $select->getAlias(); + + break; + } + } + + $collection = $context->getCollectionByAlias($alias); + if ($collection->isEmpty()) { + throw new \Exception('Invalid query: Unknown Alias context'); + } + + $attribute = $internals[$key] ?? null; + + if (is_null($attribute)) { + $attribute = $schema[$collection->getId()][$this->adapter->filter($key)] ?? null; + } + + if (is_null($attribute)) { + continue; + } + + if (empty($attributeKey)){ + $attributeKey = $attribute['$id']; + } - foreach ($attributes as $attribute) { - $key = $attribute['$id'] ?? ''; - $type = $attribute['type'] ?? ''; - $array = $attribute['array'] ?? false; - $value = $document->getAttribute($key, null); if (is_null($value)) { + $new->setAttribute($attributeKey, null); continue; } + $type = $attribute['type'] ?? ''; + $array = $attribute['array'] ?? false; + if ($array) { $value = !is_string($value) ? $value @@ -6254,26 +6433,28 @@ public function casting(Document $collection, Document $document): Document $value = [$value]; } - foreach ($value as &$node) { + foreach ($value as $i => $node) { switch ($type) { case self::VAR_BOOLEAN: - $node = (bool)$node; + $value[$i] = (bool)$node; break; + case self::VAR_INTEGER: - $node = (int)$node; + $value[$i] = (int)$node; break; + case self::VAR_FLOAT: - $node = (float)$node; - break; - default: + $value[$i] = (float)$node; break; } } - $document->setAttribute($key, ($array) ? $value : $value[0]); + $value = ($array) ? $value : $value[0]; + + $new->setAttribute($attributeKey, $value); } - return $document; + return $new; } /** @@ -6340,65 +6521,6 @@ protected function decodeAttribute(string $filter, mixed $value, Document $docum return $value; } - /** - * Validate if a set of attributes can be selected from the collection - * - * @param Document $collection - * @param array $queries - * @return array - * @throws QueryException - */ - private function validateSelections(Document $collection, array $queries): array - { - if (empty($queries)) { - return []; - } - - $selections = []; - $relationshipSelections = []; - - foreach ($queries as $query) { - if ($query->getMethod() == Query::TYPE_SELECT) { - foreach ($query->getValues() as $value) { - if (\str_contains($value, '.')) { - $relationshipSelections[] = $value; - continue; - } - $selections[] = $value; - } - } - } - - // Allow querying internal attributes - $keys = \array_map( - fn ($attribute) => $attribute['$id'], - self::getInternalAttributes() - ); - - foreach ($collection->getAttribute('attributes', []) as $attribute) { - if ($attribute['type'] !== self::VAR_RELATIONSHIP) { - // Fallback to $id when key property is not present in metadata table for some tables such as Indexes or Attributes - $keys[] = $attribute['key'] ?? $attribute['$id']; - } - } - - $invalid = \array_diff($selections, $keys); - if (!empty($invalid) && !\in_array('*', $invalid)) { - throw new QueryException('Cannot select attributes: ' . \implode(', ', $invalid)); - } - - $selections = \array_merge($selections, $relationshipSelections); - - $selections[] = '$id'; - $selections[] = '$internalId'; - $selections[] = '$collection'; - $selections[] = '$createdAt'; - $selections[] = '$updatedAt'; - $selections[] = '$permissions'; - - return $selections; - } - /** * Get adapter attribute limit, accounting for internal metadata * Returns 0 to indicate no limit @@ -6425,46 +6547,71 @@ public function getLimitForIndexes(): int } /** - * @param Document $collection * @param array $queries * @return array - * @throws QueryException * @throws Exception */ - public static function convertQueries(Document $collection, array $queries): array + public static function convertQueries(QueryContext $context, array $queries): array + { + foreach ($queries as $i => $query) { + if ($query->isNested() || $query->isJoin()) { + $values = self::convertQueries($context, $query->getValues()); + $query->setValues($values); + } + + $query = self::convertQuery($context, $query); + + $queries[$i] = $query; + } + + return $queries; + } + + /** + * @throws Exception + */ + public static function convertQuery(QueryContext $context, Query $query): Query { + $collection = clone $context->getCollectionByAlias($query->getAlias()); + + if ($collection->isEmpty()) { + throw new \Exception('Unknown Alias context'); + } + + /** + * @var array $attributes + */ $attributes = $collection->getAttribute('attributes', []); foreach (Database::INTERNAL_ATTRIBUTES as $attribute) { $attributes[] = new Document($attribute); } - foreach ($attributes as $attribute) { - foreach ($queries as $query) { - if ($query->getAttribute() === $attribute->getId()) { - $query->setOnArray($attribute->getAttribute('array', false)); - } + $attribute = new Document(); + + foreach ($attributes as $attr) { + if ($attr->getId() === $query->getAttribute()) { + $attribute = $attr; } + } + + if (! $attribute->isEmpty()) { + $query->setOnArray($attribute->getAttribute('array', false)); if ($attribute->getAttribute('type') == Database::VAR_DATETIME) { - foreach ($queries as $index => $query) { - if ($query->getAttribute() === $attribute->getId()) { - $values = $query->getValues(); - foreach ($values as $valueIndex => $value) { - try { - $values[$valueIndex] = DateTime::setTimezone($value); - } catch (\Throwable $e) { - throw new QueryException($e->getMessage(), $e->getCode(), $e); - } - } - $query->setValues($values); - $queries[$index] = $query; + $values = $query->getValues(); + foreach ($values as $valueIndex => $value) { + try { + $values[$valueIndex] = DateTime::setTimezone($value); + } catch (\Throwable $e) { + throw new QueryException($e->getMessage(), $e->getCode(), $e); } } + $query->setValues($values); } } - return $queries; + return $query; } /** @@ -6498,7 +6645,7 @@ public function getSchemaAttributes(string $collection): array /** * @param string $collectionId * @param string|null $documentId - * @param array $selects + * @param array $selects * @return array{0: ?string, 1: ?string, 2: ?string} */ public function getCacheKeys(string $collectionId, ?string $documentId = null, array $selects = []): array @@ -6526,7 +6673,7 @@ public function getCacheKeys(string $collectionId, ?string $documentId = null, a $documentKey = $documentHashKey = "{$collectionKey}:{$documentId}"; if (!empty($selects)) { - $documentHashKey = $documentKey . ':' . \md5(\implode($selects)); + $documentHashKey = $documentKey . ':' . \md5(\serialize($selects)); } } diff --git a/src/Database/Query.php b/src/Database/Query.php index 745227401..950d46e10 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -22,8 +22,12 @@ class Query public const TYPE_STARTS_WITH = 'startsWith'; public const TYPE_ENDS_WITH = 'endsWith'; + public const TYPE_RELATION_EQUAL = 'relationEqual'; + public const TYPE_SELECT = 'select'; + //public const TYPE_SELECTION = 'selection'; + // Order methods public const TYPE_ORDER_DESC = 'orderDesc'; public const TYPE_ORDER_ASC = 'orderAsc'; @@ -38,6 +42,13 @@ class Query public const TYPE_AND = 'and'; public const TYPE_OR = 'or'; + // Join methods + public const TYPE_INNER_JOIN = 'innerJoin'; + + public const TYPE_LEFT_JOIN = 'leftJoin'; + + public const TYPE_RIGHT_JOIN = 'rightJoin'; + public const DEFAULT_ALIAS = 'main'; public const TYPES = [ @@ -70,8 +81,33 @@ class Query self::TYPE_OR, ]; + protected const FILTER_TYPES = [ + self::TYPE_EQUAL, + self::TYPE_NOT_EQUAL, + self::TYPE_LESSER, + self::TYPE_LESSER_EQUAL, + self::TYPE_GREATER, + self::TYPE_GREATER_EQUAL, + self::TYPE_CONTAINS, + self::TYPE_SEARCH, + self::TYPE_IS_NULL, + self::TYPE_IS_NOT_NULL, + self::TYPE_BETWEEN, + self::TYPE_STARTS_WITH, + self::TYPE_ENDS_WITH, + self::TYPE_AND, + self::TYPE_OR, + self::TYPE_RELATION_EQUAL, + ]; + protected string $method = ''; + protected string $collection = ''; + protected string $alias = ''; protected string $attribute = ''; + protected string $aliasRight = ''; + protected string $attributeRight = ''; + protected string $as = ''; + protected bool $system = false; protected bool $onArray = false; /** @@ -86,15 +122,41 @@ 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 $collection = '', + string $as = '', + bool $system = false, + ) { if ($attribute === '' && \in_array($method, [Query::TYPE_ORDER_ASC, Query::TYPE_ORDER_DESC])) { $attribute = '$internalId'; } + /** + * We can not make the fallback in the Query::static() calls , because parse method skips it + */ + if (empty($alias)) { + $alias = Query::DEFAULT_ALIAS; + } + + if (empty($aliasRight)) { + $aliasRight = Query::DEFAULT_ALIAS; + } + $this->method = $method; + $this->alias = $alias; $this->attribute = $attribute; $this->values = $values; + $this->aliasRight = $aliasRight; + $this->attributeRight = $attributeRight; + $this->collection = $collection; + $this->as = $as; + $this->system = $system; } public function __clone(): void @@ -139,6 +201,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 getAs(): string + { + return $this->as; + } + + public function getCollection(): string + { + return $this->collection; + } + /** * Sets method * @@ -165,6 +252,41 @@ public function setAttribute(string $attribute): self return $this; } + /** + * Sets right attribute + */ + public function setAttributeRight(string $attribute): self + { + $this->attributeRight = $attribute; + + return $this; + } + + public function getCursorDirection(): string + { + if ($this->method === self::TYPE_CURSOR_AFTER) { + return Database::CURSOR_AFTER; + } + + if ($this->method === self::TYPE_CURSOR_BEFORE) { + return Database::CURSOR_BEFORE; + } + + throw new \Exception('Invalid method: Get cursor direction on "'.$this->method.'" Query'); + } + + public function getOrderDirection(): string + { + if ($this->method === self::TYPE_ORDER_ASC) { + return Database::ORDER_ASC; + } + + if ($this->method === self::TYPE_ORDER_DESC) { + return Database::ORDER_DESC; + } + + throw new \Exception('Invalid method: Get order direction on "'.$this->method.'" Query'); + } /** * Sets values * @@ -352,9 +474,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); } /** @@ -364,9 +486,9 @@ public static function equal(string $attribute, array $values): self * @param string|int|float|bool $value * @return Query */ - public static function notEqual(string $attribute, string|int|float|bool $value): self + public static function notEqual(string $attribute, string|int|float|bool $value, string $alias = ''): self { - return new self(self::TYPE_NOT_EQUAL, $attribute, [$value]); + return new self(self::TYPE_NOT_EQUAL, $attribute, [$value], alias: $alias); } /** @@ -376,9 +498,9 @@ public static function notEqual(string $attribute, string|int|float|bool $value) * @param string|int|float|bool $value * @return Query */ - public static function lessThan(string $attribute, string|int|float|bool $value): self + public static function lessThan(string $attribute, string|int|float|bool $value, string $alias = ''): self { - return new self(self::TYPE_LESSER, $attribute, [$value]); + return new self(self::TYPE_LESSER, $attribute, [$value], alias: $alias); } /** @@ -388,9 +510,9 @@ public static function lessThan(string $attribute, string|int|float|bool $value) * @param string|int|float|bool $value * @return Query */ - public static function lessThanEqual(string $attribute, string|int|float|bool $value): self + public static function lessThanEqual(string $attribute, string|int|float|bool $value, string $alias = ''): self { - return new self(self::TYPE_LESSER_EQUAL, $attribute, [$value]); + return new self(self::TYPE_LESSER_EQUAL, $attribute, [$value], alias: $alias); } /** @@ -400,9 +522,9 @@ public static function lessThanEqual(string $attribute, string|int|float|bool $v * @param string|int|float|bool $value * @return Query */ - public static function greaterThan(string $attribute, string|int|float|bool $value): self + public static function greaterThan(string $attribute, string|int|float|bool $value, string $alias = ''): self { - return new self(self::TYPE_GREATER, $attribute, [$value]); + return new self(self::TYPE_GREATER, $attribute, [$value], alias: $alias); } /** @@ -412,9 +534,9 @@ public static function greaterThan(string $attribute, string|int|float|bool $val * @param string|int|float|bool $value * @return Query */ - public static function greaterThanEqual(string $attribute, string|int|float|bool $value): self + public static function greaterThanEqual(string $attribute, string|int|float|bool $value, string $alias = ''): self { - return new self(self::TYPE_GREATER_EQUAL, $attribute, [$value]); + return new self(self::TYPE_GREATER_EQUAL, $attribute, [$value], alias: $alias); } /** @@ -437,9 +559,9 @@ public static function contains(string $attribute, array $values): self * @param string|int|float|bool $end * @return Query */ - public static function between(string $attribute, string|int|float|bool $start, string|int|float|bool $end): self + public static function between(string $attribute, string|int|float|bool $start, string|int|float|bool $end, string $alias = ''): self { - return new self(self::TYPE_BETWEEN, $attribute, [$start, $end]); + return new self(self::TYPE_BETWEEN, $attribute, [$start, $end], alias: $alias); } /** @@ -460,20 +582,25 @@ public static function search(string $attribute, string $value): self * @param array $attributes * @return Query */ - public static function select(array $attributes): self + public static function select_old(array $attributes): self { return new self(self::TYPE_SELECT, values: $attributes); } + public static function select(string $attribute, string $alias = '', string $as = '', string $function = '', bool $system = false): self + { + return new self(self::TYPE_SELECT, $attribute, [], alias: $alias, as: $as, system: $system); + } + /** * Helper method to create Query with orderDesc method * * @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); } /** @@ -482,9 +609,9 @@ public static function orderDesc(string $attribute = ''): self * @param string $attribute * @return Query */ - public static function orderAsc(string $attribute = ''): self + public static function orderAsc(string $attribute = '', string $alias = ''): self { - return new self(self::TYPE_ORDER_ASC, $attribute); + return new self(self::TYPE_ORDER_ASC, $attribute, alias: $alias); } /** @@ -581,6 +708,55 @@ public static function and(array $queries): self return new self(self::TYPE_AND, '', $queries); } + /** + * @param string $collection + * @param string $alias + * @param array $queries + * @return self + */ + public static function join(string $collection, string $alias, array $queries = []): self + { + return new self(self::TYPE_INNER_JOIN, values: $queries, alias: $alias, collection: $collection); + } + + /** + * @param string $collection + * @param string $alias + * @param array $queries + * @return self + */ + public static function innerJoin(string $collection, string $alias, array $queries = []): self + { + return new self(self::TYPE_INNER_JOIN, values: $queries, alias: $alias, collection: $collection); + } + + /** + * @param string $collection + * @param string $alias + * @param array $queries + * @return self + */ + public static function leftJoin(string $collection, string $alias, array $queries = []): self + { + return new self(self::TYPE_LEFT_JOIN, values: $queries, alias: $alias, collection: $collection); + } + + /** + * @param string $collection + * @param string $alias + * @param array $queries + * @return self + */ + public static function rightJoin(string $collection, string $alias, array $queries = []): self + { + return new self(self::TYPE_RIGHT_JOIN, values: $queries, alias: $alias, collection: $collection); + } + + public static function relationEqual(string $leftAlias, string $leftColumn, string $rightAlias, string $rightColumn): self + { + return new self(self::TYPE_RELATION_EQUAL, $leftColumn, [], alias: $leftAlias, attributeRight: $rightColumn, aliasRight: $rightAlias); + } + /** * Filters $queries for $types * @@ -588,7 +764,7 @@ public static function and(array $queries): self * @param array $types * @return array */ - public static function getByType(array $queries, array $types): array + protected static function getByType(array $queries, array $types): array { $filtered = []; @@ -601,6 +777,144 @@ public static function getByType(array $queries, array $types): array return $filtered; } + /** + * @param array $queries + * @return array + */ + public static function getSelectQueries(array $queries): array + { + return self::getByType($queries, [ + Query::TYPE_SELECT, + ]); + } + + /** + * @param array $queries + * @return array + */ + public static function getJoinQueries(array $queries): array + { + return self::getByType($queries, [ + Query::TYPE_INNER_JOIN, + Query::TYPE_LEFT_JOIN, + Query::TYPE_RIGHT_JOIN, + ]); + } + + /** + * @param array $queries + * @return array + */ + public static function getLimitQueries(array $queries): array + { + foreach ($queries as $query) { + if ($query->getMethod() === Query::TYPE_LIMIT){ + return [clone $query]; + } + } + + return []; + } + + /** + * @param array $queries + * @param int|null $default + * @return int|null + */ + public static function getLimitQuery(array $queries, ?int $default = null): ?int + { + $queries = self::getLimitQueries($queries); + + if (empty($queries)) { + return $default; + } + + return $queries[0]->getValue(); + } + + /** + * @param array $queries + * @return array + */ + public static function getOffsetQueries(array $queries): array + { + foreach ($queries as $query) { + if ($query->getMethod() === Query::TYPE_OFFSET){ + return [clone $query]; + } + } + + return []; + } + + /** + * @param array $queries + * @param int|null $default + * @return int|null + */ + public static function getOffsetQuery(array $queries, ?int $default = null): ?int + { + $queries = self::getOffsetQueries($queries); + + if (empty($queries)) { + return $default; + } + + return $queries[0]->getValue(); + } + + /** + * @param array $queries + * @return array + */ + public static function getOrderQueries(array $queries): array + { + return self::getByType($queries, [ + Query::TYPE_ORDER_ASC, + Query::TYPE_ORDER_DESC, + ]); + } + + /** + * @param array $queries + * @return Query|null + */ + public static function getCursorQueries(array $queries): ?Query + { + $queries = self::getByType($queries, [ + Query::TYPE_CURSOR_AFTER, + Query::TYPE_CURSOR_BEFORE, + ]); + + if (empty($queries)) { + return null; + } + + return $queries[0]; + } + + /** + * @param Query $query + * @return Document + */ + public function getCursorDocument(?Query $query): Document + { + if (! is_null($query) && in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE])) { + return $query->getValue(); + } + + return new Document(); + } + + /** + * @param array $queries + * @return array + */ + public static function getFilterQueries(array $queries): array + { + return self::getByType($queries, self::FILTER_TYPES); + } + /** * Iterates through queries are groups them by type * @@ -619,6 +933,7 @@ public static function getByType(array $queries, array $types): array public static function groupByType(array $queries): array { $filters = []; + $joins = []; $selections = []; $limit = null; $offset = null; @@ -712,8 +1027,24 @@ public function isNested(): bool } /** - * @return bool + * Is this query able to contain other queries */ + public function isJoin(): bool + { + $types = [self::TYPE_INNER_JOIN, self::TYPE_LEFT_JOIN, self::TYPE_RIGHT_JOIN]; + + if (in_array($this->getMethod(), $types)) { + return true; + } + + return false; + } + + public static function isFilter(string $method): bool + { + return in_array($method, self::FILTER_TYPES); + } + public function onArray(): bool { return $this->onArray; @@ -727,4 +1058,52 @@ public function setOnArray(bool $bool): void { $this->onArray = $bool; } + + /** + * Is This query added by the system + */ + public function isSystem(): bool + { + return $this->system; + } + + /** + * @param array $queries + * @param Query $query + * @return array + * @throws \Exception + */ + public static function addSelect(array $queries, Query $query): array + { + $merge = true; + $found = false; + + foreach ($queries as $q) { + if ($q->getMethod() === self::TYPE_SELECT){ + $found = true; + + if ($q->getAlias() === $query->getAlias()){ + if ($q->getAttribute() === '*'){ + $merge = false; + } + + if ($q->getAttribute() === $query->getAttribute()){ + if ($q->getAs() === $query->getAs()){ + $merge = false; + } + } + } + } + } + + if ($found && $merge){ + $queries = [ + ...$queries, + $query + ]; + } + + return $queries; + } + } diff --git a/src/Database/QueryContext.php b/src/Database/QueryContext.php new file mode 100644 index 000000000..167684b07 --- /dev/null +++ b/src/Database/QueryContext.php @@ -0,0 +1,89 @@ + + */ + protected array $collections = []; + + /** + * @var array + */ + protected array $aliases = []; + + /** + * @var array + */ + protected array $skipAuthCollections = []; + + public function __construct() + { + } + + /** + * @return array + */ + public function getCollections(): array + { + return $this->collections; + } + + public function getCollectionByAlias(string $alias): Document + { + /** + * $alias can be an empty string + */ + $collectionId = $this->aliases[$alias] ?? null; + + if (is_null($collectionId)) { + return new Document(); + } + + foreach ($this->collections as $collection) { + if ($collection->getId() === $collectionId) { + return $collection; + } + } + + return new Document(); + } + + /** + * @throws QueryException + */ + public function add(Document $collection, string $alias = Query::DEFAULT_ALIAS): void + { + if (! empty($this->aliases[$alias])) { + throw new QueryException('Ambiguous alias for collection "'.$collection->getId().'".'); + } + + $this->collections[] = $collection; + $this->aliases[$alias] = $collection->getId(); + } + + public function addSkipAuth(string $collection, string $permission, bool $skipAuth): void + { + $this->skipAuthCollections[$collection][$permission] = $skipAuth; + } + + public function skipAuth(string $collection, string $permission): bool + { + if (!Authorization::$status) { // for Authorization::disable(); + return true; + } + + if (empty($this->skipAuthCollections[$collection][$permission])) { + return false; + } + + return true; + } + + +} diff --git a/src/Database/Validator/Alias.php b/src/Database/Validator/Alias.php new file mode 100644 index 000000000..7e3ecf8f2 --- /dev/null +++ b/src/Database/Validator/Alias.php @@ -0,0 +1,70 @@ +message; + } + + /** + * Is valid. + * Returns true if valid or false if not. + * @param mixed $value + * @return bool + */ + public function isValid($value): bool + { + if (! \is_string($value)) { + return false; + } + + if (empty($value)) { + return true; + } + + if (! preg_match('/^[a-zA-Z0-9_]+$/', $value)) { + return false; + } + + if (\mb_strlen($value) >= 64) { + 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_STRING; + } +} diff --git a/src/Database/Validator/AsQuery.php b/src/Database/Validator/AsQuery.php new file mode 100644 index 000000000..84181dca3 --- /dev/null +++ b/src/Database/Validator/AsQuery.php @@ -0,0 +1,85 @@ +message; + } + + /** + * Is valid. + * Returns true if valid or false if not. + * @param mixed $value + * @return bool + */ + public function isValid($value): bool + { + if (! \is_string($value)) { + return false; + } + + if (empty($value)) { + return true; + } + + if (! preg_match('/^[a-zA-Z0-9_]+$/', $value)) { + return false; + } + + if (\mb_strlen($value) >= 64) { + return false; + } + + if($this->attribute === '*'){ + $this->message = 'Invalid "as" on attribute "*"'; + 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_STRING; + } +} diff --git a/src/Database/Validator/IndexedQueries.php b/src/Database/Validator/IndexedQueries.php index cb727c0fb..da822be07 100644 --- a/src/Database/Validator/IndexedQueries.php +++ b/src/Database/Validator/IndexedQueries.php @@ -1,115 +1,115 @@ - */ - 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) { - try { - $query = Query::parse($query); - } catch (\Throwable $e) { - $this->message = 'Invalid query: '.$e->getMessage(); - - return false; - } - } - - if ($query->isNested()) { - if (! self::isValid($query->getValues())) { - return false; - } - } - - $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; - } -} +// +//namespace Utopia\Database\Validator; +// +//use Exception; +//use Utopia\Database\Database; +//use Utopia\Database\Document; +//use Utopia\Database\Query; +//use Utopia\Database\Validator\Query\Base; +// +//class IndexedQueries extends Queries +//{ +// /** +// * @var array +// */ +// 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) { +// try { +// $query = Query::parse($query); +// } catch (\Throwable $e) { +// $this->message = 'Invalid query: '.$e->getMessage(); +// +// return false; +// } +// } +// +// if ($query->isNested()) { +// if (! self::isValid($query->getValues())) { +// return false; +// } +// } +// +// $queries[] = $query; +// } +// +// $filters = Query::getFilterQueries($queries); +// +// 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/Queries.php b/src/Database/Validator/Queries.php index b1d67aad0..c13ccc034 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -1,152 +1,159 @@ - */ - protected array $validators; - - /** - * @var int - */ - protected int $length; - - /** - * Queries constructor - * - * @param array $validators - */ - public function __construct(array $validators = [], int $length = 0) - { - $this->validators = $validators; - $this->length = $length; - } - - /** - * Get Description. - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return $this->message; - } - - /** - * @param array $value - * @return bool - */ - 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; - } - - 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->isNested()) { - if (!self::isValid($query->getValues())) { - return false; - } - } - - $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_CURSOR_AFTER, - Query::TYPE_CURSOR_BEFORE => Base::METHOD_TYPE_CURSOR, - Query::TYPE_ORDER_ASC, - Query::TYPE_ORDER_DESC => 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, - Query::TYPE_AND, - Query::TYPE_OR => 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; - } - - $methodIsValid = true; - } - - if (!$methodIsValid) { - $this->message = 'Invalid query method: ' . $method; - return false; - } - } - - return true; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return true; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_OBJECT; - } -} +// +//namespace Utopia\Database\Validator; +// +//use Utopia\Database\Query; +//use Utopia\Database\Validator\Query\Base; +//use Utopia\Validator; +// +//class Queries extends Validator +//{ +// /** +// * @var string +// */ +// protected string $message = 'Invalid queries'; +// +// /** +// * @var array +// */ +// protected array $validators; +// +// /** +// * @var int +// */ +// protected int $length; +// +// /** +// * Queries constructor +// * +// * @param array $validators +// */ +// public function __construct(array $validators = [], int $length = 0) +// { +// $this->validators = $validators; +// $this->length = $length; +// } +// +// /** +// * Get Description. +// * +// * Returns validator description +// * +// * @return string +// */ +// public function getDescription(): string +// { +// return $this->message; +// } +// +// /** +// * @param array $value +// * @return bool +// */ +// 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; +// } +// +// 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->isNested()) { +// if (!self::isValid($query->getValues())) { +// return false; +// } +// } +// +// $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_CURSOR_AFTER, +// Query::TYPE_CURSOR_BEFORE => Base::METHOD_TYPE_CURSOR, +// Query::TYPE_ORDER_ASC, +// Query::TYPE_ORDER_DESC => 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, +// 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; +// } +// +// $methodIsValid = true; +// } +// +// if (!$methodIsValid) { +// $this->message = 'Invalid query method: ' . $method; +// return false; +// } +// } +// +// return true; +// } +// +// /** +// * Is array +// * +// * Function will return true if object is array. +// * +// * @return bool +// */ +// public function isArray(): bool +// { +// return true; +// } +// +// /** +// * Get Type +// * +// * Returns validator type. +// * +// * @return string +// */ +// public function getType(): string +// { +// return self::TYPE_OBJECT; +// } +//} diff --git a/src/Database/Validator/Queries/Document.php b/src/Database/Validator/Queries/Document.php index 41c9f3f9b..8edf478d3 100644 --- a/src/Database/Validator/Queries/Document.php +++ b/src/Database/Validator/Queries/Document.php @@ -1,43 +1,44 @@ $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); - } -} +// +//namespace Utopia\Database\Validator\Queries; +// +//use Exception; +//use Utopia\Database\Database; +//use Utopia\Database\Validator\Queries; +//use Utopia\Database\Validator\Query\Select; +// +//class Document extends Queries +//{ +// /** +// * @param array $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 index abce8694f..6f274101c 100644 --- a/src/Database/Validator/Queries/Documents.php +++ b/src/Database/Validator/Queries/Documents.php @@ -1,73 +1,74 @@ $attributes - * @param array $indexes - * @throws Exception - */ - public function __construct( - array $attributes, - array $indexes, - int $maxValuesCount = 100, - \DateTime $minAllowedDate = new \DateTime('0000-01-01'), - \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), - ) { - $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( - $attributes, - $maxValuesCount, - $minAllowedDate, - $maxAllowedDate, - ), - new Order($attributes), - new Select($attributes), - ]; - - parent::__construct($attributes, $indexes, $validators); - } -} +// +//namespace Utopia\Database\Validator\Queries; +// +//use Exception; +//use Utopia\Database\Database; +//use Utopia\Database\Document; +//use Utopia\Database\Validator\IndexedQueries; +//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; +//use Utopia\Database\Validator\Query\Select; +// +//class Documents extends IndexedQueries +//{ +// /** +// * Expression constructor +// * +// * @param array $attributes +// * @param array $indexes +// * @throws Exception +// */ +// public function __construct( +// array $attributes, +// array $indexes, +// int $maxValuesCount = 100, +// \DateTime $minAllowedDate = new \DateTime('0000-01-01'), +// \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), +// ) { +// $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( +// $attributes, +// $maxValuesCount, +// $minAllowedDate, +// $maxAllowedDate, +// ), +// new Order($attributes), +// new Select($attributes), +// ]; +// +// parent::__construct($attributes, $indexes, $validators); +// } +//} diff --git a/src/Database/Validator/Queries/V2.php b/src/Database/Validator/Queries/V2.php new file mode 100644 index 000000000..7be2d8570 --- /dev/null +++ b/src/Database/Validator/Queries/V2.php @@ -0,0 +1,649 @@ + + */ + protected array $schema = []; + + protected int $maxQueriesCount; + + private int $maxValuesCount; + + protected int $maxLimit; + + protected int $maxOffset; + + protected QueryContext $context; + + protected \DateTime $minAllowedDate; + + protected \DateTime $maxAllowedDate; + + /** + * @throws Exception + */ + public function __construct( + QueryContext $context, + int $maxValuesCount = 100, + int $maxQueriesCount = 0, + \DateTime $minAllowedDate = new \DateTime('0000-01-01'), + \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), + int $maxLimit = PHP_INT_MAX, + int $maxOffset = PHP_INT_MAX + ) { + $this->context = $context; + $this->maxQueriesCount = $maxQueriesCount; + $this->maxValuesCount = $maxValuesCount; + $this->maxLimit = $maxLimit; + $this->maxOffset = $maxOffset; + $this->minAllowedDate = $minAllowedDate; + $this->maxAllowedDate = $maxAllowedDate; + + // $validators = [ + // new Limit(), + // new Offset(), + // new Cursor(), + // new Filter($collections), + // new Order($collections), + // new Select($collections), + // new Join($collections), + // ]; + + /** + * Since $context includes Documents , clone if original data is changes. + */ + foreach ($context->getCollections() as $collection) { + $collection = clone $collection; + + $attributes = $collection->getAttribute('attributes', []); + + $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, + ]); + + foreach ($attributes as $attribute) { + $key = $attribute->getAttribute('key', $attribute->getAttribute('$id')); + $this->schema[$collection->getId()][$key] = $attribute->getArrayCopy(); + } + } + } + + /** + * @param array $value + * + * @throws \Utopia\Database\Exception\Query|\Throwable + */ + public function isValid($value, string $scope = ''): bool + { + try { + if (! is_array($value)) { + throw new \Exception('Queries must be an array'); + } + + if (! array_is_list($value)) { + throw new \Exception('Queries must be an array list'); + } + + if ($this->maxQueriesCount > 0 && \count($value) > $this->maxQueriesCount) { + throw new \Exception('Queries count is greater than '.$this->maxQueriesCount); + } + + $ambiguous = []; + $duplications = []; + foreach ($value as $query) { + if (!$query instanceof Query) { + try { + $query = Query::parse($query); + } catch (\Throwable $e) { + throw new \Exception('Invalid query: ' . $e->getMessage()); + } + } + + //var_dump($query->getMethod(), $query->getCollection(), $query->getAlias()); + + $this->validateAlias($query); + + if ($query->isNested()) { + if (! self::isValid($query->getValues(), $scope)) { + throw new \Exception($this->message); + } + } + + $method = $query->getMethod(); + + switch ($method) { + case Query::TYPE_EQUAL: + case Query::TYPE_CONTAINS: + if ($this->isEmpty($query->getValues())) { + throw new \Exception('Invalid query: '.\ucfirst($method).' queries require at least one value.'); + } + + $this->validateAttributeExist($query->getAttribute(), $query->getAlias()); + $this->validateValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method); + + break; + 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) { + throw new \Exception('Invalid query: '.\ucfirst($method).' queries require exactly one value.'); + } + + $this->validateAttributeExist($query->getAttribute(), $query->getAlias()); + $this->validateValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method); + $this->validateFulltextIndex($query); + + break; + case Query::TYPE_BETWEEN: + if (count($query->getValues()) != 2) { + throw new \Exception('Invalid query: '.\ucfirst($method).' queries require exactly two values.'); + } + + $this->validateAttributeExist($query->getAttribute(), $query->getAlias()); + $this->validateValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method); + + break; + case Query::TYPE_IS_NULL: + case Query::TYPE_IS_NOT_NULL: + $this->validateAttributeExist($query->getAttribute(), $query->getAlias()); + $this->validateValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method); + + break; + case Query::TYPE_OR: + case Query::TYPE_AND: + $this->validateFilterQueries($query); + + $filters = Query::getFilterQueries($query->getValues()); + + if (count($filters) < 2) { + throw new \Exception('Invalid query: '.\ucfirst($method).' queries require at least two queries'); + } + + break; + case Query::TYPE_INNER_JOIN: + case Query::TYPE_LEFT_JOIN: + case Query::TYPE_RIGHT_JOIN: + $this->validateFilterQueries($query); + + if (! self::isValid($query->getValues(), 'joins')) { + throw new \Exception($this->message); + } + + if (! $this->isRelationExist($query->getValues(), $query->getAlias())) { + throw new \Exception('Invalid query: At least one relation query is required on the joined collection.'); + } + + /** + * todo:to all queries which uses aliases check that it is available in context scope, not just exists + */ + break; + case Query::TYPE_RELATION_EQUAL: + if ($scope !== 'joins') { + throw new \Exception('Invalid query: Relations are only valid within joins.'); + } + + $this->validateAttributeExist($query->getAttribute(), $query->getAlias()); + $this->validateAttributeExist($query->getAttributeRight(), $query->getRightAlias()); + + break; + case Query::TYPE_LIMIT: + $validator = new Limit($this->maxLimit); + if (! $validator->isValid($query)) { + throw new \Exception($validator->getDescription()); + } + + break; + case Query::TYPE_OFFSET: + $validator = new Offset($this->maxOffset); + if (! $validator->isValid($query)) { + throw new \Exception($validator->getDescription()); + } + + break; + case Query::TYPE_SELECT: + $validator = new AsValidator($query->getAttribute()); + + if (! $validator->isValid($query->getAs())) { + throw new \Exception('Invalid Query Select: '.$validator->getDescription()); + } + + $this->validateSelect($query); + + if($query->getAttribute() === '*'){ + $collection = $this->context->getCollectionByAlias($query->getAlias()); + $attributes = $this->schema[$collection->getId()]; + foreach ($attributes as $attribute){ + if (($duplications[$query->getAlias()][$attribute['$id']] ?? false) === true){ + //throw new \Exception('Ambiguous column using "*" for "'.$query->getAlias().'.'.$attribute['$id'].'"'); + } + + $duplications[$query->getAlias()][$attribute['$id']] = true; + } + } else { + if (($duplications[$query->getAlias()][$query->getAttribute()] ?? false) === true){ + //throw new \Exception('Duplicate Query Select on "'.$query->getAlias().'.'.$query->getAttribute().'"'); + } + $duplications[$query->getAlias()][$query->getAttribute()] = true; + } + + if (!empty($query->getAs())){ + $needle = $query->getAs(); + } else { + $needle = $query->getAttribute(); // todo: convert internal attribute from $id => _id + } + + if (in_array($needle, $ambiguous)){ + //throw new \Exception('Invalid Query Select: ambiguous column "'.$needle.'"'); + } + + $ambiguous[] = $needle; + + break; + case Query::TYPE_ORDER_ASC: + case Query::TYPE_ORDER_DESC: + $this->validateAttributeExist($query->getAttribute(), $query->getAlias()); + + break; + case Query::TYPE_CURSOR_AFTER: + case Query::TYPE_CURSOR_BEFORE: + $validator = new Cursor(); + if (! $validator->isValid($query)) { + throw new \Exception($validator->getDescription()); + } + + break; + default: + throw new \Exception('Invalid query: Method not found '); + } + } + + } catch (\Throwable $e) { + $this->message = $e->getMessage(); + var_dump($this->message); + var_dump($e); + + return false; + } + + return true; + } + + /** + * 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; + } + + /** + * @throws \Exception + */ + protected function validateAttributeExist(string $attributeId, string $alias): void + { + /** + * This is for making query::select('$permissions')) pass + */ + if($attributeId === '$permissions' || $attributeId === '$collection'){ + return; + } + + var_dump('=== validateAttributeExist'); + + // 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; + // } + // } + + $collection = $this->context->getCollectionByAlias($alias); + if ($collection->isEmpty()) { + throw new \Exception('Invalid query: Unknown Alias context'); + } + + if (! isset($this->schema[$collection->getId()][$attributeId])) { + throw new \Exception('Invalid query: Attribute not found in schema: '.$attributeId); + } + } + + /** + * @throws \Exception + */ + protected function validateAlias(Query $query): void + { + $validator = new AliasValidator(); + + if (! $validator->isValid($query->getAlias())) { + throw new \Exception('Query '.\ucfirst($query->getMethod()).': '.$validator->getDescription()); + } + + if (! $validator->isValid($query->getRightAlias())) { + throw new \Exception('Query '.\ucfirst($query->getMethod()).': '.$validator->getDescription()); + } + } + + /** + * @throws \Exception + */ + protected function validateFilterQueries(Query $query): void + { + $filters = Query::getFilterQueries($query->getValues()); + + if (count($query->getValues()) !== count($filters)) { + throw new \Exception('Invalid query: '.\ucfirst($query->getMethod()).' queries can only contain filter queries'); + } + } + + /** + * @param string $attributeId + * @param string $alias + * @param array $values + * @param string $method + * @return void + * @throws \Exception + */ + protected function validateValues(string $attributeId, string $alias, array $values, string $method): void + { + if (count($values) > $this->maxValuesCount) { + throw new \Exception('Invalid query: Query on attribute has greater than '.$this->maxValuesCount.' values: '.$attributeId); + } + + $collection = $this->context->getCollectionByAlias($alias); + if ($collection->isEmpty()) { + throw new \Exception('Unknown Alias context'); + } + + $attribute = $this->schema[$collection->getId()][$attributeId]; + + $array = $attribute['array'] ?? false; + $filters = $attribute['filters'] ?? []; + + 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( + min: $this->minAllowedDate, + max: $this->maxAllowedDate + ); + break; + + case Database::VAR_RELATIONSHIP: + $validator = new Text(255, 0); // The query is always on uid + break; + + default: + throw new \Exception('Unknown Data type'); + } + + if (! $validator->isValid($value)) { + throw new \Exception('Invalid query: Query value is invalid for attribute "'.$attributeId.'"'); + } + } + + 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) { + throw new \Exception('Cannot query on virtual relationship attribute'); + } + + if ($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { + throw new \Exception('Cannot query on virtual relationship attribute'); + } + + if ($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { + throw new \Exception('Cannot query on virtual relationship attribute'); + } + + if ($options['relationType'] === Database::RELATION_MANY_TO_MANY) { + throw new \Exception('Cannot query on virtual relationship attribute'); + } + } + + if ( + ! $array && + $method === Query::TYPE_CONTAINS && + $attribute['type'] !== Database::VAR_STRING + ) { + throw new \Exception('Invalid query: Cannot query contains on attribute "'.$attributeId.'" because it is not an array or string.'); + } + + if ( + $array && + ! in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL]) + ) { + throw new \Exception('Invalid query: Cannot query '.$method.' on attribute "'.$attributeId.'" because it is an array.'); + } + + if (Query::isFilter($method) && \in_array('encrypt', $filters)) { + throw new \Exception('Cannot query encrypted attribute: ' . $attributeId); + } + } + + /** + * @throws \Exception + */ + public function validateSelect(Query $query): void + { + $asValidator = new AsValidator($query->getAttribute()); + if (! $asValidator->isValid($query->getAs())) { + throw new \Exception('Query '.\ucfirst($query->getMethod()).': '.$asValidator->getDescription()); + } + + $internalKeys = \array_map( + fn ($attr) => $attr['$id'], + Database::INTERNAL_ATTRIBUTES + ); + + $attribute = $query->getAttribute(); + + if ($attribute === '*') { + return; + } + + if (\in_array($attribute, $internalKeys)) { + //return; + } + + $alias = $query->getAlias(); + + if (\str_contains($attribute, '.')) { + if (\str_contains($attribute, '.')) { + try { + /** + * Special symbols with `dots` + */ + $this->validateAttributeExist($attribute, $alias); + } catch (\Throwable $e) { + /** + * For relationships, just validate the top level. + * Will validate each nested level during the recursive calls. + */ + $attribute = \explode('.', $attribute)[0]; + $this->validateAttributeExist($attribute, $alias); + } + } + } + + $this->validateAttributeExist($attribute, $alias); + } + + /** + * @throws \Exception + */ + public function validateFulltextIndex(Query $query): void + { + if ($query->getMethod() !== Query::TYPE_SEARCH) { + return; + } + + $collection = $this->context->getCollectionByAlias($query->getAlias()); + if ($collection->isEmpty()) { + throw new \Exception('Unknown Alias context'); + } + + $indexes = $collection->getAttribute('indexes', []); + + foreach ($indexes as $index) { + if ( + $index->getAttribute('type') === Database::INDEX_FULLTEXT && + $index->getAttribute('attributes') === [$query->getAttribute()] + ) { + return; + } + } + + throw new \Exception('Searching by attribute "'.$query->getAttribute().'" requires a fulltext index.'); + } + + /** + * @param array $queries + * @param string $alias + * @return bool + */ + public function isRelationExist(array $queries, string $alias): bool + { + /** + * Do we want to validate only top lever or nesting as well? + */ + foreach ($queries as $query) { + if ($query->isNested()) { + if ($this->isRelationExist($query->getValues(), $alias)) { + return true; + } + } + + if ($query->getMethod() === Query::TYPE_RELATION_EQUAL) { + if ($query->getAlias() === $alias || $query->getRightAlias() === $alias) { + return true; + } + } + } + + return false; + } +} 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/Filter.php b/src/Database/Validator/Query/Filter.php index 9fb3fe32e..cce81ebcf 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -1,289 +1,289 @@ - */ - protected array $schema = []; - - /** - * @param array $attributes - * @param int $maxValuesCount - * @param \DateTime $minAllowedDate - * @param \DateTime $maxAllowedDate - */ - public function __construct( - array $attributes = [], - private readonly int $maxValuesCount = 100, - private readonly \DateTime $minAllowedDate = new \DateTime('0000-01-01'), - private readonly \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), - ) { - 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 - { - if ( - \in_array('encrypt', $this->schema[$attribute]['filters'] ?? []) - ) { - $this->message = 'Cannot query encrypted attribute: ' . $attribute; - return false; - } - - 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 - * @param string $method - * @return bool - */ - protected function isValidAttributeAndValues(string $attribute, array $values, string $method): 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) { - $validator = null; - - switch ($attributeType) { - 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( - min: $this->minAllowedDate, - max: $this->maxAllowedDate - ); - 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 "' . $attribute . '"'; - return false; - } - } - - if ($attributeSchema['type'] === 'relationship') { - /** - * We can not disable relationship query since we have logic that use it, - * so instead we validate against the relation type - */ - $options = $attributeSchema['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 = $attributeSchema['array'] ?? false; - - if ( - !$array && - $method === Query::TYPE_CONTAINS && - $attributeSchema['type'] !== Database::VAR_STRING - ) { - $this->message = 'Cannot query contains on attribute "' . $attribute . '" 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 "' . $attribute . '" because it is an array.'; - 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(), $method); - - 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(), $method); - - 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(), $method); - - case Query::TYPE_IS_NULL: - case Query::TYPE_IS_NOT_NULL: - return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method); - - case Query::TYPE_OR: - case Query::TYPE_AND: - $filters = Query::groupByType($value->getValues())['filters']; - - if (count($value->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; - - default: - return false; - } - } - - public function getMethodType(): string - { - return self::METHOD_TYPE_FILTER; - } -} +// +//namespace Utopia\Database\Validator\Query; +// +//use Utopia\Database\Database; +//use Utopia\Database\Document; +//use Utopia\Database\Query; +//use Utopia\Database\Validator\Datetime as DatetimeValidator; +//use Utopia\Validator\Boolean; +//use Utopia\Validator\FloatValidator; +//use Utopia\Validator\Integer; +//use Utopia\Validator\Text; +// +//class Filter extends Base +//{ +// /** +// * @var array +// */ +// protected array $schema = []; +// +// /** +// * @param array $attributes +// * @param int $maxValuesCount +// * @param \DateTime $minAllowedDate +// * @param \DateTime $maxAllowedDate +// */ +// public function __construct( +// array $attributes = [], +// private readonly int $maxValuesCount = 100, +// private readonly \DateTime $minAllowedDate = new \DateTime('0000-01-01'), +// private readonly \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), +// ) { +// 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 +// { +// if ( +// \in_array('encrypt', $this->schema[$attribute]['filters'] ?? []) +// ) { +// $this->message = 'Cannot query encrypted attribute: ' . $attribute; +// return false; +// } +// +// 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 +// * @param string $method +// * @return bool +// */ +// protected function isValidAttributeAndValues(string $attribute, array $values, string $method): 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) { +// $validator = null; +// +// switch ($attributeType) { +// 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( +// min: $this->minAllowedDate, +// max: $this->maxAllowedDate +// ); +// 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 "' . $attribute . '"'; +// return false; +// } +// } +// +// if ($attributeSchema['type'] === 'relationship') { +// /** +// * We can not disable relationship query since we have logic that use it, +// * so instead we validate against the relation type +// */ +// $options = $attributeSchema['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 = $attributeSchema['array'] ?? false; +// +// if ( +// !$array && +// $method === Query::TYPE_CONTAINS && +// $attributeSchema['type'] !== Database::VAR_STRING +// ) { +// $this->message = 'Cannot query contains on attribute "' . $attribute . '" 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 "' . $attribute . '" because it is an array.'; +// 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(), $method); +// +// 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(), $method); +// +// 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(), $method); +// +// case Query::TYPE_IS_NULL: +// case Query::TYPE_IS_NOT_NULL: +// return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method); +// +// case Query::TYPE_OR: +// case Query::TYPE_AND: +// $filters = Query::getFilterQueries($value->getValues()); +// +// if (count($value->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; +// +// default: +// return false; +// } +// } +// +// public function getMethodType(): string +// { +// return self::METHOD_TYPE_FILTER; +// } +//} diff --git a/src/Database/Validator/Query/Offset.php b/src/Database/Validator/Query/Offset.php index 8d59be4d0..8b302be47 100644 --- a/src/Database/Validator/Query/Offset.php +++ b/src/Database/Validator/Query/Offset.php @@ -39,7 +39,7 @@ public function isValid($value): bool $validator = new Numeric(); if (!$validator->isValid($offset)) { - $this->message = 'Invalid limit: ' . $validator->getDescription(); + $this->message = 'Invalid offset: ' . $validator->getDescription(); return false; } diff --git a/src/Database/Validator/Query/Order.php b/src/Database/Validator/Query/Order.php index f0e7f2d56..003374864 100644 --- a/src/Database/Validator/Query/Order.php +++ b/src/Database/Validator/Query/Order.php @@ -1,70 +1,74 @@ - */ - 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_ORDER_ASC || $method === Query::TYPE_ORDER_DESC) { - return $this->isValidAttribute($attribute); - } - - return false; - } - - public function getMethodType(): string - { - return self::METHOD_TYPE_ORDER; - } -} +// +//namespace Utopia\Database\Validator\Query; +// +//use Utopia\Database\Document; +//use Utopia\Database\Query; +// +//class Order extends Base +//{ +// /** +// * @var array +// */ +// 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_ORDER_ASC || $method === Query::TYPE_ORDER_DESC) { +// 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 index 4233a492f..1d05e1b9e 100644 --- a/src/Database/Validator/Query/Select.php +++ b/src/Database/Validator/Query/Select.php @@ -1,104 +1,95 @@ - */ - protected array $schema = []; - - /** - * List of internal attributes - * - * @var array - */ - protected const INTERNAL_ATTRIBUTES = [ - '$id', - '$internalId', - '$createdAt', - '$updatedAt', - '$permissions', - '$collection', - ]; - - /** - * @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; - } - - $internalKeys = \array_map( - fn ($attr) => $attr['$id'], - Database::INTERNAL_ATTRIBUTES - ); - - if (\count($value->getValues()) === 0) { - $this->message = 'No attributes selected'; - return false; - } - - if (\count($value->getValues()) !== \count(\array_unique($value->getValues()))) { - $this->message = 'Duplicate attributes selected'; - 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]; - } - - // Skip internal attributes - if (\in_array($attribute, $internalKeys)) { - continue; - } - - 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; - } -} +// +//namespace Utopia\Database\Validator\Query; +// +//use Utopia\Database\Database; +//use Utopia\Database\Document; +//use Utopia\Database\Query; +// +//class Select extends Base +//{ +// /** +// * @var array +// */ +// protected array $schema = []; +// +// /** +// * List of internal attributes +// * +// * @var array +// */ +// protected const INTERNAL_ATTRIBUTES = [ +// '$id', +// '$internalId', +// '$createdAt', +// '$updatedAt', +// '$permissions', +// '$collection', +// ]; +// +// /** +// * @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; +// } +// +// $internalKeys = \array_map( +// fn ($attr) => $attr['$id'], +// Database::INTERNAL_ATTRIBUTES +// ); +// +// 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]; +// } +// +// // Skip internal attributes +// if (\in_array($attribute, $internalKeys)) { +// continue; +// } +// +// 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/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index a57fe2748..ca9785c6d 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -8,6 +8,7 @@ use Tests\E2E\Adapter\Scopes\DocumentTests; use Tests\E2E\Adapter\Scopes\GeneralTests; use Tests\E2E\Adapter\Scopes\IndexTests; +use Tests\E2E\Adapter\Scopes\JoinsTests; use Tests\E2E\Adapter\Scopes\PermissionTests; use Tests\E2E\Adapter\Scopes\RelationshipTests; use Utopia\Database\Database; @@ -17,6 +18,7 @@ abstract class Base extends TestCase { + //use JoinsTests; use CollectionTests; use DocumentTests; use AttributeTests; diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index cab177400..2416fb5a7 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -219,7 +219,7 @@ public function testAttributeNamesWithDots(): void )); $document = static::getDatabase()->find('dots.parent', [ - Query::select(['dots.name']), + Query::select('dots.name'), ]); $this->assertEmpty($document); @@ -260,7 +260,7 @@ public function testAttributeNamesWithDots(): void ])); $documents = static::getDatabase()->find('dots.parent', [ - Query::select(['*']), + Query::select('*'), ]); $this->assertEquals('Bill clinton', $documents[0]['dots.name']); @@ -1598,6 +1598,9 @@ public function testCreateDatetime(): void 'Tue Dec 31 2024', ]; + /** + * ConvertQueries method will fix the dates + */ foreach ($validDates as $date) { $docs = static::getDatabase()->find('datetime', [ Query::equal('$createdAt', [$date]) diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index cd4db3959..3a8e5ec32 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -16,6 +16,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; +use Utopia\Database\QueryContext; use Utopia\Database\Validator\Authorization; trait DocumentTests @@ -772,9 +773,10 @@ public function testGetDocumentSelect(Document $document): Document $documentId = $document->getId(); $document = static::getDatabase()->getDocument('documents', $documentId, [ - Query::select(['string', 'integer_signed']), + Query::select('string'), + Query::select('integer_signed'), ]); - +var_dump($document); $this->assertEmpty($document->getId()); $this->assertFalse($document->isEmpty()); $this->assertIsString($document->getAttribute('string')); @@ -789,22 +791,26 @@ public function testGetDocumentSelect(Document $document): Document $this->assertArrayNotHasKey('$internalId', $document); $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); - $this->assertArrayNotHasKey('$permissions', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$permissions', $document); + $this->assertArrayHasKey('$collection', $document); $document = static::getDatabase()->getDocument('documents', $documentId, [ - Query::select(['string', 'integer_signed', '$id']), + Query::select('string'), + Query::select('integer_signed'), + Query::select('$id'), ]); $this->assertArrayHasKey('$id', $document); $this->assertArrayNotHasKey('$internalId', $document); $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); - $this->assertArrayNotHasKey('$permissions', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$permissions', $document); + $this->assertArrayHasKey('$collection', $document); $document = static::getDatabase()->getDocument('documents', $documentId, [ - Query::select(['string', 'integer_signed', '$permissions']), + Query::select('string'), + Query::select('integer_signed'), + Query::select('$permissions'), ]); $this->assertArrayNotHasKey('$id', $document); @@ -812,51 +818,59 @@ public function testGetDocumentSelect(Document $document): Document $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); $this->assertArrayHasKey('$permissions', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$collection', $document); $document = static::getDatabase()->getDocument('documents', $documentId, [ - Query::select(['string', 'integer_signed', '$internalId']), + Query::select('string'), + Query::select('integer_signed'), + Query::select('$internalId'), ]); $this->assertArrayNotHasKey('$id', $document); $this->assertArrayHasKey('$internalId', $document); $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); - $this->assertArrayNotHasKey('$permissions', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$permissions', $document); + $this->assertArrayHasKey('$collection', $document); $document = static::getDatabase()->getDocument('documents', $documentId, [ - Query::select(['string', 'integer_signed', '$collection']), + Query::select('string'), + Query::select('integer_signed'), + Query::select('$collection'), ]); $this->assertArrayNotHasKey('$id', $document); $this->assertArrayNotHasKey('$internalId', $document); $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); - $this->assertArrayNotHasKey('$permissions', $document); + $this->assertArrayHasKey('$permissions', $document); $this->assertArrayHasKey('$collection', $document); $document = static::getDatabase()->getDocument('documents', $documentId, [ - Query::select(['string', 'integer_signed', '$createdAt']), + Query::select('string'), + Query::select('integer_signed'), + Query::select('$createdAt'), ]); $this->assertArrayNotHasKey('$id', $document); $this->assertArrayNotHasKey('$internalId', $document); $this->assertArrayHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); - $this->assertArrayNotHasKey('$permissions', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$permissions', $document); + $this->assertArrayHasKey('$collection', $document); $document = static::getDatabase()->getDocument('documents', $documentId, [ - Query::select(['string', 'integer_signed', '$updatedAt']), + Query::select('string'), + Query::select('integer_signed'), + Query::select('$updatedAt'), ]); $this->assertArrayNotHasKey('$id', $document); $this->assertArrayNotHasKey('$internalId', $document); $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayHasKey('$updatedAt', $document); - $this->assertArrayNotHasKey('$permissions', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$permissions', $document); + $this->assertArrayHasKey('$collection', $document); return $document; } @@ -1045,7 +1059,8 @@ public function testFind(): array public function testSelectInternalID(): void { $documents = static::getDatabase()->find('movies', [ - Query::select(['$internalId', '$id']), + Query::select('$internalId'), + Query::select('$id'), Query::orderAsc(''), Query::limit(1), ]); @@ -1053,14 +1068,20 @@ public function testSelectInternalID(): void $document = $documents[0]; $this->assertArrayHasKey('$internalId', $document); - $this->assertCount(2, $document); + $this->assertArrayHasKey('$id', $document); + $this->assertArrayNotHasKey('$permissions', $document); + $this->assertArrayHasKey('$collection', $document); + $this->assertCount(3, $document); $document = static::getDatabase()->getDocument('movies', $document->getId(), [ - Query::select(['$internalId']), + Query::select('$internalId'), ]); $this->assertArrayHasKey('$internalId', $document); - $this->assertCount(1, $document); + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayHasKey('$permissions', $document); + $this->assertArrayHasKey('$collection', $document); + $this->assertCount(3, $document); } @@ -2203,7 +2224,7 @@ public function testOrMultipleQueries(): void public function testOrNested(): void { $queries = [ - Query::select(['director']), + Query::select('director'), Query::equal('director', ['Joe Johnston']), Query::or([ Query::equal('name', ['Frozen']), @@ -2388,7 +2409,8 @@ public function testFindEndsWith(): void public function testFindSelect(): void { $documents = static::getDatabase()->find('movies', [ - Query::select(['name', 'year']) + Query::select('name'), + Query::select('year') ]); foreach ($documents as $document) { @@ -2399,14 +2421,16 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('active', $document); $this->assertArrayNotHasKey('$id', $document); $this->assertArrayNotHasKey('$internalId', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$collection', $document); $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); $this->assertArrayNotHasKey('$permissions', $document); } $documents = static::getDatabase()->find('movies', [ - Query::select(['name', 'year', '$id']) + Query::select('name'), + Query::select('year'), + Query::select('$id') ]); foreach ($documents as $document) { @@ -2417,14 +2441,16 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('active', $document); $this->assertArrayHasKey('$id', $document); $this->assertArrayNotHasKey('$internalId', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$collection', $document); $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); $this->assertArrayNotHasKey('$permissions', $document); } $documents = static::getDatabase()->find('movies', [ - Query::select(['name', 'year', '$internalId']) + Query::select('name'), + Query::select('year'), + Query::select('$internalId') ]); foreach ($documents as $document) { @@ -2435,14 +2461,16 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('active', $document); $this->assertArrayNotHasKey('$id', $document); $this->assertArrayHasKey('$internalId', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$collection', $document); $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); $this->assertArrayNotHasKey('$permissions', $document); } $documents = static::getDatabase()->find('movies', [ - Query::select(['name', 'year', '$collection']) + Query::select('name'), + Query::select('year'), + Query::select('$collection') ]); foreach ($documents as $document) { @@ -2460,7 +2488,9 @@ public function testFindSelect(): void } $documents = static::getDatabase()->find('movies', [ - Query::select(['name', 'year', '$createdAt']) + Query::select('name'), + Query::select('year'), + Query::select('$createdAt') ]); foreach ($documents as $document) { @@ -2471,14 +2501,16 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('active', $document); $this->assertArrayNotHasKey('$id', $document); $this->assertArrayNotHasKey('$internalId', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$collection', $document); $this->assertArrayHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); $this->assertArrayNotHasKey('$permissions', $document); } $documents = static::getDatabase()->find('movies', [ - Query::select(['name', 'year', '$updatedAt']) + Query::select('name'), + Query::select('year'), + Query::select('$updatedAt') ]); foreach ($documents as $document) { @@ -2489,14 +2521,16 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('active', $document); $this->assertArrayNotHasKey('$id', $document); $this->assertArrayNotHasKey('$internalId', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$collection', $document); $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayHasKey('$updatedAt', $document); $this->assertArrayNotHasKey('$permissions', $document); } $documents = static::getDatabase()->find('movies', [ - Query::select(['name', 'year', '$permissions']) + Query::select('name'), + Query::select('year'), + Query::select('$permissions') ]); foreach ($documents as $document) { @@ -2507,7 +2541,7 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('active', $document); $this->assertArrayNotHasKey('$id', $document); $this->assertArrayNotHasKey('$internalId', $document); - $this->assertArrayNotHasKey('$collection', $document); + $this->assertArrayHasKey('$collection', $document); $this->assertArrayNotHasKey('$createdAt', $document); $this->assertArrayNotHasKey('$updatedAt', $document); $this->assertArrayHasKey('$permissions', $document); @@ -2849,7 +2883,10 @@ public function testEncodeDecode(): void $this->assertEquals(['admin', 'developer', 'tester',], $result->getAttribute('roles')); $this->assertEquals(['{"$id":"1","label":"x"}', '{"$id":"2","label":"y"}', '{"$id":"3","label":"z"}',], $result->getAttribute('tags')); - $result = static::getDatabase()->decode($collection, $document); + $context = new QueryContext(); + $context->add($collection); + + $result = static::getDatabase()->decode($context, $document); $this->assertEquals('608fdbe51361a', $result->getAttribute('$id')); $this->assertContains('read("any")', $result->getAttribute('$permissions')); @@ -3458,12 +3495,13 @@ public function testDeleteBulkDocuments(): void /** * Test Short select query, test pagination as well, Add order to select */ - $selects = ['$internalId', '$id', '$collection', '$permissions', '$updatedAt']; + $mandatory = ['$internalId', '$id', '$collection', '$permissions', '$updatedAt']; $count = static::getDatabase()->deleteDocuments( collection: 'bulk_delete', queries: [ - Query::select([...$selects, '$createdAt']), + Query::select('$createdAt'), + ...array_map(fn ($f) => Query::select($f), $mandatory), Query::cursorAfter($docs[6]), Query::greaterThan('$createdAt', '2000-01-01'), Query::orderAsc('$createdAt'), diff --git a/tests/e2e/Adapter/Scopes/JoinsTests.php b/tests/e2e/Adapter/Scopes/JoinsTests.php new file mode 100644 index 000000000..ce238a42a --- /dev/null +++ b/tests/e2e/Adapter/Scopes/JoinsTests.php @@ -0,0 +1,565 @@ +getAdapter()->getSupportForRelationships()) { + $this->expectNotToPerformAssertions(); + return; + } + + //Authorization::setRole('user:bob'); + + $db->createCollection('__users'); + $db->createCollection('__sessions'); + + $db->createAttribute('__users', 'username', Database::VAR_STRING, 100, false); + $db->createAttribute('__sessions', 'user_id', Database::VAR_STRING, 100, false); + $db->createAttribute('__sessions', 'boolean', Database::VAR_BOOLEAN, 0, false); + $db->createAttribute('__sessions', 'float', Database::VAR_FLOAT, 0, false); + + $user1 = $db->createDocument('__users', new Document([ + 'username' => 'Donald', + '$permissions' => [ + Permission::read(Role::any()), + Permission::read(Role::user('bob')), + ], + ])); + + $sessionNoPermissions = $db->createDocument('__sessions', new Document([ + 'user_id' => $user1->getId(), + '$permissions' => [], + ])); + + /** + * Test $session1 does not have read permissions + * Test right attribute is internal attribute + */ + $documents = $db->find( + '__users', + [ + Query::join('__sessions', 'B', + [ + Query::relationEqual('B', 'user_id', '', '$id'), + ] + ), + ] + ); + $this->assertCount(0, $documents); + + $session2 = $db->createDocument('__sessions', new Document([ + 'user_id' => $user1->getId(), + '$permissions' => [ + Permission::read(Role::any()), + ], + 'boolean' => false, + 'float' => 10.5, + ])); + + $user2 = $db->createDocument('__users', new Document([ + 'username' => 'Abraham', + '$permissions' => [ + Permission::read(Role::any()), + Permission::read(Role::user('bob')), + ], + ])); + + $session3 = $db->createDocument('__sessions', new Document([ + 'user_id' => $user2->getId(), + '$permissions' => [ + Permission::read(Role::any()), + ], + 'boolean' => true, + 'float' => 5.5, + ])); + + /** + * Test $session2 has read permissions + * Test right attribute is internal attribute + */ + $documents = $db->find( + '__users', + [ + Query::join('__sessions', 'B', + [ + Query::relationEqual('B', 'user_id', '', '$id'), + ] + ), + ] + ); + $this->assertCount(2, $documents); + + $documents = $db->find( + '__users', + [ + Query::join('__sessions', 'B', + [ + Query::relationEqual('B', 'user_id', '', '$id'), + Query::equal('user_id', [$user1->getId()], 'B'), + ] + ), + ] + ); + $this->assertCount(1, $documents); + + /** + * Test alias does not exist + */ + try { + $db->find( + '__sessions', + [ + Query::equal('user_id', ['bob'], 'alias_not_found') + ] + ); + $this->fail('Failed to throw exception'); + } catch (\Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Invalid query: Unknown Alias context', $e->getMessage()); + } + + /** + * Test Ambiguous alias + */ + try { + $db->find( + '__users', + [ + Query::join('__sessions', Query::DEFAULT_ALIAS, []), + ] + ); + $this->fail('Failed to throw exception'); + } catch (\Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Ambiguous alias for collection "__sessions".', $e->getMessage()); + } + + /** + * Test relation query exist, but not on the join alias + */ + try { + $db->find( + '__users', + [ + Query::join('__sessions', 'B', + [ + Query::relationEqual('', '$id', '', '$id'), + ] + ), + ] + ); + $this->fail('Failed to throw exception'); + } catch (\Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Invalid query: At least one relation query is required on the joined collection.', $e->getMessage()); + } + + /** + * Test if relation query exists in the join queries list + */ + try { + $db->find( + '__users', + [ + Query::join('__sessions', 'B', []), + ] + ); + $this->fail('Failed to throw exception'); + } catch (\Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Invalid query: At least one relation query is required on the joined collection.', $e->getMessage()); + } + + /** + * Test allow only filter queries in joins ON clause + */ + try { + $db->find( + '__users', + [ + Query::join('__sessions', 'B', [ + Query::orderAsc() + ]), + ] + ); + $this->fail('Failed to throw exception'); + } catch (\Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Invalid query: InnerJoin queries can only contain filter queries', $e->getMessage()); + } + + /** + * Test Relations are valid within joins + */ + try { + $db->find( + '__users', + [ + Query::relationEqual('', '$id', '', '$internalId'), + ] + ); + $this->fail('Failed to throw exception'); + } catch (\Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Invalid query: Relations are only valid within joins.', $e->getMessage()); + } + + /** + * Test invalid alias name + */ + try { + $alias = 'drop schema;'; + $db->find( + '__users', + [ + Query::join('__sessions', $alias, + [ + Query::relationEqual($alias, 'user_id', '', '$id'), + ] + ), + ] + ); + $this->fail('Failed to throw exception'); + } catch (\Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Query InnerJoin: Alias must contain at most 64 chars. Valid chars are a-z, A-Z, 0-9, and underscore.', $e->getMessage()); + } + + /** + * Test join same collection + */ + $documents = $db->find( + '__users', + [ + Query::join('__sessions', 'B', + [ + Query::relationEqual('B', 'user_id', '', '$id'), + ] + ), + Query::join('__sessions', 'C', + [ + Query::relationEqual('C', 'user_id', 'B', 'user_id'), + ] + ), + ] + ); + $this->assertCount(2, $documents); + + /** + * Test order by related collection + */ + $documents = $db->find( + '__users', + [ + Query::join('__sessions', 'B', + [ + Query::relationEqual('B', 'user_id', '', '$id'), + ] + ), + Query::orderAsc('$createdAt', 'B') + ] + ); + $this->assertEquals('Donald', $documents[0]['username']); + $this->assertEquals('Abraham', $documents[1]['username']); + + $documents = $db->find( + '__users', + [ + Query::join('__sessions', 'B', + [ + Query::relationEqual('B', 'user_id', '', '$id'), + ] + ), + Query::orderDesc('$createdAt', 'B') + ] + ); + $this->assertEquals('Abraham', $documents[0]['username']); + $this->assertEquals('Donald', $documents[1]['username']); + + /** + * Select queries + */ + $documents = $db->find( + '__users', + [ + Query::select('*', 'main'), + Query::select('user_id', 'S'), + Query::select('float', 'S'), + Query::select('boolean', 'S'), + Query::join('__sessions', 'S', + [ + Query::relationEqual('', '$id', 'S', 'user_id'), + Query::greaterThan('float', 1.1, 'S'), + ] + ), + Query::orderDesc('float', 'S'), + ] + ); + + $document = $documents[0]; + var_dump($document); + + /** + * Since we use main.* we should see all attributes + */ + //$this->assertArrayHasKey('$id', $document); + $this->assertIsFloat($document->getAttribute('float')); + $this->assertEquals(10.5, $document->getAttribute('float')); + + $this->assertIsBool($document->getAttribute('boolean')); + $this->assertEquals(false, $document->getAttribute('boolean')); + //$this->assertIsArray($document->getAttribute('colors')); + //$this->assertEquals(['pink', 'green', 'blue'], $document->getAttribute('colors')); + + /** + * Test invalid as + */ + try { + $db->find('__users', [ + Query::select('$id', as: 'truncate schema;'), + ]); + $this->fail('Failed to throw exception'); + } catch (\Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Invalid Query Select: "as" must contain at most 64 chars. Valid chars are a-z, A-Z, 0-9, and underscore.', $e->getMessage()); + } + + try { + $db->find('__users', [ + Query::select('*', as: 'as'), + ]); + $this->fail('Failed to throw exception'); + } catch (\Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Invalid Query Select: Invalid "as" on attribute "*"', $e->getMessage()); + } + + + $document = $db->getDocument( + '__sessions', + $session2->getId() + ); + var_dump($document); + $this->assertEquals('dsdsd', 'ds'); + + /** + * Simple `as` query getDocument + */ + $document = $db->getDocument( + '__sessions', + $session2->getId(), + [ + Query::select('$permissions', as: '___permissions'), + Query::select('$id', as: '___uid'), + Query::select('$internalId', as: '___id'), + Query::select('$createdAt', as: '___created'), + Query::select('user_id', as: 'user_id_as'), + Query::select('float', as: 'float_as'), + Query::select('boolean', as: 'boolean_as'), + ] + ); + + $this->assertArrayHasKey('$permissions', $document); + $this->assertArrayHasKey('___permissions', $document); + $this->assertArrayHasKey('___uid', $document); + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayHasKey('___id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayHasKey('___created', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayHasKey('user_id_as', $document); + $this->assertArrayNotHasKey('user_id', $document); + $this->assertArrayHasKey('float_as', $document); + $this->assertArrayNotHasKey('float', $document); + $this->assertIsFloat($document->getAttribute('float_as')); + $this->assertEquals(10.5, $document->getAttribute('float_as')); + $this->assertArrayHasKey('boolean_as', $document); + $this->assertArrayNotHasKey('boolean', $document); + $this->assertIsBool($document->getAttribute('boolean_as')); + $this->assertEquals(false, $document->getAttribute('boolean_as')); + + /** + * Simple `as` query getDocument + */ + $document = $db->getDocument( + '__sessions', + $session2->getId(), + [ + Query::select('$permissions', as: '___permissions'), + ] + ); + + $this->assertArrayHasKey('$permissions', $document); + $this->assertArrayHasKey('$collection', $document); + $this->assertArrayHasKey('___permissions', $document); + + /** + * Simple `as` query find + */ + $document = $db->findOne( + '__sessions', + [ + Query::select('$id', as: '___uid'), + Query::select('$internalId', as: '___id'), + Query::select('$createdAt', as: '___created'), + Query::select('user_id', as: 'user_id_as'), + Query::select('float', as: 'float_as'), + Query::select('boolean', as: 'boolean_as'), + ] + ); + + $this->assertArrayHasKey('___uid', $document); + $this->assertArrayNotHasKey('$id', $document); + $this->assertArrayHasKey('___id', $document); + $this->assertArrayNotHasKey('$internalId', $document); + $this->assertArrayHasKey('___created', $document); + $this->assertArrayNotHasKey('$createdAt', $document); + $this->assertArrayHasKey('user_id_as', $document); + $this->assertArrayNotHasKey('user_id', $document); + $this->assertArrayHasKey('float_as', $document); + $this->assertArrayNotHasKey('float', $document); + $this->assertIsFloat($document->getAttribute('float_as')); + $this->assertEquals(10.5, $document->getAttribute('float_as')); + $this->assertArrayHasKey('boolean_as', $document); + $this->assertArrayNotHasKey('boolean', $document); + $this->assertIsBool($document->getAttribute('boolean_as')); + $this->assertEquals(false, $document->getAttribute('boolean_as')); + + /** + * Select queries + */ + $document = $db->findOne( + '__users', + [ + Query::select('username', '', as: 'as_username'), + Query::select('user_id', 'S', as: 'as_user_id'), + Query::select('float', 'S', as: 'as_float'), + Query::select('boolean', 'S', as: 'as_boolean'), + Query::select('$permissions', 'S', as: 'as_permissions'), + Query::join('__sessions', 'S', + [ + Query::relationEqual('', '$id', 'S', 'user_id'), + ] + ) + ] + ); + + $this->assertArrayHasKey('as_username', $document); + $this->assertArrayHasKey('as_user_id', $document); + $this->assertArrayHasKey('as_float', $document); + $this->assertArrayHasKey('as_boolean', $document); + $this->assertArrayHasKey('as_permissions', $document); + $this->assertIsArray($document->getAttribute('as_permissions')); + + + +// /** +// * ambiguous and duplications selects +// */ +// try { +// $db->find( +// '__users', +// [ +// Query::select('$id', 'main'), +// Query::select('$id', 'S'), +// Query::join('__sessions', 'S', +// [ +// Query::relationEqual('', '$id', 'S', 'user_id'), +// ] +// ) +// ] +// ); +// $this->fail('Failed to throw exception'); +// } catch (\Throwable $e) { +// $this->assertTrue($e instanceof QueryException); +// $this->assertEquals('Invalid Query Select: ambiguous column "$id"', $e->getMessage()); +// } +// +// try { +// $db->find( +// '__users', +// [ +// Query::select('*', 'main'), +// Query::select('*', 'S'), +// Query::join('__sessions', 'S', +// [ +// Query::relationEqual('', '$id', 'S', 'user_id'), +// ] +// ) +// ] +// ); +// $this->fail('Failed to throw exception'); +// } catch (\Throwable $e) { +// $this->assertTrue($e instanceof QueryException); +// $this->assertEquals('Invalid Query Select: ambiguous column "*"', $e->getMessage()); +// } +// +// try { +// $db->find('__users', +// [ +// Query::select('$id'), +// Query::select('$id'), +// ] +// ); +// $this->fail('Failed to throw exception'); +// } catch (\Throwable $e) { +// $this->assertTrue($e instanceof QueryException); +// $this->assertEquals('Duplicate Query Select on "main.$id"', $e->getMessage()); +// } +// +// /** +// * This should fail? since 2 _uid attributes will be returned? +// */ +// try { +// $db->find( +// '__users', +// [ +// Query::select('*', 'main'), +// Query::select('$id', 'S'), +// Query::join('__sessions', 'S', +// [ +// Query::relationEqual('', '$id', 'S', 'user_id'), +// ] +// ) +// ] +// ); +// $this->fail('Failed to throw exception'); +// } catch (\Throwable $e) { +// $this->assertTrue($e instanceof QueryException); +// $this->assertEquals('Invalid Query Select: ambiguous column "$id"', $e->getMessage()); +// } + } +} diff --git a/tests/e2e/Adapter/Scopes/RelationshipTests.php b/tests/e2e/Adapter/Scopes/RelationshipTests.php index 47a4aeffd..ceeb5c0af 100644 --- a/tests/e2e/Adapter/Scopes/RelationshipTests.php +++ b/tests/e2e/Adapter/Scopes/RelationshipTests.php @@ -948,7 +948,9 @@ public function testSelectRelationshipAttributes(): void // Select some parent attributes, some child attributes $make = static::getDatabase()->findOne('make', [ - Query::select(['name', 'models.name']), + //Query::select('name'), + Query::select('models.name'), + Query::select('*'), // Added this to make tests pass, perhaps to add in to nesting queries? ]); if ($make->isEmpty()) { @@ -970,7 +972,8 @@ public function testSelectRelationshipAttributes(): void // Select internal attributes $make = static::getDatabase()->findOne('make', [ - Query::select(['name', '$id']), + Query::select('name'), + Query::select('$id') ]); if ($make->isEmpty()) { @@ -985,7 +988,8 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayNotHasKey('$permissions', $make); $make = static::getDatabase()->findOne('make', [ - Query::select(['name', '$internalId']), + Query::select('name'), + Query::select('$internalId') ]); if ($make->isEmpty()) { @@ -1000,7 +1004,8 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayNotHasKey('$permissions', $make); $make = static::getDatabase()->findOne('make', [ - Query::select(['name', '$collection']), + Query::select('name'), + Query::select('$collection'), ]); if ($make->isEmpty()) { @@ -1015,7 +1020,8 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayNotHasKey('$permissions', $make); $make = static::getDatabase()->findOne('make', [ - Query::select(['name', '$createdAt']), + Query::select('name'), + Query::select('$createdAt'), ]); if ($make->isEmpty()) { @@ -1030,7 +1036,8 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayNotHasKey('$permissions', $make); $make = static::getDatabase()->findOne('make', [ - Query::select(['name', '$updatedAt']), + Query::select('name'), + Query::select('$updatedAt'), ]); if ($make->isEmpty()) { @@ -1045,7 +1052,8 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayNotHasKey('$permissions', $make); $make = static::getDatabase()->findOne('make', [ - Query::select(['name', '$permissions']), + Query::select('name'), + Query::select('$permissions'), ]); if ($make->isEmpty()) { @@ -1061,7 +1069,8 @@ public function testSelectRelationshipAttributes(): void // Select all parent attributes, some child attributes $make = static::getDatabase()->findOne('make', [ - Query::select(['*', 'models.year']), + Query::select('*'), + Query::select('models.year') ]); if ($make->isEmpty()) { @@ -1077,7 +1086,8 @@ public function testSelectRelationshipAttributes(): void // Select all parent attributes, all child attributes $make = static::getDatabase()->findOne('make', [ - Query::select(['*', 'models.*']), + Query::select('*'), + Query::select('models.*') ]); if ($make->isEmpty()) { @@ -1094,7 +1104,7 @@ public function testSelectRelationshipAttributes(): void // Select all parent attributes, all child attributes // Must select parent if selecting children $make = static::getDatabase()->findOne('make', [ - Query::select(['models.*']), + Query::select('models.*') ]); if ($make->isEmpty()) { @@ -1110,7 +1120,7 @@ public function testSelectRelationshipAttributes(): void // Select all parent attributes, no child attributes $make = static::getDatabase()->findOne('make', [ - Query::select(['name']), + Query::select('name'), ]); if ($make->isEmpty()) { diff --git a/tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php b/tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php index 20f129718..649a71094 100644 --- a/tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php +++ b/tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php @@ -105,7 +105,7 @@ public function testManyToManyOneWayRelationship(): void $this->assertEquals(1, \count($playlist1Document->getAttribute('songs'))); $documents = static::getDatabase()->find('playlist', [ - Query::select(['name']), + Query::select('name'), Query::limit(1) ]); @@ -135,7 +135,8 @@ public function testManyToManyOneWayRelationship(): void // Select related document attributes $playlist = static::getDatabase()->findOne('playlist', [ - Query::select(['*', 'songs.name']) + Query::select('*'), + Query::select('songs.name') ]); if ($playlist->isEmpty()) { @@ -146,7 +147,8 @@ public function testManyToManyOneWayRelationship(): void $this->assertArrayNotHasKey('length', $playlist->getAttribute('songs')[0]); $playlist = static::getDatabase()->getDocument('playlist', 'playlist1', [ - Query::select(['*', 'songs.name']) + Query::select('*'), + Query::select('songs.name') ]); $this->assertEquals('Song 1', $playlist->getAttribute('songs')[0]->getAttribute('name')); @@ -517,7 +519,8 @@ public function testManyToManyTwoWayRelationship(): void // Select related document attributes $student = static::getDatabase()->findOne('students', [ - Query::select(['*', 'classes.name']) + Query::select('*'), + Query::select('classes.name') ]); if ($student->isEmpty()) { @@ -528,7 +531,8 @@ public function testManyToManyTwoWayRelationship(): void $this->assertArrayNotHasKey('number', $student->getAttribute('classes')[0]); $student = static::getDatabase()->getDocument('students', 'student1', [ - Query::select(['*', 'classes.name']) + Query::select('*'), + Query::select('classes.name') ]); $this->assertEquals('Class 1', $student->getAttribute('classes')[0]->getAttribute('name')); diff --git a/tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php b/tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php index 9ea7d7085..c551af964 100644 --- a/tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php +++ b/tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php @@ -141,7 +141,8 @@ public function testManyToOneOneWayRelationship(): void $this->assertArrayNotHasKey('reviews', $movie); $documents = static::getDatabase()->find('review', [ - Query::select(['date', 'movie.date']) + Query::select('date'), + Query::select('movie.date') ]); $this->assertCount(3, $documents); @@ -172,7 +173,8 @@ public function testManyToOneOneWayRelationship(): void // Select related document attributes $review = static::getDatabase()->findOne('review', [ - Query::select(['*', 'movie.name']) + Query::select('*'), + Query::select('movie.name') ]); if ($review->isEmpty()) { @@ -183,7 +185,8 @@ public function testManyToOneOneWayRelationship(): void $this->assertArrayNotHasKey('length', $review->getAttribute('movie')); $review = static::getDatabase()->getDocument('review', 'review1', [ - Query::select(['*', 'movie.name']) + Query::select('*'), + Query::select('movie.name') ]); $this->assertEquals('Movie 1', $review->getAttribute('movie')->getAttribute('name')); @@ -549,7 +552,8 @@ public function testManyToOneTwoWayRelationship(): void // Select related document attributes $product = static::getDatabase()->findOne('product', [ - Query::select(['*', 'store.name']) + Query::select('*'), + Query::select('store.name') ]); if ($product->isEmpty()) { @@ -560,7 +564,8 @@ public function testManyToOneTwoWayRelationship(): void $this->assertArrayNotHasKey('opensAt', $product->getAttribute('store')); $product = static::getDatabase()->getDocument('product', 'product1', [ - Query::select(['*', 'store.name']) + Query::select('*'), + Query::select('store.name') ]); $this->assertEquals('Store 1', $product->getAttribute('store')->getAttribute('name')); diff --git a/tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php b/tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php index a37ec31db..558eb9336 100644 --- a/tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php +++ b/tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php @@ -113,7 +113,7 @@ public function testOneToManyOneWayRelationship(): void ])); $documents = static::getDatabase()->find('artist', [ - Query::select(['name']), + Query::select('name'), Query::limit(1) ]); $this->assertArrayNotHasKey('albums', $documents[0]); @@ -144,7 +144,8 @@ public function testOneToManyOneWayRelationship(): void // Select related document attributes $artist = static::getDatabase()->findOne('artist', [ - Query::select(['*', 'albums.name']) + Query::select('*'), + Query::select('albums.name') ]); if ($artist->isEmpty()) { @@ -155,7 +156,8 @@ public function testOneToManyOneWayRelationship(): void $this->assertArrayNotHasKey('price', $artist->getAttribute('albums')[0]); $artist = static::getDatabase()->getDocument('artist', 'artist1', [ - Query::select(['*', 'albums.name']) + Query::select('*'), + Query::select('albums.name') ]); $this->assertEquals('Album 1', $artist->getAttribute('albums')[0]->getAttribute('name')); @@ -577,7 +579,8 @@ public function testOneToManyTwoWayRelationship(): void // Select related document attributes $customer = static::getDatabase()->findOne('customer', [ - Query::select(['*', 'accounts.name']) + Query::select('*'), + Query::select('accounts.name') ]); if ($customer->isEmpty()) { @@ -588,7 +591,8 @@ public function testOneToManyTwoWayRelationship(): void $this->assertArrayNotHasKey('number', $customer->getAttribute('accounts')[0]); $customer = static::getDatabase()->getDocument('customer', 'customer1', [ - Query::select(['*', 'accounts.name']) + Query::select('*'), + Query::select('accounts.name') ]); $this->assertEquals('Account 1', $customer->getAttribute('accounts')[0]->getAttribute('name')); @@ -908,21 +912,23 @@ public function testNestedOneToMany_OneToOneRelationship(): void $this->assertEquals('Mayor 1', $documents[0]['cities'][0]['mayor']['name']); $documents = static::getDatabase()->find('countries', [ - Query::select(['name']), + Query::select('name'), Query::limit(1) ]); $this->assertArrayHasKey('name', $documents[0]); $this->assertArrayNotHasKey('cities', $documents[0]); $documents = static::getDatabase()->find('countries', [ - Query::select(['*']), + Query::select('*'), Query::limit(1) ]); $this->assertArrayHasKey('name', $documents[0]); $this->assertArrayNotHasKey('cities', $documents[0]); $documents = static::getDatabase()->find('countries', [ - Query::select(['*', 'cities.*', 'cities.mayor.*']), + Query::select('*'), + Query::select('cities.*'), + Query::select('cities.mayor.*'), Query::limit(1) ]); diff --git a/tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php b/tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php index ee82b9631..6bc3f2d96 100644 --- a/tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php +++ b/tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php @@ -166,7 +166,7 @@ public function testOneToOneOneWayRelationship(): void $this->assertArrayNotHasKey('person', $library); $people = static::getDatabase()->find('person', [ - Query::select(['name']) + Query::select('name') ]); $this->assertArrayNotHasKey('library', $people[0]); @@ -176,7 +176,8 @@ public function testOneToOneOneWayRelationship(): void // Select related document attributes $person = static::getDatabase()->findOne('person', [ - Query::select(['*', 'library.name']) + Query::select('*'), + Query::select('library.name') ]); if ($person->isEmpty()) { @@ -187,7 +188,9 @@ public function testOneToOneOneWayRelationship(): void $this->assertArrayNotHasKey('area', $person->getAttribute('library')); $person = static::getDatabase()->getDocument('person', 'person1', [ - Query::select(['*', 'library.name', '$id']) + Query::select('*'), + Query::select('library.name'), + Query::select('$id') ]); $this->assertEquals('Library 1', $person->getAttribute('library')->getAttribute('name')); @@ -196,18 +199,18 @@ public function testOneToOneOneWayRelationship(): void $document = static::getDatabase()->getDocument('person', $person->getId(), [ - Query::select(['name']), + Query::select('name'), ]); $this->assertArrayNotHasKey('library', $document); $this->assertEquals('Person 1', $document['name']); $document = static::getDatabase()->getDocument('person', $person->getId(), [ - Query::select(['*']), + Query::select('*'), ]); $this->assertEquals('library1', $document['library']); $document = static::getDatabase()->getDocument('person', $person->getId(), [ - Query::select(['library.*']), + Query::select('library.*'), ]); $this->assertEquals('Library 1', $document['library']['name']); $this->assertArrayNotHasKey('name', $document); @@ -652,7 +655,8 @@ public function testOneToOneTwoWayRelationship(): void // Select related document attributes $country = static::getDatabase()->findOne('country', [ - Query::select(['*', 'city.name']) + Query::select('*'), + Query::select('city.name') ]); if ($country->isEmpty()) { @@ -663,7 +667,8 @@ public function testOneToOneTwoWayRelationship(): void $this->assertArrayNotHasKey('code', $country->getAttribute('city')); $country = static::getDatabase()->getDocument('country', 'country1', [ - Query::select(['*', 'city.name']) + Query::select('*'), + Query::select('city.name') ]); $this->assertEquals('London', $country->getAttribute('city')->getAttribute('name')); diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index d9ad6cd93..8eda98b04 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -19,19 +19,21 @@ 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()); @@ -88,7 +90,6 @@ public function testCreate(): void } /** - * @return void * @throws QueryException */ public function testParse(): void @@ -163,10 +164,12 @@ public function testParse(): void $this->assertEquals('director', $query->getAttribute()); $this->assertEquals(['Tarantino'], $query->getValues()); - $query = Query::parse(Query::select(['title', 'director'])->toString()); + $query = Query::parse(Query::select('title', alias: 'alias', as: 'as')->toString()); $this->assertEquals('select', $query->getMethod()); - $this->assertEquals(null, $query->getAttribute()); - $this->assertEquals(['title', 'director'], $query->getValues()); + $this->assertEquals('title', $query->getAttribute()); + $this->assertEquals('alias', $query->getAlias()); + $this->assertEquals('as', $query->getAs()); + //$this->assertEquals(['title', 'director'], $query->getValues()); $query = Query::parse(Query::between('age', 15, 18)->toString()); $this->assertEquals('between', $query->getMethod()); @@ -200,7 +203,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); @@ -289,4 +292,41 @@ 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::relationEqual('', 'id', 'u', 'user_id'), + Query::equal('id', ['usa'], 'u'), + ] + ); + + $this->assertEquals(Query::TYPE_INNER_JOIN, $query->getMethod()); + $this->assertEquals('users', $query->getCollection()); + $this->assertEquals('u', $query->getAlias()); + $this->assertCount(2, $query->getValues()); + + /** @var Query $query0 */ + $query0 = $query->getValues()[0]; + $this->assertEquals(Query::TYPE_RELATION_EQUAL, $query0->getMethod()); + $this->assertEquals(Query::DEFAULT_ALIAS, $query0->getAlias()); + $this->assertEquals('id', $query0->getAttribute()); + $this->assertEquals('u', $query0->getRightAlias()); + $this->assertEquals('user_id', $query0->getAttributeRight()); + + /** @var Query $query1 */ + $query1 = $query->getValues()[1]; + $this->assertEquals(Query::TYPE_EQUAL, $query1->getMethod()); + $this->assertEquals('u', $query1->getAlias()); + $this->assertEquals('id', $query1->getAttribute()); + $this->assertEquals(Query::DEFAULT_ALIAS, $query1->getRightAlias()); + $this->assertEquals('', $query1->getAttributeRight()); + } } diff --git a/tests/unit/Validator/DocumentQueriesTest.php b/tests/unit/Validator/DocumentQueriesTest.php index 558d0b455..b8ac467b5 100644 --- a/tests/unit/Validator/DocumentQueriesTest.php +++ b/tests/unit/Validator/DocumentQueriesTest.php @@ -8,21 +8,19 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; -use Utopia\Database\Validator\Queries\Document as DocumentQueries; +use Utopia\Database\QueryContext; +use Utopia\Database\Validator\Queries\V2 as DocumentsValidator; class DocumentQueriesTest extends TestCase { - /** - * @var array - */ - protected array $collection = []; + protected QueryContext $context; /** * @throws Exception */ public function setUp(): void { - $this->collection = [ + $collection = [ '$collection' => ID::custom(Database::METADATA), '$id' => ID::custom('movies'), 'name' => 'movies', @@ -49,6 +47,13 @@ public function setUp(): void ]) ] ]; + + $collection = new Document($collection); + + $context = new QueryContext(); + $context->add($collection); + + $this->context = $context; } public function tearDown(): void @@ -60,15 +65,15 @@ public function tearDown(): void */ public function testValidQueries(): void { - $validator = new DocumentQueries($this->collection['attributes']); + $validator = new DocumentsValidator($this->context); $queries = [ - Query::select(['title']), + Query::select('title'), ]; $this->assertEquals(true, $validator->isValid($queries)); - $queries[] = Query::select(['price.relation']); + $queries[] = Query::select('price.relation'); $this->assertEquals(true, $validator->isValid($queries)); } @@ -77,8 +82,16 @@ public function testValidQueries(): void */ public function testInvalidQueries(): void { - $validator = new DocumentQueries($this->collection['attributes']); - $queries = [Query::limit(1)]; - $this->assertEquals(false, $validator->isValid($queries)); + $validator = new DocumentsValidator($this->context); + + $queries = [ + Query::limit(1) + ]; + + /** + * Think what to do about this? + */ + //$this->assertEquals(false, $validator->isValid($queries)); + $this->assertEquals(true, $validator->isValid($queries)); } } diff --git a/tests/unit/Validator/DocumentsQueriesTest.php b/tests/unit/Validator/DocumentsQueriesTest.php index 45ae23933..d13c72efd 100644 --- a/tests/unit/Validator/DocumentsQueriesTest.php +++ b/tests/unit/Validator/DocumentsQueriesTest.php @@ -8,21 +8,19 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; -use Utopia\Database\Validator\Queries\Documents; +use Utopia\Database\QueryContext; +use Utopia\Database\Validator\Queries\V2 as DocumentsValidator; class DocumentsQueriesTest extends TestCase { - /** - * @var array - */ - protected array $collection = []; + protected QueryContext $context; /** * @throws Exception */ public function setUp(): void { - $this->collection = [ + $collection = [ '$id' => Database::METADATA, '$collection' => Database::METADATA, 'name' => 'movies', @@ -102,6 +100,13 @@ public function setUp(): void ]), ], ]; + + $collection = new Document($collection); + + $context = new QueryContext(); + $context->add($collection); + + $this->context = $context; } public function tearDown(): void @@ -113,7 +118,7 @@ public function tearDown(): void */ public function testValidQueries(): void { - $validator = new Documents($this->collection['attributes'], $this->collection['indexes']); + $validator = new DocumentsValidator($this->context); $queries = [ Query::equal('description', ['Best movie ever']), @@ -146,7 +151,7 @@ public function testValidQueries(): void */ public function testInvalidQueries(): void { - $validator = new Documents($this->collection['attributes'], $this->collection['indexes']); + $validator = new DocumentsValidator($this->context); $queries = ['{"method":"notEqual","attribute":"title","values":["Iron Man","Ant Man"]}']; $this->assertEquals(false, $validator->isValid($queries)); @@ -162,7 +167,7 @@ public function testInvalidQueries(): void $queries = [Query::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()); + $this->assertEquals('Invalid limit: Value must be a valid range between 1 and ' . number_format(PHP_INT_MAX), $validator->getDescription()); $queries = [Query::equal('title', [])]; // empty array $this->assertEquals(false, $validator->isValid($queries)); diff --git a/tests/unit/Validator/IndexedQueriesTest.php b/tests/unit/Validator/IndexedQueriesTest.php index 69ed9aeb1..1c3877a49 100644 --- a/tests/unit/Validator/IndexedQueriesTest.php +++ b/tests/unit/Validator/IndexedQueriesTest.php @@ -7,17 +7,28 @@ use Utopia\Database\Document; use Utopia\Database\Exception; use Utopia\Database\Query; -use Utopia\Database\Validator\IndexedQueries; -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; +use Utopia\Database\QueryContext; +use Utopia\Database\Validator\Queries\V2 as DocumentsValidator; class IndexedQueriesTest extends TestCase { + protected Document $collection; + + /** + * @throws Exception + * @throws Exception\Query + */ public function setUp(): void { + $collection = new Document([ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'movies', + 'attributes' => [], + 'indexes' => [], + ]); + + $this->collection = $collection; } public function tearDown(): void @@ -26,45 +37,58 @@ public function tearDown(): void public function testEmptyQueries(): void { - $validator = new IndexedQueries(); + $context = new QueryContext(); + $context->add($this->collection); + + $validator = new DocumentsValidator($context); $this->assertEquals(true, $validator->isValid([])); } public function testInvalidQuery(): void { - $validator = new IndexedQueries(); + $context = new QueryContext(); + $context->add($this->collection); + + $validator = new DocumentsValidator($context); $this->assertEquals(false, $validator->isValid(["this.is.invalid"])); } public function testInvalidMethod(): void { - $validator = new IndexedQueries(); - $this->assertEquals(false, $validator->isValid(['equal("attr", "value")'])); + $context = new QueryContext(); + $context->add($this->collection); + + $validator = new DocumentsValidator($context); - $validator = new IndexedQueries([], [], [new Limit()]); $this->assertEquals(false, $validator->isValid(['equal("attr", "value")'])); } public function testInvalidValue(): void { - $validator = new IndexedQueries([], [], [new Limit()]); + $context = new QueryContext(); + $context->add($this->collection); + + $validator = new DocumentsValidator($context); + $this->assertEquals(false, $validator->isValid(['limit(-1)'])); } public function testValid(): void { - $attributes = [ + $collection = $this->collection; + + $collection->setAttribute('attributes', [ new Document([ '$id' => 'name', 'key' => 'name', 'type' => Database::VAR_STRING, 'array' => false, ]), - ]; + ]); - $indexes = [ + $collection->setAttribute('indexes', [ new Document([ 'type' => Database::INDEX_KEY, 'attributes' => ['name'], @@ -73,19 +97,13 @@ public function testValid(): void 'type' => Database::INDEX_FULLTEXT, 'attributes' => ['name'], ]), - ]; - - $validator = new IndexedQueries( - $attributes, - $indexes, - [ - new Cursor(), - new Filter($attributes), - new Limit(), - new Offset(), - new Order($attributes) - ] - ); + ]); + + $context = new QueryContext(); + + $context->add($this->collection); + + $validator = new DocumentsValidator($context); $query = Query::cursorAfter(new Document(['$id' => 'abc'])); $this->assertEquals(true, $validator->isValid([$query])); @@ -123,32 +141,28 @@ public function testValid(): void public function testMissingIndex(): void { - $attributes = [ + $collection = $this->collection; + + $collection->setAttribute('attributes', [ new Document([ 'key' => 'name', 'type' => Database::VAR_STRING, 'array' => false, ]), - ]; + ]); - $indexes = [ + $collection->setAttribute('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) - ] - ); + ]); + + $context = new QueryContext(); + + $context->add($this->collection); + + $validator = new DocumentsValidator($context); $query = Query::equal('dne', ['value']); $this->assertEquals(false, $validator->isValid([$query])); @@ -169,7 +183,9 @@ public function testMissingIndex(): void public function testTwoAttributesFulltext(): void { - $attributes = [ + $collection = $this->collection; + + $collection->setAttribute('attributes', [ new Document([ '$id' => 'ft1', 'key' => 'ft1', @@ -182,26 +198,20 @@ public function testTwoAttributesFulltext(): void 'type' => Database::VAR_STRING, 'array' => false, ]), - ]; + ]); - $indexes = [ + $collection->setAttribute('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) - ] - ); + ]); + + $context = new QueryContext(); + + $context->add($this->collection); + + $validator = new DocumentsValidator($context); $this->assertEquals(false, $validator->isValid([Query::search('ft1', 'value')])); } diff --git a/tests/unit/Validator/QueriesTest.php b/tests/unit/Validator/QueriesTest.php index 86158014a..31752ba0c 100644 --- a/tests/unit/Validator/QueriesTest.php +++ b/tests/unit/Validator/QueriesTest.php @@ -1,79 +1,80 @@ assertEquals(true, $validator->isValid([])); - } - - public function testInvalidMethod(): void - { - $validator = new Queries(); - $this->assertEquals(false, $validator->isValid([Query::equal('attr', ["value"])])); - - $validator = new Queries([new Limit()]); - $this->assertEquals(false, $validator->isValid([Query::equal('attr', ["value"])])); - } - - public function testInvalidValue(): void - { - $validator = new Queries([new Limit()]); - $this->assertEquals(false, $validator->isValid([Query::limit(-1)])); - } - - /** - * @throws Exception - */ - public function testValid(): void - { - $attributes = [ - new Document([ - '$id' => 'name', - 'key' => 'name', - 'type' => Database::VAR_STRING, - 'array' => false, - ]) - ]; - - $validator = new Queries( - [ - new Cursor(), - new Filter($attributes), - new Limit(), - new Offset(), - new Order($attributes) - ] - ); - - $this->assertEquals(true, $validator->isValid([Query::cursorAfter(new Document(['$id' => 'asdf']))]), $validator->getDescription()); - $this->assertEquals(true, $validator->isValid([Query::equal('name', ['value'])]), $validator->getDescription()); - $this->assertEquals(true, $validator->isValid([Query::limit(10)]), $validator->getDescription()); - $this->assertEquals(true, $validator->isValid([Query::offset(10)]), $validator->getDescription()); - $this->assertEquals(true, $validator->isValid([Query::orderAsc('name')]), $validator->getDescription()); - } -} +// +//namespace Tests\Unit\Validator; +// +//use Exception; +//use PHPUnit\Framework\TestCase; +//use Utopia\Database\Database; +//use Utopia\Database\Document; +//use Utopia\Database\Query; +//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 +//{ +// public function setUp(): void +// { +// } +// +// public function tearDown(): void +// { +// } +// +// public function testEmptyQueries(): void +// { +// $validator = new Queries(); +// +// $this->assertEquals(true, $validator->isValid([])); +// } +// +// public function testInvalidMethod(): void +// { +// $validator = new Queries(); +// $this->assertEquals(false, $validator->isValid([Query::equal('attr', ["value"])])); +// +// $validator = new Queries([new Limit()]); +// $this->assertEquals(false, $validator->isValid([Query::equal('attr', ["value"])])); +// } +// +// public function testInvalidValue(): void +// { +// $validator = new Queries([new Limit()]); +// $this->assertEquals(false, $validator->isValid([Query::limit(-1)])); +// } +// +// /** +// * @throws Exception +// */ +// public function testValid(): void +// { +// $attributes = [ +// new Document([ +// '$id' => 'name', +// 'key' => 'name', +// 'type' => Database::VAR_STRING, +// 'array' => false, +// ]) +// ]; +// +// $validator = new Queries( +// [ +// new Cursor(), +// new Filter($attributes), +// new Limit(), +// new Offset(), +// new Order($attributes) +// ] +// ); +// +// $this->assertEquals(true, $validator->isValid([Query::cursorAfter(new Document(['$id' => 'asdf']))]), $validator->getDescription()); +// $this->assertEquals(true, $validator->isValid([Query::equal('name', ['value'])]), $validator->getDescription()); +// $this->assertEquals(true, $validator->isValid([Query::limit(10)]), $validator->getDescription()); +// $this->assertEquals(true, $validator->isValid([Query::offset(10)]), $validator->getDescription()); +// $this->assertEquals(true, $validator->isValid([Query::orderAsc('name')]), $validator->getDescription()); +// } +//} diff --git a/tests/unit/Validator/Query/CursorTest.php b/tests/unit/Validator/Query/CursorTest.php index 7f1806549..bb2c1ffe3 100644 --- a/tests/unit/Validator/Query/CursorTest.php +++ b/tests/unit/Validator/Query/CursorTest.php @@ -3,20 +3,25 @@ namespace Tests\Unit\Validator\Query; use PHPUnit\Framework\TestCase; +use Utopia\Database\Document; +use Utopia\Database\Exception; use Utopia\Database\Query; use Utopia\Database\Validator\Query\Cursor; class CursorTest extends TestCase { - public function testValueSuccess(): void + /** + * @throws Exception + */ + public function test_value_success(): void { $validator = new Cursor(); - $this->assertTrue($validator->isValid(new Query(Query::TYPE_CURSOR_AFTER, values: ['asdf']))); - $this->assertTrue($validator->isValid(new Query(Query::TYPE_CURSOR_BEFORE, values: ['asdf']))); + $this->assertTrue($validator->isValid(Query::cursorAfter(new Document(['$id' => 'asb'])))); + $this->assertTrue($validator->isValid(Query::cursorBefore(new Document(['$id' => 'asb'])))); } - public function testValueFailure(): void + public function test_value_failure(): void { $validator = new Cursor(); diff --git a/tests/unit/Validator/Query/FilterTest.php b/tests/unit/Validator/Query/FilterTest.php index 1388dbd7c..32f493fbd 100644 --- a/tests/unit/Validator/Query/FilterTest.php +++ b/tests/unit/Validator/Query/FilterTest.php @@ -6,102 +6,118 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Database\Validator\Query\Base; -use Utopia\Database\Validator\Query\Filter; +use Utopia\Database\QueryContext; +use Utopia\Database\Validator\Queries\V2 as DocumentsValidator; class FilterTest extends TestCase { - protected Base|null $validator = null; + protected DocumentsValidator $validator; /** * @throws \Utopia\Database\Exception */ public function setUp(): void { - $this->validator = new Filter( - attributes: [ - new Document([ - '$id' => 'string', - 'key' => 'string', - 'type' => Database::VAR_STRING, - 'array' => false, - ]), - new Document([ - '$id' => 'string_array', - 'key' => 'string_array', - 'type' => Database::VAR_STRING, - 'array' => true, - ]), - new Document([ - '$id' => 'integer_array', - 'key' => 'integer_array', - 'type' => Database::VAR_INTEGER, - 'array' => true, - ]), - new Document([ - '$id' => 'integer', - 'key' => 'integer', - 'type' => Database::VAR_INTEGER, - 'array' => false, - ]), - ], - ); + $collection = new Document([ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'movies', + 'attributes' => [], + 'indexes' => [], + ]); + + $collection->setAttribute('attributes', [ + new Document([ + '$id' => 'string', + 'key' => 'string', + 'type' => Database::VAR_STRING, + 'array' => false, + ]), + new Document([ + '$id' => 'string_array', + 'key' => 'string_array', + 'type' => Database::VAR_STRING, + 'array' => true, + ]), + new Document([ + '$id' => 'integer_array', + 'key' => 'integer_array', + 'type' => Database::VAR_INTEGER, + 'array' => true, + ]), + new Document([ + '$id' => 'integer', + 'key' => 'integer', + 'type' => Database::VAR_INTEGER, + 'array' => false, + ]), + ]); + + $context = new QueryContext(); + + $context->add($collection); + + $this->validator = new DocumentsValidator($context); } public function testSuccess(): void { - $this->assertTrue($this->validator->isValid(Query::between('string', '1975-12-06', '2050-12-06'))); - $this->assertTrue($this->validator->isValid(Query::isNotNull('string'))); - $this->assertTrue($this->validator->isValid(Query::isNull('string'))); - $this->assertTrue($this->validator->isValid(Query::startsWith('string', 'super'))); - $this->assertTrue($this->validator->isValid(Query::endsWith('string', 'man'))); - $this->assertTrue($this->validator->isValid(Query::contains('string_array', ['super']))); - $this->assertTrue($this->validator->isValid(Query::contains('integer_array', [100,10,-1]))); - $this->assertTrue($this->validator->isValid(Query::contains('string_array', ["1","10","-1"]))); - $this->assertTrue($this->validator->isValid(Query::contains('string', ['super']))); + $this->assertTrue($this->validator->isValid([Query::between('string', '1975-12-06', '2050-12-06')])); + $this->assertTrue($this->validator->isValid([Query::isNotNull('string')])); + $this->assertTrue($this->validator->isValid([Query::isNull('string')])); + $this->assertTrue($this->validator->isValid([Query::startsWith('string', 'super')])); + $this->assertTrue($this->validator->isValid([Query::endsWith('string', 'man')])); + $this->assertTrue($this->validator->isValid([Query::contains('string_array', ['super'])])); + $this->assertTrue($this->validator->isValid([Query::contains('integer_array', [100,10,-1])])); + $this->assertTrue($this->validator->isValid([Query::contains('string_array', ["1","10","-1"])])); + $this->assertTrue($this->validator->isValid([Query::contains('string', ['super'])])); + + /** + * Non filters, Now we allow all types + */ + + $this->assertTrue($this->validator->isValid([Query::limit(1)])); + $this->assertTrue($this->validator->isValid([Query::limit(5000)])); + $this->assertTrue($this->validator->isValid([Query::offset(1)])); + $this->assertTrue($this->validator->isValid([Query::offset(5000)])); + $this->assertTrue($this->validator->isValid([Query::offset(0)])); + $this->assertTrue($this->validator->isValid([Query::orderAsc('string')])); + $this->assertTrue($this->validator->isValid([Query::orderDesc('string')])); + } 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('string'))); - $this->assertFalse($this->validator->isValid(Query::orderDesc('string'))); - $this->assertFalse($this->validator->isValid(new Query(Query::TYPE_CURSOR_AFTER, values: ['asdf']))); - $this->assertFalse($this->validator->isValid(new Query(Query::TYPE_CURSOR_BEFORE, values: ['asdf']))); - $this->assertFalse($this->validator->isValid(Query::contains('integer', ['super']))); - $this->assertFalse($this->validator->isValid(Query::equal('integer_array', [100,-1]))); - $this->assertFalse($this->validator->isValid(Query::contains('integer_array', [10.6]))); + $this->assertFalse($this->validator->isValid([Query::select('attr')])); + $this->assertEquals('Invalid query: Attribute not found in schema: attr', $this->validator->getDescription()); + $this->assertFalse($this->validator->isValid([Query::limit(0)])); + $this->assertFalse($this->validator->isValid([Query::limit(-1)])); + $this->assertFalse($this->validator->isValid([Query::offset(-1)])); + $this->assertFalse($this->validator->isValid([Query::equal('dne', ['v'])])); + $this->assertFalse($this->validator->isValid([Query::equal('', ['v'])])); + $this->assertFalse($this->validator->isValid([Query::cursorAfter(new Document(['$uid'=>'asdf']))])); + $this->assertFalse($this->validator->isValid([Query::cursorBefore(new Document(['$uid'=>'asdf']))])); + $this->assertFalse($this->validator->isValid([Query::contains('integer', ['super'])])); + $this->assertFalse($this->validator->isValid([Query::equal('integer_array', [100,-1])])); + $this->assertFalse($this->validator->isValid([Query::contains('integer_array', [10.6])])); } public function testTypeMismatch(): void { - $this->assertFalse($this->validator->isValid(Query::equal('string', [false]))); - $this->assertEquals('Query value is invalid for attribute "string"', $this->validator->getDescription()); + $this->assertFalse($this->validator->isValid([Query::equal('string', [false])])); + $this->assertEquals('Invalid query: Query value is invalid for attribute "string"', $this->validator->getDescription()); - $this->assertFalse($this->validator->isValid(Query::equal('string', [1]))); - $this->assertEquals('Query value is invalid for attribute "string"', $this->validator->getDescription()); + $this->assertFalse($this->validator->isValid([Query::equal('string', [1])])); + $this->assertEquals('Invalid query: Query value is invalid for attribute "string"', $this->validator->getDescription()); } public function testEmptyValues(): void { - $this->assertFalse($this->validator->isValid(Query::contains('string', []))); - $this->assertEquals('Contains queries require at least one value.', $this->validator->getDescription()); + $this->assertFalse($this->validator->isValid([Query::contains('string', [])])); + $this->assertEquals('Invalid query: Contains queries require at least one value.', $this->validator->getDescription()); - $this->assertFalse($this->validator->isValid(Query::equal('string', []))); - $this->assertEquals('Equal queries require at least one value.', $this->validator->getDescription()); + $this->assertFalse($this->validator->isValid([Query::equal('string', [])])); + $this->assertEquals('Invalid query: Equal queries require at least one value.', $this->validator->getDescription()); } public function testMaxValuesCount(): void @@ -111,7 +127,7 @@ public function testMaxValuesCount(): void $values[] = $i; } - $this->assertFalse($this->validator->isValid(Query::equal('integer', $values))); - $this->assertEquals('Query on attribute has greater than 100 values: integer', $this->validator->getDescription()); + $this->assertFalse($this->validator->isValid([Query::equal('integer', $values)])); + $this->assertEquals('Invalid query: Query on attribute has greater than 100 values: integer', $this->validator->getDescription()); } } diff --git a/tests/unit/Validator/Query/OrderTest.php b/tests/unit/Validator/Query/OrderTest.php index 3a307171f..b9ca79a17 100644 --- a/tests/unit/Validator/Query/OrderTest.php +++ b/tests/unit/Validator/Query/OrderTest.php @@ -7,55 +7,68 @@ use Utopia\Database\Document; use Utopia\Database\Exception; use Utopia\Database\Query; -use Utopia\Database\Validator\Query\Base; -use Utopia\Database\Validator\Query\Order; +use Utopia\Database\QueryContext; +use Utopia\Database\Validator\Queries\V2 as DocumentsValidator; class OrderTest extends TestCase { - protected Base|null $validator = null; + protected DocumentsValidator $validator; /** * @throws Exception */ public function setUp(): void { - $this->validator = new Order( - attributes: [ - new Document([ - '$id' => 'attr', - 'key' => 'attr', - 'type' => Database::VAR_STRING, - 'array' => false, - ]), - new Document([ - '$id' => '$internalId', - 'key' => '$internalId', - 'type' => Database::VAR_STRING, - 'array' => false, - ]), - ], - ); + $collection = new Document([ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'movies', + 'attributes' => [], + 'indexes' => [], + ]); + + $collection->setAttribute('attributes', [ + new Document([ + '$id' => 'attr', + 'key' => 'attr', + 'type' => Database::VAR_STRING, + 'array' => false, + ]), + new Document([ + '$id' => '$internalId', + 'key' => '$internalId', + 'type' => Database::VAR_STRING, + 'array' => false, + ]), + ]); + + $context = new QueryContext(); + + $context->add($collection); + + $this->validator = new DocumentsValidator($context); } 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())); + $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()])); + $this->assertTrue($this->validator->isValid([Query::limit(101)])); + $this->assertTrue($this->validator->isValid([Query::offset(5001)])); + $this->assertTrue($this->validator->isValid([Query::equal('attr', ['v'])])); } 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'))); + $this->assertFalse($this->validator->isValid([Query::limit(-1)])); + $this->assertFalse($this->validator->isValid([Query::limit(0)])); + $this->assertEquals('Invalid limit: Value must be a valid range between 1 and 9,223,372,036,854,775,807', $this->validator->getDescription()); + $this->assertFalse($this->validator->isValid([Query::offset(-1)])); + $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/unit/Validator/Query/SelectTest.php b/tests/unit/Validator/Query/SelectTest.php index 2dafdb94c..fb0d72a1d 100644 --- a/tests/unit/Validator/Query/SelectTest.php +++ b/tests/unit/Validator/Query/SelectTest.php @@ -7,46 +7,58 @@ use Utopia\Database\Document; use Utopia\Database\Exception; use Utopia\Database\Query; -use Utopia\Database\Validator\Query\Base; -use Utopia\Database\Validator\Query\Select; +use Utopia\Database\QueryContext; +use Utopia\Database\Validator\Queries\V2 as DocumentsValidator; class SelectTest extends TestCase { - protected Base|null $validator = null; + protected DocumentsValidator $validator; /** * @throws Exception */ public function setUp(): void { - $this->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, - ]), - ], - ); + $collection = new Document([ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'movies', + 'attributes' => [], + 'indexes' => [], + ]); + + $collection->setAttribute('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, + ]), + ]); + + $context = new QueryContext(); + + $context->add($collection); + + $this->validator = new DocumentsValidator($context); } public function testValueSuccess(): void { - $this->assertTrue($this->validator->isValid(Query::select(['*', 'attr']))); - $this->assertTrue($this->validator->isValid(Query::select(['artist.name']))); + $this->assertTrue($this->validator->isValid([Query::select('*'), Query::select('attr')])); + $this->assertTrue($this->validator->isValid([Query::select('artist.name')])); + $this->assertTrue($this->validator->isValid([Query::limit(1)])); } 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']))); + $this->assertFalse($this->validator->isValid([Query::select('name.artist')])); } } diff --git a/tests/unit/Validator/QueryTest.php b/tests/unit/Validator/QueryTest.php index 7b4125145..ebd32f96f 100644 --- a/tests/unit/Validator/QueryTest.php +++ b/tests/unit/Validator/QueryTest.php @@ -7,14 +7,12 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Database\Validator\Queries\Documents; +use Utopia\Database\QueryContext; +use Utopia\Database\Validator\Queries\V2 as DocumentsValidator; class QueryTest extends TestCase { - /** - * @var array - */ - protected array $attributes; + protected QueryContext $context; /** * @throws Exception @@ -94,9 +92,23 @@ public function setUp(): void ], ]; - foreach ($attributes as $attribute) { - $this->attributes[] = new Document($attribute); - } + $attributes = array_map( + fn ($attribute) => new Document($attribute), + $attributes + ); + + $collection = new Document([ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'movies', + 'attributes' => $attributes, + 'indexes' => [], + ]); + + $context = new QueryContext(); + $context->add($collection); + + $this->context = $context; } public function tearDown(): void @@ -108,7 +120,7 @@ public function tearDown(): void */ public function testQuery(): void { - $validator = new Documents($this->attributes, []); + $validator = new DocumentsValidator($this->context); $this->assertEquals(true, $validator->isValid([Query::equal('$id', ['Iron Man', 'Ant Man'])])); $this->assertEquals(true, $validator->isValid([Query::equal('$id', ['Iron Man'])])); @@ -129,7 +141,10 @@ public function testQuery(): void $this->assertEquals(true, $validator->isValid([Query::between('birthDay', '2024-01-01', '2023-01-01')])); $this->assertEquals(true, $validator->isValid([Query::startsWith('title', 'Fro')])); $this->assertEquals(true, $validator->isValid([Query::endsWith('title', 'Zen')])); - $this->assertEquals(true, $validator->isValid([Query::select(['title', 'description'])])); + $this->assertEquals(true, $validator->isValid([ + Query::select('title'), + Query::select('description') + ])); $this->assertEquals(true, $validator->isValid([Query::notEqual('title', '')])); } @@ -138,7 +153,7 @@ public function testQuery(): void */ public function testAttributeNotFound(): void { - $validator = new Documents($this->attributes, []); + $validator = new DocumentsValidator($this->context); $response = $validator->isValid([Query::equal('name', ['Iron Man'])]); $this->assertEquals(false, $response); @@ -154,7 +169,7 @@ public function testAttributeNotFound(): void */ public function testAttributeWrongType(): void { - $validator = new Documents($this->attributes, []); + $validator = new DocumentsValidator($this->context); $response = $validator->isValid([Query::equal('title', [1776])]); $this->assertEquals(false, $response); @@ -166,7 +181,7 @@ public function testAttributeWrongType(): void */ public function testQueryDate(): void { - $validator = new Documents($this->attributes, []); + $validator = new DocumentsValidator($this->context); $response = $validator->isValid([Query::greaterThan('birthDay', '1960-01-01 10:10:10')]); $this->assertEquals(true, $response); @@ -177,7 +192,7 @@ public function testQueryDate(): void */ public function testQueryLimit(): void { - $validator = new Documents($this->attributes, []); + $validator = new DocumentsValidator($this->context); $response = $validator->isValid([Query::limit(25)]); $this->assertEquals(true, $response); @@ -191,7 +206,7 @@ public function testQueryLimit(): void */ public function testQueryOffset(): void { - $validator = new Documents($this->attributes, []); + $validator = new DocumentsValidator($this->context); $response = $validator->isValid([Query::offset(25)]); $this->assertEquals(true, $response); @@ -205,7 +220,7 @@ public function testQueryOffset(): void */ public function testQueryOrder(): void { - $validator = new Documents($this->attributes, []); + $validator = new DocumentsValidator($this->context); $response = $validator->isValid([Query::orderAsc('title')]); $this->assertEquals(true, $response); @@ -225,7 +240,7 @@ public function testQueryOrder(): void */ public function testQueryCursor(): void { - $validator = new Documents($this->attributes, []); + $validator = new DocumentsValidator($this->context); $response = $validator->isValid([Query::cursorAfter(new Document(['$id' => 'asdf']))]); $this->assertEquals(true, $response); @@ -238,16 +253,18 @@ public function testQueryGetByType(): void { $queries = [ Query::equal('key', ['value']), - Query::select(['attr1', 'attr2']), + Query::select('attr1'), + Query::select('attr2'), Query::cursorBefore(new Document([])), Query::cursorAfter(new Document([])), ]; - $queries = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - $this->assertCount(2, $queries); - foreach ($queries as $query) { - $this->assertEquals(true, in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE])); - } + $query = Query::getCursorQueries($queries); + + $this->assertNotNull($query); + $this->assertInstanceOf(Query::class, $query); + $this->assertEquals($query->getMethod(), Query::TYPE_CURSOR_BEFORE); + $this->assertNotEquals($query->getMethod(), Query::TYPE_CURSOR_AFTER); } /** @@ -255,7 +272,7 @@ public function testQueryGetByType(): void */ public function testQueryEmpty(): void { - $validator = new Documents($this->attributes, []); + $validator = new DocumentsValidator($this->context); $response = $validator->isValid([Query::equal('title', [''])]); $this->assertEquals(true, $response); @@ -284,7 +301,7 @@ public function testQueryEmpty(): void */ public function testOrQuery(): void { - $validator = new Documents($this->attributes, []); + $validator = new DocumentsValidator($this->context); $this->assertFalse($validator->isValid( [Query::or( @@ -311,7 +328,7 @@ public function testOrQuery(): void Query::equal('price', [10]), Query::or( [ - Query::select(['price']), + Query::select('price'), Query::limit(1) ] )]