diff --git a/composer.json b/composer.json index 90f4011ec..bae8d628c 100755 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "php": ">=8.0", "utopia-php/framework": "0.*.*", "utopia-php/cache": "0.8.*", - "utopia-php/mongo": "0.0.2" + "utopia-php/mongo": "0.3.*" }, "require-dev": { "ext-redis": "*", diff --git a/composer.lock b/composer.lock index 24ba6740d..ab0abe94e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "21a731a53a90dda637bc28e7d7b771c0", + "content-hash": "88a3434eb0ff1981519116680e700cab", "packages": [ { "name": "composer/package-versions-deprecated", @@ -336,23 +336,24 @@ }, { "name": "utopia-php/framework", - "version": "0.27.0", + "version": "0.28.4", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "b8d0447f5c98291d7759db05460ecced29a0f9ee" + "reference": "98c5469efe195aeecc63745dbf8e2f357f8cedac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/b8d0447f5c98291d7759db05460ecced29a0f9ee", - "reference": "b8d0447f5c98291d7759db05460ecced29a0f9ee", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/98c5469efe195aeecc63745dbf8e2f357f8cedac", + "reference": "98c5469efe195aeecc63745dbf8e2f357f8cedac", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=8.0" }, "require-dev": { "laravel/pint": "^1.2", + "phpstan/phpstan": "1.9.x-dev", "phpunit/phpunit": "^9.5.25", "vimeo/psalm": "4.27.0" }, @@ -374,22 +375,22 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.27.0" + "source": "https://github.com/utopia-php/framework/tree/0.28.4" }, - "time": "2023-01-29T05:36:17+00:00" + "time": "2023-06-03T14:09:22+00:00" }, { "name": "utopia-php/mongo", - "version": "0.0.2", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/mongo.git", - "reference": "62f9a9c0201af91b6d0dd4f0aa8a335ec9b56a1e" + "reference": "d94717db8489f6c235ec7d1f05bc9f56db179277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/62f9a9c0201af91b6d0dd4f0aa8a335ec9b56a1e", - "reference": "62f9a9c0201af91b6d0dd4f0aa8a335ec9b56a1e", + "url": "https://api.github.com/repos/utopia-php/mongo/zipball/d94717db8489f6c235ec7d1f05bc9f56db179277", + "reference": "d94717db8489f6c235ec7d1f05bc9f56db179277", "shasum": "" }, "require": { @@ -434,9 +435,9 @@ ], "support": { "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/0.0.2" + "source": "https://github.com/utopia-php/mongo/tree/0.3.0" }, - "time": "2022-11-08T11:58:46+00:00" + "time": "2023-08-14T17:20:27+00:00" } ], "packages-dev": [ @@ -788,6 +789,53 @@ }, "time": "2019-12-04T15:06:13+00:00" }, + { + "name": "doctrine/deprecations", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", + "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/v1.1.1" + }, + "time": "2023-06-03T09:27:29+00:00" + }, { "name": "doctrine/instantiator", "version": "2.0.0", @@ -860,16 +908,16 @@ }, { "name": "fakerphp/faker", - "version": "v1.21.0", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "92efad6a967f0b79c499705c69b662f738cc9e4d" + "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/92efad6a967f0b79c499705c69b662f738cc9e4d", - "reference": "92efad6a967f0b79c499705c69b662f738cc9e4d", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e3daa170d00fde61ea7719ef47bb09bb8f1d9b01", + "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01", "shasum": "" }, "require": { @@ -922,9 +970,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.21.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.23.0" }, - "time": "2022-12-13T13:54:32+00:00" + "time": "2023-06-12T08:44:38+00:00" }, { "name": "felixfbecker/advanced-json-rpc", @@ -1029,16 +1077,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -1076,7 +1124,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -1084,7 +1132,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "netresearch/jsonmapper", @@ -1139,16 +1187,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.3", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", "shasum": "" }, "require": { @@ -1189,9 +1237,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" }, - "time": "2023-01-16T22:05:37+00:00" + "time": "2023-06-25T14:52:30+00:00" }, { "name": "openlss/lib-array2xml", @@ -1469,24 +1517,27 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.2", + "version": "1.7.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "48f445a408c131e38cab1c235aa6d2bb7a0bb20d" + "reference": "b2fe4d22a5426f38e014855322200b97b5362c0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/48f445a408c131e38cab1c235aa6d2bb7a0bb20d", - "reference": "48f445a408c131e38cab1c235aa6d2bb7a0bb20d", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b2fe4d22a5426f38e014855322200b97b5362c0d", + "reference": "b2fe4d22a5426f38e014855322200b97b5362c0d", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.0", "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" }, "require-dev": { "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.1", @@ -1518,29 +1569,76 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.2" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.2" }, - "time": "2022-10-14T12:47:21+00:00" + "time": "2023-05-30T18:13:47+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.23.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a2b24135c35852b348894320d47b3902a94bc494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a2b24135c35852b348894320d47b3902a94bc494", + "reference": "a2b24135c35852b348894320d47b3902a94bc494", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.23.0" + }, + "time": "2023-07-23T22:17:56+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.24", + "version": "9.2.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed" + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2cf940ebc6355a9d430462811b5aaa308b174bed", - "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1", + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.14", + "nikic/php-parser": "^4.15", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -1555,8 +1653,8 @@ "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { @@ -1589,7 +1687,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.24" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27" }, "funding": [ { @@ -1597,7 +1696,7 @@ "type": "github" } ], - "time": "2023-01-26T08:26:55+00:00" + "time": "2023-07-26T13:44:30+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1842,16 +1941,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.3", + "version": "9.6.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e7b1615e3e887d6c719121c6d4a44b0ab9645555" + "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e7b1615e3e887d6c719121c6d4a44b0ab9645555", - "reference": "e7b1615e3e887d6c719121c6d4a44b0ab9645555", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a6d351645c3fe5a30f5e86be6577d946af65a328", + "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328", "shasum": "" }, "require": { @@ -1884,8 +1983,8 @@ "sebastian/version": "^3.0.2" }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -1924,7 +2023,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.3" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.10" }, "funding": [ { @@ -1940,7 +2040,7 @@ "type": "tidelift" } ], - "time": "2023-02-04T13:37:15+00:00" + "time": "2023-07-10T04:04:23+00:00" }, { "name": "psr/container", @@ -2345,16 +2445,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -2399,7 +2499,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -2407,7 +2507,7 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", @@ -3053,16 +3153,16 @@ }, { "name": "symfony/console", - "version": "v5.4.19", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "dccb8d251a9017d5994c988b034d3e18aaabf740" + "reference": "560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/dccb8d251a9017d5994c988b034d3e18aaabf740", - "reference": "dccb8d251a9017d5994c988b034d3e18aaabf740", + "url": "https://api.github.com/repos/symfony/console/zipball/560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8", + "reference": "560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8", "shasum": "" }, "require": { @@ -3127,12 +3227,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.19" + "source": "https://github.com/symfony/console/tree/v5.4.24" }, "funding": [ { @@ -3148,20 +3248,20 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:32:19+00:00" + "time": "2023-05-26T05:13:16+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/1ee04c65529dea5d8744774d474e7cbd2f1206d3", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { @@ -3170,7 +3270,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3199,7 +3299,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.2.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" }, "funding": [ { @@ -3215,7 +3315,7 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/polyfill-ctype", @@ -3628,16 +3728,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.2.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75" + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/aac98028c69df04ee77eb69b96b86ee51fbf4b75", - "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", "shasum": "" }, "require": { @@ -3647,13 +3747,10 @@ "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3693,7 +3790,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.2.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" }, "funding": [ { @@ -3709,20 +3806,20 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/string", - "version": "v6.2.5", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "b2dac0fa27b1ac0f9c0c0b23b43977f12308d0b0" + "reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/b2dac0fa27b1ac0f9c0c0b23b43977f12308d0b0", - "reference": "b2dac0fa27b1ac0f9c0c0b23b43977f12308d0b0", + "url": "https://api.github.com/repos/symfony/string/zipball/f2e190ee75ff0f5eced645ec0be5c66fac81f51f", + "reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f", "shasum": "" }, "require": { @@ -3733,13 +3830,13 @@ "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/translation-contracts": "<2.0" + "symfony/translation-contracts": "<2.5" }, "require-dev": { "symfony/error-handler": "^5.4|^6.0", "symfony/http-client": "^5.4|^6.0", "symfony/intl": "^6.2", - "symfony/translation-contracts": "^2.0|^3.0", + "symfony/translation-contracts": "^2.5|^3.0", "symfony/var-exporter": "^5.4|^6.0" }, "type": "library", @@ -3779,7 +3876,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.2.5" + "source": "https://github.com/symfony/string/tree/v6.3.0" }, "funding": [ { @@ -3795,7 +3892,7 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:38:09+00:00" + "time": "2023-03-21T21:06:29+00:00" }, { "name": "theseer/tokenizer", @@ -4171,5 +4268,5 @@ "ext-redis": "*", "ext-mongodb": "*" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 153784927..13c6a903e 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -295,6 +295,17 @@ abstract public function getDocument(string $collection, string $id, array $quer */ abstract public function createDocument(string $collection, Document $document): Document; + /** + * Create Documents in batches + * + * @param string $collection + * @param Document[] $documents + * @param int $batchSize + * + * @return Document[] + */ + abstract public function createDocuments(string $collection, array $documents, int $batchSize): array; + /** * Update Document * @@ -305,6 +316,17 @@ abstract public function createDocument(string $collection, Document $document): */ abstract public function updateDocument(string $collection, Document $document): Document; + /** + * Update Documents in batches + * + * @param string $collection + * @param Document[] $documents + * @param int $batchSize + * + * @return Document[] + */ + abstract public function updateDocuments(string $collection, array $documents, int $batchSize): array; + /** * Delete Document * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index c1cef9de4..ff35634a9 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -428,6 +428,99 @@ public function createDocument(string $collection, Document $document): Document return $document; } + /** + * @throws Duplicate + */ + public function createDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array + { + if (empty($documents)) { + return $documents; + } + + $this->getPDO()->beginTransaction(); + + try { + $name = $this->filter($collection); + $batches = \array_chunk($documents, $batchSize); + + foreach ($batches as $batch) { + $bindIndex = 0; + $batchKeys = []; + $bindValues = []; + $permissions = []; + + foreach ($batch as $document) { + $attributes = $document->getAttributes(); + $attributes['_uid'] = $document->getId(); + $attributes['_createdAt'] = $document->getCreatedAt(); + $attributes['_updatedAt'] = $document->getUpdatedAt(); + $attributes['_permissions'] = \json_encode($document->getPermissions()); + + $columns = []; + foreach (\array_keys($attributes) as $key => $attribute) { + $columns[$key] = "`{$this->filter($attribute)}`"; + } + + $columns = '(' . \implode(', ', $columns) . ')'; + + $bindKeys = []; + + foreach ($attributes as $value) { + if (\is_array($value)) { + $value = \json_encode($value); + } + $value = (\is_bool($value)) ? (int)$value : $value; + $bindKey = 'key_' . $bindIndex; + $bindKeys[] = ':' . $bindKey; + $bindValues[$bindKey] = $value; + $bindIndex++; + } + + $batchKeys[] = '(' . \implode(', ', $bindKeys) . ')'; + foreach (Database::PERMISSIONS as $type) { + foreach ($document->getPermissionsByType($type) as $permission) { + $permission = \str_replace('"', '', $permission); + $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}')"; + } + } + } + + $stmt = $this->getPDO()->prepare(" + INSERT INTO {$this->getSQLTable($name)} {$columns} + VALUES " . \implode(', ', $batchKeys) + ); + + foreach ($bindValues as $key => $value) { + $stmt->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmt->execute(); + + if (!empty($permissions)) { + $stmtPermissions = $this->getPDO()->prepare(" + INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document) + VALUES " . \implode(', ', $permissions) + ); + $stmtPermissions?->execute(); + } + } + + if (!$this->getPDO()->commit()) { + throw new Exception('Failed to commit transaction'); + } + + return $documents; + + } catch (PDOException $e) { + $this->getPDO()->rollBack(); + + throw match ($e->getCode()) { + 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), + default => $e, + }; + } + } + /** * Update Document * @@ -452,12 +545,14 @@ public function updateDocument(string $collection, Document $document): Document * Get current permissions from the database */ $permissionsStmt = $this->getPDO()->prepare(" - SELECT _type, _permission - FROM {$this->getSQLTable($name . '_perms')} p - WHERE p._document = :_uid + SELECT _type, _permission + FROM {$this->getSQLTable($name . '_perms')} + WHERE _document = :_uid "); + $permissionsStmt->bindValue(':_uid', $document->getId()); $permissionsStmt->execute(); + $permissions = $permissionsStmt->fetchAll(); $initial = []; @@ -616,6 +711,213 @@ public function updateDocument(string $collection, Document $document): Document return $document; } + /** + * @throws Duplicate + */ + public function updateDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array + { + if (empty($documents)) { + return $documents; + } + + $this->getPDO()->beginTransaction(); + + try { + $name = $this->filter($collection); + $batches = \array_chunk($documents, $batchSize); + + foreach ($batches as $batch) { + $bindIndex = 0; + $batchKeys = []; + $bindValues = []; + + $removeQuery = ''; + $removeBindValues = []; + + $addQuery = ''; + $addBindValues = []; + + foreach ($batch as $index => $document) { + $attributes = $document->getAttributes(); + $attributes['_uid'] = $document->getId(); + $attributes['_createdAt'] = $document->getCreatedAt(); + $attributes['_updatedAt'] = $document->getUpdatedAt(); + $attributes['_permissions'] = json_encode($document->getPermissions()); + + $columns = \array_map(function ($attribute) { + return "`" . $this->filter($attribute) . "`"; + }, \array_keys($attributes)); + + $bindKeys = []; + + foreach ($attributes as $value) { + if (\is_array($value)) { + $value = json_encode($value); + } + $value = (is_bool($value)) ? (int)$value : $value; + $bindKey = 'key_' . $bindIndex; + $bindKeys[] = ':' . $bindKey; + $bindValues[$bindKey] = $value; + $bindIndex++; + } + + $batchKeys[] = '(' . implode(', ', $bindKeys) . ')'; + + // Permissions logic + $permissionsStmt = $this->getPDO()->prepare(" + SELECT _type, _permission + FROM {$this->getSQLTable($name . '_perms')} + WHERE _document = :_uid + "); + $permissionsStmt->bindValue(':_uid', $document->getId()); + $permissionsStmt->execute(); + $permissions = $permissionsStmt->fetchAll(PDO::FETCH_ASSOC); + + $initial = []; + foreach (Database::PERMISSIONS as $type) { + $initial[$type] = []; + } + + $permissions = \array_reduce($permissions, function (array $carry, array $item) { + $carry[$item['_type']][] = $item['_permission']; + return $carry; + }, $initial); + + // Get removed Permissions + $removals = []; + foreach (Database::PERMISSIONS as $type) { + $diff = array_diff($permissions[$type], $document->getPermissionsByType($type)); + if (!empty($diff)) { + $removals[$type] = $diff; + } + } + + // Build inner query to remove permissions + if (!empty($removals)) { + foreach ($removals as $type => $permissionsToRemove) { + $bindKey = 'uid_' . $index; + $removeBindKeys[] = ':uid_' . $index; + $removeBindValues[$bindKey] = $document->getId(); + + $removeQuery .= "( + _document = :uid_{$index} + AND _type = '{$type}' + AND _permission IN (" . implode(', ', \array_map(function (string $i) use ($permissionsToRemove, $index, $type, &$removeBindKeys, &$removeBindValues) { + $bindKey = 'remove_' . $type . '_' . $index . '_' . $i; + $removeBindKeys[] = ':' . $bindKey; + $removeBindValues[$bindKey] = $permissionsToRemove[$i]; + + return ':' . $bindKey; + }, \array_keys($permissionsToRemove))) . + ") + )"; + + if ($type !== \array_key_last($removals)) { + $removeQuery .= ' OR '; + } + } + + if ($index !== \array_key_last($batch)) { + $removeQuery .= ' OR '; + } + } + + // Get added Permissions + $additions = []; + foreach (Database::PERMISSIONS as $type) { + $diff = \array_diff($document->getPermissionsByType($type), $permissions[$type]); + if (!empty($diff)) { + $additions[$type] = $diff; + } + } + + // Build inner query to add permissions + if (!empty($additions)) { + foreach ($additions as $type => $permissionsToAdd) { + foreach ($permissionsToAdd as $i => $permission) { + $bindKey = 'uid_' . $index; + $addBindValues[$bindKey] = $document->getId(); + + $bindKey = 'add_' . $type . '_' . $index . '_' . $i; + $addBindValues[$bindKey] = $permission; + + $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey})"; + + if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) { + $addQuery .= ', '; + } + } + } + if ($index !== \array_key_last($batch)) { + $addQuery .= ', '; + } + } + } + + $updateClause = ''; + for ($i = 0; $i < \count($columns); $i++) { + $column = $columns[$i]; + if (!empty($updateClause)) { + $updateClause .= ', '; + } + $updateClause .= "{$column} = VALUES({$column})"; + } + + $stmt = $this->getPDO()->prepare(" + INSERT INTO {$this->getSQLTable($name)} (" . \implode(", ", $columns) . ") + VALUES " . \implode(', ', $batchKeys) . " + ON DUPLICATE KEY UPDATE $updateClause + "); + + foreach ($bindValues as $key => $value) { + $stmt->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmt->execute(); + + if (!empty($removeQuery)) { + $stmtRemovePermissions = $this->getPDO()->prepare(" + DELETE + FROM {$this->getSQLTable($name . '_perms')} + WHERE ({$removeQuery}) + "); + + foreach ($removeBindValues as $key => $value) { + $stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmtRemovePermissions->execute(); + } + + if (!empty($addQuery)) { + $stmtAddPermissions = $this->getPDO()->prepare(" + INSERT INTO {$this->getSQLTable($name . '_perms')} (`_document`, `_type`, `_permission`) + VALUES {$addQuery} + "); + + foreach ($addBindValues as $key => $value) { + $stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmtAddPermissions->execute(); + } + } + + if (!$this->getPDO()->commit()) { + throw new Exception('Failed to commit transaction'); + } + + return $documents; + } catch (PDOException $e) { + $this->getPDO()->rollBack(); + + throw match ($e->getCode()) { + 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), + default => $e, + }; + } + } + /** * Increase or decrease an attribute value * @@ -1003,10 +1305,10 @@ protected function getSQLCondition(Query $query): string default => $query->getAttribute() }); - $attribute = "`{$query->getAttribute()}`" ; + $attribute = "`{$query->getAttribute()}`"; $placeholder = $this->getSQLPlaceholder($query); - switch ($query->getMethod()){ + switch ($query->getMethod()) { case Query::TYPE_SEARCH: /** * Replace reserved chars with space. @@ -1016,7 +1318,7 @@ protected function getSQLCondition(Query $query): string * Prepend wildcard by default on the back. */ $value = $this->getSQLValue($query->getMethod(), $value); - return 'MATCH('.$attribute.') AGAINST ('.$this->getPDO()->quote($value).' IN BOOLEAN MODE)'; + return 'MATCH(' . $attribute . ') AGAINST (' . $this->getPDO()->quote($value) . ' IN BOOLEAN MODE)'; case Query::TYPE_BETWEEN: return "table_main.{$attribute} BETWEEN :{$placeholder}_0 AND :{$placeholder}_1"; @@ -1028,7 +1330,7 @@ protected function getSQLCondition(Query $query): string default: $conditions = []; foreach ($query->getValues() as $key => $value) { - $conditions[] = $attribute.' '.$this->getSQLOperator($query->getMethod()).' '.':'.$placeholder.'_'.$key; + $conditions[] = $attribute . ' ' . $this->getSQLOperator($query->getMethod()) . ' ' . ':' . $placeholder . '_' . $key; } $condition = implode(' OR ', $conditions); return empty($condition) ? '' : '(' . $condition . ')'; diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 6025d2baf..daa69ff18 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -439,6 +439,32 @@ public function createDocument(string $collection, Document $document): Document return new Document($result); } + public function createDocuments(string $collection, array $documents, int $batchSize): array + { + $name = $this->getNamespace() . '_' . $this->filter($collection); + + $records = []; + foreach ($documents as $document) { + $document->removeAttribute('$internalId'); + + $record = $this->replaceChars('$', '_', (array)$document); + $record = $this->timeToMongo($record); + + $records[] = $this->removeNullKeys($record); + } + + $documents = $this->client->insertMany($name, $records); + + foreach ($documents as $index => $document) { + $documents[$index] = $this->replaceChars('_', '$', $this->client->toArray($document)); + $documents[$index] = $this->timeToDocument($documents[$index]); + + $documents[$index] = new Document($documents[$index]); + } + + return $documents; + } + /** * @throws Duplicate */ @@ -486,6 +512,23 @@ public function updateDocument(string $collection, Document $document): Document return $document; } + public function updateDocuments(string $collection, array $documents, int $batchSize): array + { + $name = $this->getNamespace() . '_' . $this->filter($collection); + + foreach ($documents as $index => $document) { + $document = $document->getArrayCopy(); + $document = $this->replaceChars('$', '_', $document); + $document = $this->timeToMongo($document); + + $this->client->update($name, ['_uid' => $document['_uid']], $document); + + $documents[$index] = new Document($document); + } + + return $documents; + } + /** * Increase or decrease an attribute value * diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index c0c77c700..9113136c9 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -98,7 +98,7 @@ public function createCollection(string $name, array $attributes = [], array $in //INDEX (\"_createdAt\"), //INDEX (\"_updatedAt\") $stmtIndex = $this->getPDO() - ->prepare("CREATE UNIQUE INDEX \"index_{$namespace}_{$id}_uid\" on {$this->getSQLTable($id)} (LOWER(_uid));"); + ->prepare("CREATE UNIQUE INDEX \"index_{$namespace}_{$id}_uid\" ON {$this->getSQLTable($id)} (LOWER(_uid));"); try { $stmt->execute(); $stmtIndex->execute(); @@ -422,11 +422,9 @@ public function createDocument(string $collection, Document $document): Document case 23505: $this->getPDO()->rollBack(); throw new Duplicate('Duplicated document: ' . $e->getMessage()); - break; default: throw $e; - break; } } @@ -437,6 +435,99 @@ public function createDocument(string $collection, Document $document): Document return $document; } + /** + * @throws Duplicate + */ + public function createDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array + { + if (empty($documents)) { + return $documents; + } + + $this->getPDO()->beginTransaction(); + + try { + $name = $this->filter($collection); + $batches = \array_chunk($documents, $batchSize); + + foreach ($batches as $batch) { + $bindIndex = 0; + $batchKeys = []; + $bindValues = []; + $permissions = []; + + foreach ($batch as $document) { + $attributes = $document->getAttributes(); + $attributes['_uid'] = $document->getId(); + $attributes['_createdAt'] = $document->getCreatedAt(); + $attributes['_updatedAt'] = $document->getUpdatedAt(); + $attributes['_permissions'] = \json_encode($document->getPermissions()); + + $columns = []; + foreach (\array_keys($attributes) as $key => $attribute) { + $columns[$key] = "\"{$this->filter($attribute)}\""; + } + + $columns = '(' . \implode(', ', $columns) . ')'; + + $bindKeys = []; + + foreach ($attributes as $value) { + if (\is_array($value)) { + $value = \json_encode($value); + } + $value = (\is_bool($value)) ? (int)$value : $value; + $bindKey = 'key_' . $bindIndex; + $bindKeys[] = ':' . $bindKey; + $bindValues[$bindKey] = $value; + $bindIndex++; + } + + $batchKeys[] = '(' . \implode(', ', $bindKeys) . ')'; + foreach (Database::PERMISSIONS as $type) { + foreach ($document->getPermissionsByType($type) as $permission) { + $permission = \str_replace('"', '', $permission); + $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}')"; + } + } + } + + $stmt = $this->getPDO()->prepare(" + INSERT INTO {$this->getSQLTable($name)} {$columns} + VALUES " . \implode(', ', $batchKeys) + ); + + foreach ($bindValues as $key => $value) { + $stmt->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmt->execute(); + + if (!empty($permissions)) { + $stmtPermissions = $this->getPDO()->prepare(" + INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document) + VALUES " . \implode(', ', $permissions) + ); + $stmtPermissions?->execute(); + } + } + + if (!$this->getPDO()->commit()) { + throw new Exception('Failed to commit transaction'); + } + + return $documents; + + } catch (PDOException $e) { + $this->getPDO()->rollBack(); + + throw match ($e->getCode()) { + 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), + default => $e, + }; + } + } + /** * Update Document * @@ -622,6 +713,213 @@ public function updateDocument(string $collection, Document $document): Document return $document; } + /** + * @throws Duplicate + */ + public function updateDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array + { + if (empty($documents)) { + return $documents; + } + + $this->getPDO()->beginTransaction(); + + try { + $name = $this->filter($collection); + $batches = \array_chunk($documents, $batchSize); + + foreach ($batches as $batch) { + $bindIndex = 0; + $batchKeys = []; + $bindValues = []; + + $removeQuery = ''; + $removeBindValues = []; + + $addQuery = ''; + $addBindValues = []; + + foreach ($batch as $index => $document) { + $attributes = $document->getAttributes(); + $attributes['_uid'] = $document->getId(); + $attributes['_createdAt'] = $document->getCreatedAt(); + $attributes['_updatedAt'] = $document->getUpdatedAt(); + $attributes['_permissions'] = json_encode($document->getPermissions()); + + $columns = \array_map(function ($attribute) { + return '"' . $this->filter($attribute) . '"'; + }, \array_keys($attributes)); + + $bindKeys = []; + + foreach ($attributes as $value) { + if (\is_array($value)) { + $value = json_encode($value); + } + $value = (is_bool($value)) ? (int)$value : $value; + $bindKey = 'key_' . $bindIndex; + $bindKeys[] = ':' . $bindKey; + $bindValues[$bindKey] = $value; + $bindIndex++; + } + + $batchKeys[] = '(' . implode(', ', $bindKeys) . ')'; + + // Permissions logic + $permissionsStmt = $this->getPDO()->prepare(" + SELECT _type, _permission + FROM {$this->getSQLTable($name . '_perms')} + WHERE _document = :_uid + "); + $permissionsStmt->bindValue(':_uid', $document->getId()); + $permissionsStmt->execute(); + $permissions = $permissionsStmt->fetchAll(PDO::FETCH_ASSOC); + + $initial = []; + foreach (Database::PERMISSIONS as $type) { + $initial[$type] = []; + } + + $permissions = \array_reduce($permissions, function (array $carry, array $item) { + $carry[$item['_type']][] = $item['_permission']; + return $carry; + }, $initial); + + // Get removed Permissions + $removals = []; + foreach (Database::PERMISSIONS as $type) { + $diff = array_diff($permissions[$type], $document->getPermissionsByType($type)); + if (!empty($diff)) { + $removals[$type] = $diff; + } + } + + // Build inner query to remove permissions + if (!empty($removals)) { + foreach ($removals as $type => $permissionsToRemove) { + $bindKey = 'uid_' . $index; + $removeBindKeys[] = ':uid_' . $index; + $removeBindValues[$bindKey] = $document->getId(); + + $removeQuery .= "( + _document = :uid_{$index} + AND _type = '{$type}' + AND _permission IN (" . implode(', ', \array_map(function (string $i) use ($permissionsToRemove, $index, $type, &$removeBindKeys, &$removeBindValues) { + $bindKey = 'remove_' . $type . '_' . $index . '_' . $i; + $removeBindKeys[] = ':' . $bindKey; + $removeBindValues[$bindKey] = $permissionsToRemove[$i]; + + return ':' . $bindKey; + }, \array_keys($permissionsToRemove))) . + ") + )"; + + if ($type !== \array_key_last($removals)) { + $removeQuery .= ' OR '; + } + } + + if ($index !== \array_key_last($batch)) { + $removeQuery .= ' OR '; + } + } + + // Get added Permissions + $additions = []; + foreach (Database::PERMISSIONS as $type) { + $diff = \array_diff($document->getPermissionsByType($type), $permissions[$type]); + if (!empty($diff)) { + $additions[$type] = $diff; + } + } + + // Build inner query to add permissions + if (!empty($additions)) { + foreach ($additions as $type => $permissionsToAdd) { + foreach ($permissionsToAdd as $i => $permission) { + $bindKey = 'uid_' . $index; + $addBindValues[$bindKey] = $document->getId(); + + $bindKey = 'add_' . $type . '_' . $index . '_' . $i; + $addBindValues[$bindKey] = $permission; + + $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey})"; + + if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) { + $addQuery .= ', '; + } + } + } + if ($index !== \array_key_last($batch)) { + $addQuery .= ', '; + } + } + } + + $updateClause = ''; + for ($i = 0; $i < \count($columns); $i++) { + $column = $columns[$i]; + if (!empty($updateClause)) { + $updateClause .= ', '; + } + $updateClause .= "{$column} = excluded.{$column}"; + } + + $stmt = $this->getPDO()->prepare(" + INSERT INTO {$this->getSQLTable($name)} (" . \implode(", ", $columns) . ") + VALUES " . \implode(', ', $batchKeys) . " + ON CONFLICT (LOWER(_uid)) DO UPDATE SET $updateClause + "); + + foreach ($bindValues as $key => $value) { + $stmt->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmt->execute(); + + if (!empty($removeQuery)) { + $stmtRemovePermissions = $this->getPDO()->prepare(" + DELETE + FROM {$this->getSQLTable($name . '_perms')} + WHERE ({$removeQuery}) + "); + + foreach ($removeBindValues as $key => $value) { + $stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmtRemovePermissions->execute(); + } + + if (!empty($addQuery)) { + $stmtAddPermissions = $this->getPDO()->prepare(" + INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission) + VALUES {$addQuery} + "); + + foreach ($addBindValues as $key => $value) { + $stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmtAddPermissions->execute(); + } + } + + if (!$this->getPDO()->commit()) { + throw new Exception('Failed to commit transaction'); + } + + return $documents; + } catch (PDOException $e) { + $this->getPDO()->rollBack(); + + throw match ($e->getCode()) { + 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), + default => $e, + }; + } + } + /** * Increase or decrease an attribute value * diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 9dad4a543..34d959497 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -569,6 +569,213 @@ public function updateDocument(string $collection, Document $document): Document return $document; } + /** + * @throws Duplicate + */ + public function updateDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array + { + if (empty($documents)) { + return $documents; + } + + $this->getPDO()->beginTransaction(); + + try { + $name = $this->filter($collection); + $batches = \array_chunk($documents, $batchSize); + + foreach ($batches as $batch) { + $bindIndex = 0; + $batchKeys = []; + $bindValues = []; + + $removeQuery = ''; + $removeBindValues = []; + + $addQuery = ''; + $addBindValues = []; + + foreach ($batch as $index => $document) { + $attributes = $document->getAttributes(); + $attributes['_uid'] = $document->getId(); + $attributes['_createdAt'] = $document->getCreatedAt(); + $attributes['_updatedAt'] = $document->getUpdatedAt(); + $attributes['_permissions'] = json_encode($document->getPermissions()); + + $columns = \array_map(function ($attribute) { + return "`" . $this->filter($attribute) . "`"; + }, \array_keys($attributes)); + + $bindKeys = []; + + foreach ($attributes as $value) { + if (\is_array($value)) { + $value = json_encode($value); + } + $value = (is_bool($value)) ? (int)$value : $value; + $bindKey = 'key_' . $bindIndex; + $bindKeys[] = ':' . $bindKey; + $bindValues[$bindKey] = $value; + $bindIndex++; + } + + $batchKeys[] = '(' . implode(', ', $bindKeys) . ')'; + + // Permissions logic + $permissionsStmt = $this->getPDO()->prepare(" + SELECT _type, _permission + FROM {$this->getSQLTable($name . '_perms')} + WHERE _document = :_uid + "); + $permissionsStmt->bindValue(':_uid', $document->getId()); + $permissionsStmt->execute(); + $permissions = $permissionsStmt->fetchAll(PDO::FETCH_ASSOC); + + $initial = []; + foreach (Database::PERMISSIONS as $type) { + $initial[$type] = []; + } + + $permissions = \array_reduce($permissions, function (array $carry, array $item) { + $carry[$item['_type']][] = $item['_permission']; + return $carry; + }, $initial); + + // Get removed Permissions + $removals = []; + foreach (Database::PERMISSIONS as $type) { + $diff = array_diff($permissions[$type], $document->getPermissionsByType($type)); + if (!empty($diff)) { + $removals[$type] = $diff; + } + } + + // Build inner query to remove permissions + if (!empty($removals)) { + foreach ($removals as $type => $permissionsToRemove) { + $bindKey = 'uid_' . $index; + $removeBindKeys[] = ':uid_' . $index; + $removeBindValues[$bindKey] = $document->getId(); + + $removeQuery .= "( + _document = :uid_{$index} + AND _type = '{$type}' + AND _permission IN (" . implode(', ', \array_map(function (string $i) use ($permissionsToRemove, $index, $type, &$removeBindKeys, &$removeBindValues) { + $bindKey = 'remove_' . $type . '_' . $index . '_' . $i; + $removeBindKeys[] = ':' . $bindKey; + $removeBindValues[$bindKey] = $permissionsToRemove[$i]; + + return ':' . $bindKey; + }, \array_keys($permissionsToRemove))) . + ") + )"; + + if ($type !== \array_key_last($removals)) { + $removeQuery .= ' OR '; + } + } + + if ($index !== \array_key_last($batch)) { + $removeQuery .= ' OR '; + } + } + + // Get added Permissions + $additions = []; + foreach (Database::PERMISSIONS as $type) { + $diff = \array_diff($document->getPermissionsByType($type), $permissions[$type]); + if (!empty($diff)) { + $additions[$type] = $diff; + } + } + + // Build inner query to add permissions + if (!empty($additions)) { + foreach ($additions as $type => $permissionsToAdd) { + foreach ($permissionsToAdd as $i => $permission) { + $bindKey = 'uid_' . $index; + $addBindValues[$bindKey] = $document->getId(); + + $bindKey = 'add_' . $type . '_' . $index . '_' . $i; + $addBindValues[$bindKey] = $permission; + + $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey})"; + + if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) { + $addQuery .= ', '; + } + } + } + if ($index !== \array_key_last($batch)) { + $addQuery .= ', '; + } + } + } + + $updateClause = ''; + for ($i = 0; $i < \count($columns); $i++) { + $column = $columns[$i]; + if (!empty($updateClause)) { + $updateClause .= ', '; + } + $updateClause .= "{$column} = excluded.{$column}"; + } + + $stmt = $this->getPDO()->prepare(" + INSERT INTO {$this->getSQLTable($name)} (" . \implode(", ", $columns) . ") + VALUES " . \implode(', ', $batchKeys) . " + ON CONFLICT(_uid) DO UPDATE SET $updateClause + "); + + foreach ($bindValues as $key => $value) { + $stmt->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmt->execute(); + + if (!empty($removeQuery)) { + $stmtRemovePermissions = $this->getPDO()->prepare(" + DELETE + FROM {$this->getSQLTable($name . '_perms')} + WHERE ({$removeQuery}) + "); + + foreach ($removeBindValues as $key => $value) { + $stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmtRemovePermissions->execute(); + } + + if (!empty($addQuery)) { + $stmtAddPermissions = $this->getPDO()->prepare(" + INSERT INTO {$this->getSQLTable($name . '_perms')} (`_document`, `_type`, `_permission`) + VALUES {$addQuery} + "); + + foreach ($addBindValues as $key => $value) { + $stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value)); + } + + $stmtAddPermissions->execute(); + } + } + + if (!$this->getPDO()->commit()) { + throw new Exception('Failed to commit transaction'); + } + + return $documents; + } catch (PDOException $e) { + $this->getPDO()->rollBack(); + + throw match ($e->getCode()) { + 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), + default => $e, + }; + } + } + /** * Is schemas supported? * diff --git a/src/Database/Database.php b/src/Database/Database.php index dfb78e486..f293599a3 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -82,8 +82,10 @@ class Database const EVENT_DOCUMENT_FIND = 'document_find'; const EVENT_DOCUMENT_CREATE = 'document_create'; + const EVENT_DOCUMENTS_CREATE = 'documents_create'; const EVENT_DOCUMENT_READ = 'document_read'; const EVENT_DOCUMENT_UPDATE = 'document_update'; + const EVENT_DOCUMENTS_UPDATE = 'documents_update'; const EVENT_DOCUMENT_DELETE = 'document_delete'; const EVENT_DOCUMENT_COUNT = 'document_count'; const EVENT_DOCUMENT_SUM = 'document_sum'; @@ -98,6 +100,8 @@ class Database const EVENT_INDEX_CREATE = 'index_create'; const EVENT_INDEX_DELETE = 'index_delete'; + // Insert Batch Size + const INSERT_BATCH_SIZE = 100; /** * @var Adapter @@ -1446,6 +1450,56 @@ public function createDocument(string $collection, Document $document): Document return $document; } + /** + * Create Documents in a batch + * + * @param string $collection + * @param Document $document + * + * @return Document + * + * @throws AuthorizationException + * @throws StructureException + * @throws Exception|Throwable + */ + public function createDocuments(string $collection, array $documents, int $batchSize = self::INSERT_BATCH_SIZE): array + { + if (empty($documents)) { + return []; + } + + $collection = $this->silent(fn() => $this->getCollection($collection)); + + $time = DateTime::now(); + + foreach ($documents as $key => $document) { + $document + ->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId()) + ->setAttribute('$collection', $collection->getId()) + ->setAttribute('$createdAt', $time) + ->setAttribute('$updatedAt', $time); + + $document = $this->encode($collection, $document); + + $validator = new Structure($collection); + if (!$validator->isValid($document)) { + throw new StructureException($validator->getDescription()); + } + + $documents[$key] = $document; + } + + $documents = $this->adapter->createDocuments($collection->getId(), $documents, $batchSize); + + foreach ($documents as $key => $document) { + $documents[$key] = $this->decode($collection, $document); + } + + $this->trigger(self::EVENT_DOCUMENTS_CREATE, $documents); + + return $documents; + } + /** * Update Document * @@ -1493,6 +1547,59 @@ public function updateDocument(string $collection, string $id, Document $documen return $document; } + /** + * @throws AuthorizationException + * @throws Throwable + * @throws StructureException + */ + public function updateDocuments(string $collection, array $documents, int $batchSize): array + { + if (empty($documents)) { + return []; + } + + $time = DateTime::now(); + $collection = $this->silent(fn() => $this->getCollection($collection)); + + foreach ($documents as $document) { + if (!$document->getId()) { + throw new Exception('Must define $id attribute for each document'); + } + + $document->setAttribute('$updatedAt', $time); + $document = $this->encode($collection, $document); + + $old = Authorization::skip(fn() => $this->silent(fn() => $this->getDocument( + $collection->getId(), + $document->getId()) + )); + + $validator = new Authorization(self::PERMISSION_UPDATE); + if ($collection->getId() !== self::METADATA + && !$validator->isValid($old->getUpdate())) { + throw new AuthorizationException($validator->getDescription()); + } + + $validator = new Structure($collection); + if (!$validator->isValid($document)) { + throw new StructureException($validator->getDescription()); + } + } + + $documents = $this->adapter->updateDocuments($collection->getId(), $documents, $batchSize); + + foreach ($documents as $key => $document) { + $documents[$key] = $this->decode($collection, $document); + + $this->cache->purge('cache-' . $this->getNamespace() . ':' . $collection->getId() . ':' . $document->getId() . ':*'); + } + + $this->trigger(self::EVENT_DOCUMENTS_UPDATE, $documents); + + return $documents; +} + + /** * Increase a document attribute by a value * diff --git a/tests/Database/Adapter/MariaDBTest.php b/tests/Database/Adapter/MariaDBTest.php index 9aa1c225d..32b9375fb 100644 --- a/tests/Database/Adapter/MariaDBTest.php +++ b/tests/Database/Adapter/MariaDBTest.php @@ -8,6 +8,10 @@ use Utopia\Database\Adapter\MariaDB; use Utopia\Cache\Cache; use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Database\Document; +use Utopia\Database\Helpers\Permission; +use Utopia\Database\Helpers\Role; +use Utopia\Database\Query; use Utopia\Tests\Base; class MariaDBTest extends Base @@ -34,7 +38,7 @@ static function getAdapterName(): string */ static function getDatabase(): Database { - if(!is_null(self::$database)) { + if (!is_null(self::$database)) { return self::$database; } @@ -51,8 +55,10 @@ static function getDatabase(): Database $database = new Database(new MariaDB($pdo), $cache); $database->setDefaultDatabase('utopiaTests'); - $database->setNamespace('myapp_'.uniqid()); + $database->setNamespace('myapp_' . uniqid()); + + $database->create(); return self::$database = $database; } -} \ No newline at end of file +} diff --git a/tests/Database/Adapter/MySQLTest.php b/tests/Database/Adapter/MySQLTest.php index b06176a2b..ac41d6d6a 100644 --- a/tests/Database/Adapter/MySQLTest.php +++ b/tests/Database/Adapter/MySQLTest.php @@ -64,6 +64,8 @@ static function getDatabase(): Database $database->setDefaultDatabase('utopiaTests'); $database->setNamespace('myapp_'.uniqid()); + $database->create(); + return self::$database = $database; } } \ No newline at end of file diff --git a/tests/Database/Adapter/PostgresTest.php b/tests/Database/Adapter/PostgresTest.php index a797bfa38..4eddea981 100644 --- a/tests/Database/Adapter/PostgresTest.php +++ b/tests/Database/Adapter/PostgresTest.php @@ -51,6 +51,8 @@ static function getDatabase(): Database $database->setDefaultDatabase('utopiaTests'); $database->setNamespace('myapp_'.uniqid()); + $database->create(); + return self::$database = $database; } } \ No newline at end of file diff --git a/tests/Database/Base.php b/tests/Database/Base.php index a27bb9a25..5886ed75c 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -605,6 +605,51 @@ public function testCreateDocument() return $document; } + public function testCreateDocuments() + { + $count = 3; + $collection = 'testCreateDocuments'; + + static::getDatabase()->createCollection($collection); + + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'string', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'integer', Database::VAR_INTEGER, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'bigint', Database::VAR_INTEGER, 8, true)); + + // Create an array of documents with random attributes. Don't use the createDocument function + $documents = []; + + for ($i = 0; $i < $count; $i++) { + $documents[] = new Document([ + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'string' => 'textđź“ť', + 'integer' => 5, + 'bigint' => 8589934592, // 2^33 + ]); + } + + $documents = static::getDatabase()->createDocuments($collection, $documents, 3); + + $this->assertEquals($count, count($documents)); + + foreach ($documents as $document) { + $this->assertNotEmpty(true, $document->getId()); + $this->assertIsString($document->getAttribute('string')); + $this->assertEquals('textđź“ť', $document->getAttribute('string')); // Also makes sure an emoji is working + $this->assertIsInt($document->getAttribute('integer')); + $this->assertEquals(5, $document->getAttribute('integer')); + $this->assertIsInt($document->getAttribute('bigint')); + $this->assertEquals(8589934592, $document->getAttribute('bigint')); + } + + return $documents; + } + public function testRespectNulls() { static::getDatabase()->createCollection('documents_nulls'); @@ -921,6 +966,52 @@ public function testUpdateDocument(Document $document) return $document; } + /** + * @depends testCreateDocuments + */ + public function testUpdateDocuments(array $documents) + { + $collection = 'testCreateDocuments'; + + foreach ($documents as $document) { + $document + ->setAttribute('string', 'textđź“ť updated') + ->setAttribute('integer', 6) + ->setAttribute('$permissions', [ + Permission::read(Role::users()), + Permission::create(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ]); + } + + $documents = static::getDatabase()->updateDocuments( + $collection, + $documents, + \count($documents) + ); + + foreach ($documents as $document) { + $this->assertEquals('textđź“ť updated', $document->getAttribute('string')); + $this->assertEquals(6, $document->getAttribute('integer')); + } + + $documents = static::getDatabase()->find($collection, [ + Query::limit(\count($documents)) + ]); + + foreach ($documents as $document) { + $this->assertEquals('textđź“ť updated', $document->getAttribute('string')); + $this->assertEquals(6, $document->getAttribute('integer')); + $this->assertEquals([ + Permission::read(Role::users()), + Permission::create(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ], $document->getAttribute('$permissions')); + } + } + /** * @depends testGetDocument */ @@ -3506,6 +3597,7 @@ public function testWritePermissions() $this->assertEquals('newCat', $docs[0]['type']); } + public function testEvents() { Authorization::skip(function () { diff --git a/tests/Database/Validator/StructureTest.php b/tests/Database/Validator/StructureTest.php index 293af3a33..594935fef 100644 --- a/tests/Database/Validator/StructureTest.php +++ b/tests/Database/Validator/StructureTest.php @@ -251,7 +251,7 @@ public function testStringValidation() 'feedback' => 'team@appwrite.io', ]))); - $this->assertEquals('Invalid document structure: Attribute "title" has invalid type. Value must be a valid string and no longer than 256 chars', $validator->getDescription()); + $this->assertEquals('Invalid document structure: Attribute "title" has invalid type. Value must be a valid string and at least 1 chars and no longer than 256 chars', $validator->getDescription()); } public function testArrayOfStringsValidation() @@ -269,7 +269,7 @@ public function testArrayOfStringsValidation() 'feedback' => 'team@appwrite.io', ]))); - $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and no longer than 55 chars', $validator->getDescription()); + $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and at least 1 chars and no longer than 55 chars', $validator->getDescription()); $this->assertEquals(false, $validator->isValid(new Document([ '$collection' => ID::custom('posts'), @@ -282,7 +282,7 @@ public function testArrayOfStringsValidation() 'feedback' => 'team@appwrite.io', ]))); - $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and no longer than 55 chars', $validator->getDescription()); + $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and at least 1 chars and no longer than 55 chars', $validator->getDescription()); $this->assertEquals(true, $validator->isValid(new Document([ '$collection' => ID::custom('posts'), @@ -307,7 +307,7 @@ public function testArrayOfStringsValidation() 'feedback' => 'team@appwrite.io', ]))); - $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and no longer than 55 chars', $validator->getDescription()); + $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and at least 1 chars and no longer than 55 chars', $validator->getDescription()); } public function testIntegerValidation()