From 258f51618d11f8f37ed9a313a830ae8b6921f3e4 Mon Sep 17 00:00:00 2001 From: spawnia Date: Thu, 13 Feb 2020 23:23:10 +0100 Subject: [PATCH 01/31] Add middleware that attempts authentication --- .../Http/Controllers/GraphQLController.php | 1 + .../Http/Middleware/AttemptAuthentication.php | 63 ++++++++++++ src/lighthouse.php | 5 +- .../Middleware/AttemptAuthenticationTest.php | 95 +++++++++++++++++++ 4 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 src/Support/Http/Middleware/AttemptAuthentication.php create mode 100644 tests/Support/Http/Middleware/AttemptAuthenticationTest.php diff --git a/src/Support/Http/Controllers/GraphQLController.php b/src/Support/Http/Controllers/GraphQLController.php index 87e221aad5..8a1decef2f 100644 --- a/src/Support/Http/Controllers/GraphQLController.php +++ b/src/Support/Http/Controllers/GraphQLController.php @@ -61,6 +61,7 @@ public function __construct( */ public function query(GraphQLRequest $request) { + dd($this->middleware); $this->eventsDispatcher->dispatch( new StartRequest($request) ); diff --git a/src/Support/Http/Middleware/AttemptAuthentication.php b/src/Support/Http/Middleware/AttemptAuthentication.php new file mode 100644 index 0000000000..cf07804e83 --- /dev/null +++ b/src/Support/Http/Middleware/AttemptAuthentication.php @@ -0,0 +1,63 @@ +auth = $auth; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @param string[] ...$guards + * @return mixed + * + * @throws \Illuminate\Auth\AuthenticationException + */ + public function handle($request, Closure $next, ...$guards) + { + $this->attemptAuthentication($guards); + + return $next($request); + } + + /** + * Attempt to authenticate the user, but don't do anything if they are not. + * + * @param array $guards + * @return void + */ + protected function attemptAuthentication(array $guards) + { + if (empty($guards)) { + $guards = [null]; + } + + foreach ($guards as $guard) { + if ($this->auth->guard($guard)->check()) { + return $this->auth->shouldUse($guard); + } + } + } +} diff --git a/src/lighthouse.php b/src/lighthouse.php index b6c86b4013..823ef68a3a 100644 --- a/src/lighthouse.php +++ b/src/lighthouse.php @@ -25,13 +25,12 @@ 'name' => 'graphql', /* - * * Beware that middleware defined here runs before the GraphQL execution phase, - * so you have to take extra care to return spec-compliant error responses. - * To apply middleware on a field level, use the @middleware directive. + * make sure to return spec-compliant responses in case an error is thrown. */ 'middleware' => [ \Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class, + //\Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class, ], /* diff --git a/tests/Support/Http/Middleware/AttemptAuthenticationTest.php b/tests/Support/Http/Middleware/AttemptAuthenticationTest.php new file mode 100644 index 0000000000..4d7a0f9e8b --- /dev/null +++ b/tests/Support/Http/Middleware/AttemptAuthenticationTest.php @@ -0,0 +1,95 @@ +make(AuthManager::class); + $authManager->viaRequest('foo', function () { + dd($this->user); + return $this->user; + }); + + /** @var \Illuminate\Contracts\Config\Repository $config */ + $config = $app['config']; + + $config->set('lighthouse.middleware', [ + AttemptAuthentication::class, + ]); +// dd($config->get('auth')); + $config->set('auth.guards.api.driver', 'foo'); + + } + + public function testAttemptsAuthenticationGuest(): void + { + /** @var \Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication $middleware */ + $middleware = app(AttemptAuthentication::class); + $middleware->handle(new Request(), function() {}); + + + $this->mockResolver() + ->with( + null, + [], + null + ); + + $this->schema = /** @lang GraphQL */ ' + type Query { + foo: ID @mock + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + foo + } + '); + } + + public function testAttemptsAuthenticationUser(): void + { + $this->user = new User(); + + $this->mockResolver() + ->with( + null, + [], + new Callback(function (Context $context) { + return $this->user === $context->user(); + }) + ); + + $this->schema = /** @lang GraphQL */ ' + type Query { + foo: ID @mock + } + '; + + $a = $this->graphQL(/** @lang GraphQL */ ' + { + foo + } + '); + + } +} From 154c26e803fea5aa80fe9ccc1cd99f8175997423 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Thu, 13 Feb 2020 22:23:35 +0000 Subject: [PATCH 02/31] Apply fixes from StyleCI --- .../Http/Middleware/AttemptAuthenticationTest.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/Support/Http/Middleware/AttemptAuthenticationTest.php b/tests/Support/Http/Middleware/AttemptAuthenticationTest.php index 4d7a0f9e8b..aa46d1832e 100644 --- a/tests/Support/Http/Middleware/AttemptAuthenticationTest.php +++ b/tests/Support/Http/Middleware/AttemptAuthenticationTest.php @@ -3,13 +3,11 @@ namespace Tests\Unit\Support\Http\Middleware; use Illuminate\Auth\AuthManager; -use Illuminate\Contracts\Auth\Authenticatable; -use Illuminate\Contracts\Auth\Guard; use Illuminate\Http\Request; use Nuwave\Lighthouse\Schema\Context; +use Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication; use PHPUnit\Framework\Constraint\Callback; use Tests\TestCase; -use Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication; use Tests\Utils\Models\User; class AttemptAuthenticationTest extends TestCase @@ -25,6 +23,7 @@ protected function getEnvironmentSetUp($app) $authManager = $app->make(AuthManager::class); $authManager->viaRequest('foo', function () { dd($this->user); + return $this->user; }); @@ -36,15 +35,14 @@ protected function getEnvironmentSetUp($app) ]); // dd($config->get('auth')); $config->set('auth.guards.api.driver', 'foo'); - } public function testAttemptsAuthenticationGuest(): void { /** @var \Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication $middleware */ $middleware = app(AttemptAuthentication::class); - $middleware->handle(new Request(), function() {}); - + $middleware->handle(new Request(), function () { + }); $this->mockResolver() ->with( @@ -90,6 +88,5 @@ public function testAttemptsAuthenticationUser(): void foo } '); - } } From fd69ad49e7e0fecb434dd75a0902954d2002c689 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 19 Feb 2020 12:46:59 +0100 Subject: [PATCH 03/31] Add up target to Makefile --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 60538f2661..eefb4d5be5 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ .PHONY: it help shell stan test bench vendor -it: vendor stan test ## Run useful checks before commits +it: up vendor stan test ## Run useful checks before commits help: ## Displays this list of targets with descriptions @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' +up: ## Bring up the docker-compose stack + docker-compose up -d + shell: ## Open an interactive shell into the php container docker-compose exec php bash From 74bb34da9141439f1e9e4392524f6dbaabd10e20 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 19 Feb 2020 13:04:33 +0100 Subject: [PATCH 04/31] Properly test pagination error messages (#1198) --- src/Pagination/PaginationArgs.php | 14 ++++- .../Directives/HasManyDirectiveTest.php | 9 +-- .../Directives/MorphManyDirectiveTest.php | 9 +-- .../Directives/PaginateDirectiveDBTest.php | 28 ++++----- .../Directives/PaginateDirectiveTest.php | 57 ++++++++++--------- 5 files changed, 67 insertions(+), 50 deletions(-) diff --git a/src/Pagination/PaginationArgs.php b/src/Pagination/PaginationArgs.php index aaf77c8ac4..8f9817e69b 100644 --- a/src/Pagination/PaginationArgs.php +++ b/src/Pagination/PaginationArgs.php @@ -45,7 +45,7 @@ public static function extractArgs(array $args, ?PaginationType $paginationType, if ($instance->first <= 0) { throw new Error( - "Requested pagination amount must be more than 0, got $instance->first" + self::requestedZeroOrLessItems($instance->first) ); } @@ -55,13 +55,23 @@ public static function extractArgs(array $args, ?PaginationType $paginationType, && $instance->first > $paginateMaxCount ) { throw new Error( - "Maximum number of {$paginateMaxCount} requested items exceeded. Fetch smaller chunks." + self::requestedTooManyItems($paginateMaxCount, $instance->first) ); } return $instance; } + public static function requestedZeroOrLessItems(int $amount): string + { + return "Requested pagination amount must be more than 0, got {$amount}."; + } + + public static function requestedTooManyItems(int $maxCount, int $actualCount): string + { + return "Maximum number of {$maxCount} requested items exceeded, got {$actualCount}. Fetch smaller chunks."; + } + /** * Calculate the current page to inform the user about the pagination state. * diff --git a/tests/Integration/Schema/Directives/HasManyDirectiveTest.php b/tests/Integration/Schema/Directives/HasManyDirectiveTest.php index 3ac816e5f4..35febc20aa 100644 --- a/tests/Integration/Schema/Directives/HasManyDirectiveTest.php +++ b/tests/Integration/Schema/Directives/HasManyDirectiveTest.php @@ -4,6 +4,7 @@ use GraphQL\Error\Error; use Illuminate\Support\Arr; +use Nuwave\Lighthouse\Pagination\PaginationArgs; use Tests\DBTestCase; use Tests\Utils\Models\Post; use Tests\Utils\Models\Task; @@ -235,7 +236,7 @@ public function testPaginatorTypeIsLimitedByMaxCountFromDirective(): void '); $this->assertSame( - 'Maximum number of 3 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(3, 5), $result->jsonGet('errors.0.message') ); } @@ -311,7 +312,7 @@ public function testRelayTypeIsLimitedByMaxCountFromDirective(): void '); $this->assertSame( - 'Maximum number of 3 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(3, 5), $result->jsonGet('errors.0.message') ); } @@ -347,7 +348,7 @@ public function testPaginatorTypeIsLimitedToMaxCountFromConfig(): void '); $this->assertSame( - 'Maximum number of 2 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(2, 3), $result->jsonGet('errors.0.message') ); } @@ -385,7 +386,7 @@ public function testRelayTypeIsLimitedToMaxCountFromConfig(): void '); $this->assertSame( - 'Maximum number of 2 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(2, 3), $result->jsonGet('errors.0.message') ); } diff --git a/tests/Integration/Schema/Directives/MorphManyDirectiveTest.php b/tests/Integration/Schema/Directives/MorphManyDirectiveTest.php index a410dd5070..9f323e0393 100644 --- a/tests/Integration/Schema/Directives/MorphManyDirectiveTest.php +++ b/tests/Integration/Schema/Directives/MorphManyDirectiveTest.php @@ -5,6 +5,7 @@ use GraphQL\Error\Error; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Support\Collection; +use Nuwave\Lighthouse\Pagination\PaginationArgs; use Tests\DBTestCase; use Tests\Utils\Models\Image; use Tests\Utils\Models\Post; @@ -260,7 +261,7 @@ public function testPaginatorTypeIsLimitedByMaxCountFromDirective(): void "); $this->assertSame( - 'Maximum number of 3 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(3, 10), $result->jsonGet('errors.0.message') ); } @@ -304,7 +305,7 @@ public function testPaginatorTypeIsLimitedToMaxCountFromConfig(): void "); $this->assertSame( - 'Maximum number of 2 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(2, 10), $result->jsonGet('errors.0.message') ); } @@ -507,7 +508,7 @@ public function testRelayTypeIsLimitedByMaxCountFromDirective(): void "); $this->assertSame( - 'Maximum number of 3 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(3, 10), $result->jsonGet('errors.0.message') ); } @@ -553,7 +554,7 @@ public function testRelayTypeIsLimitedToMaxCountFromConfig(): void "); $this->assertSame( - 'Maximum number of 2 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(2, 10), $result->jsonGet('errors.0.message') ); } diff --git a/tests/Integration/Schema/Directives/PaginateDirectiveDBTest.php b/tests/Integration/Schema/Directives/PaginateDirectiveDBTest.php index e4b8db0692..e6085ef890 100644 --- a/tests/Integration/Schema/Directives/PaginateDirectiveDBTest.php +++ b/tests/Integration/Schema/Directives/PaginateDirectiveDBTest.php @@ -14,7 +14,7 @@ public function testCanCreateQueryPaginators(): void { factory(User::class, 3)->create(); - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! @@ -25,7 +25,7 @@ public function testCanCreateQueryPaginators(): void } '; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { users(first: 2) { paginatorInfo { @@ -105,7 +105,7 @@ public function testCanCreateQueryPaginatorsWithDifferentPages(): void 'post_id' => $posts->first()->id, ]); - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! @@ -126,7 +126,7 @@ public function testCanCreateQueryPaginatorsWithDifferentPages(): void } '; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { users(first: 2, page: 1) { paginatorInfo { @@ -187,7 +187,7 @@ public function testCanCreateQueryConnections(): void { factory(User::class, 3)->create(); - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! @@ -198,7 +198,7 @@ public function testCanCreateQueryConnections(): void } '; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { users(first: 2) { pageInfo { @@ -225,7 +225,7 @@ public function testCanCreateQueryConnections(): void public function testQueriesConnectionWithNoData(): void { - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! @@ -236,7 +236,7 @@ public function testQueriesConnectionWithNoData(): void } '; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { users(first: 5) { pageInfo { @@ -277,7 +277,7 @@ public function testQueriesConnectionWithNoData(): void public function testQueriesPaginationWithNoData(): void { - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -287,7 +287,7 @@ public function testQueriesPaginationWithNoData(): void } '; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { users(first: 5) { paginatorInfo { @@ -327,7 +327,7 @@ public function testPaginatesWhenDefinedInTypeExtension(): void { factory(User::class, 2)->create(); - $this->schema .= ' + $this->schema .= /** @lang GraphQL */ ' type User { id: ID! name: String! @@ -338,7 +338,7 @@ public function testPaginatesWhenDefinedInTypeExtension(): void } '; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { users(first: 1) { data { @@ -354,7 +354,7 @@ public function testCanHaveADefaultPaginationCount(): void { factory(User::class, 3)->create(); - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! @@ -365,7 +365,7 @@ public function testCanHaveADefaultPaginationCount(): void } '; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { users { paginatorInfo { diff --git a/tests/Unit/Schema/Directives/PaginateDirectiveTest.php b/tests/Unit/Schema/Directives/PaginateDirectiveTest.php index a048a832dd..91dd78c9f6 100644 --- a/tests/Unit/Schema/Directives/PaginateDirectiveTest.php +++ b/tests/Unit/Schema/Directives/PaginateDirectiveTest.php @@ -2,10 +2,10 @@ namespace Tests\Unit\Schema\Directives; -use GraphQL\Error\Error; use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldDefinition; use Nuwave\Lighthouse\Exceptions\DefinitionException; +use Nuwave\Lighthouse\Pagination\PaginationArgs; use Tests\TestCase; class PaginateDirectiveTest extends TestCase @@ -21,11 +21,11 @@ public function testCanAliasRelayToConnection(): void protected function getConnectionQueryField(string $type): FieldDefinition { return $this - ->buildSchema(" + ->buildSchema(/** @lang GraphQL */ " type User { name: String } - + type Query { users: [User!]! @paginate(type: \"$type\") } @@ -36,14 +36,14 @@ protected function getConnectionQueryField(string $type): FieldDefinition public function testOnlyRegistersOneTypeForMultiplePaginators(): void { - $schema = $this->buildSchema(' + $schema = $this->buildSchema(/** @lang GraphQL */ ' type User { name: String users: [User!]! @paginate users2: [User!]! @paginate(type: "relay") users3: [User!]! @paginate(type: "connection") } - + type Query { users: [User!]! @paginate users2: [User!]! @paginate(type: "relay") @@ -65,7 +65,7 @@ public function testOnlyRegistersOneTypeForMultiplePaginators(): void public function testRegistersPaginatorFromTypeExtensionField(): void { - $schema = $this->buildSchemaWithPlaceholderQuery(' + $schema = $this->buildSchemaWithPlaceholderQuery(/** @lang GraphQL */ ' type User { id: ID! name: String! @@ -94,7 +94,7 @@ public function testHasMaxCountInGeneratedCountDescription(): void config(['lighthouse.paginate_max_count' => 5]); $queryType = $this - ->buildSchema(' + ->buildSchema(/** @lang GraphQL */ ' type Query { defaultPaginated: [User!]! @paginate defaultRelay: [User!]! @paginate(type: "relay") @@ -134,7 +134,7 @@ public function testCanChangePaginationAmountArgument(): void config(['lighthouse.pagination_amount_argument' => 'first']); $queryType = $this - ->buildSchema(' + ->buildSchema(/** @lang GraphQL */ ' type Query { defaultPaginated: [User!]! @paginate } @@ -155,19 +155,20 @@ public function testIsLimitedByMaxCountFromDirective(): void { config(['lighthouse.paginate_max_count' => 5]); - $this->schema = ' + $this->schema = /** @lang GraphQL */ + ' type User { id: ID! name: String! } - + type Query { users1: [User!]! @paginate(maxCount: 6) users2: [User!]! @paginate(maxCount: 10) } '; - $result = $this->graphQL(' + $result = $this->graphQL(/** @lang GraphQL */ ' { users1(first: 10) { data { @@ -179,7 +180,7 @@ public function testIsLimitedByMaxCountFromDirective(): void '); $this->assertSame( - 'Maximum number of 6 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(6, 10), $result->jsonGet('errors.0.message') ); } @@ -188,19 +189,19 @@ public function testIsLimitedToMaxCountFromConfig(): void { config(['lighthouse.paginate_max_count' => 5]); - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! } - + type Query { users1: [User!]! @paginate users2: [User!]! @paginate(type: "relay") } '; - $resultFromDefaultPagination = $this->graphQL(' + $resultFromDefaultPagination = $this->graphQL(/** @lang GraphQL */ ' { users1(first: 10) { data { @@ -212,11 +213,11 @@ public function testIsLimitedToMaxCountFromConfig(): void '); $this->assertSame( - 'Maximum number of 5 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(5, 10), $resultFromDefaultPagination->jsonGet('errors.0.message') ); - $resultFromRelayPagination = $this->graphQL(' + $resultFromRelayPagination = $this->graphQL(/** @lang GraphQL */ ' { users2(first: 10) { edges { @@ -230,25 +231,25 @@ public function testIsLimitedToMaxCountFromConfig(): void '); $this->assertSame( - 'Maximum number of 5 requested items exceeded. Fetch smaller chunks.', + PaginationArgs::requestedTooManyItems(5, 10), $resultFromRelayPagination->jsonGet('errors.0.message') ); } public function testThrowsWhenPaginationWithCountZeroIsRequested(): void { - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! } - + type Query { users: [User!] @paginate } '; - $this->graphQL(' + $result = $this->graphQL(/** @lang GraphQL */ ' { users(first: 0) { data { @@ -261,18 +262,22 @@ public function testThrowsWhenPaginationWithCountZeroIsRequested(): void 'data' => [ 'users' => null, ], - ]) - ->assertErrorCategory(Error::CATEGORY_GRAPHQL); + ]); + + $this->assertSame( + PaginationArgs::requestedZeroOrLessItems(0), + $result->jsonGet('errors.0.message') + ); } public function testDoesNotRequireModelWhenUsingBuilder(): void { $validationErrors = $this - ->buildSchema(' + ->buildSchema(/** @lang GraphQL */ ' type Query { users: [NotAnActualModelName!] @paginate(builder: "'.$this->qualifyTestResolver('testDoesNotRequireModelWhenUsingBuilder').'") } - + type NotAnActualModelName { id: ID! } @@ -286,7 +291,7 @@ public function testThrowsIfBuilderIsNotPresent(): void { $this->expectException(DefinitionException::class); $this->expectExceptionMessageRegExp('/NonexistingClass/'); - $this->buildSchema(' + $this->buildSchema(/** @lang GraphQL */ ' type Query { users: [Query!] @paginate(builder: "NonexistingClass@notFound") } From a651090931c96028efd5b4f7a66f78ba497a3790 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 19 Feb 2020 13:18:17 +0100 Subject: [PATCH 05/31] Test `@paginate` with `scopes` (#1199) --- .../Directives/PaginateDirectiveDBTest.php | 52 +++++++++++++++++++ tests/Utils/Models/User.php | 5 ++ 2 files changed, 57 insertions(+) diff --git a/tests/Integration/Schema/Directives/PaginateDirectiveDBTest.php b/tests/Integration/Schema/Directives/PaginateDirectiveDBTest.php index e6085ef890..9491199d79 100644 --- a/tests/Integration/Schema/Directives/PaginateDirectiveDBTest.php +++ b/tests/Integration/Schema/Directives/PaginateDirectiveDBTest.php @@ -90,6 +90,58 @@ public function testCanSpecifyCustomBuilder(): void ]); } + public function testPaginateWithScopes(): void + { + $namedUserName = 'A named user'; + factory(User::class)->create([ + 'name' => $namedUserName, + ]); + factory(User::class)->create([ + 'name' => null, + ]); + + $this->schema = /** @lang GraphQL */ ' + type User { + id: ID! + name: String! + } + + type Query { + users: [User!]! @paginate(scopes: ["named"]) + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + users(first: 5) { + paginatorInfo { + count + total + currentPage + } + data { + name + } + } + } + ')->assertExactJson([ + 'data' => [ + 'users' => [ + 'paginatorInfo' => [ + 'count' => 1, + 'total' => 1, + 'currentPage' => 1, + ], + 'data' => [ + [ + 'name' => $namedUserName, + ], + ], + ], + ], + ]); + } + public function builder(): Builder { return User::orderBy('id', 'DESC'); diff --git a/tests/Utils/Models/User.php b/tests/Utils/Models/User.php index 20edaf4f06..134c6b53e8 100644 --- a/tests/Utils/Models/User.php +++ b/tests/Utils/Models/User.php @@ -81,4 +81,9 @@ public function getCompanyNameAttribute() { return $this->company->name; } + + public function scopeNamed(Builder $query): Builder + { + return $query->whereNotNull('name'); + } } From 963829316796b72273a4037983869a8ab5cf6fa8 Mon Sep 17 00:00:00 2001 From: Fabian Torres Date: Mon, 24 Feb 2020 05:31:32 -0300 Subject: [PATCH 06/31] Add support to extend `input`, `interface` and `enum` types (#1203) --- CHANGELOG.md | 1 + .../digging-deeper/schema-organisation.md | 43 ++--- src/Schema/AST/ASTBuilder.php | 97 +++++++++-- src/Schema/AST/ASTHelper.php | 9 +- tests/Unit/Schema/AST/ASTBuilderTest.php | 153 +++++++++++++++++- 5 files changed, 270 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4843133cfc..d1fc6ddb86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ You can find and compare releases at the [GitHub release page](https://github.co get the arguments passed through a client directive https://github.com/nuwave/lighthouse/pull/1184 - Add `streamGraphQL()` helper method to `\Nuwave\Lighthouse\Testing\MakesGraphQLRequests` for simple testing of streamed responses, such as `@defer` https://github.com/nuwave/lighthouse/pull/1184 +- Add support to extend `input`, `interface` and `enum` types https://github.com/nuwave/lighthouse/pull/1203 ### Fixed diff --git a/docs/master/digging-deeper/schema-organisation.md b/docs/master/digging-deeper/schema-organisation.md index 492519092d..2fdc933466 100644 --- a/docs/master/digging-deeper/schema-organisation.md +++ b/docs/master/digging-deeper/schema-organisation.md @@ -78,8 +78,27 @@ type Query { } ``` -__Attention__: A valid `Query` type definition with at least one field -must be present in the root schema. +Now you want to add a few queries to actually fetch posts. You could add them to the main `Query` type +in your main file, but that spreads the definition apart, and could also grow quite large over time. + +Another way would be to extend the `Query` type and colocate the type definition with its Queries in `post.graphql`. + +```graphql +type Post { + title: String + author: User @belongsTo +} + +extend type Query { + posts: [Post!]! @paginate +} +``` + +The fields in the `extend type` definition are merged with those of the original type. + +### Root Definitions + +A valid `Query` type definition with at least one field must be present in the root schema. This is because `extend type` needs the original type to get merged into. You can provide an empty `Query` type (without curly braces) in the root schema: @@ -101,20 +120,8 @@ type Mutation #import post.graphql ``` -Now you want to add a few queries to actually fetch posts. You could add them to the main `Query` type -in your main file, but that spreads the definition apart, and could also grow quite large over time. - -Another way would be to extend the `Query` type and colocate the type definition with its Queries in `post.graphql`. +### Extending other types -```graphql -type Post { - title: String - author: User @belongsTo -} - -extend type Query { - posts: [Post!]! @paginate -} -``` - -The fields in the `extend type` definition are merged with those of the original type. +Apart from object types, you can also extend `input`, `interface` and `enum` types. +Lighthouse will merge the fields (or values) with the original definition and always +produce a single type in the final schema. diff --git a/src/Schema/AST/ASTBuilder.php b/src/Schema/AST/ASTBuilder.php index 0975c76de0..410aae3622 100644 --- a/src/Schema/AST/ASTBuilder.php +++ b/src/Schema/AST/ASTBuilder.php @@ -2,13 +2,22 @@ namespace Nuwave\Lighthouse\Schema\AST; +use GraphQL\Language\AST\EnumTypeDefinitionNode; +use GraphQL\Language\AST\EnumTypeExtensionNode; +use GraphQL\Language\AST\InputObjectTypeDefinitionNode; +use GraphQL\Language\AST\InputObjectTypeExtensionNode; +use GraphQL\Language\AST\InterfaceTypeDefinitionNode; +use GraphQL\Language\AST\InterfaceTypeExtensionNode; use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\AST\ObjectTypeExtensionNode; +use GraphQL\Language\AST\TypeDefinitionNode; +use GraphQL\Language\AST\TypeExtensionNode; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Contracts\Events\Dispatcher as EventDispatcher; use Illuminate\Support\Arr; use Nuwave\Lighthouse\Events\BuildSchemaString; use Nuwave\Lighthouse\Events\ManipulateAST; +use Nuwave\Lighthouse\Exceptions\DefinitionException; use Nuwave\Lighthouse\Schema\Factories\DirectiveFactory; use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider; use Nuwave\Lighthouse\Support\Contracts\ArgManipulator; @@ -18,6 +27,13 @@ class ASTBuilder { + const EXTENSION_TO_DEFINITION_CLASS = [ + ObjectTypeExtensionNode::class => ObjectTypeDefinitionNode::class, + InputObjectTypeExtensionNode::class => InputObjectTypeDefinitionNode::class, + InterfaceTypeExtensionNode::class => InterfaceTypeDefinitionNode::class, + EnumTypeExtensionNode::class => EnumTypeDefinitionNode::class, + ]; + /** * The directive factory. * @@ -170,28 +186,91 @@ protected function applyTypeExtensionManipulators(): void foreach ($this->documentAST->typeExtensions as $typeName => $typeExtensionsList) { /** @var \GraphQL\Language\AST\TypeExtensionNode $typeExtension */ foreach ($typeExtensionsList as $typeExtension) { + // Before we actually extend the types, we apply the manipulator directives + // that are defined on type extensions themselves /** @var \Nuwave\Lighthouse\Support\Contracts\TypeExtensionManipulator $typeExtensionManipulator */ foreach ( $this->directiveFactory->createAssociatedDirectivesOfType($typeExtension, TypeExtensionManipulator::class) as $typeExtensionManipulator ) { - $typeExtensionManipulator->manipulatetypeExtension($this->documentAST, $typeExtension); + $typeExtensionManipulator->manipulateTypeExtension($this->documentAST, $typeExtension); } // After manipulation on the type extension has been done, - // we can merge its fields with the original type - if ($typeExtension instanceof ObjectTypeExtensionNode) { - $relatedObjectType = $this->documentAST->types[$typeName]; - - $relatedObjectType->fields = ASTHelper::mergeUniqueNodeList( - $relatedObjectType->fields, - $typeExtension->fields - ); + // we can merge them with the original type + if ( + $typeExtension instanceof ObjectTypeExtensionNode + || $typeExtension instanceof InputObjectTypeExtensionNode + || $typeExtension instanceof InterfaceTypeExtensionNode + ) { + $this->extendObjectLikeType($typeName, $typeExtension); + } elseif ($typeExtension instanceof EnumTypeExtensionNode) { + $this->extendEnumType($typeName, $typeExtension); } } } } + /** + * @param string $typeName + * @param \GraphQL\Language\AST\ObjectTypeExtensionNode|\GraphQL\Language\AST\InputObjectTypeExtensionNode|\GraphQL\Language\AST\InterfaceTypeExtensionNode $typeExtension + * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException + */ + protected function extendObjectLikeType(string $typeName, TypeExtensionNode $typeExtension): void + { + /** @var \GraphQL\Language\AST\ObjectTypeDefinitionNode|\GraphQL\Language\AST\InputObjectTypeDefinitionNode|\GraphQL\Language\AST\InterfaceTypeDefinitionNode $extendedObjectLikeType */ + $extendedObjectLikeType = $this->documentAST->types[$typeName]; + $this->assertExtensionMatchesDefinition($typeExtension, $extendedObjectLikeType); + + $extendedObjectLikeType->fields = ASTHelper::mergeUniqueNodeList( + $extendedObjectLikeType->fields, + $typeExtension->fields + ); + } + + /** + * @param string $typeName + * @param \GraphQL\Language\AST\ObjectTypeExtensionNode|\GraphQL\Language\AST\InputObjectTypeExtensionNode|\GraphQL\Language\AST\InterfaceTypeExtensionNode $typeExtension + * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException + */ + protected function extendEnumType(string $typeName, TypeExtensionNode $typeExtension): void + { + /** @var \GraphQL\Language\AST\EnumTypeDefinitionNode $extendedEnum */ + $extendedEnum = $this->documentAST->types[$typeName]; + $this->assertExtensionMatchesDefinition($typeExtension, $extendedEnum); + + $extendedEnum->values = ASTHelper::mergeUniqueNodeList( + $extendedEnum->values, + $typeExtension->values + ); + } + + /** + * @param \GraphQL\Language\AST\ObjectTypeExtensionNode|\GraphQL\Language\AST\InputObjectTypeExtensionNode|\GraphQL\Language\AST\InterfaceTypeExtensionNode|\GraphQL\Language\AST\EnumTypeExtensionNode $extension + * @param \GraphQL\Language\AST\TypeDefinitionNode $definition + * @return void + * + * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException + */ + protected function assertExtensionMatchesDefinition(TypeExtensionNode $extension, TypeDefinitionNode $definition): void + { + if (static::EXTENSION_TO_DEFINITION_CLASS[get_class($extension)] !== get_class($definition)) { + throw new DefinitionException( + static::extensionDoesNotMatchDefinition($extension, $definition) + ); + } + } + + /** + * @param \GraphQL\Language\AST\ObjectTypeExtensionNode|\GraphQL\Language\AST\InputObjectTypeExtensionNode|\GraphQL\Language\AST\InterfaceTypeExtensionNode|\GraphQL\Language\AST\EnumTypeExtensionNode $extension + * @param \GraphQL\Language\AST\TypeDefinitionNode $definition + * @return string + */ + public static function extensionDoesNotMatchDefinition(TypeExtensionNode $extension, TypeDefinitionNode $definition): string + { + return 'The type extension '.$extension->name->value.' of kind '.$extension->kind.' can not extend a definition of kind '.$definition->kind.'.'; + } + /** * Apply directives on fields that can manipulate the AST. * diff --git a/src/Schema/AST/ASTHelper.php b/src/Schema/AST/ASTHelper.php index a0456b4529..c311278d21 100644 --- a/src/Schema/AST/ASTHelper.php +++ b/src/Schema/AST/ASTHelper.php @@ -51,6 +51,8 @@ public static function mergeNodeList($original, $addition): NodeList * @param bool $overwriteDuplicates By default this function throws if a collision occurs. * If set to true, the fields of the original list will be overwritten. * @return \GraphQL\Language\AST\NodeList + * + * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException */ public static function mergeUniqueNodeList($original, $addition, bool $overwriteDuplicates = false): NodeList { @@ -66,7 +68,7 @@ public static function mergeUniqueNodeList($original, $addition, bool $overwrite if ($collisionOccurred && ! $overwriteDuplicates) { throw new DefinitionException( - "Duplicate definition {$oldName} found when merging." + static::duplicateDefinition($oldName) ); } @@ -78,6 +80,11 @@ public static function mergeUniqueNodeList($original, $addition, bool $overwrite return self::mergeNodeList($remainingDefinitions, $addition); } + public static function duplicateDefinition(string $oldName): string + { + return "Duplicate definition {$oldName} found when merging."; + } + /** * Unwrap lists and non-nulls and get the name of the contained type. * diff --git a/tests/Unit/Schema/AST/ASTBuilderTest.php b/tests/Unit/Schema/AST/ASTBuilderTest.php index a1427db785..72f710e6fe 100644 --- a/tests/Unit/Schema/AST/ASTBuilderTest.php +++ b/tests/Unit/Schema/AST/ASTBuilderTest.php @@ -2,8 +2,10 @@ namespace Tests\Unit\Schema\AST; +use GraphQL\Language\AST\NodeKind; use Nuwave\Lighthouse\Exceptions\DefinitionException; use Nuwave\Lighthouse\Schema\AST\ASTBuilder; +use Nuwave\Lighthouse\Schema\AST\ASTHelper; use Tests\TestCase; class ASTBuilderTest extends TestCase @@ -22,15 +24,15 @@ protected function setUp(): void public function testCanMergeTypeExtensionFields(): void { - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type Query { foo: String } - + extend type Query { bar: Int! } - + extend type Query { baz: Boolean } @@ -43,19 +45,160 @@ public function testCanMergeTypeExtensionFields(): void ); } + public function testCanMergeInputExtensionFields(): void + { + $this->schema = /** @lang GraphQL */ ' + input Inputs { + foo: String + } + + extend input Inputs { + bar: Int! + } + + extend input Inputs { + baz: Boolean + } + '; + $documentAST = $this->astBuilder->documentAST(); + + $this->assertCount( + 3, + $documentAST->types['Inputs']->fields + ); + } + + public function testCanMergeInterfaceExtensionFields(): void + { + $this->schema = /** @lang GraphQL */ ' + interface Named { + name: String! + } + + extend interface Named { + bar: Int! + } + + extend interface Named { + baz: Boolean + } + '; + $documentAST = $this->astBuilder->documentAST(); + + $this->assertCount( + 3, + $documentAST->types['Named']->fields + ); + } + + public function testCanMergeEnumExtensionFields(): void + { + $this->schema = /** @lang GraphQL */ ' + enum MyEnum { + ONE + TWO + } + + extend enum MyEnum { + THREE + } + + extend enum MyEnum { + FOUR + } + '; + $documentAST = $this->astBuilder->documentAST(); + + $this->assertCount( + 4, + $documentAST->types['MyEnum']->values + ); + } + public function testDoesNotAllowDuplicateFieldsOnTypeExtensions(): void { - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type Query { foo: String } - + extend type Query { foo: Int } '; $this->expectException(DefinitionException::class); + $this->expectExceptionMessage(ASTHelper::duplicateDefinition('foo')); + $this->astBuilder->documentAST(); + } + + public function testDoesNotAllowDuplicateFieldsOnInputExtensions(): void + { + $this->schema = /** @lang GraphQL */ ' + input Inputs { + foo: String + } + + extend input Inputs { + foo: Int + } + '; + + $this->expectException(DefinitionException::class); + $this->expectExceptionMessage(ASTHelper::duplicateDefinition('foo')); + $this->astBuilder->documentAST(); + } + + public function testDoesNotAllowDuplicateFieldsOnInterfaceExtensions(): void + { + $this->schema = /** @lang GraphQL */ ' + interface Named { + foo: String + } + + extend interface Named{ + foo: Int + } + '; + + $this->expectException(DefinitionException::class); + $this->expectException(DefinitionException::class); + $this->astBuilder->documentAST(); + } + + public function testDoesNotAllowDuplicateValuesOnEnumExtensions(): void + { + $this->schema = /** @lang GraphQL */ ' + enum MyEnum { + ONE + TWO + } + + extend enum MyEnum { + TWO + THREE + } + '; + + $this->expectException(DefinitionException::class); + $this->expectExceptionMessage(ASTHelper::duplicateDefinition('TWO')); + $this->astBuilder->documentAST(); + } + + public function testDoesNotAllowMergingNonMatchingTypes(): void + { + $this->schema = /** @lang GraphQL */ ' + type Foo { + bar: ID + } + + extend interface Foo { + baz: ID + } + '; + + $this->expectException(DefinitionException::class); + $this->expectExceptionMessage('The type extension Foo of kind '.NodeKind::INTERFACE_TYPE_EXTENSION.' can not extend a definition of kind '.NodeKind::OBJECT_TYPE_DEFINITION.'.'); $this->astBuilder->documentAST(); } } From 9a30dc2dd305bd6d9fc886a148730eef7fe59547 Mon Sep 17 00:00:00 2001 From: Christoph Flathmann Date: Mon, 24 Feb 2020 13:40:32 +0100 Subject: [PATCH 07/31] Add `@hash` directive and deprecate `@bcrypt` (#1200) --- CHANGELOG.md | 8 ++ UPGRADE.md | 19 ++- docs/master/api-reference/directives.md | 35 +++-- .../custom-directives/argument-directives.md | 2 +- src/Schema/Directives/BcryptDirective.php | 5 + src/Schema/Directives/HashDirective.php | 42 ++++++ tests/Integration/ValidationTest.php | 2 +- .../Schema/Directives/BcryptDirectiveTest.php | 3 + .../Schema/Directives/HashDirectiveTest.php | 121 ++++++++++++++++++ tests/Unit/Schema/TypeRegistryTest.php | 2 +- 10 files changed, 224 insertions(+), 15 deletions(-) create mode 100644 src/Schema/Directives/HashDirective.php create mode 100644 tests/Unit/Schema/Directives/HashDirectiveTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d1fc6ddb86..de1403d528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Added + +- Add `@hash` directive which uses Laravel's hashing configuration https://github.com/nuwave/lighthouse/pull/1200 + +### Deprecated + +- Remove `@bcrypt` in favour of `@hash` https://github.com/nuwave/lighthouse/pull/1200 + ## 4.9.0 ### Added diff --git a/UPGRADE.md b/UPGRADE.md index 76a60960b0..f204a9b653 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -19,8 +19,8 @@ the [`@guard`](docs/master/api-reference/directives.md#guard) on selected fields ```diff type Query { -- profile: User! @middlware(checks: ["auth"]) -+ profile: User! @guard +- profile: User! @middlware(checks: ["auth"]) ++ profile: User! @guard } ``` @@ -78,3 +78,18 @@ You can adapt to this change in two refactoring steps that must be done in order id: ID! } ``` + +### Replace `@bcrypt` with `@hash` + +The new `@hash` directive is also used for password hashing, but respects the +configuration settings of your Laravel project. + +```diff +type Mutation { + createUser( + name: String! +- password: String! @bcrypt ++ password: String! @hash + ): User! +} +``` diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index de1c2f1fc9..a79b477550 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -242,23 +242,17 @@ type CustomRoleEdge implements Edge { ## @bcrypt -Run the `bcrypt` function on the argument it is defined on. - -```graphql -type Mutation { - createUser(name: String, password: String @bcrypt): User -} -``` - -### Definition - ```graphql """ Run the `bcrypt` function on the argument it is defined on. + +@deprecated(reason: "Use @hash instead. This directive will be removed in v5.") """ directive @bcrypt on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION ``` +Deprecated in favour of [`@hash`](#hash). + ## @broadcast Broadcast the results of a mutation to subscribed clients. @@ -1082,7 +1076,28 @@ directive @guard( ) on FIELD_DEFINITION | OBJECT ``` +## @hash +```graphql +""" +Use Laravel hashing to transform an argument value. + +Useful for hashing passwords before inserting them into the database. +This uses the default hashing driver defined in `config/hashing.php`. +""" +directive @hash on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +The most common use case for this is when dealing with passwords: + +```graphql +type Mutation { + createUser( + name: String! + password: String! @hash + ): User! +} +``` ## @hasMany diff --git a/docs/master/custom-directives/argument-directives.md b/docs/master/custom-directives/argument-directives.md index ed4daffb6f..6ec34bf429 100644 --- a/docs/master/custom-directives/argument-directives.md +++ b/docs/master/custom-directives/argument-directives.md @@ -91,7 +91,7 @@ Argument directives are evaluated in the order that they are defined in the sche ```graphql type Mutation { createUser( - password: String @trim @rules(apply: ["min:10,max:20"]) @bcrypt + password: String @trim @rules(apply: ["min:10,max:20"]) @hash ): User } ``` diff --git a/src/Schema/Directives/BcryptDirective.php b/src/Schema/Directives/BcryptDirective.php index a5f091cedd..1058fb19f8 100644 --- a/src/Schema/Directives/BcryptDirective.php +++ b/src/Schema/Directives/BcryptDirective.php @@ -5,6 +5,9 @@ use Nuwave\Lighthouse\Support\Contracts\ArgTransformerDirective; use Nuwave\Lighthouse\Support\Contracts\DefinedDirective; +/** + * @deprecated + */ class BcryptDirective extends BaseDirective implements ArgTransformerDirective, DefinedDirective { public static function definition(): string @@ -12,6 +15,8 @@ public static function definition(): string return /** @lang GraphQL */ <<<'SDL' """ Run the `bcrypt` function on the argument it is defined on. + +@deprecated(reason: "Use @hash instead. This directive will be removed in v5.") """ directive @bcrypt on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION SDL; diff --git a/src/Schema/Directives/HashDirective.php b/src/Schema/Directives/HashDirective.php new file mode 100644 index 0000000000..ac50addd90 --- /dev/null +++ b/src/Schema/Directives/HashDirective.php @@ -0,0 +1,42 @@ +hasher = $hasher; + } + + public static function definition(): string + { + return /** @lang GraphQL */ <<<'SDL' +""" +Use Laravel hashing to transform an argument value. + +Useful for hashing passwords before inserting them into the database. +This uses the default hashing driver defined in `config/hashing.php`. +""" +directive @hash on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +SDL; + } + + /** + * @param string $argumentValue + * @return string + */ + public function transform($argumentValue): string + { + return $this->hasher->make($argumentValue); + } +} diff --git a/tests/Integration/ValidationTest.php b/tests/Integration/ValidationTest.php index 9149a18499..8c2a5546cc 100644 --- a/tests/Integration/ValidationTest.php +++ b/tests/Integration/ValidationTest.php @@ -29,7 +29,7 @@ class ValidationTest extends DBTestCase password: String @trim @rules(apply: ["min:6", "max:20", "required_with:id"]) - @bcrypt + @hash bar: Bar @rules(apply: ["required_if:id,bar"]) ): String @field(resolver: "Tests\\\\Integration\\\\ValidationTest@resolvePassword") diff --git a/tests/Unit/Schema/Directives/BcryptDirectiveTest.php b/tests/Unit/Schema/Directives/BcryptDirectiveTest.php index 034a74e933..4729d7c326 100644 --- a/tests/Unit/Schema/Directives/BcryptDirectiveTest.php +++ b/tests/Unit/Schema/Directives/BcryptDirectiveTest.php @@ -4,6 +4,9 @@ use Tests\TestCase; +/** + * @deprecated + */ class BcryptDirectiveTest extends TestCase { public function testCanBcryptAnArgument(): void diff --git a/tests/Unit/Schema/Directives/HashDirectiveTest.php b/tests/Unit/Schema/Directives/HashDirectiveTest.php new file mode 100644 index 0000000000..8d370e18a2 --- /dev/null +++ b/tests/Unit/Schema/Directives/HashDirectiveTest.php @@ -0,0 +1,121 @@ +schema = /** @lang GraphQL */ ' + type Query { + foo(bar: String @hash): Foo @mock + } + + type Foo { + bar: String + } + '; + + $this->mockResolver(function ($root, $args) { + return $args; + }); + + $password = $this + ->graphQL(/** @lang GraphQL */ ' + { + foo(bar: "password") { + bar + } + } + ') + ->jsonGet('data.foo.bar'); + + $this->assertNotSame('password', $password); + $this->assertTrue(password_verify('password', $password)); + } + + public function testCanHashAnArgumentInInputObjectAndArray(): void + { + $this->schema = /** @lang GraphQL */ ' + type Query { + user(input: UserInput): User @mock + } + + type User { + password: String! + alt_passwords: [String] + friends: [User] + } + + input UserInput { + password: String @hash + alt_passwords: [String] @hash + friends: [UserInput] + } + '; + + $this->mockResolver(function ($root, array $args) { + return $args['input']; + }); + + $result = $this->graphQL(/** @lang GraphQL */ ' + { + user(input: { + password: "password" + alt_passwords: ["alt_password_1", "alt_password_2"] + friends: [ + { password: "friend_password_1" } + { password: "friend_password_2" } + { + password: "friend_password_3" + friends: [ + { password: "friend_password_4" } + ] + } + ] + }) { + password + alt_passwords + friends { + password + friends { + password + } + } + } + } + '); + + $password = $result->jsonGet('data.user.password'); + $this->assertNotSame('password', $password); + $this->assertTrue(password_verify('password', $password)); + + // apply to array + $altPasswordOne = $result->jsonGet('data.user.alt_passwords.0'); + $this->assertNotSame('alt_password_1', $altPasswordOne); + $this->assertTrue(password_verify('alt_password_1', $altPasswordOne)); + + $altPasswordTwo = $result->jsonGet('data.user.alt_passwords.1'); + $this->assertNotSame('alt_password_2', $altPasswordTwo); + $this->assertTrue(password_verify('alt_password_2', $altPasswordTwo)); + + // apply to (nested) input + $friendPasswordOne = $result->jsonGet('data.user.friends.0.password'); + $this->assertNotSame('friend_password_1', $friendPasswordOne); + $this->assertTrue(password_verify('friend_password_1', $friendPasswordOne)); + + $friendPasswordTwo = $result->jsonGet('data.user.friends.1.password'); + $this->assertNotSame('friend_password_2', $friendPasswordTwo); + $this->assertTrue(password_verify('friend_password_2', $friendPasswordTwo)); + + $friendPasswordThree = $result->jsonGet('data.user.friends.2.password'); + $this->assertNotSame('friend_password_3', $friendPasswordThree); + $this->assertTrue(password_verify('friend_password_3', $friendPasswordThree)); + + $friendPasswordFour = $result->jsonGet('data.user.friends.2.friends.0.password'); + $this->assertNotSame('friend_password_4', $friendPasswordFour); + $this->assertTrue(password_verify('friend_password_4', $friendPasswordFour)); + } +} diff --git a/tests/Unit/Schema/TypeRegistryTest.php b/tests/Unit/Schema/TypeRegistryTest.php index 0d78946fc8..3a743fc991 100644 --- a/tests/Unit/Schema/TypeRegistryTest.php +++ b/tests/Unit/Schema/TypeRegistryTest.php @@ -156,7 +156,7 @@ public function testCanTransformObjectTypes(): void { $objectTypeNode = PartialParser::objectTypeDefinition(' type User { - foo(bar: String! @bcrypt): String! + foo(bar: String! @hash): String! } '); /** @var \GraphQL\Type\Definition\ObjectType $objectType */ From 58f6ca008393bce5bc9ee721999979a7890a6643 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Wed, 26 Feb 2020 13:40:55 -0500 Subject: [PATCH 08/31] =?UTF-8?q?Add=20option=20`passOrdered`=20to=20`@met?= =?UTF-8?q?hod`=20to=20pass=20just=20the=20argument=E2=80=A6=20(#1208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Benedikt Franke Co-authored-by: Enzo Notario --- CHANGELOG.md | 3 + UPGRADE.md | 22 +++ docs/master/api-reference/directives.md | 73 +++++++-- src/Schema/AST/ASTHelper.php | 13 ++ src/Schema/Directives/MethodDirective.php | 39 +++-- .../Schema/Directives/MethodDirectiveTest.php | 147 ++++++++++++++---- 6 files changed, 241 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de1403d528..4cfccf7528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,13 @@ You can find and compare releases at the [GitHub release page](https://github.co ### Added - Add `@hash` directive which uses Laravel's hashing configuration https://github.com/nuwave/lighthouse/pull/1200 +- Add option `passOrdered` to `@method` to pass just the arguments as ordered parameters https://github.com/nuwave/lighthouse/pull/1208 ### Deprecated - Remove `@bcrypt` in favour of `@hash` https://github.com/nuwave/lighthouse/pull/1200 +- `@method` will call the underlying method with the arguments as ordered parameters instead + of the full resolver arguments https://github.com/nuwave/lighthouse/pull/1208 ## 4.9.0 diff --git a/UPGRADE.md b/UPGRADE.md index f204a9b653..1e37541fc6 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -93,3 +93,25 @@ type Mutation { ): User! } ``` + +### `@method` passes down just ordered arguments + +Instead of passing down the usual resolver arguments, the `@method` directive will +now pass just the arguments given to a field. This behaviour could previously be +enabled through the `passOrdered` option, which is now removed. + +```graphql +type User { + purchasedItemsCount( + year: Int! + includeReturns: Boolean + ): Int @method +} +``` + +The method will have to change like this: + +```diff +-public function purchasedItemsCount($root, array $args) ++public function purchasedItemsCount(int $year, ?bool $includeReturns) +``` diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index a79b477550..e11a67993f 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -1413,33 +1413,72 @@ type Post { ## @method -Call a method with a given `name` on the class that represents a type to resolve a field. -Use this if the data is not accessible as an attribute (e.g. `$model->myData`). +```graphql +""" +Resolve a field by calling a method on the parent object. + +Use this if the data is not accessible through simple property access or if you +want to pass argument to the method. +""" +directive @method( + """ + Specify the method of which to fetch the data from. + Defaults to the name of the field if not given. + """ + name: String + + """ + Pass the field arguments to the method, using the argument definition + order from the schema to sort them before passing them along. + + @deprecated This behaviour will default to true in v5 and this setting will be removed. + """ + passOrdered: Boolean = false +) on FIELD_DEFINITION +``` + +This can be useful on models or other classes that have getters: + +```graphql +type User { + mySpecialData: String! @method(name: "getMySpecialData") +} +``` + +This calls a method `App\User::getMySpecialData` with [the typical resolver arguments](resolvers.md#resolver-function-signature). +If you want to pass down only the arguments in sequence, use the `passOrdered` option: ```graphql type User { - mySpecialData: String! @method(name: "findMySpecialData") + purchasedItemsCount( + year: Int! + includeReturns: Boolean + ): Int @method(passOrdered: true) } ``` -This calls a method `App\User::findMySpecialData` with [the typical resolver arguments](resolvers.md#resolver-function-signature). +This will call the method with the arguments a client passes to the field. +Ensure the order of the argument definition matches the parameters of your method. -The first argument is an instance of the class itself, -so the method can be `public static` if needed. +```php +public function purchasedItemsCount(int $year, ?bool $includeReturns) +``` -### Definition +Lighthouse will always pass down the same number of arguments and default to `null` +if the client passes nothing. ```graphql -""" -Call a method with a given `name` on the class that represents a type to resolve a field. -Use this if the data is not accessible as an attribute (e.g. `$model->myData`). -""" -directive @method( - """ - Specify the method of which to fetch the data from. - """ - name: String -) on FIELD_DEFINITION +{ + user(id: 3) { + purchasedItemsCount(year: 2017) + } +} +``` + +The method will get called like this: + +```php +$user->purchasedItemsCount(2017, null) ``` ## @middleware diff --git a/src/Schema/AST/ASTHelper.php b/src/Schema/AST/ASTHelper.php index c311278d21..d38ed029ea 100644 --- a/src/Schema/AST/ASTHelper.php +++ b/src/Schema/AST/ASTHelper.php @@ -4,6 +4,7 @@ use GraphQL\Language\AST\ArgumentNode; use GraphQL\Language\AST\DirectiveNode; +use GraphQL\Language\AST\FieldDefinitionNode; use GraphQL\Language\AST\NamedTypeNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeList; @@ -123,6 +124,18 @@ public static function getUnderlyingNamedTypeNode(Node $node): NamedTypeNode return self::getUnderlyingNamedTypeNode($type); } + /** + * Does the given field have an argument of the given name? + * + * @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition + * @param string $name + * @return bool + */ + public static function fieldHasArgument(FieldDefinitionNode $fieldDefinition, string $name): bool + { + return self::firstByName($fieldDefinition->arguments, $name) !== null; + } + /** * Does the given directive have an argument of the given name? * diff --git a/src/Schema/Directives/MethodDirective.php b/src/Schema/Directives/MethodDirective.php index d82c8739f9..071307b2ef 100644 --- a/src/Schema/Directives/MethodDirective.php +++ b/src/Schema/Directives/MethodDirective.php @@ -14,14 +14,25 @@ public static function definition(): string { return /** @lang GraphQL */ <<<'SDL' """ -Call a method with a given `name` on the class that represents a type to resolve a field. -Use this if the data is not accessible as an attribute (e.g. `$model->myData`). +Resolve a field by calling a method on the parent object. + +Use this if the data is not accessible through simple property access or if you +want to pass argument to the method. """ directive @method( """ Specify the method of which to fetch the data from. + Defaults to the name of the field if not given. """ name: String + + """ + Pass the field arguments to the method, using the argument definition + order from the schema to sort them before passing them along. + + @deprecated This behaviour will default to true in v5 and this setting will be removed. + """ + passOrdered: Boolean = false ) on FIELD_DEFINITION SDL; } @@ -34,14 +45,24 @@ public static function definition(): string */ public function resolveField(FieldValue $fieldValue): FieldValue { - /** @var string $method */ - $method = $this->directiveArgValue( - 'name', - $this->nodeName() - ); - return $fieldValue->setResolver( - function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($method) { + function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) { + /** @var string $method */ + $method = $this->directiveArgValue( + 'name', + $this->nodeName() + ); + + // TODO always do this in v5 + if ($this->directiveArgValue('passOrdered')) { + $orderedArgs = []; + foreach ($this->definitionNode->arguments as $argDefinition) { + $orderedArgs [] = $args[$argDefinition->name->value] ?? null; + } + + return call_user_func_array([$root, $method], $orderedArgs); + } + return call_user_func([$root, $method], $root, $args, $context, $resolveInfo); } ); diff --git a/tests/Unit/Schema/Directives/MethodDirectiveTest.php b/tests/Unit/Schema/Directives/MethodDirectiveTest.php index a824348586..b019942cab 100644 --- a/tests/Unit/Schema/Directives/MethodDirectiveTest.php +++ b/tests/Unit/Schema/Directives/MethodDirectiveTest.php @@ -2,65 +2,152 @@ namespace Tests\Unit\Schema\Directives; -use Illuminate\Support\Arr; +use PHPUnit\Framework\MockObject\MockObject; use Tests\TestCase; class MethodDirectiveTest extends TestCase { - protected $schema = ' + protected $schema = /** @lang GraphQL */ ' type Query { - foo: Foo @field(resolver: "Tests\\\Unit\\\Schema\\\Directives\\\MethodDirectiveTest@resolve") - } - - type Foo { - bar(baz: String): String! @method(name: "foobar") + foo: Foo @mock } '; - public function testWillCallAMethodToResolveField(): void + public function testDefaultToFieldNameAsMethodName(): void { - $this->graphQL(' + $this->schema .= /** @lang GraphQL */ ' + type Foo { + bar: ID @method + } + '; + + $foo = $this->mockFoo(); + $foo->expects($this->once()) + ->method('bar') + // TODO remove in v5 + ->with($foo, []); + + $this->graphQL(/** @lang GraphQL */ ' { foo { bar } } - ')->assertJson([ - 'data' => [ - 'foo' => [ - 'bar' => 'foo', - ], - ], - ]); + '); } - public function testWillCallAMethodWithArgsToResolveField(): void + public function testWillPreferExplicitName(): void { - $this->graphQL(' + $this->schema .= /** @lang GraphQL */ ' + type Foo { + asdf: ID @method(name: "bar") + } + '; + + $foo = $this->mockFoo(); + $foo->expects($this->once()) + ->method('bar'); + + $this->graphQL(/** @lang GraphQL */ ' { foo { - bar(baz: "asdf") + asdf } } - ')->assertJson([ - 'data' => [ - 'foo' => [ - 'bar' => 'fooasdf', - ], - ], - ]); + '); } - public function resolve(): Foo + public function testPassArgsInLexicalOrderOfDefinition(): void { - return new Foo; + $this->schema .= /** @lang GraphQL */ ' + type Foo { + bar( + first: ID + second: ID + ): ID @method(passOrdered: true) + } + '; + + $foo = $this->mockFoo(); + $foo->expects($this->once()) + ->method('bar') + ->with(1, 2); + + $this->graphQL(/** @lang GraphQL */ ' + { + foo { + bar( + second: 2 + first: 1 + ) + } + } + '); + } + + public function testPassOrderedDefaultsToNull(): void + { + $this->schema .= /** @lang GraphQL */ ' + type Foo { + bar( + baz: ID + ): ID @method(passOrdered: true) + } + '; + + $foo = $this->mockFoo(); + $foo->expects($this->once()) + ->method('bar') + ->with(null); + + $this->graphQL(/** @lang GraphQL */ ' + { + foo { + bar + } + } + '); + } + + public function testPassOrderedWithNoArgs(): void + { + $this->schema .= /** @lang GraphQL */ ' + type Foo { + bar: ID @method(passOrdered: true) + } + '; + + $foo = $this->mockFoo(); + $foo->expects($this->once()) + ->method('bar') + ->with(); + + $this->graphQL(/** @lang GraphQL */ ' + { + foo { + bar + } + } + '); + } + + protected function mockFoo(): MockObject + { + $foo = $this->createMock(Foo::class); + + $this->mockResolver($foo); + + return $foo; } } +/** + * TODO remove in favour of ->getMockBuilder(\stdClass::class)->addMethods(['__invoke']) + * once we no longer support PHPUnit 7. + */ class Foo { - public function foobar($root, array $args = []): string + public function bar() { - return 'foo'.Arr::get($args, 'baz'); } } From 752708ecd649121efcca4b3237f7cfbf8e9d369c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Luis=20Rojas=20Aragon=C3=A9s?= Date: Wed, 26 Feb 2020 12:50:19 -0600 Subject: [PATCH 09/31] Fix typo in nested mutation docs (#1210) --- docs/master/eloquent/nested-mutations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/master/eloquent/nested-mutations.md b/docs/master/eloquent/nested-mutations.md index 4307035d2f..ea1eb94375 100644 --- a/docs/master/eloquent/nested-mutations.md +++ b/docs/master/eloquent/nested-mutations.md @@ -65,7 +65,7 @@ A nested `BelongsTo` relationship exposes the following operations: - `create` a new related model and attach it - `update` an existing model and attach it - `upsert` a new or an existing model and attach it -- `diconnect` the related model +- `disconnect` the related model - `delete` the related model and the association to it Both `disconnect` and `delete` don't make much sense in the context of an update. From 481f33f960db17286389f538014a727a80637c63 Mon Sep 17 00:00:00 2001 From: spawnia Date: Wed, 26 Feb 2020 19:51:20 +0100 Subject: [PATCH 10/31] Fix typo in older nested mutation docs, too --- docs/4.8/eloquent/nested-mutations.md | 2 +- docs/4.9/eloquent/nested-mutations.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/4.8/eloquent/nested-mutations.md b/docs/4.8/eloquent/nested-mutations.md index 39967a8061..951d534422 100644 --- a/docs/4.8/eloquent/nested-mutations.md +++ b/docs/4.8/eloquent/nested-mutations.md @@ -65,7 +65,7 @@ A nested `BelongsTo` relationship exposes the following operations: - `create` a new related model and attach it - `update` an existing model and attach it - `upsert` a new or an existing model and attach it -- `diconnect` the related model +- `disconnect` the related model - `delete` the related model and the association to it Both `disconnect` and `delete` don't make much sense in the context of an update. diff --git a/docs/4.9/eloquent/nested-mutations.md b/docs/4.9/eloquent/nested-mutations.md index 4307035d2f..ea1eb94375 100644 --- a/docs/4.9/eloquent/nested-mutations.md +++ b/docs/4.9/eloquent/nested-mutations.md @@ -65,7 +65,7 @@ A nested `BelongsTo` relationship exposes the following operations: - `create` a new related model and attach it - `update` an existing model and attach it - `upsert` a new or an existing model and attach it -- `diconnect` the related model +- `disconnect` the related model - `delete` the related model and the association to it Both `disconnect` and `delete` don't make much sense in the context of an update. From 6a0f8179efc727d2e0fd5c17d552fb734e8d8002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20S=C3=B8gaard?= Date: Thu, 27 Feb 2020 18:38:32 +0100 Subject: [PATCH 11/31] Correct link to FieldMiddleware interface in docs (#1212) --- docs/4.9/custom-directives/field-directives.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/4.9/custom-directives/field-directives.md b/docs/4.9/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/4.9/custom-directives/field-directives.md +++ b/docs/4.9/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver From 3b8bd65c394135646cb7cb8de043e2ded813635e Mon Sep 17 00:00:00 2001 From: spawnia Date: Thu, 27 Feb 2020 18:39:01 +0100 Subject: [PATCH 12/31] Correct link to FieldMiddleware interface in all versions --- docs/3.6/guides/custom-directives.md | 2 +- docs/3.7/custom-directives/field-directives.md | 2 +- docs/4.0/custom-directives/field-directives.md | 2 +- docs/4.1/custom-directives/field-directives.md | 2 +- docs/4.2/custom-directives/field-directives.md | 2 +- docs/4.3/custom-directives/field-directives.md | 2 +- docs/4.4/custom-directives/field-directives.md | 2 +- docs/4.5/custom-directives/field-directives.md | 2 +- docs/4.6/custom-directives/field-directives.md | 2 +- docs/4.7/custom-directives/field-directives.md | 2 +- docs/4.8/custom-directives/field-directives.md | 2 +- docs/master/custom-directives/field-directives.md | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/3.6/guides/custom-directives.md b/docs/3.6/guides/custom-directives.md index 2f58fdc228..54726b060d 100644 --- a/docs/3.6/guides/custom-directives.md +++ b/docs/3.6/guides/custom-directives.md @@ -60,7 +60,7 @@ It can be a great way to reuse resolver logic within a schema. ### FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/3.7/custom-directives/field-directives.md b/docs/3.7/custom-directives/field-directives.md index 71ce9d11e2..60f05cd02b 100644 --- a/docs/3.7/custom-directives/field-directives.md +++ b/docs/3.7/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/4.0/custom-directives/field-directives.md b/docs/4.0/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/4.0/custom-directives/field-directives.md +++ b/docs/4.0/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/4.1/custom-directives/field-directives.md b/docs/4.1/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/4.1/custom-directives/field-directives.md +++ b/docs/4.1/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/4.2/custom-directives/field-directives.md b/docs/4.2/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/4.2/custom-directives/field-directives.md +++ b/docs/4.2/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/4.3/custom-directives/field-directives.md b/docs/4.3/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/4.3/custom-directives/field-directives.md +++ b/docs/4.3/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/4.4/custom-directives/field-directives.md b/docs/4.4/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/4.4/custom-directives/field-directives.md +++ b/docs/4.4/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/4.5/custom-directives/field-directives.md b/docs/4.5/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/4.5/custom-directives/field-directives.md +++ b/docs/4.5/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/4.6/custom-directives/field-directives.md b/docs/4.6/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/4.6/custom-directives/field-directives.md +++ b/docs/4.6/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/4.7/custom-directives/field-directives.md b/docs/4.7/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/4.7/custom-directives/field-directives.md +++ b/docs/4.7/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/4.8/custom-directives/field-directives.md b/docs/4.8/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/4.8/custom-directives/field-directives.md +++ b/docs/4.8/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver diff --git a/docs/master/custom-directives/field-directives.md b/docs/master/custom-directives/field-directives.md index 344f187774..054cfe490c 100644 --- a/docs/master/custom-directives/field-directives.md +++ b/docs/master/custom-directives/field-directives.md @@ -11,7 +11,7 @@ It can be a great way to reuse resolver logic within a schema. ## FieldMiddleware -A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/Support/Contracts/FieldMiddleware.php) directive allows you +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). You may use it both to handle incoming values before reaching the final resolver From 468b697214c3a99056f05f981249afbe06b204d2 Mon Sep 17 00:00:00 2001 From: Aldo-f Date: Mon, 2 Mar 2020 09:03:59 +0100 Subject: [PATCH 13/31] =?UTF-8?q?Replace=20duplicate=20`CreateAuthorInput`?= =?UTF-8?q?=20in=20docs=20with=20`UpdateAutho=E2=80=A6=20(#1215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/3.6/guides/relationships.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/3.6/guides/relationships.md b/docs/3.6/guides/relationships.md index d25ed2944a..a148a6f2cd 100644 --- a/docs/3.6/guides/relationships.md +++ b/docs/3.6/guides/relationships.md @@ -604,8 +604,9 @@ input CreateAuthorInput { name: String! } -input CreateAuthorInput { - name: String! +input UpdateAuthorInput { + id: ID! + name: String } ``` From a975e7b759cb699507f2cbc30c488782dfa083b7 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Mon, 2 Mar 2020 09:04:47 +0100 Subject: [PATCH 14/31] Replace more duplicate `CreateAuthorInput` in docs with `UpdateAuthorInput` --- docs/3.0/guides/relationships.md | 5 +++-- docs/3.1/guides/relationships.md | 5 +++-- docs/3.2/guides/relationships.md | 5 +++-- docs/3.3/guides/relationships.md | 5 +++-- docs/3.4/guides/relationships.md | 5 +++-- docs/3.5/guides/relationships.md | 5 +++-- docs/3.7/eloquent/nested-mutations.md | 5 +++-- 7 files changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/3.0/guides/relationships.md b/docs/3.0/guides/relationships.md index 7091de3881..27a74dd41c 100644 --- a/docs/3.0/guides/relationships.md +++ b/docs/3.0/guides/relationships.md @@ -582,8 +582,9 @@ input CreateAuthorInput { name: String! } -input CreateAuthorInput { - name: String! +input UpdateAuthorInput { + id: ID! + name: String } ``` diff --git a/docs/3.1/guides/relationships.md b/docs/3.1/guides/relationships.md index 7091de3881..27a74dd41c 100644 --- a/docs/3.1/guides/relationships.md +++ b/docs/3.1/guides/relationships.md @@ -582,8 +582,9 @@ input CreateAuthorInput { name: String! } -input CreateAuthorInput { - name: String! +input UpdateAuthorInput { + id: ID! + name: String } ``` diff --git a/docs/3.2/guides/relationships.md b/docs/3.2/guides/relationships.md index 73669fc1f3..ca6d3e9b92 100644 --- a/docs/3.2/guides/relationships.md +++ b/docs/3.2/guides/relationships.md @@ -583,8 +583,9 @@ input CreateAuthorInput { name: String! } -input CreateAuthorInput { - name: String! +input UpdateAuthorInput { + id: ID! + name: String } ``` diff --git a/docs/3.3/guides/relationships.md b/docs/3.3/guides/relationships.md index 6b5dc88f2c..0b0ac3cf49 100644 --- a/docs/3.3/guides/relationships.md +++ b/docs/3.3/guides/relationships.md @@ -604,8 +604,9 @@ input CreateAuthorInput { name: String! } -input CreateAuthorInput { - name: String! +input UpdateAuthorInput { + id: ID! + name: String } ``` diff --git a/docs/3.4/guides/relationships.md b/docs/3.4/guides/relationships.md index 6b5dc88f2c..0b0ac3cf49 100644 --- a/docs/3.4/guides/relationships.md +++ b/docs/3.4/guides/relationships.md @@ -604,8 +604,9 @@ input CreateAuthorInput { name: String! } -input CreateAuthorInput { - name: String! +input UpdateAuthorInput { + id: ID! + name: String } ``` diff --git a/docs/3.5/guides/relationships.md b/docs/3.5/guides/relationships.md index 6b5dc88f2c..0b0ac3cf49 100644 --- a/docs/3.5/guides/relationships.md +++ b/docs/3.5/guides/relationships.md @@ -604,8 +604,9 @@ input CreateAuthorInput { name: String! } -input CreateAuthorInput { - name: String! +input UpdateAuthorInput { + id: ID! + name: String } ``` diff --git a/docs/3.7/eloquent/nested-mutations.md b/docs/3.7/eloquent/nested-mutations.md index bc168933b5..5eaf370916 100644 --- a/docs/3.7/eloquent/nested-mutations.md +++ b/docs/3.7/eloquent/nested-mutations.md @@ -468,8 +468,9 @@ input CreateAuthorInput { name: String! } -input CreateAuthorInput { - name: String! +input UpdateAuthorInput { + id: ID! + name: String } ``` From ac522044e67d246ff50913dcb84aad75aa704b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20S=C3=B8gaard?= Date: Wed, 4 Mar 2020 09:18:22 +0100 Subject: [PATCH 15/31] Access nested inputs with dot notation in `find` option of `@can` #1216 --- CHANGELOG.md | 1 + docs/4.9/api-reference/directives.md | 2 + src/Schema/Directives/CanDirective.php | 10 +- .../Schema/Directives/CanDirectiveDBTest.php | 112 ++++++++++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cfccf7528..4c9e00b4d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ You can find and compare releases at the [GitHub release page](https://github.co ### Added +- Access nested inputs with dot notation using the `find` option of `@can` https://github.com/nuwave/lighthouse/pull/1216 - Add `@hash` directive which uses Laravel's hashing configuration https://github.com/nuwave/lighthouse/pull/1200 - Add option `passOrdered` to `@method` to pass just the arguments as ordered parameters https://github.com/nuwave/lighthouse/pull/1208 diff --git a/docs/4.9/api-reference/directives.md b/docs/4.9/api-reference/directives.md index de1c2f1fc9..2983b458fa 100644 --- a/docs/4.9/api-reference/directives.md +++ b/docs/4.9/api-reference/directives.md @@ -452,6 +452,8 @@ directive @can( """ The name of the argument that is used to find a specific model instance against which the permissions should be checked. + + You may pass the string as a dot notation to search in a array. """ find: String diff --git a/src/Schema/Directives/CanDirective.php b/src/Schema/Directives/CanDirective.php index 83ec7a1f4b..973d3097bd 100644 --- a/src/Schema/Directives/CanDirective.php +++ b/src/Schema/Directives/CanDirective.php @@ -6,7 +6,9 @@ use GraphQL\Type\Definition\ResolveInfo; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Arr; use Nuwave\Lighthouse\Exceptions\AuthorizationException; +use Nuwave\Lighthouse\Exceptions\DefinitionException; use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\SoftDeletes\ForceDeleteDirective; @@ -52,6 +54,8 @@ public static function definition(): string """ The name of the argument that is used to find a specific model instance against which the permissions should be checked. + + You may pass the string as a dot notation to search in a array. """ find: String @@ -115,6 +119,10 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) protected function modelsToCheck(ArgumentSet $argumentSet, array $args): iterable { if ($find = $this->directiveArgValue('find')) { + if (($findValue = Arr::get($args, $find)) === null) { + throw new DefinitionException("Could not find key: \"${find}\". The key must be a non-null field"); + } + $queryBuilder = $this->getModelClass()::query(); $directivesContainsForceDelete = $argumentSet->directives->contains( @@ -143,7 +151,7 @@ function (Directive $directive): bool { return $directive instanceof TrashedDirective; } ) - ->findOrFail($args[$find]); + ->findOrFail($findValue); if ($modelOrModels instanceof Model) { $modelOrModels = [$modelOrModels]; diff --git a/tests/Integration/Schema/Directives/CanDirectiveDBTest.php b/tests/Integration/Schema/Directives/CanDirectiveDBTest.php index a451592337..f6a5075734 100644 --- a/tests/Integration/Schema/Directives/CanDirectiveDBTest.php +++ b/tests/Integration/Schema/Directives/CanDirectiveDBTest.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; use Nuwave\Lighthouse\Exceptions\AuthorizationException; +use Nuwave\Lighthouse\Exceptions\DefinitionException; use Tests\DBTestCase; use Tests\Utils\Models\Post; use Tests\Utils\Models\Task; @@ -87,6 +88,117 @@ public function testFailsToFindSpecificModel(): void '); } + public function testThrowsIfInvalidFindKey() + { + $this->be( + new User([ + 'name' => UserPolicy::ADMIN, + ]) + ); + + $user = factory(User::class)->create([ + 'name' => 'foo', + ]); + + $this->schema = /** @lang GraphQL */' + type Query { + user(id: ID @eq): User + @can(ability: "view", find: "INVALID_KEY") + @first + } + + type User { + id: ID! + name: String! + } + '; + + $this->expectException(DefinitionException::class); + $this->graphQL(/** @lang GraphQL */ " + { + user(id: {$user->getKey()}) { + name + } + }"); + } + + public function testThrowsIfNullFindValue() + { + $this->be( + new User([ + 'name' => UserPolicy::ADMIN, + ]) + ); + + $user = factory(User::class)->create([ + 'name' => 'foo', + ]); + + $this->schema = /** @lang GraphQL */ + ' + type Query { + user(id: ID @eq, queriedUserId: ID): User + @can(ability: "view", find: "queriedUserId") + @first + } + + type User { + id: ID! + name: String! + } + '; + + $this->expectException(DefinitionException::class); + $this->graphQL(/** @lang GraphQL */ " + { + user(id: {$user->getKey()}, queriedUserId: null) { + name + } + }"); + } + + public function testNestedQueriesForSpecificModel(): void + { + $user = factory(User::class)->create([ + 'name' => 'foo', + ]); + $this->be($user); + + $this->schema = /** @lang GraphQL */ + ' + type Query { + user(input: FindUserInput): User + @can(ability: "view", find: "input.id") + @first + } + + type User { + id: ID! + name: String! + } + + input FindUserInput { + id: ID + } + '; + + $this->graphQL(/** @lang GraphQL */ " + { + user(input: { + id: {$user->getKey()} + }) { + name + } + } + ")->assertJson([ + 'data' => [ + 'user' => [ + 'name' => 'foo', + ], + ], + ]); + } + public function testThrowsIfNotAuthorized(): void { $this->be( From 9996edd3ffeed04eadfed483022064bda1b306b1 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 4 Mar 2020 10:27:19 +0100 Subject: [PATCH 16/31] Simplify CanDirectiveDBTest.php (#1220) --- src/Schema/Directives/CanDirective.php | 14 +++- .../Schema/Directives/CanDirectiveDBTest.php | 80 ++++++------------- 2 files changed, 34 insertions(+), 60 deletions(-) diff --git a/src/Schema/Directives/CanDirective.php b/src/Schema/Directives/CanDirective.php index 973d3097bd..a632c95c7a 100644 --- a/src/Schema/Directives/CanDirective.php +++ b/src/Schema/Directives/CanDirective.php @@ -3,12 +3,12 @@ namespace Nuwave\Lighthouse\Schema\Directives; use Closure; +use GraphQL\Error\Error; use GraphQL\Type\Definition\ResolveInfo; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Nuwave\Lighthouse\Exceptions\AuthorizationException; -use Nuwave\Lighthouse\Exceptions\DefinitionException; use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\SoftDeletes\ForceDeleteDirective; @@ -114,13 +114,14 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) * @param array $args * @return iterable * - * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException + * @throws \GraphQL\Error\Error */ protected function modelsToCheck(ArgumentSet $argumentSet, array $args): iterable { if ($find = $this->directiveArgValue('find')) { - if (($findValue = Arr::get($args, $find)) === null) { - throw new DefinitionException("Could not find key: \"${find}\". The key must be a non-null field"); + $findValue = Arr::get($args, $find); + if ($findValue === null) { + throw new Error(self::missingKeyToFindModel($find)); } $queryBuilder = $this->getModelClass()::query(); @@ -163,6 +164,11 @@ function (Directive $directive): bool { return [$this->getModelClass()]; } + public static function missingKeyToFindModel(string $find): string + { + return "Got no key to find a model at the expected input path: ${find}."; + } + /** * @param \Illuminate\Contracts\Auth\Access\Gate $gate * @param string|string[] $ability diff --git a/tests/Integration/Schema/Directives/CanDirectiveDBTest.php b/tests/Integration/Schema/Directives/CanDirectiveDBTest.php index f6a5075734..a19606996a 100644 --- a/tests/Integration/Schema/Directives/CanDirectiveDBTest.php +++ b/tests/Integration/Schema/Directives/CanDirectiveDBTest.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; use Nuwave\Lighthouse\Exceptions\AuthorizationException; -use Nuwave\Lighthouse\Exceptions\DefinitionException; +use Nuwave\Lighthouse\Schema\Directives\CanDirective; use Tests\DBTestCase; use Tests\Utils\Models\Post; use Tests\Utils\Models\Task; @@ -64,8 +64,7 @@ public function testFailsToFindSpecificModel(): void $this->never() ); - $this->schema = /** @lang GraphQL */ - ' + $this->schema = /** @lang GraphQL */ ' type Query { user(id: ID @eq): User @can(ability: "view", find: "id") @@ -88,7 +87,7 @@ public function testFailsToFindSpecificModel(): void '); } - public function testThrowsIfInvalidFindKey() + public function testThrowsIfFindValueIsNotGiven(): void { $this->be( new User([ @@ -96,14 +95,10 @@ public function testThrowsIfInvalidFindKey() ]) ); - $user = factory(User::class)->create([ - 'name' => 'foo', - ]); - - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type Query { - user(id: ID @eq): User - @can(ability: "view", find: "INVALID_KEY") + user(id: ID): User + @can(ability: "view", find: "some.path") @first } @@ -113,59 +108,30 @@ public function testThrowsIfInvalidFindKey() } '; - $this->expectException(DefinitionException::class); - $this->graphQL(/** @lang GraphQL */ " + $this->graphQL(/** @lang GraphQL */ ' { - user(id: {$user->getKey()}) { + user { name } - }"); - } - - public function testThrowsIfNullFindValue() - { - $this->be( - new User([ - 'name' => UserPolicy::ADMIN, - ]) - ); - - $user = factory(User::class)->create([ - 'name' => 'foo', - ]); - - $this->schema = /** @lang GraphQL */ - ' - type Query { - user(id: ID @eq, queriedUserId: ID): User - @can(ability: "view", find: "queriedUserId") - @first } + ')->assertJson([ + 'errors' => [ + [ - type User { - id: ID! - name: String! - } - '; - - $this->expectException(DefinitionException::class); - $this->graphQL(/** @lang GraphQL */ " - { - user(id: {$user->getKey()}, queriedUserId: null) { - name - } - }"); + 'message' => CanDirective::missingKeyToFindModel('some.path'), + ], + ], + ]); } - public function testNestedQueriesForSpecificModel(): void + public function testFindUsingNestedInputWithDotNotation(): void { $user = factory(User::class)->create([ 'name' => 'foo', ]); $this->be($user); - $this->schema = /** @lang GraphQL */ - ' + $this->schema = /** @lang GraphQL */ ' type Query { user(input: FindUserInput): User @can(ability: "view", find: "input.id") @@ -176,21 +142,23 @@ public function testNestedQueriesForSpecificModel(): void id: ID! name: String! } - + input FindUserInput { id: ID } '; - $this->graphQL(/** @lang GraphQL */ " - { + $this->graphQL(/** @lang GraphQL */ ' + query ($id: ID){ user(input: { - id: {$user->getKey()} + id: $id }) { name } } - ")->assertJson([ + ', [ + 'id' => $user->id, + ])->assertJson([ 'data' => [ 'user' => [ 'name' => 'foo', From ce4340a969845663c7f8cc0dc860e8b7f6164aa9 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 4 Mar 2020 20:46:21 +0100 Subject: [PATCH 17/31] Restore feature parity in Lumen test helper (#1222) --- CHANGELOG.md | 3 +- src/Testing/MakesGraphQLRequests.php | 2 +- src/Testing/MakesGraphQLRequestsLumen.php | 50 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c9e00b4d6..330d6e46da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ You can find and compare releases at the [GitHub release page](https://github.co - Access nested inputs with dot notation using the `find` option of `@can` https://github.com/nuwave/lighthouse/pull/1216 - Add `@hash` directive which uses Laravel's hashing configuration https://github.com/nuwave/lighthouse/pull/1200 - Add option `passOrdered` to `@method` to pass just the arguments as ordered parameters https://github.com/nuwave/lighthouse/pull/1208 +- Add support to extend `input`, `interface` and `enum` types https://github.com/nuwave/lighthouse/pull/1203 +- Implement `streamGraphQL()` helper in `\Nuwave\Lighthouse\Testing\MakesGraphQLRequestsLumen` https://github.com/nuwave/lighthouse/pull/1222 ### Deprecated @@ -38,7 +40,6 @@ You can find and compare releases at the [GitHub release page](https://github.co get the arguments passed through a client directive https://github.com/nuwave/lighthouse/pull/1184 - Add `streamGraphQL()` helper method to `\Nuwave\Lighthouse\Testing\MakesGraphQLRequests` for simple testing of streamed responses, such as `@defer` https://github.com/nuwave/lighthouse/pull/1184 -- Add support to extend `input`, `interface` and `enum` types https://github.com/nuwave/lighthouse/pull/1203 ### Fixed diff --git a/src/Testing/MakesGraphQLRequests.php b/src/Testing/MakesGraphQLRequests.php index 2655629557..dc46cdb2ef 100644 --- a/src/Testing/MakesGraphQLRequests.php +++ b/src/Testing/MakesGraphQLRequests.php @@ -152,7 +152,7 @@ protected function introspectByName(string $path, string $name): ?array return Arr::first( $results, - function (array $result) use ($name): bool { + static function (array $result) use ($name): bool { return $result['name'] === $name; } ); diff --git a/src/Testing/MakesGraphQLRequestsLumen.php b/src/Testing/MakesGraphQLRequestsLumen.php index f4258c37be..16d68fb03e 100644 --- a/src/Testing/MakesGraphQLRequestsLumen.php +++ b/src/Testing/MakesGraphQLRequestsLumen.php @@ -4,6 +4,10 @@ use GraphQL\Type\Introspection; use Illuminate\Support\Arr; +use Nuwave\Lighthouse\Support\Contracts\CanStreamResponse; +use Nuwave\Lighthouse\Support\Http\Responses\MemoryStream; +use PHPUnit\Framework\Assert; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * Useful helpers for PHPUnit testing. @@ -22,6 +26,13 @@ trait MakesGraphQLRequestsLumen */ protected $introspectionResult; + /** + * Used to test deferred queries. + * + * @var \Nuwave\Lighthouse\Support\Http\Responses\MemoryStream|null + */ + protected $deferStream; + /** * Execute a query as if it was sent as a request to the server. * @@ -162,4 +173,43 @@ protected function graphQLEndpointUrl(): string { return config('lighthouse.route.uri'); } + + /** + * Send the query and capture all chunks of the streamed response. + * + * @param string $query + * @param array|null $variables + * @param array $extraParams + * @return array + */ + protected function streamGraphQL(string $query, array $variables = null, array $extraParams = []): array + { + if ($this->deferStream === null) { + $this->setUpDeferStream(); + } + + $response = $this->graphQL($query, $variables, $extraParams); + + if (! $response->response instanceof StreamedResponse) { + Assert::fail('Expected the response to be a streamed response but got a regular response.'); + } + + $response->response->send(); + + return $this->deferStream->chunks; + } + + /** + * Set up the stream to make queries with @defer. + * + * @return void + */ + protected function setUpDeferStream(): void + { + $this->deferStream = new MemoryStream; + + app()->singleton(CanStreamResponse::class, function (): MemoryStream { + return $this->deferStream; + }); + } } From 14ef616cb1116f8a621bdfbffcd8be69899e7adc Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 4 Mar 2020 20:46:34 +0100 Subject: [PATCH 18/31] Add Utils::classUsesTrait (#1221) --- src/SoftDeletes/SoftDeletesServiceProvider.php | 8 ++------ src/SoftDeletes/TrashedDirective.php | 11 ++++------- src/Support/Utils.php | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/SoftDeletes/SoftDeletesServiceProvider.php b/src/SoftDeletes/SoftDeletesServiceProvider.php index 9afb412e9b..2bbfffc438 100644 --- a/src/SoftDeletes/SoftDeletesServiceProvider.php +++ b/src/SoftDeletes/SoftDeletesServiceProvider.php @@ -9,6 +9,7 @@ use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces; use Nuwave\Lighthouse\Exceptions\DefinitionException; use Nuwave\Lighthouse\Schema\AST\PartialParser; +use Nuwave\Lighthouse\Support\Utils; class SoftDeletesServiceProvider extends ServiceProvider { @@ -24,12 +25,7 @@ class SoftDeletesServiceProvider extends ServiceProvider */ public static function assertModelUsesSoftDeletes(string $modelClass, string $exceptionMessage): void { - if ( - ! in_array( - SoftDeletes::class, - class_uses_recursive($modelClass) - ) - ) { + if (! Utils::classUsesTrait($modelClass, SoftDeletes::class)) { throw new DefinitionException($exceptionMessage); } } diff --git a/src/SoftDeletes/TrashedDirective.php b/src/SoftDeletes/TrashedDirective.php index c0650f1631..9e12edeb7a 100644 --- a/src/SoftDeletes/TrashedDirective.php +++ b/src/SoftDeletes/TrashedDirective.php @@ -3,9 +3,7 @@ namespace Nuwave\Lighthouse\SoftDeletes; use Illuminate\Database\Eloquent\Relations\Relation; -use Illuminate\Database\Eloquent\SoftDeletes; use Laravel\Scout\Builder as ScoutBuilder; -use Nuwave\Lighthouse\Exceptions\DefinitionException; use Nuwave\Lighthouse\Schema\Directives\BaseDirective; use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective; use Nuwave\Lighthouse\Support\Contracts\DefinedDirective; @@ -42,11 +40,10 @@ public function handleBuilder($builder, $value) $model = $builder->getModel(); } - if (! in_array(SoftDeletes::class, class_uses_recursive($model))) { - throw new DefinitionException( - self::MODEL_MUST_USE_SOFT_DELETES - ); - } + SoftDeletesServiceProvider::assertModelUsesSoftDeletes( + get_class($model), + self::MODEL_MUST_USE_SOFT_DELETES + ); if (! isset($value)) { return $builder; diff --git a/src/Support/Utils.php b/src/Support/Utils.php index 549520badb..89f4273121 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -95,4 +95,19 @@ public static function applyEach(\Closure $callback, $valueOrValues) return array_map($callback, $valueOrValues); } + + /** + * Determine if a class uses a trait. + * + * @param object|string $class + * @param string $trait + * @return bool + */ + public static function classUsesTrait($class, string $trait): bool + { + return in_array( + $trait, + class_uses_recursive($class) + ); + } } From 6227dbdb8de2b14c65d9f892261f3ef7382a079d Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Thu, 5 Mar 2020 19:54:04 +0100 Subject: [PATCH 19/31] Support Laravel 7 (#1219) --- .github/workflows/continuous-integration.yml | 58 +++++++++++++---- CHANGELOG.md | 1 + _ide_helper.php | 26 ++++++++ composer.json | 24 +++---- docs/master/testing/phpunit.md | 2 - phpstan.neon | 30 +++++++-- src/LighthouseServiceProvider.php | 3 +- src/Support/AppVersion.php | 39 +++++++++++ src/Support/Http/routes.php | 4 +- src/Testing/MakesGraphQLRequests.php | 19 +++--- tests/Integration/IntrospectionTest.php | 3 +- .../WhereJsonContainsDirectiveDBTest.php | 5 +- .../Schema/Types/LaravelEnumTypeDBTest.php | 65 ++++++++++--------- .../Subscriptions/SubscriptionTest.php | 12 ++-- tests/Integration/ValidationTest.php | 5 +- tests/Laravel7ExceptionHandler.php | 29 +++++++++ tests/PreLaravel7ExceptionHandler.php | 29 +++++++++ tests/SerializingArrayStore.php | 27 ++++++++ tests/TestCase.php | 35 ++++------ tests/TestResponseMixin.php | 5 +- .../Schema/Directives/CanDirectiveTest.php | 3 +- .../Unit/Schema/Types/LaravelEnumTypeTest.php | 19 ++++-- tests/Utils/LaravelEnums/AOrB.php | 11 ++++ tests/Utils/LaravelEnums/UserType.php | 11 ---- tests/Utils/Models/User.php | 8 --- tests/Utils/Models/WithEnum.php | 19 ++++++ tests/Utils/Queries/FooInvoke.php | 1 + ...28_000002_create_testbench_users_table.php | 2 - ...0002_create_testbench_with_enums_table.php | 28 ++++++++ 29 files changed, 380 insertions(+), 143 deletions(-) create mode 100644 src/Support/AppVersion.php create mode 100644 tests/Laravel7ExceptionHandler.php create mode 100644 tests/PreLaravel7ExceptionHandler.php create mode 100644 tests/Utils/LaravelEnums/AOrB.php delete mode 100644 tests/Utils/LaravelEnums/UserType.php create mode 100644 tests/Utils/Models/WithEnum.php create mode 100644 tests/database/migrations/2018_02_28_000002_create_testbench_with_enums_table.php diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index cca1cd5bf6..817421b287 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -22,14 +22,14 @@ jobs: php-version: - "7.4" laravel-version: - - "^6.0" + - "^7.0" steps: - name: "Checkout" uses: "actions/checkout@v2.0.0" - name: "Install PHP with extensions" - uses: "shivammathur/setup-php@1.7.2" + uses: "shivammathur/setup-php@2.1.0" with: coverage: "none" extensions: "${{ env.REQUIRED_PHP_EXTENSIONS }}" @@ -42,6 +42,10 @@ jobs: key: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}" restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}-" + - name: "Remove bensampo/laravel-enum for incompatible versions" + if: "matrix.laravel-version == '^7.0'" + run: "composer remove --dev bensampo/laravel-enum --no-update" + - name: "Install dependencies with composer" run: "composer require illuminate/contracts:${{ matrix.laravel-version }} --no-interaction --prefer-dist --no-interaction --no-suggest" @@ -56,16 +60,28 @@ jobs: strategy: matrix: php-version: + - "7.1" + - "7.2" + - "7.3" - "7.4" laravel-version: + - "5.6.*" + - "5.7.*" + - "5.8.*" - "^6.0" + - "^7.0" + exclude: + - php-version: "7.1" + laravel-version: "^6.0" + - php-version: "7.1" + laravel-version: "^7.0" steps: - name: "Checkout" uses: "actions/checkout@v2.0.0" - name: "Install PHP with extensions" - uses: "shivammathur/setup-php@1.7.2" + uses: "shivammathur/setup-php@2.1.0" with: coverage: "none" extensions: "${{ env.REQUIRED_PHP_EXTENSIONS }}" @@ -78,12 +94,13 @@ jobs: key: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}" restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}-" + - name: "Remove bensampo/laravel-enum for incompatible versions" + if: "matrix.laravel-version == '^7.0'" + run: "composer remove --dev bensampo/laravel-enum --no-update" + - name: "Install dependencies with composer" run: "composer require illuminate/contracts:${{ matrix.laravel-version }} --no-interaction --prefer-dist --no-interaction --no-suggest" - - name: "Create cache directory for phpstan/phpstan" - run: "mkdir -p .build/phpstan" - - name: "Run phpstan/phpstan" run: "vendor/bin/phpstan analyse --configuration=phpstan.neon" @@ -105,9 +122,12 @@ jobs: - "5.7.*" - "5.8.*" - "^6.0" + - "^7.0" exclude: - php-version: "7.1" laravel-version: "^6.0" + - php-version: "7.1" + laravel-version: "^7.0" - php-version: "7.4" laravel-version: "5.5.*" @@ -116,7 +136,7 @@ jobs: uses: "actions/checkout@v2.0.0" - name: "Install PHP with extensions" - uses: "shivammathur/setup-php@1.7.2" + uses: "shivammathur/setup-php@2.1.0" with: coverage: "none" extensions: "${{ env.REQUIRED_PHP_EXTENSIONS }}" @@ -129,10 +149,14 @@ jobs: key: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}" restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}-" - - name: "Remove larastan for Laravel 5.5" - if: "matrix.laravel-version == '5.5.*'" + - name: "Remove larastan for old Laravel versions" + if: "matrix.laravel-version != '^7.0'" run: "composer remove --dev nunomaduro/larastan --no-update" + - name: "Remove bensampo/laravel-enum for incompatible versions" + if: "matrix.laravel-version == '^7.0'" + run: "composer remove --dev bensampo/laravel-enum --no-update" + - name: "Install dependencies with composer" run: "composer require illuminate/contracts:${{ matrix.laravel-version }} --no-interaction --prefer-dist --no-interaction --no-suggest" @@ -152,14 +176,14 @@ jobs: php-version: - "7.4" laravel-version: - - "^6.0" + - "^7.0" steps: - name: "Checkout" uses: "actions/checkout@v2.0.0" - name: "Install PHP with extensions" - uses: "shivammathur/setup-php@1.7.2" + uses: "shivammathur/setup-php@2.1.0" with: coverage: "pcov" extensions: "${{ env.REQUIRED_PHP_EXTENSIONS }}" @@ -172,6 +196,10 @@ jobs: key: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}" restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}-" + - name: "Remove bensampo/laravel-enum for incompatible versions" + if: "matrix.laravel-version == '^7.0'" + run: "composer remove --dev bensampo/laravel-enum --no-update" + - name: "Install dependencies with composer" run: "composer require illuminate/contracts:${{ matrix.laravel-version }} --no-interaction --prefer-dist --no-interaction --no-suggest" @@ -196,14 +224,14 @@ jobs: php-version: - "7.4" laravel-version: - - "^6.0" + - "^7.0" steps: - name: "Checkout" uses: "actions/checkout@v2.0.0" - name: "Install PHP with extensions" - uses: "shivammathur/setup-php@1.7.2" + uses: "shivammathur/setup-php@2.1.0" with: coverage: "none" extensions: "${{ env.REQUIRED_PHP_EXTENSIONS }}" @@ -216,6 +244,10 @@ jobs: key: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}" restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}-" + - name: "Remove bensampo/laravel-enum for incompatible versions" + if: "matrix.laravel-version == '^7.0'" + run: "composer remove --dev bensampo/laravel-enum --no-update" + - name: "Install dependencies with composer" run: "composer require illuminate/contracts:${{ matrix.laravel-version }} --no-interaction --prefer-dist --no-interaction --no-suggest" diff --git a/CHANGELOG.md b/CHANGELOG.md index 330d6e46da..1aced3a49a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ You can find and compare releases at the [GitHub release page](https://github.co - Add option `passOrdered` to `@method` to pass just the arguments as ordered parameters https://github.com/nuwave/lighthouse/pull/1208 - Add support to extend `input`, `interface` and `enum` types https://github.com/nuwave/lighthouse/pull/1203 - Implement `streamGraphQL()` helper in `\Nuwave\Lighthouse\Testing\MakesGraphQLRequestsLumen` https://github.com/nuwave/lighthouse/pull/1222 +- Support Laravel 7 https://github.com/nuwave/lighthouse/pull/1219 ### Deprecated diff --git a/_ide_helper.php b/_ide_helper.php index 2780715664..718cf6e59e 100644 --- a/_ide_helper.php +++ b/_ide_helper.php @@ -26,6 +26,32 @@ public function jsonGet(string $key = null) { } } +namespace Illuminate\Testing { + class TestResponse + { + /** + * Asserts that the response contains an error from a given category. + * + * @param string $category + * @return $this + */ + public function assertErrorCategory(string $category): self + { + return $this; + } + + /** + * Just here for compatibility with Laravel 5.5, delete once we drop support. + * + * @param string|null $key + * @return mixed + */ + public function jsonGet(string $key = null) { + return; + } + } +} + namespace GraphQL\Type\Definition { class ResolveInfo { diff --git a/composer.json b/composer.json index dc8914ac6e..9ca95abb36 100644 --- a/composer.json +++ b/composer.json @@ -25,25 +25,27 @@ "require": { "php": ">= 7.1", "ext-json": "*", - "illuminate/contracts": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0", - "illuminate/http": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0", - "illuminate/pagination": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0", - "illuminate/routing": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0", - "illuminate/support": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0", - "illuminate/validation": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0", + "illuminate/contracts": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", + "illuminate/http": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", + "illuminate/pagination": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", + "illuminate/routing": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", + "illuminate/support": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", + "illuminate/validation": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", "webonyx/graphql-php": "^0.13.2" }, "require-dev": { - "bensampo/laravel-enum": "^1.28", + "bensampo/laravel-enum": "^1.28.3", + "composer/composer": "1.10.0-RC as 1.9.3", "ergebnis/composer-normalize": "^2.2.2", "haydenpierce/class-finder": "^0.4.0", - "laravel/lumen-framework": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0", + "laravel/framework": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", + "laravel/lumen-framework": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || dev-master", "laravel/scout": "^4.0 || ^5.0 || ^6.0 || ^7.0", "mll-lab/graphql-php-scalars": "^2.1", "mockery/mockery": "^1.0", - "nunomaduro/larastan": "^0.4.1", - "orchestra/database": "3.5.* || 3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4.0", - "orchestra/testbench": "3.5.* || 3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4.0", + "nunomaduro/larastan": "^0.4.3 || ^0.5.2", + "orchestra/database": "3.5.* || 3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4.0 || 5.x-dev", + "orchestra/testbench": "3.5.* || 3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4.0 || 5.x-dev", "phpbench/phpbench": "@dev", "phpunit/phpunit": "^6.5 || ^7.5 || ^8.4", "pusher/pusher-php-server": "^3.2" diff --git a/docs/master/testing/phpunit.md b/docs/master/testing/phpunit.md index 2172f8380a..5b96ab2be3 100644 --- a/docs/master/testing/phpunit.md +++ b/docs/master/testing/phpunit.md @@ -31,7 +31,6 @@ The `graphQL` test helper runs a query on your GraphQL endpoint and returns a `T ```php public function testQueriesPosts(): void { - /** @var \Illuminate\Foundation\Testing\TestResponse $response */ $response = $this->graphQL(/** @lang GraphQL */ ' { posts { @@ -48,7 +47,6 @@ If you want to use variables within your query, pass an associative array as the ```php public function testCreatePost(): void { - /** @var \Illuminate\Foundation\Testing\TestResponse $response */ $response = $this->graphQL(/** @lang GraphQL */ ' mutation CreatePost($title: String!) { createPost(title: $title) { diff --git a/phpstan.neon b/phpstan.neon index 3b94a2961f..06dffa1428 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,6 @@ includes: - vendor/nunomaduro/larastan/extension.neon -- vendor/bensampo/laravel-enum/extension.neon +#- vendor/bensampo/laravel-enum/extension.neon parameters: level: 1 paths: @@ -9,12 +9,30 @@ parameters: - tests excludes_analyse: - %rootDir%/../../../tests/database/* + # Compatibility fix because Laravel moved the TestResponse class + - %rootDir%/../../../tests/TestCase.php + - %rootDir%/../../../tests/Laravel7ExceptionHandler.php + - %rootDir%/../../../tests/PreLaravel7ExceptionHandler.php + # Workaround until bensampo/laravel-enum support Laravel 7 + - %rootDir%/../../../src/Schema/Types/LaravelEnumType.php + - %rootDir%/../../../tests/Unit/Schema/Types/LaravelEnumTypeTest.php + - %rootDir%/../../../tests/Integration/Schema/Types/LaravelEnumTypeDBTest.php + - %rootDir%/../../../tests/Utils/Models/WithEnum.php + - %rootDir%/../../../tests/Utils/LaravelEnums/* ignoreErrors: - - '#Call to an undefined static method Illuminate\\Support\\Facades\\Event::assertDispatched().#' + # Some parts of Laravel are just really magical + - '#Call to an undefined static method Illuminate\\Support\\Facades\\Event::assertDispatched\(\)\.#' + - '#Function factory invoked with 2 parameters, 0 required\.#' + - '#Function factory invoked with 1 parameter, 0 required\.#' - # Defined mixins - - '#Call to an undefined method Illuminate\\Foundation\\Testing\\TestResponse::assertErrorCategory().#' - - '#Call to an undefined method Illuminate\\Foundation\\Testing\\TestResponse::jsonGet().#' + # This is a library, so it should be extendable + - '#Unsafe usage of new static.*#' + + # Compatibility fix because Laravel moved the TestResponse class + - '#.*TestResponse.*#' # We are monkeypatching that to pass around the query builder - - '#Access to an undefined property GraphQL\\Type\\Definition\\ResolveInfo::\$builder.#' + - '#Access to an undefined property GraphQL\\Type\\Definition\\ResolveInfo::\$builder\.#' + + # TODO remove once we no longer support PHPUnit 7 + - '#Parameter \$invocationOrder of method Tests\\TestCase::mockResolverExpects\(\) has invalid typehint type PHPUnit\\Framework\\MockObject\\Rule\\InvocationOrder\.#' diff --git a/src/LighthouseServiceProvider.php b/src/LighthouseServiceProvider.php index 69e758df54..986dc334c5 100644 --- a/src/LighthouseServiceProvider.php +++ b/src/LighthouseServiceProvider.php @@ -39,6 +39,7 @@ use Nuwave\Lighthouse\Schema\Source\SchemaStitcher; use Nuwave\Lighthouse\Schema\TypeRegistry; use Nuwave\Lighthouse\Schema\Values\FieldValue; +use Nuwave\Lighthouse\Support\AppVersion; use Nuwave\Lighthouse\Support\Compatibility\LaravelMiddlewareAdapter; use Nuwave\Lighthouse\Support\Compatibility\LumenMiddlewareAdapter; use Nuwave\Lighthouse\Support\Compatibility\MiddlewareAdapter; @@ -89,7 +90,7 @@ function ($translator, array $data, array $rules, array $messages, array $custom */ protected function loadRoutesFrom($path): void { - if (Str::contains($this->app->version(), 'Lumen')) { + if (AppVersion::isLumen()) { require realpath($path); return; diff --git a/src/Support/AppVersion.php b/src/Support/AppVersion.php new file mode 100644 index 0000000000..71cb17f6bd --- /dev/null +++ b/src/Support/AppVersion.php @@ -0,0 +1,39 @@ += $version; + } + + public static function below(float $version): bool + { + return self::versionNumber() < $version; + } + + protected static function version(): string + { + return app()->version(); + } + + protected static function versionNumber(): float + { + if (self::isLumen()) { + // Lumen version strings look like: "Lumen (2.3.4)..." + return (float) Str::after('(', self::version()); + } + + // Regular Laravel versions look like: "2.3.4" + return (float) self::version(); + } +} diff --git a/src/Support/Http/routes.php b/src/Support/Http/routes.php index 384e68b4f7..e5a413e897 100644 --- a/src/Support/Http/routes.php +++ b/src/Support/Http/routes.php @@ -1,13 +1,13 @@ version(), '5.5.')) { + if (AppVersion::below(5.6)) { $method = 'match'; } diff --git a/src/Testing/MakesGraphQLRequests.php b/src/Testing/MakesGraphQLRequests.php index dc46cdb2ef..10bb277dbf 100644 --- a/src/Testing/MakesGraphQLRequests.php +++ b/src/Testing/MakesGraphQLRequests.php @@ -3,7 +3,6 @@ namespace Nuwave\Lighthouse\Testing; use GraphQL\Type\Introspection; -use Illuminate\Foundation\Testing\TestResponse; use Illuminate\Support\Arr; use Nuwave\Lighthouse\Support\Contracts\CanStreamResponse; use Nuwave\Lighthouse\Support\Http\Responses\MemoryStream; @@ -23,7 +22,7 @@ trait MakesGraphQLRequests * On the first call to introspect() this property is set to * cache the result, as introspection is quite expensive. * - * @var \Illuminate\Foundation\Testing\TestResponse|null + * @var \Illuminate\Foundation\Testing\TestResponse|\Illuminate\Testing\TestResponse|null */ protected $introspectionResult; @@ -40,9 +39,9 @@ trait MakesGraphQLRequests * @param string $query * @param array|null $variables * @param array $extraParams - * @return \Illuminate\Foundation\Testing\TestResponse + * @return \Illuminate\Foundation\Testing\TestResponse|\Illuminate\Testing\TestResponse */ - protected function graphQL(string $query, array $variables = null, array $extraParams = []): TestResponse + protected function graphQL(string $query, array $variables = null, array $extraParams = []) { $params = ['query' => $query]; @@ -60,9 +59,9 @@ protected function graphQL(string $query, array $variables = null, array $extraP * * @param mixed[] $data * @param mixed[] $headers - * @return \Illuminate\Foundation\Testing\TestResponse + * @return \Illuminate\Foundation\Testing\TestResponse|\Illuminate\Testing\TestResponse */ - protected function postGraphQL(array $data, array $headers = []): TestResponse + protected function postGraphQL(array $data, array $headers = []) { return $this->postJson( $this->graphQLEndpointUrl(), @@ -79,9 +78,9 @@ protected function postGraphQL(array $data, array $headers = []): TestResponse * * @param mixed[] $parameters * @param mixed[] $files - * @return \Illuminate\Foundation\Testing\TestResponse + * @return \Illuminate\Foundation\Testing\TestResponse|\Illuminate\Testing\TestResponse */ - protected function multipartGraphQL(array $parameters, array $files): TestResponse + protected function multipartGraphQL(array $parameters, array $files) { return $this->call( 'POST', @@ -98,9 +97,9 @@ protected function multipartGraphQL(array $parameters, array $files): TestRespon /** * Execute the introspection query on the GraphQL server. * - * @return \Illuminate\Foundation\Testing\TestResponse + * @return \Illuminate\Foundation\Testing\TestResponse|\Illuminate\Testing\TestResponse */ - protected function introspect(): TestResponse + protected function introspect() { if ($this->introspectionResult) { return $this->introspectionResult; diff --git a/tests/Integration/IntrospectionTest.php b/tests/Integration/IntrospectionTest.php index 36ee013b4c..1d042ad445 100644 --- a/tests/Integration/IntrospectionTest.php +++ b/tests/Integration/IntrospectionTest.php @@ -2,7 +2,6 @@ namespace Tests\Integration; -use Illuminate\Foundation\Testing\TestResponse; use Nuwave\Lighthouse\Schema\TypeRegistry; use Tests\TestCase; use Tests\Utils\Scalars\Email; @@ -15,7 +14,7 @@ class IntrospectionTest extends TestCase protected $typeRegistry; /** - * @var TestResponse|null + * @var \Illuminate\Foundation\Testing\TestResponse|\Illuminate\Testing\TestResponse|null */ protected $introspectionResult; diff --git a/tests/Integration/Schema/Directives/WhereJsonContainsDirectiveDBTest.php b/tests/Integration/Schema/Directives/WhereJsonContainsDirectiveDBTest.php index fc1f60b3b8..941e93404c 100644 --- a/tests/Integration/Schema/Directives/WhereJsonContainsDirectiveDBTest.php +++ b/tests/Integration/Schema/Directives/WhereJsonContainsDirectiveDBTest.php @@ -2,6 +2,7 @@ namespace Tests\Integration\Schema\Directives; +use Nuwave\Lighthouse\Support\AppVersion; use Tests\DBTestCase; use Tests\Utils\Models\User; @@ -14,14 +15,14 @@ class WhereJsonContainsDirectiveDBTest extends DBTestCase type User { name: String - } + } '; protected function setUp(): void { parent::setUp(); - if ((float) $this->app->version() < 5.6) { + if (AppVersion::below(5.6)) { $this->markTestSkipped('Laravel supports whereJsonContains from version 5.6.'); } } diff --git a/tests/Integration/Schema/Types/LaravelEnumTypeDBTest.php b/tests/Integration/Schema/Types/LaravelEnumTypeDBTest.php index be432a5704..23dad081af 100644 --- a/tests/Integration/Schema/Types/LaravelEnumTypeDBTest.php +++ b/tests/Integration/Schema/Types/LaravelEnumTypeDBTest.php @@ -4,9 +4,10 @@ use Nuwave\Lighthouse\Schema\TypeRegistry; use Nuwave\Lighthouse\Schema\Types\LaravelEnumType; +use Nuwave\Lighthouse\Support\AppVersion; use Tests\DBTestCase; -use Tests\Utils\LaravelEnums\UserType; -use Tests\Utils\Models\User; +use Tests\Utils\LaravelEnums\AOrB; +use Tests\Utils\Models\WithEnum; class LaravelEnumTypeDBTest extends DBTestCase { @@ -19,88 +20,92 @@ protected function setUp(): void { parent::setUp(); + if (AppVersion::atLeast(7.0)) { + $this->markTestSkipped('TODO remove this once bensampo/laravel-enum supports Laravel 7'); + } + $this->typeRegistry = $this->app->make(TypeRegistry::class); } public function testUseLaravelEnumType(): void { - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type Query { - user(type: UserType @eq): User @find + withEnum(type: AOrB @eq): WithEnum @find } type Mutation { - createUser(type: UserType): User @create + createWithEnum(type: AOrB): WithEnum @create } - type User { - type: UserType + type WithEnum { + type: AOrB } '; $this->typeRegistry->register( - new LaravelEnumType(UserType::class) + new LaravelEnumType(AOrB::class) ); - $typeAdmistrator = [ - 'type' => 'Administrator', + $typeA = [ + 'type' => 'A', ]; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' mutation { - createUser(type: Administrator) { + createWithEnum(type: A) { type } } - ')->assertJsonFragment($typeAdmistrator); + ')->assertJsonFragment($typeA); - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { - user(type: Administrator) { + withEnum(type: A) { type } } - ')->assertJsonFragment($typeAdmistrator); + ')->assertJsonFragment($typeA); } public function testWhereJsonContainsUsingEnumType(): void { - if ((float) $this->app->version() < 5.6) { + if (AppVersion::below(5.6)) { $this->markTestSkipped('Laravel supports whereJsonContains from version 5.6.'); } // We use the "name" field to store the "type" JSON - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type Query { - user( - type: UserType @whereJsonContains(key: "name") - ): User @find + withEnum( + type: AOrB @whereJsonContains(key: "name") + ): WithEnum @find } - type User { + type WithEnum { name: String } '; $this->typeRegistry->register( - new LaravelEnumType(UserType::class) + new LaravelEnumType(AOrB::class) ); - $encodedType = json_encode([UserType::Administrator]); + $encodedType = json_encode([AOrB::A]); - $user = new User(); - $user->name = $encodedType; - $user->save(); + $withEnum = new WithEnum(); + $withEnum->name = $encodedType; + $withEnum->save(); - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { - user(type: Administrator) { + withEnum(type: A) { name } } ')->assertJson([ 'data' => [ - 'user' => [ + 'withEnum' => [ 'name' => $encodedType, ], ], diff --git a/tests/Integration/Subscriptions/SubscriptionTest.php b/tests/Integration/Subscriptions/SubscriptionTest.php index e883348535..bd46e9fd33 100644 --- a/tests/Integration/Subscriptions/SubscriptionTest.php +++ b/tests/Integration/Subscriptions/SubscriptionTest.php @@ -2,7 +2,6 @@ namespace Tests\Integration\Subscriptions; -use Illuminate\Foundation\Testing\TestResponse; use Illuminate\Support\Arr; use Nuwave\Lighthouse\Subscriptions\BroadcastManager; use Nuwave\Lighthouse\Subscriptions\StorageManager; @@ -28,17 +27,17 @@ protected function setUp(): void type Post { body: String } - + type Subscription { onPostCreated: Post } - + type Mutation { createPost(post: String!): Post @field(resolver: \"{$this->qualifyTestResolver()}\") @broadcast(subscription: \"onPostCreated\") } - + type Query { foo: String } @@ -140,7 +139,10 @@ public function resolve($root, array $args): array return ['body' => $args['post']]; } - protected function subscribe(): TestResponse + /** + * @return \Illuminate\Foundation\Testing\TestResponse|\Illuminate\Testing\TestResponse + */ + protected function subscribe() { return $this->postGraphQL([ 'query' => ' diff --git a/tests/Integration/ValidationTest.php b/tests/Integration/ValidationTest.php index 8c2a5546cc..f167fdb7c8 100644 --- a/tests/Integration/ValidationTest.php +++ b/tests/Integration/ValidationTest.php @@ -2,7 +2,6 @@ namespace Tests\Integration; -use Illuminate\Foundation\Testing\TestResponse; use Illuminate\Support\Arr; use Illuminate\Support\Str; use Tests\DBTestCase; @@ -530,10 +529,10 @@ public function testCombinesArgumentValidationWhenGrouped(): void * Assert that the returned result contains an exactly defined array of validation keys. * * @param array $keys - * @param \Illuminate\Foundation\Testing\TestResponse $result + * @param \Illuminate\Foundation\Testing\TestResponse|\Illuminate\Testing\TestResponse $result * @return void */ - protected function assertValidationKeysSame(array $keys, TestResponse $result): void + protected function assertValidationKeysSame(array $keys, $result): void { $validation = $result->jsonGet('errors.0.extensions.validation'); diff --git a/tests/Laravel7ExceptionHandler.php b/tests/Laravel7ExceptionHandler.php new file mode 100644 index 0000000000..58d9e97044 --- /dev/null +++ b/tests/Laravel7ExceptionHandler.php @@ -0,0 +1,29 @@ +toTimestamp($seconds); + } + + /** + * Get the UNIX timestamp for the given number of seconds. + * + * @param int $seconds + * @return int + */ + protected function toTimestamp($seconds) + { + return $seconds > 0 ? $this->availableAt($seconds) : 0; + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 5c92c0c2b8..6942e28c7b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,16 +2,15 @@ namespace Tests; -use Exception; use GraphQL\Error\Debug; use GraphQL\Type\Schema; use Illuminate\Contracts\Debug\ExceptionHandler; -use Illuminate\Foundation\Testing\TestResponse; use Laravel\Scout\ScoutServiceProvider; use Nuwave\Lighthouse\GraphQL; use Nuwave\Lighthouse\LighthouseServiceProvider; use Nuwave\Lighthouse\OrderBy\OrderByServiceProvider; use Nuwave\Lighthouse\SoftDeletes\SoftDeletesServiceProvider; +use Nuwave\Lighthouse\Support\AppVersion; use Nuwave\Lighthouse\Testing\MakesGraphQLRequests; use Nuwave\Lighthouse\Testing\MocksResolvers; use Nuwave\Lighthouse\Testing\TestingServiceProvider; @@ -126,7 +125,11 @@ protected function getEnvironmentSetUp($app) $config->set('app.debug', true); - TestResponse::mixin(new TestResponseMixin()); + if (class_exists('Illuminate\Testing\TestResponse')) { + \Illuminate\Testing\TestResponse::mixin(new TestResponseMixin()); + } elseif (class_exists('Illuminate\Foundation\Testing\TestResponse')) { + \Illuminate\Foundation\Testing\TestResponse::mixin(new TestResponseMixin()); + } } /** @@ -141,27 +144,11 @@ protected function getEnvironmentSetUp($app) protected function resolveApplicationExceptionHandler($app) { $app->singleton(ExceptionHandler::class, function () { - return new class implements ExceptionHandler { - public function report(Exception $e) - { - // - } - - public function render($request, Exception $e) - { - throw $e; - } - - public function renderForConsole($output, Exception $e) - { - // - } - - public function shouldReport(Exception $e) - { - return false; - } - }; + if (AppVersion::atLeast(7.0)) { + return new Laravel7ExceptionHandler(); + } else { + return new PreLaravel7ExceptionHandler(); + } }); } diff --git a/tests/TestResponseMixin.php b/tests/TestResponseMixin.php index ac5663b294..b350bbe72a 100644 --- a/tests/TestResponseMixin.php +++ b/tests/TestResponseMixin.php @@ -3,10 +3,9 @@ namespace Tests; use Closure; -use Illuminate\Foundation\Testing\TestResponse; /** - * @mixin \Illuminate\Foundation\Testing\TestResponse + * @mixin \Illuminate\Foundation\Testing\TestResponse|\Illuminate\Testing\TestResponse */ class TestResponseMixin { @@ -19,7 +18,7 @@ public function jsonGet(): Closure public function assertErrorCategory(): Closure { - return function (string $category): TestResponse { + return function (string $category) { $this->assertJson([ 'errors' => [ [ diff --git a/tests/Unit/Schema/Directives/CanDirectiveTest.php b/tests/Unit/Schema/Directives/CanDirectiveTest.php index 6c0997981d..c88733e5f4 100644 --- a/tests/Unit/Schema/Directives/CanDirectiveTest.php +++ b/tests/Unit/Schema/Directives/CanDirectiveTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit\Schema\Directives; use Nuwave\Lighthouse\Exceptions\AuthorizationException; +use Nuwave\Lighthouse\Support\AppVersion; use Tests\TestCase; use Tests\Utils\Models\User; use Tests\Utils\Policies\UserPolicy; @@ -70,7 +71,7 @@ public function testPassesAuthIfAuthorized(): void public function testAcceptsGuestUser(): void { - if ((float) $this->app->version() < 5.7) { + if (AppVersion::below(5.7)) { $this->markTestSkipped('Version less than 5.7 do not support guest user.'); } diff --git a/tests/Unit/Schema/Types/LaravelEnumTypeTest.php b/tests/Unit/Schema/Types/LaravelEnumTypeTest.php index 3d37442a0e..795cf19656 100644 --- a/tests/Unit/Schema/Types/LaravelEnumTypeTest.php +++ b/tests/Unit/Schema/Types/LaravelEnumTypeTest.php @@ -4,10 +4,11 @@ use Nuwave\Lighthouse\Schema\TypeRegistry; use Nuwave\Lighthouse\Schema\Types\LaravelEnumType; +use Nuwave\Lighthouse\Support\AppVersion; use PHPUnit\Framework\Constraint\Callback; use Tests\TestCase; +use Tests\Utils\LaravelEnums\AOrB; use Tests\Utils\LaravelEnums\LocalizedUserType; -use Tests\Utils\LaravelEnums\UserType; class LaravelEnumTypeTest extends TestCase { @@ -20,13 +21,17 @@ protected function setUp(): void { parent::setUp(); + if (AppVersion::atLeast(7.0)) { + $this->markTestSkipped('TODO remove this once bensampo/laravel-enum supports Laravel 7'); + } + $this->typeRegistry = $this->app->make(TypeRegistry::class); } public function testMakeEnumWithCustomName(): void { $customName = 'CustomName'; - $enumType = new LaravelEnumType(UserType::class, $customName); + $enumType = new LaravelEnumType(AOrB::class, $customName); $this->assertSame($customName, $enumType->name); } @@ -42,22 +47,22 @@ public function testReceivesEnumInstanceInternally(): void { $this->schema = /** @lang GraphQL */ ' type Query { - foo(bar: UserType): Boolean @mock + foo(bar: AOrB): Boolean @mock } '; $this->typeRegistry->register( - new LaravelEnumType(UserType::class) + new LaravelEnumType(AOrB::class) ); $this->mockResolver() ->with(null, new Callback(function (array $args): bool { - return $args['bar'] instanceof UserType; + return $args['bar'] instanceof AOrB; })); - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { - foo(bar: Administrator) + foo(bar: A) } '); } diff --git a/tests/Utils/LaravelEnums/AOrB.php b/tests/Utils/LaravelEnums/AOrB.php new file mode 100644 index 0000000000..e92ecf67d8 --- /dev/null +++ b/tests/Utils/LaravelEnums/AOrB.php @@ -0,0 +1,11 @@ + UserType::class, - ]; - public function getTaskCountAsString(): string { if (! $this->relationLoaded('tasks')) { diff --git a/tests/Utils/Models/WithEnum.php b/tests/Utils/Models/WithEnum.php new file mode 100644 index 0000000000..325ac2d2cf --- /dev/null +++ b/tests/Utils/Models/WithEnum.php @@ -0,0 +1,19 @@ + AOrB::class, + ]; +} diff --git a/tests/Utils/Queries/FooInvoke.php b/tests/Utils/Queries/FooInvoke.php index 64daad9715..84b4628554 100644 --- a/tests/Utils/Queries/FooInvoke.php +++ b/tests/Utils/Queries/FooInvoke.php @@ -6,5 +6,6 @@ class FooInvoke { public function __invoke(): int { + return 42; } } diff --git a/tests/database/migrations/2018_02_28_000002_create_testbench_users_table.php b/tests/database/migrations/2018_02_28_000002_create_testbench_users_table.php index fb574d02c4..f5ae1cbc71 100644 --- a/tests/database/migrations/2018_02_28_000002_create_testbench_users_table.php +++ b/tests/database/migrations/2018_02_28_000002_create_testbench_users_table.php @@ -3,7 +3,6 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -use Tests\Utils\LaravelEnums\UserType; class CreateTestbenchUsersTable extends Migration { @@ -21,7 +20,6 @@ public function up(): void $table->string('name')->nullable(); $table->string('email')->nullable(); $table->string('password')->nullable(); - $table->enum('type', UserType::getValues())->nullable(); $table->timestamps(); }); } diff --git a/tests/database/migrations/2018_02_28_000002_create_testbench_with_enums_table.php b/tests/database/migrations/2018_02_28_000002_create_testbench_with_enums_table.php new file mode 100644 index 0000000000..010c354f46 --- /dev/null +++ b/tests/database/migrations/2018_02_28_000002_create_testbench_with_enums_table.php @@ -0,0 +1,28 @@ +increments('id'); + $table->string('name')->nullable(); + $table->enum('type', ['A', 'B'])->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::drop('users'); + } +} From a827d3e1acc234ebd94e8df307818314454eaf81 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Thu, 5 Mar 2020 18:54:20 +0000 Subject: [PATCH 20/31] Apply fixes from StyleCI --- src/Testing/MocksResolvers.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Testing/MocksResolvers.php b/src/Testing/MocksResolvers.php index 2256e5acdc..0b952883f4 100644 --- a/src/Testing/MocksResolvers.php +++ b/src/Testing/MocksResolvers.php @@ -3,7 +3,6 @@ namespace Nuwave\Lighthouse\Testing; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; -use PHPUnit\Framework\MockObject\Rule\InvocationOrder; /** * @mixin \PHPUnit\Framework\TestCase From 9cad21340bb7b93bafd1748974b34e44265dfb90 Mon Sep 17 00:00:00 2001 From: spawnia Date: Thu, 5 Mar 2020 19:57:30 +0100 Subject: [PATCH 21/31] Prepare 4.10 release --- CHANGELOG.md | 2 + docker-compose.yml | 2 +- docs/.vuepress/versions.json | 1 + docs/4.10/api-reference/commands.md | 72 + docs/4.10/api-reference/directives.md | 3050 +++++++++++++++++ docs/4.10/api-reference/events.md | 304 ++ docs/4.10/api-reference/resolvers.md | 41 + docs/4.10/api-reference/scalars.md | 46 + docs/4.10/concepts/arg-resolvers.md | 221 ++ .../custom-directives/argument-directives.md | 277 ++ .../custom-directives/field-directives.md | 30 + .../4.10/custom-directives/getting-started.md | 97 + .../4.10/custom-directives/type-directives.md | 29 + .../adding-types-programmatically.md | 93 + docs/4.10/digging-deeper/client-directives.md | 125 + docs/4.10/digging-deeper/error-handling.md | 150 + .../digging-deeper/extending-lighthouse.md | 138 + docs/4.10/digging-deeper/file-uploads.md | 82 + docs/4.10/digging-deeper/relay.md | 51 + .../digging-deeper/schema-organisation.md | 127 + .../4.10/eloquent/complex-where-conditions.md | 373 ++ docs/4.10/eloquent/getting-started.md | 287 ++ docs/4.10/eloquent/nested-mutations.md | 848 +++++ .../eloquent/polymorphic-relationships.md | 94 + docs/4.10/eloquent/relationships.md | 143 + docs/4.10/eloquent/soft-deleting.md | 101 + docs/4.10/getting-started/configuration.md | 10 + docs/4.10/getting-started/installation.md | 46 + .../migrating-to-lighthouse.md | 38 + docs/4.10/performance/deferred.md | 27 + docs/4.10/performance/n-plus-one.md | 55 + docs/4.10/performance/schema-caching.md | 28 + docs/4.10/performance/server-configuration.md | 13 + docs/4.10/performance/tracing.md | 14 + docs/4.10/security/authentication.md | 55 + docs/4.10/security/authorization.md | 162 + docs/4.10/security/resource-exhaustion.md | 15 + docs/4.10/security/sanitization.md | 25 + docs/4.10/security/validation.md | 169 + docs/4.10/sidebar.js | 103 + .../subscriptions/client-implementations.md | 198 ++ docs/4.10/subscriptions/defining-fields.md | 124 + .../subscriptions/filtering-subscriptions.md | 42 + docs/4.10/subscriptions/getting-started.md | 39 + .../subscriptions/trigger-subscriptions.md | 40 + docs/4.10/testing/extensions.md | 199 ++ docs/4.10/testing/phpunit.md | 261 ++ docs/4.10/the-basics/directives.md | 67 + docs/4.10/the-basics/fields.md | 225 ++ docs/4.10/the-basics/schema.md | 62 + docs/4.10/the-basics/types.md | 324 ++ 51 files changed, 9124 insertions(+), 1 deletion(-) create mode 100644 docs/4.10/api-reference/commands.md create mode 100644 docs/4.10/api-reference/directives.md create mode 100644 docs/4.10/api-reference/events.md create mode 100644 docs/4.10/api-reference/resolvers.md create mode 100644 docs/4.10/api-reference/scalars.md create mode 100644 docs/4.10/concepts/arg-resolvers.md create mode 100644 docs/4.10/custom-directives/argument-directives.md create mode 100644 docs/4.10/custom-directives/field-directives.md create mode 100644 docs/4.10/custom-directives/getting-started.md create mode 100644 docs/4.10/custom-directives/type-directives.md create mode 100644 docs/4.10/digging-deeper/adding-types-programmatically.md create mode 100644 docs/4.10/digging-deeper/client-directives.md create mode 100644 docs/4.10/digging-deeper/error-handling.md create mode 100644 docs/4.10/digging-deeper/extending-lighthouse.md create mode 100644 docs/4.10/digging-deeper/file-uploads.md create mode 100644 docs/4.10/digging-deeper/relay.md create mode 100644 docs/4.10/digging-deeper/schema-organisation.md create mode 100644 docs/4.10/eloquent/complex-where-conditions.md create mode 100644 docs/4.10/eloquent/getting-started.md create mode 100644 docs/4.10/eloquent/nested-mutations.md create mode 100644 docs/4.10/eloquent/polymorphic-relationships.md create mode 100644 docs/4.10/eloquent/relationships.md create mode 100644 docs/4.10/eloquent/soft-deleting.md create mode 100644 docs/4.10/getting-started/configuration.md create mode 100644 docs/4.10/getting-started/installation.md create mode 100644 docs/4.10/getting-started/migrating-to-lighthouse.md create mode 100644 docs/4.10/performance/deferred.md create mode 100644 docs/4.10/performance/n-plus-one.md create mode 100644 docs/4.10/performance/schema-caching.md create mode 100644 docs/4.10/performance/server-configuration.md create mode 100644 docs/4.10/performance/tracing.md create mode 100644 docs/4.10/security/authentication.md create mode 100644 docs/4.10/security/authorization.md create mode 100644 docs/4.10/security/resource-exhaustion.md create mode 100644 docs/4.10/security/sanitization.md create mode 100644 docs/4.10/security/validation.md create mode 100644 docs/4.10/sidebar.js create mode 100644 docs/4.10/subscriptions/client-implementations.md create mode 100644 docs/4.10/subscriptions/defining-fields.md create mode 100644 docs/4.10/subscriptions/filtering-subscriptions.md create mode 100644 docs/4.10/subscriptions/getting-started.md create mode 100644 docs/4.10/subscriptions/trigger-subscriptions.md create mode 100644 docs/4.10/testing/extensions.md create mode 100644 docs/4.10/testing/phpunit.md create mode 100644 docs/4.10/the-basics/directives.md create mode 100644 docs/4.10/the-basics/fields.md create mode 100644 docs/4.10/the-basics/schema.md create mode 100644 docs/4.10/the-basics/types.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aced3a49a..ceb61f7e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +## 4.10.0 + ### Added - Access nested inputs with dot notation using the `find` option of `@can` https://github.com/nuwave/lighthouse/pull/1216 diff --git a/docker-compose.yml b/docker-compose.yml index 318ed89135..fd3685b0f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,4 +25,4 @@ services: working_dir: /app ports: - 8080:8080 - command: /bin/bash -c "yarn && yarn start" + tty: true diff --git a/docs/.vuepress/versions.json b/docs/.vuepress/versions.json index 85143ccdc6..9ef2390682 100644 --- a/docs/.vuepress/versions.json +++ b/docs/.vuepress/versions.json @@ -1,5 +1,6 @@ [ "master", + "4.10", "4.9", "4.8", "4.7", diff --git a/docs/4.10/api-reference/commands.md b/docs/4.10/api-reference/commands.md new file mode 100644 index 0000000000..2b4cd084d1 --- /dev/null +++ b/docs/4.10/api-reference/commands.md @@ -0,0 +1,72 @@ +# Artisan Commands + +Lighthouse provides some convenient artisan commands. All of them +are namespaced under `lighthouse`. + +## clear-cache + +Clear the cache for the GraphQL AST. + + php artisan lighthouse:clear-cache + +## ide-helper + +Create a schema containing all server-side directives. +This will allow some IDEs to do code completion in your GraphQL-schema. + + php artisan lighthouse:ide-helper + +## interface + +Create a class for a GraphQL interface type. + + php artisan lighthouse:interface + +## mutation + +Create a class for a single field on the root Mutation type. + + php artisan lighthouse:mutation + +## print-schema + +Compile the final GraphQL schema and print the result. + + php artisan lighthouse:print-schema + +This can be quite useful, as the root `.graphql` files do not necessarily +contains the whole schema. Schema imports, native PHP types and schema manipulation +may influence the final schema. + +Use the `-W` / `--write` option to output the schema to the default file storage +(usually `storage/app`) as `lighthouse-schema.graphql`. + +## query + +Create a class for a single field on the root Query type. + + php artisan lighthouse:query + +## scalar + +Create a class for a GraphQL scalar type. + + php artisan lighthouse:scalar + +## subscription + +Create a class for a single field on the root Subscription type. + + php artisan lighthouse:subscription + +## union + +Create a class for a GraphQL union type. + + php artisan lighthouse:union + +## validate-schema + +Validate the GraphQL schema definition. + + php artisan lighthouse:validate-schema diff --git a/docs/4.10/api-reference/directives.md b/docs/4.10/api-reference/directives.md new file mode 100644 index 0000000000..e11a67993f --- /dev/null +++ b/docs/4.10/api-reference/directives.md @@ -0,0 +1,3050 @@ +# Directives + +## @all + +Fetch all Eloquent models and return the collection as the result for a field. + +```graphql +type Query { + users: [User!]! @all +} +``` + +This assumes your model has the same name as the type you are returning and is defined +in the default model namespace `App`. [You can change this configuration](../getting-started/configuration.md). + +### Definition + +```graphql +directive @all( + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] +) on FIELD_DEFINITION +``` + +### Examples + +If you need to use a different model for a single field, you can pass a class name as the `model` argument. + +```graphql +type Query { + posts: [Post!]! @all(model: "App\\Blog\\BlogEntry") +} +``` + +## @auth + +Return the currently authenticated user as the result of a query. + +```graphql +type Query { + me: User @auth +} +``` + +### Definition + +```graphql +""" +Return the currently authenticated user as the result of a query. +""" +directive @auth( + """ + Use a particular guard to retreive the user. + """ + guard: String +) on FIELD_DEFINITION +``` + +### Examples + +If you need to use a guard besides the default to resolve the authenticated user, +you can pass the guard name as the `guard` argument + +```graphql +type Query { + me: User @auth(guard: "api") +} +``` + +## @belongsTo + +Resolves a field through the Eloquent `BelongsTo` relationship. + +```graphql +type Post { + author: User @belongsTo +} +``` + +It assumes both the field and the relationship method to have the same name. + +```php +belongsTo(User::class); + } +} +``` + +### Definition + +```graphql +""" +Resolves a field through the Eloquent `BelongsTo` relationship. +""" +directive @belongsTo( + """ + Specify the relationship method name in the model class, + if it is named different from the field in the schema. + """ + relation: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] +) on FIELD_DEFINITION +``` + +### Examples + +The directive accepts an optional `relation` argument if your relationship method +has a different name than the field. + +```graphql +type Post { + user: User @belongsTo(relation: "author") +} +``` + +## @belongsToMany + +Resolves a field through the Eloquent `BelongsToMany` relationship. + +```graphql +type User { + roles: [Role!]! @belongsToMany +} +``` + +It assumes both the field and the relationship method to have the same name. + +```php +belongsToMany(Role::class); + } +} +``` + +### Definition + +```graphql +""" +Resolves a field through the Eloquent `BelongsToMany` relationship. +""" +directive @belongsToMany( + """ + Specify the relationship method name in the model class, + if it is named different from the field in the schema. + """ + relation: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] + + """ + ALlows to resolve the relation as a paginated list. + Allowed values: `paginator`, `connection`. + """ + type: String + + """ + Specify the default quantity of elements to be returned. + Only applies when using pagination. + """ + defaultCount: Int + + """ + Specify the maximum quantity of elements to be returned. + Only applies when using pagination. + """ + maxCount: Int + + """ + Specify a custom type that implements the Edge interface + to extend edge object. + Only applies when using Relay style "connection" pagination. + """ + edgeType: String +) on FIELD_DEFINITION +``` + +### Examples + +The directive accepts an optional `relation` argument if your relationship method +has a different name than the field. + +```graphql +type User { + jobs: [Role!]! @belongsToMany(relation: "roles") +} +``` + +When using the connection `type` argument, you may create your own +[Edge type](https://facebook.github.io/relay/graphql/connections.htm#sec-Edge-Types) which +may have fields that resolve from the model [pivot](https://laravel.com/docs/5.8/eloquent-relationships#many-to-many) +data. You may also add a custom field resolver for fields you want to resolve yourself. + +You may either specify the edge using the `edgetype` argument, or it will automatically +look for a {type}Edge type to be defined. In this case it would be `RoleEdge`. + +```graphql +type User { + roles: [Role!]! @belongsToMany(type: "connection", edgeType: "CustomRoleEdge") +} + +type CustomRoleEdge implements Edge { + cursor: String! + node: Node + meta: String +} +``` + +## @bcrypt + +```graphql +""" +Run the `bcrypt` function on the argument it is defined on. + +@deprecated(reason: "Use @hash instead. This directive will be removed in v5.") +""" +directive @bcrypt on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +Deprecated in favour of [`@hash`](#hash). + +## @broadcast + +Broadcast the results of a mutation to subscribed clients. +[Read more about subscriptions](../subscriptions/getting-started.md) + +```graphql +type Mutation { + createPost(input: CreatePostInput!): Post + @broadcast(subscription: "postCreated") +} +``` + +The `subscription` argument must reference the name of a subscription field. + +### Definition + +```graphql +""" +Broadcast the results of a mutation to subscribed clients. +""" +directive @broadcast( + """ + Name of the subscription that should be retriggered as a result of this operation.. + """ + subscription: String! + + """ + Specify whether or not the job should be queued. + This defaults to the global config option `lighthouse.subscriptions.queue_broadcasts`. + """ + shouldQueue: Boolean +) on FIELD_DEFINITION +``` + +### Examples + +You may override the default queueing behaviour from the configuration by +passing the `shouldQueue` argument. + +```graphql +type Mutation { + updatePost(input: UpdatePostInput!): Post + @broadcast(subscription: "postUpdated", shouldQueue: false) +} +``` + +## @builder + +Use an argument to modify the query builder for a field. + +```graphql +type Query { + users( + limit: Int @builder(method: "App\MyClass@limit") + ): [User!]! @all +} +``` + +You must point to a `method` which will receive the builder instance +and the argument value and can apply additional constraints to the query. + +```php +namespace App; + +class MyClass +{ + + * Add a limit constrained upon the query. + * + * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder + * @param mixed $value + * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder + */ + public function limit($builder, int $value) + { + return $builder->limit($value); + } +} +``` + +### Definition + +```graphql +""" +Use an argument to modify the query builder for a field. +""" +directive @builder( + """ + Reference a method that is passed the query builder. + Consists of two parts: a class name and a method name, separated by an `@` symbol. + If you pass only a class name, the method name defaults to `__invoke`. + """ + method: String! +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +## @cache + +Cache the result of a resolver. + +The cache is created on the first request and is cached forever by default. +Use this for values that change seldom and take long to fetch/compute. + +```graphql +type Query { + highestKnownPrimeNumber: Int! @cache +} +``` + +### Definition + +```graphql +""" +Cache the result of a resolver. +""" +directive @cache( + """ + Set the duration it takes for the cache to expire in seconds. + If not given, the result will be stored forever. + """ + maxAge: Int + + """ + Limit access to cached data to the currently authenticated user. + When the field is accessible by guest users, this will not have + any effect, they will access a shared cache. + """ + private: Boolean = false +) on FIELD_DEFINITION +``` + +### Examples + +You can set an expiration time in seconds +if you want to invalidate the cache after a while. + +```graphql +type Query { + temperature: Int! @cache(maxAge: 300) +} +``` + +You can limit the cache to the logged in user making the request by marking it as private. +This makes sense for data that is specific to a certain user. + +```graphql +type Query { + todos: [ToDo!]! @cache(private: true) +} +``` + +## @cacheKey + +Specify the field to use as a key when creating a cache. + +```graphql +type GithubProfile { + username: String @cacheKey + repos: [Repository] @cache +} +``` + +When generating a cached result for a resolver, Lighthouse produces a unique key for each type. +By default, Lighthouse will look for a field with the `ID` type to generate the key. +If you'd like to use a different field (i.e., an external API id) you can mark the field with the `@cacheKey` directive. + +### Definition + +```graphql +""" +Specify the field to use as a key when creating a cache. +""" +directive @cacheKey on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +## @can + +```graphql +""" +Check a Laravel Policy to ensure the current user is authorized to access a field. + +When `injectArgs` and `args` are used together, the client given +arguments will be passed before the static args. +""" +directive @can( + """ + The ability to check permissions for. + """ + ability: String! + + """ + The name of the argument that is used to find a specific model + instance against which the permissions should be checked. + """ + find: String + + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String + + """ + Pass along the client given input data as arguments to `Gate::check`. + """ + injectArgs: Boolean = false + + """ + Statically defined arguments that are passed to `Gate::check`. + + You may pass pass arbitrary GraphQL literals, + e.g.: [1, 2, 3] or { foo: "bar" } + """ + args: Mixed +) on FIELD_DEFINITION +``` + +The name of the returned Type `Post` is used as the Model class, however you may overwrite this by +passing the `model` argument. + +```graphql +type Mutation { + createBlogPost(input: PostInput): BlogPost + @can(ability: "create", model: "App\\Post") +} +``` + +You can find usage examples of this directive in [the authorization docs](../security/authorization.md#restrict-fields-through-policies). + +## @complexity + +Perform calculation of a fields complexity score before execution. + +```graphql +type Query { + posts: [Post!]! @complexity +} +``` + +[Read More about query complexity analysis](http://webonyx.github.io/graphql-php/security/#query-complexity-analysis) + +### Definition + +```graphql +""" +Customize the calculation of a fields complexity score before execution. +""" +directive @complexity( + """ + Reference a function to customize the complexity score calculation. + Consists of two parts: a class name and a method name, seperated by an `@` symbol. + If you pass only a class name, the method name defaults to `__invoke`. + """ + resolver: String +) on FIELD_DEFINITION +``` + +### Examples + +You can provide your own function to calculate complexity. + +```graphql +type Query { + posts: [Post!]! + @complexity(resolver: "App\\Security\\ComplexityAnalyzer@userPosts") +} +``` + +A custom complexity function may look like the following, +refer to the [complexity function signature](resolvers.md#complexity-function-signature). + +```php +namespace App\Security; + +class ComplexityAnalyzer { + + public function userPosts(int $childrenComplexity, array $args): int + { + $postComplexity = $args['includeFullText']) + ? 3 + : 2; + + return $childrenComplexity * $postComplexity; + } +``` + +## @count + +Returns the count of a given relationship or model. + +```graphql +type User { + id: ID! + likes: Int! @count(relation: "likes") +} +``` + +```graphql +type Query { + categories: Int! @count(model: "Category") +} +``` + +### Definition + +```graphql +""" +Returns the count of a given relationship or model. +""" +directive @count( + """ + The relationship which you want to run the count on. + """ + relation: String + + """ + The model to run the count on. + """ + model: String +) on FIELD_DEFINITION +``` + +## @create + +```graphql +""" +Create a new Eloquent model with the given arguments. +""" +directive @create( + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String + + """ + Specify the name of the relation on the parent model. + This is only needed when using this directive as a nested arg + resolver and if the name of the relation is not the arg name. + """ + relation: String +) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +Use it on a root mutation field that returns an instance of the Model. + +```graphql +type Mutation { + createPost(title: String!): Post @create +} +``` + +If you are using a single input object as an argument, you must tell Lighthouse +to spread out the nested values before applying it to the resolver. + +```graphql +type Mutation { + createPost(input: CreatePostInput! @spread): Post @create +} + +input CreatePostInput { + title: String! +} +``` + +If the name of the Eloquent model does not match the return type of the field, +or is located in a non-default namespace, set it with the `model` argument. + +```graphql +type Mutation { + createPost(title: String!): Post @create(model: "Foo\\Bar\\MyPost") +} +``` + +This directive can also be used as a [nested arg resolver](../concepts/arg-resolvers.md). + +## @delete + +```graphql +""" +Delete one or more models by their ID. +The field must have a single non-null argument that may be a list. +""" +directive @delete( + """ + Set to `true` to use global ids for finding the model. + If set to `false`, regular non-global ids are used. + """ + globalId: Boolean = false + + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String + + """ + Specify the name of the relation on the parent model. + This is only needed when using this directive as a nested arg + resolver and if the name of the relation is not the arg name. + """ + relation: String +) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +Use it on a root mutation field that returns an instance of the Model. + +```graphql +type Mutation { + deletePost(id: ID!): Post @delete +} +``` + +If you use global ids, you can set the `globalId` argument to `true`. +Lighthouse will decode the id for you automatically. + +```graphql +type Mutation { + deletePost(id: ID!): Post @delete(globalId: true) +} +``` + +You can also delete multiple models at once. +Define a field that takes a list of IDs and returns a collection of the +deleted models. + +_In contrast to Laravel mass updates, this does trigger model events._ + +```graphql +type Mutation { + deletePosts(id: [ID!]!): [Post!]! @delete +} +``` + +If the name of the Eloquent model does not match the return type of the field, +or is located in a non-default namespace, set it with the `model` argument. + +```graphql +type Mutation { + deletePost(id: ID!): Post @delete(model: "Bar\\Baz\\MyPost") +} +``` + +This directive can also be used as a [nested arg resolver](../concepts/arg-resolvers.md). + +```graphql +type Mutation { + updateUser( + id: Int + deleteTasks: [Int!]! @delete(relation: "tasks") + ): User @update +} +``` + +If the model relates to a single other model through a `HasOne`, `MorphOne`, `BelongsTo` or +`MorphTo` relationship, you can just pass a Boolean instead of an ID, as there is only one +possible model that can be deleted. + +```graphql +type Mutation { + updateTask( + id: Int + deleteUser: Boolean @delete(relation: "user") + ): Task @update +} +``` + +## @deprecated + +You can mark fields as deprecated by adding the `@deprecated` directive and providing a +`reason`. Deprecated fields are not included in introspection queries unless +requested and they can still be queried by clients. + +```graphql +type Query { + users: [User] @deprecated(reason: "Use the `allUsers` field") + allUsers: [User] +} +``` + +### Definition + +```graphql +""" +Marks an element of a GraphQL schema as no longer supported. +""" +directive @deprecated( + """ + Explains why this element was deprecated, usually also including a + suggestion for how to access supported similar data. Formatted + in [Markdown](https://daringfireball.net/projects/markdown/). + """ + reason: String = "No longer supported" +) on FIELD_DEFINITION +``` + +## @field + +Assign a resolver function to a field. + +Pass a class and a method to the `resolver` argument and separate them with an `@` symbol. +If you pass only a class name, the method name defaults to `__invoke`. + +```graphql +type Mutation { + createPost(title: String!): Post + @field(resolver: "App\\GraphQL\\Mutations\\PostMutator@create") +} +``` + +### Definition + +```graphql +""" +Assign a resolver function to a field. +""" +directive @field( + """ + A reference to the resolver function to be used. + Consists of two parts: a class name and a method name, seperated by an `@` symbol. + If you pass only a class name, the method name defaults to `__invoke`. + """ + resolver: String! + + """ + Supply additional data to the resolver. + """ + args: [String!] +) on FIELD_DEFINITION +``` + +### Examples + +If your field is defined on the root types `Query` or `Mutation`, you can take advantage +of the default namespaces that are defined in the [configuration](../getting-started/configuration.md). The following +will look for a class in `App\GraphQL\Queries` by default. + +```graphql +type Query { + usersTotal: Int @field(resolver: "Statistics@usersTotal") +} +``` + +Be aware that resolvers are not limited to root fields. A resolver can be used for basic tasks +such as transforming the value of scalar fields, e.g. reformat a date. + +```graphql +type User { + created_at: String! + @field(resolver: "App\\GraphQL\\Types\\UserType@created_at") +} +``` + +## @find + +Find a model based on the arguments provided. + +```graphql +type Query { + userById(id: ID! @eq): User @find +} +``` + +### Definition + +```graphql +""" +Find a model based on the arguments provided. +""" +directive @find( + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] +) on FIELD_DEFINITION +``` + +### Examples + +This throws when more then one result is returned. +Use [@first](#first) if you can not ensure that. + +If your model does not sit in the default namespace, you can overwrite it. + +```graphql +type Query { + userById(id: ID! @eq): User @find(model: "App\\Authentication\\User") +} +``` + +## @first + +Get the first query result from a collection of Eloquent models. + +```graphql +type Query { + userByFirstName(first_name: String! @eq): User @first +} +``` + +### Definition + +```graphql +""" +Get the first query result from a collection of Eloquent models. +""" +directive @first( + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] +) on FIELD_DEFINITION +``` + +### Examples + +Other then [@find](#find), this will not throw an error if more then one items are in the collection. + +If your model does not sit in the default namespace, you can overwrite it. + +```graphql +type Query { + userByFirstName(first_name: String! @eq): User + @first(model: "App\\Authentication\\User") +} +``` + +## @forceDelete + +```graphql +""" +Permanently remove one or more soft deleted models by their ID. +The field must have a single non-null argument that may be a list. +""" +directive @forceDelete( + """ + Set to `true` to use global ids for finding the model. + If set to `false`, regular non-global ids are used. + """ + globalId: Boolean = false + + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String +) on FIELD_DEFINITION +``` + +Use it on a root mutation field that returns an instance of the Model. + +```graphql +type Mutation { + forceDeletePost(id: ID!): Post @forceDelete +} +``` + +Works very similar to the [`@delete`](#delete) directive. + +## @enum + +```graphql +""" +Assign an internal value to an enum key. +When dealing with the Enum type in your code, +you will receive the defined value instead of the string key. +""" +directive @enum( + """ + The internal value of the enum key. + You can use any constant literal value: https://graphql.github.io/graphql-spec/draft/#sec-Input-Values + """ + value: Mixed +) on ENUM_VALUE +``` + +```graphql +enum Role { + ADMIN @enum(value: 1) + EMPLOYEE @enum(value: 2) +} +``` + +You do not need this directive if the internal value of each enum key +is an identical string. [Read more about enum types](../the-basics/types.md#enum) + +## @eq + +Place an equal operator on an Eloquent query. + +```graphql +type User { + posts(category: String @eq): [Post!]! @hasMany +} +``` + +### Definition + +```graphql +directive @eq( + """ + Specify the database column to compare. + Only required if database column has a different name than the attribute in your schema. + """ + key: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +### Examples + +If the name of the argument does not match the database column, +pass the actual column name as the `key`. + +```graphql +type User { + posts(category: String @eq(key: "cat")): [Post!]! @hasMany +} +``` + +## @event + +Fire an event after a mutation has taken place. +It requires the `dispatch` argument that should be +the class name of the event you want to fire. + +```graphql +type Mutation { + createPost(title: String!, content: String!): Post + @event(dispatch: "App\\Events\\PostCreated") +} +``` + +### Definition + +```graphql +""" +Fire an event after a mutation has taken place. +It requires the `dispatch` argument that should be +the class name of the event you want to fire. +""" +directive @event( + """ + Specify the fully qualified class name (FQCN) of the event to dispatch. + """ + dispatch: String! +) on FIELD_DEFINITION +``` + +## @globalId + +Converts between IDs/types and global IDs. + +```graphql +type User { + id: ID! @globalId + name: String +} +``` + +Instead of the original ID, the `id` field will now return a base64-encoded String +that globally identifies the User and can be used for querying the `node` endpoint. + +### Definition + +```graphql +""" +Converts between IDs/types and global IDs. +When used upon a field, it encodes, +when used upon an argument, it decodes. +""" +directive @globalId( + """ + By default, an array of `[$type, $id]` is returned when decoding. + You may limit this to returning just one of both. + Allowed values: "ARRAY", "TYPE", "ID" + """ + decode: String = "ARRAY" +) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION +``` + +### Examples + +```graphql +type Mutation { + deleteNode(id: ID @globalId): Node +} +``` + +The field resolver will receive the decoded version of the passed `id`, +split into type and ID. + +You may rebind the `\Nuwave\Lighthouse\Support\Contracts\GlobalId` interface to add your +own mechanism of encoding/decoding global ids. + +## @guard + +```graphql +""" +Run authentication through one or more guards. +This is run per field and may allow unauthenticated +users to still receive partial results. +""" +directive @guard( + """ + Specify which guards to use, e.g. "api". + When not defined, the default driver is used. + """ + with: [String!] +) on FIELD_DEFINITION | OBJECT +``` + +## @hash + +```graphql +""" +Use Laravel hashing to transform an argument value. + +Useful for hashing passwords before inserting them into the database. +This uses the default hashing driver defined in `config/hashing.php`. +""" +directive @hash on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +The most common use case for this is when dealing with passwords: + +```graphql +type Mutation { + createUser( + name: String! + password: String! @hash + ): User! +} +``` + +## @hasMany + +Corresponds to [the Eloquent relationship HasMany](https://laravel.com/docs/eloquent-relationships#one-to-many). + +```graphql +type User { + posts: [Post!]! @hasMany +} +``` + +### Definition + +```graphql +""" +Corresponds to [the Eloquent relationship HasMany](https://laravel.com/docs/eloquent-relationships#one-to-many). +""" +directive @hasMany( + """ + Specify the relationship method name in the model class, + if it is named different from the field in the schema. + """ + relation: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] + + """ + ALlows to resolve the relation as a paginated list. + Allowed values: `paginator`, `connection`. + """ + type: String + + """ + Specify the default quantity of elements to be returned. + Only applies when using pagination. + """ + defaultCount: Int + + """ + Specify the maximum quantity of elements to be returned. + Only applies when using pagination. + """ + maxCount: Int +) on FIELD_DEFINITION +``` + +### Examples + +You can return the related models paginated by setting the `type`. + +```graphql +type User { + postsPaginated: [Post!]! @hasMany(type: "paginator") + postsRelayConnection: [Post!]! @hasMany(type: "connection") +} +``` + +If the name of the relationship on the Eloquent model is different than the field name, +you can override it by setting `relation`. + +```graphql +type User { + posts: [Post!]! @hasMany(relation: "articles") +} +``` + +## @hasOne + +Corresponds to [Eloquent's HasOne-Relationship](https://laravel.com/docs/eloquent-relationships#one-to-one). + +```graphql +type User { + phone: Phone @hasOne +} +``` + +### Definition + +```graphql +""" +Corresponds to [the Eloquent relationship HasOne](https://laravel.com/docs/eloquent-relationships#one-to-one). +""" +directive @hasOne( + """ + Specify the relationship method name in the model class, + if it is named different from the field in the schema. + """ + relation: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] +) on FIELD_DEFINITION +``` + +### Examples + +If the name of the relationship on the Eloquent model is different than the field name, +you can override it by setting `relation`. + +```graphql +type User { + phone: Phone @hasOne(relation: "telephone") +} +``` + +## @in + +Filter a column by an array using a `whereIn` clause. + +```graphql +type Query { + posts(includeIds: [Int!] @in(key: "id")): [Post!]! @paginate +} +``` + +### Definition + +```graphql +directive @in( + """ + Specify the database column to compare. + Only required if database column has a different name than the attribute in your schema. + """ + key: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +## @include + +This directive is part of the [GraphQL spec](https://graphql.github.io/graphql-spec/June2018/#sec--include) +and it should be noted this directive is a client side and should not be included in your schema. + +Only includes a field in response if the value passed into this directive is true. This directive is one of the core +directives in the GraphQL spec. + +```graphql +directive @include( + """ + If the "if" value is true the field this is connected with will be included in the query response. + Otherwise it will not. + """ + if: Boolean +) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +``` + +### Examples + +The `@include` directive may be provided for fields, fragment spreads, and inline fragments, +and allows for conditional inclusion during execution as described by the `if` argument. + +In this example experimentalField will only be queried if the variable $someTest has the value true + +```graphql +query myQuery($someTest: Boolean) { + experimentalField @include(if: $someTest) +} +``` + +## @inject + +Inject a value from the context object into the arguments. + +```graphql +type Mutation { + createPost(title: String!, content: String!): Post + @create + @inject(context: "user.id", name: "user_id") +} +``` + +This is useful to ensure that the authenticated user's `id` is +automatically used for creating new models and can not be manipulated. + +### Definition + +```graphql +directive @inject( + """ + A path to the property of the context that will be injected. + If the value is nested within the context, you may use dot notation + to get it, e.g. "user.id". + """ + context: String! + + """ + The target name of the argument into which the value is injected. + You can use dot notation to set the value at arbitrary depth + within the incoming argument. + """ + name: String! +) on FIELD_DEFINITION +``` + +### Examples + +If you are using an Input Object as an argument, you can use dot notation to +set a nested argument. + +```graphql +type Mutation { + createTask(input: CreateTaskInput!): Task + @create + @inject(context: "user.id", name: "input.user_id") +} +``` + +## @interface + +Use a custom resolver to determine the concrete type of an interface. + +Make sure you read the [basics about Interfaces](../the-basics/types.md#interface) before deciding +to use this directive, you probably don't need it. + +Set the `resolveType` argument to a function that returns the implementing Object Type. + +```graphql +interface Commentable + @interface(resolveType: "App\\GraphQL\\Interfaces\\Commentable@resolveType") { + id: ID! +} +``` + +The function receives the value of the parent field as its single argument and must +return an Object Type. You can get the appropriate Object Type from Lighthouse's type registry. + +```php +typeRegistry = $typeRegistry; + } + + /** + * Decide which GraphQL type a resolved value has. + * + * @param mixed $rootValue The value that was resolved by the field. Usually an Eloquent model. + * @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context + * @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo + * @return \GraphQL\Type\Definition\Type + */ + public function resolveType($rootValue, GraphQLContext $context, ResolveInfo $resolveInfo): Type + { + // Default to getting a type with the same name as the passed in root value + // TODO implement your own resolver logic - if the default is fine, just delete this class + return $this->typeRegistry->get(class_basename($rootValue)); + } +} +``` + +### Definition + +```graphql +""" +Use a custom resolver to determine the concrete type of an interface. +""" +directive @interface( + """ + Reference to a custom type-resolver function. + Consists of two parts: a class name and a method name, seperated by an `@` symbol. + If you pass only a class name, the method name defaults to `__invoke`. + """ + resolveType: String! +) on INTERFACE +``` + +## @lazyLoad + +```graphql +""" +Perform a [lazy eager load](https://laravel.com/docs/eloquent-relationships#lazy-eager-loading) +on the relations of a list of models. +""" +directive @lazyLoad( + """ + The names of the relationship methods to load. + """ + relations: [String!]! +) on FIELD_DEFINITION +``` + +This is often useful when loading relationships with the [`@hasMany`](#hasmany) directive. + +```graphql +type Post { + comments: [Comment!]! @hasMany @lazyLoad(relations: ["replies"]) +} +``` + +## @method + +```graphql +""" +Resolve a field by calling a method on the parent object. + +Use this if the data is not accessible through simple property access or if you +want to pass argument to the method. +""" +directive @method( + """ + Specify the method of which to fetch the data from. + Defaults to the name of the field if not given. + """ + name: String + + """ + Pass the field arguments to the method, using the argument definition + order from the schema to sort them before passing them along. + + @deprecated This behaviour will default to true in v5 and this setting will be removed. + """ + passOrdered: Boolean = false +) on FIELD_DEFINITION +``` + +This can be useful on models or other classes that have getters: + +```graphql +type User { + mySpecialData: String! @method(name: "getMySpecialData") +} +``` + +This calls a method `App\User::getMySpecialData` with [the typical resolver arguments](resolvers.md#resolver-function-signature). +If you want to pass down only the arguments in sequence, use the `passOrdered` option: + +```graphql +type User { + purchasedItemsCount( + year: Int! + includeReturns: Boolean + ): Int @method(passOrdered: true) +} +``` + +This will call the method with the arguments a client passes to the field. +Ensure the order of the argument definition matches the parameters of your method. + +```php +public function purchasedItemsCount(int $year, ?bool $includeReturns) +``` + +Lighthouse will always pass down the same number of arguments and default to `null` +if the client passes nothing. + +```graphql +{ + user(id: 3) { + purchasedItemsCount(year: 2017) + } +} +``` + +The method will get called like this: + +```php +$user->purchasedItemsCount(2017, null) +``` + +## @middleware + +**DEPRECATED** +Use [`@guard`](#guard) or custom [`FieldMiddleware`](../custom-directives/field-directives.md#fieldmiddleware) instead. + +```graphql +""" +Run Laravel middleware for a specific field or group of fields. +This can be handy to reuse existing HTTP middleware. +""" +directive @middleware( + """ + Specify which middleware to run. + Pass in either a fully qualified class name, an alias or + a middleware group - or any combination of them. + """ + checks: [String!] +) on FIELD_DEFINITION | OBJECT +``` + +You can define middleware just like you would in Laravel. Pass in either a fully qualified +class name, an alias or a middleware group - or any combination of them. + +```graphql +type Query { + users: [User!]! + @middleware( + checks: ["auth:api", "App\\Http\\Middleware\\MyCustomAuth", "api"] + ) + @all +} +``` + +If you need to apply middleware to a group of fields, you can put [@middleware](../api-reference/directives.md#middleware) on an Object type. +The middleware will apply only to direct child fields of the type definition. + +```graphql +type Query @middleware(checks: ["auth:api"]) { + # This field will use the "auth:api" middleware + users: [User!]! @all +} + +extend type Query { + # This field will not use any middleware + posts: [Post!]! @all +} +``` + +Other then global middleware defined in the [configuration](../getting-started/configuration.md), field middleware +only applies to the specific field it is defined on. This has the benefit of limiting errors +to particular fields and not failing an entire request if a middleware fails. + +There are a few caveats to field middleware though: + +- The Request object is shared between fields. + If the middleware of one field modifies the Request, this does influence other fields. +- They not receive the complete Response object when calling `$next($request)`, + but rather the slice of data that the particular field returned. +- The `terminate` method of field middleware is not called. + +If the middleware needs to be aware of GraphQL specifics, such as the resolver arguments, +it is often more suitable to define a custom field directive. + +## @model + +```graphql +""" +Enable fetching an Eloquent model by its global id through the `node` query. + +@deprecated(reason: "Use @node instead. This directive will be repurposed and do what @modelClass does now in v5.") +""" +directive @model on OBJECT +``` + +**Deprecated** Use [`@node`](#node) for Relay global object identification. + +## @modelClass + +```graphql +""" +Map a model class to an object type. +This can be used when the name of the model differs from the name of the type. + +**This directive will be renamed to @model in v5.** +""" +directive @modelClass( + """ + The class name of the corresponding model. + """ + class: String! +) on OBJECT +``` + +**Attention** This directive will be renamed to `@model` in v5. + +Lighthouse will respect the overwritten model name in it's directives. + +```graphql +type Post @modelClass(class: "\\App\\BlogPost") { + title: String! +} +``` + +## @morphMany + +Corresponds to [Eloquent's MorphMany-Relationship](https://laravel.com/docs/5.8/eloquent-relationships#one-to-many-polymorphic-relations). + +```graphql +type Post { + images: [Image!] @morphMany +} + +type Image { + imagable: Imageable! @morphTo +} + +union Imageable = Post | User +``` + +### Definition + +```graphql +""" +Corresponds to [Eloquent's MorphMany-Relationship](https://laravel.com/docs/5.8/eloquent-relationships#one-to-one-polymorphic-relations). +""" +directive @morphMany( + """ + Specify the relationship method name in the model class, + if it is named different from the field in the schema. + """ + relation: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] + + """ + ALlows to resolve the relation as a paginated list. + Allowed values: `paginator`, `connection`. + """ + type: String + + """ + Specify the default quantity of elements to be returned. + Only applies when using pagination. + """ + defaultCount: Int + + """ + Specify the maximum quantity of elements to be returned. + Only applies when using pagination. + """ + maxCount: Int + + """ + Specify a custom type that implements the Edge interface + to extend edge object. + Only applies when using Relay style "connection" pagination. + """ + edgeType: String +) on FIELD_DEFINITION +``` + +## @morphOne + +Corresponds to [Eloquent's MorphOne-Relationship](https://laravel.com/docs/5.8/eloquent-relationships#one-to-one-polymorphic-relations). + +```graphql +type Post { + image: Image! @morphOne +} + +type Image { + imagable: Imageable! @morphTo +} + +union Imageable = Post | User +``` + +### Definition + +```graphql +""" +Corresponds to [Eloquent's MorphOne-Relationship](https://laravel.com/docs/5.8/eloquent-relationships#one-to-one-polymorphic-relations). +""" +directive @morphOne( + """ + Specify the relationship method name in the model class, + if it is named different from the field in the schema. + """ + relation: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] +) on FIELD_DEFINITION +``` + +## @morphTo + +Corresponds to [Eloquent's MorphTo-Relationship](https://laravel.com/docs/5.8/eloquent-relationships#one-to-one-polymorphic-relations). + +```graphql +type Image { + imagable: Imageable! @morphTo +} + +union Imageable = Post | User +``` + +### Definition + +```graphql +""" +Corresponds to [Eloquent's MorphTo-Relationship](https://laravel.com/docs/5.8/eloquent-relationships#one-to-one-polymorphic-relations). +""" +directive @morphTo( + """ + Specify the relationship method name in the model class, + if it is named different from the field in the schema. + """ + relation: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] +) on FIELD_DEFINITION +``` + +## @namespace + +Redefine the default namespaces used in other directives. + +The following example applies the namespace `App\Blog` +to the `@field` directive used on the `posts` field. + +```graphql +type Query { + posts: [Post!]! @field(resolver: "Post@resolveAll") @namespace(field: "App\\Blog") +} +``` + +### Definition + +```graphql +""" +Redefine the default namespaces used in other directives. +The arguments are a map from directive names to namespaces. +""" +directive @namespace on FIELD_DEFINITION | OBJECT +``` + +### Examples + +When used upon an object type or an object type extension, the namespace +applies to fields of the type as well. This allows you to specify +a common namespace for a group of fields. + +```graphql +extend type Query @namespace(field: "App\\Blog") { + posts: [Post!]! @field(resolver: "Post@resolveAll") +} +``` + +A `@namespace` directive defined on a field directive wins in case of a conflict. + +## @neq + +Place a not equals operator `!=` on an Eloquent query. + +```graphql +type User { + posts(excludeCategory: String @neq(key: "category")): [Post!]! @hasMany +} +``` + +### Definition + +```graphql +""" +Place a not equals operator `!=` on an Eloquent query. +""" +directive @neq( + """ + Specify the database column to compare. + Only required if database column has a different name than the attribute in your schema. + """ + key: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +## @nest + +```graphql +""" +A no-op nested arg resolver that delegates all calls +to the ArgResolver directives attached to the children. +""" +directive @nest on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +This may be useful to logically group arg resolvers. + +```graphql +type Mutation { + createUser( + name: String + tasks: UserTasksOperations @nest + ): User @create +} + +input UserTasksOperations { + newTask: CreateTaskInput @create(relation: "tasks") +} + +input CreateTaskInput { + name: String +} + +type Task { + name: String! +} + +type User { + name: String + tasks: [Task!]! @hasMany +} +``` + +## @node + +```graphql +""" +Register a type for Relay's global object identification. +When used without any arguments, Lighthouse will attempt +to resolve the type through a model with the same name. +""" +directive @node( + """ + Reference to a function that receives the decoded `id` and returns a result. + Consists of two parts: a class name and a method name, seperated by an `@` symbol. + If you pass only a class name, the method name defaults to `__invoke`. + """ + resolver: String + + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String +) on FIELD_DEFINITION +``` + +Lighthouse defaults to resolving types through the underlying model, +for example by calling `User::find($id)`. + +```graphql +type User @node { + id: ID! @globalId +} +``` + +You can also use a custom resolver function to resolve any kind of data. + +```graphql +type Country @node(resolver: "App\\Countries@byId") { + name: String! +} +``` + +The `resolver` argument has to specify a function which will be passed the +decoded `id` and resolves to a result. + +```php +public function byId($id): array { + return [ + 'DE' => ['name' => 'Germany'], + 'MY' => ['name' => 'Malaysia'], + ][$id]; +} +``` + +[Read more](../digging-deeper/relay.md#global-object-identification). + +### Definition + +Behind the scenes, Lighthouse will decode the global id sent from the client +to find the model by it's primary id in the database. + +## @notIn + +Filter a column by an array using a `whereNotIn` clause. + +```graphql +type Query { + posts(excludeIds: [Int!] @notIn(key: "id")): [Post!]! @paginate +} +``` + +### Definition + +```graphql +""" +Filter a column by an array using a `whereNotIn` clause. +""" +directive @notIn( + """ + Specify the name of the column. + Only required if it differs from the name of the argument. + """ + key: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +## @orderBy + +```graphql +""" +Sort a result list by one or more given columns. +""" +directive @orderBy( + """ + Restrict the allowed column names to a well-defined list. + This improves introspection capabilities and security. + If not given, the column names can be passed as a String by clients. + Mutually exclusive with the `columnsEnum` argument. + """ + columns: [String!] + + """ + Use an existing enumeration type to restrict the allowed columns to a predefined list. + This allowes you to re-use the same enum for multiple fields. + Mutually exclusive with the `columns` argument. + """ + columnsEnum: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +Use it on a field argument of an Eloquent query. The type of the argument +can be left blank as `_` , as it will be automatically generated. + +```graphql +type Query { + posts(orderBy: _ @orderBy(columns: ["posted_at", "title"])): [Post!]! @all +} +``` + +Lighthouse will automatically generate an input that takes enumerated column names, +together with the `SortOrder` enum, and add that to your schema. Here is how it looks: + +```graphql +"Allows ordering a list of records." +input PostsOrderByOrderByClause { + "The column that is used for ordering." + column: PostsOrderByColumn! + + "The direction that is used for ordering." + order: SortOrder! +} + +"Order by clause for the `orderBy` argument on the query `posts`." +enum PostsOrderByColumn { + POSTED_AT @enum(value: "posted_at") + TITLE @enum(value: "title") +} + +"The available directions for ordering a list of records." +enum SortOrder { + "Sort records in ascending order." + ASC + + "Sort records in descending order." + DESC +} +``` + +If you want to re-use a list of allowed columns, you can define your own enumeration type and use the `columnsEnum` argument instead of `columns`. +Here's an example of how you could define it in your schema: + +```graphql +type Query { + allPosts(orderBy: _ @orderBy(columnsEnum: "PostColumn")): [Post!]! @all + paginatedPosts(orderBy: _ @orderBy(columnsEnum: "PostColumn")): [Post!]! @paginate +} + +"A custom description for this custom enum." +enum PostColumn { + # Another reason why you might want to have a custom enum is to + # correct typos or bad naming in column names. + POSTED_AT @enum(value: "postd_timestamp") + TITLE @enum(value: "title") +} +``` + +Lighthouse will still automatically generate the necessary input types and the `SortOrder` enum. +But instead of generating enums for the allowed columns, it will simply use the existing `PostColumn` enum. + +Querying a field that has an `orderBy` argument looks like this: + +```graphql +{ + posts ( + orderBy: [ + { + column: POSTED_AT + order: ASC + } + ] + ) { + title + } +} +``` + +You may pass more than one sorting option to add a secondary ordering. + +### Input Definition Example + +The `@orderBy` directive can also be applied inside an input field definition when used in conjunction with the [`@spread`](#spread) directive. See below for example: + +```graphql +type Query{ + posts(filter: PostFilterInput @spread): Posts +} + +input PostFilterInput { + orderBy: [OrderByClause!] @orderBy +} +``` + +And usage example: + +```graphql +{ + posts(filter: { + orderBy: [ + { + field: "postedAt" + order: ASC + } + ] + }) { + title + } +} +``` + +## @paginate + +```graphql +""" +Query multiple model entries as a paginated list. +""" +directive @paginate( + """ + Which pagination style to use. + Allowed values: `paginator`, `connection`. + """ + type: String = "paginator" + + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String + + """ + Point to a function that provides a Query Builder instance. + This replaces the use of a model. + """ + builder: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] + + """ + Overwrite the paginate_max_count setting value to limit the + amount of items that a user can request per page. + """ + maxCount: Int + + """ + Use a default value for the amount of returned items + in case the client does not request it explicitly + """ + defaultCount: Int +) on FIELD_DEFINITION +``` + +### Basic usage + +This directive is meant to be used on root query fields: + +```graphql +type Query { + posts: [Post!]! @paginate +} +``` + +> When you want to paginate a relationship, use the to-many relationship +> directives such as [`@hasMany`](directives.md#hasmany) instead. + +The schema definition is automatically transformed to this: + +```graphql +type Query { + posts(first: Int!, page: Int): PostPaginator +} + +"A paginated list of Post items." +type PostPaginator { + "A list of Post items." + data: [Post!]! + + "Pagination information about the list of items." + paginatorInfo: PaginatorInfo! +} +``` + +And can be queried like this: + +```graphql +{ + posts(first: 10) { + data { + id + title + } + paginatorInfo { + currentPage + lastPage + } + } +} +``` + +### Pagination type + +The `type` of pagination defaults to `paginator`, but may also be set to a Relay +compliant `connection`. + +> Lighthouse does not support actual cursor-based pagination as of now, see https://github.com/nuwave/lighthouse/issues/311 for details. +> Under the hood, the "cursor" is decoded into a page offset. + +```graphql +type Query { + posts: [Post!]! @paginate(type: "connection") +} +``` + +The final schema will be transformed to this: + +```graphql +type Query { + posts(first: Int!, page: Int): PostConnection +} + +"A paginated list of Post edges." +type PostConnection { + "Pagination information about the list of edges." + pageInfo: PageInfo! + + "A list of Post edges." + edges: [PostEdge] +} + +"An edge that contains a node of type Post and a cursor." +type PostEdge { + "The Post node." + node: Post + + "A unique cursor that can be used for pagination." + cursor: String! +} +``` + +### Default count + +You can supply a `defaultCount` to set a default count for any kind of paginator. + +```graphql +type Query { + posts: [Post!]! @paginate(type: "connection", defaultCount: 25) +} +``` + +This let's you omit the `count` argument when querying: + +```graphql +query { + posts { + id + name + } +} +``` + +### Limit maximum count + +Lighthouse allows you to specify a global maximum for the number of items a user +can request through pagination through the config. You may also overwrite this +per field with the `maxCount` argument: + +```graphql +type Query { + posts: [Post!]! @paginate(maxCount: 10) +} +``` + +### Overwrite model + +By default, Lighthouse looks for an Eloquent model in the configured default namespace, with the same +name as the returned type. You can overwrite this by setting the `model` argument. + +```graphql +type Query { + posts: [Post!]! @paginate(model: "App\\Blog\\BlogPost") +} +``` + +### Custom builder + +If simply querying Eloquent does not fit your use-case, you can specify a custom `builder`. + +```graphql +type Query { + posts: [Post!]! @paginate(builder: "App\\Blog@visiblePosts") +} +``` + +Your method receives the typical resolver arguments and has to return an instance of `Illuminate\Database\Query\Builder`. + +```php +where('visible', true) + ->where('posted_at', '>', $args['after']); + } +} +``` + +## @rename + +```graphql +""" +Change the internally used name of a field or argument. +This does not change the schema from a client perspective. +""" +directive @rename( + """ + The internal name of an attribute/property/key. + """ + attribute: String! +) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +This can often be useful to ensure consistent naming of your schema +without having to change the underlying models. + +```graphql +type User { + createdAt: String! @rename(attribute: "created_at") +} + +input UserInput { + firstName: String! @rename(attribute: "first_name") +} +``` + +## @restore + +```graphql +""" +Un-delete one or more soft deleted models by their ID. +The field must have a single non-null argument that may be a list. +""" +directive @restore( + """ + Set to `true` to use global ids for finding the model. + If set to `false`, regular non-global ids are used. + """ + globalId: Boolean = false + + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String +) on FIELD_DEFINITION +``` + +Use it on a root mutation field that returns an instance of the Model. + +```graphql +type Mutation { + restorePost(id: ID!): Post @restore +} +``` + +Works very similar to the [`@delete`](#delete) directive. + +## @rules + +```graphql +""" +Validate an argument using [Laravel validation](https://laravel.com/docs/validation). +""" +directive @rules( + """ + Specify the validation rules to apply to the field. + This can either be a reference to [Laravel's built-in validation rules](https://laravel.com/docs/validation#available-validation-rules), + or the fully qualified class name of a custom validation rule. + + Rules that mutate the incoming arguments, such as `exclude_if`, are not supported + by Lighthouse. Use ArgTransformerDirectives or FieldMiddlewareDirectives instead. + """ + apply: [String!]! + + """ + Specify the messages to return if the validators fail. + Specified as an input object that maps rules to messages, + e.g. { email: "Must be a valid email", max: "The input was too long" } + """ + messages: [RulesMessageMap!] +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +For example, this rule ensures that users pass a valid 2 character country code: + +```graphql +type Query { + users( + countryCode: String @rules(apply: ["string", "size:2"]) + ): [User!]! @all +} +``` + +Read more in the [validation docs](../security/validation.md#validating-arguments). + +## @rulesForArray + +Run validation on an array itself, using [Laravel built-in validation](https://laravel.com/docs/validation). + +```graphql +type Mutation { + saveIcecream( + flavors: [IcecreamFlavor!]! @rulesForArray(apply: ["min:3"]) + ): Icecream +} +``` + +Read more in the [validation docs](../security/validation.md#validating-arrays). + +### Definition + +```graphql +""" +Run validation on an array itself, using [Laravel built-in validation](https://laravel.com/docs/validation). +""" +directive @rulesForArray( + """ + Specify the validation rules to apply to the field. + This can either be a reference to any of Laravel's built-in validation rules: https://laravel.com/docs/validation#available-validation-rules, + or the fully qualified class name of a custom validation rule. + """ + apply: [String!]! + + """ + Specify the messages to return if the validators fail. + Specified as an input object that maps rules to messages, + e.g. { email: "Must be a valid email", max: "The input was too long" } + """ + messages: [RulesMessageMap!] +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +## @scalar + +Reference a class implementing a scalar definition. +[Learn how to implement your own scalar.](http://webonyx.github.io/graphql-php/type-system/scalar-types/) + +```graphql +scalar DateTime @scalar(class: "DateTimeScalar") +``` + +If you follow the namespace convention, you do not need this directive. +Lighthouse looks into your configured scalar namespace for a class with the same name. + +### Definition + +```graphql +""" +Reference a class implementing a scalar definition. +""" +directive @scalar( + """ + Reference to a class that extends `\GraphQL\Type\Definition\ScalarType`. + """ + class: String! +) on SCALAR +``` + +### Examples + +If your class is not in the default namespace, pass a fully qualified class name. + +```graphql +scalar DateTime + @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime") +``` + +## @scope + +```graphql +""" +Adds a scope to the query builder. +The scope method will receive the client-given value of the argument as the second parameter. +""" +directive @scope( + """ + The name of the scope. + """ + name: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +You may use this in combination with field directives such as [`@all`](#all). + +```graphql +type Query { + posts( + trending: Boolean @scope(name: "trending") + ): [Post!]! @all +} +``` + +## @search + +Perform a full-text by the given input value. + +```graphql +type Query { + posts(search: String @search): [Post!]! @paginate +} +``` + +The `search()` method of the model is called with the value of the argument, +using the driver you configured for [Laravel Scout](https://laravel.com/docs/master/scout). + +Take care when using the `@search` directive in combination with other directives +that influence the database query. The usual query builder `Eloquent\Builder` +will be replaced by a `Scout\Builder`, which does not support the same methods and operations. +Regular filters such as [`@eq`](#eq) or [`@in`](#in) still work, but scopes do not. + +### Definition + +```graphql +""" +Perform a full-text by the given input value. +""" +directive @search( + """ + Specify a custom index to use for search. + """ + within: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +### Examples + +Normally the search will be performed using the index specified by the model's `searchableAs` method. +However, in some situation a custom index might be needed, this can be achieved by using the argument `within`. + +```graphql +type Query { + posts(search: String @search(within: "my.index")): [Post!]! @paginate +} +``` + +## @skip + +This directive is part of the [GraphQL spec](https://graphql.github.io/graphql-spec/June2018/#sec--include) +and it should be noted this directive is a client side directive and should not be included in your schema. + +### Definition +```graphql +directive @skip( + """ + If the value passed into the if field is true the field this + is decorating will not be included in the query response. + """ + if: Boolean! +) +on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +``` + +### Examples + +The `@skip` directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional +exclusion during execution as described by the if argument. + +In this example experimentalField will only be queried if the variable $someTest has the value `false`. + +```graphql +query myQuery($someTest: Boolean) { + experimentalField @skip(if: $someTest) +} +``` + +## @softDeletes + +```graphql +""" +Allows to filter if trashed elements should be fetched. +This manipulates the schema by adding the argument +`trashed: Trashed @trashed` to the field. +""" +directive @softDeletes on FIELD_DEFINITION +``` + +The following schema definition from a `.graphql` file: + +```graphql +type Query { + tasks: [Tasks!]! @all @softDeletes +} +``` + +Will result in a schema that looks like this: + +```graphql +type Query { + tasks(trashed: Trashed @trashed): [Tasks!]! @all +} +``` + +Find out how the added filter works: [`@trashed`](#trashed) + +## @spread + +```graphql +""" +Merge the fields of a nested input object into the arguments of its parent +when processing the field arguments given by a client. +""" +directive @spread on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +You may use `@spread` on field arguments or on input object fields: + +```graphql +type Mutation { + updatePost( + id: ID! + input: PostInput! @spread + ): Post @update +} + +input PostInput { + title: String! + content: PostContent @spread +} + +input PostContent { + imageUrl: String +} +``` + +The schema does not change, client side usage works as if `@spread` was not there: + +```graphql +mutation { + updatePost( + id: 12 + input: { + title: "My awesome title" + content: { + imageUrl: "http://some.site/image.jpg" + } + } + ) { + id + } +} +``` + +Internally, the arguments will be transformed into a flat structure before +they are passed along to the resolver: + +```php +[ + 'id' => 12, + 'title' => 'My awesome title', + 'imageUrl' = 'http://some.site/image.jpg', +] +``` + +Note that Lighthouse spreads out the arguments **after** all other [ArgDirectives](../custom-directives/argument-directives.md) +have been applied, e.g. validation, transformation. + +## @subscription + +Reference a class to handle the broadcasting of a subscription to clients. +The given class must extend `\Nuwave\Lighthouse\Schema\Types\GraphQLSubscription`. + +If you follow the default naming conventions for [defining subscription fields](../subscriptions/defining-fields.md) +you do not need this directive. It is only useful if you need to override the default namespace. + +```graphql +type Subscription { + postUpdated(author: ID!): Post + @subscription( + class: "App\\GraphQL\\Blog\\PostUpdatedSubscription" + ) +} +``` + +### Definition + +```graphql +""" +Reference a class to handle the broadcasting of a subscription to clients. +The given class must extend `\Nuwave\Lighthouse\Schema\Types\GraphQLSubscription`. +""" +directive @subscription( + """ + A reference to a subclass of `\Nuwave\Lighthouse\Schema\Types\GraphQLSubscription`. + """ + class: String! +) on FIELD_DEFINITION +``` + +## @trashed + +```graphql +""" +Allows to filter if trashed elements should be fetched. +""" +directive @trashed on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +The most convenient way to use this directive is through [`@softDeletes`](#softdeletes). + +If you want to add it manually, make sure the argument is of the +enum type `Trashed`: + +```graphql +type Query { + flights(trashed: Trashed @trashed): [Flight!]! @all +} +``` + +## @trim + +Run the `trim` function on an input value. + +```graphql +type Mutation { + createUser(name: String @trim): User +} +``` + +### Definition + +```graphql +""" +Run the `trim` function on an input value. +""" +directive @trim on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +## @union + +Use a custom function to determine the concrete type of unions. + +Make sure you read the [basics about Unions](../the-basics/types.md#union) before deciding +to use this directive, you probably don't need it. + +```graphql +type User { + id: ID! +} + +type Employee { + employeeId: ID! +} + +union Person @union(resolveType: "App\\GraphQL\\Unions\\Person@resolveType") = + User + | Employee +``` + +The function receives the value of the parent field as its single argument and must +resolve an Object Type from Lighthouse's `TypeRegistry`. + +```php +typeRegistry = $typeRegistry; + } + + /** + * Decide which GraphQL type a resolved value has. + * + * @param mixed $rootValue The value that was resolved by the field. Usually an Eloquent model. + * @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context + * @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo + * @return \GraphQL\Type\Definition\Type + */ + public function resolveType($rootValue, GraphQLContext $context, ResolveInfo $resolveInfo): Type + { + // Default to getting a type with the same name as the passed in root value + // TODO implement your own resolver logic - if the default is fine, just delete this class + return $this->typeRegistry->get(class_basename($rootValue)); + } +} +``` + +### Definition + +```graphql +""" +Use a custom function to determine the concrete type of unions. +""" +directive @union( + """ + Reference a function that returns the implementing Object Type. + Consists of two parts: a class name and a method name, seperated by an `@` symbol. + If you pass only a class name, the method name defaults to `__invoke`. + """ + resolveType: String! +) on UNION +``` + +## @update + +```graphql +""" +Update an Eloquent model with the input values of the field. +""" +directive @update( + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String + + """ + Set to `true` to use global ids for finding the model. + If set to `false`, regular non-global ids are used. + """ + globalId: Boolean = false + + """ + Specify the name of the relation on the parent model. + This is only needed when using this directive as a nested arg + resolver and if the name of the relation is not the arg name. + """ + relation: String +) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +Use it on a root mutation field that returns an instance of the Model. + +```graphql +type Mutation { + updatePost(id: ID!, content: String): Post @update +} +``` + +Lighthouse uses the argument `id` to fetch the model by its primary key. +This will work even if your model has a differently named primary key, +so you can keep your schema simple and independent of your database structure. + +If you want your schema to directly reflect your database schema, +you can also use the name of the underlying primary key. +This is not recommended as it makes client-side caching more difficult +and couples your schema to the underlying implementation. + +```graphql +type Mutation { + updatePost(post_id: ID!, content: String): Post @update +} +``` + +If the name of the Eloquent model does not match the return type of the field, +or is located in a non-default namespace, set it with the `model` argument. + +```graphql +type Mutation { + updateAuthor(id: ID!, name: String): Author @update(model: "App\\User") +} +``` + +This directive can also be used as a [nested arg resolver](../concepts/arg-resolvers.md). + +## @upsert + +```graphql +""" +Create or update an Eloquent model with the input values of the field. +""" +directive @upsert( + """ + Specify the class name of the model to use. + This is only needed when the default model detection does not work. + """ + model: String + + """ + Set to `true` to use global ids for finding the model. + If set to `false`, regular non-global ids are used. + """ + globalId: Boolean = false + + """ + Specify the name of the relation on the parent model. + This is only needed when using this directive as a nested arg + resolver and if the name of the relation is not the arg name. + """ + relation: String +) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +Lighthouse will try to to fetch the model by its primary key, just like [`@update`](#update). +If the model doesn't exist, it will be newly created with a given `id`. +In case no `id` is specified, an auto-generated fresh ID will be used instead. + +```graphql +type Mutation { + upsertPost(post_id: ID!, content: String): Post @upsert +} +``` + +This directive can also be used as a [nested arg resolver](../concepts/arg-resolvers.md). + +## @where + +Use an input value as a [where filter](https://laravel.com/docs/queries#where-clauses). + +You can specify simple operators: + +```graphql +type Query { + postsSearchTitle(title: String! @where(operator: "like")): [Post!]! @all +} +``` + +Or use the additional clauses that Laravel provides: + +```graphql +type Query { + postsByYear(created_at: Int! @where(clause: "whereYear")): [Post!]! @all +} +``` + +### Definition + +```graphql +""" +Use an input value as a [where filter](https://laravel.com/docs/queries#where-clauses). +""" +directive @where( + """ + Specify the operator to use within the WHERE condition. + """ + operator: String = "=" + + """ + Specify the database column to compare. + Only required if database column has a different name than the attribute in your schema. + """ + key: String + + """ + Use Laravel's where clauses upon the query builder. + """ + clause: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +## @whereBetween + +```graphql +""" +Verify that a column's value is between two values. +The type of the input value this is defined upon should be +an `input` object with two fields. +""" +directive @whereBetween( + """ + Specify the database column to compare. + Only required if database column has a different name than the attribute in your schema. + """ + key: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +This example defines an `input` to filter that a value is between two dates. + +```graphql +type Query { + posts( + created_at: DateRange @whereBetween + ): [Post!]! @all +} + +input DateRange { + from: Date! + to: Date! +} +``` + +You may use any custom `input` type for the argument. Make sure it has +exactly two required fields to ensure the query is valid. + +## @whereConditions + +The documentation for this directive is found in [`Complex Where Conditions`](../eloquent/complex-where-conditions.md#whereconditions). + +## @whereHasConditions + +The documentation for this directive is found in [`Complex Where Conditions`](../eloquent/complex-where-conditions.md#wherehasconditions). + +## @whereJsonContains + +Use an input value as a [whereJsonContains filter](https://laravel.com/docs/queries#json-where-clauses). + +```graphql +type Query { + posts(tags: [String]! @whereJsonContains): [Post!]! @all +} +``` + +You may use the `key` argument to look into the JSON content: + +```graphql +type Query { + posts(tags: [String]! @whereJsonContains(key: "tags->recent")): [Post!]! @all +} +``` + +### Definition + +```graphql +""" +Use an input value as a [whereJsonContains filter](https://laravel.com/docs/queries#json-where-clauses). +""" +directive @whereJsonContains( + """ + Specify the database column and path inside the JSON to compare. + Only required if database column has a different name than the attribute in your schema. + """ + key: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +## @whereNotBetween + +Verify that a column's value lies outside of two values. +The type of the input value this is defined upon should be +an `input` object with two fields. + +```graphql +type Query { + posts( + notCreatedDuring: DateRange @whereNotBetween(key: "created_at") + ): [Post!]! @all +} + +input DateRange { + from: Date! + to: Date! +} +``` + +### Definition + +```graphql +""" +Verify that a column's value lies outside of two values. +The type of the input value this is defined upon should be +an `input` object with two fields. +""" +directive @whereNotBetween( + """ + Specify the database column to compare. + Only required if database column has a different name than the attribute in your schema. + """ + key: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +## @with + +Eager-load an Eloquent relation. + +```graphql +type User { + taskSummary: String! + @with(relation: "tasks") + @method(name: "getTaskSummary") +} +``` + +### Definition + +```graphql +""" +Eager-load an Eloquent relation. +""" +directive @with( + """ + Specify the relationship method name in the model class, + if it is named different from the field in the schema. + """ + relation: String + + """ + Apply scopes to the underlying query. + """ + scopes: [String!] +) on FIELD_DEFINITION +``` + +This can be a useful optimization for fields that are not returned directly +but rather used for resolving other fields. + +If you just want to return the relation itself as-is, +look into [handling Eloquent relationships](../eloquent/relationships.md). diff --git a/docs/4.10/api-reference/events.md b/docs/4.10/api-reference/events.md new file mode 100644 index 0000000000..b5f1be7164 --- /dev/null +++ b/docs/4.10/api-reference/events.md @@ -0,0 +1,304 @@ +# Events + +This reference lists the events that Lighthouse dispatches during a request in order +of execution. + +All events reside in the namespace `\Nuwave\Lighthouse\Events`. + +## StartRequest + +```php +request = $request; + $this->moment = Carbon::now(); + } +} +``` + +## BuildSchemaString + +```php +userSchema = $userSchema; + } +} +``` + +## ManipulateAST + +```php +documentAST = $documentAST; + } +} +``` + +## RegisterDirectiveNamespaces + +```php +moment = Carbon::now(); + } +} +``` + +## BuildExtensionsResponse + +```php +key = $key; + $this->content = $content; + } + + /** + * Return the key of the extension. + * + * @return string + */ + public function key(): string + { + return $this->key; + } + + /** + * Return the JSON-encodable content of the extension. + * + * @return mixed + */ + public function content() + { + return $this->content; + } +} +``` + +## ManipulateResult + +```php +result = $result; + } +} +``` diff --git a/docs/4.10/api-reference/resolvers.md b/docs/4.10/api-reference/resolvers.md new file mode 100644 index 0000000000..3b5d4a14b8 --- /dev/null +++ b/docs/4.10/api-reference/resolvers.md @@ -0,0 +1,41 @@ +# Resolvers + +## Resolver function signature + +Resolvers are always called with the same 4 arguments: + +```php + 'Bob']` +3. `GraphQLContext $context`: Arbitrary data that is shared between all fields of a single query. +Lighthouse passes in an instance of `Nuwave\Lighthouse\Schema\Context` by default. +4. `ResolveInfo $resolveInfo`: Information about the query itself, +such as the execution state, the field name, path to the field from the root, and more. + +The return value of this must fit the return type defined for the corresponding field from the schema. + +## Complexity function signature + +The complexity function is used to calculate a query complexity score for a field. +You can define your own complexity function with the [@complexity](../api-reference/directives.md#complexity) directive. + +```php +function (int $childrenComplexity, array $args): int +``` + +1. `$childrenComplexity`: The complexity of the children of the field. In case you expect to return +multiple children, it can be useful to do some maths on this. +2. `array $args`: The arguments that were passed into the field. +For example, for a field call like `user(name: "Bob")` it would be `['name' => 'Bob']` + +Read more about query complexity in the [webonyx/graphql-php docs](http://webonyx.github.io/graphql-php/security/#query-complexity-analysis) diff --git a/docs/4.10/api-reference/scalars.md b/docs/4.10/api-reference/scalars.md new file mode 100644 index 0000000000..e2408fb321 --- /dev/null +++ b/docs/4.10/api-reference/scalars.md @@ -0,0 +1,46 @@ +# Scalars + +You can use Lighthouse's built-in scalars by defining them in your schema, +using [`@scalar`](directives.md#scalar) to point them to a FQCN. + +```graphql +"A datetime string with format `Y-m-d H:i:s`, e.g. `2018-01-01 13:00:00`." +scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime") + +type Query { + "Get the local server time." + now: DateTime! +} +``` + +## Date + +```graphql +"A date string with format `Y-m-d`, e.g. `2011-05-23`." +scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date") +``` + +Internally represented as an instance of `Carbon\Carbon`. + +## DateTime + +```graphql +"A datetime string with format `Y-m-d H:i:s`, e.g. `2018-01-01 13:00:00`." +scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime") +``` + +Internally represented as an instance of `Carbon\Carbon`. + +## Upload + +```graphql +"Can be used as an argument to upload files using https://github.com/jaydenseric/graphql-multipart-request-spec" +scalar Upload @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Upload") +``` + +This Scalar can only be used as an argument, not as a return type. +For more information, please refer to the [file uploads guide](../digging-deeper/file-uploads.md). + +The multipart form request is handled by Lighthouse, the resolver gets passed +an instance of [`\Illuminate\Http\UploadedFile`](https://laravel.com/api/5.8/Illuminate/Http/UploadedFile.html) +in the argument `array $variables`. diff --git a/docs/4.10/concepts/arg-resolvers.md b/docs/4.10/concepts/arg-resolvers.md new file mode 100644 index 0000000000..069a21ff8f --- /dev/null +++ b/docs/4.10/concepts/arg-resolvers.md @@ -0,0 +1,221 @@ +# Arg Resolvers + +To understand the concept behind arg resolvers, you should familiarize yourself with +[how field resolvers are composed](https://graphql.org/learn/execution/). + +## Motivation + +Arg resolvers are an extension of the ideas behind GraphQL field execution, +applied to input arguments. Since GraphQL queries can be used to fetch complex +and deeply nested data from the client, it is natural to assume that such complex +data can also be passed as the input arguments to a query. + +GraphQL's execution engine allows you to write small and focused field resolver functions +that only care about returning the data that it is immediately responsible for. +That makes the code much simpler and avoids duplication. + +However, a single field resolver still has to take care of all the input arguments that +are passed to it. Handling complex input data in a single function is hard because of their +dynamic nature. The input given by a client might be nested arbitrarily deep +and come in many different variations. + +The following example shows an example mutation that is actually composed out of multiple +distinct operations. + +```graphql +type Mutation { + createTask(input: CreateTaskInput): Task! +} + +input CreateTaskInput { + name: String! + notes: [CreateNoteInput!] +} + +input CreateNoteInput { + content: String! + link: String +} +``` + +In a single request, we can pass all data relating to a task, +including related entities such as notes. + +```graphql +mutation CreateTaskWithNotes { + createTask( + id: 45 + name: "Do something" + notes: [ + { + content: "Foo bar", + link: "http://foo.bar" + }, + { + content: "Awesome note" + } + ] + ) { + id + } +} +``` + +We might resolve that mutation by writing a resolver function that handles all input at once. + +```php +function createTaskWithNotes($root, array $args): \App\Models\Task { + // Pull and remove notes from the args array + $notes = \Illuminate\Support\Arr::pull($args, 'notes'); + + // Create the new task with the remaining args + $task = \App\Models\Task::create($args); + + // If the client actually passed notes, create and attach them + if($notes) { + foreach($notes as $note) { + $task->notes()->create($note); + } + } + + return $task; +} +``` + +In this contrived example, the function is still pretty simple. However, separation of concerns +is already violated: A single function is responsible for creating both tasks and notes. + +We might want to extend our schema to support more operations in the future, such as updating +a task and creating, updating or deleting notes or other, more deeply nested relations. +Such changes would force us to duplicate code and increase the complexity of our single function. + +## Solution + +Ideally, we would want to write small and focused functions that each deal with just +a part of the given input arguments. The execution engine should traverse the given +input and take care of calling the appropriate functions with their respective arguments. + +```php +function createTask($root, array $args): \App\Models\Task { + return \App\Models\Task::create($args); +} + +function createTaskNotes(\App\Models\Task $root, array $args): void { + foreach($args as $note) { + $root->notes()->create($note); + } +} +``` + +Lighthouse allows you to attach resolver functions to arguments. +Complex inputs are automatically split into smaller pieces and passed off to the responsible function. + +As Lighthouse uses the SDL as the primary building block, arg resolvers are implemented as directives. +Here is how we can define a schema that enables sending a nested mutation as in the example above. + +```diff +type Mutation { +- createTask(input: CreateTaskInput): Task! ++ createTask(input: CreateTaskInput): Task! @create +} + +input CreateTaskInput { + name: String! +- notes: [CreateNoteInput!] ++ notes: [CreateNoteInput!] @create +} + +input CreateNoteInput { + content: String! + link: String +} +``` + +The `@create` directive will behave differently, based on the context where it is used. + +On the `createTask` field, it will create a `Task` model with the given `name`, save it +to the database and return that instance to Lighthouse. + +A simplified, generic implementation of an appropriate field resolver would look something like this: + +```php +setResolver( + function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Model { + // Wrap the operation and let Lighthouse take care of splitting the input + $nestedSave = new ResolveNested(function($model, $args) { + $model->fill($args->toArray()); + $model->save(); + }); + + $modelClass = $this->getModelClass(); + /** @var \Illuminate\Database\Eloquent\Model $model */ + $model = new $modelClass; + + return $nestedSave($model, $resolveInfo->argumentSet); + } + ); + } +} +``` + +The arguments that are nested within `notes` will be handled as a nested argument resolver. +For each `CreateNoteInput`, the resolver will be called with the previously created `Task` +and create and attach a related `Note` model. + +We can extend our previous implementation of `@create` by allowing it to be used as an `ArgResolver`: + +```php +getRelationName(); + + /** @var \Illuminate\Database\Eloquent\Relations\Relation $relation */ + $relation = $parent->{$relationName}(); + $related = $relation->make(); + + return array_map( + function ($args) use ($related) { + $related->fill($args->toArray()); + $related->save(); + }, + $argsList + ); + } +} +``` + +You may define your own nested arg resolver directives by implementing [`ArgResolver`](../custom-directives/argument-directives.md#argresolver). diff --git a/docs/4.10/custom-directives/argument-directives.md b/docs/4.10/custom-directives/argument-directives.md new file mode 100644 index 0000000000..6ec34bf429 --- /dev/null +++ b/docs/4.10/custom-directives/argument-directives.md @@ -0,0 +1,277 @@ +# Argument Directives + +Argument directives can be applied to a [InputValueDefinition](https://graphql.github.io/graphql-spec/June2018/#InputValueDefinition). + +As arguments may be contained within a list in the schema definition, you must specify +what your argument should apply to in addition to its function. + +- If it applies to the individual items within the list, + implement the [`\Nuwave\Lighthouse\Support\Contracts\ArgDirective`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/ArgDirective.php) interface. +- Else, if it should apply to the whole list, + implement the [`\Nuwave\Lighthouse\Support\Contracts\ArgDirectiveForArray`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/ArgDirectiveForArray.php) interface. + +You must implement exactly one of those two interfaces in order for an argument directive to work. + +## ArgTransformerDirective + +An [`\Nuwave\Lighthouse\Support\Contracts\ArgTransformerDirective`](https://github.com/nuwave/lighthouse/blob/master/src/Support/Contracts/ArgTransformerDirective.php) +takes an incoming value an returns a new value. + +Let's take a look at the built-in `@trim` directive. + +```php + $args['name'] + ]); + } +} +``` + +### Evaluation Order + +Argument directives are evaluated in the order that they are defined in the schema. + +```graphql +type Mutation { + createUser( + password: String @trim @rules(apply: ["min:10,max:20"]) @hash + ): User +} +``` + +In the given example, Lighthouse will take the value of the `password` argument and: +1. Trim any whitespace +1. Run validation on it +1. Encrypt the password via `bcrypt` + +## ArgBuilderDirective + +An [`\Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective`](https://github.com/nuwave/lighthouse/blob/master/src/Support/Contracts/ArgBuilderDirective.php) +directive allows using arguments passed by the client to dynamically +modify the database query that Lighthouse creates for a field. + +Currently, the following directives use the defined filters for resolving the query: + +- `@all` +- `@paginate` +- `@find` +- `@first` +- `@hasMany` `@hasOne` `@belongsTo` `@belongsToMany` + +Take the following schema as an example: + +```graphql +type User { + posts(category: String @eq): [Post!]! @hasMany +} +``` + +Passing the `category` argument will select only the user's posts +where the `category` column is equal to the value of the `category` argument. + +So let's take a look at the built-in `@eq` directive. + +```php +where( + $this->directiveArgValue('key', $this->nodeName()), + $value + ); + } +} +``` + +The `handleBuilder` method takes two arguments: + +- `$builder` +The query builder for applying the additional query on to. +- `$value` +The value of the argument value that the `@eq` was applied on to. + +If you want to use a more complex value for manipulating a query, +you can build a `ArgBuilderDirective` to work with lists or nested input objects. +Lighthouse's [`@whereBetween`](../api-reference/directives.md#wherebetween) is one example of this. + +```graphql +type Query { + users( + createdBetween: DateRange @whereBetween(key: "created_at") + ): [User!]! @paginate +} + +input DateRange { + from: Date! + to: Date! +} +``` + +## ArgResolver + +An [`\Nuwave\Lighthouse\Support\Contracts\ArgResolver`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/ArgResolver.php) +directive allows you to compose resolvers for complex nested inputs, similar to the way +that field resolvers are composed together. + +For an in-depth explanation of the concept of composing arg resolvers, +read the [explanation of arg resolvers](../concepts/arg-resolvers.md). + +## ArgManipulator + +An [`\Nuwave\Lighthouse\Support\Contracts\ArgManipulator`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/ArgManipulator.php) +directive can be used to manipulate the schema AST. + +For example, you might want to add a directive that automagically derives the arguments +for a field based on an object type. A skeleton for this directive might look something like this: + +```php +directiveArgValue('name'); + $type = $documentAST->types[$typeName]; + + $input = $this->generateInputFromType($type); + $argDefinition->name->value = $input->value->name; + + $documentAST->setTypeDefinition($input); + } + + protected function generateInputFromType(ObjectTypeDefinitionNode $type): InputObjectTypeDefinitionNode + { + // TODO generate this type based on rules and conventions that work for you + } +} +``` diff --git a/docs/4.10/custom-directives/field-directives.md b/docs/4.10/custom-directives/field-directives.md new file mode 100644 index 0000000000..054cfe490c --- /dev/null +++ b/docs/4.10/custom-directives/field-directives.md @@ -0,0 +1,30 @@ +# Field Directives + +Field directives can be applied to any [FieldDefinition](https://graphql.github.io/graphql-spec/June2018/#FieldDefinition) + +## FieldResolver + +Perhaps the most important directive interface, a [`\Nuwave\Lighthouse\Support\Contracts\FieldResolver`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldResolver.php) +let's you add a resolver for a field through a directive. + +It can be a great way to reuse resolver logic within a schema. + +## FieldMiddleware + +A [`\Nuwave\Lighthouse\Support\Contracts\FieldMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldMiddleware.php) directive allows you +to wrap around the field resolver, just like [Laravel Middleware](https://laravel.com/docs/middleware). + +You may use it both to handle incoming values before reaching the final resolver +as well as the outgoing result of resolving the field. + +## FieldManipulator + +An [`\Nuwave\Lighthouse\Support\Contracts\FieldManipulator`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/FieldManipulator.php) +directive can be used to manipulate the schema AST. + +## ValidationDirective + +This directive type is implemented as an abstract class rather then a pure interface and allows +you to define complex validation rules for a field with ease. + +[Read more about it in the Validation section](../security/validation.md#validate-fields). diff --git a/docs/4.10/custom-directives/getting-started.md b/docs/4.10/custom-directives/getting-started.md new file mode 100644 index 0000000000..ab42ae216e --- /dev/null +++ b/docs/4.10/custom-directives/getting-started.md @@ -0,0 +1,97 @@ +# Implementing Your Own Directives + +As you grow your GraphQL schema, you may find the need for more specialized functionality. +Learn how you can abstract logic in a composable and reusable manner by using custom directives. + +## Naming Conventions + +Directives are implemented as PHP classes, each directive available +in the schema corresponds to a single class. + +Directive names themselves are typically defined in **camelCase**. +The class name of a directive must follow the following pattern: + + Directive + +Let's implement a simple `@upperCase` directive as a part of this introduction. +We will put it in a class called `UpperCaseDirective` and extend the +abstract class `\Nuwave\Lighthouse\Schema\Directives\BaseDirective`. + +```php +getResolver(); + + // Wrap around the resolver + $wrappedResolver = function ($root, array $args, GraphQLContext $context, ResolveInfo $info) use ($previousResolver): string { + // Call the resolver, passing along the resolver arguments + /** @var string $result */ + $result = $previousResolver($root, $args, $context, $info); + + return strtoupper($result); + }; + + // Place the wrapped resolver back upon the FieldValue + // It is not resolved right now - we just prepare it + $fieldValue->setResolver($wrappedResolver); + + // Keep the middleware chain going + return $next($fieldValue); + } +} +``` + +## Register Directives + +Now that we defined and implemented the directive, how can Lighthouse find it? + +When Lighthouse encounters a directive within the schema, it starts looking for a matching class +in the following order: + +1. User-defined namespaces as configured in `config/lighthouse.php`, defaults to `App\GraphQL\Directives` +1. The [RegisterDirectiveNamespaces](../api-reference/events.md#registerdirectivenamespaces) event is dispatched + to gather namespaces defined by plugins, extensions or other listeners +1. Lighthouse's built-in directive namespace + +This means that our directive is already registered, just by matter of defining it in the default namespace, +and will take precedence over potential other directives with the same name. diff --git a/docs/4.10/custom-directives/type-directives.md b/docs/4.10/custom-directives/type-directives.md new file mode 100644 index 0000000000..cae77daef3 --- /dev/null +++ b/docs/4.10/custom-directives/type-directives.md @@ -0,0 +1,29 @@ +# Type Directives + +These directives can generally be applied to [type definitions](../the-basics/types.md) in the schema. + +> This is not limited to `type` but also includes `input`, `enum`, `union`, `interface` and `scalar` types. + +## TypeManipulator + +The [`\Nuwave\Lighthouse\Support\Contracts\TypeManipulator`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/TypeManipulator.php) +interface can be used to manipulate the AST from a type definition node. + +## TypeMiddleware + +The [`\Nuwave\Lighthouse\Support\Contracts\TypeMiddleware`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/TypeMiddleware.php) +interface allows access to an AST node as it is converted to an executable type. + +## TypeResolver + +The [`\Nuwave\Lighthouse\Support\Contracts\TypeResolver`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/TypeResolver.php) +interface can be used for custom conversion from AST values to an executable type. + +## Type Extension Directives + +These directives can generally be applied to [type extensions](https://graphql.github.io/graphql-spec/June2018/#sec-Type-Extensions) in the schema. + +## TypeExtensionManipulator + +The [`\Nuwave\Lighthouse\Support\Contracts\TypeExtensionManipulator`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/TypeExtensionManipulator.php) +interface can be used to manipulate the AST from a type extension node. diff --git a/docs/4.10/digging-deeper/adding-types-programmatically.md b/docs/4.10/digging-deeper/adding-types-programmatically.md new file mode 100644 index 0000000000..7fe04f36fb --- /dev/null +++ b/docs/4.10/digging-deeper/adding-types-programmatically.md @@ -0,0 +1,93 @@ +# Adding Types Programmatically + +You might want to add additional types to the schema programmatically. + +## Additional Schema Definitions + +If you want to use the SDL to define additional types dynamically, +you can listen for the [`BuildSchemaString`](../api-reference/events.md#buildschemastring) +event and return additional schema definitions as a string: + +```php +app('events')->listen( + \Nuwave\Lighthouse\Events\BuildSchemaString::class, + function(): string { + // You can get your schema from anywhere you want, e.g. a database, hardcoded + } +); +``` + +When your schema is defined within files and you want to use `#import` to combine them, +you can use the `\Nuwave\Lighthouse\Schema\Source\SchemaStitcher` to load your file: + +```php +$stitcher = new \Nuwave\Lighthouse\Schema\Source\SchemaStitcher(__DIR__ . '/path/to/schema.graphql'); +return $stitcher->getSchemaString(); +``` + +## Native PHP types + +While Lighthouse is an SDL-first GraphQL server, you can also use native PHP type definitions. + +Check out the [webonyx/graphql-php documentation](http://webonyx.github.io/graphql-php/type-system/) +on how to define types. + +Note that you will not have access to a large portion of Lighthouse functionality +that is provided through server-side directives and the definition is much more verbose. + +Because of this, we do not recommend you use native PHP types for complex object types. + +However, it can be advantageous to use native types for two use cases: +- [Enum types](http://webonyx.github.io/graphql-php/type-system/enum-types/): + Allows you to reuse existing constants in your code +- [Custom Scalar types](http://webonyx.github.io/graphql-php/type-system/scalar-types/#writing-custom-scalar-types). + They will have to be implemented in PHP anyway + +## Using the TypeRegistry + +Lighthouse provides a type registry out of the box for you to register your types. +You can get an instance of it through the Laravel Container. + +```php +register( + new ObjectType([ + 'name' => 'User', + 'fields' => function() use ($typeRegistry): array { + return [ + 'email' => [ + 'type' => Type::string() + ], + 'friends' => [ + 'type' => Type::listOf( + $typeRegistry->get('User') + ) + ] + ]; + } + ]) + ); + } +} +``` diff --git a/docs/4.10/digging-deeper/client-directives.md b/docs/4.10/digging-deeper/client-directives.md new file mode 100644 index 0000000000..59c79535d4 --- /dev/null +++ b/docs/4.10/digging-deeper/client-directives.md @@ -0,0 +1,125 @@ +# Client Directives + +Client directives allow clients to change the behaviour of query execution. + +> Client directives must not be used within your schema definition. + +The [GraphQL specification](https://graphql.github.io/graphql-spec/June2018/#sec-Type-System.Directives) +mentions two client directives: [`@skip`](#skip) and [`@include`](#include). +Both are built-in to Lighthouse and work out-of-the-box. + +## @skip + +This directive is part of the [GraphQL spec](https://graphql.github.io/graphql-spec/June2018/#sec--include) +and is built-in to Lighthouse. + +The `@skip` directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional +exclusion during execution as described by the `if` argument. + +```graphql +directive @skip( + """ + If the value passed into the if field is true the field this + is decorating will not be included in the query response. + """ + if: Boolean! +) +on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +``` + +In this example experimentalField will only be queried if the variable $someTest has the value `false`. + +```graphql +query myQuery($someTest: Boolean) { + experimentalField @skip(if: $someTest) +} +``` + +## @include + +This directive is part of the [GraphQL spec](https://graphql.github.io/graphql-spec/June2018/#sec--include) +and is built-in to Lighthouse. + +The `@include` directive may be provided for fields, fragment spreads, and inline fragments, +and allows for conditional inclusion during execution as described by the `if` argument. + +```graphql +directive @include( + """ + If the "if" value is true the field this is connected with will be included in the query response. + Otherwise it will not. + """ + if: Boolean +) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +``` + +In this example experimentalField will only be queried if the variable $someTest has the value true + +```graphql +query myQuery($someTest: Boolean) { + experimentalField @include(if: $someTest) +} +``` + +## Custom Client Directives + +You can implement your own client directives. +First, add a definition of your directive to your schema. + +```graphql +"A description of what this directive does." +directive @example( + "Client directives can have arguments too!" + someArg: String +) on FIELD +``` + +By itself, a custom client directive does not do anything. +Lighthouse provides a class to retrieve information about where client directives +were placed in the query and what arguments were given to them. + +```php +$clientDirective = new \Nuwave\Lighthouse\ClientDirectives\ClientDirective('example'); +``` + +The most common use case for a client directive is to place it on a field. There is a caveat +to working with this that is unintuitive at first: There might be multiple nodes referencing a single +field, and each of those may or may not have the client directive set, with possibly different arguments. + +The following example illustrates how a field `foo` can be referenced three times with different +configurations of a client directive: + +```graphql +{ + foo + fooBar: foo @example + ... on Query { + foo @example(bar: "baz") + } +} +``` + +You can get all arguments for every node that is referencing the field you are currently +resolving, passing the fourth [resolver argument `ResolveInfo $resolveInfo`](../api-reference/resolvers.md#resolver-function-signature): + +```php +$arguments = $clientDirective->forField($resolveInfo); +``` + +The resulting `$arguments` will be an array of 1 to n values, n being the amount of nodes. +For the example query above, it will look like this: + +```php +[ + null, # No directive on the first reference + [], # Directive present, but no arguments given + ['bar' => 'baz'], # Present with arguments +] +``` + +You are then free to implement whatever logic on top of that. Some client directives may require +only one field node to have it set, whereas others might require all of them to have the same configuration. + +> There are other locations where client directives may be used on: http://spec.graphql.org/draft/#ExecutableDirectiveLocation +> You can add a PR to Lighthouse if you need them. diff --git a/docs/4.10/digging-deeper/error-handling.md b/docs/4.10/digging-deeper/error-handling.md new file mode 100644 index 0000000000..fde955acc5 --- /dev/null +++ b/docs/4.10/digging-deeper/error-handling.md @@ -0,0 +1,150 @@ +# Error Handling + +Most of the error handling in Lighthouse is pretty closely based upon **webonyx/graphql-php**, +so you can find a lot of valuable information [in their documentation](http://webonyx.github.io/graphql-php/error-handling/). + +## User-friendly Errors + +In a production setting, error messages should not be shown to the user by default +to prevent information leaking. In some cases however, you may want to display an +explicit error message to the user. + +**webonyx/graphql-php** offers the [`GraphQL\Error\ClientAware`](https://github.com/webonyx/graphql-php/blob/master/src/Error/ClientAware.php) interface, that can +be implemented by Exceptions to control how they are rendered to the client. + +Head over their [Error Handling docs](http://webonyx.github.io/graphql-php/error-handling/) to learn more. + +## Additional Error Information + +The interface [`\Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions`](https://github.com/nuwave/lighthouse/blob/master/src/Exceptions/RendersErrorsExtensions.php) +may be extended to add more information then just an error message to the rendered error output. + +Let's say you want to have a custom exception type that contains information about +the reason why the exception was thrown. + +```php +reason = $reason; + } + + /** + * Returns true when exception message is safe to be displayed to a client. + * + * @api + * @return bool + */ + public function isClientSafe(): bool + { + return true; + } + + /** + * Returns string describing a category of the error. + * + * Value "graphql" is reserved for errors produced by query parsing or validation, do not use it. + * + * @api + * @return string + */ + public function getCategory(): string + { + return 'custom'; + } + + /** + * Return the content that is put in the "extensions" part + * of the returned error. + * + * @return array + */ + public function extensionsContent(): array + { + return [ + 'some' => 'additional information', + 'reason' => $this->reason, + ]; + } +} +``` + +Now you can just throw that Exception somewhere in your code, for example your resolver, +and it will display additional error output. + +```php +errorConditionIsMet()) { + throw new CustomException( + 'This is the error message', + 'The reason why this error was thrown, is rendered in the extension output.' + ); + } + + return 'Success!'; + } +} +``` + +A query that produces an error will render like this: + +```json +{ + "data": null, + "errors": [ + { + "message": "This is the error message", + "extensions": { + "category": "custom", + "some": "additional information", + "reason": "The reason why this error was thrown, is rendered in the extension output." + } + } + ] +} +``` + +## Registering Error Handlers + +You can use the config to register error handlers that receive the Errors that occur during execution +and handle them. You may use this to log, filter or format the errors. +The classes must implement [`\Nuwave\Lighthouse\Execution\ErrorHandler`](https://github.com/nuwave/lighthouse/blob/master/src/Execution/ErrorHandler.php) + +## Collecting Errors + +As a GraphQL query may return a partial result, you may not always want to abort +execution immediately after an error occurred. You can use the [`\Nuwave\Lighthouse\Execution\ErrorBuffer`](https://github.com/nuwave/lighthouse/blob/master/src/Execution/ErrorBuffer.php) +when you want to collect multiple errors before returning a result. diff --git a/docs/4.10/digging-deeper/extending-lighthouse.md b/docs/4.10/digging-deeper/extending-lighthouse.md new file mode 100644 index 0000000000..0b9b652e1c --- /dev/null +++ b/docs/4.10/digging-deeper/extending-lighthouse.md @@ -0,0 +1,138 @@ +# Extending Lighthouse + +Lighthouse offers various extension points which can be utilized by package developers +as well as end users. + +## The Event System + +Lighthouse offers a unified way of hooking into the complete execution lifecycle +through [Laravel's event system](https://laravel.com/docs/events). +You may use any Service Provider to register listeners. + +You can find a complete list of all dispatched events [in the events API reference](../api-reference/events.md). + +## Adding Directives + +You can add your custom directives to Lighthouse by listening for the +[`RegisterDirectiveNamespaces`](../api-reference/events.md#registerdirectivenamespaces) event. + +Check out [the test suite](https://github.com/nuwave/lighthouse/tree/master/tests/Integration/Events/RegisterDirectiveNamespacesTest.php) +for an example of how this works. + +## Changing the default resolver + +The first priority when looking for a resolver is always given to `FieldResolver` directives. + +After that, Lighthouse attempts to find a default resolver. + +The interface [`\Nuwave\Lighthouse\Support\Contracts\ProvidesResolver`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/ProvidesResolver.php) +is expected to provide a resolver in case no resolver directive is defined for a field. + +If the field is defined on the root `Query` or `Mutation` types, +Lighthouse's default implementation looks for a class with the capitalized name +of the field in the configured default location and calls its `__invoke` method. + +Non-root fields fall back to [webonyx's default resolver](http://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver). +You may overwrite this by passing a `callable` to `\GraphQL\Executor\Executor::setDefaultFieldResolver`. + +When the field is defined on the root `Subscription` type, the [`\Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver`](https://github.com/nuwave/lighthouse/tree/master/src/Support/Contracts/ProvidesSubscriptionResolver.php) +interface is used instead. + +## Use a custom `GraphQLContext` + +The context is the third argument of any resolver function. + +You may replace the default `\Nuwave\Lighthouse\Schema\Context` with your own +implementation of the interface `Nuwave\Lighthouse\Support\Contracts\GraphQLContext`. +The following example is just a starting point of what you can do: + +```php +request = $request; + } + + /** + * Get instance of request. + * + * @return \Illuminate\Http\Request + */ + public function request(): Request + { + return $this->request; + } + + /** + * Get instance of authenticated user. + * + * May be null since some fields may be accessible without authentication. + * + * @return \Illuminate\Contracts\Auth\Authenticatable|null + */ + public function user() + { + // TODO implement yourself + } +} +``` + +You need a factory that creates an instance of `\Nuwave\Lighthouse\Support\Contracts\GraphQLContext`. +This factory class needs to implement `\Nuwave\Lighthouse\Support\Contracts\CreatesContext`. + +```php +app->bind( + \Nuwave\Lighthouse\Support\Contracts\CreatesContext::class, + \App\MyContextFactory::class + ); +} +``` diff --git a/docs/4.10/digging-deeper/file-uploads.md b/docs/4.10/digging-deeper/file-uploads.md new file mode 100644 index 0000000000..d0aac617fb --- /dev/null +++ b/docs/4.10/digging-deeper/file-uploads.md @@ -0,0 +1,82 @@ +# Uploading files + +Lighthouse allows you to upload files using a multipart form request +as defined in [graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec). + +## Setup + +In order to accept file uploads, you must add the `Upload` scalar to your schema. + +```graphql +"Can be used as an argument to upload files using https://github.com/jaydenseric/graphql-multipart-request-spec" +scalar Upload @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Upload") +``` + +Once the scalar is added, you can add it to a mutation. + +```graphql +type Mutation { + "Upload a file that is publicly available." + upload(file: Upload!): String +} +``` + +## Handling file uploads + +Lighthouse accepts multipart form requests that contain file uploads. +The given file is injected into the `array $variables` as an instance of [`\Illuminate\Http\UploadedFile`](https://laravel.com/api/5.8/Illuminate/Http/UploadedFile.html) +and passed into the resolver. + +It is up to you how to handle the given file in the resolver, +see the [Laravel docs for File Uploads](https://laravel.com/docs/filesystem#file-uploads). + +The field from the previous example can be implemented like this: + +```php +storePublicly('uploads'); + } +} +``` + +## Client-side Usage + +In order to upload a file, you must send a `multipart/form-data` request. +Use any of the [available client implementations](https://github.com/jaydenseric/graphql-multipart-request-spec#client) +or look at the [specification examples](https://github.com/jaydenseric/graphql-multipart-request-spec#multipart-form-field-structure) to roll your own. + +To test the example above, prepare a file you can upload. + +```bash +echo "test content" > my_file.txt +``` + +Then, send a request to upload the file to your server: + +```bash +curl localhost/graphql \ + -F operations='{ "query": "mutation ($file: Upload!) { upload(file: $file) }", "variables": { "file": null } }' \ + -F map='{ "0": ["variables.file"] }' \ + -F 0=@my_file.txt +``` + +## Testing + +Refer to [testing file uploads in PHPUnit](../testing/phpunit.md#simulating-file-uploads). diff --git a/docs/4.10/digging-deeper/relay.md b/docs/4.10/digging-deeper/relay.md new file mode 100644 index 0000000000..847bb01f1c --- /dev/null +++ b/docs/4.10/digging-deeper/relay.md @@ -0,0 +1,51 @@ +# Relay + +## Cursor Connection + +Relay requires a particular kind of pagination which is the [Cursor Connection](https://facebook.github.io/relay/graphql/connections.htm) +To get a relay-compatible connection on a root query field, use the [@paginate](../api-reference/directives.md#paginate) +directive with the pagination type `connection`. + +> Lighthouse does not support actual cursor-based pagination as of now, see https://github.com/nuwave/lighthouse/issues/311 for details. +> Under the hood, the "cursor" is decoded into a page offset. + +```graphql +type Query { + users: [User!]! @paginate(type: "connection") +} +``` + +This automatically converts the type definition into a relay connection and constructs +the appropriate queries via the underlying Eloquent model. + +Connections can also be used for sub-fields of a type, given they are defined as a HasMany-Relationship +in Eloquent. Use the [@hasMany](../api-reference/directives.md#hasmany) directive. + +```graphql +type User { + name: String + posts: [Post!]! @hasMany(type: "connection") +} +``` + +## Global Object Identification + +You may rebind the `\Nuwave\Lighthouse\Support\Contracts\GlobalId` interface to add your +own mechanism of encoding/decoding global ids. + +[Global Object Identification](https://facebook.github.io/relay/graphql/objectidentification.htm) + +[@node](../api-reference/directives.md#node) + +[@globalId](../api-reference/directives.md#globalid) + +## Input Object Mutations + +Lighthouse makes it easy to follow the principle of using a +single field argument called `input`, just use the [`@spread`](../api-reference/directives.md#spread) directive. + +```graphql +type Mutation { + introduceShip(input: IntroduceShipInput! @spread): IntroduceShipPayload! +} +``` diff --git a/docs/4.10/digging-deeper/schema-organisation.md b/docs/4.10/digging-deeper/schema-organisation.md new file mode 100644 index 0000000000..2fdc933466 --- /dev/null +++ b/docs/4.10/digging-deeper/schema-organisation.md @@ -0,0 +1,127 @@ +# Schema Organisation + +As you add more and more types to your schema, it can grow quite large. +Learn how to split your schema across multiple files and organise your types. + +## Schema Imports + +Suppose you created your schema files likes this: + +``` +graphql/ +|-- schema.graphql +|-- user.graphql +``` + +Lighthouse reads your schema from a single entrypoint, in this case `schema.graphql`. +You can import other schema files from there to split up your schema into multiple files. + +```graphql +type Query { + user: User +} + +#import user.graphql +``` + +Imports always begin on a separate line with `#import `, followed by the relative path +to the imported file. The contents of `user.graphql` are pasted in the final schema. + +```graphql +type Query { + user: User +} + +type User { + name: String! +} +``` + +The import statements are followed recursively, so it is easy to organize even the most complex of schemas. + +You can also import multiple files using wildcard import syntax. +For example, if you have your schema files like this: +``` +graphql/ + |-- schema.graphql + |-- post/ + |-- post.graphql + |-- category.graphql +``` + +Instead of naming each individual file, you can import multiple files that matches a pattern. +It will be loaded using PHP's [glob function](http://php.net/manual/function.glob.php). + +```graphql +#import post/*.graphql +``` + +## Type Extensions + +Suppose you want to add a new type `Post` to your schema. +Create a new file `post.graphql` with the schema for that type. + +```graphql +type Post { + title: String + author: User @belongsTo +} +``` + +Then you add an import to your main schema file. + +```graphql +#import post.graphql + +type Query { + me: User @auth +} +``` + +Now you want to add a few queries to actually fetch posts. You could add them to the main `Query` type +in your main file, but that spreads the definition apart, and could also grow quite large over time. + +Another way would be to extend the `Query` type and colocate the type definition with its Queries in `post.graphql`. + +```graphql +type Post { + title: String + author: User @belongsTo +} + +extend type Query { + posts: [Post!]! @paginate +} +``` + +The fields in the `extend type` definition are merged with those of the original type. + +### Root Definitions + +A valid `Query` type definition with at least one field must be present in the root schema. +This is because `extend type` needs the original type to get merged into. + +You can provide an empty `Query` type (without curly braces) in the root schema: + +```graphql +type Query + +#import post.graphql +``` + +The same applies for mutations: if you want to use them, you can define +an empty `Mutation` type (without curly braces) within your root schema: + +```graphql +type Query + +type Mutation + +#import post.graphql +``` + +### Extending other types + +Apart from object types, you can also extend `input`, `interface` and `enum` types. +Lighthouse will merge the fields (or values) with the original definition and always +produce a single type in the final schema. diff --git a/docs/4.10/eloquent/complex-where-conditions.md b/docs/4.10/eloquent/complex-where-conditions.md new file mode 100644 index 0000000000..bc7e8a0c06 --- /dev/null +++ b/docs/4.10/eloquent/complex-where-conditions.md @@ -0,0 +1,373 @@ +# Complex Where Conditions + +Adding query conditions ad-hoc can be cumbersome and limiting when you require +many different ways to filter query results. +Lighthouse's `WhereConditions` extension can give advanced query capabilities to clients +and allow them to apply complex, dynamic WHERE conditions to queries. + +## Setup + +**This is an experimental feature and not included in Lighthouse by default.** + +Add the service provider to your `config/app.php` + +```php +'providers' => [ + \Nuwave\Lighthouse\WhereConditions\WhereConditionsServiceProvider::class, +], +``` + +Install the dependency [mll-lab/graphql-php-scalars](https://github.com/mll-lab/graphql-php-scalars): + + composer require mll-lab/graphql-php-scalars + +## Usage + +You can use this feature through a set of schema directives that enhance fields +with advanced filter capabilities. + +### @whereConditions + +```graphql +""" +Add a dynamically client-controlled WHERE condition to a fields query. +""" +directive @whereConditions( + """ + Restrict the allowed column names to a well-defined list. + This improves introspection capabilities and security. + Mutually exclusive with the `columnsEnum` argument. + """ + columns: [String!] + + """ + Use an existing enumeration type to restrict the allowed columns to a predefined list. + This allowes you to re-use the same enum for multiple fields. + Mutually exclusive with the `columns` argument. + """ + columnsEnum: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +You can apply this directive on any field that performs an Eloquent query: + +```graphql +type Query { + people( + where: _ @whereConditions(columns: ["age", "type", "haircolour", "height"]) + ): [Person!]! @all +} + +type Person { + id: ID! + age: Int! + height: Int! + type: String! + hair_colour: String! +} +``` + +Lighthouse automatically generates definitions for an `Enum` type and an `Input` type +that are restricted to the defined columns, so you do not have to specify them by hand. +The blank type named `_` will be changed to the actual type. +Here are the types that will be included in the compiled schema: + +```graphql +"Dynamic WHERE conditions for the `where` argument on the query `people`." +input PeopleWhereWhereConditions { + "The column that is used for the condition." + column: PeopleWhereColumn + + "The operator that is used for the condition." + operator: SQLOperator = EQ + + "The value that is used for the condition." + value: Mixed + + "A set of conditions that requires all conditions to match." + AND: [PeopleWhereWhereConditions!] + + "A set of conditions that requires at least one condition to match." + OR: [PeopleWhereWhereConditions!] +} + +"Allowed column names for the `where` argument on the query `people`." +enum PeopleWhereColumn { + AGE @enum(value: "age") + TYPE @enum(value: "type") + HAIRCOLOUR @enum(value: "haircolour") + HEIGHT @enum(value: "height") +} +``` + +Alternatively to the `columns` argument, you can also use `columnsEnum` in case you +want to re-use a list of allowed columns. Here's how your schema could look like: + +```graphql +type Query { + allPeople( + where: _ @whereConditions(columnsEnum: "PersonColumn") + ): [Person!]! @all + + paginatedPeople( + where: _ @whereConditions(columnsEnum: "PersonColumn") + ): [Person!]! @paginated +} + +"A custom description for this custom enum." +enum PersonColumn { + AGE @enum(value: "age") + TYPE @enum(value: "type") + HAIRCOLOUR @enum(value: "haircolour") + HEIGHT @enum(value: "height") +} +``` + +Lighthouse will still automatically generate the necessary input types. +But instead of creating enums for the allowed columns, it will simply use the existing `PersonColumn` enum. + +It is recommended to either use the `columns` or the `columnsEnum` argument. +When you don't define any allowed columns, clients can specify arbitrary column names as a `String`. +This approach should by taken with care, as it carries +potential performance and security risks and offers little type safety. + +A simple query for a person who is exactly 42 years old would look like this: + +```graphql +{ + people( + where: { column: AGE, operator: EQ, value: 42 } + ) { + name + } +} +``` + +Note that the operator defaults to `EQ` (`=`) if not given, so you could +also omit it from the previous example and get the same result. + +The following query gets actors over age 37 who either have red hair or are at least 150cm: + +```graphql +{ + people( + where: { + AND: [ + { column: AGE, operator: GT, value: 37 } + { column: TYPE, value: "Actor" } + { + OR: [ + { column: HAIRCOLOUR, value: "red" } + { column: HEIGHT, operator: GTE, value: 150 } + ] + } + ] + } + ) { + name + } +} +``` + +Some operators require passing lists of values - or no value at all. The following +query gets people that have no hair and blue-ish eyes: + +```graphql +{ + people( + where: { + AND: [ + { column: HAIRCOLOUR, operator: IS_NULL } + { column: EYES, operator: IN, value: ["blue", "aqua", "turquoise"] } + ] + } + ) { + name + } +} +``` + +Using `null` as argument value does not have any effect on the query. +This query would retrieve all persons without any condition: + +```graphql +{ + people( + where: null + ) { + name + } +} +``` + +### @whereHasConditions + +```graphql +""" +Allows clients to filter a query based on the existence of a related model, using +a dynamically controlled `WHERE` condition that applies to the relationship. +""" +directive @whereHasConditions( + """ + The Eloquent relationship that the conditions will be applied to. + + This argument can be omitted if the argument name follows the naming + convention `has{$RELATION}`. For example, if the Eloquent relationship + is named `posts`, the argument name must be `hasPosts`. + """ + relation: String + + """ + Restrict the allowed column names to a well-defined list. + This improves introspection capabilities and security. + Mutually exclusive with the `columnsEnum` argument. + """ + columns: [String!] + + """ + Use an existing enumeration type to restrict the allowed columns to a predefined list. + This allowes you to re-use the same enum for multiple fields. + Mutually exclusive with the `columns` argument. + """ + columnsEnum: String +) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +This directive works very similar to [`@whereConditions`](#whereconditions), except that +the conditions are applied to a relation sub query: + +```graphql +type Query { + people( + hasRole: _ @whereHasConditions(columns: ["name", "access_level"]) + ): [Person!]! @all +} + +type Role { + name: String! + access_level: Int +} +``` + +Again, Lighthouse will auto-generate an `input` and `enum` definition for your query: + +```graphql +"Dynamic WHERE conditions for the `hasRole` argument on the query `people`." +input PeopleHasRoleWhereConditions { + "The column that is used for the condition." + column: PeopleHasRoleColumn + + "The operator that is used for the condition." + operator: SQLOperator = EQ + + "The value that is used for the condition." + value: Mixed + + "A set of conditions that requires all conditions to match." + AND: [PeopleHasRoleWhereConditions!] + + "A set of conditions that requires at least one condition to match." + OR: [PeopleHasRoleWhereConditions!] +} + +"Allowed column names for the `hasRole` argument on the query `people`." +enum PeopleHasRoleColumn { + NAME @enum(value: "name") + ACCESS_LEVEL @enum(value: "access_level") +} +``` + +A simple query for a person who has an access level of at least 5, through one of +their roles, looks like this: + +```graphql +{ + people( + hasRole: { column: ACCESS_LEVEL, operator: GTE, value: 5 } + ) { + name + } +} +``` + +You can also query for relationship existence without any condition; simply use an empty object as argument value. +This query would retrieve all persons that have a role: + +```graphql +{ + people( + hasRole: {} + ) { + name + } +} +``` + +Just like with the `@whereCondition` directive, using `null` as argument value does not have any effect on the query. +This query would retrieve all persons, no matter if they have a role or not: + +```graphql +{ + people( + hasRole: null + ) { + name + } +} +``` + + +## Custom operator + +If Lighthouse's default `SQLOperator` does not fit your use case, you can register a custom operator class. +This may be necessary if your database uses different SQL operators then Lighthouse's default or you +want to extend/restrict the allowed operators. + +First create a class that implements `\Nuwave\Lighthouse\WhereConditions\Operator`. For example: + +```php +namespace App\GraphQL; + +use Nuwave\Lighthouse\WhereConditions\Operator; + +class CustomSQLOperator implements Operator { ... } +``` + +An `Operator` has two responsibilities: +- provide an `enum` definition that will be used throughout the schema +- handle client input and apply the operators to the query builder + +To tell Lighthouse to use your custom operator class, you have to bind it in a service provider: + +```php +namespace App\GraphQL; + +use App\GraphQL\CustomSQLOperator; +use Illuminate\Support\ServiceProvider; +use Nuwave\Lighthouse\WhereConditions\Operator; + +class GraphQLServiceProvider extends ServiceProvider +{ + public function register(): void + { + $this->app->bind(Operator::class, CustomSQLOperator::class); + } +} +``` + +Don't forget to register your new service provider in `config/app.php`. +Make sure to add it after Lighthouse's `\Nuwave\Lighthouse\WhereConditions\WhereConditionsServiceProvider::class`: + +```diff +'providers' => [ + /* + * Package Service Providers... + */ + \Nuwave\Lighthouse\WhereConditions\WhereConditionsServiceProvider::class, + + /* + * Application Service Providers... + */ ++ \App\GraphQL\GraphQLServiceProvider::class, +], +``` diff --git a/docs/4.10/eloquent/getting-started.md b/docs/4.10/eloquent/getting-started.md new file mode 100644 index 0000000000..26f33a1877 --- /dev/null +++ b/docs/4.10/eloquent/getting-started.md @@ -0,0 +1,287 @@ +# Eloquent: Getting Started + +Lighthouse makes it easy for you to perform queries and mutations on your Eloquent models. + +## Defining Models + +Eloquent models usually map directly to GraphQL types. + +```graphql +type User { + id: ID! + name: String! +} +``` + +It is strongly advised to name the field that corresponds to your primary key `id`. +Popular client libraries such as Apollo provide out-of-the-box caching if you follow that convention. + +## Retrieving Models + +Instead of defining your own resolver manually, you can just rely on Lighthouse to build the Query for you. + +```graphql +type Query { + users: [User!]! @all +} +``` + +The [@all](../api-reference/directives.md#all) directive will assume the name of your model to be the same as +the return type of the Field you are trying to resolve and automatically uses Eloquent to resolve the field. + +The following query: + +```graphql +{ + users { + id + name + } +} +``` + +Will return the following result: + +```json +{ + "data": { + "users": [ + {"id": 1, "name": "James Bond"}, + {"id": 2, "name": "Madonna"} + ] + } +} +``` + +## Pagination + +You can leverage the [`@paginate`](../api-reference/directives.md#paginate) directive to +query a large list of models in chunks. + +```graphql +type Query { + posts: [Post!]! @paginate +} +``` + +The schema definition is automatically transformed to this: + +```graphql +type Query { + posts(first: Int!, page: Int): PostPaginator +} + +type PostPaginator { + data: [Post!]! + paginatorInfo: PaginatorInfo! +} +``` + +And can be queried like this: + +```graphql +{ + posts(first: 10) { + data { + id + title + } + paginatorInfo { + currentPage + lastPage + } + } +} +``` + +## Adding Query Constraints + +Lighthouse provides built-in directives to enhance your queries by giving +additional query capabilities to the client. + +The following field definition allows you to fetch a single User by ID. + +```graphql +type Query { + user(id: ID! @eq): User @find +} +``` + +You can query this field like this: + +```graphql +{ + user(id: 69){ + name + } +} +``` + +And, if a result is found, receive a result like this: + +```json +{ + "data": { + "user": { + "name": "Chuck Norris" + } + } +} +``` + +## Create + +The easiest way to create data on your server is to use the [@create](../api-reference/directives.md#create) directive. + +```graphql +type Mutation { + createUser(name: String!): User! @create +} +``` + +This will take the arguments that the `createUser` field receives and use them to create a new model instance. + +```graphql +mutation { + createUser(name: "Donald"){ + id + name + } +} +``` + +The newly created user is returned as a result: + +```json +{ + "data": { + "createUser": { + "id": "123", + "name": "Donald" + } + } +} +``` + +__Note__: Due to Laravel's protections against mass assignment, any arguments used in `@create` or `@update` must be added to the `$fillable` property in your Model. For the above example, we would need the following in `\App\Models\User`: + +```php +class User extends Model +{ + // ... + protected $fillable = ["name"]; +} +``` + +For more information, see the [laravel docs](https://laravel.com/docs/eloquent#mass-assignment). + +## Update + +You can update a model with the [@update](../api-reference/directives.md#update) directive. + +```graphql +type Mutation { + updateUser(id: ID!, name: String): User @update +} +``` + +Since GraphQL allows you to update just parts of your data, it is best to have all arguments except `id` as optional. + +```graphql +mutation { + updateUser(id: "123" name: "Hillary"){ + id + name + } +} +``` + +```json +{ + "data": { + "updateUser": { + "id": "123", + "name": "Hillary" + } + } +} +``` + +Be aware that while a create operation will always return a result, provided you pass valid data, the update +may fail to find the model you provided and return `null`: + +```json +{ + "data": { + "updateUser": null + } +} +``` + +## Upsert + +Use the [@upsert](../api-reference/directives.md#upsert) directive to update a model with +a given `id` or create it if it does not exist. + +```graphql +type Mutation { + upsertUser(id: ID!, name: String!, email: String): User @upsert +} +``` + +Since upsert can create or update your data you must have all the minimum fields for a creation as required. +The `id` is always required and must be marked as fillable in the model. + +```graphql +mutation { + upsertUser(id: "123" name: "Hillary"){ + id + name + email + } +} +``` + +```json +{ + "data": { + "upsertUser": { + "id": "123", + "name": "Hillary", + "email": null + } + } +} +``` + +## Delete + +Deleting models is a breeze using the [@delete](../api-reference/directives.md#delete) directive. Dangerously easy. + +```graphql +type Mutation { + deleteUser(id: ID!): User @delete +} +``` + +Simply call it with the ID of the user you want to delete. + +```graphql +mutation { + deleteUser(id: "123"){ + secret + } +} +``` + +This mutation will return the deleted object, so you will have a last chance to look at the data. Use it wisely. + +```json +{ + "data": { + "deleteUser": { + "secret": "Pink is my favorite color!" + } + } +} +``` diff --git a/docs/4.10/eloquent/nested-mutations.md b/docs/4.10/eloquent/nested-mutations.md new file mode 100644 index 0000000000..ea1eb94375 --- /dev/null +++ b/docs/4.10/eloquent/nested-mutations.md @@ -0,0 +1,848 @@ +# Nested Mutations + +Lighthouse allows you to create, update or delete models and their associated relationships +all in one single mutation. This is enabled by the [nested arg resolvers mechanism](../concepts/arg-resolvers.md). + +## Return Types Required + +You have to define return types on your relationship methods so that Lighthouse +can detect them. + +```php +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +class Post extends Model +{ + // WORKS + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + // DOES NOT WORK + public function comments() + { + return $this->hasMany(Comment::class); + } +} +``` + +## Partial Failure + +By default, all mutations are wrapped in a database transaction. +If any of the nested operations fail, the whole mutation is aborted +and no changes are written to the database. + +You can change this setting [in the configuration](../getting-started/configuration.md). + +## Belongs To + +We will start of by defining a mutation to create a post. + +```graphql +type Mutation { + createPost(input: CreatePostInput! @spread): Post @create +} +``` + +The mutation takes a single argument `input` that contains data about +the Post you want to create. + +```graphql +input CreatePostInput { + title: String! + author: CreateUserBelongsTo +} +``` + +The first argument `title` is a value of the `Post` itself and corresponds +to a column in the database. + +The second argument `author` is named just like the relationship method that is defined on the `Post` model. +A nested `BelongsTo` relationship exposes the following operations: + +- `connect` it to an existing model +- `create` a new related model and attach it +- `update` an existing model and attach it +- `upsert` a new or an existing model and attach it +- `disconnect` the related model +- `delete` the related model and the association to it + +Both `disconnect` and `delete` don't make much sense in the context of an update. +You can control what operations are possible by defining just what you need in the `input`. +We choose to expose the following operations on the related `User` model: + +```graphql +input CreateUserBelongsTo { + connect: ID + create: CreateUserInput + update: UpdateUserInput + upsert: UpsertUserInput +} +``` + +Finally, you need to define the input that allows you to create a new `User`. + +```graphql +input CreateUserInput { + name: String! +} +``` + +To create a new model and connect it to an existing model, +just pass the ID of the model you want to associate. + +```graphql +mutation { + createPost(input: { + title: "My new Post" + author: { + connect: 123 + } + }){ + id + author { + name + } + } +} +``` + +Lighthouse will create a new `Post` and associate an `User` with it. + +```json +{ + "data": { + "createPost": { + "id": 456, + "author": { + "name": "Herbert" + } + } + } +} +``` + +If the related model does not exist yet, you can also +create a new one. + +```graphql +mutation { + createPost(input: { + title: "My new Post" + author: { + create: { + name: "Gina" + } + } + }){ + id + author { + id + } + } +} +``` + +```json +{ + "data": { + "createPost": { + "id": 456, + "author": { + "id": 55 + } + } + } +} +``` + +When issuing an update, you can also allow the user to remove a relation. +Both `disconnect` and `delete` remove the association to the author, +but `delete` also removes the author model itself. + +```graphql +type Mutation { + updatePost(input: UpdatePostInput! @spread): Post @update +} + +input UpdatePostInput { + id: ID! + title: String + author: UpdateUserBelongsTo +} + +input UpdateUserBelongsTo { + connect: ID + create: CreateUserInput + update: UpdateUserInput + upsert: UpdateUserInput + disconnect: Boolean + delete: Boolean +} +``` + +You must pass a truthy value to `disconnect` and `delete` for them to actually run. +This structure was chosen as it is consistent with updating `BelongsToMany` relationships +and allows the query string to be mostly static, taking a variable value to control its behaviour. + +```graphql +mutation UpdatePost($disconnectAuthor: Boolean){ + updatePost(input: { + id: 1 + title: "An updated title" + author: { + disconnect: $disconnectAuthor + } + }){ + title + author { + name + } + } +} +``` + +The `author` relationship will only be disconnected if the value of the variable +`$disconnectAuthor` is `true`, if `false` or `null` are passed, it will not change. + +```json +{ + "data": { + "updatePost": { + "id": 1, + "title": "An updated title", + "author": null + } + } +} +``` + +When issuing an `upsert`, you may expose the same nested operations as an `update`. +In case a new model is created, they will simply be ignored. + +```graphql +mutation UpdatePost($disconnectAuthor: Boolean){ + upsertPost(input: { + id: 1 + title: "An updated or created title" + author: { + disconnect: $disconnectAuthor + } + }){ + id + title + author { + name + } + } +} +``` + +```json +{ + "data": { + "upsertPost": { + "id": 1, + "title": "An updated or created title", + "author": null + } + } +} +``` + +## Has Many + +The counterpart to a `BelongsTo` relationship is `HasMany`. We will start +off by defining a mutation to create an `User`. + +```graphql +type Mutation { + createUser(input: CreateUserInput! @spread): User @create +} +``` + +This mutation takes a single argument `input` that contains values +of the `User` itself and its associated `Post` models. + +```graphql +input CreateUserInput { + name: String! + posts: CreatePostsHasMany +} +``` + +Now, we can expose an operation that allows us to directly create new posts +right when we create the `User`. + +```graphql +input CreatePostsHasMany { + create: [CreatePostInput!]! +} + +input CreatePostInput { + title: String! +} +``` + +We can now create a `User` and some posts with it in one request. + +```graphql +mutation { + createUser(input: { + name: "Phil" + posts: { + create: [ + { + title: "Phils first post" + }, + { + title: "Awesome second post" + } + ] + } + }){ + id + posts { + id + } + } +} +``` + +```json +{ + "data": { + "createUser": { + "id": 23, + "posts": [ + { + "id": 434 + }, + { + "id": 435 + } + ] + } + } +} +``` + +When updating a `User`, further nested operations become possible. +It is up to you which ones you want to expose through the schema definition. + +The following example covers the full range of possible operations: + +```graphql +type Mutation { + updateUser(input: UpdateUserInput! @spread): User @update +} + +input UpdateUserInput { + id: ID! + name: String + posts: UpdatePostsHasMany +} + +input UpdatePostsHasMany { + create: [CreatePostInput!] + update: [UpdatePostInput!] + upsert: [UpsertPostInput!] + delete: [ID!] +} + +input CreatePostInput { + title: String! +} + +input UpdatePostInput { + id: ID! + title: String +} + +input UpsertPostInput { + id: ID! + title: String +} +``` + +```graphql +mutation { + updateUser(input: { + id: 3, + name: "Phillip" + posts: { + create: [ + { + title: "A new post" + } + ], + update: [ + { + id: 45, + title: "This post is updated" + } + ], + delete: [ + 8, + ] + } + }){ + id + posts { + id + } + } +} +``` + +The behaviour for `upsert` is a mix between updating and creating, +it will produce the needed action regardless of whether the model exists or not. + +## Belongs To Many + +A belongs to many relation allows you to create new related models as well +as attaching existing ones. + +```graphql +type Mutation { + createPost(input: CreatePostInput! @spread): Post @create +} + +input CreatePostInput { + title: String! + authors: CreateAuthorBelongsToMany +} + +input CreateAuthorBelongsToMany { + create: [CreateAuthorInput!] + upsert: [UpsertAuthorInput!] + connect: [ID!] + sync: [ID!] +} + +input CreateAuthorInput { + name: String! +} + +input UpsertAuthorInput { + id: ID! + name: String! +} +``` + +Just pass the ID of the models you want to associate or their full information +to create a new relation. + +```graphql +mutation { + createPost(input: { + title: "My new Post" + authors: { + create: [ + { + name: "Herbert" + } + ] + upsert: [ + { + id: 2000 + name: "Newton" + } + ] + connect: [ + 123 + ] + } + }){ + id + authors { + name + } + } +} +``` + +Lighthouse will detect the relationship and attach, update or create it. + +```json +{ + "data": { + "createPost": { + "id": 456, + "authors": [ + { + "id": 165, + "name": "Herbert" + }, + { + "id": 2000, + "name": "Newton" + }, + { + "id": 123, + "name": "Franz" + } + ] + } + } +} +``` + +It is also possible to use the `sync` operation to ensure only the given IDs +will be contained withing the relation. + +```graphql +mutation { + createPost(input: { + title: "My new Post" + authors: { + sync: [ + 123 + ] + } + }){ + id + authors { + name + } + } +} +``` + +Updates on `BelongsToMany` relations may expose additional nested operations: + +```graphql +input UpdateAuthorBelongsToMany { + create: [CreateAuthorInput!] + connect: [ID!] + update: [UpdateAuthorInput!] + upsert: [UpsertAuthorInput!] + sync: [ID!] + syncWithoutDetaching: [ID!] + delete: [ID!] + disconnect: [ID!] +} +``` + +### Storing Pivot Data + +It is common that many-to-many relations store some extra data in pivot tables. +Suppose we want to track what movies a user has seen. In addition to connecting +the two entities, we want to store how well they liked it: + +```graphql +type User { + id: ID! + seenMovies: [Movie!] @belongsToMany +} + +type Movie { + id: ID! + pivot: UserMoviePivot +} + +type UserMoviePivot { + "How well did the user like the movie?" + rating: String +} +``` + +Laravel's `sync()`, `syncWithoutDetach()` or `connect()` methods allow you to pass +an array where the keys are IDs of related models and the values are pivot data. + +Lighthouse exposes this capability through the nested operations on many-to-many relations. +Instead of passing just a list of ids, you can define an `input` type that also contains pivot data. +It must contain a field called `id` to contain the ID of the related model, +all other fields will be inserted into the pivot table. + +```graphql +type Mutation { + updateUser(input: UpdateUserInput! @spread): User @update +} + +input UpdateUserInput { + id: ID! + seenMovies: UpdateUserSeenMovies +} + +input UpdateUserSeenMovies { + connect: [ConnectUserSeenMovie!] +} + +input ConnectUserSeenMovie { + id: ID! + rating: String +} +``` + +You can now pass along pivot data when connecting users to movies: + +```graphql +mutation { + updateUser(input: { + id: 1 + seenMovies: { + connect: [ + { + id: 6 + rating: "A perfect 5/7" + } + { + id: 23 + } + ] + }, + }) { + id + seenMovies { + id + pivot { + rating + } + } + } +} +``` + +And you will get the following response: + +```json +{ + "data": { + "updateUser": { + "id": 1, + "seenMovies": [ + { + "id": 6, + "pivot": { + "rating": "A perfect 5/7" + } + }, + { + "id": 20, + "pivot": { + "rating": null + } + } + ] + } + } +} +``` + +It is also possible to use the `sync` and `syncWithoutDetach` operations. + +## MorphTo + +__The GraphQL Specification does not support Input Union types, +for now we are limiting this implementation to `connect`, `disconnect` and `delete` operations. +See https://github.com/nuwave/lighthouse/issues/900 for further discussion.__ + +```graphql +type Task { + id: ID + name: String +} + +type Image { + id: ID + url: String + imageable: Task +} + +type Mutation { + createImage(input: CreateImageInput! @spread): Image @create + updateImage(input: UpdateImageInput! @spread): Image @update + upsertImage(input: UpsertImageInput! @spread): Image @upsert +} + +input CreateImageInput { + url: String + imageable: CreateImageableMorphTo +} + +input UpdateImageInput { + id: ID! + url: String + imageable: UpdateImageableMorphTo +} + +input UpsertImageInput { + id: ID! + url: String + imageable: UpsertImageableMorphTo +} + +input CreateImageableMorphTo { + connect: ConnectImageableInput +} + +input UpdateImageableMorphTo { + connect: ConnectImageableInput + disconnect: Boolean + delete: Boolean +} + +input UpsertImageableMorphTo { + connect: ConnectImageableInput + disconnect: Boolean + delete: Boolean +} + +input ConnectImageableInput { + type: String! + id: ID! +} +``` + +You can use `connect` to associate existing models. + +```graphql +mutation { + createImage(input: { + url: "https://cats.example/cute" + imageable: { + connect: { + type: "App\\Models\\Task" + id: 1 + } + } + }) { + id + url + imageable { + id + name + } + } +} +``` + +The `disconnect` operations allows you to detach the currently associated model. + +```graphql +mutation { + updateImage(input: { + id: 1 + url: "https://dogs.example/supercute" + imageable: { + disconnect: true + } + }) { + url + imageable { + id + name + } + } +} +``` + +The `delete` operation both detaches and deletes the currently associated model. + +```graphql +mutation { + upsertImage(input: { + id: 1 + url: "https://bizniz.example/serious" + imageable: { + delete: true + } + }) { + url + imageable { + id + name + } + } +} +``` + +## Morph To Many + +A morph to many relation allows you to create new related models as well +as attaching existing ones. + +```graphql +type Mutation { + createTask(input: CreateTaskInput! @spread): Task @create +} + +input CreateTaskInput { + name: String! + tags: CreateTagMorphToMany +} + +input CreateTagMorphToMany { + create: [CreateTagInput!] + upsert: [UpsertTagInput!] + sync: [ID!] + connect: [ID!] +} + +input CreateTagInput { + name: String! +} + +input UpsertTagInput { + id: ID! + name: String! +} + + +type Task { + id: ID! + name: String! + tags: [Tag!]! +} + +type Tag { + id: ID! + name: String! +} +``` + +In this example, the tag with id `1` already exists in the database. The query connects this tag to the task using the `MorphToMany` relationship. + +```graphql +mutation { + createTask(input: { + name: "Loundry" + tags: { + connect: [1] + } + }) { + tags { + id + name + } + } +} +``` + +You can either use `connect` or `sync` during creation. + +When you want to create a new tag while creating the task, +you need to use the `create` operation to provide an array of `CreateTagInput` +or use the `upsert` operation to provide an array of `UpsertTagInput`: + +```graphql +mutation { + createTask(input: { + name: "Loundry" + tags: { + create: [ + { + name: "home" + } + ] + } + }) { + tags { + id + name + } + } +} +``` diff --git a/docs/4.10/eloquent/polymorphic-relationships.md b/docs/4.10/eloquent/polymorphic-relationships.md new file mode 100644 index 0000000000..c1175f5950 --- /dev/null +++ b/docs/4.10/eloquent/polymorphic-relationships.md @@ -0,0 +1,94 @@ +# Polymorphic Relationships + +Just like in Laravel, you can define [Polymorphic Relationships](https://laravel.com/docs/eloquent-relationships#polymorphic-relationships) in your schema. + +## One to One + +Suppose you have defined a model structure just like the Laravel example docs. +You have two models, `Post` and `User` which may both have an `Image` assigned. + +Let's start off with the plain type definitions, without any relations. + +```graphql +type Post { + id: ID! + name: String! +} + +type User { + id: ID! + name: String! +} + +type Image { + id: ID! + url: String! +} +``` + +First, let's go ahead and add the relations to `Image` since they are straightforward. +The field name should match your relationship method name and be annotated +with the [`@morphOne`](../api-reference/directives.md#morphone) directive. + +```graphql +type Post { + id: ID! + name: String! + image: Image! @morphOne +} + +type User { + id: ID! + name: String! + image: Image @morphOne +} +``` + +Depending on the rules of your application, you might require the relationship +to be there in some cases, while allowing it to be absent in others. In this +example, a `Post` must always have an `Image`, while a `User` does not require one. + +For the inverse, you will need to define a [union type](../the-basics/types.md#union) +to express that an `Image` might be linked to different models. + +```graphql +union Imageable = Post | User +``` + +Now, reference the union type from a field in your `Image` type. +You can use the [`@morphTo`](../api-reference/directives.md#morphto) directive +for performance optimization. + +```graphql +type Image { + id: ID! + url: String! + imageable: Imageable! @morphTo +} +``` + +The default type resolver will be able to determine which concrete object type is returned +when dealing with Eloquent models, so your definition should just work. + +## One to Many + +Based on the above example, you could change your application to allow +for a `Post` to have many images attached to it. +The field `images` now returns a list of `Image` object and is annotated +with the [`@morphMany`](../api-reference/directives.md#morphmany) directive. + +```graphql +type Post { + id: ID! + name: String! + images: [Image]! @morphMany +} + +type Image { + id: ID! + url: String! + imageable: Imageable! @morphTo +} + +union Imageable = Post | User +``` diff --git a/docs/4.10/eloquent/relationships.md b/docs/4.10/eloquent/relationships.md new file mode 100644 index 0000000000..d7a4de1fbd --- /dev/null +++ b/docs/4.10/eloquent/relationships.md @@ -0,0 +1,143 @@ +# Eloquent Relationships + +Just like in Laravel, you can define [Eloquent Relationships](https://laravel.com/docs/eloquent-relationships) in your schema. + +Suppose you have defined the following model: + +```php +hasMany(Comment::class); + } + + public function author(): BelongsTo + { + return $this->belongsTo(User::class); + } +} +``` + +Just add fields to your type that are named just like the relationships: + +```graphql +type Post { + author: User + comments: [Comment!] +} +``` + +Because Laravel relationships can be accessed just like regular properties on your model, +the default field resolver will work just fine. + +## Avoiding the N+1 performance problem + +When accessing Eloquent relationships as properties, the relationship data is "lazy loaded". +This means the relationship data is not actually loaded until you first access the property. + +This leads to a common performance pitfall that comes with the nested nature of GraphQL queries: +the so-called N+1 query problem. [Learn more](../performance/n-plus-one.md). + +When you decorate your relationship fields with Lighthouse's built-in relationship +directives, queries are automatically combined through a technique called *batch loading*. +That means you get fewer database requests and better performance without doing much work. + +> Batch loading might not provide ideal performance for all use cases. You can turn +> it off by setting the config option `batchload_relations` to `false`. + +## One To One + +Use the [@hasOne](../api-reference/directives.md#hasone) directive to define a [one-to-one relationship](https://laravel.com/docs/eloquent-relationships#one-to-one) +between two types in your schema. + +```graphql +type User { + phone: Phone @hasOne +} +``` + +The inverse can be defined through the [@belongsTo](../api-reference/directives.md#belongsto) directive. + +```graphql +type Phone { + user: User @belongsTo +} +``` + +## One To Many + +Use the [@hasMany](../api-reference/directives.md#hasmany) directive to define a [one-to-many relationship](https://laravel.com/docs/eloquent-relationships#one-to-many). + +```graphql +type Post { + comments: [Comment!]! @hasMany +} +``` + +Again, the inverse is defined with the [@belongsTo](../api-reference/directives.md#belongsto) directive. + +```graphql +type Comment { + post: Post! @belongsTo +} +``` + +## Many To Many + +While [many-to-many relationships](https://laravel.com/docs/eloquent-relationships#many-to-many) +are a bit more work to set up in Laravel, defining them in Lighthouse is a breeze. +Use the [@belongsToMany](../api-reference/directives.md#belongstomany) directive to define it. + +```graphql +type User { + roles: [Role!]! @belongsToMany +} +``` + +The inverse works the same. + +```graphql +type Role { + users: [User!]! @belongsToMany +} +``` + +## Renaming relations + +When you define a relation, Lighthouse assumes that the field and the relationship +method have the same name. If you need to name your field differently, you have to +specify the name of the method. + +``` +type Post { + author: User! @belongsTo(relation: "user") +} +``` + +This would work for the following model: + +```php +belongsTo(User::class); + } +} +``` diff --git a/docs/4.10/eloquent/soft-deleting.md b/docs/4.10/eloquent/soft-deleting.md new file mode 100644 index 0000000000..56ae162b3b --- /dev/null +++ b/docs/4.10/eloquent/soft-deleting.md @@ -0,0 +1,101 @@ +# Soft Deleting + +Lighthouse offers convenient helpers to work with models that utilize +[soft deletes](https://laravel.com/docs/eloquent#soft-deleting). + +## Filter Soft Deleted Models + +If your model uses the `Illuminate\Database\Eloquent\SoftDeletes` trait, +you can add the [`@softDeletes`](../api-reference/directives.md#softdeletes) directive to a field +to be able to query `onlyTrashed`, `withTrashed` or `withoutTrashed` elements. + +```graphql +type Query { + flights: [Flight!]! @all @softDeletes +} +``` + +Lighthouse will automatically add an argument `trashed` to the field definition +and include the enum `Trashed`. + +```graphql +type Query { + flights(trashed: Trashed @trashed): [Flight!]! @all +} + +""" +Used for filtering +""" +enum Trashed { + ONLY @enum(value: "only") + WITH @enum(value: "with") + WITHOUT @enum(value: "without") +} +``` + +You can include soft deleted models in your result with a query like this: + +```graphql +{ + flights(trashed: WITH) { + id + } +} +``` + +## Restoring Soft Deleted Models + +If your model uses the `Illuminate\Database\Eloquent\SoftDeletes` trait, +you can restore your model using the [`@restore`](../api-reference/directives.md#restore) directive. + +```graphql +type Mutation { + restoreFlight(id: ID!): Flight @restore +} +``` + +Simply call the field with the ID of the flight you want to restore. + +```graphql +mutation { + restoreFlight(id: 1) { + id + } +} +``` + +This mutation will return the restored object. + +## Permanently Deleting Models + +To truly remove a model from the database, +use the [@forceDelete](../api-reference/directives.md#forcedelete) directive. +Your model must use the `Illuminate\Database\Eloquent\SoftDeletes` trait. + +```graphql +type Mutation { + forceDeleteFlight(id: ID!): Flight @forceDelete +} +``` + +Simply call it with the ID of the `Flight` you want to permanently remove. + +```graphql +mutation { + forceDeleteFlight(id: 5){ + id + } +} +``` + +This mutation will return the deleted object, so you will have a last chance to look at the data. + +```json +{ + "data": { + "forceDeleteFlight": { + "id": 5 + } + } +} +``` diff --git a/docs/4.10/getting-started/configuration.md b/docs/4.10/getting-started/configuration.md new file mode 100644 index 0000000000..6307616a37 --- /dev/null +++ b/docs/4.10/getting-started/configuration.md @@ -0,0 +1,10 @@ +# Configuration + +Lighthouse comes with sensible configuration defaults and works right out of the box. +Should you feel the need to change your configuration, you need to publish the configuration file first. + +```bash +php artisan vendor:publish --provider="Nuwave\Lighthouse\LighthouseServiceProvider" --tag=config +``` + +The configuration file will be placed in `config/lighthouse.php`. diff --git a/docs/4.10/getting-started/installation.md b/docs/4.10/getting-started/installation.md new file mode 100644 index 0000000000..626074919f --- /dev/null +++ b/docs/4.10/getting-started/installation.md @@ -0,0 +1,46 @@ +# Installation + +The following section teaches you how to install Lighthouse in your project. + +## Install via composer + +```bash +composer require nuwave/lighthouse +``` + +## Publish the default schema + +Lighthouse includes a default schema to get you going right away. Publish +it using the following `artisan` command: + +```bash +php artisan vendor:publish --provider="Nuwave\Lighthouse\LighthouseServiceProvider" --tag=schema +``` + +## IDE Support + +Lighthouse makes heavy use of the SDL and utilizes schema directives. +To improve your editing experience, you can generate a definition file +[with an artisan command](../api-reference/commands.md#ide-helper): + +```bash +php artisan lighthouse:ide-helper +``` + +We recommend the following plugins: + +|IDE|Plugin| +|---|---| +|PhpStorm|https://plugins.jetbrains.com/plugin/8097-js-graphql| + +## Install GraphQL DevTools + +To make use of the amazing tooling around GraphQL, we recommend +installing [GraphQL Playground](https://github.com/mll-lab/laravel-graphql-playground) + +```bash +composer require mll-lab/laravel-graphql-playground +``` + +You can use any GraphQL client with Lighthouse, make sure to point it to the URL defined in +the config. By default, the endpoint lives at `/graphql`. diff --git a/docs/4.10/getting-started/migrating-to-lighthouse.md b/docs/4.10/getting-started/migrating-to-lighthouse.md new file mode 100644 index 0000000000..f3322dec85 --- /dev/null +++ b/docs/4.10/getting-started/migrating-to-lighthouse.md @@ -0,0 +1,38 @@ +# Migrating to Lighthouse + +This section contains advice on how you can migrate existing +API projects to Lighthouse. + +## Schema definition + +The most important thing to get you started using Lighthouse will +be a schema that is written using GraphQL Schema Definition Language. + +If you already have a server with another library up and running, you +can use introspection to retrieve this schema and save it to a file. + +A simple tool that is also generally useful is [graphql-cli](https://github.com/graphql-cli/graphql-cli). + + npm install -g graphql-cli + graphql get-schema --endpoint=example.com/graphql --output=schema.graphql + +Type definitions that previously done through code can mostly be deduced from +the schema. Sometimes, additional annotations or a PHP implementation is required. +[How to define types](../the-basics/types.md) + +## Resolver logic + +If you are coming from libraries such as [Folkloreatelier/laravel-graphql](https://github.com/Folkloreatelier/laravel-graphql), +[rebing/laravel-graphql](https://github.com/rebing/graphql-laravel) or any other library that +is originally based upon [webonyx/graphql-php](https://github.com/webonyx/graphql-php), +you should be able to reuse much of your existing code. + +You can also register your existing types within Lighthouse's type registry, so you +won't have to rewrite them in SDL: [Use native PHP types](../digging-deeper/adding-types-programmatically.md#native-php-types). + +Resolver functions share the same [common signature](../api-reference/resolvers.md#resolver-function-signature), +so you should be able to reuse any logic you have written for Queries/Mutations. + +Lighthouse simplifies many common tasks, such as [basic CRUD operations](../the-basics/fields.md), +[eager loading relationships](../eloquent/relationships.md#avoiding-the-n1-performance-problem), +[pagination](../api-reference/directives.md#paginate) or [validation](../security/validation.md). diff --git a/docs/4.10/performance/deferred.md b/docs/4.10/performance/deferred.md new file mode 100644 index 0000000000..5ebde51df0 --- /dev/null +++ b/docs/4.10/performance/deferred.md @@ -0,0 +1,27 @@ +# Deferred Fields + +::: warning +Currently the `@defer` directive requires Apollo Client in alpha. +Track the state of the PR here: https://github.com/apollographql/apollo-client/pull/3686 +::: + +Deferring fields allows you to prioritize fetching data needed to render the most important content +as fast as possible, and then loading the rest of the page in the background. + +Lighthouse's `DeferExtension` adds support for the experimental `@defer` directive +provided by Apollo which you can read more about [here](https://www.apollographql.com/docs/react/features/defer-support.html). + +## Setup + +Add the service provider to your `config/app.php` + +```php +'providers' => [ + \Nuwave\Lighthouse\Defer\DeferServiceProvider::class, +], +``` + +
+ +![defer_example](https://user-images.githubusercontent.com/1976169/48140644-71e25500-e266-11e8-924b-08ee2f7318d1.gif) +_(image from [https://blog.apollographql.com/introducing-defer-in-apollo-server-f6797c4e9d6e](https://blog.apollographql.com/introducing-defer-in-apollo-server-f6797c4e9d6e))_ diff --git a/docs/4.10/performance/n-plus-one.md b/docs/4.10/performance/n-plus-one.md new file mode 100644 index 0000000000..b59b9bb693 --- /dev/null +++ b/docs/4.10/performance/n-plus-one.md @@ -0,0 +1,55 @@ +# The N+1 Query Problem + +A common performance pitfall that comes with the nested nature of GraphQL queries +is the so-called N+1 query problem. + +Let’s imagine we want to fetch a list of posts, and for each post, we want to add on the +name of the associated author: + +```graphql +{ + posts { + title + author { + name + } + } +} +``` + +Following a naive execution strategy, Lighthouse would first query a list of posts, +then loop over that list and resolve the individual fields. +The associated author for each post would be lazily loaded, querying the database +once per post. + +## Eager Loading Relationships + +When dealing with Laravel relationships, [eager loading](https://laravel.com/docs/eloquent-relationships#eager-loading) +is commonly used to alleviate the N+1 query problem. + +You can leverage eager loading by informing Lighthouse of the relationships between your models, +using directives such as [`@belongsTo`](../api-reference/directives.md#belongsto) and [`@hasMany`](../api-reference/directives.md#hasmany). + +```graphql +type Post { + title: String! + author: User! @belongsTo +} + +type User { + name: String! + posts: [Post!]! @hasMany +} +``` + +Under the hood, Lighthouse will batch the relationship queries together in a single database query. + +If you require a relation to be loaded for some field, but do not wish to return the relationship itself, +you can use the [`@with`](../api-reference/directives.md#with) directive. + +## Data Loader + +`webonyx/graphql-php` allows deferring the actual resolution of a field until it is actually needed, +read more [in their documentation](http://webonyx.github.io/graphql-php/data-fetching/#solving-n1-problem). + +You can extend `\Nuwave\Lighthouse\Execution\DataLoader\BatchLoader` if you require custom batch loading. diff --git a/docs/4.10/performance/schema-caching.md b/docs/4.10/performance/schema-caching.md new file mode 100644 index 0000000000..54d3144bfb --- /dev/null +++ b/docs/4.10/performance/schema-caching.md @@ -0,0 +1,28 @@ +# Schema caching + +As your schema grows larger, the construction of the schema from raw `.graphql` files +becomes more and more costly. + +Make sure to enable schema caching when shipping Lighthouse to production. + +```php + /* + |-------------------------------------------------------------------------- + | Schema Cache + |-------------------------------------------------------------------------- + | + | A large part of schema generation is parsing the schema into an AST. + | This operation is pretty expensive so it is recommended to enable + | caching in production mode, especially for large schemas. + | + */ + + 'cache' => [ + 'enable' => env('LIGHTHOUSE_CACHE_ENABLE', true), + 'key' => env('LIGHTHOUSE_CACHE_KEY', 'lighthouse-schema'), + ], +``` + +You may clear your schema cache using the [clear-cache](../api-reference/commands.md#clear-cache) artisan command: + + php artisan lighthouse:clear-cache diff --git a/docs/4.10/performance/server-configuration.md b/docs/4.10/performance/server-configuration.md new file mode 100644 index 0000000000..8288eb094b --- /dev/null +++ b/docs/4.10/performance/server-configuration.md @@ -0,0 +1,13 @@ +# Server configuration + +You can tune the configuration of your PHP server for Lighthouse. + +## OPcache + +The nature of the schema operations in Lighthouse plays nicely with [PHP's OPcache](https://php.net/manual/de/book.opcache.php). +If you have the freedom to install it on your server, it's an easy way to get a nice performance boost. + +## Xdebug + +Enabling Xdebug and having an active debug session slows down execution by +an order of magnitude. diff --git a/docs/4.10/performance/tracing.md b/docs/4.10/performance/tracing.md new file mode 100644 index 0000000000..738f6e1334 --- /dev/null +++ b/docs/4.10/performance/tracing.md @@ -0,0 +1,14 @@ +# Tracing + +Tracing offers field-level performance monitoring for your GraphQL server. +Lighthouse follows the [Apollo Tracing response format](https://github.com/apollographql/apollo-tracing#response-format). + +## Setup + +Add the service provider to your `config/app.php` + +```php +'providers' => [ + \Nuwave\Lighthouse\Tracing\TracingServiceProvider::class, +], +``` diff --git a/docs/4.10/security/authentication.md b/docs/4.10/security/authentication.md new file mode 100644 index 0000000000..7a990763ee --- /dev/null +++ b/docs/4.10/security/authentication.md @@ -0,0 +1,55 @@ +# Authentication + +## Global + +You can use standard Laravel mechanisms to authenticate users of your GraphQL API. +Just add middleware trough your `lighthouse.php` configuration. +The [API Authentication](https://laravel.com/docs/api-authentication) is especially +suited because of its stateless nature. + +As all GraphQL requests are served at a single HTTP endpoint, this will guard your +entire API against unauthenticated users. + +## Guard selected fields + +If you want to guard only selected fields, you can use the [`@guard`](../api-reference/directives.md#guard) +directive to require authentication for accessing them. + +```graphql +type Query { + profile: User! @guard +} +``` + +If you need to guard multiple fields, just use [`@guard`](../api-reference/directives.md#guard) +on a `type` or an `extend type` definition. It will be applied to all fields within that type. + +```graphql +extend type Query @guard(with: ["api:admin"]){ + adminInfo: Secrets + nukeCodes: [NukeCode!]! +} +``` + +## Get the current user + +Lighthouse provides a really simple way to fetch the information of the currently authenticated user. +Just add a field that returns your `User` type and decorate it with the [`@auth`](../api-reference/directives.md#auth) directive. + +```graphql +type Query { + me: User @auth +} +``` + +Sending the following query will return the authenticated user's info +or `null` if the request is not authenticated. + +```graphql +{ + me { + name + email + } +} +``` diff --git a/docs/4.10/security/authorization.md b/docs/4.10/security/authorization.md new file mode 100644 index 0000000000..2b5eb355de --- /dev/null +++ b/docs/4.10/security/authorization.md @@ -0,0 +1,162 @@ +# Authorization + +Not every user in your application may be allowed to see all data or do any action. +You can control what they can do by enforcing authorization rules. + +## Utilize the Viewer pattern + +A common pattern is to allow users to only access entries that belong to them. +For example, a user may only be able to see notes they created. +You can utilize the nested nature of GraphQL queries to naturally limit access to such fields. + +Begin with a field that represents the currently authenticated user, commonly called `me` or `viewer`. +You can resolve that field quite easily by using the [`@auth`](../api-reference/directives.md#auth) directive. + +```graphql +type Query { + me: User! @auth +} + +type User { + name: String! +} +``` + +Now, add related entities that are present as relationships onto the `User` type. + +```graphql +type User { + name: String! + notes: [Note!]! +} + +type Note { + title: String! + content: String! +} +``` + +Now, authenticated users can query for items that belong to them and are naturally +limited to seeing just those. + +```graphql +{ + me { + name + notes { + title + content + } + } +} +``` + +## Restrict fields through policies + +Lighthouse allows you to restrict field operations to a certain group of users. +Use the [@can](../api-reference/directives.md#can) directive +to leverage [Laravel Policies](https://laravel.com/docs/authorization) for authorization. + +Starting from Laravel 5.7, [authorization of guest users](https://laravel.com/docs/authorization#guest-users) is supported. +Because of this, Lighthouse does **not** validate that the user is authenticated before passing it along to the policy. + +### Protect mutations + +As an example, you might want to allow only admin users of your application to create posts. +Start out by defining `@can` upon a mutation you want to protect: + +```graphql +type Mutation { + createPost(input: PostInput): Post @can(ability: "create") +} +``` + +The `create` ability that is referenced in the example above is backed by a Laravel policy: + +```php +class PostPolicy +{ + public function create(User $user): bool + { + return $user->is_admin; + } +} +``` + +### Protect specific model instances + +For some models, you may want to restrict access for specific instances of a model. +Use the `find` parameter to specify the name of an input argument that is the primary +key of the model. Lighthouse will use that to find a specific model +instance against which the permissions should be checked: + +```graphql +type Query { + post(id: ID @eq): Post @can(ability: "view", find: "id") +} +``` + +```php +class PostPolicy +{ + public function view(User $user, Post $post): bool + { + return $user->id === $post->author_id; + } +} +``` + +Finding models combines nicely with [soft deleting](../eloquent/soft-deleting.md). +Lighthouse will detect if the query will require a filter for trashed models and +apply that as needed. + +### Passing additional arguments + +You can pass additional arguments to the policy checks by specifying them as `args`: + +```graphql +type Mutation { + createPost(input: PostInput): Post + @can(ability: "create", args: ["FROM_GRAPHQL"]) +} +``` + +```php +class PostPolicy +{ + public function create(User $user, array $args): bool + { + // $args will be the PHP representation of what is in the schema: [0 => 'FROM_GRAPHQL'] + } +} +``` + +You can pass along the client given input data as arguments to the policy checks +with the `injectArgs` argument: + +```graphql +type Mutation { + createPost(title: String!): Post + @can(ability: "create", injectArgs: "true") +} +``` + +```php +class PostPolicy +{ + public function create(User $user, array $injected): bool + { + // $injected will hold the args given by the client: ['title' => string(?)] + } +} +``` + +When you combine both ways of passing arguments, the policy will be passed the `injectArgs` as +the second parameter and the static `args` as the third parameter: + +```php +class PostPolicy +{ + public function create($user, array $injectedArgs, array $staticArgs): bool { ... } +} +``` diff --git a/docs/4.10/security/resource-exhaustion.md b/docs/4.10/security/resource-exhaustion.md new file mode 100644 index 0000000000..1d27632d65 --- /dev/null +++ b/docs/4.10/security/resource-exhaustion.md @@ -0,0 +1,15 @@ +# Preventing Resource Exhaustion + +GraphQL gives enormous power to clients. But with great power come great responsibilities 🕷. + +Since clients have the possibility to craft very complex queries, our servers must be ready +to handle them properly. These queries may be abusive queries from evil clients, +or may simply be very large queries used by legitimate clients. +In both of these cases, the client can potentially take your GraphQL server down. + +*This intro was taken from HowToGraphQL, we recommend reading their full chapter on security https://www.howtographql.com/advanced/4-security/* + +You can utilize the built-in security options through `config/lighthouse.php`. +Read up on [the security options offered by webonyx/graphql-php](http://webonyx.github.io/graphql-php/security/) + +If you implemented some additional security feature, we are welcoming contributions! diff --git a/docs/4.10/security/sanitization.md b/docs/4.10/security/sanitization.md new file mode 100644 index 0000000000..2d0adb1afb --- /dev/null +++ b/docs/4.10/security/sanitization.md @@ -0,0 +1,25 @@ +# Sanitization + +When dealing with user input, you need to make sure the given data is valid. +While [validation](validation) is a great first line of defense, there are cases where +it is most practical to modify the given input to ensure it is valid or safe to use. + +## Single arguments + +A great way to deal with single values is to use an [`ArgTransformerDirective`](../custom-directives/argument-directives.md#argtransformerdirective). +Lighthouse offers a few built-in options, but it is also really easy to build your own. + +Here is how you can remove whitespace of a given input string by using +the built-in [`@trim`](../api-reference/directives.md#trim) directive: + +```graphql +type Mutation { + createPost(title: String @trim): Post +} +``` + +## Complex arguments + +When you need to look at multiple input fields in order to run sanitization, you can use +a [`FieldMiddlewareDirective`](../custom-directives/field-directives.md#fieldmiddleware) +to transform the given inputs before passing them along to the final resolver. diff --git a/docs/4.10/security/validation.md b/docs/4.10/security/validation.md new file mode 100644 index 0000000000..a31cbbd963 --- /dev/null +++ b/docs/4.10/security/validation.md @@ -0,0 +1,169 @@ +# Validation + +## Validating Arguments + +Lighthouse allows you to use [Laravel's validation](https://laravel.com/docs/validation) for your +queries and mutations. The simplest way to leverage the built-in validation rules is to use the +[@rules](../api-reference/directives.md#rules) directive. + +```graphql +type Mutation { + createUser( + name: String @rules(apply: ["required", "min:4"]) + email: String @rules(apply: ["email"]) + ): User +} +``` + +In the case of a validation error, Lighthouse will abort execution and return the validation messages +as part of the response. + +```graphql +mutation { + createUser(email: "hans@peter.xyz"){ + id + } +} +``` + +```json +{ + "data": { + "foo": null + }, + "errors": [ + { + "message": "validation", + "locations": [ + { + "line": 2, + "column": 13 + } + ], + "extensions": { + "validation": [ + "The name field is required." + ] + } + } + ] +} +``` + +### Custom Error Messages + +You can customize the error message for a particular argument. + +```graphql +@rules(apply: ["max:140"], messages: { max: "Tweets have a limit of 140 characters"}) +``` + +### Custom Validation Rules + +Reference custom validation rules by their fully qualified class name. + +```graphql +@rules(apply: ["App\\Rules\\MyCustomRule"]) +``` + +## Validating Input Objects + +Rules can be defined upon Input Object Values. + +```graphql +input CreatePostInput { + title: String @rules(apply: ["required"]) + content: String @rules(apply: ["min:50", "max:150"]) +} +``` + +Using the [`unique`](https://laravel.com/docs/5.8/validation#rule-unique) +validation rule can be a bit tricky. + +If the argument is nested within an input object, the argument path will not +match the column name, so you have to specify the column name explicitly. + +```graphql +input CreateUserInput { + email: String @rules(apply: ["unique:users,email_address"]) +} +``` + +## Validating Arrays + +When you are passing in an array as an argument to a field, you might +want to apply some validation on the array itself, using [@rulesForArray](../api-reference/directives.md#rules) + +```graphql +type Mutation { + makeIcecream(topping: [Topping!]! @rulesForArray(apply: ["max:3"])): Icecream +} +``` + +You can also combine this with [@rules](../api-reference/directives.md#rules) to validate +both the size and the contents of an argument array. +For example, you might require a list of at least 3 valid emails to be passed. + +```graphql +type Mutation { + attachEmails( + email: [String!]! + @rules(apply: ["email"]) + @rulesForArray(apply: ["min:3"]) + ): File +} +``` + +## Validate Fields + +In some cases, validation rules are more complex and need to use entirely custom logic +or take multiple arguments into account. + +To create a reusable validator that can be applied to fields, extend the base validation +directive `\Nuwave\Lighthouse\Schema\Directives\ValidationDirective`. Your custom directive +class should be located in one of the configured default directive namespaces, e.g. `App\GraphQL\Directives`. + +```php + ['required'], + 'name' => ['sometimes', Rule::unique('users', 'name')->ignore($this->args['id'], 'id')], + ]; + } +} +``` + +Use it in your schema upon the field you want to validate. + +```graphql +type Mutation { + updateUser(id: ID, name: String): User @update @updateUserValidation +} +``` + +You can customize the messages for the given rules by implementing the `messages` function. + +```php + /** + * @return string[] + */ + public function messages(): array + { + return [ + 'name.unique' => 'The chosen username is not available', + ]; + } +``` diff --git a/docs/4.10/sidebar.js b/docs/4.10/sidebar.js new file mode 100644 index 0000000000..709fa4176e --- /dev/null +++ b/docs/4.10/sidebar.js @@ -0,0 +1,103 @@ +module.exports = [ + { + title: 'Getting Started', + children: [ + 'getting-started/installation', + 'getting-started/configuration', + 'getting-started/migrating-to-lighthouse' + ] + }, + { + title: 'The Basics', + children: [ + 'the-basics/schema', + 'the-basics/types', + 'the-basics/fields', + 'the-basics/directives', + ] + }, + { + title: 'Eloquent', + children: [ + ['eloquent/getting-started', 'Getting Started'], + 'eloquent/relationships', + 'eloquent/polymorphic-relationships', + 'eloquent/soft-deleting', + 'eloquent/nested-mutations', + 'eloquent/complex-where-conditions', + ] + }, + { + title: "Testing", + children: [ + 'testing/phpunit', + 'testing/extensions', + ], + }, + { + title: 'Subscriptions', + children: [ + ['subscriptions/getting-started', 'Getting Started'], + 'subscriptions/defining-fields', + 'subscriptions/trigger-subscriptions', + 'subscriptions/filtering-subscriptions', + 'subscriptions/client-implementations', + ] + }, + { + title: 'Digging Deeper', + children: [ + 'digging-deeper/schema-organisation', + 'digging-deeper/relay', + 'digging-deeper/error-handling', + 'digging-deeper/adding-types-programmatically', + 'digging-deeper/file-uploads', + 'digging-deeper/extending-lighthouse' + ] + }, + { + title: 'Custom Directives', + children: [ + ['custom-directives/getting-started', 'Getting Started'], + 'custom-directives/type-directives', + 'custom-directives/field-directives', + 'custom-directives/argument-directives', + ] + }, + { + title: 'Security', + children: [ + 'security/authentication', + 'security/authorization', + 'security/validation', + 'security/sanitization', + ['security/resource-exhaustion', 'Resource Exhaustion'], + ] + }, + { + title: 'Performance', + children: [ + 'performance/schema-caching', + ['performance/n-plus-one', 'The N+1 Query Problem'], + 'performance/deferred', + 'performance/tracing', + 'performance/server-configuration', + ] + }, + { + title: 'Concepts', + children: [ + 'concepts/arg-resolvers', + ] + }, + { + title: 'API Reference', + children: [ + 'api-reference/directives', + 'api-reference/resolvers', + 'api-reference/scalars', + 'api-reference/events', + 'api-reference/commands', + ] + }, +]; diff --git a/docs/4.10/subscriptions/client-implementations.md b/docs/4.10/subscriptions/client-implementations.md new file mode 100644 index 0000000000..44cdbbfd82 --- /dev/null +++ b/docs/4.10/subscriptions/client-implementations.md @@ -0,0 +1,198 @@ +# Client Implementations + +To get you up and running quickly, the following sections show how to use subcriptions +with common GraphQL client libraries. + +## Apollo + +To use Lighthouse subscriptions with the [Apollo](https://www.apollographql.com/docs/react/) +client library you will need to create an `apollo-link` + +```js +import { ApolloLink, Observable } from "apollo-link"; + +class PusherLink extends ApolloLink { + constructor(options) { + super(); + // Retain a handle to the Pusher client + this.pusher = options.pusher; + } + + request(operation, forward) { + return new Observable(observer => { + // Check the result of the operation + forward(operation).subscribe({ + next: data => { + // If the operation has the subscription extension, it's a subscription + const subscriptionChannel = this._getChannel( + data, + operation + ); + + if (subscriptionChannel) { + this._createSubscription(subscriptionChannel, observer); + } else { + // No subscription found in the response, pipe data through + observer.next(data); + observer.complete(); + } + } + }); + }); + } + + _getChannel(data, operation) { + return !!data.extensions && + !!data.extensions.lighthouse_subscriptions && + !!data.extensions.lighthouse_subscriptions.channels + ? data.extensions.lighthouse_subscriptions.channels[ + operation.operationName + ] + : null; + } + + _createSubscription(subscriptionChannel, observer) { + const pusherChannel = this.pusher.subscribe(subscriptionChannel); + // Subscribe for more update + pusherChannel.bind("lighthouse-subscription", payload => { + if (!payload.more) { + // This is the end, the server says to unsubscribe + this.pusher.unsubscribe(subscriptionChannel); + observer.complete(); + } + const result = payload.result; + if (result) { + // Send the new response to listeners + observer.next(result); + } + }); + } +} + +export default PusherLink; +``` + +Then initialize the pusher client and use it in the link stack. + +```js +const pusherLink = new PusherLink({ + pusher: new Pusher(PUSHER_API_KEY, { + cluster: PUSHER_CLUSTER, + authEndpoint: `${API_LOCATION}/graphql/subscriptions/auth`, + auth: { + headers: { + authorization: BEARER_TOKEN + } + } + }) +}); + +const link = ApolloLink.from([pusherLink, httpLink(`${API_LOCATION}/graphql`)]); +``` + +## Relay Modern + +To use Lighthouse's subscriptions with Relay Modern you will +need to create a custom handler and inject it into Relay's environment. + +```js +import Pusher from "pusher-js"; +import { Environment, Network, Observable, RecordSource, Store } from "relay-runtime"; + +const pusherClient = new Pusher(PUSHER_API_KEY, { + cluster: "us2", + authEndpoint: `${API_LOCATION}/graphql/subscriptions/auth`, + auth: { + headers: { + authorization: BEARER_TOKEN + } + } +}); + +const createHandler = options => { + let channelName; + const { pusher, fetchOperation } = options; + + return (operation, variables, cacheConfig) => { + return Observable.create(sink => { + fetchOperation(operation, variables, cacheConfig) + .then(response => { + return response.json(); + }) + .then(json => { + channelName = + !!response.extensions && + !!response.extensions.lighthouse_subscriptions && + !!response.extensions.lighthouse_subscriptions.channels + ? response.extensions.lighthouse_subscriptions.channels[ + operation.name + ] + : null; + + if (!channelName) { + return + } + + const channel = pusherClient.subscribe(channelName) + + channel.bind(`lighthouse-subscription`, payload => { + const result = payload.result + + if (result && result.errors) { + sink.error(result.errors) + } else if (result) { + sink.next({ + data: result.data + }) + } + + if (!payload.more) { + sink.complete() + } + }) + }) + }).finally(() => { + pusherClient.unsubscribe(channelName) + }) + }; +}; + +const fetchOperation = (operation, variables, cacheConfig) => { + const bodyValues = { + variables, + query: operation.text, + operationName: operation.name + }; + + return fetch(`${API_LOCATION}/graphql`, { + method: "POST", + opts: { + credentials: "include" + }, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: BEARER_TOKEN + }, + body: JSON.stringify(bodyValues) + }); +}; + +const fetchQuery = (operation, variables, cacheConfig) => { + return fetchOperation(operation, variables, cacheConfig).then(response => { + return response.json(); + }); +}; + +const subscriptionHandler = createHandler({ + pusher: pusherClient, + fetchOperation: fetchOperation +}); + +const network = Network.create(fetchQuery, subscriptionHandler); + +export const environment = new Environment({ + network, + store: new Store(new RecordSource) +}); +``` diff --git a/docs/4.10/subscriptions/defining-fields.md b/docs/4.10/subscriptions/defining-fields.md new file mode 100644 index 0000000000..cebd2a7db8 --- /dev/null +++ b/docs/4.10/subscriptions/defining-fields.md @@ -0,0 +1,124 @@ +# Defining Fields + +Define your subscriptions as field on the root `Subscription` type in your schema. + +```graphql +type Subscription { + postUpdated(author: ID): Post +} +``` + +The quickest way to define such a field is through the `artisan` generator command: + + php artisan lighthouse:subscription PostUpdated + +Lighthouse will look for a class with the capitalized name of the field that +is defined within the default subscription namespace. +For example, the field `postUpdated` should have a corresponding class at +`App\GraphQL\Subscriptions\PostUpdated`. + +All subscription field classes **must** implement the abstract class +`Nuwave\Lighthouse\Schema\Types\GraphQLSubscription` and implement two methods: +`authorize` and `filter`. + +```php +context->user; + $author = User::find($subscriber->args['author']); + + return $user->can('viewPosts', $author); + } + + /** + * Filter which subscribers should receive the subscription. + * + * @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber + * @param mixed $root + * @return bool + */ + public function filter(Subscriber $subscriber, $root): bool + { + $user = $subscriber->context->user; + + // Don't broadcast the subscription to the same + // person who updated the post. + return $root->updated_by !== $user->id; + } + + /** + * Encode topic name. + * + * @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber + * @param string $fieldName + * @return string + */ + public function encodeTopic(Subscriber $subscriber, string $fieldName): string + { + // Optionally create a unique topic name based on the + // `author` argument. + $args = $subscriber->args; + + return Str::snake($fieldName).':'.$args['author']; + } + + /** + * Decode topic name. + * + * @param string $fieldName + * @param \App\Post $root + * @return string + */ + public function decodeTopic(string $fieldName, $root): string + { + // Decode the topic name if the `encodeTopic` has been overwritten. + $author_id = $root->author_id; + + return Str::snake($fieldName).':'.$author_id; + } + + /** + * Resolve the subscription. + * + * @param \App\Post $root + * @param mixed[] $args + * @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context + * @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo + * @return mixed + */ + public function resolve($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Post + { + // Optionally manipulate the `$root` item before it gets broadcasted to + // subscribed client(s). + $root->load(['author', 'author.achievements']); + + return $root; + } +} +``` + +If the default namespaces are not working with your application structure +or you want to be more explicit, you can use the [`@subscription`](../api-reference/directives.md#subscription) +directive to point to a different class. \ No newline at end of file diff --git a/docs/4.10/subscriptions/filtering-subscriptions.md b/docs/4.10/subscriptions/filtering-subscriptions.md new file mode 100644 index 0000000000..56801694fb --- /dev/null +++ b/docs/4.10/subscriptions/filtering-subscriptions.md @@ -0,0 +1,42 @@ +# Filtering Subscriptions + +There are times when you'll need to filter out specific events based on the arguments provided by the client. To handle this, you can return a true/false from the `filter` function to indicate whether the client should receive the subscription. For instance, using the following example: + +```graphql +subscription onPostUpdated($post_id: ID!) { + postUpdated(post_id: $post_id) { + id + title + content + } +} +``` + +To ensure only clients who are subscribed to a certain `post_id` receive an update, we can create a `filter`: + +```php +namespace App\GraphQL\Subscriptions; + +use Nuwave\Lighthouse\Schema\Subscriptions\Subscriber; +use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription; + +class PostUpdatedSubscription extends GraphQLSubscription +{ + /** + * Filter which subscribers should receive the subscription. + * + * @param \Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber + * @param mixed $root + * @return bool + */ + public function filter(Subscriber $subscriber, $root): bool + { + // Clients arguments when subscribing + $args = $subscriber->args; + + // Ensure that the Post ($root) id matches + // the requested `post_id` + return $root->id == $args['post_id']; + } +} +``` \ No newline at end of file diff --git a/docs/4.10/subscriptions/getting-started.md b/docs/4.10/subscriptions/getting-started.md new file mode 100644 index 0000000000..01ce89d568 --- /dev/null +++ b/docs/4.10/subscriptions/getting-started.md @@ -0,0 +1,39 @@ +# Subscriptions: Getting Started + +Subscriptions allow GraphQL clients to observe specific events +and receive updates from the server when those events occur. + +::: tip NOTE +Much of the credit should be given to the [Ruby implementation](https://github.com/rmosolgo/graphql-ruby/blob/master/guides/subscriptions/overview.md) as they provided a great overview of how the backend should work. +::: + +## Setup + +Install the [Pusher PHP Library](https://github.com/pusher/pusher-http-php) for interacting with the Pusher HTTP API. + + composer require pusher/pusher-php-server + +Add the service provider to your `config/app.php` + +```php +'providers' => [ + \Nuwave\Lighthouse\Subscriptions\SubscriptionServiceProvider::class, +], +``` + +### Pusher Webhook + +Subscriptions do not expire by themselves. +Unless a subscription is deleted, it will continue to broadcast events after the client has disconnected. + +Using a `Presence` webhook will mitigate this problem. +When a Pusher channel is abandoned (ie. unsubscribed), it will trigger the webhook, +which will instruct Lighthouse to delete the subscription. + +The webhook URL will typically be: + +``` +/graphql/subscriptions/webhook +``` + +You can add the webhook in the Pusher Dashboard. Select the type `Presence`. diff --git a/docs/4.10/subscriptions/trigger-subscriptions.md b/docs/4.10/subscriptions/trigger-subscriptions.md new file mode 100644 index 0000000000..f21270a89a --- /dev/null +++ b/docs/4.10/subscriptions/trigger-subscriptions.md @@ -0,0 +1,40 @@ +# Trigger Subscriptions + +Now that clients can subscribe to a field, you will need to notify Lighthouse +when the underlying data has changed. + +## Broadcast Directive + +The [`@broadcast`](../api-reference/directives.md#broadcast) +directive will broadcast all updates to the `Post` model to the `postUpdated` subscription. + +```graphql +type Mutation { + updatePost(input: UpdatePostInput!): Post + @broadcast(subscription: "postUpdated") +} +``` + +You can reference the same subscription from multiple fields, or vice-versa +trigger multiple subscriptions from a single field. + +## Fire Subscriptions From Code + +The `Subscription` class offers a utility method `broadcast` +that can be used to broadcast subscriptions from anywhere in your application. + +It accepts three parameters: + +- `string $subscriptionField` The name of the subscription field you want to trigger +- `mixed $root` The result object you want to pass through +- `bool $shouldQueue = null` Optional, overrides the default configuration `lighthouse.subscriptions.queue_broadcasts` + +The following example shows how to trigger a subscription after an update +to the `Post` model. + +```php +$post->title = $newTitle; +$post->save(); + +\Nuwave\Lighthouse\Execution\Utils\Subscription::broadcast('postUpdated', $post); +``` \ No newline at end of file diff --git a/docs/4.10/testing/extensions.md b/docs/4.10/testing/extensions.md new file mode 100644 index 0000000000..e6479f844f --- /dev/null +++ b/docs/4.10/testing/extensions.md @@ -0,0 +1,199 @@ +# Testing Lighthouse extensions + +When you extend Lighthouse with custom functionality, it is a great idea to test +your extensions in isolation from the rest of your application. + +## Use a test schema + +When you enhance functionality related to the schema definition, such as adding +a [custom directive](../custom-directives), you need a test schema where you can use it. +Add the `UsesTestSchema` trait to your test class, call `setUpTestSchema()` and define your test schema: + +```php +setUpTestSchema();; + } + + public function testSpecificScenario(): void + { + // You can overwrite the schema for testing specific cases + $this->schema = /** @lang GraphQL */ ' + type Query { + foo(bar: String @myCustom): Int + } + '; + + // ... + } +} +``` + +## Mock resolvers + +When testing custom functionality through a dummy schema, you still need to have +a way to resolve fields. Add the `MocksResolvers` trait to your test class: + +```php + 'oof'. +""" +directive @revert on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + +The simplest way to mock a resolver is to have it return static data: + +```php +public function testReverseField(): void +{ + $this->mockResolver('foo'); + + $this->schema = /** @lang GraphQL */ ' + type Query { + foo: String @reverse @mock + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + foo + } + ')->assertExactJson([ + 'data' => [ + 'foo' => 'oof', + ], + ]); +} +``` + +Since we get back an instance of PHPUnit's `InvocationMocker`, we can also assert +that our resolver is called with certain values. Note that we are not passing an +explicit resolver function here. The default resolver will simply return `null`. + +```php +public function testReverseInput(): void +{ + $this->mockResolver() + ->with(null, ['bar' => 'rab']); + + $this->schema = /** @lang GraphQL */ ' + type Query { + foo(bar: String @reverse): String @mock + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + foo(bar: "bar") + } + ')->assertExactJson([ + 'data' => [ + 'foo' => null, + ], + ]); +} +``` + +If you have to handle the incoming resolver arguments dynamically, you can also +pass a function that is called: + +```php +public function testReverseInput(): void +{ + $this->mockResolver(function($root, array $args): string { + return $args['bar']; + }); + + $this->schema = /** @lang GraphQL */ ' + type Query { + foo(bar: String @reverse): String @mock + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + foo(bar: "bar") + } + ')->assertExactJson([ + 'data' => [ + 'foo' => 'rab', + ], + ]); +} +``` + +You might have a need to add multiple resolvers to a single schema. For that case, +specify a unique `key` for the mock resolver (it defaults to `default`): + +```php +public function testMultipleResolvers(): void +{ + $this->mockResolver(..., 'first'); + $this->mockResolver(..., 'second'); + + $this->schema = /** @lang GraphQL */ ' + type Query { + foo: Int @mock(key: "first") + bar: ID @mock(key: "second") + } + '; +} +``` + +By default, the resolver from `mockResolver` expects to be called at least once. +If you want to set a different expectation, you can use `mockResolverExpects`: + +```php +public function testAbortsBeforeResolver(): void +{ + $this->mockResolverExpects( + $this->never() + ); + + $this->schema = /** @lang GraphQL */ ' + type Query { + foo: Int @someValidationThatFails @mock + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + foo + } + '); +} +``` diff --git a/docs/4.10/testing/phpunit.md b/docs/4.10/testing/phpunit.md new file mode 100644 index 0000000000..5b96ab2be3 --- /dev/null +++ b/docs/4.10/testing/phpunit.md @@ -0,0 +1,261 @@ +# Testing with PHPUnit + +Lighthouse makes it easy to add automated tests through PHPUnit. + +## Setup + +Lighthouse offers some useful test helpers that make it easy to call your API +from within a PHPUnit test. Just add the `MakesGraphQLRequests` trait to your test class. + +```diff +graphQL(/** @lang GraphQL */ ' + { + posts { + id + title + } + } + '); +} +``` + +If you want to use variables within your query, pass an associative array as the second argument: + +```php +public function testCreatePost(): void +{ + $response = $this->graphQL(/** @lang GraphQL */ ' + mutation CreatePost($title: String!) { + createPost(title: $title) { + id + } + } + ', [ + 'title' => 'Automatic testing proven to reduce stress levels in developers' + ]); +} +``` + +## Assertions + +Now that we know how to query our server in tests, we need to make sure the +returned results match our expectations. + +The returned `TestResponse` conveniently offers assertions that work quite +well with the JSON data returned by GraphQL. + +The `assertJson` method asserts that the response is a superset of the given JSON. + +```php +public function testQueriesPosts(): void +{ + $post = factory(Post::class)->create(); + + $this->graphQL(/** @lang GraphQL */ ' + { + posts { + id + title + } + } + ')->assertJson([ + 'data' => [ + 'posts' => [ + [ + 'id' => $post->id, + 'title' => $post->title, + ] + ] + ] + ]); +} +``` + +You can also extract data from the response and use it within any assertion. + +```php +public function testOrdersUsersByName(): void +{ + factory(User::class)->create(['name' => 'Oliver']); + factory(User::class)->create(['name' => 'Chris']); + factory(User::class)->create(['name' => 'Benedikt']); + + $response = $this->graphQL(/** @lang GraphQL */ ' + { + users(orderBy: "name") { + name + } + } + '); + + $names = $response->json("data.*.name"); + + $this->assertSame( + [ + 'Benedikt' + 'Chris', + 'Oliver', + ], + $names + ); +} +``` + +## Simulating File Uploads + +Lighthouse allows you to [upload files](../digging-deeper/file-uploads.md) through GraphQL. + +Since multipart form requests are tricky to construct, you can just use the `multipartGraphQL` +helper method. + +```php +$this->multipartGraphQL( + [ + 'operations' => /** @lang JSON */ + ' + { + "query": "mutation Upload($file: Upload!) { upload(file: $file) }", + "variables": { + "file": null + } + } + ', + 'map' => /** @lang JSON */ + ' + { + "0": ["variables.file"] + } + ', + ], + [ + '0' => UploadedFile::fake()->create('image.jpg', 500), + ] +) +``` + +## Introspection + +If you create or manipulate parts of your schema programmatically, you might +want to test that. You can use introspection to query your final schema in tests. + +Lighthouse uses the introspection query from [`\GraphQL\Type\Introspection::getIntrospectionQuery()`](https://github.com/webonyx/graphql-php/blob/master/src/Type/Introspection.php). + +The `introspect()` helper method runs the full introspection query against your schema. + +```php +$introspectionResult = $this->introspect(); +``` + +Most often, you will want to look for a specific named type. + +```php +$generatedType = $this->introspectType('Generated'); +// Ensure the type is present and matches a certain definition +$this->assertSame( + [], // Adjust accordingly + $generatedType +); +``` + +You can also introspect client directives. + +```php +$customDirective = $this->introspectDirective('custom'); +``` + +## Defer + +When sending requests with field containing `@defer`, use the `streamGraphQL()` helper. +It automatically captures the full streamed response and provides you the returned chunks. + +```php +$chunks = $this->streamGraphQL(/** @lang GraphQL */ ' +{ + now + later @defer +} +'); + +$this->assertSame( + [ + [ + 'data' => [ + 'now' => 'some value', + 'later' => null, + ], + ], + [ + 'later' => [ + 'data' => 'another value', + ], + ], + ], + $chunks +); +``` + +You can also set up the in-memory stream manually: + +```php +$this->setUpDeferStream(); +``` + +## Lumen + +Because the `TestResponse` class is not available in Lumen, you must use a different +test trait: + +```diff +graphQL(/** @lang GraphQL */ ' + { + hello + } + ')->seeJson([ + 'data' => [ + 'hello' => 'world' + ] + ])->seeHeader('SomeHeader', 'value'); +} +``` diff --git a/docs/4.10/the-basics/directives.md b/docs/4.10/the-basics/directives.md new file mode 100644 index 0000000000..6bac5ee4da --- /dev/null +++ b/docs/4.10/the-basics/directives.md @@ -0,0 +1,67 @@ +# Directives + +Assuming you read through the previous chapters, you should be familiar with the basics +of schema definition by now. + +You might have seen some funky extra bits in the schema definitions such as `@paginate`, +`@rules` or `@hasMany`. Those are called *directives* and are the primary way +to add functionality to your GraphQL schema. + +## Definition + +Directives always begin with an `@` symbol, followed by a unique name. They may be used +at specified parts of the GraphQL schema. + +This example directive `@upperCase` may be used on field definitions to UPPERCASE the result. + +```graphql +directive @upperCase on FIELD_DEFINITION + +type Query { + hello: String @upperCase +} +``` + +Directives may also define arguments to enable a more flexible use, and they can +be used in multiple places, depending on the [specified directive location](https://facebook.github.io/graphql/June2018/#DirectiveLocation). + +```graphql +directive @append(text: String) on FIELD_DEFINITION | ARGUMENT_DEFINITION + +type Query { + sayFriendly: String @append(text: ", please.") + shout(phrase: String @append(text: "!")): String +} +``` + +## Usage + +Lighthouse provides a plethora of built-in schema directives that are ready to +be consumed and can simply be used from within the schema. + +The following example is quite dense, but it should give you an idea of what +directives are capable of. + +```graphql +type Query { + "Return a list of posts" + posts( + "Place an exact match filter (=) on the data" + postedAt: Date @eq + "Show only posts that match one of the given topics" + topics: [String!] @in(key: "topic") + "Search by title" + title: String @where(operator: "%LIKE%") + ): [Post!]! + # Resolve as a paginated list + @paginate + # Require authentication + @guard(with: "api") +} +``` + +Explore the docs to find out more or look into the [directives API reference](../api-reference/directives.md) +for a complete list of all available directives. + +Implementing your own directives is a great way to add reusable functionality to your schema, +learn how you can [implement your own directives](../custom-directives/getting-started.md). diff --git a/docs/4.10/the-basics/fields.md b/docs/4.10/the-basics/fields.md new file mode 100644 index 0000000000..c038dd24a0 --- /dev/null +++ b/docs/4.10/the-basics/fields.md @@ -0,0 +1,225 @@ +# Fields + +The entrypoints to any GraphQL API are the fields of the root types `Query`, `Mutation` and `Subscription`. + +*Every* field has a function associated with it that is called when the field +is requested as part of a query. This function is called a **resolver**. + +## Hello World + +As is the tradition of our people, this section will teach you how to say "hello world!" through Lighthouse. + +We start out by defining the simplest possible schema: The root `Query` type +with a single field called `hello` that returns a `String`. + +```graphql +type Query { + hello: String! +} +``` + +This defines the shape of our data and informs the client what they can expect. +You need to implement the actual resolver next. + +By default, Lighthouse looks for a class with the capitalized name of the field in `App\GraphQL\Queries` +or `App\GraphQL\Mutations` and calls its `__invoke` function with [the usual resolver arguments](../api-reference/resolvers.md#resolver-function-signature). + +In this case, our field is a query and is called `hello`, so we need to define our class as follows: + +```php +id; +} +``` + +Writing out each such resolver would be pretty repetitive. +We can utilize the fourth and final resolver argument `ResolveInfo`, which will give us access +to the requested field name, to dynamically access the matching property. + +```php +{$resolveInfo->fieldName}; +} +``` + +Fortunately, the underlying GraphQL implementation already provides [a sensible default resolver](http://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver), +that plays quite nicely with the data you would typically return from +a root resolver, e.g. `Eloquent` models or associative arrays. + +This means that in most cases, you will only have to provide resolvers for the +root fields and make sure they return data in the proper shape. + +If you need to implement custom resolvers for fields that are not on one of the +root types `Query` or `Mutation`, you can use either the +[@field](../api-reference/directives.md#field) or [@method](../api-reference/directives.md#method) directive. + +You may also [change the default resolver](../digging-deeper/extending-lighthouse.md#changing-the-default-resolver) if you need. diff --git a/docs/4.10/the-basics/schema.md b/docs/4.10/the-basics/schema.md new file mode 100644 index 0000000000..17670a640e --- /dev/null +++ b/docs/4.10/the-basics/schema.md @@ -0,0 +1,62 @@ +# Schema + +A schema defines the capabilities of a GraphQL server. +Much like a database schema, it describes the structure and the types your API can return. + +## Types + +Types are the primary building blocks of a GraphQL schema. +They define the capabilities of your API and the kind of data you can get from it. + +```graphql +type User { + id: ID! + name: String! + email: String! + created_at: String! + updated_at: String +} +``` + +## The Root Types + +There can be up to 3 special *root types* in a GraphQL schema. +They define the root fields that a query may have. While they are +all [Object Types](types.md#object-type), they differ in functionality. + +### Query + +Every GraphQL schema must have a `Query` type which contains the queries your API offers. +Think of queries as REST resources which can take arguments and return a fixed result. + +```graphql +type Query { + me: User + users: [User!]! + userById(id: ID): User +} +``` + +### Mutation + +In contrast to the `Query` type, the fields of the `Mutation` type are +allowed to change data on the server. + +```graphql +type Mutation { + createUser(name: String!, email: String!, password: String!): User + updateUser(id: ID, email: String, password: String): User + deleteUser(id: ID): User +} +``` + +### Subscription + +Rather than providing a single response, the fields of the `Subscription` type +return a stream of responses, with real-time updates. + +```graphql +type Subscription { + newUser: User +} +``` diff --git a/docs/4.10/the-basics/types.md b/docs/4.10/the-basics/types.md new file mode 100644 index 0000000000..ed91b3b880 --- /dev/null +++ b/docs/4.10/the-basics/types.md @@ -0,0 +1,324 @@ +# Types + +A GraphQL schema is made out of types. This section describes the different set of types +and how they can be defined to work with Lighthouse. For a more in-depth reference about types, +look into the [GraphQL documentation](https://graphql.org/learn/schema/) + +## Object Type + +Object types define the resources of your API and are closely related to Eloquent models. +They must have a unique name and contain a set of fields. + +```graphql +type User { + id: ID! + name: String! + email: String! + created_at: String! + updated_at: String +} + +type Query { + users: [User!]! + user(id: ID!): User +} +``` + +## Scalar + +Scalar types are the most basic elements of a GraphQL schema. There are a +few built in scalars, such as `String` or `Int`. + +Lighthouse provides some scalars that work well with Laravel out of the box, +read about them in the [API reference for scalars](../api-reference/scalars.md). + +Define your own scalar types by running `php artisan lighthouse:scalar ` +and including it in your schema. Lighthouse will look for Scalar types in a configurable +default namespace. + +```graphql +scalar ZipCode + +type User { + zipCode: ZipCode +} +``` + +You can also use third-party scalars, such as those provided by [mll-lab/graphql-php-scalars](https://github.com/mll-lab/graphql-php-scalars). +Just `composer require` your package of choice and add a scalar definition to your schema. +Use the [@scalar](../api-reference/directives.md#scalar) directive to point to any fully qualified class name: + +```graphql +scalar Email @scalar(class: "MLL\\GraphQLScalars\\Email") +``` + +[Learn how to implement your own scalar.](https://webonyx.github.io/graphql-php/type-system/scalar-types/) + +## Enum + +Enums are types with a restricted set of values (similar to `enum` found in database migrations). +They are defined as a list of `UPPERCASE` string keys. + +### Schema definition + +You can define the actual values through the [@enum](../api-reference/directives.md#enum) directive. + +```graphql +enum EmploymentStatus { + INTERN @enum(value: 0) + EMPLOYEE @enum(value: 1) + TERMINATED @enum(value: 2) +} +``` + +Now we can use the enum as part of our schema. + +```graphql +type Employee { + id: ID! + name: String + status: EmploymentStatus! +} + +type Query { + employees: [Employee!]! @all +} +``` + +In this example, the underlying values are actually integers. When the models are retrieved from +the database, the mapping is applied and the integers are converted to the defined string keys. + +```php +return [ + ['name' => 'Hans', 'status' => 0], + ['name' => 'Pamela', 'status' => 1], + ['name' => 'Gerhard', 'status' => 2], +]; +``` + +Queries now return meaningful names instead of magic numbers. + +```graphql +{ + employees { + name + status + } +} +``` + +```json +{ + "data": { + "employees": [ + {"name": "Hans", "status": "INTERN"}, + {"name": "Pamela", "status": "EMPLOYEE"}, + {"name": "Gerhard", "status": "TERMINATED"} + ] + } +} +``` + +If the internal value of the enum is the same as the field name, `@enum` can be omitted: + +```graphql +enum Role { + ADMIN +} +``` + +The PHP internal value of the field `ADMIN` will be `string('ADMIN')`. + +### Native PHP definition + +If you want to reuse enum definitions or constants from PHP, you can also +register a native PHP enum type [through the TypeRegistry](../digging-deeper/adding-types-programmatically.md#native-php-types). + +Just define a [EnumType](http://webonyx.github.io/graphql-php/type-system/enum-types/) and +register it: + +```php +use GraphQL\Type\Definition\EnumType; +use Nuwave\Lighthouse\Schema\TypeRegistry; + +$episodeEnum = new EnumType([ + 'name' => 'Episode', + 'description' => 'One of the films in the Star Wars Trilogy', + 'values' => [ + 'NEWHOPE' => [ + 'value' => 4, + 'description' => 'Released in 1977.' + ], + 'EMPIRE' => [ + 'value' => 5, + 'description' => 'Released in 1980.' + ], + 'JEDI' => [ + 'value' => 6, + 'description' => 'Released in 1983.' + ], + ] +]); + +// Resolve this through the container, as it is a singleton +$typeRegistry = app(TypeRegistry::class); + +$typeRegistry->register($episodeEnum); +``` + +If you are using [BenSampo/laravel-enum](https://github.com/BenSampo/laravel-enum) +you can use `Nuwave\Lighthouse\Schema\Types\LaravelEnumType` to construct an enum type from it. + +Given the following enum: + +```php +register( + new LaravelEnumType(UserType::class) + ); + } +} +``` + +By default, the generated type will be named just like the given class. + +```php +$enum = new LaravelEnumType(UserType::class); +var_dump($enum->name); // UserType +``` + +You may overwrite the name if the default does not fit or you have a name conflict. + +```php +$enum = new LaravelEnumType(UserType::class, 'UserKind'); +var_dump($enum->name); // UserKind +``` + +## Input + +Input types can be used to describe complex objects for field arguments. +Beware that while they look similar to Object Types, they behave differently: +The fields of an Input Type are treated similar to arguments. + +```graphql +input CreateUserInput { + name: String! + email: String +} + +type User { + id: ID! + name: String! + email: String +} + +type Mutation { + createUser(input: CreateUserInput! @spread): User @create +} +``` + +## Interface + +The GraphQL `interface` type is similar to a PHP `Interface`. +It defines a set of common fields that all implementing types must also provide. +A common use-case for interfaces with a Laravel project would be polymorphic relationships. + +```graphql +interface Named { + name: String! +} +``` + +Object types can implement that interface, given that they provide all its fields. + +```graphql +type User implements Named { + id: ID! + name: String! +} +``` + +The following definition would be invalid. + +```graphql +type User implements Named { + id: ID! +} +``` + +Interfaces need a way of determining which concrete Object Type is returned by a +particular query. Lighthouse provides a default type resolver that works by calling +`class_basename($value)` on the value returned by the resolver. + +You can also provide a custom type resolver. Run `php artisan lighthouse:interface ` to create +a custom interface class. It is automatically put in the default namespace where Lighthouse can discover it by itself. + +Read more about them in the [GraphQL Reference](https://graphql.org/learn/schema/#interfaces) and the +[docs for graphql-php](http://webonyx.github.io/graphql-php/type-system/interfaces/) + +## Union + +A Union is an abstract type that simply enumerates other Object Types. +They are similar to interfaces in that they can return different types, but they can not +have fields defined. + +```graphql +union Person + = User + | Employee + +type User { + id: ID! +} + +type Employee { + employeeId: ID! +} +``` + +Just like Interfaces, you need a way to determine the concrete Object Type for a Union, +based on the resolved value. If the default type resolver does not work for you, define your +own using `php artisan lighthouse:union `. +It is automatically put in the default namespace where Lighthouse can discover it by itself. + +Read more about them in the [GraphQL Reference](https://graphql.org/learn/schema/#union-types) and the +[docs for graphql-php](http://webonyx.github.io/graphql-php/type-system/unions/) From 6c41ac1ca83bca282b6562c812614b0e40108af3 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Fri, 6 Mar 2020 08:16:43 +0100 Subject: [PATCH 22/31] Fix Lumen version check (#1224) --- CHANGELOG.md | 6 ++++++ src/Support/AppVersion.php | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb61f7e98..6da642a0ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +## 4.10.1 + +### Fixed + +- Fix Laravel version detection for Lumen https://github.com/nuwave/lighthouse/pull/1224 + ## 4.10.0 ### Added diff --git a/src/Support/AppVersion.php b/src/Support/AppVersion.php index 71cb17f6bd..e198e0d3b7 100644 --- a/src/Support/AppVersion.php +++ b/src/Support/AppVersion.php @@ -30,7 +30,7 @@ protected static function versionNumber(): float { if (self::isLumen()) { // Lumen version strings look like: "Lumen (2.3.4)..." - return (float) Str::after('(', self::version()); + return (float) Str::after(self::version(), '('); } // Regular Laravel versions look like: "2.3.4" From 78d97203fbe3ae5413f8b36f94c875ddcb9d0a56 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Mon, 9 Mar 2020 13:30:41 +0100 Subject: [PATCH 23/31] Test status 200 on error (#1228) --- tests/Integration/GraphQLTest.php | 3 +- .../Schema/Directives/FindDirectiveTest.php | 38 +++++++++---------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/tests/Integration/GraphQLTest.php b/tests/Integration/GraphQLTest.php index 84eeccf490..ee9a7d5e07 100644 --- a/tests/Integration/GraphQLTest.php +++ b/tests/Integration/GraphQLTest.php @@ -153,7 +153,8 @@ public function testHandlesErrorInResolver(): void 'message' => $message, ], ], - ]); + ]) + ->assertStatus(200); } public function testIgnoresInvalidJSONVariables(): void diff --git a/tests/Integration/Schema/Directives/FindDirectiveTest.php b/tests/Integration/Schema/Directives/FindDirectiveTest.php index 75c3de032a..0ed22c588b 100644 --- a/tests/Integration/Schema/Directives/FindDirectiveTest.php +++ b/tests/Integration/Schema/Directives/FindDirectiveTest.php @@ -14,18 +14,18 @@ public function testReturnsSingleUser(): void $userB = factory(User::class)->create(['name' => 'B']); $userC = factory(User::class)->create(['name' => 'C']); - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! } - + type Query { user(id: ID @eq): User @find(model: "User") } '; - $this->graphQL(" + $this->graphQL(/** @lang GraphQL */ " { user(id:{$userB->id}) { name @@ -43,18 +43,18 @@ public function testDefaultsToFieldTypeIfNoModelIsSupplied(): void $userA = factory(User::class)->create(['name' => 'A']); $userB = factory(User::class)->create(['name' => 'B']); - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! } - + type Query { user(id: ID @eq): User @find } '; - $this->graphQL(" + $this->graphQL(/** @lang GraphQL */ " { user(id:{$userA->id}) { name @@ -71,18 +71,18 @@ public function testCannotFetchIfMultipleModelsMatch(): void factory(User::class)->create(['name' => 'A']); factory(User::class)->create(['name' => 'B']); - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! } - + type Query { user(name: String @eq): User @find(model: "User") } '; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { user(name: "A") { name @@ -99,22 +99,22 @@ public function testCanUseScopes(): void $userB = factory(User::class)->create(['name' => 'A', 'company_id' => $companyB->id]); $userC = factory(User::class)->create(['name' => 'B', 'company_id' => $companyA->id]); - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type Company { name: String! } - + type User { id: ID! name: String! } - + type Query { user(name: String @eq, company: String!): User @find(model: "User" scopes: [companyName]) } '; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { user(name: "A" company: "CompanyA") { id @@ -133,29 +133,29 @@ public function testCanUseScopes(): void public function testReturnsAnEmptyObjectWhenTheModelIsNotFound(): void { - $this->schema = ' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! name: String! } - + type Query { user(name: String @eq): User @find(model: "User") } '; - $this->graphQL(' + $this->graphQL(/** @lang GraphQL */ ' { user(name: "A") { id name } } - ')->assertJson([ + ')->assertExactJson([ 'data' => [ 'user' => null, ], - ])->assertStatus(200); + ]); } public function testReturnsCustomAttributes(): void @@ -172,7 +172,7 @@ public function testReturnsCustomAttributes(): void name: String! companyName: String! } - + type Query { user(id: ID @eq): User @find(model: "User") } From cc1d0e2f6181f307bd76362d47bbf3d44a1f0eec Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Mon, 9 Mar 2020 13:31:06 +0100 Subject: [PATCH 24/31] Reenable CI for bensampo/laravel-enum (#1227) --- .github/workflows/continuous-integration.yml | 20 ------------------- phpstan.neon | 8 +------- .../Schema/Types/LaravelEnumTypeDBTest.php | 4 ---- .../Unit/Schema/Types/LaravelEnumTypeTest.php | 5 ----- 4 files changed, 1 insertion(+), 36 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 817421b287..ed043598b2 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -42,10 +42,6 @@ jobs: key: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}" restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}-" - - name: "Remove bensampo/laravel-enum for incompatible versions" - if: "matrix.laravel-version == '^7.0'" - run: "composer remove --dev bensampo/laravel-enum --no-update" - - name: "Install dependencies with composer" run: "composer require illuminate/contracts:${{ matrix.laravel-version }} --no-interaction --prefer-dist --no-interaction --no-suggest" @@ -94,10 +90,6 @@ jobs: key: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}" restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}-" - - name: "Remove bensampo/laravel-enum for incompatible versions" - if: "matrix.laravel-version == '^7.0'" - run: "composer remove --dev bensampo/laravel-enum --no-update" - - name: "Install dependencies with composer" run: "composer require illuminate/contracts:${{ matrix.laravel-version }} --no-interaction --prefer-dist --no-interaction --no-suggest" @@ -153,10 +145,6 @@ jobs: if: "matrix.laravel-version != '^7.0'" run: "composer remove --dev nunomaduro/larastan --no-update" - - name: "Remove bensampo/laravel-enum for incompatible versions" - if: "matrix.laravel-version == '^7.0'" - run: "composer remove --dev bensampo/laravel-enum --no-update" - - name: "Install dependencies with composer" run: "composer require illuminate/contracts:${{ matrix.laravel-version }} --no-interaction --prefer-dist --no-interaction --no-suggest" @@ -196,10 +184,6 @@ jobs: key: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}" restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}-" - - name: "Remove bensampo/laravel-enum for incompatible versions" - if: "matrix.laravel-version == '^7.0'" - run: "composer remove --dev bensampo/laravel-enum --no-update" - - name: "Install dependencies with composer" run: "composer require illuminate/contracts:${{ matrix.laravel-version }} --no-interaction --prefer-dist --no-interaction --no-suggest" @@ -244,10 +228,6 @@ jobs: key: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}" restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.laravel-version }}-" - - name: "Remove bensampo/laravel-enum for incompatible versions" - if: "matrix.laravel-version == '^7.0'" - run: "composer remove --dev bensampo/laravel-enum --no-update" - - name: "Install dependencies with composer" run: "composer require illuminate/contracts:${{ matrix.laravel-version }} --no-interaction --prefer-dist --no-interaction --no-suggest" diff --git a/phpstan.neon b/phpstan.neon index 06dffa1428..4fc6727fe3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,6 @@ includes: - vendor/nunomaduro/larastan/extension.neon -#- vendor/bensampo/laravel-enum/extension.neon +- vendor/bensampo/laravel-enum/extension.neon parameters: level: 1 paths: @@ -13,12 +13,6 @@ parameters: - %rootDir%/../../../tests/TestCase.php - %rootDir%/../../../tests/Laravel7ExceptionHandler.php - %rootDir%/../../../tests/PreLaravel7ExceptionHandler.php - # Workaround until bensampo/laravel-enum support Laravel 7 - - %rootDir%/../../../src/Schema/Types/LaravelEnumType.php - - %rootDir%/../../../tests/Unit/Schema/Types/LaravelEnumTypeTest.php - - %rootDir%/../../../tests/Integration/Schema/Types/LaravelEnumTypeDBTest.php - - %rootDir%/../../../tests/Utils/Models/WithEnum.php - - %rootDir%/../../../tests/Utils/LaravelEnums/* ignoreErrors: # Some parts of Laravel are just really magical - '#Call to an undefined static method Illuminate\\Support\\Facades\\Event::assertDispatched\(\)\.#' diff --git a/tests/Integration/Schema/Types/LaravelEnumTypeDBTest.php b/tests/Integration/Schema/Types/LaravelEnumTypeDBTest.php index 23dad081af..4d65e95ec3 100644 --- a/tests/Integration/Schema/Types/LaravelEnumTypeDBTest.php +++ b/tests/Integration/Schema/Types/LaravelEnumTypeDBTest.php @@ -20,10 +20,6 @@ protected function setUp(): void { parent::setUp(); - if (AppVersion::atLeast(7.0)) { - $this->markTestSkipped('TODO remove this once bensampo/laravel-enum supports Laravel 7'); - } - $this->typeRegistry = $this->app->make(TypeRegistry::class); } diff --git a/tests/Unit/Schema/Types/LaravelEnumTypeTest.php b/tests/Unit/Schema/Types/LaravelEnumTypeTest.php index 795cf19656..7bcba3e0fd 100644 --- a/tests/Unit/Schema/Types/LaravelEnumTypeTest.php +++ b/tests/Unit/Schema/Types/LaravelEnumTypeTest.php @@ -4,7 +4,6 @@ use Nuwave\Lighthouse\Schema\TypeRegistry; use Nuwave\Lighthouse\Schema\Types\LaravelEnumType; -use Nuwave\Lighthouse\Support\AppVersion; use PHPUnit\Framework\Constraint\Callback; use Tests\TestCase; use Tests\Utils\LaravelEnums\AOrB; @@ -21,10 +20,6 @@ protected function setUp(): void { parent::setUp(); - if (AppVersion::atLeast(7.0)) { - $this->markTestSkipped('TODO remove this once bensampo/laravel-enum supports Laravel 7'); - } - $this->typeRegistry = $this->app->make(TypeRegistry::class); } From 5677d3e6648b39ac61c24d51dbf3497aedd29036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20S=C3=B8gaard?= Date: Mon, 9 Mar 2020 16:10:53 +0100 Subject: [PATCH 25/31] =?UTF-8?q?Throw=20GraphQL=20error=20when=20ModelNot?= =?UTF-8?q?FoundException=20is=20thrown=20in=20Ca=E2=80=A6=20(#1225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ src/Schema/Directives/CanDirective.php | 23 +++++++++++-------- .../Schema/Directives/CanDirectiveDBTest.php | 14 +++++++---- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6da642a0ba..f40790cd28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Changed + +- Throw user readable `Error` instead of `ModelNotFoundException` when model is not found in `@can` https://github.com/nuwave/lighthouse/pull/1225 + ## 4.10.1 ### Fixed diff --git a/src/Schema/Directives/CanDirective.php b/src/Schema/Directives/CanDirective.php index a632c95c7a..e04015dfac 100644 --- a/src/Schema/Directives/CanDirective.php +++ b/src/Schema/Directives/CanDirective.php @@ -7,6 +7,7 @@ use GraphQL\Type\Definition\ResolveInfo; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Arr; use Nuwave\Lighthouse\Exceptions\AuthorizationException; use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet; @@ -144,15 +145,19 @@ function (Directive $directive): bool { $queryBuilder->onlyTrashed(); } - $modelOrModels = $argumentSet - ->enhanceBuilder( - $queryBuilder, - [], - function (Directive $directive): bool { - return $directive instanceof TrashedDirective; - } - ) - ->findOrFail($findValue); + try { + $modelOrModels = $argumentSet + ->enhanceBuilder( + $queryBuilder, + [], + function (Directive $directive): bool { + return $directive instanceof TrashedDirective; + } + ) + ->findOrFail($findValue); + } catch (ModelNotFoundException $exception) { + throw new Error($exception->getMessage()); + } if ($modelOrModels instanceof Model) { $modelOrModels = [$modelOrModels]; diff --git a/tests/Integration/Schema/Directives/CanDirectiveDBTest.php b/tests/Integration/Schema/Directives/CanDirectiveDBTest.php index a19606996a..74039c9df9 100644 --- a/tests/Integration/Schema/Directives/CanDirectiveDBTest.php +++ b/tests/Integration/Schema/Directives/CanDirectiveDBTest.php @@ -2,7 +2,6 @@ namespace Tests\Integration\Schema\Directives; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Nuwave\Lighthouse\Exceptions\AuthorizationException; use Nuwave\Lighthouse\Schema\Directives\CanDirective; use Tests\DBTestCase; @@ -77,14 +76,22 @@ public function testFailsToFindSpecificModel(): void } '; - $this->expectException(ModelNotFoundException::class); $this->graphQL(/** @lang GraphQL */ ' { user(id: "not-present") { name } } - '); + ')->assertJson([ + 'errors' => [ + [ + 'message' => 'No query results for model [Tests\Utils\Models\User] not-present', + ], + ], + 'data' => [ + 'user' => null, + ], + ]); } public function testThrowsIfFindValueIsNotGiven(): void @@ -117,7 +124,6 @@ public function testThrowsIfFindValueIsNotGiven(): void ')->assertJson([ 'errors' => [ [ - 'message' => CanDirective::missingKeyToFindModel('some.path'), ], ], From 4ed632bc82842e29c4cec4364ebbfe24c31fe97a Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Thu, 12 Mar 2020 10:48:15 +0000 Subject: [PATCH 26/31] Ensure subscription routes are named uniquely (#1231) --- CHANGELOG.md | 6 ++++++ src/Subscriptions/SubscriptionRouter.php | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f40790cd28..5d6a02aba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +## 4.10.2 + +### Fixed + +- Ensure subscription routes are named uniquely https://github.com/nuwave/lighthouse/pull/1231 + ### Changed - Throw user readable `Error` instead of `ModelNotFoundException` when model is not found in `@can` https://github.com/nuwave/lighthouse/pull/1225 diff --git a/src/Subscriptions/SubscriptionRouter.php b/src/Subscriptions/SubscriptionRouter.php index 622d207b14..33890c2b2d 100644 --- a/src/Subscriptions/SubscriptionRouter.php +++ b/src/Subscriptions/SubscriptionRouter.php @@ -20,7 +20,7 @@ public function pusher($router): void ]); $router->post('graphql/subscriptions/webhook', [ - 'as' => 'lighthouse.subscriptions.auth', + 'as' => 'lighthouse.subscriptions.webhook', 'uses' => SubscriptionController::class.'@webhook', ]); } From 6391ef47ea9958709e2447c23541daf5ba4c81ce Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Thu, 12 Mar 2020 20:35:05 +0000 Subject: [PATCH 27/31] Upgrade temporary dev deps to stable versions (#1232) --- composer.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 9ca95abb36..89b1d89609 100644 --- a/composer.json +++ b/composer.json @@ -35,17 +35,16 @@ }, "require-dev": { "bensampo/laravel-enum": "^1.28.3", - "composer/composer": "1.10.0-RC as 1.9.3", "ergebnis/composer-normalize": "^2.2.2", - "haydenpierce/class-finder": "^0.4.0", + "haydenpierce/class-finder": "^0.4", "laravel/framework": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", - "laravel/lumen-framework": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || dev-master", + "laravel/lumen-framework": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", "laravel/scout": "^4.0 || ^5.0 || ^6.0 || ^7.0", "mll-lab/graphql-php-scalars": "^2.1", "mockery/mockery": "^1.0", "nunomaduro/larastan": "^0.4.3 || ^0.5.2", - "orchestra/database": "3.5.* || 3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4.0 || 5.x-dev", - "orchestra/testbench": "3.5.* || 3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4.0 || 5.x-dev", + "orchestra/database": "3.5.* || 3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4.0 || ^5.0", + "orchestra/testbench": "3.5.* || 3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4.0 || ^5.0", "phpbench/phpbench": "@dev", "phpunit/phpunit": "^6.5 || ^7.5 || ^8.4", "pusher/pusher-php-server": "^3.2" From ce11864599eb23ce8ae6fa3dfed2dcf7b1aee5a4 Mon Sep 17 00:00:00 2001 From: spawnia Date: Sun, 15 Mar 2020 15:47:07 +0100 Subject: [PATCH 28/31] Write up tests and docs --- CHANGELOG.md | 4 ++ docs/master/security/authentication.md | 38 +++++++++++++++---- .../Http/Controllers/GraphQLController.php | 1 - src/lighthouse.php | 6 ++- .../Middleware/AttemptAuthenticationTest.php | 26 ++++++------- tests/TestCase.php | 4 +- 6 files changed, 54 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d6a02aba5..4432eb45ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Added + +- Add `AttemptAuthentication` middleware to optionally log in users on the GraphQL endpoint + ## 4.10.2 ### Fixed diff --git a/docs/master/security/authentication.md b/docs/master/security/authentication.md index 7a990763ee..f18b5de6b0 100644 --- a/docs/master/security/authentication.md +++ b/docs/master/security/authentication.md @@ -1,14 +1,38 @@ # Authentication +You can use standard Laravel mechanisms to authenticate users of your GraphQL API. +Generally, stateless guards such as [API Authentication](https://laravel.com/docs/api-authentication) +are recommended for most use cases. + ## Global -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -Just add middleware trough your `lighthouse.php` configuration. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +As all GraphQL requests are served at a single HTTP endpoint, middleware added +through the `lighthouse.php` config will run for all queries against your server. -As all GraphQL requests are served at a single HTTP endpoint, this will guard your -entire API against unauthenticated users. +In most cases, your schema will have some publicly accessible fields and others +that require authentication. As multiple checks for authentication or permissions may be +required in a single request, it is convenient to attempt authentication once per request. + +```php + 'route' => [ + 'middleware' => [ + \Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class . ':api', + ], + ], +``` + +Note that the `AttemptAuthentication` middleware does *not* protect any of your fields +by itself, decorate them with [`@guard`](../api-reference/directives.md#guard) as needed. + +If you want to guard all your fields against unauthenticated access, you can simply add +Laravel's build-in auth middleware. Beware that this approach does not allow any GraphQL +operations for guest users, so you will have to handle login outside of GraphQL. + +```php +'middleware' => [ + 'auth:api', +], +``` ## Guard selected fields @@ -25,7 +49,7 @@ If you need to guard multiple fields, just use [`@guard`](../api-reference/direc on a `type` or an `extend type` definition. It will be applied to all fields within that type. ```graphql -extend type Query @guard(with: ["api:admin"]){ +extend type Query @guard(with: ["api:admin"]) { adminInfo: Secrets nukeCodes: [NukeCode!]! } diff --git a/src/Support/Http/Controllers/GraphQLController.php b/src/Support/Http/Controllers/GraphQLController.php index 8a1decef2f..87e221aad5 100644 --- a/src/Support/Http/Controllers/GraphQLController.php +++ b/src/Support/Http/Controllers/GraphQLController.php @@ -61,7 +61,6 @@ public function __construct( */ public function query(GraphQLRequest $request) { - dd($this->middleware); $this->eventsDispatcher->dispatch( new StartRequest($request) ); diff --git a/src/lighthouse.php b/src/lighthouse.php index 823ef68a3a..86a810938a 100644 --- a/src/lighthouse.php +++ b/src/lighthouse.php @@ -30,7 +30,11 @@ */ 'middleware' => [ \Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class, - //\Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class, + + // Logs in a user if they are authenticated. In contrast to Laravel's 'auth' + // middleware, this delegates auth and permission checks to the field level. + // If you want to use another guard, change the suffix (remove for default). + \Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class . ':api', ], /* diff --git a/tests/Support/Http/Middleware/AttemptAuthenticationTest.php b/tests/Support/Http/Middleware/AttemptAuthenticationTest.php index aa46d1832e..41ccc8d79f 100644 --- a/tests/Support/Http/Middleware/AttemptAuthenticationTest.php +++ b/tests/Support/Http/Middleware/AttemptAuthenticationTest.php @@ -3,7 +3,6 @@ namespace Tests\Unit\Support\Http\Middleware; use Illuminate\Auth\AuthManager; -use Illuminate\Http\Request; use Nuwave\Lighthouse\Schema\Context; use Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication; use PHPUnit\Framework\Constraint\Callback; @@ -22,33 +21,30 @@ protected function getEnvironmentSetUp($app) /** @var \Illuminate\Auth\AuthManager $authManager */ $authManager = $app->make(AuthManager::class); $authManager->viaRequest('foo', function () { - dd($this->user); - return $this->user; }); /** @var \Illuminate\Contracts\Config\Repository $config */ - $config = $app['config']; + $config = $app->make('config'); - $config->set('lighthouse.middleware', [ - AttemptAuthentication::class, + $config->set('lighthouse.route.middleware', [ + AttemptAuthentication::class . ':foo', + ]); + $config->set('auth.guards.foo', [ + 'driver' => 'foo', + 'provider' => 'users', ]); -// dd($config->get('auth')); - $config->set('auth.guards.api.driver', 'foo'); } public function testAttemptsAuthenticationGuest(): void { - /** @var \Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication $middleware */ - $middleware = app(AttemptAuthentication::class); - $middleware->handle(new Request(), function () { - }); - $this->mockResolver() ->with( null, [], - null + new Callback(function (Context $context) { + return $this->user === null; + }) ); $this->schema = /** @lang GraphQL */ ' @@ -83,7 +79,7 @@ public function testAttemptsAuthenticationUser(): void } '; - $a = $this->graphQL(/** @lang GraphQL */ ' + $this->graphQL(/** @lang GraphQL */ ' { foo } diff --git a/tests/TestCase.php b/tests/TestCase.php index 6942e28c7b..08a24b351b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -89,7 +89,9 @@ protected function getEnvironmentSetUp($app) 'Tests\\Utils\\Mutations', 'Tests\\Utils\\MutationsSecondary', ], - 'subscriptions' => 'Tests\\Utils\\Subscriptions', + 'subscriptions' => [ + 'Tests\\Utils\\Subscriptions', + ], 'interfaces' => [ 'Tests\\Utils\\Interfaces', 'Tests\\Utils\\InterfacesSecondary', From 72073f5e4cf2af97295711140577ca267c49280e Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 15 Mar 2020 14:48:35 +0000 Subject: [PATCH 29/31] Apply fixes from StyleCI --- src/lighthouse.php | 2 +- tests/Support/Http/Middleware/AttemptAuthenticationTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lighthouse.php b/src/lighthouse.php index 86a810938a..63d26b6f81 100644 --- a/src/lighthouse.php +++ b/src/lighthouse.php @@ -34,7 +34,7 @@ // Logs in a user if they are authenticated. In contrast to Laravel's 'auth' // middleware, this delegates auth and permission checks to the field level. // If you want to use another guard, change the suffix (remove for default). - \Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class . ':api', + \Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class.':api', ], /* diff --git a/tests/Support/Http/Middleware/AttemptAuthenticationTest.php b/tests/Support/Http/Middleware/AttemptAuthenticationTest.php index 41ccc8d79f..c952f3d585 100644 --- a/tests/Support/Http/Middleware/AttemptAuthenticationTest.php +++ b/tests/Support/Http/Middleware/AttemptAuthenticationTest.php @@ -28,7 +28,7 @@ protected function getEnvironmentSetUp($app) $config = $app->make('config'); $config->set('lighthouse.route.middleware', [ - AttemptAuthentication::class . ':foo', + AttemptAuthentication::class.':foo', ]); $config->set('auth.guards.foo', [ 'driver' => 'foo', From f71f260f06435bde9ad6001127ee5576c2425cb5 Mon Sep 17 00:00:00 2001 From: spawnia Date: Sun, 15 Mar 2020 16:07:34 +0100 Subject: [PATCH 30/31] Fix link to Laravel auth docs --- docs/3.7/security/authentication.md | 5 ++--- docs/4.0/security/authentication.md | 5 ++--- docs/4.1/security/authentication.md | 5 ++--- docs/4.10/security/authentication.md | 7 +++---- docs/4.2/security/authentication.md | 5 ++--- docs/4.3/security/authentication.md | 5 ++--- docs/4.4/security/authentication.md | 5 ++--- docs/4.5/security/authentication.md | 5 ++--- docs/4.6/security/authentication.md | 5 ++--- docs/4.7/security/authentication.md | 5 ++--- docs/4.8/security/authentication.md | 7 +++---- docs/4.9/security/authentication.md | 7 +++---- docs/master/security/authentication.md | 5 ++--- 13 files changed, 29 insertions(+), 42 deletions(-) diff --git a/docs/3.7/security/authentication.md b/docs/3.7/security/authentication.md index 0c1bbfd73f..a955d1a3e6 100644 --- a/docs/3.7/security/authentication.md +++ b/docs/3.7/security/authentication.md @@ -1,8 +1,7 @@ # Authentication -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. ## Get the current user diff --git a/docs/4.0/security/authentication.md b/docs/4.0/security/authentication.md index 708c73c56a..74e16de6b0 100644 --- a/docs/4.0/security/authentication.md +++ b/docs/4.0/security/authentication.md @@ -1,8 +1,7 @@ # Authentication -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. ## Get the current user diff --git a/docs/4.1/security/authentication.md b/docs/4.1/security/authentication.md index 708c73c56a..74e16de6b0 100644 --- a/docs/4.1/security/authentication.md +++ b/docs/4.1/security/authentication.md @@ -1,8 +1,7 @@ # Authentication -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. ## Get the current user diff --git a/docs/4.10/security/authentication.md b/docs/4.10/security/authentication.md index 7a990763ee..d291fcc4a6 100644 --- a/docs/4.10/security/authentication.md +++ b/docs/4.10/security/authentication.md @@ -1,12 +1,11 @@ # Authentication +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. + ## Global -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. Just add middleware trough your `lighthouse.php` configuration. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. - As all GraphQL requests are served at a single HTTP endpoint, this will guard your entire API against unauthenticated users. diff --git a/docs/4.2/security/authentication.md b/docs/4.2/security/authentication.md index 708c73c56a..74e16de6b0 100644 --- a/docs/4.2/security/authentication.md +++ b/docs/4.2/security/authentication.md @@ -1,8 +1,7 @@ # Authentication -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. ## Get the current user diff --git a/docs/4.3/security/authentication.md b/docs/4.3/security/authentication.md index 708c73c56a..74e16de6b0 100644 --- a/docs/4.3/security/authentication.md +++ b/docs/4.3/security/authentication.md @@ -1,8 +1,7 @@ # Authentication -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. ## Get the current user diff --git a/docs/4.4/security/authentication.md b/docs/4.4/security/authentication.md index 708c73c56a..74e16de6b0 100644 --- a/docs/4.4/security/authentication.md +++ b/docs/4.4/security/authentication.md @@ -1,8 +1,7 @@ # Authentication -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. ## Get the current user diff --git a/docs/4.5/security/authentication.md b/docs/4.5/security/authentication.md index 708c73c56a..74e16de6b0 100644 --- a/docs/4.5/security/authentication.md +++ b/docs/4.5/security/authentication.md @@ -1,8 +1,7 @@ # Authentication -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. ## Get the current user diff --git a/docs/4.6/security/authentication.md b/docs/4.6/security/authentication.md index 708c73c56a..74e16de6b0 100644 --- a/docs/4.6/security/authentication.md +++ b/docs/4.6/security/authentication.md @@ -1,8 +1,7 @@ # Authentication -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. ## Get the current user diff --git a/docs/4.7/security/authentication.md b/docs/4.7/security/authentication.md index 708c73c56a..74e16de6b0 100644 --- a/docs/4.7/security/authentication.md +++ b/docs/4.7/security/authentication.md @@ -1,8 +1,7 @@ # Authentication -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. ## Get the current user diff --git a/docs/4.8/security/authentication.md b/docs/4.8/security/authentication.md index 7a990763ee..57bc120697 100644 --- a/docs/4.8/security/authentication.md +++ b/docs/4.8/security/authentication.md @@ -2,11 +2,10 @@ ## Global -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -Just add middleware trough your `lighthouse.php` configuration. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. +Just add middleware trough your `lighthouse.php` configuration. As all GraphQL requests are served at a single HTTP endpoint, this will guard your entire API against unauthenticated users. diff --git a/docs/4.9/security/authentication.md b/docs/4.9/security/authentication.md index 7a990763ee..57bc120697 100644 --- a/docs/4.9/security/authentication.md +++ b/docs/4.9/security/authentication.md @@ -2,11 +2,10 @@ ## Global -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -Just add middleware trough your `lighthouse.php` configuration. -The [API Authentication](https://laravel.com/docs/api-authentication) is especially -suited because of its stateless nature. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. +Just add middleware trough your `lighthouse.php` configuration. As all GraphQL requests are served at a single HTTP endpoint, this will guard your entire API against unauthenticated users. diff --git a/docs/master/security/authentication.md b/docs/master/security/authentication.md index f18b5de6b0..fa9766a945 100644 --- a/docs/master/security/authentication.md +++ b/docs/master/security/authentication.md @@ -1,8 +1,7 @@ # Authentication -You can use standard Laravel mechanisms to authenticate users of your GraphQL API. -Generally, stateless guards such as [API Authentication](https://laravel.com/docs/api-authentication) -are recommended for most use cases. +You can use [standard Laravel mechanisms](https://laravel.com/docs/authentication) +to authenticate users of your GraphQL API. Stateless guards are recommended for most use cases. ## Global From 7d0ccba67361fc8a4d2e77c27477fb9f0003ed61 Mon Sep 17 00:00:00 2001 From: spawnia Date: Sun, 15 Mar 2020 16:09:04 +0100 Subject: [PATCH 31/31] Fix namespace --- .../Support/Http/Middleware/AttemptAuthenticationTest.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{ => Unit}/Support/Http/Middleware/AttemptAuthenticationTest.php (100%) diff --git a/tests/Support/Http/Middleware/AttemptAuthenticationTest.php b/tests/Unit/Support/Http/Middleware/AttemptAuthenticationTest.php similarity index 100% rename from tests/Support/Http/Middleware/AttemptAuthenticationTest.php rename to tests/Unit/Support/Http/Middleware/AttemptAuthenticationTest.php