apigenie-generatorGraphQLserver absinthe

server absinthe

Generate an Absinthe server from a GraphQL schema (SDL).

Usage

apigenie-generator server absinthe [...OPTIONS] INPUT_FILE

The input is a GraphQL schema: a .graphql, .graphqls or .gql file. It must be valid and self-contained, and have a query type.

Description

The generator writes a Mix project (Elixir 1.18 or later) that depends on Absinthe 1.12, Absinthe.Plug, Plug, Bandit and Jason. The code passes mix format --check-formatted, mix compile --warnings-as-errors and mix credo --strict, and mix run --no-halt serves it.

File Contents
priv/graphql/schema.graphql the schema, as written
lib/<app>/schema.ex the schema module: import_sdl of the schema, and hydrate/2 attaching resolvers, type resolution and scalars
lib/<app>/schema/directives.ex the custom directives of the schema, declared in a prototype schema
lib/<app>/resolvers/<type>.ex a behaviour per object type with root fields or fields with arguments, with a callback per field
lib/<app>/resolvers.ex the lookup of the configured implementations
lib/<app>/type_resolver.ex the object types of the values of interfaces and unions
lib/<app>/scalars.ex the parsing and serialization of the custom scalars
lib/<app>/loader.ex the Dataloader source, with the dataloader option
lib/<app>/router.ex the Plug router: the GraphQL endpoint, and GraphiQL in the dev environment
lib/<app>/application.ex the application, starting Bandit

The schema

<Root>.Schema imports the schema from priv/graphql/schema.graphql with import_sdl, and recompiles when the file changes. Absinthe keeps everything the schema says:

  • descriptions of types, fields, arguments, enum values and input fields;
  • deprecations of fields, arguments, input fields and enum values;
  • default values, interfaces implementing interfaces, and @oneOf input types, which it enforces;
  • type extensions.

Two things need code next to the SDL:

  • Custom directives need a prototype schema. Absinthe applies only the directives its prototype schema declares, so <Root>.Schema.Directives declares the directives of the schema, along with the enums their arguments use.
  • Custom scalars need parse and serialize functions, which hydrate/2 attaches.

Resolvers and implementations

Root fields, and fields with arguments, are resolved by the module of their type: <Root>.Resolvers.Query, <Root>.Resolvers.Mutation, <Root>.Resolvers.Warehouse… Each one is a behaviour with a callback per field:

  • a root field’s callback receives the arguments (a map with atom keys, as Absinthe gives them) and the resolution;
  • a nested field’s callback receives the parent value first.
@callback track_shipment(args :: track_shipment_args(), resolution :: Absinthe.Resolution.t()) ::
            Resolvers.result()

Implement the behaviours and configure the implementations in config/config.exs. Regenerating never overwrites them:

config :carpet_express, CarpetExpress.Resolvers.Query, impl: MyApp.Query

A callback returns {:ok, value} or {:error, reason}. Fields without an implementation answer an error (“Query.trackShipment is not implemented”). The other fields read their value from the parent value with Absinthe’s default resolution: the key that is the snake_case name of the field, in a map or a struct.

Interfaces and unions

<Root>.TypeResolver resolves the object type of a value of an interface or a union:

  • a map with a __typename key resolves to the type it names;
  • other values go to the resolve_type(type, value, resolution) callback of the implementation configured for <Root>.TypeResolver.

Scalars

<Root>.Scalars has a parse_<scalar>/1 and a serialize_<scalar>/1 function per custom scalar. Until an implementation of its parse/2 and serialize/2 callbacks is configured, they pass string, number and boolean literals and values through as they are.

Router and GraphiQL

<Root>.Router parses JSON, form and multipart bodies (Absinthe.Plug.Parser) and forwards the endpoint’s path (/graphql) to Absinthe.Plug, which takes queries and mutations by POST and GET. When the application’s :graphiql setting is true, as config/dev.exs sets it, the router also serves GraphiQL at /graphiql. Bandit listens on 127.0.0.1:4000 in development and on all interfaces in production; the PORT environment variable sets the port.

Dataloader

With dataloader: true, context/1 puts a Dataloader in the context of the resolvers (resolution.context.loader), with a Dataloader.KV source named <Root>.Loader. Its batches go to the fetch(batch_key, ids) callback of the implementation configured for <Root>.Loader.

Limitations

  • Subscriptions get their resolvers, but the application has no subscription transport (Absinthe.Phoenix channels), so they aren’t served.
  • Absinthe’s introspection doesn’t expose the specifiedByURL of scalars, although the schema keeps @specifiedBy.
  • Absinthe can’t import extensions of a root type that the schema definition gives another name (schema { query: Root } with extend type Root). The generator reports this as an error.
  • The arguments of custom directives can only have built-in scalar and enum types.

Options

-c, –config

Path to the configuration file to use. See the “Configuration” section for more information.

–no-config

Doesn’t look for apigenie.yaml (or .apigenie.yaml) in the current directory and its parents.

-o, –output

Directory where the project is generated.

–package-name

Name of the Mix application, converted to snake_case. carpet-express gives the application carpet_express and the root module CarpetExpress. Defaults to the name of the output directory.

–package-version

Version of the Mix project; 0.1.0 by default.

-p, –property NAME=VALUE

Sets a generator option (see below), e.g. -p dataloader=true. Options given on the command line take precedence over the configuration file.

Configuration

version: '1.0'
commands:
  generate:
    server_absinthe:
      app: carpet_express
      path: /api/graphql
      dataloader: true
Option Type Default Description
app string name of the output directory Name of the Mix application (snake_case, the directory under lib/); also set by --package-name.
namespace string the application in PascalCase Root module of the generated modules, e.g. CarpetExpress.
path string /graphql Path of the GraphQL endpoint.
graphiql_path string /graphiql Path of GraphiQL, served in the dev environment.
dataloader boolean false Adds a Dataloader to the context of the resolvers.
dev_deps boolean true Lists credo as a dependency of mix.exs (only: [:dev, :test]).