diff --git a/.travis.yml b/.travis.yml index 31a29fdc8..5d80c1d66 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,18 +31,8 @@ script: composer test jobs: include: - - stage: Test - php: 5.6 - env: COMPOSER_UPDATE_FLAGS=--prefer-lowest SYMFONY_DEPRECATIONS_HELPER=disabled GRAPHQLPHP_VERSION=^0.11.2 - - php: 5.6 - env: SYMFONY_VERSION=3.1.* SYMFONY_DEPRECATIONS_HELPER=disabled GRAPHQLPHP_VERSION=0.12 - - php: 7.0 - env: SYMFONY_VERSION=3.2.* PHPUNIT_VERSION=^5.7.26 - - php: 7.1 - php: 7.2 - env: SYMFONY_VERSION=3.3.* - - php: 7.2 - env: SYMFONY_VERSION=3.4.* + env: SYMFONY_VERSION=3.4.* GRAPHQLPHP_VERSION=^0.11.2 - php: 7.2 env: SYMFONY_VERSION=4.1.* STABILITY=beta - php: nightly diff --git a/README.md b/README.md index 4fd97a1be..739334df7 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,13 @@ It also supports: Browse your version documentation: -* [0.8](https://github.com/overblog/GraphQLBundle/blob/0.8/README.md) -* [0.9](https://github.com/overblog/GraphQLBundle/blob/0.9/README.md) -* [0.10](https://github.com/overblog/GraphQLBundle/blob/0.10/README.md) -* [0.11](https://github.com/overblog/GraphQLBundle/blob/0.11/README.md) +* [0.8 (OBSOLETE)](https://github.com/overblog/GraphQLBundle/blob/0.8/README.md) +* [0.9 (OBSOLETE)](https://github.com/overblog/GraphQLBundle/blob/0.9/README.md) +* [0.10 (OBSOLETE)](https://github.com/overblog/GraphQLBundle/blob/0.10/README.md) +* [0.11 (STABLE)](https://github.com/overblog/GraphQLBundle/blob/0.11/README.md) +* [0.12 (DEV)](https://github.com/overblog/GraphQLBundle/blob/master/README.md) + +[Versions requirements](docs/index.md#versions-requirements) Documentation ------------- @@ -63,6 +66,7 @@ Documentation - [Promise](docs/data-fetching/promise.md) - [Security](docs/security/index.md) - [Handle CORS](docs/security/handle-cors.md) + - [Object access control](docs/security/object-access-control.md) - [Fields access control](docs/security/fields-access-control.md) - [Fields public control](docs/security/fields-public-control.md) - [Limiting query depth](docs/security/limiting-query-depth.md) @@ -74,6 +78,8 @@ Documentation Talks and slides to help you start ---------------------------------- +* GraphQL in Symfony *by Bernd Alter* - [Twitter](https://twitter.com/bazoo0815) + - [Talk about GraphQL and its implementation with Symfony (26.04.2017)](https://www.slideshare.net/berndalter7/graphql-in-symfony) `English` * GraphQL is right in front of us, let's to it! *by Renato Mendes Figueiredo* - [Twitter](https://twitter.com/renatomefi), [GitHub](https://github.com/renatomefi) - [Slides at http://talks.mefi.in/graphql-scotphp17](http://talks.mefi.in/graphql-scotphp17/) `English` - [Video at SymfonyCamp UA 2017](https://www.youtube.com/watch?v=jyoYlnCPNgk) `English` diff --git a/composer.json b/composer.json index 51fb7d0a3..798da23ab 100644 --- a/composer.json +++ b/composer.json @@ -30,16 +30,16 @@ "sort-packages": true }, "require": { - "php": ">=5.6", + "php": ">=7.1", "overblog/graphql-php-generator": "^0.7.0", "psr/log": "^1.0", - "symfony/config": "^3.1 || ^4.0", - "symfony/dependency-injection": "^3.1 || ^4.0", - "symfony/event-dispatcher": "^3.1 || ^4.0", - "symfony/expression-language": "^3.1 || ^4.0", - "symfony/framework-bundle": "^3.1 || ^4.0", - "symfony/options-resolver": "^3.1 || ^4.0", - "symfony/property-access": "^3.1 || ^4.0", + "symfony/config": "^3.4 || ^4.0", + "symfony/dependency-injection": "^3.4 || ^4.0", + "symfony/event-dispatcher": "^3.4 || ^4.0", + "symfony/expression-language": "^3.4 || ^4.0", + "symfony/framework-bundle": "^3.4 || ^4.0", + "symfony/options-resolver": "^3.4 || ^4.0", + "symfony/property-access": "^3.4 || ^4.0", "webonyx/graphql-php": "^0.11.2 || ^0.12.0" }, "suggest": { @@ -51,20 +51,20 @@ "phpunit/phpunit": "^5.7.26 || ^6.0", "react/promise": "^2.5", "sensio/framework-extra-bundle": "^3.0", - "symfony/asset": "^3.1 || ^4.0", - "symfony/browser-kit": "^3.1 || ^4.0", - "symfony/console": "^3.1 || ^4.0", - "symfony/css-selector": "^3.1 || ^4.0", - "symfony/phpunit-bridge": "^3.1 || ^4.0", - "symfony/process": "^3.1 || ^4.0", - "symfony/security-bundle": "^3.1 || ^4.0", - "symfony/templating": "^3.1 || ^4.0", - "symfony/web-profiler-bundle": "^3.1 || ^4.0", - "symfony/yaml": "^3.1 || ^4.0" + "symfony/asset": "^3.4 || ^4.0", + "symfony/browser-kit": "^3.4 || ^4.0", + "symfony/console": "^3.4 || ^4.0", + "symfony/css-selector": "^3.4 || ^4.0", + "symfony/phpunit-bridge": "^3.4 || ^4.0", + "symfony/process": "^3.4 || ^4.0", + "symfony/security-bundle": "^3.4 || ^4.0", + "symfony/templating": "^3.4 || ^4.0", + "symfony/web-profiler-bundle": "^3.4 || ^4.0", + "symfony/yaml": "^3.4 || ^4.0" }, "extra": { "branch-alias": { - "dev-master": "0.11-dev" + "dev-master": "0.12-dev" } }, "scripts": { diff --git a/docs/definitions/expression-language.md b/docs/definitions/expression-language.md index f1c918779..4e852a310 100644 --- a/docs/definitions/expression-language.md +++ b/docs/definitions/expression-language.md @@ -1,7 +1,7 @@ Expression language =================== -All definitions configs entries can use expression language but it must be explicitly triggered using "@=" like prefix. +All definition config entries can use expression language but it must be explicitly triggered using "@=" like prefix. **Functions description:** @@ -43,11 +43,11 @@ Expression | Description | Scope Custom expression function -------------------------- -Custom expression function is easy as creating a tagged service. -Adding useful expression function can help user create simple resolver without having to leave config file, -this also improve performance by removing a useless external resolver call. +Adding custom expression function is easy since all you need to do is create a tagged service. +Expression functions can help user create simple resolver without having to leave config file, +this also improves performance by removing a useless external resolver call. -Here an example to add an custom expression equivalent to php `json_decode`: +Here is an example to add a custom expression equivalent to php `json_decode`: ```php [GraphQL documentation about types and fields](https://graphql.github.io/learn/schema/#object-types-and-fields). + + ```graphql # config/graphql/schema.types.graphql -type Query { - bar: Foo! - baz(id: ID!): Baz +type Character { + # Name of the character + name: String! + # This character appears in those episodes + appearsIn: [Episode]! } +``` + +##### Define Enumeration types -scalar Baz +> [GraphQL documentation about Enumerations types](https://graphql.github.io/learn/schema/#enumeration-types). -interface Foo { - # Description of my is_foo field - is_foo: Boolean +```graphql +# Enumeration of episodes +enum Episode { + NEWHOPE + EMPIRE + JEDI } -type Bar implements Foo { - is_foo: Boolean - user: User! +``` + +##### Define Interfaces + +> [GraphQL documentation about Interfaces](https://graphql.github.io/learn/schema/#interfaces). + +```graphql +interface Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! +} +``` + +```graphql +type Human implements Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + starships: [Starship] + totalCredits: Int } -enum User { - TATA - TITI - TOTO +type Droid implements Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + primaryFunction: String } ``` +##### Define queries + +> [GraphQL documentation about Query type](https://graphql.github.io/learn/schema/#the-query-and-mutation-types). + +```graphql +type RootQuery { + # Access all characters + characters: [Character]! +} +``` + +Do not forget to configure your schema **query** type, as described in the [schema documentation](https://github.com/overblog/GraphQLBundle/blob/master/Resources/doc/definitions/schema.md). + +```yml +overblog_graphql: + definitions: + schema: + query: RootQuery +``` + +##### Define mutations + +> [GraphQL documentation about Mutation type](https://graphql.github.io/learn/schema/#the-query-and-mutation-types). + +```graphql +input CreateCharacter { + name: String! +} + +input UpdateCharacter { + name: String! +} + +type RootMutation { + createCharacter(character: CreateCharacter!): Character! + updateCharacter(characterId: ID!, character: UpdateCharacter!): Character! +} +``` + +Do not forget to configure your schema **mutation** type, as described in the [schema documentation](https://github.com/overblog/GraphQLBundle/blob/master/Resources/doc/definitions/schema.md). + +```yml +overblog_graphql: + definitions: + schema: + mutation: RootMutation +``` + +--- + When using this shorthand syntax, you define your field resolvers (and some more configuration) separately from the schema. Since the schema already describes all of the fields, arguments, and result types, the only thing left is a collection of callable that are called to actually execute these fields. diff --git a/docs/definitions/mutation.md b/docs/definitions/mutation.md index 802dc2842..bba66517e 100644 --- a/docs/definitions/mutation.md +++ b/docs/definitions/mutation.md @@ -1,6 +1,6 @@ # Mutation -Here an example without using relay: +Here an example of mutation without using [relay](https://facebook.github.io/relay/): ```yaml Mutation: @@ -34,4 +34,56 @@ IntroduceShipInput: type: "String!" ``` +To implement the logic behind your mutation, you should create a new class that +implements `MutationInterface` and `AliasedInterface` interfaces. + +```php +factionRepository = $factionRepository; + } + + public function createShip(string $shipName, int $factionId): array + { + // `$shipName` has the value of `args['input']['shipName']` + // `$factionId` has the value of `args['input']['factionId']` + + // Do something with `$shipName` and `$factionId` ... + $ship = new Ship($shipName); + $faction = $this->factionRepository->find($factionId); + $faction->addShip($ship); + // ... + + + // Then returns our payload, it should fits `IntroduceShipPayload` type + return [ + 'ship' => $ship, + 'faction' => $faction, + ]; + } + + /** + * {@inheritdoc} + */ + public static function getAliases() + { + return [ + // `create_ship` is the name of the mutation that you SHOULD use inside of your types definition + // `createShip` is the method that will be executed when you call `@=resolver('create_ship')` + 'createShip' => 'create_ship' + ]; + } +} +``` + Here the same example [using relay mutation](relay/mutation.md). diff --git a/docs/definitions/relay/connection.md b/docs/definitions/relay/connection.md index 4a93236ea..c3db5ef49 100644 --- a/docs/definitions/relay/connection.md +++ b/docs/definitions/relay/connection.md @@ -22,7 +22,7 @@ User: resolve: '@=resolver("friends", [value, args])' friendsForward: type: userConnection - argsBuilder: "Relay::ForwardConnection + argsBuilder: "Relay::ForwardConnection" resolve: '@=resolver("friends", [value, args])' friendsBackward: type: userConnection diff --git a/docs/definitions/resolver.md b/docs/definitions/resolver.md index d926ebde4..98b2e4680 100644 --- a/docs/definitions/resolver.md +++ b/docs/definitions/resolver.md @@ -7,181 +7,207 @@ To ease development we named 2 types of resolver: This is just a recommendation. -Resolvers can be define 2 different ways +Resolvers can be define 2 different ways: -1. **The PHP way** +## The PHP way - You can declare a resolver (any class that implements `Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface` - or `Overblog\GraphQLBundle\Definition\Resolver\MutationInterface`) - in `src/*Bundle/GraphQL` or `app/GraphQL` and they will be auto discovered. - Auto map classes method are accessible by: - * double-colon (::) to separate service id (class name) and the method names - (example: `AppBunble\GraphQL\CustomResolver::myMethod`) - * for callable classes you can use the service id (example: `AppBunble\GraphQL\InvokeResolver` for a resolver implementing the `__invoke` method) - you can also alias a type by implementing `Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface` - which returns a map of method/alias. The service created will autowire the `__construct` - and `Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer` methods. - Here an example: - ```php - 'say_hello']; - } + return ['sayHello' => 'say_hello']; } - ``` +} +```` - `SayHello` resolver can be access by using `App\GraphQL\Resolver\Greetings::sayHello` or - `say_hello` alias. +Example using a fully qualified method name: +````yaml +resolve: '@=resolver("App\\GraphQL\\Resolver\\Greetings::sayHello", [args["name"]])' +```` - we can also use class invoker: - ```php - value += $number; + } - class CalcMutation implements MutationInterface, AliasedInterface + /** + * {@inheritdoc} + */ + public static function getAliases() { - private $value; - - public function addition($number) - { - $this->value += $number; - } - - /** - * {@inheritdoc} - */ - public static function getAliases() - { - return ['addition' => 'add']; - } + return ['addition' => 'add']; } - ``` - `addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or - `add` alias. - - You can also define custom dirs using the config (Symfony <3.3): - ```yaml - overblog_graphql: - definitions: - auto_mapping: - directories: - - "%kernel.root_dir%/src/*Bundle/CustomDir" - - "%kernel.root_dir%/src/AppBundle/{foo,bar}" - ``` - - If using Symfony 3.3+ disabling auto mapping can be a solution to leave place to native - DI `autoconfigure`: - - ```yaml - overblog_graphql: - definitions: - auto_mapping: false - ``` - - Here an example of how this can be done with DI `autoconfigure`: - - ```yaml - services: - App\Mutation\: - resource: '../src/Mutation' - tags: ['overblog_graphql.mutation'] - - App\Resolver\: - resource: '../src/Resolver' - tags: ['overblog_graphql.resolver'] - ``` - - **Note:** - * When using service id as FQCN in yaml definition, backslashes must be correctly escaped, - here an example `'@=resolver("App\\GraphQL\\Resolver\\Greetings", [args['name']])'`. - * You can also see the more straight forward way using [resolver map](resolver-map.md) - -2. **The service way** - - Creating a service tagged `overblog_graphql.resolver` for resolvers - or `overblog_graphql.mutation` for mutations. - - Using the php way examples: - - ```yaml - services: - App\GraphQL\Resolver\Greetings: - # only for sf < 3.3 - #class: App\GraphQL\Resolver\Greetings - tags: - - { name: overblog_graphql.resolver, method: sayHello, alias: say_hello } # add alias say_hello - - { name: overblog_graphql.resolver, method: sayHello } # add service id "App\GraphQL\Resolver\Greetings" - ``` - - `SayHello` resolver can be access by using `App\GraphQL\Resolver\Greetings::sayHello` or - `say_hello` alias. - - for invokable classes no need to use `alias` and `method` attributes: - - ```yaml - services: - App\GraphQL\Resolver\Greetings: - # only for sf < 3.3 - #class: App\GraphQL\Resolver\Greetings - tags: - - { name: overblog_graphql.resolver } - ``` - - This way resolver can be accessed with service id `App\GraphQL\Resolver\Greetings`. - - for mutation: - - ```yaml - services: - App\GraphQL\Mutation\CalcMutation: - # only for sf < 3.3 - #class: App\GraphQL\Mutation\CalcMutation - tags: - - { name: overblog_graphql.mutation, method: addition, alias: add } - ``` - `addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or - `add` alias. +} +``` +`addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or +`add` alias. + +You can also define custom dirs using the config (Symfony <3.3): +```yaml +overblog_graphql: + definitions: + auto_mapping: + directories: + - "%kernel.root_dir%/src/*Bundle/CustomDir" + - "%kernel.root_dir%/src/AppBundle/{foo,bar}" +``` + +If using Symfony 3.3+ disabling auto mapping can be a solution to leave place to native +DI `autoconfigure`: + +```yaml +overblog_graphql: + definitions: + auto_mapping: false +``` + +Here an example of how this can be done with DI `autoconfigure`: + +```yaml +services: + App\Mutation\: + resource: '../src/Mutation' + tags: ['overblog_graphql.mutation'] + + App\Resolver\: + resource: '../src/Resolver' + tags: ['overblog_graphql.resolver'] +``` + +## The service way + +Creating a service tagged `overblog_graphql.resolver` for resolvers +or `overblog_graphql.mutation` for mutations. + +Using the php way examples: + +```yaml +services: + App\GraphQL\Resolver\Greetings: + # only for sf < 3.3 + #class: App\GraphQL\Resolver\Greetings + tags: + - { name: overblog_graphql.resolver, method: sayHello, alias: say_hello } # add alias say_hello + - { name: overblog_graphql.resolver, method: sayHello } # add service id "App\GraphQL\Resolver\Greetings" +``` + +`SayHello` resolver can be access by using `App\GraphQL\Resolver\Greetings::sayHello` or +`say_hello` alias. + +for invokable classes no need to use `alias` and `method` attributes: + +```yaml +services: + App\GraphQL\Resolver\Greetings: + # only for sf < 3.3 + #class: App\GraphQL\Resolver\Greetings + tags: + - { name: overblog_graphql.resolver } +``` + +This way resolver can be accessed with service id `App\GraphQL\Resolver\Greetings`. + +for mutation: + +```yaml +services: + App\GraphQL\Mutation\CalcMutation: + # only for sf < 3.3 + #class: App\GraphQL\Mutation\CalcMutation + tags: + - { name: overblog_graphql.mutation, method: addition, alias: add } +``` +`addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or +`add` alias. Next step [solving N+1 problem](solving-n-plus-1-problem.md) diff --git a/docs/definitions/type-inheritance.md b/docs/definitions/type-inheritance.md index 3b9c43a93..9d2ab8c44 100644 --- a/docs/definitions/type-inheritance.md +++ b/docs/definitions/type-inheritance.md @@ -138,6 +138,8 @@ CharacterWizard: for example `CharacterWizard` config is the result of `array_replace_recursive(CharacterConfig, CharacterWizardConfig)` +**Decorators:** + You can also create decorator types to be used as reusable templates. Decorators are only virtual and will not exists in final schema. That is the reason why decorator should never be reference as type in schema definition. @@ -151,3 +153,46 @@ ObjectA: fields: bar: {type: String!} ``` + +You can use interfaces with decorators. Because they are only virtual, you do not have to implement the interface on the decorator itself. +But you have to implement it on the type you decorate. + +Imagine the following situation: + +```yaml +Node: + type: 'interface' + config: + # [...] resolveType logic unimportant here + fields: + id: + type: 'ID!' + # [...] resolve logic unimportant here + +NodeEditPermission: + type: 'object' + decorator: true + config: + # This interface must be implemented by type which inherits from this decorator + interfaces: ["Node"] + fields: + canEdit: + type: 'Boolean!' + # The `value` is enforced to implement "Node" no matter which type uses this decorator. + # The resolver could be for example a (cached) user of symfony/security authorization checker + # which does ->isGranted([attribute] $attribute, [subject] $value) + resolve: '@=resolver("Permission.nodeAttribute", ["edit", value])' + +Product: + type: 'object' + inherits: + - 'NodeEditPermission' + # - 'NodeRemovePermission' + # - [...] + config: + fields: + # Must implement "Node" interface because "NodeEditPermission" decorator requires it + # Not implementing would raise an graphql error + id: + type: 'ID!' +``` \ No newline at end of file diff --git a/docs/definitions/type-system/enum.md b/docs/definitions/type-system/enum.md index fc089a688..66d1d3172 100644 --- a/docs/definitions/type-system/enum.md +++ b/docs/definitions/type-system/enum.md @@ -5,7 +5,7 @@ Enum # MyBundle/Resources/config/graphql/Episode.types.yml # The original trilogy consists of three movies. # This implements the following type system shorthand: -# enum Episode { NEWHOPE, EMPIRE, JEDI } +# enum Episode { NEWHOPE, EMPIRE, JEDI, FORCEAWAKENS } Episode: type: enum config: @@ -17,10 +17,10 @@ Episode: # to deprecate a value, only set the deprecation reason #deprecationReason: "Just because" EMPIRE: - value: 5 + # We can use a PHP constant to avoid a magic number + value: '@=constant("App\\StarWars\\Movies::MOVIE_EMPIRE")' description: "Released in 1980." JEDI: 6 # using the short syntax (JEDI value equal to 6) -# in this case FORCEAWAKENS value = FORCEAWAKENS -# FORCEAWAKENS: -# description: "Released in 2015." + FORCEAWAKENS: # in this case FORCEAWAKENS value = FORCEAWAKENS + description: "Released in 2015." ``` diff --git a/docs/definitions/type-system/input-object.md b/docs/definitions/type-system/input-object.md index 10280b627..edba9d5b6 100644 --- a/docs/definitions/type-system/input-object.md +++ b/docs/definitions/type-system/input-object.md @@ -5,7 +5,7 @@ Input object # src/MyBundle/Resources/config/graphql/HumanAndDroid.types.yml # # This implements the following type system shorthand: -# type HeroInput { +# input HeroInput { # name: Episode! # } HeroInput: diff --git a/docs/error-handling/index.md b/docs/error-handling/index.md index a6159fbba..67ef8023e 100644 --- a/docs/error-handling/index.md +++ b/docs/error-handling/index.md @@ -95,7 +95,7 @@ define a custom exception mapping: ```yaml overblog_graphql: #... - definitions: + errors_handler: #... # change to true to try to map an exception to a parent exception if the exact exception is not in # the mapping diff --git a/docs/helpers/relay-paginator.md b/docs/helpers/relay-paginator.md index 8e606faa4..c1551ad36 100644 --- a/docs/helpers/relay-paginator.md +++ b/docs/helpers/relay-paginator.md @@ -174,6 +174,11 @@ class DataBackend { return count($array); } + + public function countAll() + { + return count($this->data); + } } $backend = new DataBackend(); @@ -195,6 +200,50 @@ $result = $paginator->backward( You should get the 4 last items of the _data set_. +#### Within a resolver + +````yaml +resolve: '@=resolver("App\\GraphQL\\Resolver\\Greetings::sayHello", [args])' +```` + +```` +sayHello(first: 1, after: "YXJyYXljb25uZWN0aW9uOjI="){ # after: base64_encode('arrayconnection:2') + edges { + cursor # YXJyYXljb25uZWN0aW9uOjM= + node # D + } + pageInfo { + hasNextPage # true + } +} +```` + +````php +getData($offset, $limit); + }); + + return $paginator->auto($args, function() use ($backend) { + return $backend->countAll(); + }); + } +} +```` + #### Promise handling Paginator also supports promises if you [use that feature](https://github.com/webonyx/graphql-php/pull/67) diff --git a/docs/index.md b/docs/index.md index a37967eca..2c1ddd3d3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,14 +5,16 @@ This Symfony bundle provides integration of [GraphQL](https://facebook.github.io and [GraphQL Relay](https://facebook.github.io/relay/docs/graphql-relay-specification.html). It also supports batching using libs like [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer) or [Apollo GraphQL](http://dev.apollodata.com/core/network.html#query-batching). -Requirements ------------- - -| Version | PHP | Symfony | -|------------------------------------------------------------:|------------:|------------------:| -| `>= 0.10` | `>= 5.6` | `>= 3.1` | -| [`0.9`](https://github.com/overblog/GraphQLBundle/tree/0.9) | `>= 5.5.9` | `>= 2.8, <= 3.1` | -| [`0.8`](https://github.com/overblog/GraphQLBundle/tree/0.8) | `>= 5.4 ` | `>= 2.7, <= 3.1` | +Versions requirements +---------------------- + +| Version | PHP | Symfony | Support | +|----------------------------------------------------------------:|------------:|------------------:|--------------------:| +| [`0.12`](https://github.com/overblog/GraphQLBundle/tree/master) | `>= 7.1` | `>= 3.1` | DEV | +| [`0.11`](https://github.com/overblog/GraphQLBundle/tree/0.11) | `>= 5.6` | `>= 3.1, <= 4.0` | Active support | +| [`0.10`](https://github.com/overblog/GraphQLBundle/tree/0.10) | `>= 5.5.9` | `>= 2.8, <= 3.1` | End of life | +| [`0.9`](https://github.com/overblog/GraphQLBundle/tree/0.9) | `>= 5.5.9` | `>= 2.8, <= 3.1` | End of life | +| [`0.8`](https://github.com/overblog/GraphQLBundle/tree/0.8) | `>= 5.4 ` | `>= 2.7, <= 3.1` | End of life | After installation ------------ diff --git a/docs/security/fields-access-control.md b/docs/security/fields-access-control.md index 523234dc5..7949edbfb 100644 --- a/docs/security/fields-access-control.md +++ b/docs/security/fields-access-control.md @@ -1,10 +1,14 @@ Fields access Control ====================== -An access control can be add on each field using `config.fields.*.access` or globally with `config.fieldsDefaultAccess`. +An access control can be added on each field using `config.fields.*.access` or globally with `config.fieldsDefaultAccess`. If `config.fields.*.access` value is true field will be normally resolved but will be `null` otherwise. Act like access is`true` if not set. +Note: +- in query mode: execute resolver -> execute access -> manage result in function of access +- in mutation mode: execute access -> execute resolver if access result is true + In the example below the Human name is available only for authenticated users. ```yaml @@ -32,3 +36,9 @@ Human: description: "The home planet of the human, or null if unknown." interfaces: [Character] ``` + +Performance +----------- +Checking access on each field can be a performance issue and may be dealt with using: +- using a custom cache to do the check only once +- using [Object access control](object-access-control.md) diff --git a/docs/security/fields-public-control.md b/docs/security/fields-public-control.md index 2ec84b1b3..98338b8e2 100644 --- a/docs/security/fields-public-control.md +++ b/docs/security/fields-public-control.md @@ -1,7 +1,7 @@ Fields public Control ===================== -You can use `config.fields.*.public` to control if a field needs to be removed the results. +You can use `config.fields.*.public` to control if a field needs to be removed from the results. If `config.fields.*.public` value is true or is not set, the field will be visible. If value is false, then the field will be removed (in any query, including inspection queries). @@ -20,7 +20,7 @@ AnObject: ``` -You can also use `config.fieldsDefaultPublic` to handle the setting globally on an object : +You can also use `config.fieldsDefaultPublic` to handle the setting globally on an object: ```yaml AnObject: @@ -34,6 +34,6 @@ AnObject: type: "String" ``` -Have you noticed `typeName` and `fieldName` here ? This variables are always set to the current +Have you noticed `typeName` and `fieldName` here? These variables are always set to the current type name and current field name, meaning you can apply a per field `public` setting on all the fields with one line of yaml. diff --git a/docs/security/handle-cors.md b/docs/security/handle-cors.md index 2b48d6b37..e39ac769a 100644 --- a/docs/security/handle-cors.md +++ b/docs/security/handle-cors.md @@ -2,9 +2,9 @@ Handle CORS =========== The bundle comes out of the box with a generic and simple CORS (Cross-Origin Resource Sharing) handler -but we recommends using [NelmioCorsBundle](https://github.com/nelmio/NelmioCorsBundle) for more flexibility... +but we recommend using [NelmioCorsBundle](https://github.com/nelmio/NelmioCorsBundle) for more flexibility... -The handler is disabled by default. To enabled it: +The handler is disabled by default. To enable it: ```yaml overblog_graphql: @@ -13,7 +13,7 @@ overblog_graphql: handle_cors: true ``` -Here the values of the headers that will be returns on preflight request: +These headers will be returned on preflight requests: Headers | Value -------------------------------- | --------------------------------------- diff --git a/docs/security/index.md b/docs/security/index.md index 078f93e98..c5ef898e4 100644 --- a/docs/security/index.md +++ b/docs/security/index.md @@ -2,6 +2,7 @@ Security ======== * [Handle CORS](handle-cors.md) +* [Object access control](object-access-control.md) * [Fields access control](fields-access-control.md) * [Fields public control](fields-public-control.md) * [Limiting query depth](limiting-query-depth.md) diff --git a/docs/security/object-access-control.md b/docs/security/object-access-control.md new file mode 100644 index 000000000..c490cfaa9 --- /dev/null +++ b/docs/security/object-access-control.md @@ -0,0 +1,33 @@ +Object access Control +====================== + +If your GraphQL schema has multiple paths to the same resolver, you may end up with duplicated access control on the different fields leading to this resolver. + +Access control can be added to an object as a whole using a decorator type for this protected field and make every parent extend this type. + + +Access control can be added to individual fields using `config.fields.*.access` or globally with `config.fieldsDefaultAccess`. +If the value returned by `config.fields.*.access` is true, the field will be resolved normally, and `null` will be returned otherwise. +If not set, acts like if access is `true`. + +In the example below the user field protection is set by the decorator: + +```yaml +ProtectedUser: + type: object + decorator: true + config: + fields: + user: {type: User, access: '@=isAuthenticated()'} + +Foo: + type: object + inherits: [ProtectedUser] + config: + fields: + other: {type: String!} + +Bar: + type: object + inherits: [ProtectedUser] +``` diff --git a/src/Command/GraphQLDumpSchemaCommand.php b/src/Command/GraphQLDumpSchemaCommand.php index 549d51a1f..62261825a 100644 --- a/src/Command/GraphQLDumpSchemaCommand.php +++ b/src/Command/GraphQLDumpSchemaCommand.php @@ -60,6 +60,12 @@ protected function configure() InputOption::VALUE_NONE, 'Enabled classic json format: { "__schema": {...} }.' ) + ->addOption( + 'with-descriptions', + null, + InputOption::VALUE_NONE, + 'Dump schema including descriptions.' + ) ; } @@ -74,6 +80,7 @@ private function createFile(InputInterface $input) { $format = strtolower($input->getOption('format')); $schemaName = $input->getOption('schema'); + $includeDescription = $input->getOption('with-descriptions'); $file = $input->getOption('file') ?: $this->baseExportPath.sprintf('/../var/schema%s.%s', $schemaName ? '.'.$schemaName : '', $format); @@ -81,7 +88,7 @@ private function createFile(InputInterface $input) case 'json': $request = [ // TODO(mcg-web): remove silence deprecation notices after removing webonyx/graphql-php <= 0.11 - 'query' => @Introspection::getIntrospectionQuery(false), + 'query' => @Introspection::getIntrospectionQuery($includeDescription), 'variables' => [], 'operationName' => null, ]; diff --git a/src/Config/Processor/InheritanceProcessor.php b/src/Config/Processor/InheritanceProcessor.php index dc548f57c..008fbe9d7 100644 --- a/src/Config/Processor/InheritanceProcessor.php +++ b/src/Config/Processor/InheritanceProcessor.php @@ -121,7 +121,7 @@ private static function checkTypeExists($name, array $configs, $child) { if (!isset($configs[$name])) { throw new \InvalidArgumentException(sprintf( - 'Type %s inherits by %s not found.', + 'Type %s inherited by %s not found.', json_encode($name), json_encode($child) )); @@ -142,9 +142,9 @@ private static function checkAllowedInheritsTypes($name, array $config, array $a { if (empty($config['decorator']) && isset($config['type']) && !in_array($config['type'], $allowedTypes)) { throw new \InvalidArgumentException(sprintf( - 'Type %s can\'t inherits %s because %s is not allowed type (%s).', - json_encode($name), + 'Type %s can\'t inherit %s because its type (%s) is not allowed type (%s).', json_encode($child), + json_encode($name), json_encode($config['type']), json_encode($allowedTypes) )); diff --git a/src/Generator/TypeGenerator.php b/src/Generator/TypeGenerator.php index 7ef810c49..9f0f10489 100644 --- a/src/Generator/TypeGenerator.php +++ b/src/Generator/TypeGenerator.php @@ -187,6 +187,11 @@ protected function generateParentClassName(array $config) } } + protected function generateTypeName(array $config) + { + return $this->varExport($config['config']['name']); + } + public function compile($mode) { $cacheDir = $this->getCacheDir(); diff --git a/src/Resources/skeleton/TypeSystem.php.skeleton b/src/Resources/skeleton/TypeSystem.php.skeleton index e6c0f6f0e..ba436aac9 100644 --- a/src/Resources/skeleton/TypeSystem.php.skeleton +++ b/src/Resources/skeleton/TypeSystem.php.skeleton @@ -4,6 +4,7 @@ class extends { +const NAME = ; public function __construct(ConfigProcessor $configProcessor, GlobalVariables $globalVariables = null) { diff --git a/tests/Config/Processor/InheritanceProcessorTest.php b/tests/Config/Processor/InheritanceProcessorTest.php index 3854dcaa6..c4f6b291e 100644 --- a/tests/Config/Processor/InheritanceProcessorTest.php +++ b/tests/Config/Processor/InheritanceProcessorTest.php @@ -17,7 +17,7 @@ class InheritanceProcessorTest extends TestCase /** * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Type "toto" inherits by "bar" not found. + * @expectedExceptionMessage Type "toto" inherited by "bar" not found. */ public function testExtendsUnknownType() { @@ -53,7 +53,7 @@ public function testCircularExtendsType() /** * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Type "toto" can't inherits "bar" because "enum" is not allowed type (["object","interface"]). + * @expectedExceptionMessage Type "bar" can't inherit "toto" because its type ("enum") is not allowed type (["object","interface"]). */ public function testNotAllowedType() { diff --git a/tests/Functional/Command/GraphDumpSchemaCommandTest.php b/tests/Functional/Command/GraphDumpSchemaCommandTest.php index 99e01366d..5b391a228 100644 --- a/tests/Functional/Command/GraphDumpSchemaCommandTest.php +++ b/tests/Functional/Command/GraphDumpSchemaCommandTest.php @@ -51,6 +51,20 @@ public function testDump($format, $withFormatOption = true) ); } + public function testDumpWithDescriptions() + { + $file = $this->cacheDir.'/schema.json'; + $this->assertCommandExecution( + [ + '--file' => $file, + '--with-descriptions' => true, + ], + __DIR__.'/fixtures/schema.descriptions.json', + $file, + 'json' + ); + } + public function testClassicJsonFormat() { $file = $this->cacheDir.'/schema.json'; diff --git a/tests/Functional/Command/fixtures/schema.descriptions.json b/tests/Functional/Command/fixtures/schema.descriptions.json new file mode 100644 index 000000000..63d265259 --- /dev/null +++ b/tests/Functional/Command/fixtures/schema.descriptions.json @@ -0,0 +1,1391 @@ +{ + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "User", + "description": null, + "fields": [ + { + "name": "name", + "description": "the user name", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "friends", + "description": null, + "args": [ + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "friendConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "friendsForward", + "description": null, + "args": [ + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "userConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "friendsBackward", + "description": null, + "args": [ + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "userConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8\ncharacter sequences. The String type is most often used by GraphQL to\nrepresent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric\nvalues. Int can represent values between -(2^31) and 2^31 - 1. ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "friendConnection", + "description": "A connection to a list of items.", + "fields": [ + { + "name": "totalCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "edges", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "friendEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PageInfo", + "description": "Information about pagination in a connection.", + "fields": [ + { + "name": "hasNextPage", + "description": "When paginating forwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPreviousPage", + "description": "When paginating backwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startCursor", + "description": "When paginating backwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endCursor", + "description": "When paginating forwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "friendEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "friendshipTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "userConnection", + "description": "A connection to a list of items.", + "fields": [ + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "edges", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "userEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "userEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to\nrefetch an object or as key for a cache. The ID type appears in a JSON\nresponse as a String; however, it is not intended to be human-readable.\nWhen expected as an input type, any string (such as `\"4\"`) or integer\n(such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Float", + "description": "The `Float` scalar type represents signed double-precision fractional\nvalues as specified by\n[IEEE 754](http:\/\/en.wikipedia.org\/wiki\/IEEE_floating_point). ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onOperation", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onFragment", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onField", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": [ + "FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": "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\/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"" + } + ] + } + ] + } +}