amazeelabs / graphql_directives
Directive based GraphQL schemas for Drupal.
Installs: 3 521
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 4
Forks: 0
Open Issues: 2
Type:drupal-module
README
A directive-based approach to GraphQL schema implementations. Provides a 'Directable' schema plugin that loads a GraphQL schema definition file and implements it using directive plugins.
Usage
Create a GraphQL schema definition file, and annotate it with directives.
type Query { hello: String @value(json: "\"Hello world!\"") }
Configure a GraphQL server with the "Directable" schema plugin, and set the schema definition path to the created schema file.
Directive definitions will be automatically prepended to the schema. To support
IDE's with autocompletion and syntax checking, there is a drush
command to
generate a schema file with all directives and information where they are
implemented.
drush graphql:directives >> directives.graphqls
Chaining
Directives can be chained to combine reusable data producers. They are composed from left to right, meaning the output of the left directive is passed as parent value to its right neighbour.
type Query { # This will emit "three". list: String! @value(json: "[\"one\", \"two\", \"three\"]") @seek(pos: 2) }
Mapping
The @map
directive allows to map over the output of its left neighbour and
apply all directives on the right side to each item.
type Query { # This will emit ["a", "b"]. map: [String!]! @value(json: "[{\"x\": \"a\"},{\"x\": \"b\"}]") @map @prop(key: "x") }
Default values
Since Drupal's data structures can't guarantee integrity, the graphql schema
will enforce default values as much as possible. Whenever a type is used in a non-nullable position (no !
at the end
), it attempts to apply a default value if the value is null
. The
default value is determined by the type, e.g. 0
for Int
, false
for
Boolean
, ""
for String
and []
for list types like [String!]!
.
For custom types, interface, unions or scalars, the @default
directive can be
used to start a directive chain that generates a default value.
scalar MyScalar @default @value(json: "\"bar\"") type Query { # This will emit `''`. string: String! @value(json: "null") # This will emit `0`. int: Int! @value(json: "null") # This will emit `[]`. list: [String!]! @value(json: "null") # This will emit `bar` manual: MyScalar! @value(json: "null") }
Type resolution
Directives can also be used to resolve the runtime types of unions and interfaces. To do that, apply any directives that can be used to resolve field values to the interface or union. The chain of directives should resolve to a string value which will be treated as a type id.
union Letters @prop(key: "type") = A | B
This resolved type id will then be matched against object types annotated with
the @type
directive to retrieve the actual type.
type A @type(id: "a") { type: String! } type B @type(id: "b") { type: String! }
Argument handling
Directives can use the ArgumentTrait
to apply dynamic arguments. If the
directive argument equals $
, the current value will be passed as the argument
value. If its $
, followed by any characters, these characters will be used as
a key to retrieve the value from the current query arguments.
Arguments that implement this behaviour are marked to be (dynamic).
type Query { static: Post @loadEntity(type: "node", id: "1") parent: Post @value(json: "1") @loadEntity(type: "node", id: "$") argument(id: String!): Post @loadEntity(type: "node", id: "$id") }
Directives
@value
The @value
directive allows you to define a static value for a field as a JSON
encoded string.
type Query { hello: String @value(json: "\"Hello world!\"") }
@seek
Extracts a value from a list or iterable. The pos
argument marks the target
position.
type Query { # This will emit "three". list: String! @value(json: "[\"one\", \"two\", \"three\"]") @seek(pos: 2) }
@prop
Extracts a property from an object or map. The key
argument marks the target
key.
type Query { # This will emit "bar". prop: String! @value(json: "{\"foo\": \"bar\"}") @prop(key: "foo") }
@map
Iterate over the current result list and apply the following directives to each item.
type Query { # This will emit ["a", "b"]. map: [String!]! @value(json: "[{\"x\": \"a\"},{\"x\": \"b\"}]") @map @prop(key: "x") }
@type
Annotate an object type with a specific id that will be used for interface- and union type resolution.
union Letters @prop(key: "type") = A | B type A @type(id: "a") { type: String! } type B @type(id: "b") { type: String! }
@arg
Retrieve an arguments value and inject it as the current value that will be passed as parent to subsequent directives.
type Query { post(path: String!): Page @arg(name: "path") @route(path: "$") @loadEntity }
@route
Resolve a path (dynamic) to a Drupal Url
object.
type Query { post(path: String!): Page @route(path: "$path") @loadEntity }
@loadEntity
Load Drupal entities in various ways. If there are no arguments, it assumes that
the parent value contains a Url
object generated by @route
, and it attempts
to load the entity from there.
If used with id
or uuid
, an optional operation
argument allows to define
a specific operation for access checks.
type Query { post(path: String!): Page @route(path: "$path") @loadEntity }
Otherwise, it requires to define a static type
argument and either an id
(dynamic) or uuid
(dynamic) argument.
type Query { id(id: String!): Post @loadEntity(type: "node", id: "$id") uuid(uuid: String!): Post @loadEntity(type: "node", uuid: "$uuid") }
@resolveEntity[...]
Retrieve various simple properties of an entity. The following directives are supported:
@resolveEntityId
@resolveEntityUuid
@resolveEntityType
@resolveEntityBundle
@resolveEntityLabel
@resolveEntityPath
@resolveEntityLanguage
type Query { post(id: String!): Post @loadEntity(type: "node", id: "$id") } type Post { title: String! @resolveEntityLabel }
@resolveEntityTranslation
Retrieve as specific translation of an entity, defined by the lang
(dynamic)
argument.
type Query { post(id: String!, lang: String!): Post @loadEntity(type: "node", id: "$id") @resolveEntityTranslation(lang: "$lang") }
@resolveEntityTranslations
Retrieve all translations of an entity.
type Query { post(id: String!): Post @loadEntity(type: "node", id: "$id") } type Post { translations: [Post!]! @resolveEntityTranslations }
@resolveProperty
Retrieve a property of an entity by its path
argument.
type Post { body: String @resolveProperty(path: "body.value") }
@lang
Switch the current execution language for the remaining subtree below the
current field. Accepts either a code
argument (dynamic) or, if omitted, uses
the parent value. If the latter is a string, it will be used as-is, if its an
instance of TranslatableInteface
, the language is derived from there.
type Query { post(id: String!): Post @loadEntity(type: "node", id: "$id") @lang }
@resolveMenuItems
Retrieve all items of a menu entity. Accepts an optional max_level
argument
that caps the maximum number of menu levels. The tree is flattened to a list, to
avoid the necessity of nested fragments. The @resolveMenuItemId
and
@resolveMenuItemParentId
directives should be used to reconstruct the tree in
the consumer. The list of menu items is also filtered by language, respecting
the current execution context language, as it can be controlled by @lang
.
type Query { menu: Limited! @loadEntity(type: "menu", id: "main", operation: "view label") } type Menu { items: [MenuItem!]! @lang(code: "fr") @resolveMenuItems(max_level: 2) }
@resolveMenuItem[...]
Various menu item properties.
@resolveMenuItemId
@resolveMenuItemParentId
@resolveMenuItemLabel
@resolveMenuItemUrl
@resolveEntityReference
& @resolveEntityReferenceRevisions
Resolve referenced entities attached to a given field
. Will attempt to
retrieve translations matching the current host entity.
type Query { post(id: String!): Post @loadEntity(type: "node", id: "$id") @lang } type Post { title: String! @resolveEntityLabel related: [Post!]! @resolveEntityReference(field: "field_related") }
Extending
To add custom directives, create a module and add new Plugins in the
src/Plugin/GraphQL/Directive
directory. The plugins "id" will be the handle to
invoke it in the schema, without the @
prefix.
<?php namespace Drupal\my_directives\Plugin\GraphQL\Directive; use Drupal\graphql_directives\DirectiveInterface; /** * @Directive( * id="echo", * description="Return the same string that you put in.", * arguments = { * "input" = "String!", * } * ) */ class EchoDirective extends PluginBase implements DirectiveInterface { /** * {@inheritdoc} */ public function buildResolver( ResolverBuilder $builder, array $arguments ) : ResolverInterface { return $builder->fromValue($arguments['input']); } }
Schema Extensions
The module provides a DirectableSchemaExtensionPluginBase
class that can be
used to create schema extensions that react to directives in the parent schema
definition. A schema extension plugin for the Drupal GraphQL module provides two
schema definitions: one for the "base" schema and one for the actual extensions.
In case of directable schema extension, the base schema definition should
contain the directives while the extension schema defines the derived types and
fields.
For a very simple example, please refer to the graphql_directives_test
module.