├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── run-tests.yml │ └── static-analysis.yml ├── .gitignore ├── .phpunit-watcher.yml ├── .travis.yml ├── LICENSE ├── README.md ├── SECURITY.md ├── artwork.svg ├── composer.json ├── config └── bakery.php ├── phpstan.neon.dist ├── phpunit.xml ├── resources └── views │ └── graphiql.blade.php ├── src ├── Bakery.php ├── BakeryServiceProvider.php ├── Console │ ├── InstallCommand.php │ ├── ModelSchemaCommand.php │ └── stubs │ │ ├── modelschema.stub │ │ └── user-schema.stub ├── Eloquent │ ├── Concerns │ │ ├── Authorizable.php │ │ ├── InteractsWithAttributes.php │ │ ├── InteractsWithRelations.php │ │ ├── MutatesModel.php │ │ └── QueuesTransactions.php │ ├── ModelSchema.php │ └── Traits │ │ └── BakeryTransactionalAware.php ├── Errors │ └── ValidationError.php ├── Exceptions │ ├── InvariantViolation.php │ ├── PaginationMaxCountExceededException.php │ ├── TooManyResultsException.php │ ├── TypeNotFound.php │ └── UnauthorizedException.php ├── Field.php ├── Fields │ ├── EloquentField.php │ ├── Field.php │ └── PolymorphicField.php ├── Http │ ├── Controllers │ │ └── BakeryController.php │ └── routes.php ├── Mutations │ ├── AttachPivotMutation.php │ ├── Concerns │ │ └── QueriesModel.php │ ├── CreateMutation.php │ ├── DeleteMutation.php │ ├── DetachPivotMutation.php │ ├── EloquentMutation.php │ ├── Mutation.php │ └── UpdateMutation.php ├── Queries │ ├── Concerns │ │ └── EagerLoadRelationships.php │ ├── EloquentCollectionQuery.php │ ├── EloquentQuery.php │ ├── Query.php │ └── SingleEntityQuery.php ├── Support │ ├── Arguments.php │ ├── DefaultSchema.php │ ├── Facades │ │ └── Bakery.php │ ├── RootField.php │ ├── RootMutation.php │ ├── RootQuery.php │ ├── Schema.php │ └── TypeRegistry.php ├── Traits │ ├── FiltersQueries.php │ ├── JoinsRelationships.php │ ├── OrdersQueries.php │ └── SearchesQueries.php ├── Type.php ├── Types │ ├── AttachUnionEntityInputType.php │ ├── CollectionFilterType.php │ ├── CollectionOrderByType.php │ ├── CollectionRootSearchType.php │ ├── CollectionSearchType.php │ ├── Concerns │ │ ├── InteractsWithPivot.php │ │ └── InteractsWithPolymorphism.php │ ├── CreateInputType.php │ ├── CreatePivotInputType.php │ ├── CreateUnionEntityInputType.php │ ├── CreateWithPivotInputType.php │ ├── CustomEntityType.php │ ├── Definitions │ │ ├── EloquentInputType.php │ │ ├── EloquentType.php │ │ ├── EnumType.php │ │ ├── InputType.php │ │ ├── InternalType.php │ │ ├── NamedType.php │ │ ├── ObjectType.php │ │ ├── ReferenceType.php │ │ ├── RootType.php │ │ ├── ScalarType.php │ │ └── UnionType.php │ ├── EloquentMutationInputType.php │ ├── EntityCollectionType.php │ ├── EntityLookupType.php │ ├── EntityType.php │ ├── OrderType.php │ ├── PaginationType.php │ ├── PivotInputType.php │ ├── UnionEntityType.php │ └── UpdateInputType.php ├── Utils │ └── Utils.php ├── helpers.php └── macros │ └── bakeryPaginate.php └── tests ├── BakeryServiceProviderTest.php ├── CacheTest.php ├── Factories ├── ArticleFactory.php ├── CommentFactory.php ├── PhoneFactory.php ├── RoleFactory.php ├── TagFactory.php └── UserFactory.php ├── Feature ├── AttachPivotMutationTest.php ├── AuthorizationTest.php ├── CollectionQueryTest.php ├── CreateMutationTest.php ├── CustomMutationTest.php ├── DeleteMutationTest.php ├── DetachPivotMutationTest.php ├── EntityQueryTest.php ├── GraphQLControllerTest.php ├── GraphiQLControllerTest.php └── UpdateMutationTest.php ├── Fixtures ├── IntegrationTestSchema.php ├── Models │ ├── Article.php │ ├── Comment.php │ ├── Phone.php │ ├── Role.php │ ├── Tag.php │ ├── User.php │ └── UserRole.php ├── Mutations │ └── InviteUserMutation.php ├── Policies │ ├── AllowAllPolicy.php │ ├── ArticlePolicy.php │ ├── CommentPolicy.php │ ├── PhonePolicy.php │ ├── RolePolicy.php │ ├── TagPolicy.php │ ├── UserPolicy.php │ └── UserRolePolicy.php ├── Schemas │ ├── ArticleSchema.php │ ├── CommentSchema.php │ ├── PhoneSchema.php │ ├── RoleSchema.php │ ├── TagSchema.php │ ├── UserRoleSchema.php │ └── UserSchema.php └── Types │ ├── InviteUserInputType.php │ └── TimestampType.php ├── IntegrationTest.php ├── Migrations ├── 0000_00_00_000000_create_users_table.php ├── 0000_00_00_000001_create_articles_table.php ├── 0000_00_00_000002_create_roles_table.php ├── 0000_00_00_000003_create_comments_table.php ├── 0000_00_00_000004_create_tags_table.php └── 0000_00_00_000005_create_phones_table.php ├── MutationTest.php ├── QueryTest.php ├── SchemaTest.php ├── Stubs ├── DummyClass.php ├── DummyModel.php ├── DummyModelSchema.php ├── DummyMutation.php ├── DummyQuery.php ├── DummyReadOnlySchema.php ├── DummyType.php └── EnumTypeStub.php ├── TestCase.php ├── Traits ├── BakeryTransactionalAwareTest.php └── JoinsRelationshipsTest.php ├── TypeRegistryTest.php └── Types ├── CollectionSearchTypeTest.php ├── EnumTypeTest.php └── FieldTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.php] 14 | indent_style = space 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 13 | - Laravel Version: [e.g. 5.6.23] 14 | - Bakery Version [e.g. 1.0.4] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | php: [7.2, 7.3, 7.4, 8.0] 12 | laravel: [^6.0, ^7.0, ^8.0] 13 | stability: [prefer-lowest, prefer-stable] 14 | exclude: 15 | - laravel: ^8.0 16 | php: 7.2 17 | 18 | name: PHP ${{ matrix.php }} on L${{ matrix.laravel }} - ${{ matrix.stability }} 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v1 23 | 24 | - name: Cache dependencies 25 | uses: actions/cache@v1 26 | with: 27 | path: ~/.composer/cache/files 28 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 29 | 30 | - name: Setup PHP 31 | uses: shivammathur/setup-php@master 32 | with: 33 | php-version: ${{ matrix.php }} 34 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd 35 | coverage: none 36 | 37 | - name: Remove laravel/legacy-factories 38 | if: ${{ matrix.laravel != '^8.0' }} 39 | run: composer remove --dev "laravel/legacy-factories" --no-interaction --no-update 40 | 41 | - name: Set Laravel version 42 | run: composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 43 | 44 | - name: Install dependencies 45 | run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-suggest 46 | 47 | - name: Run tests 48 | run: vendor/bin/phpunit 49 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static analysis 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | phpstan: 7 | name: PHPStan 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | php: [8.0] 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@master 19 | with: 20 | php-version: ${{ matrix.php }} 21 | coverage: none 22 | 23 | - name: Determine composer cache directory 24 | id: composer-cache 25 | run: echo "::set-output name=directory::$(composer config cache-dir)" 26 | 27 | - name: Cache dependencies 28 | uses: actions/cache@v1 29 | with: 30 | path: ${{ steps.composer-cache.outputs.directory }} 31 | key: composer-${{ matrix.php }}-${{ hashFiles('composer.*') }} 32 | restore-keys: | 33 | composer-${{ matrix.php }}- 34 | composer- 35 | 36 | - name: Install dependencies 37 | run: composer update --no-interaction --no-progress --optimize-autoloader 38 | 39 | - name: Run static analysis 40 | if: ${{ matrix.analyse }} 41 | run: vendor/bin/phpstan analyse 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | .DS_Store 4 | .idea/ 5 | .vscode/ 6 | .phpunit.result.cache 7 | -------------------------------------------------------------------------------- /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | passingTests: true 3 | failingTests: true 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '7.1' 5 | - '7.2' 6 | - '7.3' 7 | 8 | env: 9 | - TESTBENCH_VERSION="3.8.*" # Laravel 5.8 10 | 11 | install: 12 | - travis_retry composer self-update 13 | - travis_retry composer require orchestra/testbench:${TESTBENCH_VERSION} 14 | - travis_retry composer install --no-interaction 15 | 16 | script: 17 | - vendor/bin/phpunit --coverage-clover=coverage.xml 18 | 19 | after_success: 20 | - bash <(curl -s https://codecov.io/bash) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Scrn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 2.1.x | :white_check_mark: | 8 | | < 2.1 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | If you have discovered a potential security vulnerability in this project, please send an e-mail to dev@scrn.com. 13 | 14 | It is important to include the following details: 15 | 16 | The versions affected 17 | Detailed description of the vulnerability 18 | Information on known exploits 19 | 20 | We will review your e-mail and contact you to to collaborate on resolving the issue. 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrnhq/laravel-bakery", 3 | "description": "An on-the-fly GraphQL Schema generator from Eloquent models for Laravel.", 4 | "keywords": [ 5 | "laravel", 6 | "graphql" 7 | ], 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Erik Gaal", 12 | "email": "e.gaal@scrn.com" 13 | }, 14 | { 15 | "name": "Robert van Steen", 16 | "email": "r.vansteen@scrn.com" 17 | } 18 | ], 19 | "require": { 20 | "php": "^7.2|^8.0", 21 | "ext-json": "*", 22 | "illuminate/support": "^6.0|^7.0|^8.0", 23 | "webonyx/graphql-php": "^0.13.0" 24 | }, 25 | "require-dev": { 26 | "fakerphp/faker": "^1.9", 27 | "laravel/legacy-factories": "^1.1", 28 | "mockery/mockery": "^1.3.2", 29 | "nunomaduro/larastan": "^0.7.0", 30 | "orchestra/testbench": "^4.0|^5.0|^6.0", 31 | "phpunit/phpunit": "^8.0|^9.0" 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true, 35 | "autoload": { 36 | "psr-4": { 37 | "Bakery\\": "src" 38 | }, 39 | "files": [ 40 | "src/helpers.php" 41 | ] 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Bakery\\Tests\\": "tests/" 46 | } 47 | }, 48 | "scripts": { 49 | "test": "vendor/bin/phpunit" 50 | }, 51 | "config": { 52 | "process-timeout": 0, 53 | "sort-packages": true 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "providers": [ 58 | "Bakery\\BakeryServiceProvider" 59 | ], 60 | "aliases": { 61 | "Bakery": "Bakery\\Support\\Facades\\Bakery" 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /config/bakery.php: -------------------------------------------------------------------------------- 1 | '/graphql', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Bakery Domain 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This configuration option determines the domain where GraphQL will be 24 | | accessible from. 25 | | 26 | */ 27 | 28 | 'domain' => null, 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Bakery GraphiQL 33 | |-------------------------------------------------------------------------- 34 | | 35 | | This configuration option determines if GraphiQL should be enabled. 36 | | 37 | | https://github.com/graphql/graphiql 38 | | 39 | */ 40 | 41 | 'graphiql' => true, 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Bakery Route Middleware 46 | |-------------------------------------------------------------------------- 47 | | 48 | | These middleware will be assigned to every Telescope route, giving you 49 | | the chance to add your own middleware to this list or change any of 50 | | the existing middleware. Or, you can simply stick with this list. 51 | | 52 | */ 53 | 54 | 'middleware' => [ 55 | 56 | ], 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Bakery Controller 61 | |-------------------------------------------------------------------------- 62 | | 63 | | This configuration option determines the controller to be used for 64 | | GraphQL requests. 65 | | 66 | */ 67 | 68 | 'controller' => '\Bakery\Http\Controllers\BakeryController@graphql', 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Bakery Schema 73 | |-------------------------------------------------------------------------- 74 | | 75 | | This configuration option determines the schema to be used for 76 | | GraphQL requests. 77 | | 78 | */ 79 | 80 | 'schema' => \Bakery\Support\DefaultSchema::class, 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | Bakery PostgreSQL Full-text Search Dictionary 85 | |-------------------------------------------------------------------------- 86 | | 87 | | This configuration option determines the dictionary to be used 88 | | when performing full-text search in collection queries. 89 | | 90 | */ 91 | 92 | 'postgresDictionary' => 'simple', 93 | 94 | 'security' => [ 95 | 96 | /* 97 | |-------------------------------------------------------------------------- 98 | | Bakery GraphQL Introspection 99 | |-------------------------------------------------------------------------- 100 | | 101 | | This configuration option determines if the GraphQL introspection 102 | | query will be allowed. Introspection is a mechanism for fetching 103 | | schema structure. 104 | | 105 | | http://webonyx.github.io/graphql-php/security/#disabling-introspection 106 | | 107 | */ 108 | 'disableIntrospection' => env('BAKERY_DISABLE_INTROSPECTION', false), 109 | 110 | /* 111 | |-------------------------------------------------------------------------- 112 | | Bakery Pagination Max Count 113 | |-------------------------------------------------------------------------- 114 | | 115 | | This configuration option determines the maximum amount of items that 116 | | can be requested in a single page for collection queries. 117 | | 118 | */ 119 | 'paginationMaxCount' => env('BAKERY_PAGINATION_MAX_COUNT', 1000), 120 | 121 | /* 122 | |-------------------------------------------------------------------------- 123 | | Bakery Eager Loading Maximum Depth 124 | |-------------------------------------------------------------------------- 125 | | 126 | | This configuration option determines the maximum depth of the query 127 | | that will allow eager relation loading. 128 | | 129 | */ 130 | 'eagerLoadingMaxDepth' => env('BAKERY_EAGER_LOADING_MAX_DEPTH', 5), 131 | ], 132 | ]; 133 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/nunomaduro/larastan/extension.neon 3 | parameters: 4 | level: 0 5 | paths: 6 | - %currentWorkingDirectory%/src/ 7 | excludePaths: 8 | - src/macros/* 9 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/ 14 | 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/views/graphiql.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Loading...
24 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/Bakery.php: -------------------------------------------------------------------------------- 1 | toGraphQLSchema(); 26 | } 27 | 28 | /** 29 | * Execute the GraphQL query. 30 | * 31 | * @param array $input 32 | * @param \GraphQL\Type\Schema|\Bakery\Support\Schema $schema 33 | * @return \GraphQL\Executor\ExecutionResult 34 | * 35 | * @throws \Exception 36 | */ 37 | public function executeQuery($input, $schema = null): ExecutionResult 38 | { 39 | if (! $schema) { 40 | $schema = $this->schema(); 41 | } elseif ($schema instanceof BakerySchema) { 42 | $schema = $schema->toGraphQLSchema(); 43 | } 44 | 45 | $root = null; 46 | $context = []; 47 | $query = Arr::get($input, 'query'); 48 | $variables = Arr::get($input, 'variables'); 49 | if (is_string($variables)) { 50 | $variables = json_decode($variables, true); 51 | } 52 | $operationName = Arr::get($input, 'operationName'); 53 | 54 | return GraphQL::executeQuery($schema, $query, $root, $context, $variables, $operationName); 55 | } 56 | 57 | /** 58 | * Serve the GraphiQL tool. 59 | * 60 | * @param $route 61 | * @param array $headers 62 | * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 63 | */ 64 | public function graphiql($route, $headers = []) 65 | { 66 | return view( 67 | 'bakery::graphiql', 68 | ['endpoint' => route($route), 'headers' => $headers] 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/BakeryServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerRoutes(); 19 | $this->registerPublishing(); 20 | 21 | $this->loadViewsFrom( 22 | __DIR__.'/../resources/views', 'bakery' 23 | ); 24 | } 25 | 26 | /** 27 | * Register the package routes. 28 | * 29 | * @return void 30 | */ 31 | protected function registerRoutes() 32 | { 33 | if (! config('bakery.path')) { 34 | return; 35 | } 36 | 37 | $this->loadRoutesFrom(__DIR__.'/Http/routes.php'); 38 | } 39 | 40 | /** 41 | * Register the package's publishable resources. 42 | * 43 | * @return void 44 | */ 45 | protected function registerPublishing() 46 | { 47 | $this->publishes([ 48 | __DIR__.'/../config/bakery.php' => config_path('bakery.php'), 49 | ], 'bakery-config'); 50 | } 51 | 52 | /** 53 | * Register any application services. 54 | * 55 | * @return void 56 | */ 57 | public function register() 58 | { 59 | $this->mergeConfigFrom(__DIR__.'/../config/bakery.php', 'bakery'); 60 | 61 | $this->registerBakery(); 62 | 63 | $this->registerMacros(); 64 | 65 | $this->commands([ 66 | Console\InstallCommand::class, 67 | Console\ModelSchemaCommand::class, 68 | ]); 69 | } 70 | 71 | /** 72 | * Register the Bakery instance. 73 | * 74 | * @return void 75 | */ 76 | protected function registerBakery() 77 | { 78 | $this->app->singleton(Bakery::class, function () { 79 | $bakery = new Bakery(); 80 | 81 | $this->registerSecurityRules(); 82 | 83 | return $bakery; 84 | }); 85 | } 86 | 87 | /** 88 | * Register the GraphQL security rules. 89 | * 90 | * @return void 91 | */ 92 | protected function registerSecurityRules() 93 | { 94 | if (config('bakery.security.disableIntrospection') === true) { 95 | DocumentValidator::addRule(new DisableIntrospection()); 96 | } 97 | } 98 | 99 | /** 100 | * Register the macros used by Bakery. 101 | * 102 | * @return void 103 | */ 104 | protected function registerMacros() 105 | { 106 | require_once __DIR__.'/macros/bakeryPaginate.php'; // TODO: Remove this once fixed upstream. 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Console/InstallCommand.php: -------------------------------------------------------------------------------- 1 | comment('Publishing Bakery Configuration...'); 31 | $this->callSilent('vendor:publish', ['--tag' => 'bakery-config']); 32 | 33 | $this->comment('Generating User Schema...'); 34 | $this->callSilent('bakery:modelschema', ['name' => 'User']); 35 | copy(__DIR__.'/stubs/user-schema.stub', app_path('Bakery/User.php')); 36 | 37 | $this->setAppNamespace(app_path('Bakery/User.php'), $this->laravel->getNamespace()); 38 | } 39 | 40 | /** 41 | * Set the namespace on the given file. 42 | * 43 | * @param string $file 44 | * @param string $namespace 45 | * @return void 46 | */ 47 | public function setAppNamespace($file, $namespace) 48 | { 49 | file_put_contents($file, str_replace('App\\', $namespace, file_get_contents($file))); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Console/ModelSchemaCommand.php: -------------------------------------------------------------------------------- 1 | option('model'); 41 | 42 | if (is_null($model)) { 43 | $model = $this->rootNamespace().$this->argument('name'); 44 | } elseif (! Str::startsWith($model, [$this->rootNamespace(), '\\'])) { 45 | $model = $this->rootNamespace().$model; 46 | } 47 | 48 | return str_replace('DummyFullModel', $model, parent::buildClass($name)); 49 | } 50 | 51 | /** 52 | * Get the stub file for the generator. 53 | * 54 | * @return string 55 | */ 56 | protected function getStub() 57 | { 58 | return __DIR__.'/stubs/modelschema.stub'; 59 | } 60 | 61 | /** 62 | * Get the default namespace for the class. 63 | * 64 | * @param string $rootNamespace 65 | * @return string 66 | */ 67 | protected function getDefaultNamespace($rootNamespace) 68 | { 69 | return $rootNamespace.'\Bakery'; 70 | } 71 | 72 | /** 73 | * Get the console command options. 74 | * 75 | * @return array 76 | */ 77 | protected function getOptions() 78 | { 79 | return [ 80 | ['model', 'm', InputOption::VALUE_REQUIRED, 'The model class being represented.'], 81 | ]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Console/stubs/modelschema.stub: -------------------------------------------------------------------------------- 1 | Field::string(), 26 | 'email' => Field::string()->unique(), 27 | ]; 28 | } 29 | 30 | /** 31 | * Get the relations for the schema. 32 | * 33 | * @return array 34 | */ 35 | public function relations(): array 36 | { 37 | return []; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Eloquent/Concerns/InteractsWithAttributes.php: -------------------------------------------------------------------------------- 1 | $value) { 27 | $this->instance->setAttribute($key, $value); 28 | } 29 | } 30 | 31 | /** 32 | * Check the policies for the scalars in the model. 33 | * 34 | * @param array $scalars 35 | * 36 | * @throws \Illuminate\Auth\Access\AuthorizationException 37 | */ 38 | protected function checkScalars(array $scalars) 39 | { 40 | foreach ($scalars as $key => $value) { 41 | /** @var \Bakery\Fields\Field $field */ 42 | $field = $this->getFields()->get($key); 43 | 44 | $field->authorizeToStore($this->instance, $value, $key); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Eloquent/Concerns/QueuesTransactions.php: -------------------------------------------------------------------------------- 1 | queue[] = $closure; 23 | } 24 | 25 | /** 26 | * Persist the DB transactions that are queued. 27 | * 28 | * @return void 29 | */ 30 | public function persistQueuedDatabaseTransactions() 31 | { 32 | foreach ($this->queue as $key => $closure) { 33 | $closure($this); 34 | unset($this->queue[$key]); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Eloquent/Traits/BakeryTransactionalAware.php: -------------------------------------------------------------------------------- 1 | addObservableEvents(['persisting', 'persisted']); 30 | }); 31 | } 32 | 33 | /** 34 | * Start the transaction for this model. 35 | * 36 | * @return void 37 | */ 38 | public function startTransaction() 39 | { 40 | $this->inTransaction = true; 41 | $this->fireModelEvent('persisting'); 42 | } 43 | 44 | /** 45 | * End the transaction for this model. 46 | * 47 | * @return void 48 | */ 49 | public function endTransaction() 50 | { 51 | $this->inTransaction = false; 52 | $this->fireModelEvent('persisted'); 53 | } 54 | 55 | /** 56 | * Return if the model is currently in transaction. 57 | * 58 | * @return bool 59 | */ 60 | public function inTransaction(): bool 61 | { 62 | return $this->inTransaction; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Errors/ValidationError.php: -------------------------------------------------------------------------------- 1 | errors()->first()); 26 | 27 | $this->validator = $validator; 28 | $this->extensions['validation'] = $validator->errors(); 29 | } 30 | 31 | /** 32 | * Returns true when exception message is safe to be displayed to a client. 33 | * 34 | * @return bool 35 | * 36 | * @api 37 | */ 38 | public function isClientSafe() 39 | { 40 | return true; 41 | } 42 | 43 | /** 44 | * Returns string describing a category of the error. 45 | * 46 | * Value "graphql" is reserved for errors produced by query parsing or validation, do not use it. 47 | * 48 | * @return string 49 | * 50 | * @api 51 | */ 52 | public function getCategory() 53 | { 54 | return 'user'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Exceptions/InvariantViolation.php: -------------------------------------------------------------------------------- 1 | model = $model; 34 | $this->ids = Arr::wrap($ids); 35 | 36 | $this->message = "Too many results for model [{$model}]"; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * Get the affected Eloquent model. 43 | * 44 | * @return string 45 | */ 46 | public function getModel(): string 47 | { 48 | return $this->model; 49 | } 50 | 51 | /** 52 | * Get the affected Eloquent model IDs. 53 | * 54 | * @return array 55 | */ 56 | public function getIds(): array 57 | { 58 | return $this->ids; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Exceptions/TypeNotFound.php: -------------------------------------------------------------------------------- 1 | code = $code ?: 0; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Field.php: -------------------------------------------------------------------------------- 1 | field(self::getRegistry()->string()); 24 | } 25 | 26 | public static function int(): Fields\Field 27 | { 28 | return self::getRegistry()->field(self::getRegistry()->int()); 29 | } 30 | 31 | public static function ID(): Fields\Field 32 | { 33 | return self::getRegistry()->field(self::getRegistry()->ID()); 34 | } 35 | 36 | public static function boolean(): Fields\Field 37 | { 38 | return self::getRegistry()->field(self::getRegistry()->boolean()); 39 | } 40 | 41 | public static function float(): Fields\Field 42 | { 43 | return self::getRegistry()->field(self::getRegistry()->float()); 44 | } 45 | 46 | public static function model(string $class): Fields\EloquentField 47 | { 48 | return self::getRegistry()->eloquent($class); 49 | } 50 | 51 | public static function collection(string $class): Fields\EloquentField 52 | { 53 | return self::model($class)->list(); 54 | } 55 | 56 | public static function type(string $type): Fields\Field 57 | { 58 | return self::getRegistry()->field($type); 59 | } 60 | 61 | public static function list(string $type): Fields\Field 62 | { 63 | return self::type($type)->list(); 64 | } 65 | 66 | public static function polymorphic(array $modelSchemas): PolymorphicField 67 | { 68 | return self::getRegistry()->polymorphic($modelSchemas); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Fields/EloquentField.php: -------------------------------------------------------------------------------- 1 | modelSchemaClass = $class; 43 | 44 | parent::__construct($registry); 45 | } 46 | 47 | /** 48 | * Set the name of the inverse relationship. 49 | * 50 | * @param string $relationName 51 | * @return $this 52 | */ 53 | public function inverse(string $relationName): self 54 | { 55 | $this->inverseRelationName = $relationName; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Get the name of the inverse relationship. 62 | * 63 | * @return string 64 | */ 65 | public function getInverse(): ?string 66 | { 67 | return $this->inverseRelationName; 68 | } 69 | 70 | /** 71 | * Get the type of the Eloquent field. 72 | * 73 | * @return \Bakery\Types\Definitions\RootType 74 | */ 75 | protected function type(): RootType 76 | { 77 | return $this->registry->type($this->getName()); 78 | } 79 | 80 | /** 81 | * @return \Bakery\Eloquent\ModelSchema 82 | */ 83 | protected function getModelClass(): ModelSchema 84 | { 85 | return $this->registry->getModelSchema($this->modelSchemaClass); 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function name(): string 92 | { 93 | return $this->getModelClass()->getTypename(); 94 | } 95 | 96 | /** 97 | * Set a custom relation resolver. 98 | */ 99 | public function relation(callable $resolver): self 100 | { 101 | $this->relationResolver = $resolver; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Return the Eloquent relation. 108 | */ 109 | public function getRelation(Model $model): Relation 110 | { 111 | if ($resolver = $this->relationResolver) { 112 | return $resolver($model); 113 | } 114 | 115 | $accessor = $this->getAccessor(); 116 | 117 | Utils::invariant( 118 | method_exists($model, $accessor), 119 | 'Relation "'.$accessor.'" is not defined on "'.get_class($model).'".' 120 | ); 121 | 122 | return $model->{$accessor}(); 123 | } 124 | 125 | /** 126 | * Determine if the field has a relation resolver. 127 | */ 128 | public function hasRelationResolver(): bool 129 | { 130 | return isset($this->relationResolver); 131 | } 132 | 133 | /** 134 | * Get the result of the field. 135 | */ 136 | public function getResult(Model $model) 137 | { 138 | if ($resolver = $this->relationResolver) { 139 | return $resolver($model)->get(); 140 | } 141 | 142 | return $model->{$this->getAccessor()}; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Fields/PolymorphicField.php: -------------------------------------------------------------------------------- 1 | modelSchemas = $modelSchemas; 36 | } 37 | 38 | /** 39 | * Get the model schemas of a polymorphic type. 40 | * 41 | * @return array 42 | */ 43 | public function getModelSchemas(): array 44 | { 45 | return $this->modelSchemas; 46 | } 47 | 48 | /** 49 | * Get the model schema by key. 50 | * 51 | * @param string $key 52 | * @return mixed 53 | */ 54 | public function getModelSchemaByKey(string $key) 55 | { 56 | return collect($this->modelSchemas)->first(function ($definition) use ($key) { 57 | return Utils::single(resolve($definition)->getModel()) === $key; 58 | }); 59 | } 60 | 61 | /** 62 | * Define the type resolver. 63 | * 64 | * @param callable $resolver 65 | * @return $this 66 | */ 67 | public function typeResolver(callable $resolver) 68 | { 69 | $this->typeResolver = $resolver; 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Get the type resolver. 76 | * 77 | * @return callable 78 | */ 79 | public function getTypeResolver() 80 | { 81 | return $this->typeResolver; 82 | } 83 | 84 | /** 85 | * Get the underlying (wrapped) type. 86 | * 87 | * @return \Bakery\Types\Definitions\RootType 88 | */ 89 | public function type(): RootType 90 | { 91 | return $this->registry->type($this->name); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Http/Controllers/BakeryController.php: -------------------------------------------------------------------------------- 1 | runningUnitTests()) { 27 | $debug = Debug::INCLUDE_DEBUG_MESSAGE; 28 | } 29 | 30 | if ($this->isExceptionHandlingDisabled()) { 31 | $debug = Debug::RETHROW_INTERNAL_EXCEPTIONS; 32 | } 33 | 34 | return $debug; 35 | } 36 | 37 | /** 38 | * Handle an HTTP response containing the GraphQL query. 39 | * 40 | * @param Request $request 41 | * @param \Bakery\Bakery $bakery 42 | * @return JsonResponse 43 | * 44 | * @throws \Exception 45 | */ 46 | public function graphql(Request $request, Bakery $bakery): JsonResponse 47 | { 48 | $input = $request->all(); 49 | 50 | $data = $bakery->executeQuery($input)->toArray($this->debug()); 51 | 52 | return response()->json($data, 200, []); 53 | } 54 | 55 | /** 56 | * Serve the GraphiQL tool. 57 | * 58 | * @param \Illuminate\Http\Request $request 59 | * @param \Bakery\Bakery $bakery 60 | * @return \Illuminate\Contracts\View\View 61 | */ 62 | public function graphiql(Request $request, Bakery $bakery) 63 | { 64 | if (! app()->isLocal()) { 65 | abort(404); 66 | } 67 | 68 | return $bakery->graphiql('bakery.graphql'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Http/routes.php: -------------------------------------------------------------------------------- 1 | domain(config('bakery.domain', null)) 7 | ->middleware(config('bakery.middleware', [])) 8 | ->as('bakery.') 9 | ->prefix(config('bakery.path')) 10 | ->group(function () { 11 | $controller = config('bakery.controller', 'BakeryController@graphql'); 12 | 13 | Route::get('/', $controller)->name('graphql'); 14 | Route::post('/', $controller)->name('graphql'); 15 | 16 | if (config('bakery.graphiql')) { 17 | Route::get('/explore', 'BakeryController@graphiql')->name('explore'); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/Mutations/AttachPivotMutation.php: -------------------------------------------------------------------------------- 1 | name)) { 25 | return $this->name; 26 | } 27 | 28 | $relation = Str::studly($this->pivotRelationName); 29 | 30 | return 'attach'.$relation.'On'.$this->modelSchema->getTypename(); 31 | } 32 | 33 | /** 34 | * Get the arguments of the mutation. 35 | * 36 | * @return array 37 | */ 38 | public function args(): array 39 | { 40 | $relation = $this->relation->getRelationName(); 41 | 42 | if ($this->getPivotModelSchema()) { 43 | $typename = Str::studly($relation).'PivotInput'; 44 | $type = $this->registry->type($typename)->list(); 45 | } else { 46 | $type = $this->registry->ID()->list(); 47 | } 48 | 49 | return $this->modelSchema->getLookupFields() 50 | ->map(function (Field $field) { 51 | return $field->getType(); 52 | }) 53 | ->merge(['input' => $type]) 54 | ->toArray(); 55 | } 56 | 57 | /** 58 | * Get the pivot relation. 59 | * 60 | * @param \Illuminate\Database\Eloquent\Model $model 61 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 62 | */ 63 | protected function getRelation(Model $model): BelongsToMany 64 | { 65 | return $model->{$this->pivotRelationName}(); 66 | } 67 | 68 | /** 69 | * Resolve the mutation. 70 | * 71 | * @param Arguments $args 72 | * @return Model 73 | */ 74 | public function resolve(Arguments $args): Model 75 | { 76 | $input = $args->input->toArray(); 77 | $model = $this->findOrFail($args); 78 | 79 | return DB::transaction(function () use ($input, $model) { 80 | $modelSchema = $this->registry->getSchemaForModel($model); 81 | 82 | $relation = $this->getRelation($model); 83 | 84 | $modelSchema->connectBelongsToManyRelation($relation, $input, false); 85 | $modelSchema->save(); 86 | 87 | // Refresh the model to accommodate for any side effects 88 | // that the pivot relation may have caused. 89 | return $modelSchema->getInstance()->refresh(); 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Mutations/Concerns/QueriesModel.php: -------------------------------------------------------------------------------- 1 | model->getKeyName(); 32 | 33 | $query = $this->modelSchema->getQuery(); 34 | 35 | if (property_exists($args, $primaryKey)) { 36 | return $query->find($args->$primaryKey); 37 | } 38 | 39 | $fields = Arr::except($args->toArray(), ['input']); 40 | 41 | foreach ($fields as $key => $value) { 42 | $query->where($key, $value); 43 | } 44 | 45 | $results = $query->get(); 46 | 47 | if ($results->count() > 1) { 48 | throw (new TooManyResultsException)->setModel(get_class($this->model), 49 | Arr::pluck($results, $this->model->getKeyName())); 50 | } 51 | 52 | return $results->first(); 53 | } 54 | 55 | /** 56 | * Get the model based on the arguments provided. 57 | * Otherwise fail. 58 | * 59 | * @param Arguments $args 60 | * @return Model 61 | */ 62 | public function findOrFail(Arguments $args): Model 63 | { 64 | $result = $this->find($args); 65 | 66 | if (! $result) { 67 | throw (new ModelNotFoundException)->setModel(class_basename($this->model)); 68 | } 69 | 70 | return $result; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Mutations/CreateMutation.php: -------------------------------------------------------------------------------- 1 | name)) { 19 | return $this->name; 20 | } 21 | 22 | return 'create'.$this->modelSchema->getTypename(); 23 | } 24 | 25 | /** 26 | * Resolve the mutation. 27 | * 28 | * @param Arguments $args 29 | * @return Model 30 | */ 31 | public function resolve(Arguments $args): Model 32 | { 33 | $input = $args->input->toArray(); 34 | 35 | return DB::transaction(function () use ($input) { 36 | return $this->getModelSchema()->createIfAuthorized($input); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Mutations/DeleteMutation.php: -------------------------------------------------------------------------------- 1 | name)) { 21 | return $this->name; 22 | } 23 | 24 | return 'delete'.$this->modelSchema->getTypename(); 25 | } 26 | 27 | /** 28 | * Get the return type of the mutation. 29 | * 30 | * @return RootType 31 | */ 32 | public function type(): RootType 33 | { 34 | return $this->registry->boolean(); 35 | } 36 | 37 | /** 38 | * Get the arguments of the mutation. 39 | * 40 | * @return array 41 | */ 42 | public function args(): array 43 | { 44 | return $this->modelSchema->getLookupFields()->map(function (Field $field) { 45 | return $field->getType(); 46 | })->toArray(); 47 | } 48 | 49 | /** 50 | * Resolve the mutation. 51 | * 52 | * @param Arguments $args 53 | * @return Model 54 | * 55 | * @throws AuthorizationException 56 | */ 57 | public function resolve(Arguments $args): Model 58 | { 59 | /** @var Model $model */ 60 | $model = $this->findOrFail($args); 61 | 62 | $modelSchema = $this->registry->getSchemaForModel($model); 63 | $modelSchema->authorizeToDelete(); 64 | 65 | $model->delete(); 66 | 67 | return $model; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Mutations/DetachPivotMutation.php: -------------------------------------------------------------------------------- 1 | name)) { 24 | return $this->name; 25 | } 26 | 27 | $relation = Str::studly($this->pivotRelationName); 28 | 29 | return 'detach'.$relation.'On'.$this->modelSchema->getTypename(); 30 | } 31 | 32 | /** 33 | * Get the pivot relation for a model. 34 | * 35 | * @param \Illuminate\Database\Eloquent\Model $model 36 | * @return mixed 37 | */ 38 | protected function getRelation(Model $model): Relations\BelongsToMany 39 | { 40 | return $model->{$this->pivotRelationName}(); 41 | } 42 | 43 | /** 44 | * Get the arguments of the mutation. 45 | * 46 | * @return array 47 | */ 48 | public function args(): array 49 | { 50 | $relation = $this->relation->getRelationName(); 51 | 52 | if ($this->getPivotModelSchema()) { 53 | $typename = Str::studly($relation).'PivotInput'; 54 | $type = $this->registry->type($typename)->list(); 55 | } else { 56 | $type = $this->registry->ID()->list(); 57 | } 58 | 59 | return $this->modelSchema->getLookupFields()->map(function (Field $field) { 60 | return $field->getType(); 61 | })->merge(['input' => $type])->toArray(); 62 | } 63 | 64 | /** 65 | * Resolve the mutation. 66 | * 67 | * @param Arguments $args 68 | * @return Model 69 | */ 70 | public function resolve(Arguments $args): Model 71 | { 72 | $model = $this->findOrFail($args); 73 | $modelSchema = $this->registry->getSchemaForModel($model); 74 | $relation = $this->getRelation($model); 75 | 76 | $input = collect($args['input']); 77 | $pivotAccessor = $relation->getPivotAccessor(); 78 | $pivotSchema = $this->getPivotModelSchema(); 79 | 80 | $input->map(function ($input) use ($pivotSchema, $relation, $pivotAccessor, $model) { 81 | $key = $input[$model->getKeyName()] ?? $input; 82 | $pivotWhere = $input[$pivotAccessor] ?? []; 83 | 84 | $query = $this->getRelation($model); 85 | 86 | foreach ($pivotWhere as $column => $value) { 87 | // Transform modelId to model_id. 88 | if ($pivotSchema->getRelationFields()->keys()->contains(Str::before($column, 'Id'))) { 89 | $column = Str::snake($column); 90 | } 91 | 92 | $query->wherePivot($column, $value); 93 | } 94 | 95 | $query->wherePivot($relation->getRelatedPivotKeyName(), $key); 96 | 97 | return $query; 98 | })->map(function (Relations\BelongsToMany $query) use ($modelSchema) { 99 | $query->each(function (Model $related) use ($modelSchema) { 100 | $modelSchema->authorizeToDetach($related); 101 | }); 102 | 103 | return $query; 104 | })->each(function (Relations\BelongsToMany $query) { 105 | $query->detach(); 106 | }); 107 | 108 | // Refresh the model to accommodate for any side effects 109 | // that the pivot relation may have caused. 110 | return $model->refresh(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Mutations/EloquentMutation.php: -------------------------------------------------------------------------------- 1 | modelSchema = $modelSchema; 37 | } elseif (is_string($this->modelSchema)) { 38 | $this->modelSchema = $this->registry->getModelSchema($this->modelSchema); 39 | } 40 | 41 | Utils::invariant( 42 | $this->modelSchema instanceof ModelSchema, 43 | 'Model schema on '.get_class($this).' should be an instance of '.ModelSchema::class 44 | ); 45 | 46 | $this->model = $this->modelSchema->getModel(); 47 | } 48 | 49 | /** 50 | * Get the name of the Mutation, if no name is specified fall back 51 | * on a name based on the class name. 52 | * 53 | * @return string 54 | */ 55 | public function name(): string 56 | { 57 | if (isset($this->name)) { 58 | return $this->name; 59 | } 60 | 61 | return Str::camel(Str::before(class_basename($this), 'Mutation')); 62 | } 63 | 64 | /** 65 | * The type of the Mutation. 66 | * 67 | * @return RootType 68 | */ 69 | public function type(): RootType 70 | { 71 | return $this->registry->type($this->modelSchema->typename())->nullable(false); 72 | } 73 | 74 | /** 75 | * The arguments for the Mutation. 76 | * 77 | * @return array 78 | */ 79 | public function args(): array 80 | { 81 | $inputTypeName = Utils::typename($this->name()).'Input'; 82 | 83 | return [ 84 | 'input' => $this->registry->type($inputTypeName)->nullable(false), 85 | ]; 86 | } 87 | 88 | /** 89 | * @return \Bakery\Eloquent\ModelSchema 90 | */ 91 | public function getModelSchema(): ModelSchema 92 | { 93 | return $this->modelSchema; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Mutations/Mutation.php: -------------------------------------------------------------------------------- 1 | name)) { 19 | return $this->name; 20 | } 21 | 22 | return Str::camel(Str::before(class_basename($this), 'Mutation')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mutations/UpdateMutation.php: -------------------------------------------------------------------------------- 1 | name)) { 20 | return $this->name; 21 | } 22 | 23 | return 'update'.$this->modelSchema->getTypename(); 24 | } 25 | 26 | /** 27 | * Get the arguments of the mutation. 28 | * 29 | * @return array 30 | */ 31 | public function args(): array 32 | { 33 | return array_merge( 34 | parent::args(), 35 | $this->modelSchema->getLookupFields()->map(function (Field $field) { 36 | return $field->getType(); 37 | })->toArray() 38 | ); 39 | } 40 | 41 | /** 42 | * Resolve the mutation. 43 | * 44 | * @param Arguments $args 45 | * @return Model 46 | */ 47 | public function resolve(Arguments $args): Model 48 | { 49 | $input = $args->input->toArray(); 50 | $model = $this->findOrFail($args); 51 | 52 | return DB::transaction(function () use ($input, $model) { 53 | $modelSchema = $this->registry->getSchemaForModel($model); 54 | 55 | return $modelSchema->updateIfAuthorized($input); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Queries/Concerns/EagerLoadRelationships.php: -------------------------------------------------------------------------------- 1 | $subFields) { 28 | $field = $schema->getFieldByKey($key); 29 | 30 | // Skip if this is not a defined field. 31 | if (! $field) { 32 | continue; 33 | } 34 | 35 | // If a custom relation resolver is provided we cannot eager load. 36 | if ($field instanceof EloquentField && $field->hasRelationResolver()) { 37 | continue; 38 | } 39 | 40 | $with = array_map(function ($with) use ($path) { 41 | return $path ? "{$path}.{$with}" : $with; 42 | }, $field->getWith() ?? []); 43 | 44 | $query->with($with); 45 | 46 | if ($field instanceof EloquentField) { 47 | $accessor = $field->getAccessor(); 48 | $related = $field->getRelation($schema->getModel())->getRelated(); 49 | $relatedSchema = $this->registry->getSchemaForModel($related); 50 | $this->eagerLoadRelations($query, $subFields, $relatedSchema, $path ? "{$path}.{$accessor}" : $accessor); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Queries/EloquentCollectionQuery.php: -------------------------------------------------------------------------------- 1 | name)) { 36 | return $this->name; 37 | } 38 | 39 | return Utils::plural($this->modelSchema->getTypename()); 40 | } 41 | 42 | /** 43 | * The type of the CollectionQuery. 44 | * 45 | * @return RootType 46 | */ 47 | public function type(): RootType 48 | { 49 | return $this->registry->type($this->modelSchema->typename().'Collection'); 50 | } 51 | 52 | /** 53 | * The arguments for the CollectionQuery. 54 | * 55 | * @return array 56 | */ 57 | public function args(): array 58 | { 59 | $args = collect([ 60 | 'page' => $this->registry->int()->nullable(), 61 | 'count' => $this->registry->int()->nullable(), 62 | 'filter' => $this->registry->type($this->modelSchema->typename().'Filter')->nullable(), 63 | ]); 64 | 65 | if ($this->modelSchema->isSearchable()) { 66 | $args->put('search', $this->registry->type($this->modelSchema->typename().'RootSearch')->nullable()); 67 | } 68 | 69 | if (! empty($this->modelSchema->getFields())) { 70 | $args->put('orderBy', $this->registry->type($this->modelSchema->typename().'OrderBy')->nullable()); 71 | } 72 | 73 | return $args->toArray(); 74 | } 75 | 76 | /** 77 | * Resolve the CollectionQuery. 78 | * 79 | * @param Arguments $args 80 | * @param mixed $root 81 | * @param mixed $context 82 | * @param \GraphQL\Type\Definition\ResolveInfo $info 83 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 84 | * 85 | * @throws PaginationMaxCountExceededException 86 | */ 87 | public function resolve(Arguments $args, $root, $context, ResolveInfo $info): LengthAwarePaginator 88 | { 89 | $page = $args->page ?? 1; 90 | $count = $args->count ?? 15; 91 | 92 | $maxCount = config('bakery.security.paginationMaxCount'); 93 | 94 | if ($count > $maxCount) { 95 | throw new PaginationMaxCountExceededException($maxCount); 96 | } 97 | 98 | $query = $this->scopeQuery($this->modelSchema->getQuery()); 99 | 100 | $fields = $info->getFieldSelection(config('bakery.security.eagerLoadingMaxDepth')); 101 | $this->eagerLoadRelations($query, $fields['items'], $this->modelSchema); 102 | 103 | // Select all columns from the table. 104 | $query->addSelect($this->model->getTable().'.*'); 105 | 106 | if ($args->filter) { 107 | $query = $this->applyFilters($query, $args->filter); 108 | } 109 | 110 | if ($args->search) { 111 | $query = $this->applySearch($query, $args->search); 112 | } 113 | 114 | if ($args->orderBy) { 115 | $query = $this->applyOrderBy($query, $args->orderBy); 116 | } 117 | 118 | return $query->distinct()->bakeryPaginate($count, ['*'], 'page', $page); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Queries/EloquentQuery.php: -------------------------------------------------------------------------------- 1 | modelSchema = $modelSchema; 39 | } elseif (is_string($this->modelSchema)) { 40 | $this->modelSchema = $this->registry->getModelSchema($this->modelSchema); 41 | } 42 | 43 | Utils::invariant( 44 | $this->modelSchema instanceof ModelSchema, 45 | 'Model schema on '.get_class($this).' should be an instance of '.ModelSchema::class 46 | ); 47 | 48 | $this->model = $this->modelSchema->getModel(); 49 | } 50 | 51 | /** 52 | * Get the model schema. 53 | * 54 | * @return \Bakery\Eloquent\ModelSchema 55 | */ 56 | public function getModelSchema(): ModelSchema 57 | { 58 | return $this->modelSchema; 59 | } 60 | 61 | /** 62 | * Scope the query. 63 | * This can be overwritten to make your own collection queries. 64 | * 65 | * @param Builder $query 66 | * @return Builder 67 | */ 68 | protected function scopeQuery(Builder $query): Builder 69 | { 70 | return $query; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Queries/Query.php: -------------------------------------------------------------------------------- 1 | name)) { 19 | return $this->name; 20 | } 21 | 22 | return Str::camel(Str::before(class_basename($this), 'Query')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Queries/SingleEntityQuery.php: -------------------------------------------------------------------------------- 1 | name)) { 22 | return $this->name; 23 | } 24 | 25 | return Utils::single($this->modelSchema->getTypename()); 26 | } 27 | 28 | /** 29 | * The return type of the query. 30 | */ 31 | public function type(): RootType 32 | { 33 | return $this->registry->type($this->modelSchema->typename())->nullable(); 34 | } 35 | 36 | /** 37 | * The arguments for the Query. 38 | */ 39 | public function args(): array 40 | { 41 | return $this->modelSchema->getLookupTypes()->toArray(); 42 | } 43 | 44 | /** 45 | * Resolve the EloquentQuery. 46 | */ 47 | public function resolve(Arguments $args, $root, $context, ResolveInfo $info): ?Model 48 | { 49 | $query = $this->scopeQuery($this->modelSchema->getQuery()); 50 | 51 | $fields = $info->getFieldSelection(config('bakery.security.eagerLoadingMaxDepth')); 52 | $this->eagerLoadRelations($query, $fields, $this->modelSchema); 53 | 54 | if ($args->primaryKey) { 55 | return $query->find($args->primaryKey); 56 | } 57 | 58 | $results = $this->queryByArgs($query, $args)->get(); 59 | 60 | if ($results->count() < 1) { 61 | return null; 62 | } 63 | 64 | if ($results->count() > 1) { 65 | throw (new TooManyResultsException)->setModel(get_class($this->model), 66 | Arr::pluck($results, $this->model->getKeyName())); 67 | } 68 | 69 | return $results->first(); 70 | } 71 | 72 | /** 73 | * Query by the arguments supplied to the query. 74 | */ 75 | protected function queryByArgs(Builder $query, Arguments $args): Builder 76 | { 77 | foreach ($args as $key => $value) { 78 | if ($value instanceof Arguments) { 79 | $query->whereHas($key, function (Builder $query) use ($value) { 80 | $this->queryByArgs($query, $value); 81 | }); 82 | } else { 83 | $query->where($key, $value); 84 | } 85 | } 86 | 87 | return $query; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Support/Arguments.php: -------------------------------------------------------------------------------- 1 | $value) { 19 | if (is_array($value)) { 20 | $value = new self($value); 21 | } 22 | 23 | $data[$key] = $value; 24 | } 25 | 26 | parent::__construct($data, ArrayObject::ARRAY_AS_PROPS); 27 | } 28 | 29 | /** 30 | * @param $offset 31 | * @return mixed|null 32 | */ 33 | public function offsetGet($offset) 34 | { 35 | if (! $this->offsetExists($offset)) { 36 | return null; 37 | } 38 | 39 | return parent::offsetGet($offset); 40 | } 41 | 42 | /** 43 | * Get the instance as an array. 44 | * 45 | * @return array 46 | */ 47 | public function toArray(): array 48 | { 49 | return $this->getArrayCopy(); 50 | } 51 | 52 | /** 53 | * @return array 54 | */ 55 | public function getArrayCopy(): array 56 | { 57 | $array = parent::getArrayCopy(); 58 | 59 | foreach ($array as $key => $value) { 60 | if ($value instanceof self) { 61 | $array[$key] = $value->getArrayCopy(); 62 | } 63 | } 64 | 65 | return $array; 66 | } 67 | 68 | /** 69 | * Return if the arguments are empty. 70 | * 71 | * @return bool 72 | */ 73 | public function isEmpty() 74 | { 75 | return empty($this->toArray()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Support/DefaultSchema.php: -------------------------------------------------------------------------------- 1 | modelsIn(app_path('Bakery')); 17 | 18 | Utils::invariant(count($models) > 0, 'There must be model schema\'s defined in the Bakery directory.'); 19 | 20 | return $models; 21 | } 22 | 23 | /** 24 | * Get the types from the config. 25 | * 26 | * @return array 27 | */ 28 | public function types(): array 29 | { 30 | return config('bakery.types') ?: []; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Support/Facades/Bakery.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 19 | 20 | parent::__construct($registry); 21 | } 22 | 23 | /** 24 | * Get the fields for the type. 25 | * 26 | * @return Collection 27 | */ 28 | public function getFields(): Collection 29 | { 30 | $fields = collect($this->fields); 31 | 32 | return $fields->map(function (RootField $field) { 33 | return $field->toArray(); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Support/RootQuery.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 19 | 20 | parent::__construct($registry); 21 | } 22 | 23 | /** 24 | * Get the fields for the type. 25 | * 26 | * @return Collection 27 | */ 28 | public function getFields(): Collection 29 | { 30 | $fields = collect($this->fields); 31 | 32 | return $fields->map(function (RootField $field) { 33 | return $field->toArray(); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Traits/JoinsRelationships.php: -------------------------------------------------------------------------------- 1 | getRelated(); 20 | 21 | if ($relation instanceof Relations\BelongsTo) { 22 | $ownerKeyName = $relation->getQualifiedOwnerKeyName(); 23 | $foreignKeyName = $relation->getQualifiedForeignKeyName(); 24 | 25 | $query->join($related->getTable(), $ownerKeyName, '=', $foreignKeyName, $type, $where); 26 | } elseif ($relation instanceof Relations\BelongsToMany) { 27 | $foreignPivotKeyName = $relation->getQualifiedForeignPivotKeyName(); 28 | $relatedPivotKeyName = $relation->getQualifiedRelatedPivotKeyName(); 29 | $parentKeyName = $relation->getQualifiedParentKeyName(); 30 | $relatedKeyName = $related->getQualifiedKeyName(); 31 | 32 | $query->join($relation->getTable(), $parentKeyName, '=', $foreignPivotKeyName, $type, $where); 33 | $query->join($related->getTable(), $relatedPivotKeyName, '=', $relatedKeyName, $type, $where); 34 | } elseif ($relation instanceof Relations\HasOneOrMany) { 35 | $foreignKeyName = $relation->getQualifiedForeignKeyName(); 36 | $parentKeyName = $relation->getQualifiedParentKeyName(); 37 | 38 | $query->join($related->getTable(), $foreignKeyName, '=', $parentKeyName, $type, $where); 39 | } 40 | 41 | return $query; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Traits/OrdersQueries.php: -------------------------------------------------------------------------------- 1 | $value) { 28 | $field = $this->modelSchema->getFieldByKey($key); 29 | 30 | if ($field->isRelationship()) { 31 | $relation = $field->getAccessor(); 32 | $this->applyRelationalOrderBy($query, $this->model, $relation, $value); 33 | } else { 34 | $column = $field->getAccessor(); 35 | $this->orderBy($query, $query->getModel()->getTable().'.'.$column, $value); 36 | } 37 | } 38 | 39 | return $query; 40 | } 41 | 42 | /** 43 | * Apply relational order by. 44 | * 45 | * @param \Illuminate\Database\Eloquent\Builder $query 46 | * @param \Illuminate\Database\Eloquent\Model $model 47 | * @param string $relation 48 | * @param array $args 49 | * @return \Illuminate\Database\Eloquent\Builder 50 | */ 51 | protected function applyRelationalOrderBy(Builder $query, Model $model, string $relation, Arguments $args): Builder 52 | { 53 | /** @var Relation $relation */ 54 | $relation = $model->$relation(); 55 | $related = $relation->getRelated(); 56 | $query = $this->joinRelation($query, $relation, 'left'); 57 | 58 | foreach ($args as $key => $value) { 59 | $schema = $this->registry->resolveSchemaForModel(get_class($related)); 60 | 61 | $relations = $schema->getRelationFields(); 62 | if ($relations->keys()->contains($key)) { 63 | $query = $this->applyRelationalOrderBy($query, $related, $key, $value); 64 | } else { 65 | $query = $this->orderBy($query, $related->getTable().'.'.$key, $value); 66 | } 67 | } 68 | 69 | return $query; 70 | } 71 | 72 | /** 73 | * Apply the ordering. 74 | * 75 | * @param Builder $query 76 | * @param string $column 77 | * @param string $ordering 78 | * @return Builder 79 | */ 80 | protected function orderBy(Builder $query, string $column, string $ordering) 81 | { 82 | // Alias so this doesn't conflict with actually selected fields. 83 | $alias = 'bakery_'.str_replace('.', '_', $column); 84 | 85 | $query->addSelect("{$column} as {$alias}"); 86 | 87 | return $query->orderBy($alias, $ordering); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Traits/SearchesQueries.php: -------------------------------------------------------------------------------- 1 | tsFields = []; 39 | 40 | $needle = $args['query']; 41 | $fields = $args['fields']; 42 | 43 | $relations = $this->modelSchema->getRelationFields(); 44 | 45 | foreach ($fields as $key => $value) { 46 | $field = $this->modelSchema->getFieldByKey($key); 47 | $accessor = $field->getAccessor(); 48 | if ($relations->keys()->contains($key)) { 49 | $this->applyRelationalSearch($query, $this->model, $accessor, $needle, $value->toArray()); 50 | } else { 51 | $this->tsFields[] = $this->model->getTable().'.'.$accessor; 52 | } 53 | } 54 | 55 | if (empty($needle) || empty($this->tsFields)) { 56 | return $query; 57 | } 58 | 59 | $grammar = $connection->getQueryGrammar(); 60 | 61 | if ($grammar instanceof Grammars\PostgresGrammar) { 62 | $dictionary = config('bakery.postgresDictionary'); 63 | $fields = implode(', ', $this->tsFields); 64 | $tsQueryString = Str::of($needle) 65 | ->replace(['&', '|', '!', '<->', '<', '>', ':', '"', '\''], ' ') // Replace reserved tsquery characters 66 | ->trim() // Trim whitespace 67 | ->split('/\s+/') // Split into words 68 | ->map(function ($str) { 69 | return $str.':*'; 70 | }) // Use prefix matching 71 | ->join(' & '); // Join together with AND operators 72 | 73 | $query->whereRaw("to_tsvector('${dictionary}', concat_ws(' ', ".$fields.")) @@ to_tsquery('${dictionary}', ?)", $tsQueryString); 74 | } 75 | 76 | return $query; 77 | } 78 | 79 | /** 80 | * Apply a relational search. 81 | * 82 | * @param \Illuminate\Database\Eloquent\Builder $query 83 | * @param \Illuminate\Database\Eloquent\Model $model 84 | * @param string $relation 85 | * @param string $needle 86 | * @param array $fields 87 | */ 88 | protected function applyRelationalSearch( 89 | Builder $query, 90 | Model $model, 91 | string $relation, 92 | string $needle, 93 | array $fields 94 | ) { 95 | /** @var \Illuminate\Database\Eloquent\Relations\Relation $relation */ 96 | $relation = $model->$relation(); 97 | $related = $relation->getRelated(); 98 | $this->joinRelation($query, $relation, 'left'); 99 | 100 | foreach ($fields as $key => $value) { 101 | $schema = $this->registry->getSchemaForModel($related); 102 | 103 | $relations = $schema->getRelationFields(); 104 | if ($relations->keys()->contains($key)) { 105 | $this->applyRelationalSearch($query, $related, $key, $needle, $value); 106 | } else { 107 | $this->tsFields[] = $related->getTable().'.'.$key; 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Type.php: -------------------------------------------------------------------------------- 1 | string(); 24 | } 25 | 26 | /** 27 | * @return \Bakery\Types\Definitions\InternalType 28 | */ 29 | public static function int(): InternalType 30 | { 31 | return self::getRegistry()->int(); 32 | } 33 | 34 | /** 35 | * @return \Bakery\Types\Definitions\InternalType 36 | */ 37 | public static function ID(): InternalType 38 | { 39 | return self::getRegistry()->ID(); 40 | } 41 | 42 | /** 43 | * @return \Bakery\Types\Definitions\InternalType 44 | */ 45 | public static function boolean(): InternalType 46 | { 47 | return self::getRegistry()->boolean(); 48 | } 49 | 50 | /** 51 | * @return \Bakery\Types\Definitions\InternalType 52 | */ 53 | public static function float(): InternalType 54 | { 55 | return self::getRegistry()->float(); 56 | } 57 | 58 | /** 59 | * Create a new type by reference. 60 | * 61 | * @param string $name 62 | * @return \Bakery\Types\Definitions\RootType 63 | */ 64 | public static function of(string $name): Types\Definitions\RootType 65 | { 66 | return self::getRegistry()->type($name); 67 | } 68 | 69 | /** 70 | * Create a new type for a model schema. 71 | * 72 | * @param string $class 73 | * @return \Bakery\Types\Definitions\RootType 74 | */ 75 | public static function modelSchema(string $class): Types\Definitions\RootType 76 | { 77 | $modelSchema = self::getRegistry()->getModelSchema($class); 78 | 79 | return self::getRegistry()->type($modelSchema->getTypename()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Types/AttachUnionEntityInputType.php: -------------------------------------------------------------------------------- 1 | getModelSchemas()->reduce(function (array $fields, ModelSchema $modelSchema) { 22 | $key = Utils::single($modelSchema->typename()); 23 | $fields[$key] = $this->registry->field($this->registry->ID())->nullable(); 24 | 25 | return $fields; 26 | }, []); 27 | } 28 | 29 | /** 30 | * Get the name of the Create Union Input BakeField. 31 | * 32 | * @param $name 33 | * @return \Bakery\Types\AttachUnionEntityInputType 34 | */ 35 | public function setName($name): self 36 | { 37 | $this->name = 'Attach'.$name.'Input'; 38 | 39 | return $this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Types/CollectionFilterType.php: -------------------------------------------------------------------------------- 1 | modelSchema->typename().'Filter'; 41 | } 42 | 43 | /** 44 | * Return the fields for the collection filter type. 45 | * 46 | * @return array 47 | */ 48 | public function fields(): array 49 | { 50 | $fields = collect() 51 | ->merge($this->getScalarFilters()) 52 | ->merge($this->getRelationFilters()) 53 | ->put('AND', $this->registry->field($this->registry->type($this->name()))->list()) 54 | ->put('OR', $this->registry->field($this->registry->type($this->name()))->list()); 55 | 56 | return $fields->map(function (Field $type) { 57 | return $type->nullable()->nullableItems(); 58 | })->toArray(); 59 | } 60 | 61 | /** 62 | * Return the filters for the scalar fields. 63 | * 64 | * @return \Illuminate\Support\Collection 65 | */ 66 | protected function getScalarFilters(): Collection 67 | { 68 | $fields = $this->modelSchema->getFields(); 69 | 70 | return $fields->reject(function (Field $field) { 71 | return $field instanceof PolymorphicField; 72 | })->keys()->reduce(function (Collection $result, string $name) use ($fields) { 73 | $field = $fields->get($name); 74 | 75 | return $field->getType()->isLeafType() ? $result->merge($this->getFilters($name, $field)) : $result; 76 | }, collect()); 77 | } 78 | 79 | /** 80 | * Return the filters for the relation fields. 81 | * 82 | * @return \Illuminate\Support\Collection 83 | */ 84 | protected function getRelationFilters(): Collection 85 | { 86 | $fields = $this->modelSchema->getRelationFields(); 87 | 88 | return $fields->filter(function (Field $field) { 89 | return $field instanceof EloquentField; 90 | })->keys()->reduce(function (Collection $result, string $name) use ($fields) { 91 | $field = $fields->get($name); 92 | 93 | return $result->put($name, $this->registry->field($this->registry->type($field->name().'Filter'))); 94 | }, collect()); 95 | } 96 | 97 | /** 98 | * Return the filters for a field. 99 | * 100 | * @param string $name 101 | * @param \Bakery\Fields\Field $field 102 | * @return \Illuminate\Support\Collection 103 | */ 104 | protected function getFilters(string $name, Field $field): Collection 105 | { 106 | $fields = collect(); 107 | 108 | $type = $field->getType(); 109 | 110 | $fields->put($name, $this->registry->field($type)); 111 | 112 | foreach (self::$filters as $filter) { 113 | if (Str::endsWith($filter, 'In')) { 114 | $fields->put($name.$filter, $this->registry->field($type)->list()); 115 | } else { 116 | $fields->put($name.$filter, $this->registry->field($type)); 117 | } 118 | } 119 | 120 | return $fields; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Types/CollectionOrderByType.php: -------------------------------------------------------------------------------- 1 | modelSchema->typename().'OrderBy'; 19 | } 20 | 21 | /** 22 | * Return the fields for the collection order by type. 23 | * 24 | * @return array 25 | */ 26 | public function fields(): array 27 | { 28 | $fields = collect(); 29 | 30 | foreach ($this->modelSchema->getFields() as $name => $field) { 31 | $fields->put($name, $this->registry->field('Order')->nullable()); 32 | } 33 | 34 | $this->modelSchema->getRelationFields()->filter(function (Field $field) { 35 | return $field instanceof EloquentField; 36 | })->each(function (EloquentField $field, $relation) use ($fields) { 37 | $fields->put($relation, $this->registry->field($field->name().'OrderBy')->nullable()); 38 | }); 39 | 40 | return $fields->toArray(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Types/CollectionRootSearchType.php: -------------------------------------------------------------------------------- 1 | modelSchema->typename().'RootSearch'; 17 | } 18 | 19 | /** 20 | * Return the fields for the collection filter type. 21 | * 22 | * @return array 23 | */ 24 | public function fields(): array 25 | { 26 | return [ 27 | 'query' => $this->registry->field($this->registry->string())->nullable(), 28 | 'fields' => $this->registry->field($this->modelSchema->typename().'Search'), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Types/CollectionSearchType.php: -------------------------------------------------------------------------------- 1 | modelSchema->typename().'Search'; 19 | } 20 | 21 | /** 22 | * Return the fields for the collection filter type. 23 | * 24 | * @return array 25 | */ 26 | public function fields(): array 27 | { 28 | $fields = $this->modelSchema->getSearchableFields()->map(function (Field $field) { 29 | return $this->registry->field($this->registry->boolean())->nullable(); 30 | }); 31 | 32 | $relations = $this->modelSchema->getSearchableRelationFields()->map(function (EloquentField $field) { 33 | $searchTypeName = $field->getName().'Search'; 34 | 35 | return $this->registry->field($searchTypeName)->nullable(); 36 | }); 37 | 38 | return $fields->merge($relations)->toArray(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Types/Concerns/InteractsWithPivot.php: -------------------------------------------------------------------------------- 1 | relation = $relation; 42 | $this->pivotParent = $relation->getParent(); 43 | $this->pivotRelationName = $relation->getRelationName(); 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * Get the pivot relation for a model. 50 | * 51 | * @return BelongsToMany 52 | */ 53 | protected function getRelation(): BelongsToMany 54 | { 55 | if (isset($this->relation)) { 56 | return $this->relation; 57 | } 58 | 59 | return $this->relation = $this->model->{$this->pivotRelationName}(); 60 | } 61 | 62 | /** 63 | * @return \Bakery\Eloquent\ModelSchema 64 | */ 65 | public function getPivotModelSchema() 66 | { 67 | if (isset($this->pivotModelSchema)) { 68 | return $this->pivotModelSchema; 69 | } 70 | 71 | if ($this->registry->hasSchemaForModel($this->relation->getPivotClass())) { 72 | $this->pivotModelSchema = $this->registry->resolveSchemaForModel($this->relation->getPivotClass()); 73 | 74 | return $this->pivotModelSchema; 75 | } 76 | 77 | return null; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Types/Concerns/InteractsWithPolymorphism.php: -------------------------------------------------------------------------------- 1 | modelSchemas = $modelSchemas; 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * Get the model schemas of the polymorphic type. 36 | * 37 | * @return \Illuminate\Support\Collection 38 | */ 39 | public function getModelSchemas(): Collection 40 | { 41 | Utils::invariant( 42 | ! empty($this->modelSchemas), 43 | 'No model schemas defined on "'.get_class($this).'"' 44 | ); 45 | 46 | return collect($this->modelSchemas)->map(function (string $class) { 47 | return $this->registry->getModelSchema($class); 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Types/CreateInputType.php: -------------------------------------------------------------------------------- 1 | modelSchema->typename().'Input'; 19 | } 20 | 21 | /** 22 | * Return the fields for the Create Input BakeField. 23 | * 24 | * @return array 25 | */ 26 | public function fields(): array 27 | { 28 | $fields = array_merge( 29 | $this->getFillableFields()->toArray(), 30 | $this->getRelationFields()->toArray() 31 | ); 32 | 33 | Utils::invariant( 34 | count($fields) > 0, 35 | 'There are no fillable fields defined for '.class_basename($this->model).'. '. 36 | 'Make sure that a mutable model has at least one fillable field.' 37 | ); 38 | 39 | return $fields; 40 | } 41 | 42 | /** 43 | * Get the fillable fields of the model. 44 | * 45 | * @return Collection 46 | */ 47 | protected function getFillableFields(): Collection 48 | { 49 | $fields = parent::getFillableFields(); 50 | $defaults = $this->model->getAttributes(); 51 | 52 | return $fields->map(function (Field $field, string $key) use ($defaults) { 53 | if (in_array($key, array_keys($defaults))) { 54 | return $field->nullable(); 55 | } 56 | 57 | return $field; 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Types/CreatePivotInputType.php: -------------------------------------------------------------------------------- 1 | getModelSchemas()->reduce(function (array $fields, ModelSchema $modelSchema) { 22 | $inputType = 'Create'.$modelSchema->typename().'Input'; 23 | 24 | $fields[Utils::single($modelSchema->typename())] = $this->registry->field($inputType)->nullable(); 25 | 26 | return $fields; 27 | }, []); 28 | } 29 | 30 | /** 31 | * Get the name of the Create Union Input BakeField. 32 | * 33 | * @param $name 34 | * @return \Bakery\Types\CreateUnionEntityInputType 35 | */ 36 | public function setName($name): self 37 | { 38 | $this->name = 'Create'.$name.'Input'; 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Types/CreateWithPivotInputType.php: -------------------------------------------------------------------------------- 1 | relation->getRelationName(); 19 | $parentSchema = $this->registry->getSchemaForModel($this->relation->getParent()); 20 | 21 | return 'Create'.Utils::typename($relation).'On'.$parentSchema->typename().'WithPivotInput'; 22 | } 23 | 24 | /** 25 | * Return the fields for the input type. 26 | * 27 | * @return array 28 | */ 29 | public function fields(): array 30 | { 31 | $fields = parent::fields(); 32 | 33 | $modelSchema = $this->getPivotModelSchema(); 34 | $accessor = $this->relation->getPivotAccessor(); 35 | $typename = 'Create'.$modelSchema->typename().'Input'; 36 | 37 | $fields = array_merge($fields, [ 38 | $accessor => $this->registry->field($typename)->nullable(), 39 | ]); 40 | 41 | Utils::invariant( 42 | count($fields) > 0, 43 | 'There are no fields defined for '.class_basename($this->model) 44 | ); 45 | 46 | return $fields; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Types/CustomEntityType.php: -------------------------------------------------------------------------------- 1 | name)) { 19 | return $this->name; 20 | } 21 | 22 | return Utils::typename(Str::before(class_basename($this), 'BakeField')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Types/Definitions/EloquentInputType.php: -------------------------------------------------------------------------------- 1 | modelSchema = $modelSchema; 33 | $this->model = $modelSchema->getModel(); 34 | } 35 | 36 | /** 37 | * Define the fields that should be serialized. 38 | * 39 | * @return array 40 | */ 41 | public function __sleep() 42 | { 43 | $fields = [ 44 | 'modelSchema', 45 | 'model', 46 | ]; 47 | 48 | return array_merge($fields, parent::__sleep()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Types/Definitions/EloquentType.php: -------------------------------------------------------------------------------- 1 | modelSchema = $modelSchema; 33 | $this->model = $this->modelSchema->getModel(); 34 | } 35 | 36 | /** 37 | * Define the fields that should be serialized. 38 | * 39 | * @return array 40 | */ 41 | public function __sleep() 42 | { 43 | $fields = [ 44 | 'modelSchema', 45 | 'model', 46 | ]; 47 | 48 | return array_merge($fields, parent::__sleep()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Types/Definitions/EnumType.php: -------------------------------------------------------------------------------- 1 | values(); 29 | 30 | return new BaseEnumType([ 31 | 'name' => $this->name(), 32 | 'values' => empty($values) ? null : $values, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Types/Definitions/InputType.php: -------------------------------------------------------------------------------- 1 | fields()); 20 | 21 | return $fields->map(function (Field $field) { 22 | return $field->getType()->toType(); 23 | }); 24 | } 25 | 26 | /** 27 | * Get the attributes for the type. 28 | * 29 | * @return array 30 | */ 31 | public function getAttributes(): array 32 | { 33 | return [ 34 | 'name' => $this->name(), 35 | 'fields' => [$this, 'resolveFields'], 36 | ]; 37 | } 38 | 39 | /** 40 | * Convert the Bakery type to a GraphQL type. 41 | * 42 | * @param array $options 43 | * @return GraphQLType 44 | */ 45 | public function toType(array $options = []): GraphQLType 46 | { 47 | return new InputObjectType($this->getAttributes()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Types/Definitions/InternalType.php: -------------------------------------------------------------------------------- 1 | fields; 27 | } 28 | 29 | /** 30 | * Get the fields for the type. 31 | * 32 | * @return Collection 33 | */ 34 | public function getFields(): Collection 35 | { 36 | $fields = collect($this->fields()); 37 | 38 | return $fields->map(function (Field $field) { 39 | return $field->toArray(); 40 | }); 41 | } 42 | 43 | /** 44 | * Get the attributes for the type. 45 | * 46 | * @return array 47 | */ 48 | public function getAttributes(): array 49 | { 50 | $attributes = [ 51 | 'name' => $this->name(), 52 | 'fields' => [$this, 'resolveFields'], 53 | ]; 54 | 55 | return $attributes; 56 | } 57 | 58 | /** 59 | * Resolve the fields. 60 | * 61 | * @return array 62 | */ 63 | public function resolveFields(): array 64 | { 65 | return $this->getFields()->toArray(); 66 | } 67 | 68 | /** 69 | * Convert the type to a GraphQL type. 70 | * 71 | * @return GraphQLType 72 | */ 73 | public function toType(): GraphQLType 74 | { 75 | return new GraphQLObjectType($this->getAttributes()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Types/Definitions/ReferenceType.php: -------------------------------------------------------------------------------- 1 | reference = $reference; 28 | } 29 | 30 | /** 31 | * @return \GraphQL\Type\Definition\NamedType 32 | * 33 | * @throws \Bakery\Exceptions\TypeNotFound 34 | */ 35 | public function getType(): GraphQLNamedType 36 | { 37 | if (isset($this->type)) { 38 | return $this->type; 39 | } 40 | 41 | $this->type = $this->getRegistry()->resolve($this->reference); 42 | 43 | return $this->type; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Types/Definitions/ScalarType.php: -------------------------------------------------------------------------------- 1 | $this->name(), 51 | 'serialize' => [$this, 'serialize'], 52 | 'parseValue' => [$this, 'parseValue'], 53 | 'parseLiteral' => [$this, 'parseLiteral'], 54 | ]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Types/Definitions/UnionType.php: -------------------------------------------------------------------------------- 1 | types)) { 33 | return $this->types; 34 | } 35 | 36 | return []; 37 | } 38 | 39 | /** 40 | * Define the type resolver. 41 | * 42 | * @param callable $resolver 43 | * @return $this 44 | */ 45 | public function typeResolver($resolver) 46 | { 47 | $this->typeResolver = $resolver; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Get the type resolver. 54 | * 55 | * @param $value 56 | * @param $context 57 | * @param \GraphQL\Type\Definition\ResolveInfo $info 58 | * @return callable 59 | */ 60 | public function getTypeResolver($value, $context, ResolveInfo $info) 61 | { 62 | if (isset($this->typeResolver)) { 63 | return call_user_func_array($this->typeResolver, [$value, $context, $info]); 64 | } 65 | 66 | return $this->resolveType($value, $context, $info); 67 | } 68 | 69 | /** 70 | * Get the attributes for the type. 71 | * 72 | * @return array 73 | */ 74 | public function getAttributes(): array 75 | { 76 | return [ 77 | 'name' => $this->name(), 78 | 'types' => collect($this->types())->map(function (RootType $type) { 79 | return $type->toNamedType(); 80 | })->toArray(), 81 | 'resolveType' => [$this, 'getTypeResolver'], 82 | ]; 83 | } 84 | 85 | /** 86 | * Convert the Bakery type to a GraphQL type. 87 | * 88 | * @return GraphQLType 89 | */ 90 | public function toType(): GraphQLType 91 | { 92 | return new GraphQLUnionType($this->getAttributes()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Types/EloquentMutationInputType.php: -------------------------------------------------------------------------------- 1 | modelSchema->getFillableFields() 30 | ->filter(function (Field $field) { 31 | return ! $field instanceof PolymorphicField && $field->setRegistry($this->registry)->getType()->isLeafType(); 32 | }); 33 | } 34 | 35 | /** 36 | * Get the fields for the relations of the model. 37 | * 38 | * @return Collection 39 | */ 40 | protected function getRelationFields(): Collection 41 | { 42 | $relations = $this->modelSchema->getFillableRelationFields(); 43 | 44 | return $relations->keys()->reduce(function (Collection $fields, $key) use ($relations) { 45 | $field = $relations[$key]; 46 | 47 | if ($field instanceof EloquentField) { 48 | return $fields->merge($this->getFieldsForRelation($key, $field)); 49 | } elseif ($field instanceof PolymorphicField) { 50 | return $fields->merge($this->getFieldsForPolymorphicRelation($key, $field)); 51 | } 52 | 53 | return $fields; 54 | }, collect()); 55 | } 56 | 57 | /** 58 | * Set the relation fields. 59 | * 60 | * @param string $relation 61 | * @param EloquentField $root 62 | * @return Collection 63 | */ 64 | protected function getFieldsForRelation(string $relation, EloquentField $root): Collection 65 | { 66 | $fields = collect(); 67 | $root->setRegistry($this->registry); 68 | $inputType = 'Create'.$root->getName().'Input'; 69 | $relationship = $root->getRelation($this->model); 70 | 71 | if ($root->isList()) { 72 | $name = Str::singular($relation).'Ids'; 73 | $field = $this->registry->field($this->registry->ID())->list()->nullable()->accessor($relation); 74 | $fields->put($name, $field); 75 | 76 | if ($this->registry->hasType($inputType)) { 77 | $field = $this->registry->field($inputType)->list()->nullable()->accessor($relation); 78 | $fields->put($relation, $field); 79 | } 80 | } else { 81 | $name = Str::singular($relation).'Id'; 82 | $field = $this->registry->field($this->registry->ID())->nullable()->accessor($relation); 83 | $fields->put($name, $field); 84 | 85 | if ($this->registry->hasType($inputType)) { 86 | $field = $this->registry->field($inputType)->nullable()->accessor($relation); 87 | $fields->put($relation, $field); 88 | } 89 | } 90 | 91 | if ($relationship instanceof BelongsToMany) { 92 | $fields = $fields->merge($this->getFieldsForPivot($root, $relationship)); 93 | } 94 | 95 | return $fields; 96 | } 97 | 98 | /** 99 | * Get the polymorphic relation fields. 100 | * 101 | * @param string $relation 102 | * @param \Bakery\Fields\PolymorphicField $field 103 | * @return Collection 104 | */ 105 | protected function getFieldsForPolymorphicRelation(string $relation, PolymorphicField $field) 106 | { 107 | $fields = collect(); 108 | $typename = Utils::typename($relation).'On'.$this->modelSchema->typename(); 109 | $createInputType = 'Create'.$typename.'Input'; 110 | $attachInputType = 'Attach'.$typename.'Input'; 111 | 112 | if ($this->registry->hasType($createInputType)) { 113 | if ($field->isList()) { 114 | $fields->put($relation, $this->registry->field($createInputType)->list()->nullable()); 115 | $fields->put($relation.'Ids', $this->registry->field($attachInputType)->list()->nullable()); 116 | } else { 117 | $fields->put($relation, $this->registry->field($createInputType)->nullable()); 118 | $fields->put($relation.'Id', $this->registry->field($attachInputType)->nullable()); 119 | } 120 | } 121 | 122 | return $fields; 123 | } 124 | 125 | /** 126 | * Get the fields for a pivot relation. 127 | * 128 | * @param \Bakery\Fields\EloquentField $field 129 | * @param \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation 130 | * @return Collection 131 | */ 132 | protected function getFieldsForPivot(EloquentField $field, BelongsToMany $relation): Collection 133 | { 134 | $fields = collect(); 135 | $pivot = $relation->getPivotClass(); 136 | $relationName = $relation->getRelationName(); 137 | 138 | if (! $this->registry->hasSchemaForModel($pivot)) { 139 | return collect(); 140 | } 141 | 142 | $inputType = 'Create'.Utils::typename($relationName).'On'.$this->modelSchema->typename().'WithPivotInput'; 143 | $fields->put($relationName, $this->registry->field($inputType)->list()->nullable()); 144 | 145 | $name = Str::singular($relationName).'Ids'; 146 | $inputType = Utils::pluralTypename($relationName).'PivotInput'; 147 | $fields->put($name, $this->registry->field($inputType)->list()->nullable()); 148 | 149 | return $fields; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Types/EntityCollectionType.php: -------------------------------------------------------------------------------- 1 | modelSchema->typename().'Collection'; 18 | } 19 | 20 | /** 21 | * Return the fields for the entity collection type. 22 | * 23 | * @return array 24 | */ 25 | public function fields(): array 26 | { 27 | return [ 28 | 'pagination' => $this->registry->field('Pagination')->resolve([$this, 'resolvePaginationField']), 29 | 'items' => $this->registry->field($this->modelSchema->typename())->list()->resolve([$this, 'resolveItemsField']), 30 | ]; 31 | } 32 | 33 | /** 34 | * Resolve the items field. 35 | * 36 | * @param LengthAwarePaginator $paginator 37 | * @return array 38 | */ 39 | public function resolveItemsField(LengthAwarePaginator $paginator): array 40 | { 41 | return $paginator->items(); 42 | } 43 | 44 | /** 45 | * Resolve the pagination field. 46 | * 47 | * @param LengthAwarePaginator $paginator 48 | * @return array 49 | */ 50 | public function resolvePaginationField(LengthAwarePaginator $paginator): array 51 | { 52 | $lastPage = $paginator->lastPage(); 53 | $currentPage = $paginator->currentPage(); 54 | $previousPage = $currentPage > 1 ? $currentPage - 1 : null; 55 | $nextPage = $lastPage > $currentPage ? $currentPage + 1 : null; 56 | 57 | return [ 58 | 'last_page' => $lastPage, 59 | 'current_page' => $currentPage, 60 | 'next_page' => $nextPage, 61 | 'previous_page' => $previousPage, 62 | 'per_page' => $paginator->perPage(), 63 | 'total' => $paginator->total(), 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Types/EntityLookupType.php: -------------------------------------------------------------------------------- 1 | modelSchema->typename().'LookupType'; 17 | } 18 | 19 | /** 20 | * Define the fields for the entity lookup type. 21 | * 22 | * @return array 23 | */ 24 | public function fields(): array 25 | { 26 | return $this->modelSchema->getLookupFields()->toArray(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Types/OrderType.php: -------------------------------------------------------------------------------- 1 | $this->registry->field($this->registry->int()), 15 | 'per_page' => $this->registry->field($this->registry->int()), 16 | 'current_page' => $this->registry->field($this->registry->int()), 17 | 'last_page' => $this->registry->field($this->registry->int()), 18 | 'next_page' => $this->registry->field($this->registry->int())->nullable(), 19 | 'previous_page' => $this->registry->field($this->registry->int())->nullable(), 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Types/PivotInputType.php: -------------------------------------------------------------------------------- 1 | pivotRelationName; 19 | $typename = Utils::pluralTypename($relation); 20 | 21 | return $typename.'PivotInput'; 22 | } 23 | 24 | /** 25 | * Return the fields for the input type. 26 | * 27 | * @return array 28 | */ 29 | public function fields(): array 30 | { 31 | $modelSchema = $this->getPivotModelSchema(); 32 | $accessor = $this->relation->getPivotAccessor(); 33 | $relatedKey = $this->relation->getRelated()->getKeyName(); 34 | 35 | $fields = collect()->put($relatedKey, $this->registry->field($this->registry->ID())); 36 | 37 | if ($modelSchema) { 38 | $fields->put($accessor, $this->registry->field('Create'.$modelSchema->typename().'Input')->nullable()); 39 | } 40 | 41 | return $fields->toArray(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Types/UnionEntityType.php: -------------------------------------------------------------------------------- 1 | modelSchemas)->map(function ($modelSchema) { 22 | return $modelSchema instanceof RootType 23 | ? $modelSchema 24 | : $this->registry->type($this->registry->getModelSchema($modelSchema)->typename()); 25 | })->toArray(); 26 | } 27 | 28 | /** 29 | * Receives $value from resolver of the parent field and returns concrete Object BakeField for this $value. 30 | * 31 | * @param $value 32 | * @param $context 33 | * @param ResolveInfo $info 34 | * @return mixed 35 | * 36 | * @throws \Bakery\Exceptions\TypeNotFound 37 | */ 38 | public function resolveType($value, $context, ResolveInfo $info) 39 | { 40 | return $this->registry->resolve($this->registry->getSchemaForModel($value)->typename()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Types/UpdateInputType.php: -------------------------------------------------------------------------------- 1 | modelSchema->typename().'Input'; 19 | } 20 | 21 | /** 22 | * Return the fields for theUpdate Input BakeField. 23 | * 24 | * @return array 25 | */ 26 | public function fields(): array 27 | { 28 | $fields = array_merge( 29 | $this->getFillableFields()->toArray(), 30 | $this->getRelationFields()->toArray() 31 | ); 32 | 33 | Utils::invariant( 34 | count($fields) > 0, 35 | 'There are no fillable fields defined for '.class_basename($this->model).'. '. 36 | 'Make sure that a mutable model has at least one fillable field.' 37 | ); 38 | 39 | return $fields; 40 | } 41 | 42 | /** 43 | * Get the fillable fields of the model. 44 | * 45 | * Updating in Bakery works like PATCH you only have to pass in 46 | * the values you want to update. The rest stays untouched. 47 | * Because of that we have to remove the nonNull wrappers on the fields. 48 | * 49 | * @return Collection 50 | */ 51 | protected function getFillableFields(): Collection 52 | { 53 | return parent::getFillableFields()->map(function (Field $field) { 54 | return $field->nullable(); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Utils/Utils.php: -------------------------------------------------------------------------------- 1 | model->getPerPage(); 21 | 22 | $countColumns = $this->query->distinct ? [$this->model->getQualifiedKeyName()] : ['*']; 23 | 24 | $results = ($total = $this->toBase()->getCountForPagination($countColumns)) 25 | ? $this->forPage($page, $perPage)->get($columns) 26 | : $this->model->newCollection(); 27 | 28 | return $this->paginator($results, $total, $perPage, $page, [ 29 | 'path' => Paginator::resolveCurrentPath(), 30 | 'pageName' => $pageName, 31 | ]); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/BakeryServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(app()->bound(Bakery::class)); 13 | } 14 | 15 | /** @test */ 16 | public function it_resolves_as_singleton() 17 | { 18 | $this->assertSame(resolve(Bakery::class), resolve(Bakery::class)); 19 | } 20 | 21 | /** @test */ 22 | public function it_loads_the_config() 23 | { 24 | $config = app()->config['bakery']; 25 | $this->assertNotEmpty($config); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/CacheTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete(); 15 | $schema = new DefaultSchema(); 16 | $schema = serialize($schema); 17 | $schema = unserialize($schema); 18 | $this->assertNull($schema->toGraphQLSchema()->assertValid()); 19 | } 20 | 21 | /** @test */ 22 | public function it_can_execute_a_query_on_a_cached_schema() 23 | { 24 | $this->markTestIncomplete(); 25 | $schema = new DefaultSchema(); 26 | $schema = serialize($schema); 27 | $schema = unserialize($schema); 28 | $schema = $schema->toGraphQLSchema(); 29 | 30 | $article = factory(Article::class)->create(); 31 | 32 | $query = ' 33 | query { 34 | article(id: "'.$article->id.'") { 35 | id 36 | } 37 | } 38 | '; 39 | 40 | $response = GraphQL::executeQuery($schema, $query)->toArray(); 41 | $this->assertEquals($article->id, $response['data']['article']['id']); 42 | } 43 | 44 | /** @test */ 45 | public function it_can_handle_variables_of_internal_types_on_a_cached_schema() 46 | { 47 | $this->markTestIncomplete(); 48 | $schema = new DefaultSchema(); 49 | $schema = serialize($schema); 50 | $schema = unserialize($schema); 51 | $schema = $schema->toGraphQLSchema(); 52 | 53 | $article = factory(Article::class)->create(); 54 | 55 | $query = ' 56 | query($id: ID!) { 57 | article(id: $id) { 58 | id 59 | } 60 | } 61 | '; 62 | 63 | $variables = [ 64 | 'id' => $article->id, 65 | ]; 66 | 67 | $response = GraphQL::executeQuery($schema, $query, null, null, $variables)->toArray(); 68 | $this->assertEquals($article->id, $response['data']['article']['id']); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Factories/ArticleFactory.php: -------------------------------------------------------------------------------- 1 | define(Bakery\Tests\Fixtures\Models\Article::class, function (Faker\Generator $faker) { 16 | return [ 17 | 'user_id' => factory(Bakery\Tests\Fixtures\Models\User::class), 18 | 'title' => $faker->word, 19 | 'slug' => $faker->unique()->slug, 20 | 'content' => $faker->text, 21 | ]; 22 | }); 23 | -------------------------------------------------------------------------------- /tests/Factories/CommentFactory.php: -------------------------------------------------------------------------------- 1 | define(Bakery\Tests\Fixtures\Models\Comment::class, function (Faker\Generator $faker) { 16 | return [ 17 | 'commentable_id' => factory(Bakery\Tests\Fixtures\Models\Article::class), 18 | 'commentable_type' => Bakery\Tests\Fixtures\Models\Article::class, 19 | 'author_id' => factory(Bakery\Tests\Fixtures\Models\User::class), 20 | 'body' => $faker->sentence, 21 | ]; 22 | }); 23 | -------------------------------------------------------------------------------- /tests/Factories/PhoneFactory.php: -------------------------------------------------------------------------------- 1 | define(Bakery\Tests\Fixtures\Models\Phone::class, function (Faker\Generator $faker) { 16 | return [ 17 | 'number' => $faker->phoneNumber, 18 | 'user_id' => factory(Bakery\Tests\Fixtures\Models\User::class), 19 | ]; 20 | }); 21 | -------------------------------------------------------------------------------- /tests/Factories/RoleFactory.php: -------------------------------------------------------------------------------- 1 | define(Bakery\Tests\Fixtures\Models\Role::class, function (Faker\Generator $faker) { 16 | return [ 17 | 'name' => $faker->name, 18 | ]; 19 | }); 20 | -------------------------------------------------------------------------------- /tests/Factories/TagFactory.php: -------------------------------------------------------------------------------- 1 | define(Bakery\Tests\Fixtures\Models\Tag::class, function (Faker\Generator $faker) { 16 | return [ 17 | 'name' => $faker->word, 18 | ]; 19 | }); 20 | -------------------------------------------------------------------------------- /tests/Factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(Bakery\Tests\Fixtures\Models\User::class, function (Faker\Generator $faker) { 16 | return [ 17 | 'name' => $faker->name, 18 | 'email' => $faker->unique()->safeEmail, 19 | 'password' => $faker->password, 20 | 'restricted' => 'Yes', 21 | ]; 22 | }); 23 | -------------------------------------------------------------------------------- /tests/Feature/AttachPivotMutationTest.php: -------------------------------------------------------------------------------- 1 | create(); 17 | $this->actingAs($user); 18 | 19 | $article = factory(Article::class)->create(); 20 | $tags = factory(Tag::class, 2)->create(); 21 | 22 | $query = ' 23 | mutation { 24 | attachTagsOnArticle(id: "'.$article->id.'", input: [ 25 | "'.$tags[0]->id.'" 26 | "'.$tags[1]->id.'" 27 | ]) { 28 | id 29 | } 30 | } 31 | '; 32 | 33 | $response = $this->json('GET', '/graphql', ['query' => $query]); 34 | $response->assertJsonFragment(['id' => $article->id]); 35 | $this->assertDatabaseHas('taggables', ['taggable_id' => '1', 'tag_id' => '1']); 36 | $this->assertDatabaseHas('taggables', ['taggable_id' => '1', 'tag_id' => '2']); 37 | } 38 | 39 | /** @test */ 40 | public function it_lets_you_attach_pivot_ids_with_missing_pivot_data() 41 | { 42 | $user = factory(User::class)->create(); 43 | $role = factory(Role::class)->create(); 44 | $this->actingAs($user); 45 | 46 | $query = ' 47 | mutation { 48 | attachRolesOnUser(id: "'.$user->id.'", input: [ 49 | { id: "'.$role->id.'" } 50 | ]) { 51 | id 52 | } 53 | } 54 | '; 55 | 56 | $response = $this->json('GET', '/graphql', ['query' => $query]); 57 | $response->assertJsonFragment(['id' => $user->id]); 58 | $this->assertDatabaseHas('role_user', [ 59 | 'user_id' => '1', 60 | 'role_id' => '1', 61 | ]); 62 | } 63 | 64 | /** @test */ 65 | public function it_lets_you_attach_pivot_ids_with_pivot_data() 66 | { 67 | $user = factory(User::class)->create(); 68 | $role = factory(Role::class)->create(); 69 | $this->actingAs($user); 70 | 71 | $query = ' 72 | mutation { 73 | attachRolesOnUser(id: "'.$user->id.'", input: [ 74 | { id: "'.$role->id.'", pivot: { admin: true } } 75 | ]) { 76 | id 77 | } 78 | } 79 | '; 80 | 81 | $response = $this->json('GET', '/graphql', ['query' => $query]); 82 | $response->assertJsonFragment(['id' => $user->id]); 83 | $this->assertDatabaseHas('role_user', [ 84 | 'user_id' => '1', 85 | 'role_id' => '1', 86 | 'admin' => true, 87 | ]); 88 | } 89 | 90 | /** @test */ 91 | public function it_lets_you_attach_pivot_ids_with_pivot_relation_data() 92 | { 93 | $user = factory(User::class)->create(); 94 | $role = factory(Role::class)->create(); 95 | $tag = factory(Tag::class)->create(); 96 | $this->actingAs($user); 97 | 98 | $query = ' 99 | mutation { 100 | attachRolesOnUser(id: "'.$user->id.'", input: [ 101 | { id: "'.$role->id.'", pivot: {tagId: "'.$tag->id.'" } } 102 | ]) { 103 | id 104 | } 105 | } 106 | '; 107 | 108 | $response = $this->json('GET', '/graphql', ['query' => $query]); 109 | $response->assertJsonFragment(['id' => $user->id]); 110 | $this->assertDatabaseHas('role_user', [ 111 | 'user_id' => '1', 112 | 'role_id' => '1', 113 | 'tag_id' => '1', 114 | ]); 115 | } 116 | 117 | /** @test */ 118 | public function it_lets_you_attach_pivot_with_create() 119 | { 120 | $user = factory(User::class)->create(); 121 | $role = factory(Role::class)->create(); 122 | $this->actingAs($user); 123 | 124 | $query = ' 125 | mutation { 126 | attachRolesOnUser(id: "'.$user->id.'", input: [ 127 | { id: "'.$role->id.'", pivot: {tag: {name: "foobar"} } } 128 | ]) { 129 | id 130 | } 131 | } 132 | '; 133 | 134 | $response = $this->json('GET', '/graphql', ['query' => $query]); 135 | $response->assertJsonFragment(['id' => $user->id]); 136 | $this->assertDatabaseHas('role_user', [ 137 | 'user_id' => '1', 138 | 'role_id' => '1', 139 | 'tag_id' => '1', 140 | ]); 141 | 142 | $this->assertDatabaseHas('tags', [ 143 | 'name' => 'foobar', 144 | ]); 145 | } 146 | 147 | /** @test */ 148 | public function it_lets_you_attach_pivot_ids_with_pivot_data_inversed() 149 | { 150 | $user = factory(User::class)->create(); 151 | $role = factory(Role::class)->create(); 152 | $this->actingAs($user); 153 | 154 | $query = ' 155 | mutation { 156 | attachUsersOnRole(id: "'.$role->id.'", input: [ 157 | { id: "'.$user->id.'", pivot: { admin: true } } 158 | ]) { 159 | id 160 | } 161 | } 162 | '; 163 | 164 | $response = $this->json('GET', '/graphql', ['query' => $query]); 165 | $response->assertJsonFragment(['id' => $user->id]); 166 | $this->assertDatabaseHas('role_user', [ 167 | 'user_id' => '1', 168 | 'role_id' => '1', 169 | 'admin' => true, 170 | ]); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /tests/Feature/CustomMutationTest.php: -------------------------------------------------------------------------------- 1 | authenticate(); 19 | $this->graphql('mutation($input: InviteUserInput!) { inviteUser(input: $input) }', [ 20 | 'input' => [ 21 | 'email' => 'john@example.com', 22 | ], 23 | ]); 24 | 25 | $user = User::first(); 26 | $this->assertEquals('john@example.com', $user->email); 27 | } 28 | 29 | /** @test */ 30 | public function it_validates_a_custom_mutation() 31 | { 32 | $this->authenticate(); 33 | $this->withExceptionHandling(); 34 | $this->graphql('mutation($input: InviteUserInput!) { inviteUser(input: $input) }', [ 35 | 'input' => [ 36 | 'email' => 'invalid-email', 37 | ], 38 | ])->assertJsonFragment([ 39 | 'message' => 'The email must be a valid email address.', 40 | 'validation' => [ 41 | 'input.email' => ['The email must be a valid email address.'], 42 | ], 43 | ]); 44 | 45 | $this->assertDatabaseMissing('users', [ 46 | 'email' => 'invalid-email', 47 | ]); 48 | } 49 | 50 | /** @test */ 51 | public function it_checks_authorization() 52 | { 53 | $this->withExceptionHandling() 54 | ->graphql('mutation($input: InviteUserInput!) { inviteUser(input: $input) }', [ 55 | 'input' => ['email' => 'john@example.com'], 56 | ])->assertJsonFragment(['message' => 'This action is unauthorized.']); 57 | 58 | $this->assertDatabaseMissing('users', [ 59 | 'email' => 'invalid-email', 60 | ]); 61 | } 62 | 63 | /** @test */ 64 | public function it_checks_authorization_and_shows_custom_message() 65 | { 66 | $_SERVER['graphql.inviteUser.authorize'] = 'You need to be logged in to do this!'; 67 | 68 | $this->withExceptionHandling() 69 | ->graphql('mutation($input: InviteUserInput!) { inviteUser(input: $input) }', [ 70 | 'input' => ['email' => 'fail@example.com'], 71 | ])->assertJsonFragment(['message' => 'You need to be logged in to do this!']); 72 | 73 | $this->assertDatabaseMissing('users', [ 74 | 'email' => 'invalid-email', 75 | ]); 76 | 77 | unset($_SERVER['graphql.inviteUser.authorize']); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Feature/DeleteMutationTest.php: -------------------------------------------------------------------------------- 1 | authenticate(); 17 | } 18 | 19 | /** @test */ 20 | public function it_can_delete_models() 21 | { 22 | $user = factory(User::class)->create(); 23 | 24 | $this->graphql('mutation($id: ID!) { deleteUser(id: $id) }', [ 25 | 'id' => $user->id, 26 | ]); 27 | 28 | $user = User::first(); 29 | $this->assertNull($user); 30 | } 31 | 32 | /** @test */ 33 | public function it_cant_update_models_if_the_model_has_no_policy() 34 | { 35 | Gate::policy(User::class, null); 36 | 37 | $user = factory(User::class)->create(); 38 | 39 | $this->withExceptionHandling()->graphql('mutation($id: ID!) { deleteUser(id: $id) }', [ 40 | 'id' => $user->id, 41 | ]); 42 | 43 | $user = User::first(); 44 | $this->assertNotNull($user); 45 | } 46 | 47 | /** @test */ 48 | public function it_cant_update_models_if_not_authorized() 49 | { 50 | $_SERVER['graphql.user.deletable'] = false; 51 | 52 | $user = factory(User::class)->create(); 53 | 54 | $this->withExceptionHandling()->graphql('mutation($id: ID!) { deleteUser(id: $id) }', [ 55 | 'id' => $user->id, 56 | ]); 57 | 58 | unset($_SERVER['graphql.user.deletable']); 59 | 60 | $user = User::first(); 61 | $this->assertNotNull($user); 62 | } 63 | 64 | /** @test */ 65 | public function it_throws_too_many_results_exception_when_lookup_is_not_specific_enough() 66 | { 67 | factory(Article::class, 2)->create([ 68 | 'slug' => 'hello-world', 69 | ]); 70 | 71 | $response = $this->withExceptionHandling()->graphql('mutation($slug: String!) { deleteArticle(slug: $slug) }', [ 72 | 'slug' => 'hello-world', 73 | ]); 74 | 75 | $response->assertJsonFragment(['message' => 'Too many results for model [Bakery\Tests\Fixtures\Models\Article]']); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Feature/GraphQLControllerTest.php: -------------------------------------------------------------------------------- 1 | json('GET', '/graphql'); 14 | $response->assertStatus(200); 15 | } 16 | 17 | /** @test */ 18 | public function it_returns_the_introspection() 19 | { 20 | $query = ' 21 | query { 22 | __schema { 23 | types { 24 | name 25 | } 26 | } 27 | } 28 | '; 29 | 30 | DB::enableQueryLog(); 31 | $response = $this->json('GET', '/graphql', ['query' => $query]); 32 | DB::disableQueryLog(); 33 | 34 | $this->assertCount(0, DB::getQueryLog()); 35 | $response->assertStatus(200); 36 | $response->assertJsonStructure([ 37 | 'data' => [ 38 | '__schema' => [ 39 | 'types', 40 | ], 41 | ], 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Feature/GraphiQLControllerTest.php: -------------------------------------------------------------------------------- 1 | expectException(NotFoundHttpException::class); 14 | 15 | $response = $this->get('graphql/explore'); 16 | $response->assertStatus(404); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Fixtures/IntegrationTestSchema.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 17 | } 18 | 19 | public function comments() 20 | { 21 | return $this->morphMany(Comment::class, 'commentable'); 22 | } 23 | 24 | public function tags() 25 | { 26 | return $this->morphToMany(Tag::class, 'taggable'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Fixtures/Models/Comment.php: -------------------------------------------------------------------------------- 1 | morphTo(); 14 | } 15 | 16 | public function author() 17 | { 18 | return $this->belongsTo(User::class, 'author_id'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Fixtures/Models/Phone.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Fixtures/Models/Role.php: -------------------------------------------------------------------------------- 1 | belongsToMany(User::class) 14 | ->as($_SERVER['eloquent.role.users.pivot'] ?? 'pivot') 15 | ->using(UserRole::class) 16 | ->withPivot(['admin']) 17 | ->withTimestamps(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Fixtures/Models/Tag.php: -------------------------------------------------------------------------------- 1 | morphedByMany(Article::class, 'taggable'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Fixtures/Models/User.php: -------------------------------------------------------------------------------- 1 | false, 11 | ]; 12 | 13 | protected $keyType = 'string'; 14 | 15 | public function phone() 16 | { 17 | return $this->hasOne(Phone::class); 18 | } 19 | 20 | public function articles() 21 | { 22 | return $this->hasMany(Article::class); 23 | } 24 | 25 | public function comments() 26 | { 27 | return $this->hasMany(Comment::class); 28 | } 29 | 30 | public function roles() 31 | { 32 | return $this->belongsToMany(Role::class) 33 | ->as($_SERVER['eloquent.user.roles.pivot'] ?? 'pivot') 34 | ->using(UserRole::class) 35 | ->withPivot(['admin']) 36 | ->withTimestamps(); 37 | } 38 | 39 | public function rolesAlias() 40 | { 41 | return $this->roles(); 42 | } 43 | 44 | public function noPivotRoles() 45 | { 46 | return $this->belongsToMany(Role::class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Fixtures/Models/UserRole.php: -------------------------------------------------------------------------------- 1 | belongsTo(Tag::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Fixtures/Mutations/InviteUserMutation.php: -------------------------------------------------------------------------------- 1 | Type::of('InviteUserInput'), 31 | ]; 32 | } 33 | 34 | /** 35 | * Define the validation rules of the mutation. 36 | * 37 | * @return array 38 | */ 39 | public function rules() 40 | { 41 | return [ 42 | 'input.email' => 'email', 43 | ]; 44 | } 45 | 46 | /** 47 | * Get custom attributes for validator errors. 48 | * 49 | * @return array 50 | */ 51 | public function attributes() 52 | { 53 | return [ 54 | 'input.email' => 'email', 55 | ]; 56 | } 57 | 58 | /** 59 | * Define the authorization check for the mutation. 60 | */ 61 | public function authorize() 62 | { 63 | if (! auth()->user()) { 64 | return $this->deny($_SERVER['graphql.inviteUser.authorize'] ?? null); 65 | } 66 | 67 | return true; 68 | } 69 | 70 | public function resolve(Arguments $arguments) 71 | { 72 | $user = new User(); 73 | $user->name = Str::random(); 74 | $user->email = $arguments->input->email; 75 | $user->password = Str::random(); 76 | $user->save(); 77 | 78 | return true; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Fixtures/Policies/AllowAllPolicy.php: -------------------------------------------------------------------------------- 1 | Field::string()->unique(), 17 | 'title' => Field::string()->searchable(), 18 | 'name' => Field::string()->accessor('title')->searchable()->readOnly(), 19 | 'authorName' => Field::string()->with('user')->readOnly()->resolve(function (Article $article) { 20 | return $article->user->name; 21 | }), 22 | 'created_at' => Field::type('Timestamp')->readOnly(), 23 | 'createdAt' => Field::type('Timestamp')->accessor('created_at')->readOnly(), 24 | ]; 25 | } 26 | 27 | public function relations(): array 28 | { 29 | return [ 30 | 'user' => Field::model(UserSchema::class)->nullable()->searchable(), 31 | 'author' => Field::model(UserSchema::class)->nullable()->searchable()->accessor('user'), 32 | 'tags' => Field::collection(TagSchema::class), 33 | 'comments' => Field::collection(CommentSchema::class), 34 | 'remarks' => Field::collection(CommentSchema::class)->accessor('comments'), 35 | 'myComments' => Field::collection(CommentSchema::class)->relation(function (Article $article) { 36 | return $article->comments()->where('author_id', optional(auth()->user())->getAuthIdentifier()); 37 | }), 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Fixtures/Schemas/CommentSchema.php: -------------------------------------------------------------------------------- 1 | Field::string(), 17 | ]; 18 | } 19 | 20 | public function relations(): array 21 | { 22 | return [ 23 | 'author' => Field::model(UserSchema::class), 24 | 'commentable' => Field::polymorphic([ 25 | ArticleSchema::class, 26 | UserSchema::class, // Not really, just for testing. 27 | ]), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Fixtures/Schemas/PhoneSchema.php: -------------------------------------------------------------------------------- 1 | Field::string(), 19 | ]; 20 | } 21 | 22 | public function relations(): array 23 | { 24 | return [ 25 | 'user' => Field::model(UserSchema::class), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Fixtures/Schemas/RoleSchema.php: -------------------------------------------------------------------------------- 1 | Field::string(), 17 | ]; 18 | } 19 | 20 | public function relations(): array 21 | { 22 | return [ 23 | 'users' => Field::collection(UserSchema::class) 24 | ->inverse($_SERVER['eloquent.user.roles.inverseRelation'] ?? 'roles'), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Fixtures/Schemas/TagSchema.php: -------------------------------------------------------------------------------- 1 | Field::string(), 17 | ]; 18 | } 19 | 20 | public function relations(): array 21 | { 22 | return [ 23 | 'articles' => Field::collection(ArticleSchema::class), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Fixtures/Schemas/UserRoleSchema.php: -------------------------------------------------------------------------------- 1 | Field::boolean()->nullable(), 17 | ]; 18 | } 19 | 20 | public function relations(): array 21 | { 22 | return [ 23 | 'tag' => Field::model(TagSchema::class)->nullable(), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Fixtures/Schemas/UserSchema.php: -------------------------------------------------------------------------------- 1 | Field::string()->searchable(), 17 | 'email' => Field::string()->unique()->searchable(), 18 | 'admin' => Field::boolean()->canStoreWhen('setAdmin'), 19 | 'password' => Field::string()->canSee(function () { 20 | return $_SERVER['graphql.user.canSeePassword'] ?? true; 21 | }), 22 | 'restricted' => Field::string()->canSeeWhen('viewRestricted')->canStoreWhen('storeRestricted')->nullable(), 23 | ]; 24 | } 25 | 26 | public function relations(): array 27 | { 28 | return [ 29 | 'articles' => Field::collection(ArticleSchema::class)->canSee(function () { 30 | return $_SERVER['graphql.user.canSeeArticles'] ?? true; 31 | }), 32 | 'roles' => Field::collection(RoleSchema::class), 33 | 'noPivotRoles' => Field::collection(RoleSchema::class), 34 | 'phone' => Field::model(PhoneSchema::class), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Fixtures/Types/InviteUserInputType.php: -------------------------------------------------------------------------------- 1 | Field::string(), 14 | ]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Fixtures/Types/TimestampType.php: -------------------------------------------------------------------------------- 1 | toAtomString(); 28 | } 29 | throw new UserError(sprintf('Timestamp cannot represent non Carbon value: %s', Utils::printSafe($value))); 30 | } 31 | 32 | /** 33 | * Parses an externally provided value (query variable) to use as an input. 34 | * 35 | * @param mixed $value 36 | * @return mixed 37 | */ 38 | public function parseValue($value) 39 | { 40 | return Carbon::parse($value); 41 | } 42 | 43 | /** 44 | * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input. 45 | * 46 | * @param \GraphQL\Language\AST\Node $ast 47 | * @return mixed 48 | */ 49 | public function parseLiteral($ast) 50 | { 51 | if ($ast instanceof StringValueNode) { 52 | return Carbon::parse($ast->value); 53 | } 54 | 55 | return null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/IntegrationTest.php: -------------------------------------------------------------------------------- 1 | withoutExceptionHandling(); 46 | 47 | $this->loadMigrationsFrom(__DIR__.'/Migrations'); 48 | 49 | $this->withFactories(__DIR__.'/Factories'); 50 | 51 | $this->gate = resolve(Gate::class); 52 | $this->gate->policy(Models\User::class, Policies\UserPolicy::class); 53 | $this->gate->policy(Models\Role::class, Policies\RolePolicy::class); 54 | $this->gate->policy(Models\UserRole::class, Policies\UserRolePolicy::class); 55 | $this->gate->policy(Models\Article::class, Policies\ArticlePolicy::class); 56 | $this->gate->policy(Models\Phone::class, Policies\PhonePolicy::class); 57 | $this->gate->policy(Models\Comment::class, Policies\CommentPolicy::class); 58 | $this->gate->policy(Models\Tag::class, Policies\TagPolicy::class); 59 | 60 | // Disable policy name guessing for testing purposes. 61 | $this->gate->guessPolicyNamesUsing(function () { 62 | return null; 63 | }); 64 | } 65 | 66 | /** 67 | * Get package providers. 68 | * 69 | * @param \Illuminate\Foundation\Application $app 70 | * @return array 71 | */ 72 | protected function getPackageProviders($app) 73 | { 74 | return [ 75 | BakeryServiceProvider::class, 76 | ]; 77 | } 78 | 79 | /** 80 | * Define environment setup. 81 | * 82 | * @param \Illuminate\Foundation\Application $app 83 | * @return void 84 | */ 85 | protected function getEnvironmentSetUp($app) 86 | { 87 | $app['config']->set('bakery.schema', IntegrationTestSchema::class); 88 | 89 | $app['config']->set('database.default', 'sqlite'); 90 | 91 | $app['config']->set('database.connections.sqlite', [ 92 | 'driver' => 'sqlite', 93 | 'database' => ':memory:', 94 | 'prefix' => '', 95 | ]); 96 | } 97 | 98 | /** 99 | * Authenticate as an anonymous user. 100 | * 101 | * @return $this 102 | */ 103 | public function authenticate() 104 | { 105 | $this->user = Mockery::mock(Authenticatable::class); 106 | 107 | $this->actingAs($this->user); 108 | 109 | $this->user->shouldReceive('getAuthIdentifier')->andReturn(1); 110 | $this->user->shouldReceive('getKey')->andReturn(1); 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Visit the GraphQL endpoint. 117 | * 118 | * @param string $query 119 | * @param array|null $variables 120 | * @return \Illuminate\Foundation\Testing\TestResponse 121 | */ 122 | protected function graphql(string $query, array $variables = []) 123 | { 124 | return $this->postJson('/graphql', [ 125 | 'query' => $query, 126 | 'variables' => $variables, 127 | ]); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/Migrations/0000_00_00_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->string('password'); 21 | $table->boolean('admin'); 22 | $table->rememberToken(); 23 | $table->string('restricted')->default('Yes'); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('users'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Migrations/0000_00_00_000001_create_articles_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('title'); 19 | $table->string('slug'); 20 | $table->text('content')->nullable(); 21 | $table->unsignedInteger('user_id')->index()->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('articles'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Migrations/0000_00_00_000002_create_roles_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->timestamps(); 20 | }); 21 | 22 | Schema::create('role_user', function (Blueprint $table) { 23 | $table->increments('id'); 24 | $table->unsignedInteger('user_id'); 25 | $table->unsignedInteger('role_id'); 26 | $table->boolean('admin')->nullable(); 27 | $table->unsignedInteger('tag_id')->nullable(); 28 | $table->timestamps(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('roles'); 40 | Schema::dropIfExists('user_roles'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Migrations/0000_00_00_000003_create_comments_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('commentable_type')->nullable(); 19 | $table->unsignedInteger('commentable_id')->nullable(); 20 | $table->unsignedInteger('author_id')->index(); 21 | $table->text('body'); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('comments'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Migrations/0000_00_00_000004_create_tags_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->timestamps(); 20 | }); 21 | 22 | Schema::create('taggables', function (Blueprint $table) { 23 | $table->unsignedInteger('tag_id'); 24 | $table->morphs('taggable'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('tags'); 36 | Schema::dropIfExists('taggables'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Migrations/0000_00_00_000005_create_phones_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('user_id')->index(); 19 | $table->string('number')->unique(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('phones'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/MutationTest.php: -------------------------------------------------------------------------------- 1 | getRegistry()))->toArray(); 24 | 25 | $this->assertTrue(is_array($mutation)); 26 | } 27 | 28 | /** @test */ 29 | public function it_falls_back_to_generated_name_if_name_is_missing() 30 | { 31 | $schema = new Schema(); 32 | $mutation = (new CreateCustomMutation($schema->getRegistry()))->toArray(); 33 | 34 | $this->assertEquals($mutation['name'], 'createCustom'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/QueryTest.php: -------------------------------------------------------------------------------- 1 | getRegistry()))->toArray(); 24 | 25 | $this->assertTrue(is_array($query)); 26 | } 27 | 28 | /** @test */ 29 | public function it_falls_back_to_class_name_if_name_is_missing() 30 | { 31 | $schema = new DefaultSchema(); 32 | $query = (new CustomQuery($schema->getRegistry()))->toArray(); 33 | 34 | $this->assertEquals($query['name'], 'custom'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/SchemaTest.php: -------------------------------------------------------------------------------- 1 | DummyQuery::class, 44 | ]; 45 | 46 | protected $mutations = [ 47 | 'createDummyModel' => DummyMutation::class, 48 | ]; 49 | 50 | protected $types = [ 51 | 'DummyModelSchema' => DummyType::class, 52 | ]; 53 | } 54 | 55 | class SchemaTest extends IntegrationTest 56 | { 57 | /** @test */ 58 | public function it_throws_exception_when_there_are_no_query_fields_defined_in_a_schema() 59 | { 60 | $this->expectException(InvariantViolation::class); 61 | 62 | $schema = resolve(Schema::class); 63 | $schema->toGraphQLSchema(); 64 | } 65 | 66 | /** @test */ 67 | public function throw_exception_if_the_model_does_extend_the_correct_class() 68 | { 69 | $this->expectException(InvariantViolation::class); 70 | 71 | $schema = resolve(NotResourceSchema::class); 72 | $schema->toGraphQLSchema(); 73 | } 74 | 75 | /** @test */ 76 | public function it_registers_class_that_does_extend_the_correct_class() 77 | { 78 | $schema = resolve(InlineEloquentSchema::class); 79 | $schema->toGraphQLSchema(); 80 | $queries = $schema->getQueries(); 81 | 82 | $this->assertArrayHasKey('dummyModel', $queries); 83 | $this->assertArrayHasKey('dummyModels', $queries); 84 | } 85 | 86 | /** @test */ 87 | public function it_ignores_mutations_for_read_only_models() 88 | { 89 | $schema = resolve(ReadOnlySchema::class); 90 | $schema->toGraphQLSchema(); 91 | $mutations = $schema->getMutations(); 92 | $queries = $schema->getQueries(); 93 | 94 | $this->assertArrayHasKey('dummyModel', $queries); 95 | $this->assertArrayHasKey('dummyModels', $queries); 96 | $this->assertEmpty($mutations); 97 | } 98 | 99 | /** @test */ 100 | public function it_can_override_entity_queries() 101 | { 102 | $schema = resolve(TestSchema::class); 103 | $schema->toGraphQLSchema(); 104 | $queries = $schema->getQueries(); 105 | 106 | $this->assertInstanceOf(DummyQuery::class, $queries['dummyModel']); 107 | } 108 | 109 | /** @test */ 110 | public function it_builds_the_entity_mutations() 111 | { 112 | $schema = resolve(TestSchema::class); 113 | $schema->toGraphQLSchema(); 114 | $mutations = $schema->getMutations(); 115 | 116 | $this->assertArrayHasKey('createDummyModel', $mutations); 117 | $this->assertArrayHasKey('updateDummyModel', $mutations); 118 | $this->assertArrayHasKey('deleteDummyModel', $mutations); 119 | } 120 | 121 | /** @test */ 122 | public function it_can_override_entity_mutations() 123 | { 124 | $schema = resolve(TestSchema::class); 125 | $schema->toGraphQLSchema(); 126 | $mutations = $schema->getMutations(); 127 | 128 | $this->assertInstanceOf(DummyMutation::class, $mutations['createDummyModel']); 129 | } 130 | 131 | /** @test */ 132 | public function it_can_override_types() 133 | { 134 | $schema = resolve(TestSchema::class); 135 | $schema->toGraphQLSchema(); 136 | $types = $schema->getTypes(); 137 | 138 | $this->assertEquals(DummyType::class, $types['DummyModelSchema']); 139 | } 140 | 141 | /** @test */ 142 | public function it_validates_the_graphql_schema() 143 | { 144 | /** @var Schema $schema */ 145 | $schema = resolve(TestSchema::class); 146 | $schema = $schema->toGraphQLSchema(); 147 | 148 | $this->assertInstanceOf(GraphQLSchema::class, $schema); 149 | $schema->assertValid(); 150 | } 151 | 152 | /** @test */ 153 | public function it_can_load_the_model_schemas_from_a_given_directory() 154 | { 155 | $models = Schema::modelsIn(__DIR__.'/Fixtures/Schemas', 'Bakery\\Tests\\Fixtures\\Schemas\\'); 156 | 157 | $expected = [ 158 | 'Bakery\Tests\Fixtures\Schemas\ArticleSchema', 159 | 'Bakery\Tests\Fixtures\Schemas\CommentSchema', 160 | 'Bakery\Tests\Fixtures\Schemas\PhoneSchema', 161 | 'Bakery\Tests\Fixtures\Schemas\RoleSchema', 162 | 'Bakery\Tests\Fixtures\Schemas\TagSchema', 163 | 'Bakery\Tests\Fixtures\Schemas\UserRoleSchema', 164 | 'Bakery\Tests\Fixtures\Schemas\UserSchema', 165 | ]; 166 | 167 | $this->assertEquals($expected, array_intersect($expected, $models)); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tests/Stubs/DummyClass.php: -------------------------------------------------------------------------------- 1 | Field::string(), 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Stubs/DummyMutation.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'value' => 'A', 17 | ], 18 | ]; 19 | } 20 | 21 | public function fields(): array 22 | { 23 | return [ 24 | 'test' => [ 25 | 'type' => Type::string(), 26 | ], 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | schema = new IntegrationTestSchema(); 30 | $this->schema->toGraphQLSchema(); 31 | $this->registry = $this->schema->getRegistry(); 32 | } 33 | 34 | /** @test */ 35 | public function it_fires_events_about_transactions() 36 | { 37 | Event::fake(); 38 | 39 | $user = factory(User::class)->create(); 40 | $article = factory(Article::class)->make(); 41 | $this->actingAs($article->user); 42 | 43 | $schema = $this->registry->resolveSchemaForModel(Article::class); 44 | $schema->create([ 45 | 'title' => $article->title, 46 | 'slug' => $article->slug, 47 | 'content' => $article->content, 48 | 'userId' => $user->id, 49 | ]); 50 | 51 | Event::assertDispatched('eloquent.persisting: '.Article::class, 1); 52 | Event::assertDispatched('eloquent.persisted: '.Article::class, 1); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Traits/JoinsRelationshipsTest.php: -------------------------------------------------------------------------------- 1 | joinRelation($articles, $article->user()); 22 | 23 | $this->assertEquals('select * from `articles` inner join `users` on `users`.`id` = `articles`.`user_id`', $articles->toSql()); 24 | } 25 | 26 | /** @test */ 27 | public function it_can_join_a_belongs_to_many_relation() 28 | { 29 | $role = resolve(Role::class); 30 | $roles = Role::query(); 31 | 32 | $this->joinRelation($roles, $role->users()); 33 | 34 | $this->assertEquals('select * from `roles` inner join `role_user` on `roles`.`id` = `role_user`.`role_id` inner join `users` on `role_user`.`user_id` = `users`.`id`', $roles->toSql()); 35 | } 36 | 37 | /** @test */ 38 | public function it_can_join_a_has_many_relation() 39 | { 40 | $user = resolve(User::class); 41 | $users = User::query(); 42 | 43 | $this->joinRelation($users, $user->articles()); 44 | 45 | $this->assertEquals('select * from `users` inner join `articles` on `articles`.`user_id` = `users`.`id`', $users->toSql()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/TypeRegistryTest.php: -------------------------------------------------------------------------------- 1 | schema = new IntegrationTestSchema(); 31 | $this->schema->toGraphQLSchema(); 32 | $this->registry = $this->schema->getRegistry(); 33 | } 34 | 35 | /** @test */ 36 | public function it_binds_the_registry_as_singleton() 37 | { 38 | $registry = resolve(TypeRegistry::class); 39 | $this->assertSame($registry, $this->registry); 40 | } 41 | 42 | /** @test */ 43 | public function it_throws_exception_for_unregistered_type() 44 | { 45 | $this->expectException(TypeNotFound::class); 46 | 47 | $this->registry->resolve('WrongType'); 48 | } 49 | 50 | /** @test */ 51 | public function it_returns_registered_model_schema_for_a_class_name() 52 | { 53 | $schema = $this->registry->getModelSchema(UserSchema::class); 54 | $this->assertInstanceOf(UserSchema::class, $schema); 55 | } 56 | 57 | /** @test */ 58 | public function it_wraps_model_schema_around_an_eloquent_model() 59 | { 60 | $user = new User(); 61 | $schema = $this->registry->getSchemaForModel($user); 62 | $this->assertInstanceOf(UserSchema::class, $schema); 63 | $this->assertSame($schema->getModel(), $user); 64 | } 65 | 66 | /** @test */ 67 | public function it_returns_model_schema_for_a_model_class() 68 | { 69 | $schema = $this->registry->resolveSchemaForModel(User::class); 70 | $this->assertInstanceOf(UserSchema::class, $schema); 71 | $this->assertSame($schema, $this->registry->resolveSchemaForModel(User::class)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Types/CollectionSearchTypeTest.php: -------------------------------------------------------------------------------- 1 | toGraphQLSchema(); 17 | $type = new CollectionSearchType($schema->getRegistry(), new ArticleSchema($schema->getRegistry())); 18 | 19 | $actual = $type->resolveFields(); 20 | $this->assertArrayHasKey('title', $actual); 21 | 22 | $this->assertArrayNotHasKey('slug', $actual); 23 | $this->assertArrayNotHasKey('created_at', $actual); 24 | } 25 | 26 | /** @test */ 27 | public function it_generates_search_fields_for_relation_fields() 28 | { 29 | $schema = new IntegrationTestSchema(); 30 | $schema->toGraphQLSchema(); 31 | $type = new CollectionSearchType($schema->getRegistry(), new ArticleSchema($schema->getRegistry())); 32 | 33 | $actual = $type->resolveFields(); 34 | $this->assertArrayHasKey('user', $actual); 35 | 36 | $this->assertArrayNotHasKey('tags', $actual); 37 | $this->assertArrayNotHasKey('category', $actual); 38 | $this->assertArrayNotHasKey('comments', $actual); 39 | $this->assertArrayNotHasKey('upvotes', $actual); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Types/EnumTypeTest.php: -------------------------------------------------------------------------------- 1 | getRegistry()); 17 | $objectType = $type->toType(); 18 | 19 | $this->assertInstanceOf(GraphQLEnumType::class, $objectType); 20 | $this->assertEquals($type->name, $objectType->name); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Types/FieldTest.php: -------------------------------------------------------------------------------- 1 | authenticate(); 31 | $this->schema = new DefaultSchema(); 32 | $this->registry = $this->schema->getRegistry(); 33 | } 34 | 35 | /** @test */ 36 | public function it_can_set_a_store_policy_with_a_closure() 37 | { 38 | $user = new User(); 39 | 40 | $field = new Field($this->registry); 41 | $field->canStore(function () { 42 | return true; 43 | }); 44 | 45 | $this->assertTrue($field->authorizeToStore($user, 'email', 'value')); 46 | } 47 | 48 | /** @test */ 49 | public function it_throws_exception_if_policy_returns_false() 50 | { 51 | $this->expectException(AuthorizationException::class); 52 | 53 | $user = new User(); 54 | 55 | $field = new Field($this->registry); 56 | $field->canStore(function () { 57 | return false; 58 | }); 59 | 60 | $field->authorizeToStore($user, 'email', 'value'); 61 | } 62 | 63 | /** @test */ 64 | public function it_can_set_a_store_policy_with_a_policy_name_that_returns_true() 65 | { 66 | $user = new User(); 67 | $field = new Field($this->registry); 68 | $field->canStoreWhen('storeRestricted'); 69 | 70 | $this->assertTrue($field->authorizeToStore($user, 'restricted', 'No')); 71 | } 72 | 73 | /** @test */ 74 | public function it_can_set_a_store_policy_with_a_policy_name_that_returns_false() 75 | { 76 | $this->expectException(AuthorizationException::class); 77 | 78 | $user = new User(); 79 | 80 | $field = new Field($this->registry); 81 | $field->canStoreWhen('setType'); 82 | 83 | $this->assertTrue($field->authorizeToStore($user, 'email', 'value')); 84 | } 85 | 86 | /** @test */ 87 | public function it_throws_exception_if_policy_does_not_exist() 88 | { 89 | $this->expectException(AuthorizationException::class); 90 | 91 | $user = new User(); 92 | 93 | $field = new Field($this->registry); 94 | $field->canStoreWhen('nonExistingPolicy'); 95 | 96 | $this->assertTrue($field->authorizeToStore($user, 'email', 'value')); 97 | } 98 | } 99 | --------------------------------------------------------------------------------