graphql-erlang / graphql

Erlang GraphQL implementation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Erlang GraphQL Build Status

An Erlang implementation of Facebook's GraphQL.

This is the core GraphQL query parsing and execution engine which goal is to be transport, server and datastore agnostic.

Status

This is a work in progress, here's todo list:

  • Parser for GraphQL
  • Schema definition
  • Query execution
    • Pre-defined basic scalar types
    • Complex types (List, Object, Union, etc)
    • Arguments
    • Variables
    • Fragments and inline fragments in queries
    • Custom types
    • Parallel execution
    • Return maps option
  • Mutations
  • Subscriptions
  • Introspection
  • Directives
  • "Compile" and validate schema before start the app
  • GraphQL Guard (may be another app)
    • Calculate query/mutation complexity
    • White list of queries with big complexity

Installation

Rebar3 - hex package

    {deps, [
      {graphql, "0.2.10", {pkg, graphql_erlang}}
    ]}.

Rebar - git with version tag

{deps, [
    {graphql, "", {git, "https://github.com/graphql-erlang/graphql.git", {tag, "v0.2.10"}}}
]]

Get started

First - define you schema:

-module(graphql_example).
-export([schema/0]).
-include_lib("graphql/include/types.hrl").

schema() -> ?SCHEMA(#{
  query => query()
}).

query() -> ?OBJECT("QueryRoot", "Example Query", #{
  "greatings" => ?FIELD(?STRING, "Greating", #{
      "name" => ?ARG(?NON_NULL(?STRING) "The name of who you'd like to great")
    },
    fun(_ParrentObject, Args, _Context) ->
      maps:get(<<"name">>, Args)
    end
  )
}).

In handler - call query for schema with optional options:

Document = <<"query($name: String!) { greatings(name: $name) }">>,  % or list
Context = #{},  % you can pass request through context or whatever you want use in resolver acress the schema
InitValue = #{},  % root value, passed to query/mutation root resolver
VariableValues = #{  % variables passed with query
  <<"name">> => <<"world">>
},

Result = graphql:execute(graphql_example:schema(), Document, InitValue, Context),

io:format("Result: ~p~n", [Result]).  % #{data => [{<<"name">>, <<"world">>}]}

Types include

We provide macros for simple types definition. Include: -include_lib("graphql/include/types.hrl").

All values for types can be null except ?NON_NULL and ObjectType

Scalars

?BOOLEAN - boolean type. Represent atoms true/false.

?FLOAT and ?INT - represent float and integer.

?LIST(OfType) - represent list where OfType - inner list type. Example: ?LIST(?INT) - [Int]

?NON_NULL(OfType) - non nullable type - wrapper over nullable type. Example: ?NON_NULL(?LIST(?INT)) - [Int]!

?STRING - binary string

Complex types

?ENUM(Name, Description, EnumValues) - enumaration:

my_enum() -> ?ENUM("MyEnum", "MyEnumDescription", [
  % ?ENUM_VAL(InternalValue, EnumValue, Description, Options) % Options - optional 
  ?ENUM_VAL(1, "ONE", "REPRESENT 1"),
  ?DEPRECATED("Deprecation reason", ?ENUM_VAL(2, "TWO", "Deprecated exempla"))
]).

Deal with Object type:

?DEPRECATED(Reason, FieldOrEnumVal) - mark whole field or enum value deprecated

?OBJECT(Name, Fields) or ?OBJECT(Name, Description, Fields) - declare object type. Name and Description can be list - it automatically converts to binary. Fields should be map.

?FIELD(Type) or ?FIELD(Type, Description) or ?FIELD(Type, Description, Resolver) or ?FIELD(Type, Description, Args, Resolver)

?ARG(Type) or ?ARG(Type, Description) or ?ARG(Type, DefaultValue, Description) - define argument item

The most basic components of a GraphQL schema are object types, which just represent a kind of object you can fetch from your service, and what fields it has.

Creating object type example:

?OBJECT("Name", "Description", #{
  "field_name" => ?DEPRECATED("Human redable deprecation reason", ?FIELD(
    ?STRING,  % what type of value returned by resolver
    "Human redable description",  % optional - can be null
    #{  % map of field arguments
      "arg_name">> => ?ARG(?STRING, <<"Default value">>, "Optional description")        
    }
    fun() -> <<"resolved string">> end  % resolver function
  ))
}).

Resolver - function that resolves field value. Arity and arguments passing:

case erlang:fun_info(Resolver, arity) of
  {arity, 0} -> Resolver();
  {arity, 1} -> Resolver(ObjectValue);
  {arity, 2} -> Resolver(ObjectValue, ArgumentValues);
  {arity, 3} -> Resolver(ObjectValue, ArgumentValues, Context)
end.

where:

  • ObjectValue - result of the parent resolver (or InitialValue for query root)
  • ArgumentsValues - map of arguments. When argument not provided in query - value is 'null'
  • Context - context from parent resolver.

Resolver can return resolved value witch passed to scalar serialize function if type is scalar or as ObjectValue for each field in child object type

Resolver can also overwrite context for his childrens. In this case resolver may look like:

resolver => fun(_,_,Context) ->
  {overvrite_context,
    <<"resolved string">>,
    Context#{
      additional_context_data => <<"Data">>
    }
  }
end

NOTE: additional_context_data avaliable only for children object type in query

Deal with Union type

?UNION(Name, Description, PossibleTypes, ResolveTypeFun) - type can be one of the listed in PossibleTypes. PossibleTypes must be list of ObjectTypes.

Example:

Type definitions:

one() -> ?OBJECT("One", "Has field named one", #{
  "one" => ?ARG(?INT)
}).

two() -> ?OBJECT("Two", "Has field named two", #{
  "two" => ?ARG(?INT)
}).

union_default() -> ?UNION("UnionDefaultResolve", "Used default type resolver", [
  fun one/0,
  fun two/0
]).

union_custom() -> ?UNION("UnionCastomResolve", "Used custom type resolver", [
  fun one/0,
  fun two/0
], fun(Value, _ThisTypeDefinition)->
  Type = case maps:get(<<"one">>, Value, undefined) of
    undefined -> fun two/0;
    _ -> fun one/0
  end,
  {Type, Value}  % resolver should return tuple {ResolvedType, UnwrappedValue}
end).

Object definition:

test_unions() -> ?OBJECT("TestUnions", "", #{
  "default_resolver" => ?FIELD(?LIST(fun unions_default/0), "Default resolver example",
    % default resolver just compares first elem in tuple with possibleTypes in union  
    fun() ->
      [
        {fun one/0, #{<<"one">> => 1}},
        {fun two/0, #{<<"two">> => 2}},
        {fun one/0, #{<<"one">> => 3}}
      ]
    end
  ),
  
  "custom_resolver" => ?FIELD(?LIST(fun unions_custom/0), "Custom resolver example",
    fun()->
      [
        #{<<"one">> => 1},
        #{<<"two">> => 2},
        #{<<"one">> => 3}
      ]
    end
  )
}).

Custom scalar types

custom_type()-> #{
  kind => 'SCALAR',  % required kind for scalar type
  name => 'IntCustom',  % atom
  ofType => null,  % used for nested data types
  description => <<
    "The `Int` scalar type represents non-fractional signed whole numeric ",
    "values. Int can represent values between -(2^53 - 1) and 2^53 - 1 since ",
    "represented in JSON as double-precision floating point numbers specified ",
    "by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point)."
  >>,

  % this function serialize resolved value
  % Serialize(Result, FieldType, Context);
  serialize => fun serialize/3,
  
  % this function parse variable value
  % ParseValue(maps:get(VariableName, VariableValues, null), ArgumentType);
  parse_value => fun parse_value/2,
  
  % this function parse value defined in query
  % ParseLiteral(Value, ArgumentType)
  parse_literal => fun parse_literal/2
}.

Special thanks

Thanks graphql-elixir for great lex/yecc erlang parser :)

About

Erlang GraphQL implementation


Languages

Language:Erlang 100.0%