diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b51d5e7..d6f3380 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,7 +1,9 @@ name: Tests on: pull_request: - push: { branches: [main] } + push: + branches: + - main jobs: tests: @@ -12,8 +14,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Run Tests run: | - docker-compose up -d --build && sleep 5 && docker compose exec tests php ./vendor/bin/phpunit \ No newline at end of file + docker compose up -d --build + sleep 5 + docker compose exec tests php vendor/bin/phpunit \ No newline at end of file diff --git a/.gitignore b/.gitignore index 316c952..d16782c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -composer.lock /vendor/ /.idea/ *.cache diff --git a/Dockerfile b/Dockerfile index f62c026..bc56b8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,35 @@ -FROM supabase/postgres:15.1.0.96 as supabase-db +FROM supabase/postgres:15.1.0.96 AS supabase-db + COPY tests/Migration/resources/supabase/1_globals.sql /docker-entrypoint-initdb.d/1_globals.sql COPY tests/Migration/resources/supabase/2_main.sql /docker-entrypoint-initdb.d/2_main.sql + RUN rm -rf /docker-entrypoint-initdb.d/migrate.sh -FROM postgres:alpine3.18 as nhost-db +FROM postgres:alpine3.18 AS nhost-db + COPY tests/Migration/resources/nhost/1_globals.sql /docker-entrypoint-initdb.d/1_globals.sql COPY tests/Migration/resources/nhost/2_main.sql /docker-entrypoint-initdb.d/2_main.sql -FROM composer:2.0 as composer -WORKDIR /usr/local/src/ -COPY composer.json /usr/local/src/ +FROM composer:2.0 AS composer + +COPY composer.json /app +COPY composer.lock /app + RUN composer install --ignore-platform-reqs -FROM php:8.1.21-fpm-alpine3.18 as tests +FROM php:8.3.10-cli-alpine3.20 AS tests + # Postgres RUN set -ex \ && apk --no-cache add postgresql-libs postgresql-dev \ && docker-php-ext-install pdo pdo_pgsql \ && apk del postgresql-dev + COPY ./src /app/src COPY ./tests /app/src/tests -COPY --from=composer /usr/local/src/vendor /app/vendor + +COPY --from=composer /app/vendor /app/vendor + +WORKDIR /app + CMD tail -f /dev/null diff --git a/bin/MigrationCLI.php b/bin/MigrationCLI.php index f6024f0..68779b4 100644 --- a/bin/MigrationCLI.php +++ b/bin/MigrationCLI.php @@ -1,8 +1,15 @@ 'databases', + '$id' => 'collections', + 'name' => 'Collections', + 'attributes' => [ + [ + '$id' => 'databaseInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'databaseId', + 'type' => Database::VAR_STRING, + 'signed' => true, + 'size' => Database::LENGTH_KEY, + 'format' => '', + 'filters' => [], + 'required' => true, + 'default' => null, + 'array' => false, + ], + [ + '$id' => 'name', + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'enabled', + 'type' => Database::VAR_BOOLEAN, + 'signed' => true, + 'size' => 0, + 'format' => '', + 'filters' => [], + 'required' => true, + 'default' => null, + 'array' => false, + ], + [ + '$id' => 'documentSecurity', + 'type' => Database::VAR_BOOLEAN, + 'signed' => true, + 'size' => 0, + 'format' => '', + 'filters' => [], + 'required' => true, + 'default' => null, + 'array' => false, + ], + [ + '$id' => 'attributes', + 'type' => Database::VAR_STRING, + 'size' => 1000000, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => ['subQueryAttributes'], + ], + [ + '$id' => 'indexes', + 'type' => Database::VAR_STRING, + 'size' => 1000000, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => ['subQueryIndexes'], + ], + [ + '$id' => 'search', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [ + [ + '$id' => '_fulltext_search', + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['search'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => '_key_name', + 'type' => Database::INDEX_KEY, + 'attributes' => ['name'], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], + ], + [ + '$id' => '_key_enabled', + 'type' => Database::INDEX_KEY, + 'attributes' => ['enabled'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ], + [ + '$id' => '_key_documentSecurity', + 'type' => Database::INDEX_KEY, + 'attributes' => ['documentSecurity'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ], + ], + ]; + /** * Prints the current status of migrations as a table after wiping the screen */ - public function drawFrame() + public function drawFrame(): void { - echo chr(27).chr(91).'H'.chr(27).chr(91).'J'; + echo chr(27) . chr(91) . 'H' . chr(27) . chr(91) . 'J'; $statusCounters = $this->transfer->getStatusCounters(); @@ -42,39 +169,39 @@ public function drawFrame() // Render Errors $destErrors = $this->destination->getErrors(); - if (! empty($destErrors)) { + if (!empty($destErrors)) { echo "\n\nDestination Errors:\n"; foreach ($destErrors as $error) { /** @var Utopia\Migration\Exception $error */ - echo $error->getResourceName().'['.$error->getResourceId().'] - '.$error->getMessage()."\n"; + echo $error->getResourceName() . '[' . $error->getResourceId() . '] - ' . $error->getMessage() . "\n"; } } $sourceErrors = $this->source->getErrors(); - if (! empty($sourceErrors)) { + if (!empty($sourceErrors)) { echo "\n\nSource Errors:\n"; foreach ($sourceErrors as $error) { /** @var Utopia\Migration\Exception $error */ - echo $error->getResourceGroup().'['.$error->getResourceId().'] - '.$error->getMessage()."\n"; + echo $error->getResourceGroup() . '[' . $error->getResourceId() . '] - ' . $error->getMessage() . "\n"; } } // Render Warnings $sourceWarnings = $this->source->getWarnings(); - if (! empty($sourceWarnings)) { + if (!empty($sourceWarnings)) { echo "\n\nSource Warnings:\n"; foreach ($sourceWarnings as $warning) { /** @var Utopia\Migration\Warning $warning */ - echo $warning->getResourceName().'['.$warning->getResourceId().'] - '.$warning->getMessage()."\n"; + echo $warning->getResourceName() . '[' . $warning->getResourceId() . '] - ' . $warning->getMessage() . "\n"; } } $destWarnings = $this->destination->getWarnings(); - if (! empty($destWarnings)) { + if (!empty($destWarnings)) { echo "\n\nDestination Warnings:\n"; foreach ($destWarnings as $warning) { /** @var Utopia\Migration\Warning $warning */ - echo $warning->getResourceName().'['.$warning->getResourceId().'] - '.$warning->getMessage()."\n"; + echo $warning->getResourceName() . '[' . $warning->getResourceId() . '] - ' . $warning->getMessage() . "\n"; } } } @@ -99,7 +226,7 @@ public function getSource(): Source ); case 'firebase': return new Firebase( - json_decode(file_get_contents(__DIR__.'/serviceAccount.json'), true) + json_decode(file_get_contents(__DIR__ . '/serviceAccount.json'), true) ); case 'nhost': return new NHost( @@ -122,7 +249,9 @@ public function getDestination(): Destination return new DestinationsAppwrite( $_ENV['DESTINATION_APPWRITE_TEST_PROJECT'], $_ENV['DESTINATION_APPWRITE_TEST_ENDPOINT'], - $_ENV['DESTINATION_APPWRITE_TEST_KEY'] + $_ENV['DESTINATION_APPWRITE_TEST_KEY'], + $this->getDatabase(), + self::STRUCTURE ); case 'local': return new Local('./localBackup'); @@ -131,7 +260,131 @@ public function getDestination(): Destination } } - public function start() + public function getDatabase(): Database + { + Database::addFilter( + 'subQueryAttributes', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + $attributes = $database->find('attributes', [ + Query::equal('collectionInternalId', [$document->getInternalId()]), + Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), + Query::limit($database->getLimitForAttributes()), + ]); + + foreach ($attributes as $attribute) { + if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) { + $options = $attribute->getAttribute('options'); + foreach ($options as $key => $value) { + $attribute->setAttribute($key, $value); + } + $attribute->removeAttribute('options'); + } + } + + return $attributes; + } + ); + + Database::addFilter( + 'subQueryIndexes', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database + ->find('indexes', [ + Query::equal('collectionInternalId', [$document->getInternalId()]), + Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]), + Query::limit($database->getLimitForIndexes()), + ]); + } + ); + + Database::addFilter( + 'casting', + function (mixed $value) { + return json_encode(['value' => $value], JSON_PRESERVE_ZERO_FRACTION); + }, + function (mixed $value) { + if (is_null($value)) { + return; + } + + return json_decode($value, true)['value']; + } + ); + + Database::addFilter( + 'enum', + function (mixed $value, Document $attribute) { + if ($attribute->isSet('elements')) { + $attribute->removeAttribute('elements'); + } + + return $value; + }, + function (mixed $value, Document $attribute) { + $formatOptions = \json_decode($attribute->getAttribute('formatOptions', '[]'), true); + if (isset($formatOptions['elements'])) { + $attribute->setAttribute('elements', $formatOptions['elements']); + } + + return $value; + } + ); + + Database::addFilter( + 'range', + function (mixed $value, Document $attribute) { + if ($attribute->isSet('min')) { + $attribute->removeAttribute('min'); + } + if ($attribute->isSet('max')) { + $attribute->removeAttribute('max'); + } + + return $value; + }, + function (mixed $value, Document $attribute) { + $formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true); + if (isset($formatOptions['min']) || isset($formatOptions['max'])) { + $attribute + ->setAttribute('min', $formatOptions['min']) + ->setAttribute('max', $formatOptions['max']) + ; + } + + return $value; + } + ); + + $database = new Database( + new MariaDB(new PDO( + $_ENV['DESTINATION_APPWRITE_TEST_DSN'], + $_ENV['DESTINATION_APPWRITE_TEST_USER'], + $_ENV['DESTINATION_APPWRITE_TEST_PASSWORD'], + [ + PDO::ATTR_TIMEOUT => 3, + PDO::ATTR_PERSISTENT => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => true, + PDO::ATTR_STRINGIFY_FETCHES => true + ], + )), + new Cache(new None()) + ); + + $database + ->setDatabase('appwrite') + ->setNamespace('_' . $_ENV['DESTINATION_APPWRITE_TEST_NAMESPACE']); + + return $database; + } + + public function start(): void { $dotenv = Dotenv::createImmutable(__DIR__); $dotenv->load(); @@ -156,12 +409,12 @@ public function start() /** * Run Transfer */ - $this->transfer->run( + Authorization::skip(fn () => $this->transfer->run( $this->source->getSupportedResources(), - function (array $resources) { + function () { $this->drawFrame(); } - ); + )); } } diff --git a/composer.json b/composer.json index ccc4648..450cd5c 100644 --- a/composer.json +++ b/composer.json @@ -11,20 +11,35 @@ } }, "autoload-dev": { - "psr-4": {"Utopia\\Tests\\": "tests/Migration"} + "psr-4": { + "Utopia\\Tests\\": "tests/Migration" + } }, "scripts": { + "test": "./vendor/bin/phpunit", "lint": "./vendor/bin/pint --test", - "format": "./vendor/bin/pint" + "format": "./vendor/bin/pint", + "check": "./vendor/bin/phpstan analyse --level=8 --memory-limit 512M" }, "require": { - "php": "8.*", - "appwrite/appwrite": "10.1.0" + "php": "8.3.*", + "ext-curl": "*", + "ext-openssl": "*", + "appwrite/appwrite": "11.1.*", + "utopia-php/database": "0.52.*", + "utopia-php/storage": "0.18.*", + "utopia-php/dsn": "0.2.*", + "utopia-php/framework": "0.33.*" }, "require-dev": { - "phpunit/phpunit": "9.*", - "vlucas/phpdotenv": "5.*", - "laravel/pint": "1.*", - "utopia-php/cli": "^0.18.0" + "ext-pdo": "*", + "phpunit/phpunit": "11.2.*", + "vlucas/phpdotenv": "5.6.*", + "laravel/pint": "1.17.*", + "phpstan/phpstan": "1.11.*", + "utopia-php/cli": "0.16.*" + }, + "platform": { + "php": "8.3" } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..d37ed66 --- /dev/null +++ b/composer.lock @@ -0,0 +1,2830 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "09c49772fb03e8a2c305a6e24f3e2bf7", + "packages": [ + { + "name": "appwrite/appwrite", + "version": "11.1.0", + "source": { + "type": "git", + "url": "https://github.com/appwrite/sdk-for-php.git", + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/1d043f543acdb17b9fdb440b1b2dd208e400bad3", + "reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">=7.1.0" + }, + "require-dev": { + "mockery/mockery": "^1.6.6", + "phpunit/phpunit": "^10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Appwrite\\": "src/Appwrite" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API", + "support": { + "email": "team@appwrite.io", + "issues": "https://github.com/appwrite/sdk-for-php/issues", + "source": "https://github.com/appwrite/sdk-for-php/tree/11.1.0", + "url": "https://appwrite.io/support" + }, + "time": "2024-06-26T07:03:23+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" + }, + "time": "2024-03-08T09:58:59+00:00" + }, + { + "name": "mongodb/mongodb", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/mongodb/mongo-php-library.git", + "reference": "b0bbd657f84219212487d01a8ffe93a789e1e488" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/b0bbd657f84219212487d01a8ffe93a789e1e488", + "reference": "b0bbd657f84219212487d01a8ffe93a789e1e488", + "shasum": "" + }, + "require": { + "ext-hash": "*", + "ext-json": "*", + "ext-mongodb": "^1.11.0", + "jean85/pretty-package-versions": "^1.2 || ^2.0.1", + "php": "^7.1 || ^8.0", + "symfony/polyfill-php80": "^1.19" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "squizlabs/php_codesniffer": "^3.6", + "symfony/phpunit-bridge": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "MongoDB\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Andreas Braun", + "email": "andreas.braun@mongodb.com" + }, + { + "name": "Jeremy Mikola", + "email": "jmikola@gmail.com" + } + ], + "description": "MongoDB driver library", + "homepage": "https://jira.mongodb.org/browse/PHPLIB", + "keywords": [ + "database", + "driver", + "mongodb", + "persistence" + ], + "support": { + "issues": "https://github.com/mongodb/mongo-php-library/issues", + "source": "https://github.com/mongodb/mongo-php-library/tree/1.10.0" + }, + "time": "2021-10-20T22:22:37+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T15:07:36+00:00" + }, + { + "name": "utopia-php/cache", + "version": "0.10.2", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/cache.git", + "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/b22c6eb6d308de246b023efd0fc9758aee8b8247", + "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-memcached": "*", + "ext-redis": "*", + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.9.x-dev", + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.13.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Cache\\": "src/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple cache library to manage application cache storing, loading and purging", + "keywords": [ + "cache", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/cache/issues", + "source": "https://github.com/utopia-php/cache/tree/0.10.2" + }, + "time": "2024-06-25T20:36:35+00:00" + }, + { + "name": "utopia-php/database", + "version": "0.52.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/database.git", + "reference": "0b48921dd5e9e07529983f954cf987e7d4461f6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/database/zipball/0b48921dd5e9e07529983f954cf987e7d4461f6e", + "reference": "0b48921dd5e9e07529983f954cf987e7d4461f6e", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-pdo": "*", + "php": ">=8.0", + "utopia-php/cache": "0.10.*", + "utopia-php/framework": "0.33.*", + "utopia-php/mongo": "0.3.*" + }, + "require-dev": { + "fakerphp/faker": "1.23.*", + "laravel/pint": "1.17.*", + "pcov/clobber": "2.0.*", + "phpstan/phpstan": "1.11.*", + "phpunit/phpunit": "9.6.*", + "rregeer/phpunit-coverage-check": "0.3.*", + "swoole/ide-helper": "5.1.3", + "utopia-php/cli": "0.14.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Database\\": "src/Database" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple library to manage application persistence using multiple database adapters", + "keywords": [ + "database", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/database/issues", + "source": "https://github.com/utopia-php/database/tree/0.52.0" + }, + "time": "2024-08-21T08:11:14+00:00" + }, + { + "name": "utopia-php/dsn", + "version": "0.2.1", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/dsn.git", + "reference": "42ee37a3d1785100b2f69091c9d4affadb6846eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/dsn/zipball/42ee37a3d1785100b2f69091c9d4affadb6846eb", + "reference": "42ee37a3d1785100b2f69091c9d4affadb6846eb", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.6", + "vimeo/psalm": "4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\DSN\\": "src/DSN" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple library for parsing and managing Data Source Names ( DSNs )", + "keywords": [ + "dsn", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/dsn/issues", + "source": "https://github.com/utopia-php/dsn/tree/0.2.1" + }, + "time": "2024-05-07T02:01:25+00:00" + }, + { + "name": "utopia-php/framework", + "version": "0.33.8", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/http.git", + "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/http/zipball/a7f577540a25cb90896fef2b64767bf8d700f3c5", + "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple, light and advanced PHP framework", + "keywords": [ + "framework", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/http/issues", + "source": "https://github.com/utopia-php/http/tree/0.33.8" + }, + "time": "2024-08-15T14:10:09+00:00" + }, + { + "name": "utopia-php/mongo", + "version": "0.3.1", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/mongo.git", + "reference": "52326a9a43e2d27ff0c15c48ba746dacbe9a7aee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/mongo/zipball/52326a9a43e2d27ff0c15c48ba746dacbe9a7aee", + "reference": "52326a9a43e2d27ff0c15c48ba746dacbe9a7aee", + "shasum": "" + }, + "require": { + "ext-mongodb": "*", + "mongodb/mongodb": "1.10.0", + "php": ">=8.0" + }, + "require-dev": { + "fakerphp/faker": "^1.14", + "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.8.*", + "phpunit/phpunit": "^9.4", + "swoole/ide-helper": "4.8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Mongo\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eldad Fux", + "email": "eldad@appwrite.io" + }, + { + "name": "Wess", + "email": "wess@appwrite.io" + } + ], + "description": "A simple library to manage Mongo database", + "keywords": [ + "database", + "mongo", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/mongo/issues", + "source": "https://github.com/utopia-php/mongo/tree/0.3.1" + }, + "time": "2023-09-01T17:25:28+00:00" + }, + { + "name": "utopia-php/storage", + "version": "0.18.4", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/storage.git", + "reference": "94ab8758fabcefee5c5fa723616e45719833f922" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/94ab8758fabcefee5c5fa723616e45719833f922", + "reference": "94ab8758fabcefee5c5fa723616e45719833f922", + "shasum": "" + }, + "require": { + "ext-brotli": "*", + "ext-fileinfo": "*", + "ext-lz4": "*", + "ext-snappy": "*", + "ext-xz": "*", + "ext-zlib": "*", + "ext-zstd": "*", + "php": ">=8.0", + "utopia-php/framework": "0.*.*", + "utopia-php/system": "0.*.*" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Storage\\": "src/Storage" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple Storage library to manage application storage", + "keywords": [ + "framework", + "php", + "storage", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/storage/issues", + "source": "https://github.com/utopia-php/storage/tree/0.18.4" + }, + "time": "2024-04-02T08:24:09+00:00" + }, + { + "name": "utopia-php/system", + "version": "0.8.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/system.git", + "reference": "a2cbfb3c69b9ecb8b6f06c5774f3cf279ea7665e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/system/zipball/a2cbfb3c69b9ecb8b6f06c5774f3cf279ea7665e", + "reference": "a2cbfb3c69b9ecb8b6f06c5774f3cf279ea7665e", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "require-dev": { + "laravel/pint": "1.13.*", + "phpstan/phpstan": "1.10.*", + "phpunit/phpunit": "9.6.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\System\\": "src/System" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eldad Fux", + "email": "eldad@appwrite.io" + }, + { + "name": "Torsten Dittmann", + "email": "torsten@appwrite.io" + } + ], + "description": "A simple library for obtaining information about the host's system.", + "keywords": [ + "framework", + "php", + "system", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/system/issues", + "source": "https://github.com/utopia-php/system/tree/0.8.0" + }, + "time": "2024-04-01T10:22:28+00:00" + } + ], + "packages-dev": [ + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.17.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.61.1", + "illuminate/view": "^10.48.18", + "larastan/larastan": "^2.9.8", + "laravel-zero/framework": "^10.4.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^1.15.1", + "pestphp/pest": "^2.35.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2024-08-06T15:11:54+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-06-12T14:39:25+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.1.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" + }, + "time": "2024-07-01T20:03:41+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.3", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:41:07+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.11.11", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/707c2aed5d8d0075666e673a5e71440c1d01a5a3", + "reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-08-19T14:37:29+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ebdffc9e09585dafa71b9bffcdb0a229d4704c45", + "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.1.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "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": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:37:56+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6ed896bf50bbbfe4d504a33ed5886278c78e4a26", + "reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:06:37+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.2.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "c197bbaaca360efda351369bf1fd9cc1ca6bcbf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c197bbaaca360efda351369bf1fd9cc1ca6bcbf7", + "reference": "c197bbaaca360efda351369bf1fd9cc1ca6bcbf7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.5", + "phpunit/php-file-iterator": "^5.0.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.1", + "sebastian/comparator": "^6.0.1", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.1.3", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.0.1", + "sebastian/version": "^5.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.2-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.2.9" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-07-30T11:09:23+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "6bb7d09d6623567178cf54126afa9c2310114268" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/6bb7d09d6623567178cf54126afa9c2310114268", + "reference": "6bb7d09d6623567178cf54126afa9c2310114268", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:44:28+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/450d8f237bd611c45b5acf0733ce43e6bb280f81", + "reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-12T06:07:25+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:54:44+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", + "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:56:19+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:10:34+00:00" + }, + { + "name": "sebastian/type", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb6a6566f9589e86661291d13eba708cce5eb4aa", + "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:11:49+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/45c9debb7d039ce9b97de2f749c2cf5832a06ac4", + "reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:13:08+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T15:07:36+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-06-19T12:30:46+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "utopia-php/cli", + "version": "0.16.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/cli.git", + "reference": "5b936638c90c86d1bae83d0dbe81fe14d12ff8ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/5b936638c90c86d1bae83d0dbe81fe14d12ff8ff", + "reference": "5b936638c90c86d1bae83d0dbe81fe14d12ff8ff", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "utopia-php/framework": "0.*.*" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.6", + "vimeo/psalm": "4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\CLI\\": "src/CLI" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple CLI library to manage command line applications", + "keywords": [ + "cli", + "command line", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/cli/issues", + "source": "https://github.com/utopia-php/cli/tree/0.16.0" + }, + "time": "2023-08-05T13:13:08+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.1", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:52:34+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "8.3.*", + "ext-curl": "*", + "ext-openssl": "*" + }, + "platform-dev": { + "ext-pdo": "*" + }, + "plugin-api-version": "2.6.0" +} diff --git a/docker-compose.yml b/docker-compose.yml index a151232..2d7c30d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3' - services: supabase-db: build: @@ -46,14 +44,12 @@ services: tests: build: context: . - target: tests networks: - tests volumes: - - ./tests:/app/tests - ./src:/app/src + - ./tests:/app/tests - ./phpunit.xml:/app/phpunit.xml - working_dir: /app depends_on: - supabase-db - nhost-db diff --git a/phpcs.xml b/phpcs.xml deleted file mode 100644 index 89c508c..0000000 --- a/phpcs.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - ./src - ./tests - - - - * - - - - * - - \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 9a35000..fad0dec 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,16 +1,17 @@ - + + ./tests/Migration/E2E + + ./tests/Migration/Unit + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..288cbb8 --- /dev/null +++ b/pint.json @@ -0,0 +1,17 @@ +{ + "preset": "psr12", + "exclude": [], + "rules": { + "array_indentation": true, + "single_import_per_statement": true, + "simplified_null_return": true, + "ordered_imports": { + "sort_algorithm": "alpha", + "imports_order": [ + "const", + "class", + "function" + ] + } + } +} diff --git a/src/Migration/Cache.php b/src/Migration/Cache.php index cc0ce77..e9d2de1 100644 --- a/src/Migration/Cache.php +++ b/src/Migration/Cache.php @@ -2,6 +2,7 @@ namespace Utopia\Migration; +use Utopia\Migration\Resources\Functions\Deployment; use Utopia\Migration\Resources\Storage\File; /** @@ -11,7 +12,10 @@ */ class Cache { - protected $cache = []; + /** + * @var array> $cache + */ + protected array $cache = []; public function __construct() { @@ -23,15 +27,14 @@ public function __construct() * * Places the resource in the cache, in the cache backend this also gets assigned a unique ID. * - * @param resource $resource - * @return void */ - public function add($resource) + public function add(Resource $resource): void { if (! $resource->getInternalId()) { $resourceId = uniqid(); if (isset($this->cache[$resource->getName()][$resourceId])) { $resourceId = uniqid(); + // todo: $resourceId is not used? } $resource->setInternalId(uniqid()); } @@ -47,10 +50,10 @@ public function add($resource) /** * Add All Resources * - * @param resource[] $resources + * @param array $resources * @return void */ - public function addAll(array $resources) + public function addAll(array $resources): void { foreach ($resources as $resource) { $this->add($resource); @@ -63,10 +66,10 @@ public function addAll(array $resources) * Updates the resource in the cache, if the resource does not exist in the cache an exception is thrown. * Use Add to add a new resource to the cache. * - * @param resource $resource + * @param Resource $resource * @return void */ - public function update($resource) + public function update(Resource $resource): void { if (! in_array($resource->getName(), $this->cache)) { $this->add($resource); @@ -75,7 +78,11 @@ public function update($resource) $this->cache[$resource->getName()][$resource->getInternalId()] = $resource; } - public function updateAll($resources) + /** + * @param array $resources + * @return void + */ + public function updateAll(array $resources): void { foreach ($resources as $resource) { $this->update($resource); @@ -87,10 +94,11 @@ public function updateAll($resources) * * Removes the resource from the cache, if the resource does not exist in the cache an exception is thrown. * - * @param resource $resource + * @param Resource $resource * @return void + * @throws \Exception */ - public function remove($resource) + public function remove(Resource $resource): void { if (! in_array($resource, $this->cache[$resource->getName()])) { throw new \Exception('Resource does not exist in cache'); @@ -102,10 +110,10 @@ public function remove($resource) /** * Get Resources * - * @param string|resource $resourceType - * @return resource[] + * @param string|Resource $resource + * @return array */ - public function get($resource) + public function get(string|Resource $resource): array { if (is_string($resource)) { return $this->cache[$resource] ?? []; @@ -117,9 +125,9 @@ public function get($resource) /** * Get All Resources * - * @return array + * @return array> */ - public function getAll() + public function getAll(): array { return $this->cache; } @@ -131,7 +139,7 @@ public function getAll() * * @return void */ - public function wipe() + public function wipe(): void { $this->cache = []; } diff --git a/src/Migration/Destination.php b/src/Migration/Destination.php index bde243e..a20919e 100644 --- a/src/Migration/Destination.php +++ b/src/Migration/Destination.php @@ -9,17 +9,11 @@ abstract class Destination extends Target */ protected Source $source; - /** - * Get Source - */ public function getSource(): Source { return $this->source; } - /** - * Set Soruce - */ public function setSource(Source $source): self { $this->source = $source; @@ -30,20 +24,30 @@ public function setSource(Source $source): self /** * Transfer Resources to Destination from Source callback * - * @param string[] $resources Resources to transfer - * @param callable $callback Callback to run after transfer + * @param array $resources Resources to transfer + * @param callable $callback Callback to run after transfer + * @param string $rootResourceId Root resource ID, If enabled you can only transfer a single root resource */ - public function run(array $resources, callable $callback): void - { - $this->source->run($resources, function (array $resources) use ($callback) { - $this->import($resources, $callback); - }); + public function run( + array $resources, + callable $callback, + string $rootResourceId = '', + string $rootResourceType = '', + ): void { + $this->source->run( + $resources, + function (array $resources) use ($callback) { + $this->import($resources, $callback); + }, + $rootResourceId, + $rootResourceType, + ); } /** * Import Resources * - * @param resource[] $resources Resources to import + * @param Resource[] $resources Resources to import * @param callable $callback Callback to run after import */ abstract protected function import(array $resources, callable $callback): void; diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 452cc36..05d71cd 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -2,13 +2,29 @@ namespace Utopia\Migration\Destinations; +use Appwrite\AppwriteException; use Appwrite\Client; +use Appwrite\Enums\Compression; +use Appwrite\Enums\PasswordHash; +use Appwrite\Enums\Runtime; use Appwrite\InputFile; -use Appwrite\Services\Databases; use Appwrite\Services\Functions; use Appwrite\Services\Storage; use Appwrite\Services\Teams; use Appwrite\Services\Users; +use Override; +use Utopia\Database\Database as UtopiaDatabase; +use Utopia\Database\Document as UtopiaDocument; +use Utopia\Database\Exception as DatabaseException; +use Utopia\Database\Exception\Authorization as AuthorizationException; +use Utopia\Database\Exception\Duplicate as DuplicateException; +use Utopia\Database\Exception\Limit as LimitException; +use Utopia\Database\Exception\Structure as StructureException; +use Utopia\Database\Helpers\ID; +use Utopia\Database\Helpers\Permission; +use Utopia\Database\Query; +use Utopia\Database\Validator\Index as IndexValidator; +use Utopia\Database\Validator\Structure; use Utopia\Migration\Destination; use Utopia\Migration\Exception; use Utopia\Migration\Resource; @@ -17,34 +33,49 @@ use Utopia\Migration\Resources\Auth\Team; use Utopia\Migration\Resources\Auth\User; use Utopia\Migration\Resources\Database\Attribute; -use Utopia\Migration\Resources\Database\Attributes\DateTime; -use Utopia\Migration\Resources\Database\Attributes\Decimal; -use Utopia\Migration\Resources\Database\Attributes\Email; -use Utopia\Migration\Resources\Database\Attributes\Enum; -use Utopia\Migration\Resources\Database\Attributes\IP; -use Utopia\Migration\Resources\Database\Attributes\Relationship; -use Utopia\Migration\Resources\Database\Attributes\Text; use Utopia\Migration\Resources\Database\Collection; use Utopia\Migration\Resources\Database\Database; use Utopia\Migration\Resources\Database\Document; +use Utopia\Migration\Resources\Database\Index; use Utopia\Migration\Resources\Functions\Deployment; use Utopia\Migration\Resources\Functions\EnvVar; use Utopia\Migration\Resources\Functions\Func; use Utopia\Migration\Resources\Storage\Bucket; use Utopia\Migration\Resources\Storage\File; -use Utopia\Migration\Resources\Storage\Index; use Utopia\Migration\Transfer; class Appwrite extends Destination { protected Client $client; - protected string $project; protected string $key; - public function __construct(string $project, string $endpoint, string $key) - { + + private Functions $functions; + private Storage $storage; + private Teams $teams; + private Users $users; + + /** + * @var array + */ + private array $documentBuffer = []; + + /** + * @param string $project + * @param string $endpoint + * @param string $key + * @param UtopiaDatabase $database + * @param array> $collectionStructure + */ + public function __construct( + string $project, + string $endpoint, + string $key, + protected UtopiaDatabase $database, + protected array $collectionStructure + ) { $this->project = $project; $this->endpoint = $endpoint; $this->key = $key; @@ -53,6 +84,11 @@ public function __construct(string $project, string $endpoint, string $key) ->setEndpoint($endpoint) ->setProject($project) ->setKey($key); + + $this->functions = new Functions($this->client); + $this->storage = new Storage($this->client); + $this->teams = new Teams($this->client); + $this->users = new Users($this->client); } public static function getName(): string @@ -60,6 +96,9 @@ public static function getName(): string return 'Appwrite'; } + /** + * @return array + */ public static function getSupportedResources(): array { return [ @@ -83,160 +122,120 @@ public static function getSupportedResources(): array Resource::TYPE_FUNCTION, Resource::TYPE_DEPLOYMENT, Resource::TYPE_ENVIRONMENT_VARIABLE, - - // Settings ]; } + /** + * @param array $resources + * @return array + * @throws AppwriteException + */ + #[Override] public function report(array $resources = []): array { if (empty($resources)) { $resources = $this->getSupportedResources(); } - $databases = new Databases($this->client); - $functions = new Functions($this->client); - $storage = new Storage($this->client); - $teams = new Teams($this->client); - $users = new Users($this->client); + $scope = ''; - $currentPermission = ''; // Most of these API calls are purposely wrong. Appwrite will throw a 403 before a 400. // We want to make sure the API key has full read and write access to the project. - try { // Auth - if (in_array(Resource::TYPE_USER, $resources)) { - $currentPermission = 'users.read'; - $users->list(); - - $currentPermission = 'users.write'; - $users->create('', '', ''); - } - - if (in_array(Resource::TYPE_TEAM, $resources)) { - $currentPermission = 'teams.read'; - $teams->list(); - - $currentPermission = 'teams.write'; - $teams->create('', ''); - } - - if (in_array(Resource::TYPE_MEMBERSHIP, $resources)) { - $currentPermission = 'memberships.read'; - $teams->listMemberships(''); - - $currentPermission = 'memberships.write'; - $teams->createMembership('', [], ''); - } - - // Database - if (in_array(Resource::TYPE_DATABASE, $resources)) { - $currentPermission = 'database.read'; - $databases->list(); - - $currentPermission = 'database.write'; - $databases->create('', ''); - } - - if (in_array(Resource::TYPE_COLLECTION, $resources)) { - $currentPermission = 'collections.read'; - $databases->listCollections(''); - - $currentPermission = 'collections.write'; - $databases->createCollection('', '', ''); - } + if (\in_array(Resource::TYPE_USER, $resources)) { + $scope = 'users.read'; + $this->users->list(); - if (in_array(Resource::TYPE_ATTRIBUTE, $resources)) { - $currentPermission = 'attributes.read'; - $databases->listAttributes('', ''); - - $currentPermission = 'attributes.write'; - $databases->createStringAttribute('', '', '', 0, false); + $scope = 'users.write'; + $this->users->create(''); } - if (in_array(Resource::TYPE_INDEX, $resources)) { - $currentPermission = 'indexes.read'; - $databases->listIndexes('', ''); + if (\in_array(Resource::TYPE_TEAM, $resources)) { + $scope = 'teams.read'; + $this->teams->list(); - $currentPermission = 'indexes.write'; - $databases->createIndex('', '', '', '', []); + $scope = 'teams.write'; + $this->teams->create('', ''); } - if (in_array(Resource::TYPE_DOCUMENT, $resources)) { - $currentPermission = 'documents.read'; - $databases->listDocuments('', ''); + if (\in_array(Resource::TYPE_MEMBERSHIP, $resources)) { + $scope = 'memberships.read'; + $this->teams->listMemberships(''); - $currentPermission = 'documents.write'; - $databases->createDocument('', '', '', []); + $scope = 'memberships.write'; + $this->teams->createMembership('', [], ''); } // Storage - if (in_array(Resource::TYPE_BUCKET, $resources)) { - $currentPermission = 'storage.read'; - $storage->listBuckets(); + if (\in_array(Resource::TYPE_BUCKET, $resources)) { + $scope = 'storage.read'; + $this->storage->listBuckets(); - $currentPermission = 'storage.write'; - $storage->createBucket('', ''); + $scope = 'storage.write'; + $this->storage->createBucket('', ''); } - if (in_array(Resource::TYPE_FILE, $resources)) { - $currentPermission = 'files.read'; - $storage->listFiles(''); + if (\in_array(Resource::TYPE_FILE, $resources)) { + $scope = 'files.read'; + $this->storage->listFiles(''); - $currentPermission = 'files.write'; - $storage->createFile('', '', new InputFile()); + $scope = 'files.write'; + $this->storage->createFile('', '', new InputFile()); } // Functions - if (in_array(Resource::TYPE_FUNCTION, $resources)) { - $currentPermission = 'functions.read'; - $functions->list(); + if (\in_array(Resource::TYPE_FUNCTION, $resources)) { + $scope = 'functions.read'; + $this->functions->list(); - $currentPermission = 'functions.write'; - $functions->create('', '', ''); + $scope = 'functions.write'; + $this->functions->create('', '', Runtime::NODE180()); } - return []; - } catch (\Throwable $exception) { - if ($exception->getCode() === 403) { - throw new \Exception('Missing permission: '.$currentPermission); - } else { - throw $exception; + } catch (AppwriteException $e) { + if ($e->getCode() === 403) { + throw new \Exception('Missing scope: ' . $scope, previous: $e); } + throw $e; } + + return []; } + /** + * @param array $resources + * @param callable $callback + * @return void + */ + #[Override] protected function import(array $resources, callable $callback): void { if (empty($resources)) { return; } - foreach ($resources as $resource) { - /** @var resource $resource */ + $total = \count($resources); + + foreach ($resources as $index => $resource) { $resource->setStatus(Resource::STATUS_PROCESSING); + $isLast = $index === $total - 1; + try { - switch ($resource->getGroup()) { - case Transfer::GROUP_DATABASES: - $responseResource = $this->importDatabaseResource($resource); - break; - case Transfer::GROUP_STORAGE: - $responseResource = $this->importFileResource($resource); - break; - case Transfer::GROUP_AUTH: - $responseResource = $this->importAuthResource($resource); - break; - case Transfer::GROUP_FUNCTIONS: - $responseResource = $this->importFunctionResource($resource); - break; - } + $responseResource = match ($resource->getGroup()) { + Transfer::GROUP_DATABASES => $this->importDatabaseResource($resource, $isLast), + Transfer::GROUP_STORAGE => $this->importFileResource($resource), + Transfer::GROUP_AUTH => $this->importAuthResource($resource), + Transfer::GROUP_FUNCTIONS => $this->importFunctionResource($resource), + default => throw new \Exception('Invalid resource group'), + }; } catch (\Throwable $e) { if ($e->getCode() === 409) { $resource->setStatus(Resource::STATUS_SKIPPED, $e->getMessage()); } else { $resource->setStatus(Resource::STATUS_ERROR, $e->getMessage()); + $this->addError(new Exception( resourceName: $resource->getName(), resourceGroup: $resource->getGroup(), @@ -256,191 +255,686 @@ protected function import(array $resources, callable $callback): void $callback($resources); } - public function importDatabaseResource(Resource $resource): Resource + /** + * @throws AppwriteException + * @throws \Exception + * @throws \Throwable + */ + public function importDatabaseResource(Resource $resource, bool $isLast): Resource { - $databaseService = new Databases($this->client); - switch ($resource->getName()) { case Resource::TYPE_DATABASE: /** @var Database $resource */ - $databaseService->create($resource->getId(), $resource->getDBName()); + $success = $this->createDatabase($resource); break; case Resource::TYPE_COLLECTION: /** @var Collection $resource */ - $newCollection = $databaseService->createCollection( - $resource->getDatabase()->getId(), - $resource->getId(), - $resource->getCollectionName(), - $resource->getPermissions(), - $resource->getDocumentSecurity() - ); - $resource->setId($newCollection['$id']); - break; - case Resource::TYPE_INDEX: - /** @var Index $resource */ - $databaseService->createIndex( - $resource->getCollection()->getDatabase()->getId(), - $resource->getCollection()->getId(), - $resource->getKey(), - $resource->getType(), - $resource->getAttributes(), - $resource->getOrders() - ); + $success = $this->createCollection($resource); break; case Resource::TYPE_ATTRIBUTE: /** @var Attribute $resource */ - $this->createAttribute($resource); + $success = $this->createAttribute($resource); + break; + case Resource::TYPE_INDEX: + /** @var Index $resource */ + $success = $this->createIndex($resource); break; case Resource::TYPE_DOCUMENT: /** @var Document $resource */ - // Check if document has already been created by subcollection - $docExists = array_key_exists($resource->getId(), $this->cache->get(Resource::TYPE_DOCUMENT)); + $success = $this->createDocument($resource, $isLast); + break; + default: + $success = false; + break; + } - if ($docExists) { - $resource->setStatus(Resource::STATUS_SKIPPED, 'Document has been already created by relationship'); + if ($success) { + $resource->setStatus(Resource::STATUS_SUCCESS); + } - return $resource; - } + return $resource; + } - $databaseService->createDocument( - $resource->getDatabase()->getId(), - $resource->getCollection()->getId(), - $resource->getId(), - $resource->getData(), - $resource->getPermissions() - ); - break; + /** + * @throws AuthorizationException + * @throws StructureException + * @throws DatabaseException + */ + protected function createDatabase(Database $resource): bool + { + $resourceId = $resource->getId() == 'unique()' + ? ID::unique() + : $resource->getId(); + + $resource->setId($resourceId); + + $database = $this->database->createDocument('databases', new UtopiaDocument([ + '$id' => $resource->getId(), + 'name' => $resource->getDatabaseName(), + 'enabled' => true, + 'search' => implode(' ', [$resource->getId(), $resource->getDatabaseName()]), + ])); + + $resource->setInternalId($database->getInternalId()); + + $attributes = \array_map( + fn ($attr) => new UtopiaDocument($attr), + $this->collectionStructure['attributes'] + ); + $indexes = \array_map( + fn ($index) => new UtopiaDocument($index), + $this->collectionStructure['indexes'] + ); + + $this->database->createCollection( + 'database_' . $database->getInternalId(), + $attributes, + $indexes + ); + + return true; + } + + /** + * @throws AuthorizationException + * @throws DatabaseException + * @throws StructureException + * @throws Exception + */ + protected function createCollection(Collection $resource): bool + { + $resourceId = $resource->getId() == 'unique()' + ? ID::unique() + : $resource->getId(); + + $resource->setId($resourceId); + + $database = $this->database->getDocument( + 'databases', + $resource->getDatabase()->getId() + ); + + if ($database->isEmpty()) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Database not found', + ); } - $resource->setStatus(Resource::STATUS_SUCCESS); + $collection = $this->database->createDocument('database_' . $database->getInternalId(), new UtopiaDocument([ + '$id' => $resource->getId(), + 'databaseInternalId' => $database->getInternalId(), + 'databaseId' => $resource->getDatabase()->getId(), + '$permissions' => Permission::aggregate($resource->getPermissions()), + 'documentSecurity' => $resource->getDocumentSecurity(), + 'enabled' => true, + 'name' => $resource->getCollectionName(), + 'search' => implode(' ', [$resource->getId(), $resource->getCollectionName()]), + ])); + + $resource->setInternalId($collection->getInternalId()); + + $this->database->createCollection( + 'database_' . $database->getInternalId() . '_collection_' . $resource->getInternalId(), + permissions: $resource->getPermissions(), + documentSecurity: $resource->getDocumentSecurity() + ); - return $resource; + return true; } - public function createAttribute(Attribute $attribute): void + /** + * @throws AppwriteException + * @throws \Exception + * @throws \Throwable + */ + protected function createAttribute(Attribute $resource): bool { - $databaseService = new Databases($this->client); + $type = match ($resource->getType()) { + Attribute::TYPE_DATETIME => UtopiaDatabase::VAR_DATETIME, + Attribute::TYPE_BOOLEAN => UtopiaDatabase::VAR_BOOLEAN, + Attribute::TYPE_INTEGER => UtopiaDatabase::VAR_INTEGER, + Attribute::TYPE_FLOAT => UtopiaDatabase::VAR_FLOAT, + Attribute::TYPE_RELATIONSHIP => UtopiaDatabase::VAR_RELATIONSHIP, + Attribute::TYPE_STRING, + Attribute::TYPE_IP, + Attribute::TYPE_EMAIL, + Attribute::TYPE_URL, + Attribute::TYPE_ENUM => UtopiaDatabase::VAR_STRING, + default => throw new \Exception('Invalid resource type '.$resource->getType()), + }; + + $database = $this->database->getDocument( + 'databases', + $resource->getCollection()->getDatabase()->getId(), + ); + if ($database->isEmpty()) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Database not found', + ); + } - switch ($attribute->getTypeName()) { - case Attribute::TYPE_STRING: - /** @var Text $attribute */ - $databaseService->createStringAttribute($attribute->getCollection()->getDatabase()->getId(), $attribute->getCollection()->getId(), $attribute->getKey(), $attribute->getSize(), $attribute->getRequired(), $attribute->getDefault(), $attribute->getArray()); - break; - case Attribute::TYPE_INTEGER: - /** @var int $attribute */ - $databaseService->createIntegerAttribute($attribute->getCollection()->getDatabase()->getId(), $attribute->getCollection()->getId(), $attribute->getKey(), $attribute->getRequired(), $attribute->getMin(), $attribute->getMax() ?? null, $attribute->getDefault(), $attribute->getArray()); - break; - case Attribute::TYPE_FLOAT: - /** @var Decimal $attribute */ - $databaseService->createFloatAttribute($attribute->getCollection()->getDatabase()->getId(), $attribute->getCollection()->getId(), $attribute->getKey(), $attribute->getRequired(), null, null, $attribute->getDefault(), $attribute->getArray()); - break; - case Attribute::TYPE_BOOLEAN: - /** @var bool $attribute */ - $databaseService->createBooleanAttribute($attribute->getCollection()->getDatabase()->getId(), $attribute->getCollection()->getId(), $attribute->getKey(), $attribute->getRequired(), $attribute->getDefault(), $attribute->getArray()); - break; - case Attribute::TYPE_DATETIME: - /** @var DateTime $attribute */ - $databaseService->createDatetimeAttribute($attribute->getCollection()->getDatabase()->getId(), $attribute->getCollection()->getId(), $attribute->getKey(), $attribute->getRequired(), $attribute->getDefault(), $attribute->getArray()); - break; - case Attribute::TYPE_EMAIL: - /** @var Email $attribute */ - $databaseService->createEmailAttribute($attribute->getCollection()->getDatabase()->getId(), $attribute->getCollection()->getId(), $attribute->getKey(), $attribute->getRequired(), $attribute->getDefault(), $attribute->getArray()); - break; - case Attribute::TYPE_IP: - /** @var IP $attribute */ - $databaseService->createIpAttribute($attribute->getCollection()->getDatabase()->getId(), $attribute->getCollection()->getId(), $attribute->getKey(), $attribute->getRequired(), $attribute->getDefault(), $attribute->getArray()); - break; - case Attribute::TYPE_URL: - /** @var URLAttribute $attribute */ - $databaseService->createUrlAttribute($attribute->getCollection()->getDatabase()->getId(), $attribute->getCollection()->getId(), $attribute->getKey(), $attribute->getRequired(), $attribute->getDefault(), $attribute->getArray()); - break; - case Attribute::TYPE_ENUM: - /** @var Enum $attribute */ - $databaseService->createEnumAttribute($attribute->getCollection()->getDatabase()->getId(), $attribute->getCollection()->getId(), $attribute->getKey(), $attribute->getElements(), $attribute->getRequired(), $attribute->getDefault(), $attribute->getArray()); - break; - case Attribute::TYPE_RELATIONSHIP: - /** @var Relationship $attribute */ - $databaseService->createRelationshipAttribute( - $attribute->getCollection()->getDatabase()->getId(), - $attribute->getCollection()->getId(), - $attribute->getRelatedCollection(), - $attribute->getRelationType(), - $attribute->getTwoWay(), - $attribute->getKey(), - $attribute->getTwoWayKey(), - $attribute->getOnDelete() + $collection = $this->database->getDocument( + 'database_' . $database->getInternalId(), + $resource->getCollection()->getId(), + ); + if ($collection->isEmpty()) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Collection not found', + ); + } + + if (!empty($resource->getFormat())) { + if (!Structure::hasFormat($resource->getFormat(), $type)) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: "Format {$resource->getFormat()} not available for attribute type {$type}", ); - break; - default: - throw new \Exception('Invalid attribute type'); + } + } + if ($resource->isRequired() && $resource->getDefault() !== null) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Cannot set default value for required attribute', + ); + } + if ($resource->isArray() && $resource->getDefault() !== null) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Cannot set default value for array attribute', + ); + } + if ($type === UtopiaDatabase::VAR_RELATIONSHIP) { + $resource->getOptions()['side'] = UtopiaDatabase::RELATION_SIDE_PARENT; + $relatedCollection = $this->database->getDocument( + 'database_' . $database->getInternalId(), + $resource->getOptions()['relatedCollection'] + ); + if ($relatedCollection->isEmpty()) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Related collection not found', + ); + } } - // Wait for attribute to be created - $this->awaitAttributeCreation($attribute, 5); + try { + $attribute = new UtopiaDocument([ + '$id' => ID::custom($database->getInternalId() . '_' . $collection->getInternalId() . '_' . $resource->getKey()), + 'key' => $resource->getKey(), + 'databaseInternalId' => $database->getInternalId(), + 'databaseId' => $database->getId(), + 'collectionInternalId' => $collection->getInternalId(), + 'collectionId' => $collection->getId(), + 'type' => $type, + 'status' => 'available', + 'size' => $resource->getSize(), + 'required' => $resource->isRequired(), + 'signed' => $resource->isSigned(), + 'default' => $resource->getDefault(), + 'array' => $resource->isArray(), + 'format' => $resource->getFormat(), + 'formatOptions' => $resource->getFormatOptions(), + 'filters' => $resource->getFilters(), + 'options' => $resource->getOptions(), + ]); + + $this->database->checkAttribute($collection, $attribute); + + $attribute = $this->database->createDocument('attributes', $attribute); + } catch (DuplicateException) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Attribute already exists', + ); + } catch (LimitException) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Attribute limit exceeded', + ); + } catch (\Throwable $e) { + $this->database->purgeCachedDocument('database_' . $database->getInternalId(), $collection->getId()); + $this->database->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); + throw $e; + } + + $this->database->purgeCachedDocument('database_' . $database->getInternalId(), $collection->getId()); + $this->database->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); + $options = $resource->getOptions(); + + $twoWayKey = null; + + if ($type === UtopiaDatabase::VAR_RELATIONSHIP && $options['twoWay']) { + $twoWayKey = $options['twoWayKey']; + $options['relatedCollection'] = $collection->getId(); + $options['twoWayKey'] = $resource->getKey(); + $options['side'] = UtopiaDatabase::RELATION_SIDE_CHILD; + + try { + $twoWayAttribute = new UtopiaDocument([ + '$id' => ID::custom($database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $twoWayKey), + 'key' => $twoWayKey, + 'databaseInternalId' => $database->getInternalId(), + 'databaseId' => $database->getId(), + 'collectionInternalId' => $relatedCollection->getInternalId(), + 'collectionId' => $relatedCollection->getId(), + 'type' => $type, + 'status' => 'available', + 'size' => $resource->getSize(), + 'required' => $resource->isRequired(), + 'signed' => $resource->isSigned(), + 'default' => $resource->getDefault(), + 'array' => $resource->isArray(), + 'format' => $resource->getFormat(), + 'formatOptions' => $resource->getFormatOptions(), + 'filters' => $resource->getFilters(), + 'options' => $options, + ]); + + $this->database->createDocument('attributes', $twoWayAttribute); + } catch (DuplicateException) { + $this->database->deleteDocument('attributes', $attribute->getId()); + + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Attribute already exists', + ); + } catch (LimitException) { + $this->database->deleteDocument('attributes', $attribute->getId()); + + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Attribute limit exceeded', + ); + } catch (\Throwable $e) { + $this->database->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId()); + $this->database->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); + throw $e; + } + } + + try { + switch ($type) { + case UtopiaDatabase::VAR_RELATIONSHIP: + if (!$this->database->createRelationship( + collection: 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), + relatedCollection: 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), + type: $options['relationType'], + twoWay: $options['twoWay'], + id: $resource->getKey(), + twoWayKey: $options['twoWay'] ? $twoWayKey : $options['twoWayKey'] ?? null, + onDelete: $options['onDelete'], + )) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Failed to create relationship', + ); + } + break; + default: + if (!$this->database->createAttribute( + 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), + $resource->getKey(), + $type, + $resource->getSize(), + $resource->isRequired(), + $resource->getDefault(), + $resource->isSigned(), + $resource->isArray(), + $resource->getFormat(), + $resource->getFormatOptions(), + $resource->getFilters(), + )) { + throw new \Exception('Failed to create Attribute'); + } + } + } catch (\Throwable) { + $this->database->deleteDocument('attributes', $attribute->getId()); + + if (isset($twoWayAttribute)) { + $this->database->deleteDocument('attributes', $twoWayAttribute->getId()); + } + + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Failed to create attribute', + ); + } + + if ($type === UtopiaDatabase::VAR_RELATIONSHIP && $options['twoWay']) { + $this->database->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId()); + } + + $this->database->purgeCachedDocument('database_' . $database->getInternalId(), $collection->getId()); + $this->database->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); + + return true; } /** - * Await Attribute Creation + * @throws Exception + * @throws \Throwable */ - public function awaitAttributeCreation(Attribute $attribute, int $timeout): bool + protected function createIndex(Index $resource): bool { - $databaseService = new Databases($this->client); + $database = $this->database->getDocument( + 'databases', + $resource->getCollection()->getDatabase()->getId(), + ); + if ($database->isEmpty()) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Database not found', + ); + } + + $collection = $this->database->getDocument( + 'database_' . $database->getInternalId(), + $resource->getCollection()->getId(), + ); + if ($collection->isEmpty()) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Collection not found', + ); + } + + $count = $this->database->count('indexes', [ + Query::equal('collectionInternalId', [$collection->getInternalId()]), + Query::equal('databaseInternalId', [$database->getInternalId()]) + ], $this->database->getLimitForIndexes()); + + if ($count >= $this->database->getLimitForIndexes()) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Index limit reached for collection', + ); + } + + /** + * @var array $collectionAttributes + */ + $collectionAttributes = $collection->getAttribute('attributes'); + + $oldAttributes = \array_map( + fn ($attr) => $attr->getArrayCopy(), + $collectionAttributes + ); + + $oldAttributes[] = [ + 'key' => '$id', + 'type' => UtopiaDatabase::VAR_STRING, + 'status' => 'available', + 'required' => true, + 'array' => false, + 'default' => null, + 'size' => UtopiaDatabase::LENGTH_KEY + ]; + $oldAttributes[] = [ + 'key' => '$createdAt', + 'type' => UtopiaDatabase::VAR_DATETIME, + 'status' => 'available', + 'signed' => false, + 'required' => false, + 'array' => false, + 'default' => null, + 'size' => 0 + ]; + $oldAttributes[] = [ + 'key' => '$updatedAt', + 'type' => UtopiaDatabase::VAR_DATETIME, + 'status' => 'available', + 'signed' => false, + 'required' => false, + 'array' => false, + 'default' => null, + 'size' => 0 + ]; + + // Lengths hidden by default + $lengths = []; - $start = \time(); + foreach ($resource->getAttributes() as $i => $attribute) { + // find attribute metadata in collection document + $attributeIndex = \array_search( + $attribute, + \array_column($oldAttributes, 'key') + ); - while (\time() - $start < $timeout) { - $response = $databaseService->getAttribute($attribute->getCollection()->getDatabase()->getId(), $attribute->getCollection()->getId(), $attribute->getKey()); + if ($attributeIndex === false) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Attribute not found in collection: ' . $attribute, + ); + } - if ($response['status'] === 'available') { - return true; + $attributeStatus = $oldAttributes[$attributeIndex]['status']; + $attributeType = $oldAttributes[$attributeIndex]['type']; + $attributeSize = $oldAttributes[$attributeIndex]['size']; + $attributeArray = $oldAttributes[$attributeIndex]['array'] ?? false; + + if ($attributeType === UtopiaDatabase::VAR_RELATIONSHIP) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Relationship attributes are not supported in indexes', + ); } - \usleep(500000); + // Ensure attribute is available + if ($attributeStatus !== 'available') { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Attribute not available: ' . $attribute, + ); + } + + $lengths[$i] = null; + + if ($attributeType === UtopiaDatabase::VAR_STRING) { + $lengths[$i] = $attributeSize; // set attribute size as index length only for strings + } + + if ($attributeArray === true) { + $lengths[$i] = UtopiaDatabase::ARRAY_INDEX_LENGTH; + $orders[$i] = null; + } } - throw new \Exception('Attribute creation timeout'); + $index = new UtopiaDocument([ + '$id' => ID::custom($database->getInternalId() . '_' . $collection->getInternalId() . '_' . $resource->getKey()), + 'key' => $resource->getKey(), + 'status' => 'available', // processing, available, failed, deleting, stuck + 'databaseInternalId' => $database->getInternalId(), + 'databaseId' => $database->getId(), + 'collectionInternalId' => $collection->getInternalId(), + 'collectionId' => $collection->getId(), + 'type' => $resource->getType(), + 'attributes' => $resource->getAttributes(), + 'lengths' => $lengths, + 'orders' => $resource->getOrders(), + ]); + + $validator = new IndexValidator( + $collectionAttributes, + $this->database->getAdapter()->getMaxIndexLength() + ); + + if (!$validator->isValid($index)) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Invalid index: ' . $validator->getDescription(), + ); + } + + $index = $this->database->createDocument('indexes', $index); + + try { + $result = $this->database->createIndex( + 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), + $resource->getKey(), + $resource->getType(), + $resource->getAttributes(), + $lengths, + $resource->getOrders() + ); + + if (!$result) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Failed to create index', + ); + } + } catch (\Throwable $th) { + $this->database->deleteDocument('indexes', $index->getId()); + + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Failed to create index', + ); + } + + $this->database->purgeCachedDocument( + 'database_' . $database->getInternalId(), + $collection->getId() + ); + + return true; } - public function importFileResource(File|Bucket $resource): Resource + /** + * @throws AuthorizationException + * @throws DatabaseException + * @throws StructureException + */ + protected function createDocument(Document $resource, bool $isLast): bool { - $storageService = new Storage($this->client); + // Check if document has already been created + $exists = \array_key_exists( + $resource->getId(), + $this->cache->get(Resource::TYPE_DOCUMENT) + ); - $response = null; + if ($exists) { + $resource->setStatus( + Resource::STATUS_SKIPPED, + 'Document has already been created' + ); + return false; + } + $this->documentBuffer[] = new UtopiaDocument(\array_merge([ + '$id' => $resource->getId(), + '$permissions' => $resource->getPermissions(), + ], $resource->getData())); + + if ($isLast) { + try { + $database = $this->database->getDocument( + 'databases', + $resource->getCollection()->getDatabase()->getId(), + ); + $collection = $this->database->getDocument( + 'database_' . $database->getInternalId(), + $resource->getCollection()->getId(), + ); + + $databaseInternalId = $database->getInternalId(); + $collectionInternalId = $collection->getInternalId(); + + $this->database + ->setPreserveDates(true) + ->createDocuments( + 'database_' . $databaseInternalId . '_collection_' . $collectionInternalId, + $this->documentBuffer + ); + } finally { + $this->documentBuffer = []; + $this->database->setPreserveDates(false); + } + } + + + return true; + } + + /** + * @throws AppwriteException + */ + public function importFileResource(Resource $resource): Resource + { switch ($resource->getName()) { case Resource::TYPE_FILE: /** @var File $resource */ return $this->importFile($resource); - break; case Resource::TYPE_BUCKET: /** @var Bucket $resource */ - if (! $resource->getUpdateLimits()) { - $response = $storageService->createBucket( - $resource->getId() ?? 'unique()', - $resource->getBucketName(), - $resource->getPermissions(), - $resource->getFileSecurity(), - true, // Set to true for now, we'll come back later. - null, - null, - $resource->getCompression() ?? 'none', - $resource->getEncryption() ?? null, - $resource->getAntiVirus() ?? null - ); - } else { - $response = $storageService->updateBucket( - $resource->getId(), - $resource->getBucketName(), - $resource->getPermissions(), - $resource->getFileSecurity(), - $resource->getEnabled(), - $resource->getMaxFileSize() ?? null, - $resource->getAllowedFileExtensions() ?? null, - $resource->getCompression() ?? 'none', - $resource->getEncryption() ?? null, - $resource->getAntiVirus() ?? null - ); - } + + $compression = match ($resource->getCompression()) { + 'none' => Compression::NONE(), + 'gzip' => Compression::GZIP(), + 'zstd' => Compression::ZSTD(), + default => throw new \Exception('Invalid Compression: ' . $resource->getCompression()), + }; + + $response = $this->storage->createBucket( + $resource->getId(), + $resource->getBucketName(), + $resource->getPermissions(), + $resource->getFileSecurity(), + $resource->getEnabled(), + $resource->getMaxFileSize(), + $resource->getAllowedFileExtensions(), + $compression, + $resource->getEncryption(), + $resource->getAntiVirus() + ); + $resource->setId($response['$id']); } @@ -453,6 +947,7 @@ public function importFileResource(File|Bucket $resource): Resource * Import File Data * * @returns File + * @throws AppwriteException */ public function importFile(File $file): File { @@ -472,7 +967,7 @@ public function importFile(File $file): File [ 'bucketId' => $bucketId, 'fileId' => $file->getId(), - 'file' => new \CurlFile('data://'.$file->getMimeType().';base64,'.base64_encode($file->getData()), $file->getMimeType(), $file->getFileName()), + 'file' => new \CurlFile('data://' . $file->getMimeType() . ';base64,' . base64_encode($file->getData()), $file->getMimeType(), $file->getFileName()), 'permissions' => $file->getPermissions(), ] ); @@ -488,14 +983,14 @@ public function importFile(File $file): File "/storage/buckets/{$bucketId}/files", [ 'content-type' => 'multipart/form-data', - 'content-range' => 'bytes '.($file->getStart()).'-'.($file->getEnd() == ($file->getSize() - 1) ? $file->getSize() : $file->getEnd()).'/'.$file->getSize(), - 'X-Appwrite-Project' => $this->project, - 'x-Appwrite-Key' => $this->key, + 'content-range' => 'bytes ' . ($file->getStart()) . '-' . ($file->getEnd() == ($file->getSize() - 1) ? $file->getSize() : $file->getEnd()) . '/' . $file->getSize(), + 'x-appwrite-project' => $this->project, + 'x-appwrite-key' => $this->key, ], [ 'bucketId' => $bucketId, 'fileId' => $file->getId(), - 'file' => new \CurlFile('data://'.$file->getMimeType().';base64,'.base64_encode($file->getData()), $file->getMimeType(), $file->getFileName()), + 'file' => new \CurlFile('data://' . $file->getMimeType() . ';base64,' . base64_encode($file->getData()), $file->getMimeType(), $file->getFileName()), 'permissions' => $file->getPermissions(), ] ); @@ -503,9 +998,9 @@ public function importFile(File $file): File if ($file->getEnd() == ($file->getSize() - 1)) { $file->setStatus(Resource::STATUS_SUCCESS); - // Signatures for Encrypted files are invalid, so we skip the check - if ($file->getBucket()->getEncryption() == false || $file->getSize() > (20 * 1024 * 1024)) { - if ($response['signature'] !== $file->getSignature()) { + // Signatures for encrypted files are invalid, so we skip the check + if (!$file->getBucket()->getEncryption() || $file->getSize() > (20 * 1024 * 1024)) { + if (\is_array($response) && $response['signature'] !== $file->getSignature()) { $file->setStatus(Resource::STATUS_WARNING, 'File signature mismatch, Possibly corrupted.'); } } @@ -516,18 +1011,18 @@ public function importFile(File $file): File return $file; } + /** + * @throws AppwriteException + */ public function importAuthResource(Resource $resource): Resource { - $userService = new Users($this->client); - $teamService = new Teams($this->client); - switch ($resource->getName()) { case Resource::TYPE_USER: /** @var User $resource */ - if (! empty($resource->getPasswordHash())) { + if (!empty($resource->getPasswordHash())) { $this->importPasswordUser($resource); } else { - $userService->create( + $this->users->create( $resource->getId(), $resource->getEmail(), $resource->getPhone(), @@ -536,44 +1031,48 @@ public function importAuthResource(Resource $resource): Resource ); } - if ($resource->getUsername()) { - $userService->updateName($resource->getId(), $resource->getUsername()); + if (!empty($resource->getUsername())) { + $this->users->updateName($resource->getId(), $resource->getUsername()); } - if ($resource->getPhone()) { - $userService->updatePhone($resource->getId(), $resource->getPhone()); + if (!empty($resource->getPhone())) { + $this->users->updatePhone($resource->getId(), $resource->getPhone()); } if ($resource->getEmailVerified()) { - $userService->updateEmailVerification($resource->getId(), $resource->getEmailVerified()); + $this->users->updateEmailVerification($resource->getId(), true); } if ($resource->getPhoneVerified()) { - $userService->updatePhoneVerification($resource->getId(), $resource->getPhoneVerified()); + $this->users->updatePhoneVerification($resource->getId(), true); } if ($resource->getDisabled()) { - $userService->updateStatus($resource->getId(), ! $resource->getDisabled()); + $this->users->updateStatus($resource->getId(), false); } - if ($resource->getPreferences()) { - $userService->updatePrefs($resource->getId(), $resource->getPreferences()); + if (!empty($resource->getPreferences())) { + $this->users->updatePrefs($resource->getId(), $resource->getPreferences()); } - if ($resource->getLabels()) { - $userService->updateLabels($resource->getId(), $resource->getLabels()); + if (!empty($resource->getLabels())) { + $this->users->updateLabels($resource->getId(), $resource->getLabels()); } break; case Resource::TYPE_TEAM: /** @var Team $resource */ - $teamService->create($resource->getId(), $resource->getTeamName()); - $teamService->updatePrefs($resource->getId(), $resource->getPreferences()); + $this->teams->create($resource->getId(), $resource->getTeamName()); + + if (!empty($resource->getPreferences())) { + $this->teams->updatePrefs($resource->getId(), $resource->getPreferences()); + } break; case Resource::TYPE_MEMBERSHIP: /** @var Membership $resource */ $user = $resource->getUser(); - $teamService->createMembership( + + $this->teams->createMembership( $resource->getTeam()->getId(), $resource->getRoles(), userId: $user->getId(), @@ -586,19 +1085,24 @@ public function importAuthResource(Resource $resource): Resource return $resource; } + /** + * @param User $user + * @return array|null + * @throws AppwriteException + * @throws \Exception + */ public function importPasswordUser(User $user): ?array { - $auth = new Users($this->client); $hash = $user->getPasswordHash(); $result = null; - if (! $hash) { + if (!$hash) { throw new \Exception('Password hash is missing'); } switch ($hash->getAlgorithm()) { case Hash::ALGORITHM_SCRYPT_MODIFIED: - $result = $auth->createScryptModifiedUser( + $result = $this->users->createScryptModifiedUser( $user->getId(), $user->getEmail(), $hash->getHash(), @@ -609,7 +1113,7 @@ public function importPasswordUser(User $user): ?array ); break; case Hash::ALGORITHM_BCRYPT: - $result = $auth->createBcryptUser( + $result = $this->users->createBcryptUser( $user->getId(), $user->getEmail(), $hash->getHash(), @@ -617,7 +1121,7 @@ public function importPasswordUser(User $user): ?array ); break; case Hash::ALGORITHM_ARGON2: - $result = $auth->createArgon2User( + $result = $this->users->createArgon2User( $user->getId(), $user->getEmail(), $hash->getHash(), @@ -625,16 +1129,16 @@ public function importPasswordUser(User $user): ?array ); break; case Hash::ALGORITHM_SHA256: - $result = $auth->createShaUser( + $result = $this->users->createShaUser( $user->getId(), $user->getEmail(), $hash->getHash(), - 'sha256', + PasswordHash::SHA256(), empty($user->getUsername()) ? null : $user->getUsername() ); break; case Hash::ALGORITHM_PHPASS: - $result = $auth->createPHPassUser( + $result = $this->users->createPHPassUser( $user->getId(), $user->getEmail(), $hash->getHash(), @@ -642,7 +1146,7 @@ public function importPasswordUser(User $user): ?array ); break; case Hash::ALGORITHM_SCRYPT: - $result = $auth->createScryptUser( + $result = $this->users->createScryptUser( $user->getId(), $user->getEmail(), $hash->getHash(), @@ -655,7 +1159,7 @@ public function importPasswordUser(User $user): ?array ); break; case Hash::ALGORITHM_PLAINTEXT: - $result = $auth->create( + $result = $this->users->create( $user->getId(), $user->getEmail(), $user->getPhone(), @@ -668,35 +1172,90 @@ public function importPasswordUser(User $user): ?array return $result; } + /** + * @throws AppwriteException + */ public function importFunctionResource(Resource $resource): Resource { - $functions = new Functions($this->client); - switch ($resource->getName()) { case Resource::TYPE_FUNCTION: /** @var Func $resource */ - $functions->create( + + $runtime = match ($resource->getRuntime()) { + 'node-14.5' => Runtime::NODE145(), + 'node-16.0' => Runtime::NODE160(), + 'node-18.0' => Runtime::NODE180(), + 'node-19.0' => Runtime::NODE190(), + 'node-20.0' => Runtime::NODE200(), + 'node-21.0' => Runtime::NODE210(), + 'php-8.0' => Runtime::PHP80(), + 'php-8.1' => Runtime::PHP81(), + 'php-8.2' => Runtime::PHP82(), + 'php-8.3' => Runtime::PHP83(), + 'ruby-3.0' => Runtime::RUBY30(), + 'ruby-3.1' => Runtime::RUBY31(), + 'ruby-3.2' => Runtime::RUBY32(), + 'ruby-3.3' => Runtime::RUBY33(), + 'python-3.8' => Runtime::PYTHON38(), + 'python-3.9' => Runtime::PYTHON39(), + 'python-3.10' => Runtime::PYTHON310(), + 'python-3.11' => Runtime::PYTHON311(), + 'python-3.12' => Runtime::PYTHON312(), + 'python-ml-3.11' => Runtime::PYTHONML311(), + 'dart-3.0' => Runtime::DART30(), + 'dart-3.1' => Runtime::DART31(), + 'dart-3.3' => Runtime::DART33(), + 'dart-2.15' => Runtime::DART215(), + 'dart-2.16' => Runtime::DART216(), + 'dart-2.17' => Runtime::DART217(), + 'dart-2.18' => Runtime::DART218(), + 'deno-1.21' => Runtime::DENO121(), + 'deno-1.24' => Runtime::DENO124(), + 'deno-1.35' => Runtime::DENO135(), + 'deno-1.40' => Runtime::DENO140(), + 'dotnet-3.1' => Runtime::DOTNET31(), + 'dotnet-6.0' => Runtime::DOTNET60(), + 'dotnet-7.0' => Runtime::DOTNET70(), + 'java-8.0' => Runtime::JAVA80(), + 'java-11.0' => Runtime::JAVA110(), + 'java-17.0' => Runtime::JAVA170(), + 'java-18.0' => Runtime::JAVA180(), + 'java-21.0' => Runtime::JAVA210(), + 'swift-5.5' => Runtime::SWIFT55(), + 'swift-5.8' => Runtime::SWIFT58(), + 'swift-5.9' => Runtime::SWIFT59(), + 'kotlin-1.6' => Runtime::KOTLIN16(), + 'kotlin-1.8' => Runtime::KOTLIN18(), + 'kotlin-1.9' => Runtime::KOTLIN19(), + 'cpp-17' => Runtime::CPP17(), + 'cpp-20' => Runtime::CPP20(), + 'bun-1.0' => Runtime::BUN10(), + default => throw new \Exception('Invalid Runtime: ' . $resource->getRuntime()), + }; + + $this->functions->create( $resource->getId(), $resource->getFunctionName(), - $resource->getRuntime(), + $runtime, $resource->getExecute(), $resource->getEvents(), $resource->getSchedule(), $resource->getTimeout(), - $resource->getEnabled() + $resource->getEnabled(), + entrypoint: $resource->getEntrypoint(), ); break; case Resource::TYPE_ENVIRONMENT_VARIABLE: /** @var EnvVar $resource */ - $functions->createVariable( + $this->functions->createVariable( $resource->getFunc()->getId(), $resource->getKey(), $resource->getValue() ); break; case Resource::TYPE_DEPLOYMENT: + /** @var Deployment $resource */ return $this->importDeployment($resource); - break; } $resource->setStatus(Resource::STATUS_SUCCESS); @@ -704,6 +1263,10 @@ public function importFunctionResource(Resource $resource): Resource return $resource; } + /** + * @throws AppwriteException + * @throws \Exception + */ private function importDeployment(Deployment $deployment): Resource { $functionId = $deployment->getFunction()->getId(); @@ -719,7 +1282,7 @@ private function importDeployment(Deployment $deployment): Resource ], [ 'functionId' => $functionId, - 'code' => new \CurlFile('data://application/gzip;base64,'.base64_encode($deployment->getData()), 'application/gzip', 'deployment.tar.gz'), + 'code' => new \CurlFile('data://application/gzip;base64,' . base64_encode($deployment->getData()), 'application/gzip', 'deployment.tar.gz'), 'activate' => $deployment->getActivated() ? 'true' : 'false', 'entrypoint' => $deployment->getEntrypoint(), ] @@ -735,17 +1298,21 @@ private function importDeployment(Deployment $deployment): Resource "/v1/functions/{$functionId}/deployments", [ 'content-type' => 'multipart/form-data', - 'content-range' => 'bytes '.($deployment->getStart()).'-'.($deployment->getEnd() == ($deployment->getSize() - 1) ? $deployment->getSize() : $deployment->getEnd()).'/'.$deployment->getSize(), + 'content-range' => 'bytes ' . ($deployment->getStart()) . '-' . ($deployment->getEnd() == ($deployment->getSize() - 1) ? $deployment->getSize() : $deployment->getEnd()) . '/' . $deployment->getSize(), 'x-appwrite-id' => $deployment->getId(), ], [ 'functionId' => $functionId, - 'code' => new \CurlFile('data://application/gzip;base64,'.base64_encode($deployment->getData()), 'application/gzip', 'deployment.tar.gz'), + 'code' => new \CurlFile('data://application/gzip;base64,' . base64_encode($deployment->getData()), 'application/gzip', 'deployment.tar.gz'), 'activate' => $deployment->getActivated(), 'entrypoint' => $deployment->getEntrypoint(), ] ); + if (!\is_array($response) || !isset($response['$id'])) { + throw new \Exception('Deployment creation failed'); + } + if ($deployment->getStart() === 0) { $deployment->setId($response['$id']); } diff --git a/src/Migration/Destinations/Local.php b/src/Migration/Destinations/Local.php index ad3e22a..68ea59d 100644 --- a/src/Migration/Destinations/Local.php +++ b/src/Migration/Destinations/Local.php @@ -6,7 +6,6 @@ use Utopia\Migration\Resource; use Utopia\Migration\Resources\Functions\Deployment; use Utopia\Migration\Resources\Storage\File; -use Utopia\Migration\Transfer; /** * Local @@ -16,6 +15,9 @@ */ class Local extends Destination { + /** + * @var array>> + */ private array $data = []; protected string $path; @@ -26,8 +28,8 @@ public function __construct(string $path) if (! \file_exists($this->path)) { mkdir($this->path, 0777, true); - mkdir($this->path.'/files', 0777, true); - mkdir($this->path.'/deployments', 0777, true); + mkdir($this->path . '/files', 0777, true); + mkdir($this->path . '/deployments', 0777, true); } } @@ -36,72 +38,84 @@ public static function getName(): string return 'Local'; } + /** + * @return array + */ public static function getSupportedResources(): array { return [ - Resource::TYPE_ATTRIBUTE, - Resource::TYPE_BUCKET, - Resource::TYPE_COLLECTION, + // Auth + Resource::TYPE_USER, + Resource::TYPE_TEAM, + Resource::TYPE_MEMBERSHIP, + Resource::TYPE_HASH, + + // Database Resource::TYPE_DATABASE, - Resource::TYPE_DEPLOYMENT, + Resource::TYPE_COLLECTION, + Resource::TYPE_ATTRIBUTE, + Resource::TYPE_INDEX, Resource::TYPE_DOCUMENT, - Resource::TYPE_ENVIRONMENT_VARIABLE, + + // Storage + Resource::TYPE_BUCKET, Resource::TYPE_FILE, + + // Functions Resource::TYPE_FUNCTION, - Resource::TYPE_HASH, - Resource::TYPE_INDEX, - Resource::TYPE_TEAM, - Resource::TYPE_MEMBERSHIP, - Resource::TYPE_USER, + Resource::TYPE_DEPLOYMENT, + Resource::TYPE_ENVIRONMENT_VARIABLE, ]; } + /** + * @throws \Exception + */ public function report(array $resources = []): array { $report = []; - if (empty($resources)) { - $resources = $this->getSupportedResources(); - } - - // Check we can write to the file - if (! \is_writable($this->path.'/backup.json')) { - $report[Transfer::GROUP_DATABASES][] = 'Unable to write to file: '.$this->path; - throw new \Exception('Unable to write to file: '.$this->path); + if (!\is_writable($this->path . '/backup.json')) { + throw new \Exception('Unable to write to file: ' . $this->path); } return $report; } + /** + * @throws \Exception + */ private function sync(): void { - $jsonEncodedData = \json_encode($this->data, JSON_PRETTY_PRINT); + $json = \json_encode($this->data, JSON_PRETTY_PRINT); - if ($jsonEncodedData === false) { + if ($json === false) { throw new \Exception('Unable to encode data to JSON, Are you accidentally encoding binary data?'); } - \file_put_contents($this->path.'/backup.json', \json_encode($this->data, JSON_PRETTY_PRINT)); + \file_put_contents($this->path . '/backup.json', $json); } + /** + * @param array $resources + * @param callable $callback + * @throws \Exception + */ protected function import(array $resources, callable $callback): void { foreach ($resources as $resource) { - /** @var resource $resource */ switch ($resource->getName()) { case Resource::TYPE_DEPLOYMENT: /** @var Deployment $resource */ if ($resource->getStart() === 0) { - $this->data[$resource->getGroup()][$resource->getName()][] = $resource->asArray(); + $this->data[$resource->getGroup()][$resource->getName()][] = (string) \json_encode($resource); } - file_put_contents($this->path.'deployments/'.$resource->getId().'.tar.gz', $resource->getData(), FILE_APPEND); + file_put_contents($this->path . 'deployments/' . $resource->getId() . '.tar.gz', $resource->getData(), FILE_APPEND); $resource->setData(''); break; case Resource::TYPE_FILE: /** @var File $resource */ - - // Handle folders if (str_contains($resource->getFileName(), '/')) { $folders = explode('/', $resource->getFileName()); $folderPath = $this->path.'/files'; @@ -123,12 +137,13 @@ protected function import(array $resources, callable $callback): void $resource->setData(''); break; default: - $this->data[$resource->getGroup()][$resource->getName()][] = $resource->asArray(); + $this->data[$resource->getGroup()][$resource->getName()][] = (string) \json_encode($resource); break; } $resource->setStatus(Resource::STATUS_SUCCESS); $this->cache->update($resource); + $this->sync(); } diff --git a/src/Migration/Exception.php b/src/Migration/Exception.php index 46ecf52..1b7ed0f 100644 --- a/src/Migration/Exception.php +++ b/src/Migration/Exception.php @@ -2,16 +2,22 @@ namespace Utopia\Migration; -class Exception extends \Exception +class Exception extends \Exception implements \JsonSerializable { public string $resourceName; public string $resourceGroup; - public string $resourceId; + public ?string $resourceId; - public function __construct(string $resourceName, string $resourceGroup, string $message, int $code = 0, ?\Throwable $previous = null, string $resourceId = '') - { + public function __construct( + string $resourceName, + string $resourceGroup, + ?string $resourceId = null, + string $message = '', + int $code = 0, + ?\Throwable $previous = null, + ) { $this->resourceName = $resourceName; $this->resourceId = $resourceId; $this->resourceGroup = $resourceGroup; @@ -31,6 +37,21 @@ public function getResourceGroup(): string public function getResourceId(): string { - return $this->resourceId; + return $this->resourceId ?? ''; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'code' => $this->getCode(), + 'message' => $this->getMessage(), + 'resourceName' => $this->resourceName, + 'resourceGroup' => $this->resourceGroup, + 'resourceId' => $this->resourceId, + 'trace' => $this->getPrevious()?->getTraceAsString(), + ]; } } diff --git a/src/Migration/Resource.php b/src/Migration/Resource.php index 0516dd9..d317b76 100644 --- a/src/Migration/Resource.php +++ b/src/Migration/Resource.php @@ -2,7 +2,7 @@ namespace Utopia\Migration; -abstract class Resource +abstract class Resource implements \JsonSerializable { public const STATUS_PENDING = 'pending'; @@ -52,7 +52,7 @@ abstract class Resource public const TYPE_HASH = 'hash'; - public const TYPE_ENVIRONMENT_VARIABLE = 'environment variable'; + public const TYPE_ENVIRONMENT_VARIABLE = 'environment-variable'; public const ALL_RESOURCES = [ self::TYPE_ATTRIBUTE, @@ -81,6 +81,9 @@ abstract class Resource protected string $message = ''; + /** + * @var array + */ protected array $permissions = []; abstract public static function getName(): string; @@ -149,7 +152,7 @@ public function setMessage(string $message): self } /** - * @returns string[] + * @returns array */ public function getPermissions(): array { @@ -157,7 +160,7 @@ public function getPermissions(): array } /** - * @param string[] $permissions + * @param array $permissions */ public function setPermissions(array $permissions): self { @@ -165,6 +168,4 @@ public function setPermissions(array $permissions): self return $this; } - - abstract public function asArray(): array; } diff --git a/src/Migration/Resources/Auth/Hash.php b/src/Migration/Resources/Auth/Hash.php index e1fef05..401cd66 100644 --- a/src/Migration/Resources/Auth/Hash.php +++ b/src/Migration/Resources/Auth/Hash.php @@ -10,51 +10,70 @@ */ class Hash extends Resource { - public const ALGORITHM_SCRYPT_MODIFIED = 'scryptModified'; + public const string ALGORITHM_SCRYPT_MODIFIED = 'scryptModified'; - public const ALGORITHM_BCRYPT = 'bcrypt'; + public const string ALGORITHM_BCRYPT = 'bcrypt'; - public const ALGORITHM_MD5 = 'md5'; + public const string ALGORITHM_MD5 = 'md5'; - public const ALGORITHM_ARGON2 = 'argon2'; + public const string ALGORITHM_ARGON2 = 'argon2'; - public const ALGORITHM_SHA256 = 'sha256'; + public const string ALGORITHM_SHA256 = 'sha256'; - public const ALGORITHM_PHPASS = 'phpass'; + public const string ALGORITHM_PHPASS = 'phpass'; - public const ALGORITHM_SCRYPT = 'scrypt'; + public const string ALGORITHM_SCRYPT = 'scrypt'; - public const ALGORITHM_PLAINTEXT = 'plainText'; + public const string ALGORITHM_PLAINTEXT = 'plainText'; - private string $hash; - - private string $salt = ''; - - private string $algorithm = self::ALGORITHM_SHA256; - - private string $separator = ''; - - private string $signingKey = ''; - - private int $passwordCpu = 0; - - private int $passwordMemory = 0; - - private int $passwordParallel = 0; + public function __construct( + private readonly string $hash, + private readonly string $salt = '', + private readonly string $algorithm = self::ALGORITHM_SHA256, + private readonly string $separator = '', + private readonly string $signingKey = '', + private readonly int $passwordCpu = 0, + private readonly int $passwordMemory = 0, + private readonly int $passwordParallel = 0, + private readonly int $passwordLength = 0 + ) { + } - private int $passwordLength = 0; + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['hash'] ?? '', + $array['salt'] ?? '', + $array['algorithm'] ?? self::ALGORITHM_SHA256, + $array['separator'] ?? '', + $array['signingKey'] ?? '', + $array['passwordCpu'] ?? 0, + $array['passwordMemory'] ?? 0, + $array['passwordParallel'] ?? 0, + $array['passwordLength'] ?? 0 + ); + } - public function __construct(string $hash, string $salt = '', string $algorithm = self::ALGORITHM_SHA256, string $separator = '', string $signingKey = '', int $passwordCpu = 0, int $passwordMemory = 0, int $passwordParallel = 0, int $passwordLength = 0) + /** + * @return array + */ + public function jsonSerialize(): array { - $this->hash = $hash; - $this->salt = $salt; - $this->algorithm = $algorithm; - $this->separator = $separator; - $this->signingKey = $signingKey; - $this->passwordCpu = $passwordCpu; - $this->passwordMemory = $passwordMemory; - $this->passwordParallel = $passwordParallel; - $this->passwordLength = $passwordLength; + return [ + 'hash' => $this->hash, + 'salt' => $this->salt, + 'algorithm' => $this->algorithm, + 'separator' => $this->separator, + 'signingKey' => $this->signingKey, + 'passwordCpu' => $this->passwordCpu, + 'passwordMemory' => $this->passwordMemory, + 'passwordParallel' => $this->passwordParallel, + 'passwordLength' => $this->passwordLength, + ]; } public static function getName(): string @@ -75,16 +94,6 @@ public function getHash(): string return $this->hash; } - /** - * Set Hash - */ - public function setHash(string $hash): self - { - $this->hash = $hash; - - return $this; - } - /** * Get Salt */ @@ -93,16 +102,6 @@ public function getSalt(): string return $this->salt; } - /** - * Set Salt - */ - public function setSalt(string $salt): self - { - $this->salt = $salt; - - return $this; - } - /** * Get Algorithm */ @@ -111,16 +110,6 @@ public function getAlgorithm(): string return $this->algorithm; } - /** - * Set Algorithm - */ - public function setAlgorithm(string $algorithm): self - { - $this->algorithm = $algorithm; - - return $this; - } - /** * Get Separator */ @@ -129,16 +118,6 @@ public function getSeparator(): string return $this->separator; } - /** - * Set Separator - */ - public function setSeparator(string $separator): self - { - $this->separator = $separator; - - return $this; - } - /** * Get Signing Key */ @@ -147,16 +126,6 @@ public function getSigningKey(): string return $this->signingKey; } - /** - * Set Signing Key - */ - public function setSigningKey(string $signingKey): self - { - $this->signingKey = $signingKey; - - return $this; - } - /** * Get Password CPU */ @@ -165,16 +134,6 @@ public function getPasswordCpu(): int return $this->passwordCpu; } - /** - * Set Password CPU - */ - public function setPasswordCpu(int $passwordCpu): self - { - $this->passwordCpu = $passwordCpu; - - return $this; - } - /** * Get Password Memory */ @@ -183,16 +142,6 @@ public function getPasswordMemory(): int return $this->passwordMemory; } - /** - * Set Password Memory - */ - public function setPasswordMemory(int $passwordMemory): self - { - $this->passwordMemory = $passwordMemory; - - return $this; - } - /** * Get Password Parallel */ @@ -201,16 +150,6 @@ public function getPasswordParallel(): int return $this->passwordParallel; } - /** - * Set Password Parallel - */ - public function setPasswordParallel(int $passwordParallel): self - { - $this->passwordParallel = $passwordParallel; - - return $this; - } - /** * Get Password Length */ @@ -218,32 +157,4 @@ public function getPasswordLength(): int { return $this->passwordLength; } - - /** - * Set Password Length - */ - public function setPasswordLength(int $passwordLength): self - { - $this->passwordLength = $passwordLength; - - return $this; - } - - /** - * As Array - */ - public function asArray(): array - { - return [ - 'hash' => $this->hash, - 'salt' => $this->salt, - 'algorithm' => $this->algorithm, - 'separator' => $this->separator, - 'signingKey' => $this->signingKey, - 'passwordCpu' => $this->passwordCpu, - 'passwordMemory' => $this->passwordMemory, - 'passwordParallel' => $this->passwordParallel, - 'passwordLength' => $this->passwordLength, - ]; - } } diff --git a/src/Migration/Resources/Auth/Membership.php b/src/Migration/Resources/Auth/Membership.php index efe5816..a97915a 100644 --- a/src/Migration/Resources/Auth/Membership.php +++ b/src/Migration/Resources/Auth/Membership.php @@ -10,20 +10,50 @@ */ class Membership extends Resource { - protected Team $team; - - protected User $user; - - protected array $roles; + /** + * @param string $id + * @param Team $team + * @param User $user + * @param array $roles + * @param bool $active + */ + public function __construct( + string $id, + private readonly Team $team, + private readonly User $user, + private readonly array $roles = [], + private readonly bool $active = true + ) { + $this->id = $id; + } - protected bool $active = true; + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + Team::fromArray($array['team'] ?? []), + User::fromArray($array['user'] ?? []), + $array['roles'] ?? [], + $array['active'] ?? true + ); + } - public function __construct(Team $team, User $user, array $roles = [], bool $active = true) + /** + * @return array + */ + public function jsonSerialize(): array { - $this->team = $team; - $this->user = $user; - $this->roles = $roles; - $this->active = $active; + return [ + 'id' => $this->id, + 'team' => $this->team, + 'user' => $this->user, + 'roles' => $this->roles, + 'active' => $this->active, + ]; } public static function getName(): string @@ -41,55 +71,21 @@ public function getTeam(): Team return $this->team; } - public function setTeam(Team $team): self - { - $this->team = $team; - - return $this; - } - public function getUser(): User { return $this->user; } - public function setUser(User $user): self - { - $this->user = $user; - - return $this; - } - + /** + * @return array + */ public function getRoles(): array { return $this->roles; } - public function setRoles(array $roles): self - { - $this->roles = $roles; - - return $this; - } - public function getActive(): bool { return $this->active; } - - public function setActive(bool $active): self - { - $this->active = $active; - - return $this; - } - - public function asArray(): array - { - return [ - 'userId' => $this->user->getId(), - 'roles' => $this->roles, - 'active' => $this->active, - ]; - } } diff --git a/src/Migration/Resources/Auth/Team.php b/src/Migration/Resources/Auth/Team.php index b379129..c9d8440 100644 --- a/src/Migration/Resources/Auth/Team.php +++ b/src/Migration/Resources/Auth/Team.php @@ -3,23 +3,50 @@ namespace Utopia\Migration\Resources\Auth; use Utopia\Migration\Resource; -use Utopia\Migration\Resources\User; use Utopia\Migration\Transfer; class Team extends Resource { - protected string $name; - - protected array $preferences = []; + /** + * @param string $id + * @param string $name + * @param array $preferences + * @param array $members + */ + public function __construct( + string $id, + private readonly string $name, + private readonly array $preferences = [], + private readonly array $members = [] + ) { + $this->id = $id; + } - protected array $members = []; + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + $array['name'], + $array['preferences'] ?? [], + $array['members'] ?? [] + ); + } - public function __construct(string $id, string $name, array $preferences = [], array $members = []) + /** + * @return array + */ + public function jsonSerialize(): array { - $this->id = $id; - $this->name = $name; - $this->preferences = $preferences; - $this->members = $members; + return [ + 'id' => $this->id, + 'name' => $this->name, + 'preferences' => $this->preferences, + 'members' => $this->members, + ]; } public static function getName(): string @@ -37,46 +64,19 @@ public function getTeamName(): string return $this->name; } - public function setTeamName(string $name): self - { - $this->name = $name; - - return $this; - } - + /** + * @return array + */ public function getPreferences(): array { return $this->preferences; } - public function setPreferences(array $preferences): self - { - $this->preferences = $preferences; - - return $this; - } - - public function getMembers(): array - { - return $this->members; - } - /** - * @param User[] $members + * @return array */ - public function setMembers(array $members): self - { - $this->members = $members; - - return $this; - } - - public function asArray(): array + public function getMembers(): array { - return [ - 'id' => $this->id, - 'name' => $this->name, - 'preferences' => $this->preferences, - ]; + return $this->members; } } diff --git a/src/Migration/Resources/Auth/User.php b/src/Migration/Resources/Auth/User.php index df8430c..3f72eef 100644 --- a/src/Migration/Resources/Auth/User.php +++ b/src/Migration/Resources/Auth/User.php @@ -7,94 +7,97 @@ class User extends Resource { - protected ?string $email = null; - - protected ?string $username = null; - - protected ?Hash $passwordHash = null; - - protected ?string $phone = null; - - protected array $labels = []; - - protected string $oauthProvider = ''; - - protected bool $emailVerified = false; - - protected bool $phoneVerified = false; - - protected bool $disabled = false; - - protected array $preferences = []; - + /** + * @param string $id + * @param string $email + * @param string $username + * @param ?Hash $passwordHash + * @param ?string $phone + * @param array $labels + * @param string $oauthProvider + * @param bool $emailVerified + * @param bool $phoneVerified + * @param bool $disabled + * @param array $preferences + */ public function __construct( string $id, - ?string $email = null, - ?string $username = null, - ?Hash $passwordHash = null, - ?string $phone = null, - array $labels = [], - string $oauthProvider = '', - bool $emailVerified = false, - bool $phoneVerified = false, - bool $disabled = false, - array $preferences = [] + private readonly string $email = '', + private readonly string $username = '', + private readonly ?Hash $passwordHash = null, + private readonly ?string $phone = null, + private readonly array $labels = [], + private readonly string $oauthProvider = '', + private readonly bool $emailVerified = false, + private readonly bool $phoneVerified = false, + private readonly bool $disabled = false, + private readonly array $preferences = [] ) { $this->id = $id; - $this->email = $email; - $this->username = $username; - $this->passwordHash = $passwordHash; - $this->phone = $phone; - $this->labels = $labels; - $this->oauthProvider = $oauthProvider; - $this->emailVerified = $emailVerified; - $this->phoneVerified = $phoneVerified; - $this->disabled = $disabled; - $this->preferences = $preferences; } /** - * Get Name + * @param array $array + * @return self */ - public static function getName(): string + public static function fromArray(array $array): self { - return Resource::TYPE_USER; + return new self( + $array['id'], + $array['email'] ?? '', + $array['username'] ?? '', + $array['passwordHash'] ?? null, + $array['phone'] ?? '', + $array['labels'] ?? [], + $array['oauthProvider'] ?? '', + $array['emailVerified'] ?? false, + $array['phoneVerified'] ?? false, + $array['disabled'] ?? false, + $array['preferences'] ?? [] + ); } /** - * Get Email + * @return array */ - public function getEmail(): ?string + public function jsonSerialize(): array { - return $this->email; + return [ + 'id' => $this->id, + 'email' => $this->email, + 'username' => $this->username, + 'passwordHash' => $this->passwordHash, + 'phone' => $this->phone, + 'labels' => $this->labels, + 'oauthProvider' => $this->oauthProvider, + 'emailVerified' => $this->emailVerified, + 'phoneVerified' => $this->phoneVerified, + 'disabled' => $this->disabled, + 'preferences' => $this->preferences, + ]; } /** - * Set Email + * Get Name */ - public function setEmail(string $email): self + public static function getName(): string { - $this->email = $email; - - return $this; + return Resource::TYPE_USER; } /** - * Get Username + * Get Email */ - public function getUsername(): ?string + public function getEmail(): string { - return $this->username; + return $this->email; } - /** - * Set Username + * Get Username */ - public function setUsername(string $username): self + public function getUsername(): ?string { - $this->username = $username; - - return $this; + return $this->username; } /** @@ -105,16 +108,6 @@ public function getPasswordHash(): ?Hash return $this->passwordHash; } - /** - * Set Password Hash - */ - public function setPasswordHash(Hash $passwordHash): self - { - $this->passwordHash = $passwordHash; - - return $this; - } - /** * Get Phone */ @@ -123,34 +116,16 @@ public function getPhone(): ?string return $this->phone; } - /** - * Set Phone - */ - public function setPhone(string $phone): self - { - $this->phone = $phone; - - return $this; - } - /** * Get Labels + * + * @return array */ public function getLabels(): array { return $this->labels; } - /** - * Set Labels - */ - public function setLabels(array $labels): self - { - $this->labels = $labels; - - return $this; - } - /** * Get OAuth Provider */ @@ -159,16 +134,6 @@ public function getOAuthProvider(): string return $this->oauthProvider; } - /** - * Set OAuth Provider - */ - public function setOAuthProvider(string $oauthProvider): self - { - $this->oauthProvider = $oauthProvider; - - return $this; - } - /** * Get Email Verified */ @@ -177,16 +142,6 @@ public function getEmailVerified(): bool return $this->emailVerified; } - /** - * Set Email Verified - */ - public function setEmailVerified(bool $verified): self - { - $this->emailVerified = $verified; - - return $this; - } - /** * Get Email Verified */ @@ -195,16 +150,6 @@ public function getPhoneVerified(): bool return $this->phoneVerified; } - /** - * Set Phone Verified - */ - public function setPhoneVerified(bool $verified): self - { - $this->phoneVerified = $verified; - - return $this; - } - public function getGroup(): string { return Transfer::GROUP_AUTH; @@ -218,48 +163,13 @@ public function getDisabled(): bool return $this->disabled; } - /** - * Set Disabled - */ - public function setDisabled(bool $disabled): self - { - $this->disabled = $disabled; - - return $this; - } - /** * Get Preferences + * + * @return array */ public function getPreferences(): array { return $this->preferences; } - - /** - * Set Preferences - */ - public function setPreferences(array $preferences): self - { - $this->preferences = $preferences; - - return $this; - } - - /** - * As Array - */ - public function asArray(): array - { - return [ - 'id' => $this->id, - 'email' => $this->email, - 'username' => $this->username, - 'passwordHash' => $this->passwordHash ? $this->passwordHash->asArray() : null, - 'phone' => $this->phone, - 'oauthProvider' => $this->oauthProvider, - 'emailVerified' => $this->emailVerified, - 'phoneVerified' => $this->phoneVerified, - ]; - } } diff --git a/src/Migration/Resources/Database/Attribute.php b/src/Migration/Resources/Database/Attribute.php index a81909b..2d50d2a 100644 --- a/src/Migration/Resources/Database/Attribute.php +++ b/src/Migration/Resources/Database/Attribute.php @@ -7,43 +7,64 @@ abstract class Attribute extends Resource { - public const TYPE_STRING = 'string'; + public const string TYPE_STRING = 'string'; + public const string TYPE_INTEGER = 'int'; + public const string TYPE_FLOAT = 'float'; + public const string TYPE_BOOLEAN = 'bool'; + public const string TYPE_DATETIME = 'dateTime'; + public const string TYPE_EMAIL = 'email'; + public const string TYPE_ENUM = 'enum'; + public const string TYPE_IP = 'IP'; + public const string TYPE_URL = 'URL'; + public const string TYPE_RELATIONSHIP = 'relationship'; - public const TYPE_INTEGER = 'int'; - - public const TYPE_FLOAT = 'float'; - - public const TYPE_BOOLEAN = 'bool'; - - public const TYPE_DATETIME = 'dateTime'; - - public const TYPE_EMAIL = 'email'; - - public const TYPE_ENUM = 'enum'; - - public const TYPE_IP = 'IP'; - - public const TYPE_URL = 'URL'; - - public const TYPE_RELATIONSHIP = 'relationship'; - - protected string $key; - - protected bool $required; - - protected bool $array; - - protected Collection $collection; + /** + * @param string $key + * @param Collection $collection + * @param int $size + * @param bool $required + * @param mixed|null $default + * @param bool $array + * @param bool $signed + * @param string $format + * @param array $formatOptions + * @param array $filters + * @param array $options + */ + public function __construct( + protected readonly string $key, + protected readonly Collection $collection, + protected readonly int $size = 0, + protected readonly bool $required = false, + protected readonly mixed $default = null, + protected readonly bool $array = false, + protected readonly bool $signed = false, + protected readonly string $format = '', + protected readonly array $formatOptions = [], + protected readonly array $filters = [], + protected array $options = [], + ) { + } /** - * @param int $size + * @return array */ - public function __construct(string $key, Collection $collection, bool $required = false, bool $array = false) + public function jsonSerialize(): array { - $this->key = $key; - $this->required = $required; - $this->array = $array; - $this->collection = $collection; + return [ + 'key' => $this->key, + 'collection' => $this->collection, + 'type' => $this->getType(), + 'size' => $this->size, + 'required' => $this->required, + 'default' => $this->default, + 'array' => $this->array, + 'signed' => $this->signed, + 'format' => $this->format, + 'formatOptions' => $this->formatOptions, + 'filters' => $this->filters, + 'options' => $this->options, + ]; } public static function getName(): string @@ -51,7 +72,7 @@ public static function getName(): string return Resource::TYPE_ATTRIBUTE; } - abstract public function getTypeName(): string; + abstract public function getType(): string; public function getGroup(): string { @@ -63,56 +84,62 @@ public function getKey(): string return $this->key; } - public function setKey(string $key): self - { - $this->key = $key; - - return $this; - } - public function getCollection(): Collection { return $this->collection; } - public function setCollection(Collection $collection) + public function getSize(): int { - $this->collection = $collection; - - return $this; + return $this->size; } - public function getRequired(): bool + public function isRequired(): bool { return $this->required; } - public function setRequired(bool $required): self + public function getDefault(): mixed { - $this->required = $required; - - return $this; + return $this->default; } - public function getArray(): bool + public function isArray(): bool { return $this->array; } - public function setArray(bool $array): self + public function isSigned(): bool { - $this->array = $array; + return $this->signed; + } - return $this; + public function getFormat(): string + { + return $this->format; } - public function asArray(): array + /** + * @return array + */ + public function getFormatOptions(): array { - return [ - 'key' => $this->key, - 'required' => $this->required, - 'array' => $this->array, - 'type' => $this->getName(), - ]; + return $this->formatOptions; + } + + /** + * @return array + */ + public function getFilters(): array + { + return $this->filters; + } + + /** + * @return array + */ + public function &getOptions(): array + { + return $this->options; } } diff --git a/src/Migration/Resources/Database/Attributes/Boolean.php b/src/Migration/Resources/Database/Attributes/Boolean.php index 1d43f67..73a2927 100644 --- a/src/Migration/Resources/Database/Attributes/Boolean.php +++ b/src/Migration/Resources/Database/Attributes/Boolean.php @@ -7,42 +7,54 @@ class Boolean extends Attribute { - protected string $key; - - protected bool $required; - - protected bool $array; - - protected ?bool $default; + public function __construct( + string $key, + Collection $collection, + bool $required = false, + ?bool $default = null, + bool $array = false, + ) { + parent::__construct( + $key, + $collection, + required: $required, + default: $default, + array: $array + ); + } /** - * @param ?bool $default + * @param array{ + * key: string, + * collection: array{ + * database: array{ + * id: string, + * name: string, + * }, + * name: string, + * id: string, + * documentSecurity: bool, + * permissions: ?array + * }, + * required: bool, + * array: bool, + * default: ?bool, + * } $array + * @return self */ - public function __construct(string $key, Collection $collection, bool $required = false, bool $array = false, ?bool $default = null) + public static function fromArray(array $array): self { - parent::__construct($key, $collection, $required, $array); - $this->default = $default; + return new self( + $array['key'], + Collection::fromArray($array['collection']), + required: $array['required'], + default: $array['default'], + array: $array['array'], + ); } - public function getTypeName(): string + public function getType(): string { return Attribute::TYPE_BOOLEAN; } - - public function getDefault(): ?bool - { - return $this->default; - } - - public function setDefault(bool $default): void - { - $this->default = $default; - } - - public function asArray(): array - { - return array_merge(parent::asArray(), [ - 'default' => $this->default, - ]); - } } diff --git a/src/Migration/Resources/Database/Attributes/DateTime.php b/src/Migration/Resources/Database/Attributes/DateTime.php index 363f0f5..5ad52c6 100644 --- a/src/Migration/Resources/Database/Attributes/DateTime.php +++ b/src/Migration/Resources/Database/Attributes/DateTime.php @@ -7,29 +7,55 @@ class DateTime extends Attribute { - protected ?string $default; - - /** - * @param ?string $default - */ - public function __construct(string $key, Collection $collection, bool $required = false, bool $array = false, ?string $default = null) - { - parent::__construct($key, $collection, $required, $array); - $this->default = $default; + public function __construct( + string $key, + Collection $collection, + bool $required = false, + ?string $default = null, + bool $array = false, + ) { + parent::__construct( + $key, + $collection, + required: $required, + default: $default, + array: $array, + filters: ['datetime'], + ); } - public function getDefault(): ?string + public function getType(): string { - return $this->default; - } - - public function setDefault(string $default): void - { - $this->default = $default; + return Attribute::TYPE_DATETIME; } - public function getTypeName(): string + /** + * @param array{ + * key: string, + * collection: array{ + * database: array{ + * id: string, + * name: string, + * }, + * name: string, + * id: string, + * documentSecurity: bool, + * permissions: ?array + * }, + * required: bool, + * array: bool, + * default: ?string, + * } $array + * @return self + */ + public static function fromArray(array $array): self { - return Attribute::TYPE_DATETIME; + return new self( + $array['key'], + Collection::fromArray($array['collection']), + required: $array['required'], + default: $array['default'], + array: $array['array'], + ); } } diff --git a/src/Migration/Resources/Database/Attributes/Decimal.php b/src/Migration/Resources/Database/Attributes/Decimal.php index 111b326..2109855 100644 --- a/src/Migration/Resources/Database/Attributes/Decimal.php +++ b/src/Migration/Resources/Database/Attributes/Decimal.php @@ -7,72 +7,81 @@ class Decimal extends Attribute { - protected ?float $default; - - protected ?float $min; - - protected ?float $max; + public function __construct( + string $key, + Collection $collection, + bool $required = false, + ?float $default = null, + bool $array = false, + ?float $min = null, + ?float $max = null, + bool $signed = true, + ) { + $min ??= PHP_FLOAT_MIN; + $max ??= PHP_FLOAT_MAX; + + parent::__construct( + $key, + $collection, + required: $required, + default: $default, + array: $array, + signed: $signed, + formatOptions: [ + 'min' => $min, + 'max' => $max, + ] + ); + } /** - * @param ?float $default - * @param ?float $min - * @param ?float $max + * @param array{ + * key: string, + * collection: array{ + * database: array{ + * id: string, + * name: string, + * }, + * name: string, + * id: string, + * documentSecurity: bool, + * permissions: ?array + * }, + * required: bool, + * array: bool, + * default: ?float, + * formatOptions: array{ + * min: ?float, + * max: ?float + * } + * } $array + * @return self */ - public function __construct(string $key, Collection $collection, bool $required = false, bool $array = false, ?float $default = null, ?float $min = null, ?float $max = null) + public static function fromArray(array $array): self { - parent::__construct($key, $collection, $required, $array); - $this->default = $default; - $this->min = $min; - $this->max = $max; + return new self( + $array['key'], + Collection::fromArray($array['collection']), + required: $array['required'], + default: $array['default'], + array: $array['array'], + min: $array['formatOptions']['min'], + max: $array['formatOptions']['max'], + ); } - public function getTypeName(): string + public function getType(): string { return Attribute::TYPE_FLOAT; } public function getMin(): ?float { - return $this->min; + return (float)$this->formatOptions['min']; } public function getMax(): ?float { - return $this->max; - } - - public function setMin(float $min): self - { - $this->min = $min; - - return $this; - } - - public function setMax(float $max): self - { - $this->max = $max; - - return $this; - } - - public function getDefault(): ?float - { - return $this->default; - } - - public function setDefault(float $default): self - { - $this->default = $default; - - return $this; - } - - public function asArray(): array - { - return array_merge(parent::asArray(), [ - 'min' => $this->getMin(), - 'max' => $this->getMax(), - 'default' => $this->getDefault(), - ]); + return (float)$this->formatOptions['max']; } } diff --git a/src/Migration/Resources/Database/Attributes/Email.php b/src/Migration/Resources/Database/Attributes/Email.php index 4fa025a..57dabc4 100644 --- a/src/Migration/Resources/Database/Attributes/Email.php +++ b/src/Migration/Resources/Database/Attributes/Email.php @@ -5,30 +5,28 @@ use Utopia\Migration\Resources\Database\Attribute; use Utopia\Migration\Resources\Database\Collection; -class Email extends Attribute +class Email extends Text { - protected ?string $default; - - /** - * @param ?string $default - */ - public function __construct(string $key, Collection $collection, bool $required = false, bool $array = false, ?string $default = null) - { - parent::__construct($key, $collection, $required, $array); - $this->default = $default; - } - - public function getDefault(): ?string - { - return $this->default; - } - - public function setDefault(string $default): void - { - $this->default = $default; + public function __construct( + string $key, + Collection $collection, + bool $required = false, + ?string $default = null, + bool $array = false, + int $size = 254 + ) { + parent::__construct( + $key, + $collection, + required: $required, + default: $default, + array: $array, + size: $size, + format: 'email', + ); } - public function getTypeName(): string + public function getType(): string { return Attribute::TYPE_EMAIL; } diff --git a/src/Migration/Resources/Database/Attributes/Enum.php b/src/Migration/Resources/Database/Attributes/Enum.php index e80d36d..0c24e27 100644 --- a/src/Migration/Resources/Database/Attributes/Enum.php +++ b/src/Migration/Resources/Database/Attributes/Enum.php @@ -7,52 +7,78 @@ class Enum extends Attribute { - protected ?string $default; - - protected array $elements; + /** + * @param array $elements + */ + public function __construct( + string $key, + Collection $collection, + array $elements, + bool $required = false, + ?string $default = null, + bool $array = false, + int $size = 256 + ) { + parent::__construct( + $key, + $collection, + size: $size, + required: $required, + default: $default, + array: $array, + format: 'enum', + formatOptions: [ + 'elements' => $elements, + ], + ); + } /** - * @param string[] $elements - * @param ?string $default + * @param array{ + * key: string, + * collection: array{ + * database: array{ + * id: string, + * name: string, + * }, + * name: string, + * id: string, + * documentSecurity: bool, + * permissions: ?array + * }, + * size: int, + * required: bool, + * default: ?string, + * array: bool, + * formatOptions: array{ + * elements: array + * } + * } $array + * @return self */ - public function __construct(string $key, Collection $collection, array $elements, bool $required, bool $array, ?string $default) + public static function fromArray(array $array): self { - parent::__construct($key, $collection, $required, $array); - $this->default = $default; - $this->elements = $elements; + return new self( + $array['key'], + Collection::fromArray($array['collection']), + elements: $array['formatOptions']['elements'], + required: $array['required'], + default: $array['default'], + array: $array['array'], + size: $array['size'], + ); } - public function getTypeName(): string + public function getType(): string { return Attribute::TYPE_ENUM; } + /** + * @return array + */ public function getElements(): array { - return $this->elements; - } - - public function setElements(array $elements): self - { - $this->elements = $elements; - - return $this; - } - - public function getDefault(): ?string - { - return $this->default; - } - - public function setDefault(string $default): void - { - $this->default = $default; - } - - public function asArray(): array - { - return array_merge(parent::asArray(), [ - 'elements' => $this->elements, - ]); + return (array)$this->formatOptions['elements']; } } diff --git a/src/Migration/Resources/Database/Attributes/IP.php b/src/Migration/Resources/Database/Attributes/IP.php index 35f3c1d..f843995 100644 --- a/src/Migration/Resources/Database/Attributes/IP.php +++ b/src/Migration/Resources/Database/Attributes/IP.php @@ -5,27 +5,28 @@ use Utopia\Migration\Resources\Database\Attribute; use Utopia\Migration\Resources\Database\Collection; -class IP extends Attribute +class IP extends Text { - protected ?string $default; - - public function __construct(string $key, Collection $collection, bool $required = false, bool $array = false, ?string $default = null) - { - parent::__construct($key, $collection, $required, $array); - $this->default = $default; - } - - public function getDefault(): ?string - { - return $this->default; - } - - public function setDefault(string $default): void - { - $this->default = $default; + public function __construct( + string $key, + Collection $collection, + bool $required = false, + ?string $default = null, + bool $array = false, + int $size = 39 + ) { + parent::__construct( + $key, + $collection, + required: $required, + default: $default, + array: $array, + size: $size, + format: 'ip', + ); } - public function getTypeName(): string + public function getType(): string { return Attribute::TYPE_IP; } diff --git a/src/Migration/Resources/Database/Attributes/Integer.php b/src/Migration/Resources/Database/Attributes/Integer.php index 75ecee1..0f107df 100644 --- a/src/Migration/Resources/Database/Attributes/Integer.php +++ b/src/Migration/Resources/Database/Attributes/Integer.php @@ -7,72 +7,83 @@ class Integer extends Attribute { - protected ?int $default; - - protected ?int $min; - - protected ?int $max; + public function __construct( + string $key, + Collection $collection, + bool $required = false, + ?int $default = null, + bool $array = false, + ?int $min = null, + ?int $max = null, + bool $signed = true, + ) { + $min ??= PHP_INT_MIN; + $max ??= PHP_INT_MAX; + $size = $max > 2147483647 ? 8 : 4; + + parent::__construct( + $key, + $collection, + size: $size, + required: $required, + default: $default, + array: $array, + signed: $signed, + formatOptions: [ + 'min' => $min, + 'max' => $max, + ] + ); + } /** - * @param ?int $default - * @param ?int $min - * @param ?int $max + * @param array{ + * key: string, + * collection: array{ + * database: array{ + * id: string, + * name: string, + * }, + * name: string, + * id: string, + * documentSecurity: bool, + * permissions: ?array + * }, + * required: bool, + * array: bool, + * default: ?int, + * formatOptions: array{ + * min: ?int, + * max: ?int + * } + * } $array + * @return self */ - public function __construct(string $key, Collection $collection, bool $required = false, bool $array = false, ?int $default = null, ?int $min = null, ?int $max = null) + public static function fromArray(array $array): self { - parent::__construct($key, $collection, $required, $array); - $this->default = $default; - $this->min = $min; - $this->max = $max; + return new self( + $array['key'], + Collection::fromArray($array['collection']), + required: $array['required'], + default: $array['default'], + array: $array['array'], + min: $array['formatOptions']['min'] ?? null, + max: $array['formatOptions']['max'] ?? null + ); } - public function getTypeName(): string + public function getType(): string { return Attribute::TYPE_INTEGER; } public function getMin(): ?int { - return $this->min; + return (int)$this->formatOptions['min']; } public function getMax(): ?int { - return $this->max; - } - - public function setMin(?int $min): self - { - $this->min = $min; - - return $this; - } - - public function setMax(?int $max): self - { - $this->max = $max; - - return $this; - } - - public function getDefault(): ?int - { - return $this->default; - } - - public function setDefault(int $default): self - { - $this->default = $default; - - return $this; - } - - public function asArray(): array - { - return array_merge(parent::asArray(), [ - 'min' => $this->getMin(), - 'max' => $this->getMax(), - 'default' => $this->getDefault(), - ]); + return (int)$this->formatOptions['max']; } } diff --git a/src/Migration/Resources/Database/Attributes/Relationship.php b/src/Migration/Resources/Database/Attributes/Relationship.php index 0aa3b48..b332d65 100644 --- a/src/Migration/Resources/Database/Attributes/Relationship.php +++ b/src/Migration/Resources/Database/Attributes/Relationship.php @@ -2,96 +2,106 @@ namespace Utopia\Migration\Resources\Database\Attributes; +use Utopia\Database\Database; use Utopia\Migration\Resources\Database\Attribute; use Utopia\Migration\Resources\Database\Collection; class Relationship extends Attribute { - protected string $relatedCollection; - - protected string $relationType; - - protected bool $twoWay; - - protected string $twoWayKey; - - protected string $onDelete; - - protected string $side; + public function __construct( + string $key, + Collection $collection, + string $relatedCollection, + string $relationType, + bool $twoWay = false, + ?string $twoWayKey = null, + string $onDelete = Database::RELATION_MUTATE_RESTRICT, + string $side = Database::RELATION_SIDE_PARENT + ) { + parent::__construct( + $key, + $collection, + options: [ + 'relatedCollection' => $relatedCollection, + 'relationType' => $relationType, + 'twoWay' => $twoWay, + 'twoWayKey' => $twoWayKey, + 'onDelete' => $onDelete, + 'side' => $side, + ] + ); + } - public function __construct(string $key, Collection $collection, bool $required = false, bool $array = false, string $relatedCollection = '', string $relationType = '', bool $twoWay = false, string $twoWayKey = '', string $onDelete = '', string $side = '') + /** + * @param array{ + * key: string, + * collection: array{ + * database: array{ + * id: string, + * name: string, + * }, + * name: string, + * id: string, + * documentSecurity: bool, + * permissions: ?array + * }, + * options: array{ + * relatedCollection: string, + * relationType: string, + * twoWay: bool, + * twoWayKey: ?string, + * onDelete: string, + * side: string, + * } + * } $array + * @return self + */ + public static function fromArray(array $array): self { - parent::__construct($key, $collection, $required, $array); - $this->relatedCollection = $relatedCollection; - $this->relationType = $relationType; - $this->twoWay = $twoWay; - $this->twoWayKey = $twoWayKey; - $this->onDelete = $onDelete; - $this->side = $side; + return new self( + $array['key'], + Collection::fromArray($array['collection']), + relatedCollection: $array['options']['relatedCollection'], + relationType: $array['options']['relationType'], + twoWay: $array['options']['twoWay'], + twoWayKey: $array['options']['twoWayKey'], + onDelete: $array['options']['onDelete'], + side: $array['options']['side'], + ); } - public function getTypeName(): string + public function getType(): string { return Attribute::TYPE_RELATIONSHIP; } public function getRelatedCollection(): string { - return $this->relatedCollection; - } - - public function setRelatedCollection(string $relatedCollection): void - { - $this->relatedCollection = $relatedCollection; + return $this->options['relatedCollection']; } public function getRelationType(): string { - return $this->relationType; - } - - public function setRelationType(string $relationType): void - { - $this->relationType = $relationType; + return $this->options['relationType']; } public function getTwoWay(): bool { - return $this->twoWay; + return $this->options['twoWay']; } - public function setTwoWay(bool $twoWay): void + public function getTwoWayKey(): ?string { - $this->twoWay = $twoWay; - } - - public function getTwoWayKey(): string - { - return $this->twoWayKey; - } - - public function setTwoWayKey(string $twoWayKey): void - { - $this->twoWayKey = $twoWayKey; + return $this->options['twoWayKey']; } public function getOnDelete(): string { - return $this->onDelete; - } - - public function setOnDelete(string $onDelete): void - { - $this->onDelete = $onDelete; + return $this->options['onDelete']; } public function getSide(): string { - return $this->side; - } - - public function setSide(string $side): void - { - $this->side = $side; + return $this->options['side']; } } diff --git a/src/Migration/Resources/Database/Attributes/Text.php b/src/Migration/Resources/Database/Attributes/Text.php index 8b4b6ba..6effa0e 100644 --- a/src/Migration/Resources/Database/Attributes/Text.php +++ b/src/Migration/Resources/Database/Attributes/Text.php @@ -2,26 +2,67 @@ namespace Utopia\Migration\Resources\Database\Attributes; +use Utopia\Database\Database; use Utopia\Migration\Resources\Database\Attribute; use Utopia\Migration\Resources\Database\Collection; class Text extends Attribute { - protected ?string $default; - - protected int $size = 256; + public function __construct( + string $key, + Collection $collection, + bool $required = false, + ?string $default = null, + bool $array = false, + int $size = Database::LENGTH_KEY, + string $format = '', + ) { + parent::__construct( + $key, + $collection, + size: $size, + required: $required, + default: $default, + array: $array, + format: $format, + ); + } /** - * @param ?string $default + * @param array{ + * key: string, + * collection: array{ + * database: array{ + * id: string, + * name: string, + * }, + * name: string, + * id: string, + * documentSecurity: bool, + * permissions: ?array + * }, + * required: bool, + * default: ?string, + * array: bool, + * size: int, + * format: string, + * } $array + * @return self */ - public function __construct(string $key, Collection $collection, bool $required = false, bool $array = false, ?string $default = null, int $size = 256) + public static function fromArray(array $array): self { - parent::__construct($key, $collection, $required, $array); - $this->default = $default; - $this->size = $size; + return new self( + $array['key'], + Collection::fromArray($array['collection']), + required: $array['required'], + default: $array['default'] ?? null, + array: $array['array'], + size: $array['size'], + format: $array['format'], + ); } - public function getTypeName(): string + public function getType(): string { return Attribute::TYPE_STRING; } @@ -31,27 +72,8 @@ public function getSize(): int return $this->size; } - public function setSize(int $size): self - { - $this->size = $size; - - return $this; - } - - public function getDefault(): ?string - { - return $this->default; - } - - public function setDefault(string $default): void - { - $this->default = $default; - } - - public function asArray(): array + public function getFormat(): string { - return array_merge(parent::asArray(), [ - 'size' => $this->size, - ]); + return $this->format; } } diff --git a/src/Migration/Resources/Database/Attributes/URL.php b/src/Migration/Resources/Database/Attributes/URL.php index 3b76afe..afe80ce 100644 --- a/src/Migration/Resources/Database/Attributes/URL.php +++ b/src/Migration/Resources/Database/Attributes/URL.php @@ -5,30 +5,28 @@ use Utopia\Migration\Resources\Database\Attribute; use Utopia\Migration\Resources\Database\Collection; -class URL extends Attribute +class URL extends Text { - protected ?string $default; - - /** - * @param ?string $default - */ - public function __construct(string $key, Collection $collection, bool $required = false, bool $array = false, ?string $default = null) - { - parent::__construct($key, $collection, $required, $array); - $this->default = $default; - } - - public function getDefault(): ?string - { - return $this->default; - } - - public function setDefault(string $default): void - { - $this->default = $default; + public function __construct( + string $key, + Collection $collection, + bool $required = false, + ?string $default = null, + bool $array = false, + int $size = 2000 + ) { + parent::__construct( + $key, + $collection, + required: $required, + default: $default, + array: $array, + size: $size, + format: 'url', + ); } - public function getTypeName(): string + public function getType(): string { return Attribute::TYPE_URL; } diff --git a/src/Migration/Resources/Database/Collection.php b/src/Migration/Resources/Database/Collection.php index 5d03619..fcf79e2 100644 --- a/src/Migration/Resources/Database/Collection.php +++ b/src/Migration/Resources/Database/Collection.php @@ -8,28 +8,58 @@ class Collection extends Resource { /** - * @var list + * @param Database $database + * @param string $name + * @param string $id + * @param bool $documentSecurity + * @param array $permissions */ - private array $columns = []; + public function __construct( + private readonly Database $database, + private readonly string $name, + string $id, + private readonly bool $documentSecurity = false, + array $permissions = [], + ) { + $this->id = $id; + $this->permissions = $permissions; + } /** - * @var list + * @param array{ + * database: array{ + * id: string, + * name: string, + * }, + * name: string, + * id: string, + * documentSecurity: bool, + * permissions: ?array + * } $array */ - private array $indexes = []; - - private Database $database; - - protected bool $documentSecurity = false; - - protected string $name; + public static function fromArray(array $array): self + { + return new self( + Database::fromArray($array['database']), + id: $array['id'], + name: $array['name'], + documentSecurity: $array['documentSecurity'], + permissions: $array['permissions'] ?? [] + ); + } - public function __construct(Database $database, string $name, string $id, bool $documentSecurity = false, array $permissions = []) + /** + * @return array + */ + public function jsonSerialize(): array { - $this->database = $database; - $this->name = $name; - $this->id = $id; - $this->documentSecurity = $documentSecurity; - $this->permissions = $permissions; + return array_merge([ + 'database' => $this->database, + 'id' => $this->id, + 'name' => $this->name, + 'documentSecurity' => $this->documentSecurity, + 'permissions' => $this->permissions, + ]); } public static function getName(): string @@ -47,44 +77,13 @@ public function getDatabase(): Database return $this->database; } - public function setDatabase(Database $database): self - { - $this->database = $database; - - return $this; - } - public function getCollectionName(): string { return $this->name; } - public function setCollectionName(string $name): self - { - $this->name = $name; - - return $this; - } - public function getDocumentSecurity(): bool { return $this->documentSecurity; } - - public function setDocumentSecurity(bool $documentSecurity): self - { - $this->documentSecurity = $documentSecurity; - - return $this; - } - - public function asArray(): array - { - return [ - 'name' => $this->name, - 'id' => $this->id, - 'permissions' => $this->permissions, - 'documentSecurity' => $this->documentSecurity, - ]; - } } diff --git a/src/Migration/Resources/Database/Database.php b/src/Migration/Resources/Database/Database.php index 804543d..39d8879 100644 --- a/src/Migration/Resources/Database/Database.php +++ b/src/Migration/Resources/Database/Database.php @@ -15,17 +15,37 @@ class Database extends Resource { + public function __construct( + string $id = '', + private readonly string $name = '', + ) { + $this->id = $id; + // Do we need to $this->name = $name; + } + /** - * @var list + * @param array{ + * id: string, + * name: string, + * } $array */ - private array $collections = []; - - protected string $name; + public static function fromArray(array $array): self + { + return new self( + $array['id'], + $array['name'], + ); + } - public function __construct(string $name = '', string $id = '') + /** + * @return array + */ + public function jsonSerialize(): array { - $this->name = $name; - $this->id = $id; + return [ + 'id' => $this->id, + 'name' => $this->name, + ]; } public static function getName(): string @@ -38,37 +58,8 @@ public function getGroup(): string return Transfer::GROUP_DATABASES; } - public function getDBName(): string + public function getDatabaseName(): string { return $this->name; } - - /** - * @return list - */ - public function getCollections(): array - { - return $this->collections; - } - - /** - * @param list $collections - */ - public function setCollections(array $collections): self - { - $this->collections = $collections; - - return $this; - } - - public function asArray(): array - { - return [ - 'name' => $this->name, - 'id' => $this->id, - 'collections' => array_map(function ($collection) { - return $collection->asArray(); - }, $this->collections), - ]; - } } diff --git a/src/Migration/Resources/Database/Document.php b/src/Migration/Resources/Database/Document.php index 4124bcf..db3d41a 100644 --- a/src/Migration/Resources/Database/Document.php +++ b/src/Migration/Resources/Database/Document.php @@ -7,41 +7,70 @@ class Document extends Resource { - protected Database $database; - - protected Collection $collection; - - protected array $data; - - public function __construct(string $id, Database $database, Collection $collection, array $data = [], array $permissions = []) - { + /** + * @param string $id + * @param Collection $collection + * @param array $data + * @param array $permissions + */ + public function __construct( + string $id, + private readonly Collection $collection, + private readonly array $data = [], + array $permissions = [] + ) { $this->id = $id; - $this->database = $database; - $this->collection = $collection; - $this->data = $data; $this->permissions = $permissions; } - public static function getName(): string + /** + * @param array{ + * id: string, + * collection: array{ + * database: array{ + * id: string, + * name: string, + * }, + * name: string, + * id: string, + * documentSecurity: bool, + * permissions: ?array + * }, + * data: array, + * permissions: ?array + * } $array + */ + public static function fromArray(array $array): self { - return Resource::TYPE_DOCUMENT; + return new self( + $array['id'], + Collection::fromArray($array['collection']), + $array['data'], + $array['permissions'] ?? [] + ); } - public function getGroup(): string + /** + * @return array + */ + public function jsonSerialize(): array { - return Transfer::GROUP_DATABASES; + return [ + 'id' => $this->id, + 'collection' => $this->collection, + 'data' => $this->data, + 'permissions' => $this->permissions, + ]; } - public function getDatabase(): Database + public static function getName(): string { - return $this->database; + return Resource::TYPE_DOCUMENT; } - public function setDatabase(Database $database): self + public function getGroup(): string { - $this->database = $database; - - return $this; + return Transfer::GROUP_DATABASES; } public function getCollection(): Collection @@ -49,38 +78,11 @@ public function getCollection(): Collection return $this->collection; } - public function setCollection(Collection $collection): self - { - $this->collection = $collection; - - return $this; - } - - public function getData(): array - { - return $this->data; - } - /** - * Set Data - * - * @param array $data + * @return array */ - public function setData(array $data): self - { - $this->data = $data; - - return $this; - } - - public function asArray(): array + public function getData(): array { - return [ - 'id' => $this->id, - 'database' => $this->database, - 'collection' => $this->collection, - 'attributes' => $this->data, - 'permissions' => $this->permissions, - ]; + return $this->data; } } diff --git a/src/Migration/Resources/Database/Index.php b/src/Migration/Resources/Database/Index.php index bf0381f..59b4c80 100644 --- a/src/Migration/Resources/Database/Index.php +++ b/src/Migration/Resources/Database/Index.php @@ -7,33 +7,80 @@ class Index extends Resource { - protected string $key; + public const string TYPE_UNIQUE = 'unique'; - protected string $type; + public const string TYPE_FULLTEXT = 'fulltext'; - protected array $attributes; + public const string TYPE_KEY = 'key'; - protected array $orders; - - protected Collection $collection; - - public const TYPE_UNIQUE = 'unique'; - - public const TYPE_FULLTEXT = 'fulltext'; + /** + * @param string $id + * @param string $key + * @param Collection $collection + * @param string $type + * @param array $attributes + * @param array $lengths + * @param array $orders + */ + public function __construct( + string $id, + private readonly string $key, + private readonly Collection $collection, + private readonly string $type = '', + private readonly array $attributes = [], + private readonly array $lengths = [], + private readonly array $orders = [] + ) { + $this->id = $id; + } - public const TYPE_KEY = 'key'; + /** + * @param array{ + * id: string, + * key: string, + * collection: array{ + * database: array{ + * id: string, + * name: string, + * }, + * name: string, + * id: string, + * documentSecurity: bool, + * permissions: ?array + * }, + * type: string, + * attributes: array, + * lengths: ?array, + * orders: ?array + * } $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + $array['key'], + Collection::fromArray($array['collection']), + $array['type'], + $array['attributes'], + $array['lengths'] ?? [], + $array['orders'] ?? [] + ); + } /** - * @param list $attributes + * @return array */ - public function __construct(string $id, string $key, Collection $collection, string $type = '', array $attributes = [], array $orders = []) + public function jsonSerialize(): array { - $this->id = $id; - $this->key = $key; - $this->type = $type; - $this->attributes = $attributes; - $this->orders = $orders; - $this->collection = $collection; + return [ + 'id' => $this->getId(), + 'key' => $this->key, + 'collection' => $this->collection, + 'type' => $this->type, + 'attributes' => $this->attributes, + 'lengths' => $this->lengths, + 'orders' => $this->orders, + ]; } public static function getName(): string @@ -51,71 +98,29 @@ public function getKey(): string return $this->key; } - public function setKey(string $key): self - { - $this->key = $key; - - return $this; - } - public function getCollection(): Collection { return $this->collection; } - public function setCollection(Collection $collection): self - { - $this->collection = $collection; - - return $this; - } - public function getType(): string { return $this->type; } - public function setType(string $type): self - { - $this->type = $type; - - return $this; - } - + /** + * @return array + */ public function getAttributes(): array { return $this->attributes; } /** - * @param list $attributes + * @return array */ - public function setAttributes(array $attributes): self - { - $this->attributes = $attributes; - - return $this; - } - public function getOrders(): array { return $this->orders; } - - public function setOrders(array $orders): self - { - $this->orders = $orders; - - return $this; - } - - public function asArray(): array - { - return [ - 'key' => $this->key, - 'type' => $this->type, - 'attributes' => $this->attributes, - 'orders' => $this->orders, - ]; - } } diff --git a/src/Migration/Resources/Functions/Deployment.php b/src/Migration/Resources/Functions/Deployment.php index 39927c5..5be2bd1 100644 --- a/src/Migration/Resources/Functions/Deployment.php +++ b/src/Migration/Resources/Functions/Deployment.php @@ -7,30 +7,52 @@ class Deployment extends Resource { - protected Func $func; - - protected string $entrypoint; - - protected int $size; - - protected int $start; - - protected int $end; - - protected string $data; + public function __construct( + string $id, + private readonly Func $func, + private readonly int $size, + private readonly string $entrypoint, + private int $start = 0, + private int $end = 0, + private string $data = '', + private readonly bool $activated = false + ) { + $this->id = $id; + } - protected bool $activated; + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + Func::fromArray($array['func']), + $array['size'], + $array['entrypoint'], + $array['start'] ?? 0, + $array['end'] ?? 0, + $array['data'] ?? '', + $array['activated'] ?? false + ); + } - public function __construct(string $id, Func $func, int $size, string $entrypoint, int $start = 0, int $end = 0, string $data = '', bool $activated = false) + /** + * @return array + */ + public function jsonSerialize(): array { - $this->id = $id; - $this->func = $func; - $this->size = $size; - $this->entrypoint = $entrypoint; - $this->start = $start; - $this->end = $end; - $this->data = $data; - $this->activated = $activated; + return [ + 'id' => $this->id, + 'func' => $this->func, + 'size' => $this->size, + 'entrypoint' => $this->entrypoint, + 'start' => $this->start, + 'end' => $this->end, + 'data' => $this->data, + 'activated' => $this->activated, + ]; } public static function getName(): string @@ -48,32 +70,11 @@ public function getFunction(): Func return $this->func; } - public function setFunction(Func $func): self - { - $this->func = $func; - - return $this; - } - - public function setSize(int $size): self - { - $this->size = $size; - - return $this; - } - public function getSize(): int { return $this->size; } - public function setEntrypoint(string $entrypoint): self - { - $this->entrypoint = $entrypoint; - - return $this; - } - public function getEntrypoint(): string { return $this->entrypoint; @@ -115,28 +116,8 @@ public function getData(): string return $this->data; } - public function setActivated(bool $activated): self - { - $this->activated = $activated; - - return $this; - } - public function getActivated(): bool { return $this->activated; } - - public function asArray(): array - { - return [ - 'id' => $this->id, - 'func' => $this->func->asArray(), - 'size' => $this->size, - 'entrypoint' => $this->entrypoint, - 'start' => $this->start, - 'end' => $this->end, - 'activated' => $this->activated, - ]; - } } diff --git a/src/Migration/Resources/Functions/EnvVar.php b/src/Migration/Resources/Functions/EnvVar.php index c2f7b75..2cd7f97 100644 --- a/src/Migration/Resources/Functions/EnvVar.php +++ b/src/Migration/Resources/Functions/EnvVar.php @@ -7,17 +7,40 @@ class EnvVar extends Resource { - protected Func $func; - - protected string $key; + public function __construct( + string $id, + private readonly Func $func, + private readonly string $key, + private readonly string $value + ) { + $this->id = $id; + } - protected string $value; + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + Func::fromArray($array['func']), + $array['key'], + $array['value'] + ); + } - public function __construct(Func $func, string $key, string $value) + /** + * @return array + */ + public function jsonSerialize(): array { - $this->func = $func; - $this->key = $key; - $this->value = $value; + return [ + 'id' => $this->id, + 'func' => $this->func, + 'key' => $this->key, + 'value' => $this->value, + ]; } public static function getName(): string @@ -35,43 +58,13 @@ public function getFunc(): Func return $this->func; } - public function setFunc(Func $func): self - { - $this->func = $func; - - return $this; - } - public function getKey(): string { return $this->key; } - public function setKey(string $key): self - { - $this->key = $key; - - return $this; - } - public function getValue(): string { return $this->value; } - - public function setValue(string $value): self - { - $this->value = $value; - - return $this; - } - - public function asArray(): array - { - return [ - 'func' => $this->func->getId(), - 'key' => $this->key, - 'value' => $this->value, - ]; - } } diff --git a/src/Migration/Resources/Functions/Func.php b/src/Migration/Resources/Functions/Func.php index 93c62a3..930f921 100644 --- a/src/Migration/Resources/Functions/Func.php +++ b/src/Migration/Resources/Functions/Func.php @@ -7,33 +7,70 @@ class Func extends Resource { - protected string $name; - - protected array $execute; - - protected bool $enabled; - - protected string $runtime; - - protected array $events; - - protected string $schedule; - - protected int $timeout; + /** + * @param string $id + * @param string $name + * @param string $runtime + * @param array $execute + * @param bool $enabled + * @param array $events + * @param string $schedule + * @param int $timeout + * @param string $activeDeployment + * @param string $entrypoint + */ + public function __construct( + string $id, + private readonly string $name, + private readonly string $runtime, + private readonly array $execute = [], + private readonly bool $enabled = true, + private readonly array $events = [], + private readonly string $schedule = '', + private readonly int $timeout = 0, + private readonly string $activeDeployment = '', + private readonly string $entrypoint = '' + ) { + $this->id = $id; + } - protected string $activeDeployment; + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + $array['name'], + $array['runtime'], + $array['execute'] ?? [], + $array['enabled'] ?? true, + $array['events'] ?? [], + $array['schedule'] ?? '', + $array['timeout'] ?? 0, + $array['activeDeployment'] ?? '', + $array['entrypoint'] ?? '' + ); + } - public function __construct(string $name, string $id, string $runtime, array $execute = [], bool $enabled = true, array $events = [], string $schedule = '', int $timeout = 0, string $activeDeployment = '') + /** + * @return array + */ + public function jsonSerialize(): array { - $this->name = $name; - $this->id = $id; - $this->execute = $execute; - $this->enabled = $enabled; - $this->runtime = $runtime; - $this->events = $events; - $this->schedule = $schedule; - $this->timeout = $timeout; - $this->activeDeployment = $activeDeployment; + return [ + 'id' => $this->id, + 'name' => $this->name, + 'execute' => $this->execute, + 'enabled' => $this->enabled, + 'runtime' => $this->runtime, + 'events' => $this->events, + 'schedule' => $this->schedule, + 'timeout' => $this->timeout, + 'activeDeployment' => $this->activeDeployment, + 'entrypoint' => $this->entrypoint, + ]; } public static function getName(): string @@ -51,102 +88,49 @@ public function getFunctionName(): string return $this->name; } + /** + * @return array + */ public function getExecute(): array { return $this->execute; } - public function setExecute(array $execute): self - { - $this->execute = $execute; - - return $this; - } - public function getEnabled(): bool { return $this->enabled; } - public function setEnabled(bool $enabled): self - { - $this->enabled = $enabled; - - return $this; - } - public function getRuntime(): string { return $this->runtime; } - public function setRuntime(string $runtime): self - { - $this->runtime = $runtime; - - return $this; - } - + /** + * @return array + */ public function getEvents(): array { return $this->events; } - public function setEvents(array $events): self - { - $this->events = $events; - - return $this; - } - public function getSchedule(): string { return $this->schedule; } - public function setSchedule(string $schedule): self - { - $this->schedule = $schedule; - - return $this; - } - public function getTimeout(): int { return $this->timeout; } - public function setTimeout(int $timeout): self - { - $this->timeout = $timeout; - - return $this; - } - public function getActiveDeployment(): string { return $this->activeDeployment; } - public function setActiveDeployment(string $activeDeployment): self + public function getEntrypoint(): string { - $this->activeDeployment = $activeDeployment; - - return $this; - } - - public function asArray(): array - { - return [ - 'name' => $this->name, - 'id' => $this->id, - 'execute' => $this->execute, - 'enabled' => $this->enabled, - 'runtime' => $this->runtime, - 'events' => $this->events, - 'schedule' => $this->schedule, - 'timeout' => $this->timeout, - 'activeDeployment' => $this->activeDeployment, - ]; + return $this->entrypoint; } } diff --git a/src/Migration/Resources/Storage/Bucket.php b/src/Migration/Resources/Storage/Bucket.php index 6d26f00..cd6fdf0 100644 --- a/src/Migration/Resources/Storage/Bucket.php +++ b/src/Migration/Resources/Storage/Bucket.php @@ -7,36 +7,74 @@ class Bucket extends Resource { - protected ?bool $fileSecurity; - - protected string $name; - - protected ?bool $enabled; - - protected ?int $maxFileSize; - - protected ?array $allowedFileExtensions; - - protected ?string $compression; - - protected ?bool $encryption; - - protected ?bool $antiVirus; - - protected bool $updateLimits = false; - - public function __construct(string $id = '', string $name = '', array $permissions = [], bool $fileSecurity = false, bool $enabled = false, ?int $maxFileSize = null, array $allowedFileExtensions = [], string $compression = 'none', bool $encryption = false, bool $antiVirus = false, bool $updateLimits = false) - { + /** + * @param string $id + * @param string $name + * @param array $permissions + * @param bool $fileSecurity + * @param bool $enabled + * @param int|null $maxFileSize + * @param array $allowedFileExtensions + * @param string $compression + * @param bool $encryption + * @param bool $antiVirus + * @param bool $updateLimits + */ + public function __construct( + string $id = '', + private readonly string $name = '', + array $permissions = [], + private readonly bool $fileSecurity = false, + private readonly bool $enabled = false, + private readonly ?int $maxFileSize = null, + private readonly array $allowedFileExtensions = [], + private readonly string $compression = 'none', + private readonly bool $encryption = false, + private readonly bool $antiVirus = false, + private readonly bool $updateLimits = false, + ) { $this->id = $id; - $this->name = $name; $this->permissions = $permissions; - $this->fileSecurity = $fileSecurity; - $this->enabled = $enabled; - $this->maxFileSize = $maxFileSize; - $this->allowedFileExtensions = $allowedFileExtensions; - $this->compression = $compression; - $this->encryption = $encryption; - $this->antiVirus = $antiVirus; + } + + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + $array['name'] ?? '', + $array['permissions'] ?? [], + $array['fileSecurity'] ?? false, + $array['enabled'] ?? false, + $array['maxFileSize'] ?? null, + $array['allowedFileExtensions'] ?? [], + $array['compression'] ?? 'none', + $array['encryption'] ?? false, + $array['antiVirus'] ?? false, + $array['updateLimits'] ?? false + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'fileSecurity' => $this->fileSecurity, + 'enabled' => $this->enabled, + 'maxFileSize' => $this->maxFileSize, + 'allowedFileExtensions' => $this->allowedFileExtensions, + 'compression' => $this->compression, + 'encryption' => $this->encryption, + 'antiVirus' => $this->antiVirus, + 'updateLimits' => $this->updateLimits, + ]; } public static function getName(): string @@ -54,122 +92,43 @@ public function getFileSecurity(): bool return $this->fileSecurity; } - public function setFileSecurity(bool $fileSecurity): self - { - $this->fileSecurity = $fileSecurity; - - return $this; - } - public function getBucketName(): string { return $this->name; } - public function setBucketName(string $name): self - { - $this->name = $name; - - return $this; - } public function getEnabled(): bool { return $this->enabled; } - public function setEnabled(bool $enabled): self - { - $this->enabled = $enabled; - - return $this; - } - public function getMaxFileSize(): ?int { return $this->maxFileSize; } - public function setMaxFileSize(?int $maxFileSize): self - { - $this->maxFileSize = $maxFileSize; - - return $this; - } + /** + * @return array + */ public function getAllowedFileExtensions(): array { return $this->allowedFileExtensions; } - public function setAllowedFileExtensions(array $allowedFileExtensions): self - { - $this->allowedFileExtensions = $allowedFileExtensions; - - return $this; - } - public function getCompression(): string { return $this->compression; } - public function setCompression(string $compression): self - { - $this->compression = $compression; - - return $this; - } - public function getEncryption(): bool { return $this->encryption; } - public function setEncryption(bool $encryption): self - { - $this->encryption = $encryption; - - return $this; - } - public function getAntiVirus(): bool { return $this->antiVirus; } - - public function setAntiVirus(bool $antiVirus): self - { - $this->antiVirus = $antiVirus; - - return $this; - } - - public function getUpdateLimits(): bool - { - return $this->updateLimits; - } - - public function setUpdateLimits(bool $updateLimits): self - { - $this->updateLimits = $updateLimits; - - return $this; - } - - public function asArray(): array - { - return [ - 'id' => $this->id, - 'permissions' => $this->permissions, - 'fileSecurity' => $this->fileSecurity, - 'name' => $this->name, - 'enabled' => $this->enabled, - 'maxFileSize' => $this->maxFileSize, - 'allowedFileExtensions' => $this->allowedFileExtensions, - 'compression' => $this->compression, - 'encryption' => $this->encryption, - 'antiVirus' => $this->antiVirus, - ]; - } } diff --git a/src/Migration/Resources/Storage/File.php b/src/Migration/Resources/Storage/File.php index 99914e7..ba47ec0 100644 --- a/src/Migration/Resources/Storage/File.php +++ b/src/Migration/Resources/Storage/File.php @@ -7,34 +7,70 @@ class File extends Resource { - protected Bucket $bucket; - - protected string $name; - - protected string $signature; - - protected string $mimeType; - - protected int $size; - - protected string $data; - - protected int $start; + /** + * @param string $id + * @param Bucket $bucket + * @param string $name + * @param string $signature + * @param string $mimeType + * @param array $permissions + * @param int $size + * @param string $data + * @param int $start + * @param int $end + */ + public function __construct( + string $id, + private readonly Bucket $bucket, + private readonly string $name = '', + private readonly string $signature = '', + private readonly string $mimeType = '', + array $permissions = [], + private readonly int $size = 0, + private string $data = '', + private int $start = 0, + private int $end = 0 + ) { + $this->id = $id; + $this->permissions = $permissions; + } - protected int $end; + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + Bucket::fromArray($array['bucket']), + $array['name'] ?? '', + $array['signature'] ?? '', + $array['mimeType'] ?? '', + $array['permissions'] ?? [], + $array['size'] ?? 0, + $array['data'] ?? '', + $array['start'] ?? 0, + $array['end'] ?? 0 + ); + } - public function __construct(string $id = '', ?Bucket $bucket = null, string $name = '', string $signature = '', string $mimeType = '', array $permissions = [], int $size = 0, string $data = '', int $start = 0, int $end = 0) + /** + * @return array + */ + public function jsonSerialize(): array { - $this->id = $id; - $this->bucket = $bucket; - $this->name = $name; - $this->signature = $signature; - $this->mimeType = $mimeType; - $this->permissions = $permissions; - $this->size = $size; - $this->data = $data; - $this->start = $start; - $this->end = $end; + return [ + 'id' => $this->id, + 'bucket' => $this->bucket, + 'name' => $this->name, + 'signature' => $this->signature, + 'mimeType' => $this->mimeType, + 'permissions' => $this->permissions, + 'size' => $this->size, + 'start' => $this->start, + 'end' => $this->end, + ]; } public static function getName(): string @@ -52,25 +88,11 @@ public function getBucket(): Bucket return $this->bucket; } - public function setBucket(Bucket $bucket): self - { - $this->bucket = $bucket; - - return $this; - } - public function getFileName(): string { return $this->name; } - public function setName(string $name): self - { - $this->name = $name; - - return $this; - } - public function getSize(): int { return $this->size; @@ -81,25 +103,11 @@ public function getSignature(): string return $this->signature; } - public function setSignature(string $signature): self - { - $this->signature = $signature; - - return $this; - } - public function getMimeType(): string { return $this->mimeType; } - public function setMimeType(string $mimeType): self - { - $this->mimeType = $mimeType; - - return $this; - } - public function getData(): string { return $this->data; @@ -145,19 +153,4 @@ public function getChunkSize(): int { return $this->end - $this->start; } - - public function asArray(): array - { - return [ - 'id' => $this->id, - 'bucket' => $this->bucket->getId(), - 'name' => $this->name, - 'signature' => $this->signature, - 'mimeType' => $this->mimeType, - 'permissions' => $this->permissions, - 'size' => $this->size, - 'start' => $this->start, - 'end' => $this->end, - ]; - } } diff --git a/src/Migration/Source.php b/src/Migration/Source.php index 45ba29a..46333b6 100644 --- a/src/Migration/Source.php +++ b/src/Migration/Source.php @@ -4,6 +4,9 @@ abstract class Source extends Target { + /** + * @var callable(array): void $transferCallback + */ protected $transferCallback; /** @@ -11,6 +14,10 @@ abstract class Source extends Target */ public array $previousReport = []; + /** + * @param array $resources + * @return void + */ public function callback(array $resources): void { ($this->transferCallback)($resources); @@ -19,48 +26,53 @@ public function callback(array $resources): void /** * Transfer Resources into destination * - * @param string[] $resources Resources to transfer - * @param callable $callback Callback to run after transfer + * @param array $resources Resources to transfer + * @param callable $callback Callback to run after transfer + * @param string $rootResourceId Root resource ID, If enabled you can only transfer a single root resource */ - public function run(array $resources, callable $callback): void + public function run(array $resources, callable $callback, string $rootResourceId = '', string $rootResourceType = ''): void { + $this->rootResourceId = $rootResourceId; + $this->rootResourceType = $rootResourceType; + $this->transferCallback = function (array $returnedResources) use ($callback, $resources) { - $prunedResurces = []; + $prunedResources = []; foreach ($returnedResources as $resource) { - /** @var resource $resource */ + /** @var Resource $resource */ if (! in_array($resource->getName(), $resources)) { $resource->setStatus(Resource::STATUS_SKIPPED); } else { - $prunedResurces[] = $resource; + $prunedResources[] = $resource; } } $callback($returnedResources); - $this->cache->addAll($prunedResurces); + $this->cache->addAll($prunedResources); }; - $this->exportResources($resources, 100); + $this->exportResources($resources); } /** * Export Resources * - * @param string[] $resources Resources to export - * @param int $batchSize Max 100 + * @param array $resources Resources to export */ - public function exportResources(array $resources, int $batchSize) + public function exportResources(array $resources): void { // Convert Resources back into their relevant groups + $batchSize = $this->getBatchSize(); + $groups = []; foreach ($resources as $resource) { - if (in_array($resource, Transfer::GROUP_AUTH_RESOURCES)) { + if (\in_array($resource, Transfer::GROUP_AUTH_RESOURCES)) { $groups[Transfer::GROUP_AUTH][] = $resource; - } elseif (in_array($resource, Transfer::GROUP_DATABASES_RESOURCES)) { + } elseif (\in_array($resource, Transfer::GROUP_DATABASES_RESOURCES)) { $groups[Transfer::GROUP_DATABASES][] = $resource; - } elseif (in_array($resource, Transfer::GROUP_STORAGE_RESOURCES)) { + } elseif (\in_array($resource, Transfer::GROUP_STORAGE_RESOURCES)) { $groups[Transfer::GROUP_STORAGE][] = $resource; - } elseif (in_array($resource, Transfer::GROUP_FUNCTIONS_RESOURCES)) { + } elseif (\in_array($resource, Transfer::GROUP_FUNCTIONS_RESOURCES)) { $groups[Transfer::GROUP_FUNCTIONS][] = $resource; } } @@ -87,36 +99,40 @@ public function exportResources(array $resources, int $batchSize) } } } + public function getBatchSize(): int + { + return 100; + } /** * Export Auth Group * * @param int $batchSize Max 100 - * @param string[] $resources Resources to export + * @param array $resources Resources to export */ - abstract protected function exportGroupAuth(int $batchSize, array $resources); + abstract protected function exportGroupAuth(int $batchSize, array $resources): void; /** * Export Databases Group * * @param int $batchSize Max 100 - * @param string[] $resources Resources to export + * @param array $resources Resources to export */ - abstract protected function exportGroupDatabases(int $batchSize, array $resources); + abstract protected function exportGroupDatabases(int $batchSize, array $resources): void; /** * Export Storage Group * * @param int $batchSize Max 5 - * @param string[] $resources Resources to export + * @param array $resources Resources to export */ - abstract protected function exportGroupStorage(int $batchSize, array $resources); + abstract protected function exportGroupStorage(int $batchSize, array $resources): void; /** * Export Functions Group * * @param int $batchSize Max 100 - * @param string[] $resources Resources to export + * @param array $resources Resources to export */ - abstract protected function exportGroupFunctions(int $batchSize, array $resources); + abstract protected function exportGroupFunctions(int $batchSize, array $resources): void; } diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 00abd19..07789a4 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -2,6 +2,7 @@ namespace Utopia\Migration\Sources; +use Appwrite\AppwriteException; use Appwrite\Client; use Appwrite\Query; use Appwrite\Services\Databases; @@ -9,6 +10,7 @@ use Appwrite\Services\Storage; use Appwrite\Services\Teams; use Appwrite\Services\Users; +use Utopia\Database\Database as UtopiaDatabase; use Utopia\Migration\Exception; use Utopia\Migration\Resource; use Utopia\Migration\Resources\Auth\Hash; @@ -40,25 +42,36 @@ class Appwrite extends Source { - /** - * @var Client|null - */ - protected $client = null; + protected Client $client; - protected string $project = ''; + private Users $users; - protected string $key = ''; + private Teams $teams; - public function __construct(string $project, string $endpoint, string $key) - { + private Databases $database; + + private Storage $storage; + + private Functions $functions; + + public function __construct( + protected string $project, + string $endpoint, + protected string $key + ) { $this->client = (new Client()) ->setEndpoint($endpoint) ->setProject($project) ->setKey($key); + $this->users = new Users($this->client); + $this->teams = new Teams($this->client); + $this->database = new Databases($this->client); + $this->storage = new Storage($this->client); + $this->functions = new Functions($this->client); + $this->endpoint = $endpoint; - $this->project = $project; - $this->key = $key; + $this->headers['X-Appwrite-Project'] = $this->project; $this->headers['X-Appwrite-Key'] = $this->key; } @@ -68,6 +81,9 @@ public static function getName(): string return 'Appwrite'; } + /** + * @return array + */ public static function getSupportedResources(): array { return [ @@ -96,109 +112,129 @@ public static function getSupportedResources(): array ]; } + /** + * @param array $resources + * @return array + * + * @throws \Exception + */ public function report(array $resources = []): array { $report = []; - $currentPermission = ''; if (empty($resources)) { $resources = $this->getSupportedResources(); } - $usersClient = new Users($this->client); - $teamsClient = new Teams($this->client); - $databaseClient = new Databases($this->client); - $storageClient = new Storage($this->client); - $functionsClient = new Functions($this->client); - // Auth try { - $currentPermission = 'users.read'; - if (in_array(Resource::TYPE_USER, $resources)) { - $report[Resource::TYPE_USER] = $usersClient->list()['total']; + $scope = 'users.read'; + if (\in_array(Resource::TYPE_USER, $resources)) { + $report[Resource::TYPE_USER] = $this->users->list()['total']; } - $currentPermission = 'teams.read'; - if (in_array(Resource::TYPE_TEAM, $resources)) { - $report[Resource::TYPE_TEAM] = $teamsClient->list()['total']; + $scope = 'teams.read'; + if (\in_array(Resource::TYPE_TEAM, $resources)) { + $report[Resource::TYPE_TEAM] = $this->teams->list()['total']; } - if (in_array(Resource::TYPE_MEMBERSHIP, $resources)) { + if (\in_array(Resource::TYPE_MEMBERSHIP, $resources)) { $report[Resource::TYPE_MEMBERSHIP] = 0; - $teams = $teamsClient->list()['teams']; + $teams = $this->teams->list()['teams']; foreach ($teams as $team) { - $report[Resource::TYPE_MEMBERSHIP] += $teamsClient->listMemberships($team['$id'], [Query::limit(1)])['total']; + $report[Resource::TYPE_MEMBERSHIP] += $this->teams->listMemberships( + $team['$id'], + [Query::limit(1)] + )['total']; } } // Databases - $currentPermission = 'databases.read'; - if (in_array(Resource::TYPE_DATABASE, $resources)) { - $report[Resource::TYPE_DATABASE] = $databaseClient->list()['total']; + $scope = 'databases.read'; + if (\in_array(Resource::TYPE_DATABASE, $resources)) { + $report[Resource::TYPE_DATABASE] = $this->database->list()['total']; } - $currentPermission = 'collections.read'; - if (in_array(Resource::TYPE_COLLECTION, $resources)) { + $scope = 'collections.read'; + if (\in_array(Resource::TYPE_COLLECTION, $resources)) { $report[Resource::TYPE_COLLECTION] = 0; - $databases = $databaseClient->list()['databases']; + $databases = $this->database->list()['databases']; foreach ($databases as $database) { - $report[Resource::TYPE_COLLECTION] += $databaseClient->listCollections($database['$id'], [Query::limit(1)])['total']; + $report[Resource::TYPE_COLLECTION] += $this->database->listCollections( + $database['$id'], + [Query::limit(1)] + )['total']; } } - $currentPermission = 'documents.read'; - if (in_array(Resource::TYPE_DOCUMENT, $resources)) { + $scope = 'documents.read'; + if (\in_array(Resource::TYPE_DOCUMENT, $resources)) { $report[Resource::TYPE_DOCUMENT] = 0; - $databases = $databaseClient->list()['databases']; + $databases = $this->database->list()['databases']; foreach ($databases as $database) { - $collections = $databaseClient->listCollections($database['$id'])['collections']; + $collections = $this->database->listCollections($database['$id'])['collections']; foreach ($collections as $collection) { - $report[Resource::TYPE_DOCUMENT] += $databaseClient->listDocuments($database['$id'], $collection['$id'], [Query::limit(1)])['total']; + $report[Resource::TYPE_DOCUMENT] += $this->database->listDocuments( + $database['$id'], + $collection['$id'], + [Query::limit(1)] + )['total']; } } } - $currentPermission = 'attributes.read'; - if (in_array(Resource::TYPE_ATTRIBUTE, $resources)) { + $scope = 'attributes.read'; + if (\in_array(Resource::TYPE_ATTRIBUTE, $resources)) { $report[Resource::TYPE_ATTRIBUTE] = 0; - $databases = $databaseClient->list()['databases']; + $databases = $this->database->list()['databases']; foreach ($databases as $database) { - $collections = $databaseClient->listCollections($database['$id'])['collections']; + $collections = $this->database->listCollections($database['$id'])['collections']; foreach ($collections as $collection) { - $report[Resource::TYPE_ATTRIBUTE] += $databaseClient->listAttributes($database['$id'], $collection['$id'])['total']; + $report[Resource::TYPE_ATTRIBUTE] += $this->database->listAttributes( + $database['$id'], + $collection['$id'] + )['total']; } } } - $currentPermission = 'indexes.read'; - if (in_array(Resource::TYPE_INDEX, $resources)) { + $scope = 'indexes.read'; + if (\in_array(Resource::TYPE_INDEX, $resources)) { $report[Resource::TYPE_INDEX] = 0; - $databases = $databaseClient->list()['databases']; + $databases = $this->database->list()['databases']; foreach ($databases as $database) { - $collections = $databaseClient->listCollections($database['$id'])['collections']; + $collections = $this->database->listCollections($database['$id'])['collections']; foreach ($collections as $collection) { - $report[Resource::TYPE_INDEX] += $databaseClient->listIndexes($database['$id'], $collection['$id'])['total']; + $report[Resource::TYPE_INDEX] += $this->database->listIndexes( + $database['$id'], + $collection['$id'] + )['total']; } } } // Storage - $currentPermission = 'buckets.read'; - if (in_array(Resource::TYPE_BUCKET, $resources)) { - $report[Resource::TYPE_BUCKET] = $storageClient->listBuckets()['total']; + $scope = 'buckets.read'; + if (\in_array(Resource::TYPE_BUCKET, $resources)) { + $report[Resource::TYPE_BUCKET] = $this->storage->listBuckets()['total']; } - $currentPermission = 'files.read'; - if (in_array(Resource::TYPE_FILE, $resources)) { + $scope = 'files.read'; + if (\in_array(Resource::TYPE_FILE, $resources)) { $report[Resource::TYPE_FILE] = 0; $report['size'] = 0; $buckets = []; $lastBucket = null; while (true) { - $currentBuckets = $storageClient->listBuckets($lastBucket ? [Query::cursorAfter($lastBucket)] : [Query::limit(20)])['buckets']; + $currentBuckets = $this->storage->listBuckets( + $lastBucket + ? [Query::cursorAfter($lastBucket)] + : [Query::limit(20)] + )['buckets']; + $buckets = array_merge($buckets, $currentBuckets); - $lastBucket = $buckets[count($buckets) - 1]['$id']; + $lastBucket = $buckets[count($buckets) - 1]['$id'] ?? null; if (count($currentBuckets) < 20) { break; @@ -210,7 +246,13 @@ public function report(array $resources = []): array $lastFile = null; while (true) { - $currentFiles = $storageClient->listFiles($bucket['$id'], $lastFile ? [Query::cursorAfter($lastFile)] : [Query::limit(20)])['files']; + $currentFiles = $this->storage->listFiles( + $bucket['$id'], + $lastFile + ? [Query::cursorAfter($lastFile)] + : [Query::limit(20)] + )['files']; + $files = array_merge($files, $currentFiles); $lastFile = $files[count($files) - 1]['$id']; @@ -221,44 +263,54 @@ public function report(array $resources = []): array $report[Resource::TYPE_FILE] += count($files); foreach ($files as $file) { - $report['size'] += $storageClient->getFile($bucket['$id'], $file['$id'])['sizeOriginal']; + $report['size'] += $this->storage->getFile( + $bucket['$id'], + $file['$id'] + )['sizeOriginal']; } } $report['size'] = $report['size'] / 1000 / 1000; // MB } // Functions - $currentPermission = 'functions.read'; - if (in_array(Resource::TYPE_FUNCTION, $resources)) { - $report[Resource::TYPE_FUNCTION] = $functionsClient->list()['total']; + $scope = 'functions.read'; + if (\in_array(Resource::TYPE_FUNCTION, $resources)) { + $report[Resource::TYPE_FUNCTION] = $this->functions->list()['total']; } - if (in_array(Resource::TYPE_DEPLOYMENT, $resources)) { + if (\in_array(Resource::TYPE_DEPLOYMENT, $resources)) { $report[Resource::TYPE_DEPLOYMENT] = 0; - $functions = $functionsClient->list()['functions']; + $functions = $this->functions->list()['functions']; foreach ($functions as $function) { - if (! empty($function['deployment'])) { + if (!empty($function['deployment'])) { $report[Resource::TYPE_DEPLOYMENT] += 1; } } } - if (in_array(Resource::TYPE_ENVIRONMENT_VARIABLE, $resources)) { + if (\in_array(Resource::TYPE_ENVIRONMENT_VARIABLE, $resources)) { $report[Resource::TYPE_ENVIRONMENT_VARIABLE] = 0; - $functions = $functionsClient->list()['functions']; + $functions = $this->functions->list()['functions']; foreach ($functions as $function) { - $report[Resource::TYPE_ENVIRONMENT_VARIABLE] += $functionsClient->listVariables($function['$id'])['total']; + $report[Resource::TYPE_ENVIRONMENT_VARIABLE] += $this->functions->listVariables($function['$id'])['total']; } } - $report['version'] = $this->call('GET', '/health/version', ['X-Appwrite-Key' => '', 'X-Appwrite-Project' => ''])['version']; + $report['version'] = $this->call( + 'GET', + '/health/version', + [ + 'X-Appwrite-Key' => '', + 'X-Appwrite-Project' => '', + ] + )['version']; $this->previousReport = $report; return $report; } catch (\Throwable $e) { if ($e->getCode() === 403) { - throw new \Exception("Missing Permission: {$currentPermission}."); + throw new \Exception("Missing scope: $scope."); } else { throw new \Exception($e->getMessage()); } @@ -268,71 +320,76 @@ public function report(array $resources = []): array /** * Export Auth Resources * - * @param int $batchSize Max 100 - * @param string[] $resources - * @return void + * @param int $batchSize Max 100 + * @param array $resources */ - protected function exportGroupAuth(int $batchSize, array $resources) + protected function exportGroupAuth(int $batchSize, array $resources): void { try { - if (in_array(Resource::TYPE_USER, $resources)) { + if (\in_array(Resource::TYPE_USER, $resources)) { $this->exportUsers($batchSize); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_USER, Transfer::GROUP_AUTH, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } try { - if (in_array(Resource::TYPE_TEAM, $resources)) { + if (\in_array(Resource::TYPE_TEAM, $resources)) { $this->exportTeams($batchSize); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_TEAM, Transfer::GROUP_AUTH, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } try { - if (in_array(Resource::TYPE_MEMBERSHIP, $resources)) { + if (\in_array(Resource::TYPE_MEMBERSHIP, $resources)) { $this->exportMemberships($batchSize); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_MEMBERSHIP, Transfer::GROUP_AUTH, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } } - private function exportUsers(int $batchSize) + /** + * @throws AppwriteException + */ + private function exportUsers(int $batchSize): void { - $usersClient = new Users($this->client); $lastDocument = null; - // Export Users while (true) { $users = []; $queries = [Query::limit($batchSize)]; + if ($this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_USER) { + $queries[] = Query::equal('$id', $this->rootResourceId); + $queries[] = Query::limit(1); + } + if ($lastDocument) { $queries[] = Query::cursorAfter($lastDocument); } - $response = $usersClient->list($queries); + $response = $this->users->list($queries); if ($response['total'] == 0) { break; } @@ -348,7 +405,7 @@ private function exportUsers(int $batchSize) '', $user['emailVerification'] ?? false, $user['phoneVerification'] ?? false, - ! $user['status'], + !$user['status'], $user['prefs'] ?? [], ); @@ -363,22 +420,29 @@ private function exportUsers(int $batchSize) } } - private function exportTeams(int $batchSize) + /** + * @throws AppwriteException + */ + private function exportTeams(int $batchSize): void { - $teamsClient = new Teams($this->client); + $this->teams = new Teams($this->client); $lastDocument = null; - // Export Teams while (true) { $teams = []; $queries = [Query::limit($batchSize)]; + if ($this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_TEAM) { + $queries[] = Query::equal('$id', $this->rootResourceId); + $queries[] = Query::limit(1); + } + if ($lastDocument) { $queries[] = Query::cursorAfter($lastDocument); } - $response = $teamsClient->list($queries); + $response = $this->teams->list($queries); if ($response['total'] == 0) { break; } @@ -401,16 +465,18 @@ private function exportTeams(int $batchSize) } } - private function exportMemberships(int $batchSize) + /** + * @throws AppwriteException + * @throws \Exception + */ + private function exportMemberships(int $batchSize): void { - $teamsClient = new Teams($this->client); - - // Export Memberships $cacheTeams = $this->cache->get(Team::getName()); - /** @var array - array where key is user ID */ + + /** @var array $cacheUsers */ $cacheUsers = []; + foreach ($this->cache->get(User::getName()) as $cacheUser) { - /** @var User $cacheUser */ $cacheUsers[$cacheUser->getId()] = $cacheUser; } @@ -427,7 +493,7 @@ private function exportMemberships(int $batchSize) $queries[] = Query::cursorAfter($lastDocument); } - $response = $teamsClient->listMemberships($team->getId(), $queries); + $response = $this->teams->listMemberships($team->getId(), $queries); if ($response['total'] == 0) { break; @@ -440,6 +506,7 @@ private function exportMemberships(int $batchSize) } $memberships[] = new Membership( + $membership['$id'], $team, $user, $membership['roles'], @@ -458,10 +525,10 @@ private function exportMemberships(int $batchSize) } } - protected function exportGroupDatabases(int $batchSize, array $resources) + protected function exportGroupDatabases(int $batchSize, array $resources): void { try { - if (in_array(Resource::TYPE_DATABASE, $resources)) { + if (\in_array(Resource::TYPE_DATABASE, $resources)) { $this->exportDatabases($batchSize); } } catch (\Throwable $e) { @@ -469,15 +536,15 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_DATABASE, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_COLLECTION, $resources)) { + if (\in_array(Resource::TYPE_COLLECTION, $resources)) { $this->exportCollections($batchSize); } } catch (\Throwable $e) { @@ -485,15 +552,15 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_COLLECTION, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_ATTRIBUTE, $resources)) { + if (\in_array(Resource::TYPE_ATTRIBUTE, $resources)) { $this->exportAttributes($batchSize); } } catch (\Throwable $e) { @@ -501,15 +568,15 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_ATTRIBUTE, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_INDEX, $resources)) { + if (\in_array(Resource::TYPE_INDEX, $resources)) { $this->exportIndexes($batchSize); } } catch (\Throwable $e) { @@ -517,15 +584,15 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_INDEX, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_DOCUMENT, $resources)) { + if (\in_array(Resource::TYPE_DOCUMENT, $resources)) { $this->exportDocuments($batchSize); } } catch (\Throwable $e) { @@ -533,38 +600,19 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_DOCUMENT, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } } - public function stripMetadata(array $document, bool $root = true) - { - if ($root) { - unset($document['$id']); - } - - unset($document['$permissions']); - unset($document['$collectionId']); - unset($document['$updatedAt']); - unset($document['$createdAt']); - unset($document['$databaseId']); - - foreach ($document as $key => $value) { - if (is_array($value)) { - $document[$key] = $this->stripMetadata($value, false); - } - } - - return $document; - } - - private function exportDocuments(int $batchSize) + /** + * @throws AppwriteException + */ + private function exportDocuments(int $batchSize): void { - $databaseClient = new Databases($this->client); $collections = $this->cache->get(Collection::getName()); foreach ($collections as $collection) { @@ -580,17 +628,67 @@ private function exportDocuments(int $batchSize) $queries[] = Query::cursorAfter($lastDocument); } - $response = $databaseClient->listDocuments( + $selects = ['*', '$id', '$permissions', '$updatedAt', '$createdAt']; // We want Relations flat! + $manyToMany = []; + + $attributes = $this->cache->get(Attribute::getName()); + foreach ($attributes as $attribute) { + /** @var Attribute|Relationship $attribute */ + if ( + $attribute->getCollection()->getId() === $collection->getId() && + $attribute->getType() === Attribute::TYPE_RELATIONSHIP && + $attribute->getSide() === 'parent' && + $attribute->getRelationType() == 'manyToMany' + ) { + /** + * Blockers: + * we should use but Does not work properly: + * $selects[] = $attribute->getKey() . '.$id'; + * when selecting for a relation we get all relations not just the one we were asking. + * when selecting for a relation like select(*, relation.$id) , all relations get resolve + */ + $manyToMany[] = $attribute->getKey(); + } + } + + $queries[] = Query::select($selects); + + $response = $this->database->listDocuments( $collection->getDatabase()->getId(), $collection->getId(), $queries ); foreach ($response['documents'] as $document) { + // HACK: Handle many to many + if(!empty($manyToMany)) { + $stack = ['$id']; // Adding $id because we can't select only relations + foreach ($manyToMany as $relation) { + $stack[] = $relation . '.$id'; + } + + $doc = $this->database->getDocument( + $collection->getDatabase()->getId(), + $collection->getId(), + $document['$id'], + [Query::select($stack)] + ); + + foreach ($manyToMany as $key) { + $document[$key] = []; + foreach ($doc[$key] as $relationDocument) { + $document[$key][] = $relationDocument['$id']; + } + } + } + $id = $document['$id']; $permissions = $document['$permissions']; - $document = $this->stripMetadata($document); + unset($document['$id']); + unset($document['$permissions']); + unset($document['$collectionId']); + unset($document['$databaseId']); // Certain Appwrite versions allowed for data to be required but null // This isn't allowed in modern versions so we need to remove it by comparing their attributes and replacing it with default value. @@ -601,8 +699,8 @@ private function exportDocuments(int $batchSize) continue; } - if ($attribute->getRequired() && ! isset($document[$attribute->getKey()])) { - switch ($attribute->getTypeName()) { + if ($attribute->isRequired() && !isset($document[$attribute->getKey()])) { + switch ($attribute->getType()) { case Attribute::TYPE_BOOLEAN: $document[$attribute->getKey()] = false; break; @@ -616,7 +714,7 @@ private function exportDocuments(int $batchSize) $document[$attribute->getKey()] = 0.0; break; case Attribute::TYPE_DATETIME: - $document[$attribute->getKey()] = 0; + $document[$attribute->getKey()] = '1970-01-01 00:00:00.000'; break; case Attribute::TYPE_URL: $document[$attribute->getKey()] = 'http://null'; @@ -625,15 +723,13 @@ private function exportDocuments(int $batchSize) } } - $cleanData = $this->stripMetadata($document); - $documents[] = new Document( $id, - $collection->getDatabase(), $collection, - $cleanData, + $document, $permissions ); + $lastDocument = $id; } @@ -646,148 +742,148 @@ private function exportDocuments(int $batchSize) } } + /** + * @throws \Exception + */ private function convertAttribute(array $value, Collection $collection): Attribute { switch ($value['type']) { case 'string': - if (! isset($value['format'])) { + if (!isset($value['format'])) { return new Text( $value['key'], $collection, - $value['required'], - $value['array'], - $value['default'], - $value['size'] ?? 0 + required: $value['required'], + default: $value['default'], + array: $value['array'], + size: $value['size'] ?? 0 ); } - switch ($value['format']) { - case 'email': - return new Email( - $value['key'], - $collection, - $value['required'], - $value['array'], - $value['default'] - ); - case 'enum': - return new Enum( - $value['key'], - $collection, - $value['elements'], - $value['required'], - $value['array'], - $value['default'] - ); - case 'url': - return new URL( - $value['key'], - $collection, - $value['required'], - $value['array'], - $value['default'] - ); - case 'ip': - return new IP( - $value['key'], - $collection, - $value['required'], - $value['array'], - $value['default'] - ); - case 'datetime': - return new DateTime( - $value['key'], - $collection, - $value['required'], - $value['array'], - $value['default'] - ); - default: - return new Text( - $value['key'], - $collection, - $value['required'], - $value['array'], - $value['default'], - $value['size'] ?? 0 - ); - } + return match ($value['format']) { + 'email' => new Email( + $value['key'], + $collection, + required: $value['required'], + default: $value['default'], + array: $value['array'], + size: $value['size'] ?? 254, + ), + 'enum' => new Enum( + $value['key'], + $collection, + elements: $value['elements'], + required: $value['required'], + default: $value['default'], + array: $value['array'], + size: $value['size'] ?? UtopiaDatabase::LENGTH_KEY, + ), + 'url' => new URL( + $value['key'], + $collection, + required: $value['required'], + default: $value['default'], + array: $value['array'], + size: $value['size'] ?? 2000, + ), + 'ip' => new IP( + $value['key'], + $collection, + required: $value['required'], + default: $value['default'], + array: $value['array'], + size: $value['size'] ?? 39, + ), + default => new Text( + $value['key'], + $collection, + required: $value['required'], + default: $value['default'], + array: $value['array'], + size: $value['size'] ?? 0, + ), + }; case 'boolean': return new Boolean( $value['key'], $collection, - $value['required'], - $value['array'], - $value['default'] + required: $value['required'], + default: $value['default'], + array: $value['array'] ); case 'integer': return new Integer( $value['key'], $collection, - $value['required'], - $value['array'], - $value['default'], - $value['min'] ?? 0, - $value['max'] ?? 0 + required: $value['required'], + default: $value['default'], + array: $value['array'], + min: $value['min'] ?? null, + max: $value['max'] ?? null, ); case 'double': return new Decimal( $value['key'], $collection, - $value['required'], - $value['array'], - $value['default'], - $value['min'] ?? 0, - $value['max'] ?? 0 + required: $value['required'], + default: $value['default'], + array: $value['array'], + min: $value['min'] ?? null, + max: $value['max'] ?? null, ); case 'relationship': return new Relationship( $value['key'], $collection, - $value['required'], - $value['array'], - $value['relatedCollection'], - $value['relationType'], - $value['twoWay'], - $value['twoWayKey'], - $value['onDelete'], - $value['side'] + relatedCollection: $value['relatedCollection'], + relationType: $value['relationType'], + twoWay: $value['twoWay'], + twoWayKey: $value['twoWayKey'], + onDelete: $value['onDelete'], + side: $value['side'], ); case 'datetime': return new DateTime( $value['key'], $collection, - $value['required'], - $value['array'], - $value['default'] + required: $value['required'], + default: $value['default'], + array: $value['array'], ); } - throw new \Exception('Unknown attribute type: '.$value['type']); + throw new \Exception('Unknown attribute type: ' . $value['type']); } - private function exportDatabases(int $batchSize) + /** + * @throws AppwriteException + */ + private function exportDatabases(int $batchSize): void { - $databaseClient = new Databases($this->client); + $this->database = new Databases($this->client); $lastDatabase = null; - // Transfer Databases while (true) { $queries = [Query::limit($batchSize)]; + + if ($this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_DATABASE) { + $queries[] = Query::equal('$id', $this->rootResourceId); + $queries[] = Query::limit(1); + } + $databases = []; if ($lastDatabase) { $queries[] = Query::cursorAfter($lastDatabase); } - $response = $databaseClient->list($queries); + $response = $this->database->list($queries); foreach ($response['databases'] as $database) { $newDatabase = new Database( + $database['$id'], $database['name'], - $database['$id'] ); $databases[] = $newDatabase; @@ -807,13 +903,13 @@ private function exportDatabases(int $batchSize) } } - private function exportCollections(int $batchSize) + /** + * @throws AppwriteException + */ + private function exportCollections(int $batchSize): void { - $databaseClient = new Databases($this->client); - - // Transfer Collections - $databases = $this->cache->get(Database::getName()); + foreach ($databases as $database) { $lastCollection = null; @@ -826,7 +922,7 @@ private function exportCollections(int $batchSize) $queries[] = Query::cursorAfter($lastCollection); } - $response = $databaseClient->listCollections( + $response = $this->database->listCollections( $database->getId(), $queries ); @@ -843,7 +939,9 @@ private function exportCollections(int $batchSize) $collections[] = $newCollection; } - $lastCollection = $collections[count($collections) - 1]->getId(); + $lastCollection = !empty($collection) + ? $collections[count($collections) - 1]->getId() + : null; $this->callback($collections); @@ -854,11 +952,12 @@ private function exportCollections(int $batchSize) } } - private function exportAttributes(int $batchSize) + /** + * @throws AppwriteException + * @throws \Exception + */ + private function exportAttributes(int $batchSize): void { - $databaseClient = new Databases($this->client); - - // Transfer Attributes $collections = $this->cache->get(Collection::getName()); /** @var Collection[] $collections */ foreach ($collections as $collection) { @@ -872,31 +971,17 @@ private function exportAttributes(int $batchSize) $queries[] = Query::cursorAfter($lastAttribute); } - $response = $databaseClient->listAttributes( + $response = $this->database->listAttributes( $collection->getDatabase()->getId(), $collection->getId(), $queries ); - // Remove two way relationship attributes - $this->cache->get(Resource::TYPE_ATTRIBUTE); - - $knownTwoways = []; - - foreach ($this->cache->get(Resource::TYPE_ATTRIBUTE) as $attribute) { - /** @var Attribute|Relationship $attribute */ - if ($attribute->getTypeName() == Attribute::TYPE_RELATIONSHIP && $attribute->getTwoWay()) { - $knownTwoways[] = $attribute->getTwoWayKey(); - } - } - foreach ($response['attributes'] as $attribute) { - if (in_array($attribute['key'], $knownTwoways)) { - continue; - } + /** @var array $attribute */ - if ($attribute['type'] === 'relationship') { - $knownTwoways[] = $attribute['twoWayKey']; + if ($attribute['type'] === 'relationship' && $attribute['side'] === 'child') { + continue; } $attributes[] = $this->convertAttribute($attribute, $collection); @@ -916,10 +1001,11 @@ private function exportAttributes(int $batchSize) } } - private function exportIndexes(int $batchSize) + /** + * @throws AppwriteException + */ + private function exportIndexes(int $batchSize): void { - $databaseClient = new Databases($this->client); - $collections = $this->cache->get(Resource::TYPE_COLLECTION); // Transfer Indexes @@ -935,7 +1021,7 @@ private function exportIndexes(int $batchSize) $queries[] = Query::cursorAfter($lastIndex); } - $response = $databaseClient->listIndexes( + $response = $this->database->listIndexes( $collection->getDatabase()->getId(), $collection->getId(), $queries @@ -948,6 +1034,7 @@ private function exportIndexes(int $batchSize) $collection, $index['type'], $index['attributes'], + [], $index['orders'] ); } @@ -966,26 +1053,26 @@ private function exportIndexes(int $batchSize) } } - protected function exportGroupStorage(int $batchSize, array $resources) + protected function exportGroupStorage(int $batchSize, array $resources): void { try { - if (in_array(Resource::TYPE_BUCKET, $resources)) { - $this->exportBuckets($batchSize, false); + if (\in_array(Resource::TYPE_BUCKET, $resources)) { + $this->exportBuckets($batchSize); } } catch (\Throwable $e) { $this->addError( new Exception( Resource::TYPE_BUCKET, Transfer::GROUP_STORAGE, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_FILE, $resources)) { + if (\in_array(Resource::TYPE_FILE, $resources)) { $this->exportFiles($batchSize); } } catch (\Throwable $e) { @@ -993,9 +1080,9 @@ protected function exportGroupStorage(int $batchSize, array $resources) new Exception( Resource::TYPE_FILE, Transfer::GROUP_STORAGE, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } @@ -1009,19 +1096,27 @@ protected function exportGroupStorage(int $batchSize, array $resources) new Exception( Resource::TYPE_BUCKET, Transfer::GROUP_STORAGE, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } } - private function exportBuckets(int $batchSize, bool $updateLimits) + /** + * @throws AppwriteException + */ + private function exportBuckets(int $batchSize): void { - $storageClient = new Storage($this->client); + $queries = []; - $buckets = $storageClient->listBuckets(); + if ($this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_BUCKET) { + $queries[] = Query::equal('$id', $this->rootResourceId); + $queries[] = Query::limit(1); + } + + $buckets = $this->storage->listBuckets($queries); $convertedBuckets = []; @@ -1037,10 +1132,7 @@ private function exportBuckets(int $batchSize, bool $updateLimits) $bucket['compression'], $bucket['encryption'], $bucket['antivirus'], - $updateLimits ); - - $bucket->setUpdateLimits($updateLimits); $convertedBuckets[] = $bucket; } @@ -1051,11 +1143,13 @@ private function exportBuckets(int $batchSize, bool $updateLimits) $this->callback($convertedBuckets); } - private function exportFiles(int $batchSize) + /** + * @throws AppwriteException + */ + private function exportFiles(int $batchSize): void { - $storageClient = new Storage($this->client); - $buckets = $this->cache->get(Bucket::getName()); + foreach ($buckets as $bucket) { /** @var Bucket $bucket */ $lastDocument = null; @@ -1067,7 +1161,7 @@ private function exportFiles(int $batchSize) $queries[] = Query::cursorAfter($lastDocument); } - $response = $storageClient->listFiles( + $response = $this->storage->listFiles( $bucket->getId(), $queries ); @@ -1087,9 +1181,9 @@ private function exportFiles(int $batchSize) $this->addError(new Exception( resourceName: Resource::TYPE_FILE, resourceGroup: Transfer::GROUP_STORAGE, + resourceId: $file['$id'], message: $e->getMessage(), - code: $e->getCode(), - resourceId: $file['$id'] + code: $e->getCode() )); } @@ -1103,7 +1197,10 @@ private function exportFiles(int $batchSize) } } - private function exportFileData(File $file) + /** + * @throws \Exception + */ + private function exportFileData(File $file): void { // Set the chunk size (5MB) $start = 0; @@ -1125,7 +1222,8 @@ private function exportFileData(File $file) ); // Send the chunk to the callback function - $file->setData($chunkData) + $file + ->setData($chunkData) ->setStart($start) ->setEnd($end); @@ -1141,42 +1239,52 @@ private function exportFileData(File $file) } } - protected function exportGroupFunctions(int $batchSize, array $resources) + protected function exportGroupFunctions(int $batchSize, array $resources): void { try { - if (in_array(Resource::TYPE_FUNCTION, $resources)) { + if (\in_array(Resource::TYPE_FUNCTION, $resources)) { $this->exportFunctions($batchSize); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_FUNCTION, Transfer::GROUP_FUNCTIONS, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } try { - if (in_array(Resource::TYPE_DEPLOYMENT, $resources)) { + if (\in_array(Resource::TYPE_DEPLOYMENT, $resources)) { $this->exportDeployments($batchSize, true); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_DEPLOYMENT, Transfer::GROUP_FUNCTIONS, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } } - private function exportFunctions(int $batchSize) + /** + * @throws AppwriteException + */ + private function exportFunctions(int $batchSize): void { - $functionsClient = new Functions($this->client); + $this->functions = new Functions($this->client); + + $queries = []; + + if ($this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_FUNCTION) { + $queries[] = Query::equal('$id', $this->rootResourceId); + $queries[] = Query::limit(1); + } - $functions = $functionsClient->list(); + $functions = $this->functions->list($queries); if ($functions['total'] === 0) { return; @@ -1186,21 +1294,23 @@ private function exportFunctions(int $batchSize) foreach ($functions['functions'] as $function) { $convertedFunc = new Func( - $function['name'], $function['$id'], + $function['name'], $function['runtime'], $function['execute'], $function['enabled'], $function['events'], $function['schedule'], $function['timeout'], - $function['deployment'] + $function['deployment'], + $function['entrypoint'] ); $convertedResources[] = $convertedFunc; foreach ($function['vars'] as $var) { $convertedResources[] = new EnvVar( + $var['$id'], $convertedFunc, $var['key'], $var['value'], @@ -1211,24 +1321,20 @@ private function exportFunctions(int $batchSize) $this->callback($convertedResources); } - private function exportDeployments(int $batchSize, bool $exportOnlyActive = false) + /** + * @throws AppwriteException + */ + private function exportDeployments(int $batchSize, bool $exportOnlyActive = false): void { - $functionsClient = new Functions($this->client); + $this->functions = new Functions($this->client); $functions = $this->cache->get(Func::getName()); - // exportDeploymentData doesn't exist on Appwrite versions prior to 1.4 - $appwriteVersion = $this->call('GET', '/health/version', ['X-Appwrite-Key' => '', 'X-Appwrite-Project' => ''])['version']; - - if (version_compare($appwriteVersion, '1.4.0', '<')) { - return; - } - foreach ($functions as $func) { /** @var Func $func */ $lastDocument = null; if ($exportOnlyActive && $func->getActiveDeployment()) { - $deployment = $functionsClient->getDeployment($func->getId(), $func->getActiveDeployment()); + $deployment = $this->functions->getDeployment($func->getId(), $func->getActiveDeployment()); try { $this->exportDeploymentData($func, $deployment); @@ -1246,7 +1352,7 @@ private function exportDeployments(int $batchSize, bool $exportOnlyActive = fals $queries[] = Query::cursorAfter($lastDocument); } - $response = $functionsClient->listDeployments( + $response = $this->functions->listDeployments( $func->getId(), $queries ); @@ -1268,7 +1374,10 @@ private function exportDeployments(int $batchSize, bool $exportOnlyActive = fals } } - private function exportDeploymentData(Func $func, array $deployment) + /** + * @throws \Exception + */ + private function exportDeploymentData(Func $func, array $deployment): void { // Set the chunk size (5MB) $start = 0; @@ -1286,7 +1395,7 @@ private function exportDeploymentData(Func $func, array $deployment) ); // Content-Length header was missing, file is less than max buffer size. - if (! array_key_exists('Content-Length', $responseHeaders)) { + if (!array_key_exists('Content-Length', $responseHeaders)) { $file = $this->call( 'GET', "/functions/{$func->getId()}/deployments/{$deployment['$id']}/download", @@ -1295,10 +1404,16 @@ private function exportDeploymentData(Func $func, array $deployment) $responseHeaders ); + $size = mb_strlen($file); + + if ($end > $size) { + $end = $size - 1; + } + $deployment = new Deployment( $deployment['$id'], $func, - strlen($file), + $size, $deployment['entrypoint'], $start, $end, @@ -1307,7 +1422,8 @@ private function exportDeploymentData(Func $func, array $deployment) ); $deployment->setInternalId($deployment->getId()); - return $this->callback([$deployment]); + $this->callback([$deployment]); + return; } $fileSize = $responseHeaders['Content-Length']; @@ -1334,9 +1450,11 @@ private function exportDeploymentData(Func $func, array $deployment) ); // Send the chunk to the callback function - $deployment->setData($chunkData); - $deployment->setStart($start); - $deployment->setEnd($end); + $deployment + ->setData($chunkData) + ->setStart($start) + ->setEnd($end); + $this->callback([$deployment]); // Update the range @@ -1348,4 +1466,9 @@ private function exportDeploymentData(Func $func, array $deployment) } } } + + public function getBatchSize(): int + { + return 250; + } } diff --git a/src/Migration/Sources/Firebase.php b/src/Migration/Sources/Firebase.php index 6f5ad79..015ae73 100644 --- a/src/Migration/Sources/Firebase.php +++ b/src/Migration/Sources/Firebase.php @@ -22,6 +22,9 @@ class Firebase extends Source { + /** + * @var array + */ private array $serviceAccount; private string $projectID; @@ -30,6 +33,9 @@ class Firebase extends Source private int $tokenExpires = 0; + /** + * @param array $serviceAccount + */ public function __construct(array $serviceAccount) { $this->serviceAccount = $serviceAccount; @@ -41,7 +47,7 @@ public static function getName(): string return 'Firebase'; } - private function base64UrlEncode($data) + private function base64UrlEncode(string $data): string { return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data)); } @@ -51,8 +57,8 @@ private function calculateJWT(): string $jwtClaim = [ 'iss' => $this->serviceAccount['client_email'], 'scope' => 'https://www.googleapis.com/auth/firebase https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore', - 'exp' => time() + 3600, - 'iat' => time(), + 'exp' => \time() + 3600, + 'iat' => \time(), 'aud' => 'https://oauth2.googleapis.com/token', ]; @@ -64,16 +70,23 @@ private function calculateJWT(): string $jwtPayload = $this->base64UrlEncode(json_encode($jwtHeader)).'.'.$this->base64UrlEncode(json_encode($jwtClaim)); $jwtSignature = ''; - openssl_sign($jwtPayload, $jwtSignature, $this->serviceAccount['private_key'], 'sha256'); + + \openssl_sign( + $jwtPayload, + $jwtSignature, + $this->serviceAccount['private_key'], + 'sha256' + ); + $jwtSignature = $this->base64UrlEncode($jwtSignature); - return $jwtPayload.'.'.$jwtSignature; + return $jwtPayload . '.' . $jwtSignature; } /** * Computes the JWT then fetches an auth token from the Google OAuth2 API which is valid for an hour */ - private function authenticate() + private function authenticate(): void { if (time() < $this->tokenExpires) { return; @@ -95,7 +108,7 @@ private function authenticate() } } - protected function call(string $method, string $path = '', array $headers = [], array $params = [], &$responseHeaders = []): array|string + protected function call(string $method, string $path = '', array $headers = [], array $params = [], array &$responseHeaders = []): array|string { $this->authenticate(); @@ -148,7 +161,7 @@ public function report(array $resources = []): array return []; } - protected function exportGroupAuth(int $batchSize, array $resources) + protected function exportGroupAuth(int $batchSize, array $resources): void { // Check if Auth is enabled try { @@ -165,7 +178,7 @@ protected function exportGroupAuth(int $batchSize, array $resources) } try { - if (in_array(Resource::TYPE_USER, $resources)) { + if (\in_array(Resource::TYPE_USER, $resources)) { $this->exportUsers($batchSize); } } catch (\Throwable $e) { @@ -173,15 +186,15 @@ protected function exportGroupAuth(int $batchSize, array $resources) new Exception( Resource::TYPE_USER, Transfer::GROUP_AUTH, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } } - private function exportUsers(int $batchSize) + private function exportUsers(int $batchSize): void { // Fetch our Hash Config $hashConfig = ($this->call('GET', 'https://identitytoolkit.googleapis.com/admin/v2/projects/'.$this->projectID.'/config'))['signIn']['hashConfig']; @@ -213,11 +226,17 @@ private function exportUsers(int $batchSize) $nextPageToken = $response['nextPageToken'] ?? null; foreach ($result as $user) { + $hash = null; + + if (array_key_exists('passwordHash', $user)) { + $hash = new Hash($user['passwordHash'], $user['salt'] ?? '', Hash::ALGORITHM_SCRYPT_MODIFIED, $hashConfig['saltSeparator'] ?? '', $hashConfig['signerKey'] ?? ''); + } + $transferUser = new User( $user['localId'] ?? '', $user['email'] ?? null, $user['displayName'] ?? $user['email'] ?? null, - null, + $hash, $user['phoneNumber'] ?? null, [], '', @@ -226,12 +245,6 @@ private function exportUsers(int $batchSize) $user['disabled'] ?? false ); - if (array_key_exists('passwordHash', $user)) { - $transferUser->setPasswordHash( - new Hash($user['passwordHash'], $user['salt'] ?? '', Hash::ALGORITHM_SCRYPT_MODIFIED, $hashConfig['saltSeparator'] ?? '', $hashConfig['signerKey'] ?? '') - ); - } - $users[] = $transferUser; } @@ -243,7 +256,7 @@ private function exportUsers(int $batchSize) } } - protected function exportGroupDatabases(int $batchSize, array $resources) + protected function exportGroupDatabases(int $batchSize, array $resources): void { // Check if Firestore is enabled try { @@ -260,7 +273,7 @@ protected function exportGroupDatabases(int $batchSize, array $resources) } try { - if (in_array(Resource::TYPE_DATABASE, $resources)) { + if (\in_array(Resource::TYPE_DATABASE, $resources)) { $database = new Database('default', 'default'); $database->setOriginalId('(default)'); $this->callback([$database]); @@ -270,15 +283,15 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_DATABASE, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_COLLECTION, $resources)) { + if (\in_array(Resource::TYPE_COLLECTION, $resources)) { $this->exportDB($batchSize, in_array(Resource::TYPE_DOCUMENT, $resources), $database); } } catch (\Throwable $e) { @@ -286,15 +299,15 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_COLLECTION, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } } - private function exportDB(int $batchSize, bool $pushDocuments, Database $database) + private function exportDB(int $batchSize, bool $pushDocuments, Database $database): void { $baseURL = "https://firestore.googleapis.com/v1/projects/{$this->projectID}/databases/(default)/documents"; @@ -352,28 +365,97 @@ private function exportDB(int $batchSize, bool $pushDocuments, Database $databas } } + /** + * @throws \Exception + */ private function convertAttribute(Collection $collection, string $key, array $field): Attribute { if (array_key_exists('booleanValue', $field)) { - return new Boolean($key, $collection, false, false, null); + return new Boolean( + $key, + $collection, + required:false, + default: null, + array: false, + ); } elseif (array_key_exists('bytesValue', $field)) { - return new Text($key, $collection, false, false, null, 1000000); + return new Text( + $key, + $collection, + required: false, + default: null, + array: false, + size: 1000000, + ); } elseif (array_key_exists('doubleValue', $field)) { - return new Decimal($key, $collection, false, false, null); + return new Decimal( + $key, + $collection, + required: false, + default: null, + array: false, + ); } elseif (array_key_exists('integerValue', $field)) { - return new Integer($key, $collection, false, false, null); + return new Integer( + $key, + $collection, + required: false, + default: null, + array: false, + ); } elseif (array_key_exists('mapValue', $field)) { - return new Text($key, $collection, false, false, null, 1000000); + return new Text( + $key, + $collection, + required: false, + default: null, + array: false, + size: 1000000, + ); } elseif (array_key_exists('nullValue', $field)) { - return new Text($key, $collection, false, false, null, 1000000); + return new Text( + $key, + $collection, + required: false, + default: null, + array: false, + size: 1000000, + ); } elseif (array_key_exists('referenceValue', $field)) { - return new Text($key, $collection, false, false, null, 1000000); //TODO: This should be a reference attribute + return new Text( + $key, + $collection, + required: false, + default: null, + array: false, + size: 1000000, + ); //TODO: This should be a reference attribute } elseif (array_key_exists('stringValue', $field)) { - return new Text($key, $collection, false, false, null, 1000000); + return new Text( + $key, + $collection, + required: false, + default: null, + array: false, + size: 1000000, + ); } elseif (array_key_exists('timestampValue', $field)) { - return new DateTime($key, $collection, false, false, null); + return new DateTime( + $key, + $collection, + required: false, + default: null, + array: false, + ); } elseif (array_key_exists('geoPointValue', $field)) { - return new Text($key, $collection, false, false, null, 1000000); + return new Text( + $key, + $collection, + required: false, + default: null, + array: false, + size: 1000000, + ); } elseif (array_key_exists('arrayValue', $field)) { return $this->calculateArrayType($collection, $key, $field['arrayValue']); } else { @@ -404,7 +486,7 @@ private function calculateArrayType(Collection $collection, string $key, array $ } } - private function exportCollection(Collection $collection, int $batchSize, bool $transferDocuments) + private function exportCollection(Collection $collection, int $batchSize, bool $transferDocuments): void { $resourceURL = 'https://firestore.googleapis.com/v1/projects/'.$this->projectID.'/databases/'.$collection->getDatabase()->getOriginalId().'/documents/'.$collection->getId(); @@ -516,10 +598,10 @@ private function convertDocument(Collection $collection, array $document): Docum $documentId = preg_replace("/[^A-Za-z0-9\_\-]/", '', $documentId); $documentId = strtolower($documentId); - return new Document($documentId, $collection->getDatabase(), $collection, $data, []); + return new Document($documentId, $collection, $data, []); } - protected function exportGroupStorage(int $batchSize, array $resources) + protected function exportGroupStorage(int $batchSize, array $resources): void { // Check if storage is enabled try { @@ -540,36 +622,36 @@ protected function exportGroupStorage(int $batchSize, array $resources) } try { - if (in_array(Resource::TYPE_BUCKET, $resources)) { + if (\in_array(Resource::TYPE_BUCKET, $resources)) { $this->exportBuckets($batchSize); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_BUCKET, Transfer::GROUP_STORAGE, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } try { - if (in_array(Resource::TYPE_FILE, $resources)) { + if (\in_array(Resource::TYPE_FILE, $resources)) { $this->exportFiles($batchSize); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_FILE, Transfer::GROUP_STORAGE, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } } - private function exportBuckets(int $batchsize) + private function exportBuckets(int $batchsize): void { $endpoint = 'https://storage.googleapis.com/storage/v1/b'; @@ -609,7 +691,7 @@ private function exportBuckets(int $batchsize) } } - private function sanitizeBucketId($id) + private function sanitizeBucketId($id): array|string|null { // Step 1: Check if the ID looks like a URL (contains ".") if (strpos($id, '.') !== false) { @@ -634,7 +716,7 @@ private function sanitizeBucketId($id) return $id; } - private function exportFiles(int $batchsize) + private function exportFiles(int $batchsize): void { $buckets = $this->cache->get(Bucket::getName()); @@ -673,7 +755,7 @@ private function exportFiles(int $batchsize) } } - private function exportFile(File $file) + private function exportFile(File $file): void { $endpoint = 'https://storage.googleapis.com/storage/v1/b/'.$file->getBucket()->getOriginalId().'/o/'.$file->getId().'?alt=media'; $start = 0; @@ -703,7 +785,7 @@ private function exportFile(File $file) } } - protected function exportGroupFunctions(int $batchSize, array $resources) + protected function exportGroupFunctions(int $batchSize, array $resources): void { throw new \Exception('Not implemented'); } diff --git a/src/Migration/Sources/NHost.php b/src/Migration/Sources/NHost.php index b8306df..b6e24ae 100644 --- a/src/Migration/Sources/NHost.php +++ b/src/Migration/Sources/NHost.php @@ -114,7 +114,7 @@ public function report(array $resources = []): array } // Auth - if (in_array(Resource::TYPE_USER, $resources)) { + if (\in_array(Resource::TYPE_USER, $resources)) { $statement = $db->prepare('SELECT COUNT(*) FROM auth.users'); $statement->execute(); @@ -126,11 +126,11 @@ public function report(array $resources = []): array } // Databases - if (in_array(Resource::TYPE_DATABASE, $resources)) { + if (\in_array(Resource::TYPE_DATABASE, $resources)) { $report[Resource::TYPE_DATABASE] = 1; } - if (in_array(Resource::TYPE_COLLECTION, $resources)) { + if (\in_array(Resource::TYPE_COLLECTION, $resources)) { $statement = $db->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = \'public\''); $statement->execute(); @@ -141,7 +141,7 @@ public function report(array $resources = []): array $report[Resource::TYPE_COLLECTION] = $statement->fetchColumn(); } - if (in_array(Resource::TYPE_ATTRIBUTE, $resources)) { + if (\in_array(Resource::TYPE_ATTRIBUTE, $resources)) { $statement = $db->prepare('SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = \'public\''); $statement->execute(); @@ -152,7 +152,7 @@ public function report(array $resources = []): array $report[Resource::TYPE_ATTRIBUTE] = $statement->fetchColumn(); } - if (in_array(Resource::TYPE_INDEX, $resources)) { + if (\in_array(Resource::TYPE_INDEX, $resources)) { $statement = $db->prepare('SELECT COUNT(*) FROM pg_indexes WHERE schemaname = \'public\''); $statement->execute(); @@ -163,7 +163,7 @@ public function report(array $resources = []): array $report[Resource::TYPE_INDEX] = $statement->fetchColumn(); } - if (in_array(Resource::TYPE_DOCUMENT, $resources)) { + if (\in_array(Resource::TYPE_DOCUMENT, $resources)) { $statement = $db->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = \'public\''); $statement->execute(); @@ -175,7 +175,7 @@ public function report(array $resources = []): array } // Storage - if (in_array(Resource::TYPE_BUCKET, $resources)) { + if (\in_array(Resource::TYPE_BUCKET, $resources)) { $statement = $db->prepare('SELECT COUNT(*) FROM storage.buckets'); $statement->execute(); @@ -186,7 +186,7 @@ public function report(array $resources = []): array $report[Resource::TYPE_BUCKET] = $statement->fetchColumn(); } - if (in_array(Resource::TYPE_FILE, $resources)) { + if (\in_array(Resource::TYPE_FILE, $resources)) { $statement = $db->prepare('SELECT COUNT(*) FROM storage.files'); $statement->execute(); @@ -211,24 +211,24 @@ public function report(array $resources = []): array return $report; } - protected function exportGroupAuth(int $batchSize, array $resources) + protected function exportGroupAuth(int $batchSize, array $resources): void { try { - if (in_array(Resource::TYPE_USER, $resources)) { + if (\in_array(Resource::TYPE_USER, $resources)) { $this->exportUsers($batchSize); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_USER, Transfer::GROUP_AUTH, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } } - private function exportUsers(int $batchSize) + private function exportUsers(int $batchSize): void { $db = $this->getDatabase(); @@ -249,11 +249,17 @@ private function exportUsers(int $batchSize) $transferUsers = []; foreach ($users as $user) { + $hash = null; + + if (array_key_exists('password_hash', $user)) { + $hash = new Hash($user['password_hash'], '', Hash::ALGORITHM_BCRYPT); + } + $transferUser = new User( $user['id'], $user['email'] ?? null, $user['display_name'] ?? null, - null, + $hash, $user['phone_number'] ?? null, [], '', @@ -263,10 +269,6 @@ private function exportUsers(int $batchSize) [] ); - if (array_key_exists('password_hash', $user)) { - $transferUser->setPasswordHash(new Hash($user['password_hash'], '', Hash::ALGORITHM_BCRYPT)); - } - $transferUsers[] = $transferUser; } @@ -274,10 +276,10 @@ private function exportUsers(int $batchSize) } } - protected function exportGroupDatabases(int $batchSize, array $resources) + protected function exportGroupDatabases(int $batchSize, array $resources): void { try { - if (in_array(Resource::TYPE_DATABASE, $resources)) { + if (\in_array(Resource::TYPE_DATABASE, $resources)) { $this->exportDatabases($batchSize); } } catch (\Throwable $e) { @@ -285,15 +287,15 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_DATABASE, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_COLLECTION, $resources)) { + if (\in_array(Resource::TYPE_COLLECTION, $resources)) { $this->exportCollections($batchSize); } } catch (\Throwable $e) { @@ -301,15 +303,15 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_COLLECTION, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_ATTRIBUTE, $resources)) { + if (\in_array(Resource::TYPE_ATTRIBUTE, $resources)) { $this->exportAttributes($batchSize); } } catch (\Throwable $e) { @@ -317,15 +319,15 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_ATTRIBUTE, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_DOCUMENT, $resources)) { + if (\in_array(Resource::TYPE_DOCUMENT, $resources)) { $this->exportDocuments($batchSize); } } catch (\Throwable $e) { @@ -333,15 +335,15 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_DOCUMENT, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_INDEX, $resources)) { + if (\in_array(Resource::TYPE_INDEX, $resources)) { $this->exportIndexes($batchSize); } } catch (\Throwable $e) { @@ -349,9 +351,9 @@ protected function exportGroupDatabases(int $batchSize, array $resources) new Exception( Resource::TYPE_INDEX, Transfer::GROUP_DATABASES, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } @@ -365,7 +367,7 @@ private function exportDatabases(int $batchSize): void $this->callback([$transferDatabase]); } - private function exportCollections(int $batchSize) + private function exportCollections(int $batchSize): void { $databases = $this->cache->get(Database::getName()); $db = $this->getDatabase(); @@ -397,7 +399,7 @@ private function exportCollections(int $batchSize) } } - private function exportAttributes(int $batchSize) + private function exportAttributes(int $batchSize): void { $collections = $this->cache->get(Collection::getName()); $db = $this->getDatabase(); @@ -419,7 +421,7 @@ private function exportAttributes(int $batchSize) } } - private function exportIndexes(int $batchSize) + private function exportIndexes(int $batchSize): void { $collections = $this->cache->get(Collection::getName()); $db = $this->getDatabase(); @@ -444,7 +446,7 @@ private function exportIndexes(int $batchSize) } } - private function exportDocuments(int $batchSize) + private function exportDocuments(int $batchSize): void { $databases = $this->cache->get(Database::getName()); $collections = $this->cache->get(Collection::getName()); @@ -458,12 +460,12 @@ private function exportDocuments(int $batchSize) foreach ($collections as $collection) { /** @var Collection $collection */ - $total = $db->query('SELECT COUNT(*) FROM '.$collection->getDatabase()->getDBName().'."'.$collection->getCollectionName().'"')->fetchColumn(); + $total = $db->query('SELECT COUNT(*) FROM '.$collection->getDatabase()->getDatabaseName().'."'.$collection->getCollectionName().'"')->fetchColumn(); $offset = 0; while ($offset < $total) { - $statement = $db->prepare('SELECT row_to_json(t) FROM (SELECT * FROM '.$collection->getDatabase()->getDBName().'."'.$collection->getCollectionName().'" LIMIT :limit OFFSET :offset) t;'); + $statement = $db->prepare('SELECT row_to_json(t) FROM (SELECT * FROM '.$collection->getDatabase()->getDatabaseName().'."'.$collection->getCollectionName().'" LIMIT :limit OFFSET :offset) t;'); $statement->bindValue(':limit', $batchSize, \PDO::PARAM_INT); $statement->bindValue(':offset', $offset, \PDO::PARAM_INT); $statement->execute(); @@ -485,14 +487,14 @@ private function exportDocuments(int $batchSize) $processedData = []; foreach ($collectionAttributes as $attribute) { /** @var Attribute $attribute */ - if (! $attribute->getArray() && \is_array($data[$attribute->getKey()])) { + if (! $attribute->isArray() && \is_array($data[$attribute->getKey()])) { $processedData[$attribute->getKey()] = json_encode($data[$attribute->getKey()]); } else { $processedData[$attribute->getKey()] = $data[$attribute->getKey()]; } } - $transferDocuments[] = new Document('unique()', $database, $collection, $processedData); + $transferDocuments[] = new Document('unique()', $collection, $processedData); } $this->callback($transferDocuments); @@ -509,7 +511,13 @@ private function convertAttribute(array $column, Collection $collection): Attrib // Numbers case 'boolean': case 'bool': - return new Boolean($column['column_name'], $collection, $column['is_nullable'] === 'NO', $isArray, $column['column_default']); + return new Boolean( + $column['column_name'], + $collection, + required: $column['is_nullable'] === 'NO', + default: $column['column_default'], + array: $isArray, + ); case 'smallint': case 'int2': if (! is_numeric($column['column_default']) && ! is_null($column['column_default'])) { @@ -525,7 +533,15 @@ private function convertAttribute(array $column, Collection $collection): Attrib $column['column_default'] = null; } - return new Integer($column['column_name'], $collection, $column['is_nullable'] === 'NO', $isArray, $column['column_default'], -32768, 32767); + return new Integer( + $column['column_name'], + $collection, + required: $column['is_nullable'] === 'NO', + default:$column['column_default'], + array: $isArray, + min: -32768, + max: 32767, + ); case 'integer': case 'int4': if (! is_numeric($column['column_default']) && ! is_null($column['column_default'])) { @@ -541,7 +557,15 @@ private function convertAttribute(array $column, Collection $collection): Attrib $column['column_default'] = null; } - return new Integer($column['column_name'], $collection, $column['is_nullable'] === 'NO', $isArray, $column['column_default'], -2147483648, 2147483647); + return new Integer( + $column['column_name'], + $collection, + required: $column['is_nullable'] === 'NO', + default: $column['column_default'], + array: $isArray, + min: -2147483648, + max: 2147483647, + ); case 'bigint': case 'int8': case 'numeric': @@ -557,7 +581,13 @@ private function convertAttribute(array $column, Collection $collection): Attrib $column['column_default'] = null; } - return new Integer($column['column_name'], $collection, $column['is_nullable'] === 'NO', $isArray, $column['column_default']); + return new Integer( + $column['column_name'], + $collection, + required: $column['is_nullable'] === 'NO', + default: $column['column_default'], + array: $isArray, + ); case 'decimal': case 'real': case 'double precision': @@ -577,7 +607,13 @@ private function convertAttribute(array $column, Collection $collection): Attrib $column['column_default'] = null; } - return new Decimal($column['column_name'], $collection, $column['is_nullable'] === 'NO', $isArray, $column['column_default']); + return new Decimal( + $column['column_name'], + $collection, + required: $column['is_nullable'] === 'NO', + default: $column['column_default'], + array: $isArray, + ); // Time (Conversion happens with documents) case 'timestamp with time zone': case 'date': @@ -588,36 +624,23 @@ private function convertAttribute(array $column, Collection $collection): Attrib case 'time': case 'timetz': case 'interval': - return new DateTime($column['column_name'], $collection, $column['is_nullable'] === 'NO', $isArray, null); - break; - // Strings and Objects - case 'uuid': - case 'character varying': - case 'text': - case 'character': - case 'json': - case 'jsonb': - case 'varchar': - case 'bytea': - return new Text( + return new DateTime( $column['column_name'], $collection, - $column['is_nullable'] === 'NO', - $isArray, - $column['column_default'], - $column['character_maximum_length'] ?? $column['character_octet_length'] ?? 10485760 + required: $column['is_nullable'] === 'NO', + default: null, + array: $isArray, ); - break; default: + // Strings and Objects return new Text( $column['column_name'], $collection, - $column['is_nullable'] === 'NO', - $isArray, - $column['column_default'], - $column['character_maximum_length'] ?? $column['character_octet_length'] ?? 10485760 + required: $column['is_nullable'] === 'NO', + default: $column['column_default'], + array: $isArray, + size: $column['character_maximum_length'] ?? $column['character_octet_length'] ?? 10485760, ); - break; } } @@ -665,10 +688,10 @@ private function convertIndex(array $index, Collection $collection): Index|false } } - protected function exportGroupStorage(int $batchSize, array $resources) + protected function exportGroupStorage(int $batchSize, array $resources): void { try { - if (in_array(Resource::TYPE_BUCKET, $resources)) { + if (\in_array(Resource::TYPE_BUCKET, $resources)) { $this->exportBuckets($batchSize); } } catch (\Throwable $e) { @@ -676,15 +699,15 @@ protected function exportGroupStorage(int $batchSize, array $resources) new Exception( Resource::TYPE_BUCKET, Transfer::GROUP_STORAGE, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } try { - if (in_array(Resource::TYPE_FILE, $resources)) { + if (\in_array(Resource::TYPE_FILE, $resources)) { $this->exportFiles($batchSize); } } catch (\Throwable $e) { @@ -692,15 +715,15 @@ protected function exportGroupStorage(int $batchSize, array $resources) new Exception( Resource::TYPE_FILE, Transfer::GROUP_STORAGE, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e ) ); } } - protected function exportBuckets(int $batchSize) + protected function exportBuckets(int $batchSize): void { $db = $this->getDatabase(); $total = $db->query('SELECT COUNT(*) FROM storage.buckets')->fetchColumn(); @@ -730,7 +753,7 @@ protected function exportBuckets(int $batchSize) } } - private function exportFiles(int $batchSize) + private function exportFiles(int $batchSize): void { $buckets = $this->cache->get(Bucket::getName()); $db = $this->getDatabase(); @@ -768,7 +791,7 @@ private function exportFiles(int $batchSize) } } - private function exportFile(File $file) + private function exportFile(File $file): void { $start = 0; $end = Transfer::STORAGE_MAX_CHUNK_SIZE - 1; @@ -816,7 +839,7 @@ private function exportFile(File $file) } } - protected function exportGroupFunctions(int $batchSize, array $resources) + protected function exportGroupFunctions(int $batchSize, array $resources): void { throw new \Exception('Not Implemented'); } diff --git a/src/Migration/Sources/Supabase.php b/src/Migration/Sources/Supabase.php index 102affd..e06af13 100644 --- a/src/Migration/Sources/Supabase.php +++ b/src/Migration/Sources/Supabase.php @@ -248,7 +248,7 @@ public function report(array $resources = []): array } // Auth - if (in_array(Resource::TYPE_USER, $resources)) { + if (\in_array(Resource::TYPE_USER, $resources)) { $statement = $this->pdo->prepare('SELECT COUNT(*) FROM auth.users'); $statement->execute(); @@ -260,11 +260,11 @@ public function report(array $resources = []): array } // Databases - if (in_array(Resource::TYPE_DATABASE, $resources)) { + if (\in_array(Resource::TYPE_DATABASE, $resources)) { $report[Resource::TYPE_DATABASE] = 1; } - if (in_array(Resource::TYPE_COLLECTION, $resources)) { + if (\in_array(Resource::TYPE_COLLECTION, $resources)) { $statement = $this->pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = \'public\''); $statement->execute(); @@ -275,7 +275,7 @@ public function report(array $resources = []): array $report[Resource::TYPE_COLLECTION] = $statement->fetchColumn(); } - if (in_array(Resource::TYPE_ATTRIBUTE, $resources)) { + if (\in_array(Resource::TYPE_ATTRIBUTE, $resources)) { $statement = $this->pdo->prepare('SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = \'public\''); $statement->execute(); @@ -286,7 +286,7 @@ public function report(array $resources = []): array $report[Resource::TYPE_ATTRIBUTE] = $statement->fetchColumn(); } - if (in_array(Resource::TYPE_INDEX, $resources)) { + if (\in_array(Resource::TYPE_INDEX, $resources)) { $statement = $this->pdo->prepare('SELECT COUNT(*) FROM pg_indexes WHERE schemaname = \'public\''); $statement->execute(); @@ -297,7 +297,7 @@ public function report(array $resources = []): array $report[Resource::TYPE_INDEX] = $statement->fetchColumn(); } - if (in_array(Resource::TYPE_DOCUMENT, $resources)) { + if (\in_array(Resource::TYPE_DOCUMENT, $resources)) { $statement = $this->pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = \'public\''); $statement->execute(); @@ -309,7 +309,7 @@ public function report(array $resources = []): array } // Storage - if (in_array(Resource::TYPE_BUCKET, $resources)) { + if (\in_array(Resource::TYPE_BUCKET, $resources)) { $statement = $this->pdo->prepare('SELECT COUNT(*) FROM storage.buckets'); $statement->execute(); @@ -320,7 +320,7 @@ public function report(array $resources = []): array $report[Resource::TYPE_BUCKET] = $statement->fetchColumn(); } - if (in_array(Resource::TYPE_FILE, $resources)) { + if (\in_array(Resource::TYPE_FILE, $resources)) { $statement = $this->pdo->prepare('SELECT COUNT(*) FROM storage.objects'); $statement->execute(); @@ -346,24 +346,24 @@ public function report(array $resources = []): array return $report; } - protected function exportGroupAuth(int $batchSize, array $resources) + protected function exportGroupAuth(int $batchSize, array $resources): void { try { - if (in_array(Resource::TYPE_USER, $resources)) { + if (\in_array(Resource::TYPE_USER, $resources)) { $this->exportUsers($batchSize); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_BUCKET, Transfer::GROUP_STORAGE, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } } - private function exportUsers(int $batchSize) + private function exportUsers(int $batchSize): void { $total = $this->pdo->query('SELECT COUNT(*) FROM auth.users')->fetchColumn(); @@ -382,11 +382,17 @@ private function exportUsers(int $batchSize) $transferUsers = []; foreach ($users as $user) { + $hash = null; + + if (array_key_exists('encrypted_password', $user)) { + $hash = new Hash($user['encrypted_password'], '', Hash::ALGORITHM_BCRYPT); + } + $transferUser = new User( $user['id'], $user['email'] ?? null, '', - null, + $hash, $user['phone'] ?? null, [], '', @@ -396,10 +402,6 @@ private function exportUsers(int $batchSize) [] ); - if (array_key_exists('encrypted_password', $user)) { - $transferUser->setPasswordHash(new Hash($user['encrypted_password'], '', Hash::ALGORITHM_BCRYPT)); - } - $transferUsers[] = $transferUser; } @@ -418,38 +420,38 @@ private function convertMimes(array $mimes): array return $extensions; } - protected function exportGroupStorage(int $batchSize, array $resources) + protected function exportGroupStorage(int $batchSize, array $resources): void { try { - if (in_array(Resource::TYPE_BUCKET, $resources)) { + if (\in_array(Resource::TYPE_BUCKET, $resources)) { $this->exportBuckets($batchSize); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_BUCKET, Transfer::GROUP_STORAGE, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } try { - if (in_array(Resource::TYPE_FILE, $resources)) { + if (\in_array(Resource::TYPE_FILE, $resources)) { $this->exportFiles($batchSize); } } catch (\Throwable $e) { $this->addError(new Exception( Resource::TYPE_BUCKET, Transfer::GROUP_STORAGE, - $e->getMessage(), - $e->getCode(), - $e + message: $e->getMessage(), + code: $e->getCode(), + previous: $e )); } } - protected function exportBuckets(int $batchSize) + protected function exportBuckets(int $batchSize): void { $statement = $this->pdo->prepare('SELECT * FROM storage.buckets order by created_at'); $statement->execute(); @@ -475,7 +477,7 @@ protected function exportBuckets(int $batchSize) $this->callback($transferBuckets); } - public function exportFiles(int $batchSize) + public function exportFiles(int $batchSize): void { /** * TODO: Supabase has folders, with enough folders within folders this could cause us to hit the max name length @@ -519,7 +521,7 @@ public function exportFiles(int $batchSize) } } - public function exportFile(File $file) + public function exportFile(File $file): void { $start = 0; $end = Transfer::STORAGE_MAX_CHUNK_SIZE - 1; diff --git a/src/Migration/Target.php b/src/Migration/Target.php index e07e5e3..664f5c1 100644 --- a/src/Migration/Target.php +++ b/src/Migration/Target.php @@ -9,27 +9,31 @@ abstract class Target * * @var array */ - protected $headers = [ + protected array $headers = [ 'Content-Type' => '', ]; - public $cache; + public Cache $cache; /** * Errors * * @var array */ - public $errors = []; + public array $errors = []; /** * Warnings * * @var array */ - public $warnings = []; + public array $warnings = []; - protected $endpoint = ''; + protected string $endpoint = ''; + + protected string $rootResourceId = ''; + + protected string $rootResourceType = ''; abstract public static function getName(): string; @@ -43,10 +47,11 @@ public function registerCache(Cache &$cache): void /** * Run Transfer * - * @param string[] $resources Resources to transfer + * @param array $resources Resources to transfer * @param callable $callback Callback to run after transfer + * @param string $rootResourceId Root resource ID, If enabled you can only transfer a single root resource */ - abstract public function run(array $resources, callable $callback): void; + abstract public function run(array $resources, callable $callback, string $rootResourceId = ''): void; /** * Report Resources @@ -57,38 +62,44 @@ abstract public function run(array $resources, callable $callback): void; * On Destinations, this function should just return nothing but still check if the API is available. * If any issues are found then an exception should be thrown with an error message. * - * @param string[] $resources Resources to report + * @param array $resources Resources to report + * @return array */ abstract public function report(array $resources = []): array; /** - * Call - * * Make an API call * + * @param array $headers + * @param array $params + * @param array $responseHeaders + * @return array|string + * * @throws \Exception */ - protected function call(string $method, string $path = '', array $headers = [], array $params = [], &$responseHeaders = []): array|string - { - $headers = array_merge($this->headers, $headers); - $ch = curl_init((str_contains($path, 'http') ? $path.(($method == 'GET' && ! empty($params)) ? '?'.http_build_query($params) : '') : $this->endpoint.$path.(($method == 'GET' && ! empty($params)) ? '?'.http_build_query($params) : ''))); - $responseStatus = -1; - $responseType = ''; - $responseBody = ''; - - switch ($headers['Content-Type']) { - case 'application/json': - $query = json_encode($params); - break; - - case 'multipart/form-data': - $query = $this->flatten($params); - break; - - default: - $query = http_build_query($params); - break; - } + protected function call( + string $method, + string $path = '', + array $headers = [], + array $params = [], + array &$responseHeaders = [] + ): array|string { + $headers = \array_merge($this->headers, $headers); + $ch = \curl_init(( + \str_contains($path, 'http') + ? $path.(($method == 'GET' && ! empty($params)) ? '?'.\http_build_query($params) : '') + : $this->endpoint.$path.( + ($method == 'GET' && ! empty($params)) + ? '?'.\http_build_query($params) + : '' + ) + )); + + $query = match ($headers['Content-Type']) { + 'application/json' => \json_encode($params), + 'multipart/form-data' => $this->flatten($params), + default => \http_build_query($params), + }; foreach ($headers as $i => $header) { $headers[] = $i.':'.$header; @@ -96,51 +107,51 @@ protected function call(string $method, string $path = '', array $headers = [], } if ($method === 'HEAD') { - curl_setopt($ch, CURLOPT_NOBODY, true); + \curl_setopt($ch, CURLOPT_NOBODY, true); } else { - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); } - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_USERAGENT, php_uname('s').'-'.php_uname('r').':php-'.phpversion()); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, CURLOPT_USERAGENT, php_uname('s').'-'.php_uname('r').':php-'.phpversion()); + \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + \curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + \curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { $len = strlen($header); $header = explode(':', strtolower($header), 2); - if (count($header) < 2) { // ignore invalid headers + if (\count($header) < 2) { // ignore invalid headers return $len; } - $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); + $responseHeaders[\strtolower(\trim($header[0]))] = \trim($header[1]); return $len; }); if ($method != 'GET') { - curl_setopt($ch, CURLOPT_POSTFIELDS, $query); + \curl_setopt($ch, CURLOPT_POSTFIELDS, $query); } $responseBody = curl_exec($ch); $responseType = $responseHeaders['Content-Type'] ?? $responseHeaders['content-type'] ?? ''; - $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $responseStatus = \curl_getinfo($ch, CURLINFO_HTTP_CODE); - switch (substr($responseType, 0, strpos($responseType, ';'))) { + switch (\substr($responseType, 0, \strpos($responseType, ';'))) { case 'application/json': - $responseBody = json_decode($responseBody, true); + $responseBody = \json_decode($responseBody, true); break; } - if (curl_errno($ch)) { - throw new \Exception(curl_error($ch)); + if (\curl_errno($ch)) { + throw new \Exception(\curl_error($ch)); } - curl_close($ch); + \curl_close($ch); if ($responseStatus >= 400) { - if (is_array($responseBody)) { - throw new \Exception(json_encode($responseBody)); + if (\is_array($responseBody)) { + throw new \Exception(\json_encode($responseBody)); } else { throw new \Exception($responseStatus.': '.$responseBody); } @@ -151,6 +162,9 @@ protected function call(string $method, string $path = '', array $headers = [], /** * Flatten params array to PHP multiple format + * + * @param array $data + * @return array */ protected function flatten(array $data, string $prefix = ''): array { @@ -159,7 +173,7 @@ protected function flatten(array $data, string $prefix = ''): array foreach ($data as $key => $value) { $finalKey = $prefix ? "{$prefix}[{$key}]" : $key; - if (is_array($value)) { + if (\is_array($value)) { $output += $this->flatten($value, $finalKey); } else { $output[$finalKey] = $value; @@ -204,4 +218,18 @@ public function addWarning(Warning $warning): void { $this->warnings[] = $warning; } + + /** + * Completion callback + */ + public function shutdown(): void + { + } + + /** + * Error callback + */ + public function error(): void + { + } } diff --git a/src/Migration/Transfer.php b/src/Migration/Transfer.php index 7bce6e8..44463f7 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -4,52 +4,71 @@ class Transfer { - public const GROUP_GENERAL = 'general'; + public const string GROUP_GENERAL = 'general'; - public const GROUP_AUTH = 'auth'; + public const string GROUP_AUTH = 'auth'; - public const GROUP_STORAGE = 'storage'; + public const string GROUP_STORAGE = 'storage'; - public const GROUP_FUNCTIONS = 'functions'; + public const string GROUP_FUNCTIONS = 'functions'; - public const GROUP_DATABASES = 'databases'; + public const string GROUP_DATABASES = 'databases'; - public const GROUP_SETTINGS = 'settings'; + public const string GROUP_SETTINGS = 'settings'; - public const GROUP_AUTH_RESOURCES = [Resource::TYPE_USER, Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, Resource::TYPE_HASH]; - - public const GROUP_STORAGE_RESOURCES = [Resource::TYPE_FILE, Resource::TYPE_BUCKET]; + public const array GROUP_AUTH_RESOURCES = [ + Resource::TYPE_USER, + Resource::TYPE_TEAM, + Resource::TYPE_MEMBERSHIP, + Resource::TYPE_HASH + ]; - public const GROUP_FUNCTIONS_RESOURCES = [Resource::TYPE_FUNCTION, Resource::TYPE_ENVIRONMENT_VARIABLE, Resource::TYPE_DEPLOYMENT]; + public const array GROUP_STORAGE_RESOURCES = [ + Resource::TYPE_FILE, + Resource::TYPE_BUCKET + ]; - public const GROUP_DATABASES_RESOURCES = [Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_INDEX, Resource::TYPE_ATTRIBUTE, Resource::TYPE_DOCUMENT]; + public const array GROUP_FUNCTIONS_RESOURCES = [ + Resource::TYPE_FUNCTION, + Resource::TYPE_ENVIRONMENT_VARIABLE, + Resource::TYPE_DEPLOYMENT + ]; - public const GROUP_SETTINGS_RESOURCES = []; + public const array GROUP_DATABASES_RESOURCES = [ + Resource::TYPE_DATABASE, + Resource::TYPE_COLLECTION, + Resource::TYPE_INDEX, + Resource::TYPE_ATTRIBUTE, + Resource::TYPE_DOCUMENT + ]; - public const ALL_PUBLIC_RESOURCES = [ - Resource::TYPE_USER, Resource::TYPE_TEAM, - Resource::TYPE_MEMBERSHIP, Resource::TYPE_FILE, - Resource::TYPE_BUCKET, Resource::TYPE_FUNCTION, - Resource::TYPE_ENVIRONMENT_VARIABLE, Resource::TYPE_DEPLOYMENT, - Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, - Resource::TYPE_INDEX, Resource::TYPE_ATTRIBUTE, + public const array GROUP_SETTINGS_RESOURCES = []; + + public const array ALL_PUBLIC_RESOURCES = [ + Resource::TYPE_USER, + Resource::TYPE_TEAM, + Resource::TYPE_MEMBERSHIP, + Resource::TYPE_FILE, + Resource::TYPE_BUCKET, + Resource::TYPE_FUNCTION, + Resource::TYPE_ENVIRONMENT_VARIABLE, + Resource::TYPE_DEPLOYMENT, + Resource::TYPE_DATABASE, + Resource::TYPE_COLLECTION, + Resource::TYPE_INDEX, + Resource::TYPE_ATTRIBUTE, Resource::TYPE_DOCUMENT, ]; - public const STORAGE_MAX_CHUNK_SIZE = 1024 * 1024 * 5; // 5MB - - public function __construct(Source $source, Destination $destination) - { - $this->source = $source; - $this->destination = $destination; - $this->cache = new Cache(); - - $this->source->registerCache($this->cache); - $this->destination->registerCache($this->cache); - $this->destination->setSource($source); + public const array ROOT_RESOURCES = [ + Resource::TYPE_BUCKET, + Resource::TYPE_DATABASE, + Resource::TYPE_FUNCTION, + Resource::TYPE_USER, + Resource::TYPE_TEAM, + ]; - return $this; - } + public const int STORAGE_MAX_CHUNK_SIZE = 1024 * 1024 * 5; // 5MB protected Source $source; @@ -62,9 +81,35 @@ public function __construct(Source $source, Destination $destination) */ protected Cache $cache; + /** + * @var array + */ + protected array $options = []; + + /** + * @var array + */ + protected array $events = []; + + /** + * @var array + */ protected array $resources = []; - public function getStatusCounters() + public function __construct(Source $source, Destination $destination) + { + $this->source = $source; + $this->destination = $destination; + $this->cache = new Cache(); + + $this->source->registerCache($this->cache); + $this->destination->registerCache($this->cache); + $this->destination->setSource($source); + + return $this; + } + + public function getStatusCounters(): array { $status = []; @@ -89,7 +134,6 @@ public function getStatusCounters() foreach ($this->cache->getAll() as $resources) { foreach ($resources as $resource) { - /** @var resource $resource */ if (isset($status[$resource->getName()])) { $status[$resource->getName()][$resource->getStatus()]++; if ($status[$resource->getName()]['pending'] > 0) { @@ -101,15 +145,13 @@ public function getStatusCounters() // Process Destination Errors foreach ($this->destination->getErrors() as $error) { - /** @var Exception $error */ if (isset($status[$error->getResourceGroup()])) { $status[$error->getResourceGroup()][Resource::STATUS_ERROR]++; } } - // Process Source Errprs + // Process source errors foreach ($this->source->getErrors() as $error) { - /** @var Exception $error */ if (isset($status[$error->getResourceGroup()])) { $status[$error->getResourceGroup()][Resource::STATUS_ERROR]++; } @@ -135,11 +177,23 @@ public function getStatusCounters() /** * Transfer Resources between adapters + * + * @param array $resources Resources to transfer + * @param callable $callback Callback to run after transfer + * @param string|null $rootResourceId Root resource ID, If enabled you can only transfer a single root resource + * @throws \Exception */ - public function run(array $resources, callable $callback): void - { + public function run( + array $resources, + callable $callback, + string $rootResourceId = null, + string $rootResourceType = null, + ): void { // Allows you to push entire groups if you want. $computedResources = []; + $rootResourceId = $rootResourceId ?? ''; + $rootResourceType = $rootResourceType ?? ''; + foreach ($resources as $resource) { if (is_array($resource)) { $computedResources = array_merge($computedResources, $resource); @@ -150,8 +204,34 @@ public function run(array $resources, callable $callback): void $computedResources = array_map('strtolower', $computedResources); + if ($rootResourceId !== '') { + if ($rootResourceType === '') { + throw new \Exception('Resource type must be set when resource ID is set.'); + } + + if(!in_array($rootResourceType, self::ROOT_RESOURCES)) { + throw new \Exception('Resource type must be one of ' . implode(', ', self::ROOT_RESOURCES)); + } + + $rootResources = \array_intersect($computedResources, self::ROOT_RESOURCES); + + if (\count($rootResources) > 1) { + throw new \Exception('Multiple root resources found. Only one root resource can be transferred at a time if using $rootResourceId.'); + } + + if (\count($rootResources) === 0) { + throw new \Exception('No root resources found.'); + } + } + $this->resources = $computedResources; - $this->destination->run($computedResources, $callback, $this->source); + + $this->destination->run( + $computedResources, + $callback, + $rootResourceId, + $rootResourceType, + ); } /** @@ -174,7 +254,8 @@ public function getCurrentResource(): string /** * Get Transfer Report * - * @param string $statusLevel If no status level is provided, all status types will be returned. + * @param string $statusLevel If no status level is provided, all status types will be returned. + * @return array> */ public function getReport(string $statusLevel = ''): array { @@ -199,4 +280,25 @@ public function getReport(string $statusLevel = ''): array return $report; } + + /** + * @throws \Exception + */ + public static function extractServices(array $services): array + { + $resources = []; + foreach ($services as $service) { + $resources = match ($service) { + self::GROUP_FUNCTIONS => array_merge($resources, self::GROUP_FUNCTIONS_RESOURCES), + self::GROUP_STORAGE => array_merge($resources, self::GROUP_STORAGE_RESOURCES), + self::GROUP_GENERAL => array_merge($resources, []), + self::GROUP_AUTH => array_merge($resources, self::GROUP_AUTH_RESOURCES), + self::GROUP_DATABASES => array_merge($resources, self::GROUP_DATABASES_RESOURCES), + self::GROUP_SETTINGS => array_merge($resources, self::GROUP_SETTINGS_RESOURCES), + default => throw new \Exception('No service group found'), + }; + } + + return $resources; + } } diff --git a/tests/Migration/E2E/Sources/Base.php b/tests/Migration/E2E/Sources/Base.php index c5a7f23..7b5bd40 100644 --- a/tests/Migration/E2E/Sources/Base.php +++ b/tests/Migration/E2E/Sources/Base.php @@ -7,7 +7,7 @@ use Utopia\Migration\Resource; use Utopia\Migration\Source; use Utopia\Migration\Transfer; -use Utopia\Tests\E2E\Adapters\Mock; +use Utopia\Tests\Unit\Adapters\MockDestination; abstract class Base extends TestCase { @@ -23,7 +23,7 @@ protected function setUp(): void throw new \Exception('Source not set'); } - $this->destination = new Mock(); + $this->destination = new MockDestination(); $this->transfer = new Transfer($this->source, $this->destination); } diff --git a/tests/Migration/E2E/Sources/NHostTest.php b/tests/Migration/E2E/Sources/NHostTest.php index cf75c90..b0d153b 100644 --- a/tests/Migration/E2E/Sources/NHostTest.php +++ b/tests/Migration/E2E/Sources/NHostTest.php @@ -2,12 +2,18 @@ namespace Utopia\Tests\E2E\Sources; +use PHPUnit\Framework\Attributes\Depends; use Utopia\Migration\Destination; use Utopia\Migration\Resource; +use Utopia\Migration\Resources\Auth\User; +use Utopia\Migration\Resources\Database\Collection; +use Utopia\Migration\Resources\Database\Database; +use Utopia\Migration\Resources\Storage\Bucket; +use Utopia\Migration\Resources\Storage\File; use Utopia\Migration\Source; use Utopia\Migration\Sources\NHost; use Utopia\Migration\Transfer; -use Utopia\Tests\E2E\Adapters\Mock; +use Utopia\Tests\Unit\Adapters\MockDestination; class NHostTest extends Base { @@ -17,6 +23,9 @@ class NHostTest extends Base protected ?Destination $destination = null; + /** + * @throws \Exception + */ protected function setUp(): void { // Check DB is online and ready @@ -28,7 +37,7 @@ protected function setUp(): void $pdo = new \PDO('pgsql:host=nhost-db'.';port=5432;dbname=postgres', 'postgres', 'postgres'); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - if ($pdo && $pdo->query('SELECT 1')->fetchColumn() === 1) { + if ($pdo->query('SELECT 1')->fetchColumn() === 1) { break; } else { var_dump('DB was offline, waiting 1s then retrying.'); @@ -51,7 +60,7 @@ protected function setUp(): void $this->call('GET', 'http://nhost-storage/', ['Content-Type' => 'text/plain']); break; - } catch (\Exception $e) { + } catch (\Exception) { } sleep(5); @@ -73,10 +82,13 @@ protected function setUp(): void $this->source->pdo = new \PDO('pgsql:host=nhost-db'.';port=5432;dbname=postgres', 'postgres', 'postgres'); $this->source->storageURL = 'http://nhost-storage'; - $this->destination = new Mock(); + $this->destination = new MockDestination(); $this->transfer = new Transfer($this->source, $this->destination); } + /** + * @throws \Exception + */ public function testSourceReport() { // Test report all @@ -90,8 +102,9 @@ public function testSourceReport() } /** - * @depends testSourceReport + * @throws \Exception */ + #[Depends('testSourceReport')] public function testRunTransfer($state) { $this->transfer->run( @@ -99,38 +112,58 @@ public function testRunTransfer($state) function () {} ); - $this->assertEquals(0, count($this->transfer->getReport('error'))); + $this->assertCount(0, $this->transfer->getReport('error')); return array_merge($state, [ 'transfer' => $this->transfer, 'source' => $this->source, + 'destination' => $this->destination, ]); } - /** - * @depends testRunTransfer - */ - public function testValidateTransfer($state) + #[Depends('testRunTransfer')] + public function testValidateSourceErrors($state) { - $statusCounters = $state['transfer']->getStatusCounters(); + /** @var Transfer $transfer */ + $transfer = $state['transfer']; + + /** @var Source $source */ + $source = $state['source']; + + $statusCounters = $transfer->getStatusCounters(); $this->assertNotEmpty($statusCounters); - foreach ($statusCounters as $resource => $counters) { - $this->assertNotEmpty($counters); + $errors = $source->getErrors(); - if ($counters[Resource::STATUS_ERROR] > 0) { - $this->fail('Resource '.$resource.' has '.$counters[Resource::STATUS_ERROR].' errors'); + if (!empty($errors)) { + $this->fail('[Source] Failed: ' . \json_encode($errors, JSON_PRETTY_PRINT)); + } - return; - } + return $state; + } + + #[Depends('testValidateSourceErrors')] + public function testValidateDestinationErrors($state) + { + /** @var Transfer $transfer */ + $transfer = $state['transfer']; + + /** @var Destination $destination */ + $destination = $state['destination']; + + $statusCounters = $transfer->getStatusCounters(); + $this->assertNotEmpty($statusCounters); + + $errors = $destination->getErrors(); + + if (!empty($errors)) { + $this->fail('[Destination] Failed: ' . \json_encode($errors, JSON_PRETTY_PRINT)); } return $state; } - /** - * @depends testValidateTransfer - */ + #[Depends('testValidateDestinationErrors')] public function testValidateUserTransfer($state): void { // Find known user @@ -138,7 +171,7 @@ public function testValidateUserTransfer($state): void $foundUser = null; foreach ($users as $user) { - /** @var \Utopia\Migration\Resources\Auth\User $user */ + /** @var User $user */ if ($user->getEmail() === 'test@test.com') { $foundUser = $user; } @@ -148,8 +181,6 @@ public function testValidateUserTransfer($state): void if (! $foundUser) { $this->fail('User "test@test.com" not found'); - - return; } $this->assertEquals('success', $foundUser->getStatus()); @@ -158,9 +189,7 @@ public function testValidateUserTransfer($state): void $this->assertEquals('test@test.com', $foundUser->getUsername()); } - /** - * @depends testValidateTransfer - */ + #[Depends('testValidateDestinationErrors')] public function testValidateDatabaseTransfer($state) { // Find known database @@ -168,8 +197,8 @@ public function testValidateDatabaseTransfer($state) $foundDatabase = null; foreach ($databases as $database) { - /** @var \Utopia\Migration\Resources\Database $database */ - if ($database->getDBName() === 'public') { + /** @var Database $database */ + if ($database->getDatabaseName() === 'public') { $foundDatabase = $database; } @@ -178,12 +207,10 @@ public function testValidateDatabaseTransfer($state) if (! $foundDatabase) { $this->fail('Database "public" not found'); - - return; } $this->assertEquals('success', $foundDatabase->getStatus()); - $this->assertEquals('public', $foundDatabase->getDBName()); + $this->assertEquals('public', $foundDatabase->getDatabaseName()); $this->assertEquals('public', $foundDatabase->getId()); // Find known collection @@ -191,7 +218,7 @@ public function testValidateDatabaseTransfer($state) $foundCollection = null; foreach ($collections as $collection) { - /** @var \Utopia\Migration\Resources\Database\Collection $collection */ + /** @var Collection $collection */ if ($collection->getCollectionName() === 'TestTable') { $foundCollection = $collection; @@ -201,8 +228,6 @@ public function testValidateDatabaseTransfer($state) if (! $foundCollection) { $this->fail('Collection "TestTable" not found'); - - return; } $this->assertEquals('success', $foundCollection->getStatus()); @@ -213,9 +238,7 @@ public function testValidateDatabaseTransfer($state) return $state; } - /** - * @depends testValidateDatabaseTransfer - */ + #[Depends('testValidateDatabaseTransfer')] public function testDatabaseFunctionalDefaultsWarn($state): void { // Find known collection @@ -223,7 +246,7 @@ public function testDatabaseFunctionalDefaultsWarn($state): void $foundCollection = null; foreach ($collections as $collection) { - /** @var \Utopia\Migration\Resources\Database\Collection $collection */ + /** @var Collection $collection */ if ($collection->getCollectionName() === 'FunctionalDefaultTestTable') { $foundCollection = $collection; } @@ -233,8 +256,6 @@ public function testDatabaseFunctionalDefaultsWarn($state): void if (! $foundCollection) { $this->fail('Collection "FunctionalDefaultTestTable" not found'); - - return; } $this->assertEquals('warning', $foundCollection->getStatus()); @@ -243,9 +264,7 @@ public function testDatabaseFunctionalDefaultsWarn($state): void $this->assertEquals('public', $foundCollection->getDatabase()->getId()); } - /** - * @depends testValidateTransfer - */ + #[Depends('testValidateDatabaseTransfer')] public function testValidateStorageTransfer($state): void { // Find known bucket @@ -253,7 +272,7 @@ public function testValidateStorageTransfer($state): void $foundBucket = null; foreach ($buckets as $bucket) { - /** @var \Utopia\Migration\Resources\Bucket $bucket */ + /** @var Bucket $bucket */ if ($bucket->getId() === 'default') { $foundBucket = $bucket; } @@ -263,8 +282,6 @@ public function testValidateStorageTransfer($state): void if (! $foundBucket) { $this->fail('Bucket "default" not found'); - - return; } $this->assertEquals('success', $foundBucket->getStatus()); @@ -275,7 +292,7 @@ public function testValidateStorageTransfer($state): void $foundFile = null; foreach ($files as $file) { - /** @var \Utopia\Migration\Resources\File $file */ + /** @var File $file */ if ($file->getFileName() === 'tulips.png') { $foundFile = $file; } @@ -285,10 +302,8 @@ public function testValidateStorageTransfer($state): void if (! $foundFile) { $this->fail('File "tulips.png" not found'); - - return; } - /** @var \Utopia\Migration\Resources\Storage\File $foundFile */ + /** @var File $foundFile */ $this->assertEquals('success', $foundFile->getStatus()); $this->assertEquals('tulips.png', $foundFile->getFileName()); $this->assertEquals('default', $foundFile->getBucket()->getId()); diff --git a/tests/Migration/E2E/Sources/SupabaseTest.php b/tests/Migration/E2E/Sources/SupabaseTest.php index ba09804..53096a5 100644 --- a/tests/Migration/E2E/Sources/SupabaseTest.php +++ b/tests/Migration/E2E/Sources/SupabaseTest.php @@ -2,12 +2,19 @@ namespace Utopia\Tests\E2E\Sources; +use PHPUnit\Framework\Attributes\Depends; use Utopia\Migration\Destination; use Utopia\Migration\Resource; +use Utopia\Migration\Resources\Auth\User; +use Utopia\Migration\Resources\Database\Collection; +use Utopia\Migration\Resources\Database\Database; +use Utopia\Migration\Resources\Database\Document; +use Utopia\Migration\Resources\Storage\Bucket; +use Utopia\Migration\Resources\Storage\File; use Utopia\Migration\Source; use Utopia\Migration\Sources\Supabase; use Utopia\Migration\Transfer; -use Utopia\Tests\E2E\Adapters\Mock; +use Utopia\Tests\Unit\Adapters\MockDestination; class SupabaseTest extends Base { @@ -17,6 +24,9 @@ class SupabaseTest extends Base protected ?Destination $destination = null; + /** + * @throws \Exception + */ protected function setUp(): void { // Check DB is online and ready @@ -28,12 +38,12 @@ protected function setUp(): void $pdo = new \PDO('pgsql:host=supabase-db'.';port=5432;dbname=postgres', 'postgres', 'postgres'); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - if ($pdo && $pdo->query('SELECT 1')->fetchColumn() === 1) { + if ($pdo->query('SELECT 1')->fetchColumn() === 1) { break; } else { var_dump('DB was offline, waiting 1s then retrying.'); } - } catch (\PDOException $e) { + } catch (\PDOException) { } sleep(1); @@ -53,10 +63,13 @@ protected function setUp(): void 'postgres' ); - $this->destination = new Mock(); + $this->destination = new MockDestination(); $this->transfer = new Transfer($this->source, $this->destination); } + /** + * @throws \Exception + */ public function testSourceReport() { // Test report all @@ -70,46 +83,68 @@ public function testSourceReport() } /** - * @depends testSourceReport + * @throws \Exception */ + #[Depends('testSourceReport')] public function testRunTransfer($state) { - $this->transfer->run($this->source->getSupportedResources(), + $this->transfer->run( + $this->source->getSupportedResources(), function () {} ); - $this->assertEquals(0, count($this->transfer->getReport('error'))); + $this->assertCount(0, $this->transfer->getReport('error')); return array_merge($state, [ 'transfer' => $this->transfer, 'source' => $this->source, + 'destination' => $this->destination, ]); } - /** - * @depends testRunTransfer - */ - public function testValidateTransfer($state) + #[Depends('testRunTransfer')] + public function testValidateSourceErrors($state) { - $statusCounters = $state['transfer']->getStatusCounters(); + /** @var Transfer $transfer */ + $transfer = $state['transfer']; + + /** @var Source $source */ + $source = $state['source']; + + $statusCounters = $transfer->getStatusCounters(); $this->assertNotEmpty($statusCounters); - foreach ($statusCounters as $resource => $counters) { - $this->assertNotEmpty($counters); + $errors = $source->getErrors(); - if ($counters[Resource::STATUS_ERROR] > 0) { - $this->fail('Resource '.$resource.' has '.$counters[Resource::STATUS_ERROR].' errors'); + if (!empty($errors)) { + $this->fail('[Source] Failed: ' . \json_encode($errors, JSON_PRETTY_PRINT)); + } - return; - } + return $state; + } + + #[Depends('testValidateSourceErrors')] + public function testValidateDestinationErrors($state) + { + /** @var Transfer $transfer */ + $transfer = $state['transfer']; + + /** @var Destination $destination */ + $destination = $state['destination']; + + $statusCounters = $transfer->getStatusCounters(); + $this->assertNotEmpty($statusCounters); + + $errors = $destination->getErrors(); + + if (!empty($errors)) { + $this->fail('[Destination] Failed: ' . \json_encode($errors, JSON_PRETTY_PRINT)); } return $state; } - /** - * @depends testValidateTransfer - */ + #[Depends('testValidateDestinationErrors')] public function testValidateUserTransfer($state): void { // Find known user @@ -118,7 +153,7 @@ public function testValidateUserTransfer($state): void $foundUser = null; foreach ($users as $user) { - /** @var \Utopia\Migration\Resources\Auth\User $user */ + /** @var User $user */ if ($user->getEmail() == 'albert.kihn95@yahoo.com') { $foundUser = $user; @@ -128,8 +163,6 @@ public function testValidateUserTransfer($state): void if (! $foundUser) { $this->fail('User "albert.kihn95@yahoo.com" not found'); - - return; } $this->assertEquals('success', $foundUser->getStatus()); @@ -137,9 +170,7 @@ public function testValidateUserTransfer($state): void $this->assertEquals('bcrypt', $foundUser->getPasswordHash()->getAlgorithm()); } - /** - * @depends testValidateTransfer - */ + #[Depends('testValidateDestinationErrors')] public function testValidateDatabaseTransfer($state) { // Find known database @@ -148,8 +179,8 @@ public function testValidateDatabaseTransfer($state) $foundDatabase = null; foreach ($databases as $database) { - /** @var \Utopia\Migration\Resources\Database $database */ - if ($database->getDBName() === 'public') { + /** @var Database $database */ + if ($database->getDatabaseName() === 'public') { $foundDatabase = $database; break; @@ -158,12 +189,10 @@ public function testValidateDatabaseTransfer($state) if (! $foundDatabase) { $this->fail('Database "public" not found'); - - return; } $this->assertEquals('success', $foundDatabase->getStatus()); - $this->assertEquals('public', $foundDatabase->getDBName()); + $this->assertEquals('public', $foundDatabase->getDatabaseName()); $this->assertEquals('public', $foundDatabase->getId()); // Find Known Collections @@ -173,8 +202,8 @@ public function testValidateDatabaseTransfer($state) $foundCollection = null; foreach ($collections as $collection) { - /** @var \Utopia\Migration\Resources\Database\Collection $collection */ - if ($collection->getDatabase()->getDBName() === 'public' && $collection->getCollectionName() === 'test') { + /** @var Collection $collection */ + if ($collection->getDatabase()->getDatabaseName() === 'public' && $collection->getCollectionName() === 'test') { $foundCollection = $collection; break; @@ -183,13 +212,11 @@ public function testValidateDatabaseTransfer($state) if (! $foundCollection) { $this->fail('Collection "test" not found'); - - return; } $this->assertEquals('success', $foundCollection->getStatus()); $this->assertEquals('test', $foundCollection->getCollectionName()); - $this->assertEquals('public', $foundCollection->getDatabase()->getDBName()); + $this->assertEquals('public', $foundCollection->getDatabase()->getDatabaseName()); $this->assertEquals('public', $foundCollection->getDatabase()->getId()); // Find Known Documents @@ -199,8 +226,8 @@ public function testValidateDatabaseTransfer($state) $foundDocument = null; foreach ($documents as $document) { - /** @var \Utopia\Migration\Resources\Database\Document $document */ - if ($document->getCollection()->getDatabase()->getDBName() === 'public' && $document->getCollection()->getCollectionName() === 'test') { + /** @var Document $document */ + if ($document->getCollection()->getDatabase()->getDatabaseName() === 'public' && $document->getCollection()->getCollectionName() === 'test') { $foundDocument = $document; } @@ -209,8 +236,6 @@ public function testValidateDatabaseTransfer($state) if (! $foundDocument) { $this->fail('Document "1" not found'); - - return; } $this->assertEquals('success', $foundDocument->getStatus()); @@ -218,9 +243,7 @@ public function testValidateDatabaseTransfer($state) return $state; } - /** - * @depends testValidateDatabaseTransfer - */ + #[Depends('testValidateDatabaseTransfer')] public function testDatabaseFunctionalDefaultsWarn($state): void { // Find known collection @@ -228,7 +251,7 @@ public function testDatabaseFunctionalDefaultsWarn($state): void $foundCollection = null; foreach ($collections as $collection) { - /** @var \Utopia\Migration\Resources\Database\Collection $collection */ + /** @var Collection $collection */ if ($collection->getCollectionName() === 'FunctionalDefaultTestTable') { $foundCollection = $collection; } @@ -238,8 +261,6 @@ public function testDatabaseFunctionalDefaultsWarn($state): void if (! $foundCollection) { $this->fail('Collection "FunctionalDefaultTestTable" not found'); - - return; } $this->assertEquals('warning', $foundCollection->getStatus()); @@ -248,9 +269,7 @@ public function testDatabaseFunctionalDefaultsWarn($state): void $this->assertEquals('public', $foundCollection->getDatabase()->getId()); } - /** - * @depends testValidateTransfer - */ + #[Depends('testValidateDestinationErrors')] public function testValidateStorageTransfer($state): void { // Find known bucket @@ -260,7 +279,7 @@ public function testValidateStorageTransfer($state): void $foundBucket = null; foreach ($buckets as $bucket) { - /** @var \Utopia\Migration\Resources\Storage\Bucket $bucket */ + /** @var Bucket $bucket */ if ($bucket->getBucketName() === 'Test Bucket 1') { $foundBucket = $bucket; } @@ -270,8 +289,6 @@ public function testValidateStorageTransfer($state): void if (! $foundBucket) { $this->fail('Bucket "Test Bucket 1" not found'); - - return; } $this->assertEquals('success', $foundBucket->getStatus()); @@ -283,7 +300,7 @@ public function testValidateStorageTransfer($state): void $foundFile = null; foreach ($files as $file) { - /** @var \Utopia\Migration\Resources\File $file */ + /** @var File $file */ if ($file->getFileName() === 'tulips.png') { $foundFile = $file; } @@ -293,10 +310,8 @@ public function testValidateStorageTransfer($state): void if (! $foundFile) { $this->fail('File "tulips.png" not found'); - - return; } - /** @var \Utopia\Migration\Resources\Storage\File $foundFile */ + /** @var File $foundFile */ $this->assertEquals('success', $foundFile->getStatus()); $this->assertEquals('tulips.png', $foundFile->getFileName()); $this->assertEquals('image/png', $foundFile->getMimeType()); diff --git a/tests/Migration/E2E/Adapters/Mock.php b/tests/Migration/Unit/Adapters/MockDestination.php similarity index 57% rename from tests/Migration/E2E/Adapters/Mock.php rename to tests/Migration/Unit/Adapters/MockDestination.php index e2c318a..b6970fa 100644 --- a/tests/Migration/E2E/Adapters/Mock.php +++ b/tests/Migration/Unit/Adapters/MockDestination.php @@ -1,17 +1,34 @@ data[$group] ?? []; + } + + public function getResourceTypeData(string $group, string $resourceType): array + { + return array_keys($this->data[$group][$resourceType]) ?? []; + } + + public function getResourceById(string $group, string $resourceType, string $resourceId): ?Resource + { + return $this->data[$group][$resourceType][$resourceId] ?? null; + } + public static function getName(): string { - return 'Mock'; + return 'MockDestination'; } public static function getSupportedResources(): array @@ -37,12 +54,12 @@ public static function getSupportedResources(): array public function import(array $resources, callable $callback): void { foreach ($resources as $resource) { - /** @var resource $resource */ + /** @var Resource $resource */ switch ($resource->getName()) { case 'Deployment': /** @var Deployment $resource */ if ($resource->getStart() === 0) { - $this->data[$resource->getGroup()][$resource->getName()][$resource->getInternalId()] = $resource->asArray(); + $this->data[$resource->getGroup()][$resource->getName()][$resource->getId()] = $resource; } // file_put_contents($this->path . 'deployments/' . $resource->getId() . '.tar.gz', $resource->getData(), FILE_APPEND); @@ -52,6 +69,15 @@ public function import(array $resources, callable $callback): void break; } + if (!key_exists($resource->getGroup(), $this->data)) { + $this->data[$resource->getGroup()] = []; + } + + if (!key_exists($resource->getName(), $this->data[$resource->getGroup()])) { + $this->data[$resource->getGroup()][$resource->getName()] = []; + } + + $this->data[$resource->getGroup()][$resource->getName()][$resource->getId()] = $resource; $resource->setStatus(Resource::STATUS_SUCCESS); $this->cache->update($resource); } @@ -59,7 +85,7 @@ public function import(array $resources, callable $callback): void $callback($resources); } - public function report(array $groups = []): array + public function report(array $resources = []): array { return []; } diff --git a/tests/Migration/Unit/Adapters/MockSource.php b/tests/Migration/Unit/Adapters/MockSource.php new file mode 100644 index 0000000..66f22ac --- /dev/null +++ b/tests/Migration/Unit/Adapters/MockSource.php @@ -0,0 +1,155 @@ +getGroup(), $this->mockResources)) { + $this->mockResources[$resource->getGroup()] = []; + } + + if (!key_exists($resource->getName(), $this->mockResources[$resource->getGroup()])) { + $this->mockResources[$resource->getGroup()][$resource->getName()] = []; + } + + $this->mockResources[$resource->getGroup()][$resource->getName()][$resource->getId()] = $resource; + } + + public function getMockResources(): array + { + return $this->mockResources; + } + + public function getMockResourcesByType(string $group, string $type): array + { + return array_values($this->mockResources[$group][$type]) ?? []; + } + + public function getMockResourceById(string $group, string $type, string $id): ?Resource + { + return $this->mockResources[$group][$type][$id] ?? null; + } + + public function clearMockResources(): void + { + $this->mockResources = []; + } + + private function handleResourceTransfer(string $group, string $type): void + { + if (in_array($type, Transfer::ROOT_RESOURCES) && !empty($this->rootResourceId)) { + $this->callback([$this->getMockResourceById($group, $type, $this->rootResourceId)]); + return; + } + + $resources = $this->getMockResourcesByType($group, $type) ?? []; + $this->callback($resources); + return; + } + + public static function getName(): string + { + return 'MockSource'; + } + + public static function getSupportedResources(): array + { + return [ + Resource::TYPE_ATTRIBUTE, + Resource::TYPE_BUCKET, + Resource::TYPE_COLLECTION, + Resource::TYPE_DATABASE, + Resource::TYPE_DOCUMENT, + Resource::TYPE_FILE, + Resource::TYPE_FUNCTION, + Resource::TYPE_DEPLOYMENT, + Resource::TYPE_HASH, + Resource::TYPE_INDEX, + Resource::TYPE_USER, + Resource::TYPE_ENVIRONMENT_VARIABLE, + Resource::TYPE_TEAM, + Resource::TYPE_MEMBERSHIP, + ]; + } + + public function report(array $resources = []): array + { + return []; + } + + /** + * Export Auth Group + * + * @param int $batchSize Max 100 + * @param string[] $resources Resources to export + */ + protected function exportGroupAuth(int $batchSize, array $resources): void + { + foreach (Transfer::GROUP_AUTH_RESOURCES as $resource) { + if (!\in_array($resource, $resources)) { + continue; + } + + $this->handleResourceTransfer(Transfer::GROUP_AUTH, $resource); + } + } + + /** + * Export Databases Group + * + * @param int $batchSize Max 100 + * @param string[] $resources Resources to export + */ + protected function exportGroupDatabases(int $batchSize, array $resources): void + { + foreach (Transfer::GROUP_DATABASES_RESOURCES as $resource) { + if (!\in_array($resource, $resources)) { + continue; + } + + $this->handleResourceTransfer(Transfer::GROUP_DATABASES, $resource); + } + } + + /** + * Export Storage Group + * + * @param int $batchSize Max 5 + * @param string[] $resources Resources to export + */ + protected function exportGroupStorage(int $batchSize, array $resources): void + { + foreach (Transfer::GROUP_STORAGE_RESOURCES as $resource) { + if (!\in_array($resource, $resources)) { + continue; + } + + $this->handleResourceTransfer(Transfer::GROUP_STORAGE, $resource); + } + } + + /** + * Export Functions Group + * + * @param int $batchSize Max 100 + * @param string[] $resources Resources to export + */ + protected function exportGroupFunctions(int $batchSize, array $resources): void + { + foreach (Transfer::GROUP_FUNCTIONS_RESOURCES as $resource) { + if (!\in_array($resource, $resources)) { + continue; + } + + $this->handleResourceTransfer(Transfer::GROUP_FUNCTIONS, $resource); + } + } +} diff --git a/tests/Migration/Unit/General/TransferTest.php b/tests/Migration/Unit/General/TransferTest.php new file mode 100644 index 0000000..0283437 --- /dev/null +++ b/tests/Migration/Unit/General/TransferTest.php @@ -0,0 +1,65 @@ +source = new MockSource(); + $this->destination = new MockDestination(); + + $this->transfer = new Transfer( + $this->source, + $this->destination + ); + } + + /** + * @throws \Exception + */ + public function testRootResourceId(): void + { + /** + * TEST FOR FAILURE + * Make sure we can't create a transfer with multiple root resources when supplying a rootResourceId + */ + try { + $this->transfer->run([Resource::TYPE_USER, Resource::TYPE_DATABASE], function () {}, 'rootResourceId'); + $this->fail('Multiple root resources should not be allowed'); + } catch (\Exception $e) { + $this->assertEquals('Resource type must be set when resource ID is set.', $e->getMessage()); + } + + $this->source->pushMockResource(new Database('test', 'test')); + $this->source->pushMockResource(new Database('test2', 'test')); + + /** + * TEST FOR SUCCESS + */ + $this->transfer->run( + [Resource::TYPE_DATABASE], + function () {}, + 'test', + Resource::TYPE_DATABASE + ); + $this->assertCount(1, $this->destination->getResourceTypeData(Transfer::GROUP_DATABASES, Resource::TYPE_DATABASE)); + + $database = $this->destination->getResourceById(Transfer::GROUP_DATABASES, Resource::TYPE_DATABASE, 'test'); + /** @var Database $database */ + $this->assertNotNull($database); + $this->assertEquals('test', $database->getDatabaseName()); + $this->assertEquals('test', $database->getId()); + } +}