From 7e795d8809a17bda0d34a7fce6afc2d990638ce4 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 29 Nov 2021 18:06:28 +0100 Subject: [PATCH] feat(schema): build schema json file (#10) --- .codeclimate.yml | 50 ++ .gitignore | 1 + composer.json | 3 +- composer.lock | 823 ++++++++++++++++++++- routes/routes.php | 2 + src/Auth/OAuth2/ForestProvider.php | 14 +- src/ForestServiceProvider.php | 10 +- src/Http/Controllers/ApiMapsController.php | 26 + src/Http/Controllers/AuthController.php | 1 - src/Listeners/ArtisanStart.php | 30 + src/Providers/EventProvider.php | 26 + src/Schema/Concerns/DataTypes.php | 55 ++ src/Schema/Concerns/Relationships.php | 68 ++ src/Schema/ForestModel.php | 463 ++++++++++++ src/Schema/Schema.php | 175 ++++- src/Utils/Database.php | 37 + tests/Feature/ApiMapsControllerTest.php | 33 + tests/Feature/ArtisanTest.php | 33 + tests/Feature/AuthControllerTest.php | 2 +- tests/Feature/ForestCorsTest.php | 2 +- tests/Feature/Models/Author.php | 16 + tests/Feature/Models/Book.php | 101 +++ tests/Feature/Models/Bookstore.php | 16 + tests/Feature/Models/Buy.php | 24 + tests/Feature/Models/Category.php | 24 + tests/Feature/Models/Comment.php | 32 + tests/Feature/Models/Company.php | 16 + tests/Feature/Models/Editor.php | 16 + tests/Feature/Models/Image.php | 24 + tests/Feature/Models/Product.php | 33 + tests/Feature/Models/Range.php | 16 + tests/Feature/Models/Tag.php | 24 + tests/Feature/Models/User.php | 51 ++ tests/Feature/SchemaTest.php | 115 +++ tests/TestCase.php | 227 +++++- tests/Unit/ForestModelTest.php | 570 ++++++++++++++ tests/Unit/SchemaTest.php | 125 ++++ tests/Unit/Traits/DataTypesTest.php | 32 + tests/Unit/Traits/RelationshipsTest.php | 32 + tests/Unit/Utils/DatabaseTest.php | 43 ++ 40 files changed, 3322 insertions(+), 69 deletions(-) create mode 100644 .codeclimate.yml create mode 100644 src/Http/Controllers/ApiMapsController.php create mode 100644 src/Listeners/ArtisanStart.php create mode 100644 src/Providers/EventProvider.php create mode 100644 src/Schema/Concerns/DataTypes.php create mode 100644 src/Schema/Concerns/Relationships.php create mode 100644 src/Schema/ForestModel.php create mode 100644 src/Utils/Database.php create mode 100644 tests/Feature/ApiMapsControllerTest.php create mode 100644 tests/Feature/ArtisanTest.php create mode 100644 tests/Feature/Models/Author.php create mode 100644 tests/Feature/Models/Book.php create mode 100644 tests/Feature/Models/Bookstore.php create mode 100644 tests/Feature/Models/Buy.php create mode 100644 tests/Feature/Models/Category.php create mode 100644 tests/Feature/Models/Comment.php create mode 100644 tests/Feature/Models/Company.php create mode 100644 tests/Feature/Models/Editor.php create mode 100644 tests/Feature/Models/Image.php create mode 100644 tests/Feature/Models/Product.php create mode 100644 tests/Feature/Models/Range.php create mode 100644 tests/Feature/Models/Tag.php create mode 100644 tests/Feature/Models/User.php create mode 100644 tests/Feature/SchemaTest.php create mode 100644 tests/Unit/ForestModelTest.php create mode 100644 tests/Unit/SchemaTest.php create mode 100644 tests/Unit/Traits/DataTypesTest.php create mode 100644 tests/Unit/Traits/RelationshipsTest.php create mode 100644 tests/Unit/Utils/DatabaseTest.php diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..afac33e1 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,50 @@ +version: "2" + +checks: + argument-count: + enabled: true + config: + threshold: 4 + complex-logic: + enabled: true + config: + threshold: 4 + file-lines: + enabled: true + config: + threshold: 250 + method-complexity: + enabled: true + config: + threshold: 5 + method-count: + enabled: true + config: + threshold: 20 + method-lines: + enabled: true + config: + threshold: 30 + nested-control-flow: + enabled: true + config: + threshold: 4 + return-statements: + enabled: true + config: + threshold: 4 + similar-code: + enabled: true + config: + threshold: 28 + identical-code: + enabled: true + config: + threshold: 28 + +exclude_patterns: + - "config/" + - "node_modules/" + - "script/" + - "src/Exceptions/" + - "src/Utils/ErrorMessages.php" diff --git a/.gitignore b/.gitignore index 9d3eed76..cb840c32 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ vendor # phpunit .phpunit.result.cache coverage +coverage.xml diff --git a/composer.json b/composer.json index 42bd711e..8d1846a7 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "guzzlehttp/guzzle": "^7.4", "league/oauth2-client": "^2.6", "firebase/php-jwt": "^5.4", - "ext-json": "*" + "ext-json": "*", + "composer/composer": "^2.1" }, "require-dev": { "phpunit/phpunit": "^9.5", diff --git a/composer.lock b/composer.lock index f43774a6..44fcf7a7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "10d71ae96a07f6eb6c96e2d24f2fdec3", + "content-hash": "18df44564356b18c564ecdb1e4e4dc05", "packages": [ { "name": "asm89/stack-cors", @@ -122,42 +122,508 @@ ], "time": "2021-08-15T20:50:18+00:00" }, + { + "name": "composer/ca-bundle", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", + "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.3.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-10-28T20:44:15+00:00" + }, + { + "name": "composer/composer", + "version": "2.1.12", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "6e3c2b122e0ec41a7e885fcaf19fa15e2e0819a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/6e3c2b122e0ec41a7e885fcaf19fa15e2e0819a0", + "reference": "6e3c2b122e0ec41a7e885fcaf19fa15e2e0819a0", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "composer/metadata-minifier": "^1.0", + "composer/semver": "^3.0", + "composer/spdx-licenses": "^1.2", + "composer/xdebug-handler": "^2.0", + "justinrainbow/json-schema": "^5.2.11", + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1.0 || ^2.0", + "react/promise": "^1.2 || ^2.7", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.0", + "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/filesystem": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" + }, + "require-dev": { + "phpspec/prophecy": "^1.10", + "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.1-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "https://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/composer/issues", + "source": "https://github.com/composer/composer/tree/2.1.12" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-11-09T15:02:04+00:00" + }, + { + "name": "composer/metadata-minifier", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/metadata-minifier.git", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2", + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\MetadataMinifier\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Small utility library that handles metadata minification and expansion.", + "keywords": [ + "composer", + "compression" + ], + "support": { + "issues": "https://github.com/composer/metadata-minifier/issues", + "source": "https://github.com/composer/metadata-minifier/tree/1.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-04-07T13:37:33+00:00" + }, { "name": "composer/package-versions-deprecated", "version": "1.11.99.4", "source": { "type": "git", - "url": "https://github.com/composer/package-versions-deprecated.git", - "reference": "b174585d1fe49ceed21928a945138948cb394600" + "url": "https://github.com/composer/package-versions-deprecated.git", + "reference": "b174585d1fe49ceed21928a945138948cb394600" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600", + "reference": "b174585d1fe49ceed21928a945138948cb394600", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1.0 || ^2.0", + "php": "^7 || ^8" + }, + "replace": { + "ocramius/package-versions": "1.11.99" + }, + "require-dev": { + "composer/composer": "^1.9.3 || ^2.0@dev", + "ext-zip": "^1.13", + "phpunit/phpunit": "^6.5 || ^7" + }, + "type": "composer-plugin", + "extra": { + "class": "PackageVersions\\Installer", + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/composer/package-versions-deprecated/issues", + "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-09-13T08:41:34+00:00" + }, + { + "name": "composer/semver", + "version": "3.2.6", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "83e511e247de329283478496f7a1e114c9517506" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/83e511e247de329283478496f7a1e114c9517506", + "reference": "83e511e247de329283478496f7a1e114c9517506", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.54", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.2.6" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-10-25T11:34:17+00:00" + }, + { + "name": "composer/spdx-licenses", + "version": "1.5.5", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "de30328a7af8680efdc03e396aad24befd513200" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/de30328a7af8680efdc03e396aad24befd513200", + "reference": "de30328a7af8680efdc03e396aad24befd513200", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Spdx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "SPDX licenses list and validation library.", + "keywords": [ + "license", + "spdx", + "validator" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.5.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-12-03T16:04:16+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600", - "reference": "b174585d1fe49ceed21928a945138948cb394600", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/84674dd3a7575ba617f5a76d7e9e29a7d3891339", + "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1.0 || ^2.0", - "php": "^7 || ^8" - }, - "replace": { - "ocramius/package-versions": "1.11.99" + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "composer/composer": "^1.9.3 || ^2.0@dev", - "ext-zip": "^1.13", - "phpunit/phpunit": "^6.5 || ^7" - }, - "type": "composer-plugin", - "extra": { - "class": "PackageVersions\\Installer", - "branch-alias": { - "dev-master": "1.x-dev" - } + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" }, + "type": "library", "autoload": { "psr-4": { - "PackageVersions\\": "src/PackageVersions" + "Composer\\XdebugHandler\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -166,18 +632,19 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be" + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], "support": { - "issues": "https://github.com/composer/package-versions-deprecated/issues", - "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4" + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/2.0.2" }, "funding": [ { @@ -193,7 +660,7 @@ "type": "tidelift" } ], - "time": "2021-09-13T08:41:34+00:00" + "time": "2021-07-31T17:03:58+00:00" }, { "name": "dflydev/dot-access-data", @@ -1356,6 +1823,76 @@ ], "time": "2021-10-06T17:43:30+00:00" }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.11", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ab6744b7296ded80f8cc4f9509abbff393399aa", + "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/5.2.11" + }, + "time": "2021-07-22T09:24:00+00:00" + }, { "name": "laravel/framework", "version": "v8.69.0", @@ -3105,6 +3642,167 @@ ], "time": "2021-09-25T23:10:38+00:00" }, + { + "name": "react/promise", + "version": "v2.8.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/f3cff96a19736714524ca0dd1d4130de73dbbbc4", + "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^6.5 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v2.8.0" + }, + "time": "2020-05-12T15:16:56+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.8.3", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9ad6ce79c342fbd44df10ea95511a1b24dee5b57", + "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.8.3" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2020-11-11T09:19:24+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "749042a2315705d2dfbbc59234dd9ceb22bf3ff0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/749042a2315705d2dfbbc59234dd9ceb22bf3ff0", + "reference": "749042a2315705d2dfbbc59234dd9ceb22bf3ff0", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.1.2" + }, + "time": "2021-08-19T21:01:38+00:00" + }, { "name": "swiftmailer/swiftmailer", "version": "v6.3.0", @@ -3644,6 +4342,69 @@ ], "time": "2021-03-23T23:28:01+00:00" }, + { + "name": "symfony/filesystem", + "version": "v5.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "343f4fe324383ca46792cae728a3b6e2f708fb32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/343f4fe324383ca46792cae728a3b6e2f708fb32", + "reference": "343f4fe324383ca46792cae728a3b6e2f708fb32", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.3.4" + }, + "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": "2021-07-21T12:40:44+00:00" + }, { "name": "symfony/finder", "version": "v5.3.7", diff --git a/routes/routes.php b/routes/routes.php index 562546f2..127f74b8 100644 --- a/routes/routes.php +++ b/routes/routes.php @@ -1,5 +1,6 @@ 'forest', ], function () { + Route::get('/', [ApiMapsController::class, 'index']); Route::post('authentication', [AuthController::class, 'login'])->name('forest.auth.login'); Route::get('authentication/callback', [AuthController::class, 'callback'])->name('forest.auth.callback'); //Route::get('custom-route', fn() => view('welcome')); diff --git a/src/Auth/OAuth2/ForestProvider.php b/src/Auth/OAuth2/ForestProvider.php index 5101ad6d..12ab6d0b 100644 --- a/src/Auth/OAuth2/ForestProvider.php +++ b/src/Auth/OAuth2/ForestProvider.php @@ -107,15 +107,11 @@ protected function getDefaultScopes() */ protected function checkResponse(ResponseInterface $response, $data) { - if (200 !== $response->getStatusCode()) { - if (404 === $response->getStatusCode()) { - throw new AuthorizationException(ErrorMessages::SECRET_NOT_FOUND); - } - - if (422 === $response->getStatusCode()) { - throw new AuthorizationException(ErrorMessages::SECRET_AND_RENDERINGID_INCONSISTENT); - } - + if (404 === $response->getStatusCode()) { + throw new AuthorizationException(ErrorMessages::SECRET_NOT_FOUND); + } elseif (422 === $response->getStatusCode()) { + throw new AuthorizationException(ErrorMessages::SECRET_AND_RENDERINGID_INCONSISTENT); + } elseif (200 !== $response->getStatusCode()) { $serverError = (array_key_exists('errors', $data) && count($data['errors']) > 0) ? $data['errors'][0] : null; if (null !== $serverError && array_key_exists('name', $serverError) && $serverError['name'] === ErrorMessages::TWO_FACTOR_AUTHENTICATION_REQUIRED) { throw new AuthorizationException(ErrorMessages::TWO_FACTOR_AUTHENTICATION_REQUIRED); diff --git a/src/ForestServiceProvider.php b/src/ForestServiceProvider.php index 12cd6cd3..49397e88 100644 --- a/src/ForestServiceProvider.php +++ b/src/ForestServiceProvider.php @@ -3,6 +3,7 @@ namespace ForestAdmin\LaravelForestAdmin; use ForestAdmin\LaravelForestAdmin\Http\Middleware\ForestCors; +use ForestAdmin\LaravelForestAdmin\Providers\EventProvider; use ForestAdmin\LaravelForestAdmin\Schema\Schema; use Illuminate\Console\Events\ArtisanStarting; use Illuminate\Contracts\Http\Kernel; @@ -31,6 +32,8 @@ class ForestServiceProvider extends ServiceProvider */ public function boot(Kernel $kernel): void { + $this->app->register(EventProvider::class); + $this->publishes( [ $this->configFile() => $this->app['path.config'] . DIRECTORY_SEPARATOR . 'forest.php', @@ -38,13 +41,6 @@ public function boot(Kernel $kernel): void 'config' ); - if (null !== Request::server('argv')) { - $currentCommand = implode(' ', Request::server('argv')); - if (Str::startsWith($currentCommand, $this->serveCommand)) { - $this->app['events']->listen(ArtisanStarting::class, [Schema::class, 'handle']); // @codeCoverageIgnore - } - } - $this->loadRoutesFrom(__DIR__ . '/../routes/routes.php'); $kernel->pushMiddleware(ForestCors::class); } diff --git a/src/Http/Controllers/ApiMapsController.php b/src/Http/Controllers/ApiMapsController.php new file mode 100644 index 00000000..6d04166b --- /dev/null +++ b/src/Http/Controllers/ApiMapsController.php @@ -0,0 +1,26 @@ +noContent(); + } +} diff --git a/src/Http/Controllers/AuthController.php b/src/Http/Controllers/AuthController.php index 682acec0..b262bf84 100644 --- a/src/Http/Controllers/AuthController.php +++ b/src/Http/Controllers/AuthController.php @@ -10,7 +10,6 @@ use GuzzleHttp\Exception\GuzzleException; use Illuminate\Http\JsonResponse; use Illuminate\Routing\Controller; -use Illuminate\Support\Facades\Htptp; use JsonException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; diff --git a/src/Listeners/ArtisanStart.php b/src/Listeners/ArtisanStart.php new file mode 100644 index 00000000..4eeeaba4 --- /dev/null +++ b/src/Listeners/ArtisanStart.php @@ -0,0 +1,30 @@ +command === 'serve') { + app()->make(Schema::class)->sendApiMap(); + } + } +} diff --git a/src/Providers/EventProvider.php b/src/Providers/EventProvider.php new file mode 100644 index 00000000..d9ac2ebb --- /dev/null +++ b/src/Providers/EventProvider.php @@ -0,0 +1,26 @@ + [ + ArtisanStart::class, + ], + ]; +} diff --git a/src/Schema/Concerns/DataTypes.php b/src/Schema/Concerns/DataTypes.php new file mode 100644 index 00000000..756f77b7 --- /dev/null +++ b/src/Schema/Concerns/DataTypes.php @@ -0,0 +1,55 @@ + 'unknown', + Types::ASCII_STRING => 'String', + Types::BIGINT => 'Number', + Types::BINARY => 'unknown', + Types::BLOB => 'unknown', + Types::BOOLEAN => 'Boolean', + Types::DATE_MUTABLE => 'Date', + Types::DATE_IMMUTABLE => 'Date', + Types::DATEINTERVAL => 'unknown', + Types::DATETIME_MUTABLE => 'Date', + Types::DATETIME_IMMUTABLE => 'Date', + Types::DATETIMETZ_MUTABLE => 'Date', + Types::DATETIMETZ_IMMUTABLE => 'Date', + Types::DECIMAL => 'Number', + Types::FLOAT => 'Number', + Types::GUID => 'Uuid', + Types::INTEGER => 'Number', + Types::JSON => 'Json', + Types::OBJECT => 'unknown', + Types::SIMPLE_ARRAY => 'unknown', + Types::SMALLINT => 'Number', + Types::STRING => 'String', + Types::TEXT => 'String', + Types::TIME_MUTABLE => 'Time', + Types::TIME_IMMUTABLE => 'Time', + ]; + + /** + * @param string $dbType + * @return string + */ + public function getType(string $dbType): string + { + return $this->dbTypes[$dbType]; + } +} diff --git a/src/Schema/Concerns/Relationships.php b/src/Schema/Concerns/Relationships.php new file mode 100644 index 00000000..467d0f57 --- /dev/null +++ b/src/Schema/Concerns/Relationships.php @@ -0,0 +1,68 @@ + 'BelongsTo', + BelongsToMany::class => 'BelongsToMany', + HasMany::class => 'HasMany', + HasManyThrough::class => 'HasMany', + HasOne::class => 'HasOne', + HasOneThrough::class => 'HasOne', + MorphMany::class => 'HasMany', + MorphOne::class => 'BelongsTo', + MorphToMany::class => 'HasMany', + ]; + + /** + * @param string $type + * @return string + */ + protected function mapRelationships(string $type): string + { + return $this->doctrineTypes[$type]; + } + + /** + * @param Model $model + * @return array + */ + public function getRelations(Model $model): array + { + return array_reduce( + (new \ReflectionClass($model))->getMethods(\ReflectionMethod::IS_PUBLIC), + function ($result, \ReflectionMethod $method) { + ($returnType = $method->getReturnType()) && + in_array($returnType->getName(), array_keys($this->doctrineTypes), true) && + ($result = array_merge($result, [$method->getName() => $returnType->getName()])); + + return $result; + }, + [] + ); + } +} diff --git a/src/Schema/ForestModel.php b/src/Schema/ForestModel.php new file mode 100644 index 00000000..3816614f --- /dev/null +++ b/src/Schema/ForestModel.php @@ -0,0 +1,463 @@ +model = $laravelModel; + $this->table = $laravelModel->getConnection()->getTablePrefix() . $laravelModel->getTable(); + if (strpos($this->table, '.')) { + [$this->database, $this->table] = explode('.', $this->table); + } + + $this->name = class_basename($this->model); + $this->oldName = class_basename($this->model); + } + + /** + * @return array + * @throws Exception + */ + public function serialize(): array + { + return [ + 'name' => $this->getName(), + 'old_name' => $this->getOldName(), + 'icon' => $this->getIcon(), + 'is_read_only' => $this->isReadOnly(), + 'is_virtual' => $this->isVirtual(), + 'only_for_relationships' => $this->isOnlyForRelationships(), + 'pagination_type' => $this->getPaginationType(), + 'fields' => $this->getFields(), + ]; + } + + /** + * @return array + * @throws Exception + */ + public function getFields(): array + { + $fields = $this->fetchFieldsFromTable(); + + foreach ($this->fields as $field) { + $values = $fields->firstWhere('field', $field['field']) ?: $this->fieldDefaultValues($field['field']); + if (array_key_exists('enums', $field)) { + $values['type'] = 'Enum'; + } + $fields->put($field['field'], array_merge($values, $field)); + } + + return $fields->values()->toArray(); + } + + /** + * @param array $fields + * @return ForestModel + */ + public function setFields(array $fields): ForestModel + { + $this->fields = $fields; + return $this; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + * @return ForestModel + */ + public function setName(string $name): ForestModel + { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getOldName(): string + { + return $this->oldName; + } + + /** + * @param string $oldName + * @return ForestModel + */ + public function setOldName(string $oldName): ForestModel + { + $this->oldName = $oldName; + return $this; + } + + /** + * @return string|null + */ + public function getIcon(): ?string + { + return $this->icon; + } + + /** + * @param string|null $icon + * @return ForestModel + */ + public function setIcon(?string $icon): ForestModel + { + $this->icon = $icon; + return $this; + } + + /** + * @return bool + */ + public function isReadOnly(): bool + { + return $this->isReadOnly; + } + + /** + * @param bool $isReadOnly + * @return ForestModel + */ + public function setIsReadOnly(bool $isReadOnly): ForestModel + { + $this->isReadOnly = $isReadOnly; + return $this; + } + + /** + * @return bool + */ + public function isSearchable(): bool + { + return $this->isSearchable; + } + + /** + * @param bool $isSearchable + * @return ForestModel + */ + public function setIsSearchable(bool $isSearchable): ForestModel + { + $this->isSearchable = $isSearchable; + return $this; + } + + /** + * @return bool + */ + public function isVirtual(): bool + { + return $this->isVirtual; + } + + /** + * @param bool $isVirtual + * @return ForestModel + */ + public function setIsVirtual(bool $isVirtual): ForestModel + { + $this->isVirtual = $isVirtual; + return $this; + } + + /** + * @return bool + */ + public function isOnlyForRelationships(): bool + { + return $this->onlyForRelationships; + } + + /** + * @param bool $onlyForRelationships + * @return ForestModel + */ + public function setOnlyForRelationships(bool $onlyForRelationships): ForestModel + { + $this->onlyForRelationships = $onlyForRelationships; + return $this; + } + + /** + * @return string + */ + public function getPaginationType(): string + { + return $this->paginationType; + } + + /** + * @param string $paginationType + * @return ForestModel + */ + public function setPaginationType(string $paginationType): ForestModel + { + $this->paginationType = $paginationType; + return $this; + } + + /** + * @return LaravelModel + */ + public function getModel(): LaravelModel + { + return $this->model; + } + + /** + * @return string|null + */ + public function getDatabase() + { + return $this->database; + } + + /** + * @param string|null $database + * @return ForestModel + */ + public function setDatabase($database) + { + $this->database = $database; + return $this; + } + + /** + * @return string + */ + public function getTable() + { + return $this->table; + } + + /** + * @param string $table + * @return ForestModel + */ + public function setTable($table) + { + $this->table = $table; + return $this; + } + + /** + * @return Collection + * @throws Exception + */ + protected function fetchFieldsFromTable(): Collection + { + $fields = new Collection(); + $connexion = $this->model->getConnection()->getDoctrineSchemaManager(); + $columns = $connexion->listTableColumns($this->table, $this->database); + + if ($columns) { + foreach ($columns as $column) { + $field = $this->fieldDefaultValues(); + $field['field'] = $column->getName(); + $field['type'] = $this->getType($column->getType()->getName()); + $field['is_required'] = $column->getNotnull(); + $field['default_value'] = $column->getDefault(); + $fields->put($column->getName(), $field); + } + } + + $fields = $fields->reject(fn ($item) => $item['type'] === 'unknown'); + + return $this->mergeFieldsWithRelations($fields, $this->getRelations($this->model)); + } + + /** + * @param Collection $fields + * @param array $relations + * @return Collection + */ + protected function mergeFieldsWithRelations(Collection $fields, array $relations): Collection + { + foreach ($relations as $name => $type) { + $relation = $this->model->$name(); + + switch ($type) { + case BelongsTo::class: + $field = $fields->firstWhere('field', $relation->getForeignKeyName()); + $field = array_merge( + $field, + [ + 'field' => $relation->getRelationName(), + 'reference' => $relation->getRelated()->getTable() . '.' . $relation->getOwnerKeyName(), + 'inverse_of' => $relation->getOwnerKeyName(), + ] + ); + $name = $relation->getForeignKeyName(); + break; + case BelongsToMany::class: + case MorphToMany::class: + $field = array_merge( + $this->fieldDefaultValues(), + [ + 'field' => $relation->getRelationName(), + 'inverse_of' => $relation->getRelatedPivotKeyName() + ] + ); + $name = $relation->getRelationName(); + break; + case HasMany::class: + case HasOne::class: + case MorphOne::class: + case MorphMany::class: + $field = array_merge( + $this->fieldDefaultValues(), + [ + 'field' => $name, + 'reference' => $relation->getRelated()->getTable() . '.' . $relation->getForeignKeyName(), + 'inverse_of' => $relation instanceof MorphOneOrMany ? null : $relation->getForeignKeyName(), + ] + ); + $name = $relation->getRelated()->getTable(); + break; + case HasOneThrough::class: + case HasManyThrough::class: + $field = array_merge( + $this->fieldDefaultValues(), + [ + 'field' => $name, + 'reference' => $relation->getRelated()->getTable() . '.' . $relation->getLocalKeyName(), + ] + ); + $name = $relation->getParent()->getTable(); + break; + } + + $field['type'] = $this->getType(Types::INTEGER); + $field['relationship'] = $this->mapRelationships($type); + $fields->put($name, $field); + } + + return $fields; + } + + /** + * @param string|null $name + * @return array + */ + protected function fieldDefaultValues(?string $name = null): array + { + return [ + 'field' => $name, + 'type' => 'string', + 'default_value' => null, + 'enums' => null, + 'integration' => null, + 'is_filterable' => true, + 'is_read_only' => false, + 'is_required' => false, + 'is_sortable' => true, + 'is_virtual' => false, + 'reference' => null, + 'inverse_of' => null, + 'widget' => null, + 'validations' => [], + ]; + } +} diff --git a/src/Schema/Schema.php b/src/Schema/Schema.php index bd49bef1..e3092a2c 100644 --- a/src/Schema/Schema.php +++ b/src/Schema/Schema.php @@ -2,6 +2,21 @@ namespace ForestAdmin\LaravelForestAdmin\Schema; +use Composer\Autoload\ClassMapGenerator; +use Doctrine\DBAL\Exception; +use ForestAdmin\LaravelForestAdmin\Services\ForestApiRequester; +use ForestAdmin\LaravelForestAdmin\Utils\Database; +use ForestAdmin\LaravelForestAdmin\Utils\Traits\FormatGuzzle; +use GuzzleHttp\Exception\GuzzleException; +use Illuminate\Contracts\Config\Repository as Config; +use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Http\Response; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\File; +use Symfony\Component\Console\Output\ConsoleOutput; + /** * Class Schema * @@ -11,13 +26,165 @@ */ class Schema { + use FormatGuzzle; + + public const LIANA_NAME = 'laravel-forestadmin'; + + public const LIANA_VERSION = '0.0.1'; + + /** + * @var string + */ + protected string $directory; + + /** + * @var Config + */ + protected Config $config; + + /** + * @var ForestApiRequester + */ + private ForestApiRequester $forestApi; + + /** + * @var ConsoleOutput + */ + private ConsoleOutput $console; + + /** + * @param Config $config + * @param ForestApiRequester $forestApi + * @param ConsoleOutput $console + */ + public function __construct(Config $config, ForestApiRequester $forestApi, ConsoleOutput $console) + { + $this->config = $config; + $this->directory = App::basePath($config->get('forest.models_directory')); + $this->forestApi = $forestApi; + $this->console = $console; + } + /** - * Dump DB Schema to json - * * @return void + * @throws Exception + * @throws GuzzleException + * @throws BindingResolutionException + */ + public function sendApiMap(): void + { + $response = $this->forestApi->post( + '/forest/apimaps', + [], + $this->serialize() + ); + + $this->console->write('🌳🌳🌳 '); + + if (in_array($response->getStatusCode(), [Response::HTTP_OK, Response::HTTP_ACCEPTED, Response::HTTP_NO_CONTENT], true)) { + $this->console->writeln('Apimap Received'); + } else { + $this->console->writeln('Cannot send the apimap to Forest. Are you online?'); + } + } + + /** + * @return array + * @throws BindingResolutionException + * @throws Exception + * @throws \JsonException + */ + private function generate(): array + { + $files = $this->fetchFiles(); + $schema = new Collection($this->metadata()); + $collections = []; + + foreach ($files as $file) { + if (class_exists($file)) { + $class = (new \ReflectionClass($file)); + if ($class->isSubclassOf(Model::class) && $class->isInstantiable()) { + $model = app()->make($file); + $forestModel = new ForestModel($model); + $collections[] = $forestModel->serialize(); + } + } + } + $schema->put('collections', $collections); + File::put($this->config->get('forest.json_file_path'), json_encode($schema, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); + + return $schema->toArray(); + } + + /** + * @return array + * @throws BindingResolutionException + * @throws Exception + * @throws \JsonException + */ + private function serialize(): array + { + $schema = $this->generate(); + $data = []; + $included = []; + + foreach ($schema['collections'] as $collection) { + $data[] = [ + 'id' => $collection['name'], + 'type' => 'collections', + 'attributes' => $collection, + 'relationships' => [ + 'actions' => [ + 'data' => [] + ], + 'segments' => [ + 'data' => [] + ] + ] + ]; + } + + return [ + 'data' => $data, + 'included' => $included, + 'meta' => $schema['meta'], + ]; + } + + /** + * Fetch all files in the model directory + * @return Collection + */ + private function fetchFiles(): Collection + { + $files = new Collection(); + + foreach (glob($this->directory, GLOB_ONLYDIR) as $dir) { + if (file_exists($dir)) { + $fileClass = ClassMapGenerator::createMap($dir); + foreach (array_keys($fileClass) as $file) { + $files->push($file); + } + } + } + + return $files; + } + + /** + * @return array */ - public function handle(): void + private function metadata(): array { - // + return [ + 'meta' => [ + 'liana' => self::LIANA_NAME, + 'liana_version' => self::LIANA_VERSION, + 'stack' => [ + 'database_type' => Database::getSource($this->config->get('database.default')), + 'orm_version' => app()->version(), + ], + ], + ]; } } diff --git a/src/Utils/Database.php b/src/Utils/Database.php new file mode 100644 index 00000000..3283c11e --- /dev/null +++ b/src/Utils/Database.php @@ -0,0 +1,37 @@ +apiMapsController = new ApiMapsController(); + $indexRoute = $this->apiMapsController->index(); + + $this->assertEmpty($indexRoute->getContent()); + $this->assertEquals(204, $indexRoute->getStatusCode()); + } +} diff --git a/tests/Feature/ArtisanTest.php b/tests/Feature/ArtisanTest.php new file mode 100644 index 00000000..d7c1ab9a --- /dev/null +++ b/tests/Feature/ArtisanTest.php @@ -0,0 +1,33 @@ + 'array', + 'active' => 'boolean', + ]; + + /** + * @return BelongsTo + */ + public function category(): BelongsTo + { + return $this->belongsTo(Category::class); + } + + /** + * @return BelongsToMany + */ + public function ranges(): BelongsToMany + { + return $this->belongsToMany(Range::class); + } + + /** + * @return HasMany + */ + public function comments(): HasMany + { + return $this->hasMany(Comment::class); + } + + /** + * @return HasManyThrough + */ + public function bookstores(): HasManyThrough + { + return $this->hasManyThrough(Company::class, Bookstore::class); + } + + /** + * @return HasOne + */ + public function editor(): HasOne + { + return $this->hasOne(Editor::class); + } + + /** + * @return HasOneThrough + */ + public function author(): HasOneThrough + { + return $this->hasOneThrough(User::class, Author::class); + } + + /** + * @return MorphOne + */ + public function image(): MorphOne + { + return $this->morphOne(Image::class, 'imageable'); + } + + /** + * @return MorphMany + */ + public function tags(): MorphMany + { + return $this->morphMany(Tag::class, 'taggable'); + } + + /** + * @return MorphToMany + */ + public function buys(): MorphToMany + { + return $this->morphToMany(Buy::class, 'buyable'); + } +} diff --git a/tests/Feature/Models/Bookstore.php b/tests/Feature/Models/Bookstore.php new file mode 100644 index 00000000..82616fd9 --- /dev/null +++ b/tests/Feature/Models/Bookstore.php @@ -0,0 +1,16 @@ +morphedByMany(Book::class, 'buyable'); + } +} diff --git a/tests/Feature/Models/Category.php b/tests/Feature/Models/Category.php new file mode 100644 index 00000000..fbd87c87 --- /dev/null +++ b/tests/Feature/Models/Category.php @@ -0,0 +1,24 @@ +belongsTo(Product::class); + } +} diff --git a/tests/Feature/Models/Comment.php b/tests/Feature/Models/Comment.php new file mode 100644 index 00000000..312800e8 --- /dev/null +++ b/tests/Feature/Models/Comment.php @@ -0,0 +1,32 @@ +belongsTo(Book::class); + } + + /** + * @return BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/tests/Feature/Models/Company.php b/tests/Feature/Models/Company.php new file mode 100644 index 00000000..1d98b88d --- /dev/null +++ b/tests/Feature/Models/Company.php @@ -0,0 +1,16 @@ +morphTo(); + } +} diff --git a/tests/Feature/Models/Product.php b/tests/Feature/Models/Product.php new file mode 100644 index 00000000..80e905f9 --- /dev/null +++ b/tests/Feature/Models/Product.php @@ -0,0 +1,33 @@ +belongsTo(User::class); + } + + /** + * @return HasMany + */ + public function categories(): HasMany + { + return $this->hasMany(Category::class); + } +} diff --git a/tests/Feature/Models/Range.php b/tests/Feature/Models/Range.php new file mode 100644 index 00000000..d86969a0 --- /dev/null +++ b/tests/Feature/Models/Range.php @@ -0,0 +1,16 @@ +morphTo(); + } +} diff --git a/tests/Feature/Models/User.php b/tests/Feature/Models/User.php new file mode 100644 index 00000000..8fdf221e --- /dev/null +++ b/tests/Feature/Models/User.php @@ -0,0 +1,51 @@ + 'datetime', + ]; + + /** + * @return HasMany + */ + public function products(): HasMany + { + return $this->hasMany(Product::class); + } +} diff --git a/tests/Feature/SchemaTest.php b/tests/Feature/SchemaTest.php new file mode 100644 index 00000000..c0c11c74 --- /dev/null +++ b/tests/Feature/SchemaTest.php @@ -0,0 +1,115 @@ +andReturn(__DIR__ . '/../Feature/Models'); + $schema = new Schema($this->getConfig(), $this->forestApiPost(204), $this->getConsole('Apimap Received')); + File::shouldReceive('put')->andReturn(true); + + $schema->sendApiMap(); + } + + /** + * @throws Exception + * @throws GuzzleException + * @throws BindingResolutionException + * @return void + */ + public function testHandleException(): void + { + App::shouldReceive('basePath')->andReturn(__DIR__ . '/../Feature/Models'); + $schema = new Schema($this->getConfig(), $this->forestApiPost(404), $this->getConsole('Cannot send the apimap to Forest. Are you online?')); + File::shouldReceive('put')->andReturn(true); + + $schema->sendApiMap(); + } + + /** + * @param int $status + * @return object + */ + public function forestApiPost(int $status) + { + $forestApiPost = $this->prophesize(ForestApiRequester::class); + $forestApiPost + ->post(Argument::type('string'), Argument::size(0), Argument::type('array')) + ->shouldBeCalled() + ->willReturn( + new Response($status, [], null) + ); + + return $forestApiPost->reveal(); + } + + /** + * @return object + */ + public function getConfig() + { + $config = $this->prophesize(Repository::class); + $config + ->get('database.default') + ->willReturn('sqlite'); + $config + ->get('forest.models_directory') + ->willReturn(__DIR__ . '/../Feature/Models'); + $config + ->get('forest.json_file_path') + ->willReturn('.forestadmin-schema.json'); + + + return $config->reveal(); + } + + /** + * @param string $notice + * @return object + */ + public function getConsole(string $notice) + { + $console = $this->prophesize(ConsoleOutput::class); + $console + ->write('🌳🌳🌳 ') + ->willReturn('🌳🌳🌳 '); + + $console + ->writeln($notice) + ->willReturn($notice); + + return $console->reveal(); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 195e02df..fb17fec4 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,6 +4,8 @@ use ForestAdmin\LaravelForestAdmin\ForestServiceProvider; use Illuminate\Foundation\Application; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Schema\Blueprint; use Orchestra\Testbench\TestCase as OrchestraTestCase; /** @@ -15,19 +17,6 @@ */ class TestCase extends OrchestraTestCase { - /** - * @param Application $app - * @return void - */ - protected function getEnvironmentSetUp($app): void - { - parent::getEnvironmentSetUp($app); - $config = $app['config']; - $config->set('app.debug', true); - $config->set('forest.api.secret', 'my-secret-key'); - $config->set('forest.api.auth-secret', 'auth-secret-key'); - } - /** * Call protected/private method of a class. * @param object $object @@ -49,18 +38,56 @@ public function invokeMethod(object &$object, string $methodName, array $paramet * Call protected/private property of a class. * @param object $object * @param string $methodName + * @param null $setData * @return mixed * @throws \ReflectionException */ - public function invokeProperty(object &$object, string $methodName) + public function invokeProperty(object &$object, string $methodName, $setData = null) { $reflection = new \ReflectionClass(get_class($object)); $property = $reflection->getProperty($methodName); $property->setAccessible(true); + if ($setData) { + $property->setValue($object, $setData); + } + return $property->getValue($object); } + /** + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + + $db = new DB(); + $db->addConnection( + [ + 'driver' => 'sqlite', + 'database' => ':memory:', + ] + ); + + $db->setAsGlobal(); + $db->bootEloquent(); + $this->migrate(); + } + + /** + * @param Application $app + * @return void + */ + protected function getEnvironmentSetUp($app): void + { + parent::getEnvironmentSetUp($app); + $config = $app['config']; + $config->set('app.debug', true); + $config->set('forest.api.secret', 'my-secret-key'); + $config->set('forest.api.auth-secret', 'auth-secret-key'); + } + /** * Get package providers. * @@ -74,4 +101,176 @@ protected function getPackageProviders($app) ForestServiceProvider::class, ]; } + + /** + * Make some dummy tables + * @return void + */ + protected function migrate(): void + { + DB::schema()->create( + 'users', + function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'products', + function (Blueprint $table) { + $table->id(); + $table->string('label'); + $table->decimal('price'); + $table->foreignId('user_id')->constrained(); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'categories', + function (Blueprint $table) { + $table->id(); + $table->string('label'); + $table->foreignId('product_id')->constrained(); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'books', + function (Blueprint $table) { + $table->id(); + $table->string('label'); + $table->text('comment'); + $table->enum('difficulty', ['easy', 'hard']); + $table->float('amount', 8, 2); + $table->boolean('active')->default(true); + $table->jsonb('options'); + $table->string('other')->default('N/A'); + $table->foreignId('category_id')->constrained(); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'ranges', + function (Blueprint $table) { + $table->id(); + $table->string('label'); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'book_range', + function (Blueprint $table) { + $table->id(); + $table->foreignId('book_id')->constrained(); + $table->foreignId('range_id')->constrained(); + } + ); + + DB::schema()->create( + 'comments', + function (Blueprint $table) { + $table->id(); + $table->string('body'); + $table->foreignId('book_id')->constrained(); + $table->foreignId('user_id')->constrained(); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'companies', + function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->foreignId('book_id')->constrained(); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'bookstores', + function (Blueprint $table) { + $table->id(); + $table->string('label'); + $table->foreignId('company_id')->constrained(); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'authors', + function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained(); + $table->timestamps(); + } + ); + + DB::schema()->table( + 'users', + function (Blueprint $table) { + $table->foreignId('book_id')->nullable()->constrained(); + } + ); + + DB::schema()->create( + 'editors', + function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->foreignId('book_id')->constrained(); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'tags', + function (Blueprint $table) { + $table->id(); + $table->string('label'); + $table->morphs('taggable'); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'images', + function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('url'); + $table->morphs('imageable'); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'buys', + function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->timestamps(); + } + ); + + DB::schema()->create( + 'buyables', + function (Blueprint $table) { + $table->id(); + $table->foreignId('buy_id')->constrained(); + $table->integer('buyable_id'); + $table->string('buyable_type'); + } + ); + } } diff --git a/tests/Unit/ForestModelTest.php b/tests/Unit/ForestModelTest.php new file mode 100644 index 00000000..2f598b01 --- /dev/null +++ b/tests/Unit/ForestModelTest.php @@ -0,0 +1,570 @@ +getLaravelModel()]) + ->makePartial(); + + $forestModel->shouldReceive('getFields') + ->andReturn( + [ + [ + 'field' => 'label', + 'is_required' => true, + 'type' => 'String', + 'default_value' => null, + 'enums' => null, + 'integration' => null, + 'is_filterable' => true, + 'is_read_only' => false, + 'is_sortable' => true, + 'is_virtual' => false, + 'reference' => null, + 'inverse_of' => null, + 'widget' => null, + 'validations' => [], + + ] + ] + ); + + $serialize = $forestModel->serialize(); + + $this->assertArrayHasKey('name', $serialize); + $this->assertArrayHasKey('old_name', $serialize); + $this->assertArrayHasKey('icon', $serialize); + $this->assertArrayHasKey('is_read_only', $serialize); + $this->assertArrayHasKey('is_virtual', $serialize); + $this->assertArrayHasKey('only_for_relationships', $serialize); + $this->assertArrayHasKey('pagination_type', $serialize); + $this->assertArrayHasKey('fields', $serialize); + $this->assertEquals('label', $serialize['fields'][0]['field']); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testGetFields(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial() + ->shouldAllowMockingProtectedMethods(); + $forestModel->shouldReceive('fetchFieldsFromTable') + ->andReturn( + collect( + [ + 'foo' => [ + 'field' => 'foo', + 'enums' => null, + 'type' => 'Enum', + 'default_value' => null, + 'integration' => null, + 'is_filterable' => true, + 'is_read_only' => false, + 'is_required' => true, + 'is_sortable' => true, + 'is_virtual' => false, + 'reference' => null, + 'inverse_of' => null, + 'widget' => null, + 'validations' => [], + ], + 'bar' => [ + 'field' => 'bar', + 'type' => 'Number', + 'default_value' => null, + 'enums' => null, + 'integration' => null, + 'is_filterable' => true, + 'is_read_only' => false, + 'is_required' => true, + 'is_sortable' => true, + 'is_virtual' => false, + 'reference' => null, + 'inverse_of' => null, + 'widget' => null, + 'validations' => [], + ] + ] + ) + ); + $forestModel->setFields( + [ + ['field' => 'label', 'is_required' => false], + ['field' => 'bar', 'enums' => ['easy', 'hard']], + ] + ); + $fields = $forestModel->getFields(); + $foo = array_search('foo', array_column($fields, 'field'), true); + $bar = array_search('bar', array_column($fields, 'field'), true); + $label = array_search('label', array_column($fields, 'field'), true); + $defaultValues = $this->invokeMethod($forestModel, 'fieldDefaultValues'); + + $this->assertIsArray($fields); + $this->assertNotNull($foo); + $this->assertNotNull($bar); + $this->assertNotNull($label); + $this->assertEquals($fields[$bar]['type'], 'Enum'); + $this->assertEquals($fields[$bar]['enums'], ['easy', 'hard']); + $this->assertEquals($fields[$label]['default_value'], $defaultValues['default_value']); + $this->assertEquals($fields[$label]['enums'], $defaultValues['enums']); + $this->assertEquals($fields[$label]['integration'], $defaultValues['integration']); + $this->assertEquals($fields[$label]['is_filterable'], $defaultValues['is_filterable']); + $this->assertEquals($fields[$label]['is_read_only'], $defaultValues['is_read_only']); + $this->assertEquals($fields[$label]['is_required'], $defaultValues['is_required']); + $this->assertEquals($fields[$label]['is_sortable'], $defaultValues['is_sortable']); + $this->assertEquals($fields[$label]['is_virtual'], $defaultValues['is_virtual']); + $this->assertEquals($fields[$label]['reference'], $defaultValues['reference']); + $this->assertEquals($fields[$label]['widget'], $defaultValues['widget']); + $this->assertEquals($fields[$label]['validations'], $defaultValues['validations']); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testFetchFieldsFromTable(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial() + ->shouldAllowMockingProtectedMethods(); + $forestModel->shouldReceive('getRelations') + ->withAnyArgs() + ->andReturn([]); + + $fields = $forestModel->fetchFieldsFromTable(); + + $this->assertInstanceOf(Collection::class, $fields); + $this->assertNull($fields->firstWhere('field', 'field_exclude')); + $this->assertIsArray($fields['id']); + $this->assertEquals($fields['id']['field'], 'id'); + $this->assertEquals($fields['id']['type'], 'Number'); + $this->assertEquals($fields['id']['is_required'], true); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testGetRelations(): void + { + $dummyModel = new Book(); + $forestModel = m::mock(ForestModel::class, [$dummyModel]) + ->makePartial(); + + $relations = $forestModel->getRelations($forestModel->getModel()); + foreach ((new \ReflectionClass($forestModel->getModel()))->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + $publicMethods[$method->getName()] = (string) $method->getReturnType(); + } + + $this->assertIsArray($relations); + foreach ($relations as $key => $value) { + $this->assertcontains($key, array_keys($publicMethods)); + $this->assertcontains($value, $publicMethods); + } + } + + /** + * @return void + */ + public function testMergeFieldsWithRelationsBelongsTo(): void + { + [$forestModel, $fields] = $this->makeForestModel(); + $relations = $forestModel->getRelations($forestModel->getModel()); + $merge = $forestModel->mergeFieldsWithRelations($fields, $relations); + + $fieldCategory = $merge->firstWhere('field', 'category'); + $category = $forestModel->getModel()->category(); + $this->assertNotNull($fieldCategory); + $this->assertEquals($fieldCategory['relationship'], $forestModel->mapRelationships(BelongsTo::class)); + $this->assertEquals($fieldCategory['reference'], $category->getRelated()->getTable() . '.' . $category->getOwnerKeyName()); + $this->assertEquals($fieldCategory['inverse_of'], $category->getOwnerKeyName()); + } + + /** + * @return void + */ + public function testMergeFieldsWithRelationsBelongsToMany(): void + { + [$forestModel, $fields] = $this->makeForestModel(); + $relations = $forestModel->getRelations($forestModel->getModel()); + $merge = $forestModel->mergeFieldsWithRelations($fields, $relations); + + $fieldRange = $merge->firstWhere('field', 'ranges'); + $ranges = $forestModel->getModel()->ranges(); + $this->assertNotNull($fieldRange); + $this->assertEquals($fieldRange['relationship'], $forestModel->mapRelationships(BelongsToMany::class)); + $this->assertEquals($fieldRange['inverse_of'], $ranges->getRelatedPivotKeyName()); + } + + /** + * @return void + */ + public function testMergeFieldsWithRelationsHasMany(): void + { + [$forestModel, $fields] = $this->makeForestModel(); + $relations = $forestModel->getRelations($forestModel->getModel()); + $merge = $forestModel->mergeFieldsWithRelations($fields, $relations); + + $fieldComment = $merge->firstWhere('field', 'comments'); + $comments = $forestModel->getModel()->comments(); + $this->assertNotNull($fieldComment); + $this->assertEquals($fieldComment['relationship'], $forestModel->mapRelationships(HasMany::class)); + $this->assertEquals($fieldComment['field'], 'comments'); + $this->assertEquals($fieldComment['reference'], $comments->getRelated()->getTable() . '.' . $comments->getForeignKeyName()); + $this->assertEquals($fieldComment['inverse_of'], $comments->getForeignKeyName()); + } + + /** + * @return void + */ + public function testMergeFieldsWithRelationsHasManyThrough(): void + { + [$forestModel, $fields] = $this->makeForestModel(); + $relations = $forestModel->getRelations($forestModel->getModel()); + $merge = $forestModel->mergeFieldsWithRelations($fields, $relations); + + $fieldBookstore = $merge->firstWhere('field', 'bookstores'); + $bookstores = $forestModel->getModel()->bookstores(); + $this->assertNotNull($fieldBookstore); + $this->assertEquals($fieldBookstore['relationship'], $forestModel->mapRelationships(HasManyThrough::class)); + $this->assertEquals($fieldBookstore['field'], 'bookstores'); + $this->assertEquals($fieldBookstore['reference'], $bookstores->getRelated()->getTable() . '.' . $bookstores->getLocalKeyName()); + } + + /** + * @return void + */ + public function testMergeFieldsWithRelationsHasOne(): void + { + [$forestModel, $fields] = $this->makeForestModel(); + $relations = $forestModel->getRelations($forestModel->getModel()); + $merge = $forestModel->mergeFieldsWithRelations($fields, $relations); + + $editor = $merge->firstWhere('field', 'editor'); + $editors = $forestModel->getModel()->editor(); + $this->assertNotNull($editor); + $this->assertEquals($editor['relationship'], $forestModel->mapRelationships(HasOne::class)); + $this->assertEquals($editor['field'], 'editor'); + $this->assertEquals($editor['reference'], $editors->getRelated()->getTable() . '.' . $editors->getForeignKeyName()); + $this->assertEquals($editor['inverse_of'], $editors->getForeignKeyName()); + } + + /** + * @return void + */ + public function testMergeFieldsWithRelationsHasOneThrough(): void + { + [$forestModel, $fields] = $this->makeForestModel(); + $relations = $forestModel->getRelations($forestModel->getModel()); + $merge = $forestModel->mergeFieldsWithRelations($fields, $relations); + + $fieldAuthor = $merge->firstWhere('field', 'author'); + $author = $forestModel->getModel()->author(); + $this->assertNotNull($fieldAuthor); + $this->assertEquals($fieldAuthor['relationship'], $forestModel->mapRelationships(HasOneThrough::class)); + $this->assertEquals($fieldAuthor['field'], 'author'); + $this->assertEquals($fieldAuthor['reference'], $author->getRelated()->getTable() . '.' . $author->getLocalKeyName()); + } + + /** + * @return void + */ + public function testMergeFieldsWithRelationsMorphOne(): void + { + [$forestModel, $fields] = $this->makeForestModel(); + $relations = $forestModel->getRelations($forestModel->getModel()); + $merge = $forestModel->mergeFieldsWithRelations($fields, $relations); + + $image = $merge->firstWhere('field', 'image'); + $images = $forestModel->getModel()->image(); + $this->assertNotNull($image); + $this->assertEquals($image['relationship'], $forestModel->mapRelationships(MorphOne::class)); + $this->assertEquals($image['field'], 'image'); + $this->assertEquals($image['reference'], $images->getRelated()->getTable() . '.' . $images->getForeignKeyName()); + } + + /** + * @return void + */ + public function testMergeFieldsWithRelationsMorphMany(): void + { + [$forestModel, $fields] = $this->makeForestModel(); + $relations = $forestModel->getRelations($forestModel->getModel()); + $merge = $forestModel->mergeFieldsWithRelations($fields, $relations); + + $fieldTag = $merge->firstWhere('field', 'tags'); + $tags = $forestModel->getModel()->tags(); + $this->assertNotNull($fieldTag); + $this->assertEquals($fieldTag['relationship'], $forestModel->mapRelationships(MorphMany::class)); + $this->assertEquals($fieldTag['field'], 'tags'); + $this->assertEquals($fieldTag['reference'], $tags->getRelated()->getTable() . '.' . $tags->getForeignKeyName()); + } + + /** + * @return void + */ + public function testMergeFieldsWithRelationsMorphToMany(): void + { + [$forestModel, $fields] = $this->makeForestModel(); + $relations = $forestModel->getRelations($forestModel->getModel()); + $merge = $forestModel->mergeFieldsWithRelations($fields, $relations); + + $fieldBuy = $merge->firstWhere('field', 'buys'); + $buys = $forestModel->getModel()->buys(); + $this->assertNotNull($fieldBuy); + $this->assertEquals($fieldBuy['relationship'], $forestModel->mapRelationships(MorphToMany::class)); + $this->assertEquals($fieldBuy['field'], 'buys'); + $this->assertEquals($fieldBuy['inverse_of'], $buys->getRelatedPivotKeyName()); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testSetName(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial(); + $value = 'name'; + $forestModel->setName($value); + + $this->assertEquals($value, $forestModel->getName()); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testSetOldName(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial(); + $value = 'old-name'; + $forestModel->setOldName($value); + + $this->assertEquals($value, $forestModel->getOldName()); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testSetIcon(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial(); + $value = 'icon'; + $forestModel->setIcon($value); + + $this->assertEquals($value, $forestModel->getIcon()); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testSetIsReadOnly(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial(); + $value = true; + $forestModel->setIsReadOnly($value); + + $this->assertEquals($value, $forestModel->isReadOnly()); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testSetIsSearchable(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial(); + $value = true; + $forestModel->setIsSearchable($value); + + $this->assertEquals($value, $forestModel->isSearchable()); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testSetIsVirtual(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial(); + $value = true; + $forestModel->setIsVirtual($value); + + $this->assertEquals($value, $forestModel->isVirtual()); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testSetOnlyForRelationships(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial(); + $value = true; + $forestModel->setOnlyForRelationships($value); + + $this->assertEquals($value, $forestModel->isOnlyForRelationships()); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testSetPaginationType(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial(); + $value = 'Page'; + $forestModel->setPaginationType($value); + + $this->assertEquals($value, $forestModel->getPaginationType()); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testSetTable(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial(); + $value = 'table'; + $forestModel->setTable($value); + + $this->assertEquals($value, $forestModel->getTable()); + } + + /** + * @return void + * @throws SchemaException + * @throws Exception + */ + public function testSetDatabase(): void + { + $forestModel = m::mock(ForestModel::class, [$this->getLaravelModel()]) + ->makePartial(); + $value = 'db'; + $forestModel->setDatabase($value); + + $this->assertEquals($value, $forestModel->getDatabase()); + } + + /** + * @return object + * @throws Exception + * @throws SchemaException + */ + public function getLaravelModel() + { + $schemaManager = $this->prophesize(AbstractSchemaManager::class); + $schemaManager->listTableColumns(Argument::any(), Argument::any()) + ->willReturn( + [ + 'id' => new Column('id', Type::getType('bigint')), + 'foo' => new Column('foo', Type::getType('string')), + 'field_exclude' => new Column('field_exclude', Type::getType('array')), + ] + ); + + $connection = $this->prophesize(Connection::class); + $connection->getTablePrefix() + ->shouldBeCalled() + ->willReturn('prefix.'); + $connection->getDoctrineSchemaManager() + ->willReturn($schemaManager->reveal()); + + $model = $this->prophesize(Model::class); + $model + ->getConnection() + ->shouldBeCalled() + ->willReturn($connection->reveal()); + $model + ->getTable() + ->shouldBeCalledOnce() + ->willReturn('dummy_tables'); + + return $model->reveal(); + } + + /** + * @return array + */ + public function makeForestModel(): array + { + $dummyModel = new Book(); + $forestModel = m::mock(ForestModel::class, [$dummyModel])->makePartial(); + + $fields = collect( + [ + 'category_id' => ['field' => 'category_id'], + ] + ); + + return [$forestModel, $fields]; + } +} diff --git a/tests/Unit/SchemaTest.php b/tests/Unit/SchemaTest.php new file mode 100644 index 00000000..50e43af9 --- /dev/null +++ b/tests/Unit/SchemaTest.php @@ -0,0 +1,125 @@ +getConfig(), $this->getForestApi(), $this->getConsole()); + $this->invokeProperty($schema, 'directory', __DIR__ . '/../Feature/Models'); + $files = $this->invokeMethod($schema, 'fetchFiles'); + + $directory = glob(__DIR__ . '/../Feature/Models', GLOB_ONLYDIR); + $directoryFiles = new Collection(); + foreach ($directory as $dir) { + if (file_exists($dir)) { + $fileClass = ClassMapGenerator::createMap($dir); + foreach (array_keys($fileClass) as $file) { + $directoryFiles->push($file); + } + } + } + + $this->assertInstanceOf(Collection::class, $files); + $this->assertEquals($files, $directoryFiles); + } + + /** + * @return void + * @throws \ReflectionException + */ + public function testMetadata(): void + { + $schema = new Schema($this->getConfig(), $this->getForestApi(), $this->getConsole()); + $metadata = $this->invokeMethod($schema, 'metadata'); + + $this->assertIsArray($metadata); + $this->assertArrayHasKey('meta', $metadata); + $this->assertArrayHasKey('liana', $metadata['meta']); + $this->assertArrayHasKey('liana_version', $metadata['meta']); + $this->assertArrayHasKey('stack', $metadata['meta']); + $this->assertArrayHasKey('database_type', $metadata['meta']['stack']); + $this->assertArrayHasKey('orm_version', $metadata['meta']['stack']); + $this->assertEquals('laravel-forestadmin', $metadata['meta']['liana']); + $this->assertEquals('sqlite', $metadata['meta']['stack']['database_type']); + } + + /** + * @throws \ReflectionException + * @return void + */ + public function testGenerate(): void + { + App::shouldReceive('basePath') + ->andReturn(__DIR__ . '/../Feature/Models'); + $schema = new Schema($this->getConfig(), $this->getForestApi(), $this->getConsole()); + File::shouldReceive('put')->andReturn(true); + $generate = $this->invokeMethod($schema, 'generate'); + + $this->assertIsArray($generate); + $this->assertArrayHasKey('meta', $generate); + $this->assertArrayHasKey('collections', $generate); + } + + /** + * @return object + */ + private function getForestApi() + { + return $this->prophesize(ForestApiRequester::class)->reveal(); + } + + /** + * @return object + */ + private function getConfig() + { + $config = $this->prophesize(Repository::class); + $config + ->get('database.default') + ->willReturn('sqlite'); + $config + ->get('forest.models_directory') + ->willReturn(__DIR__ . '/../Feature/Models'); + $config + ->get('forest.json_file_path') + ->willReturn('.forestadmin-schema.json'); + + return $config->reveal(); + } + + /** + * @return object + */ + public function getConsole() + { + $console = $this->prophesize(ConsoleOutput::class); + + return $console->reveal(); + } +} diff --git a/tests/Unit/Traits/DataTypesTest.php b/tests/Unit/Traits/DataTypesTest.php new file mode 100644 index 00000000..005fb18f --- /dev/null +++ b/tests/Unit/Traits/DataTypesTest.php @@ -0,0 +1,32 @@ +getObjectForTrait(DataTypes::class); + $types = $this->invokeProperty($trait, 'dbTypes'); + + foreach ($types as $key => $value) { + $getType = $this->invokeMethod($trait, 'getType', [$key]); + + $this->assertEquals($value, $getType); + } + } +} diff --git a/tests/Unit/Traits/RelationshipsTest.php b/tests/Unit/Traits/RelationshipsTest.php new file mode 100644 index 00000000..c3f03c94 --- /dev/null +++ b/tests/Unit/Traits/RelationshipsTest.php @@ -0,0 +1,32 @@ +getObjectForTrait(Relationships::class); + $types = $this->invokeProperty($trait, 'doctrineTypes'); + + foreach ($types as $key => $value) { + $getType = $this->invokeMethod($trait, 'mapRelationships', [$key]); + + $this->assertEquals($types[$key], $getType); + } + } +} diff --git a/tests/Unit/Utils/DatabaseTest.php b/tests/Unit/Utils/DatabaseTest.php new file mode 100644 index 00000000..3ae4e976 --- /dev/null +++ b/tests/Unit/Utils/DatabaseTest.php @@ -0,0 +1,43 @@ +assertEquals($mysql, Database::getSource($mysql)); + + $postgres = 'pgsql'; + $this->assertEquals('postgres', Database::getSource($postgres)); + + $sqlserver = 'sqlsrv'; + $this->assertEquals('mssql', Database::getSource($sqlserver)); + } + + /** + * @return void + */ + public function testGetSourceException(): void + { + $foo = 'foo'; + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("The database dialect `$foo` is not supported"); + + Database::getSource($foo); + } +}