├── .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 |
--------------------------------------------------------------------------------