diff --git a/doc/code.asciidoc b/doc/code.asciidoc index 9530b26..8a84270 100644 --- a/doc/code.asciidoc +++ b/doc/code.asciidoc @@ -59,7 +59,7 @@ sections for their description. === Application `sw_core` -.`priv/sw.schmea` +.`priv/sw.schema` The schema definition file which can be read by the {project} system. It defines the schema rules for the Star Wars API. diff --git a/doc/object_resolution.asciidoc b/doc/object_resolution.asciidoc index 400248c..5fbde77 100644 --- a/doc/object_resolution.asciidoc +++ b/doc/object_resolution.asciidoc @@ -266,7 +266,7 @@ NOTE: An alternative implementation of the current object for Starships would be to return a `map()` which had all the relevant fields from the `#transport{}` and `#starship{}` rows merged. In short, it would be the same as if you had executed `SELECT * FROM -Starship INNER JOIN Tranport USING (Id)`. It is worth examining if a +Starship INNER JOIN Transport USING (Id)`. It is worth examining if a different representation will help in a given situation. ==== Id handling @@ -469,7 +469,7 @@ return {ok, [{ok, A}, {error, Reason}, {ok, C}]} ---- -to signify that the two values `A` and `C` suceeded but `B` failed. If +to signify that the two values `A` and `C` succeeded but `B` failed. If the data fetch fails as a whole you can of course fail the full field by returning `{error, Reason}`. @@ -487,7 +487,7 @@ include::{sw_core}/src/sw_core_species.erl[tags=execute] ---- Since the result can't fail, we wrap every result in an `{ok, EC}` to -state that every eye colors loaded succesfully. +state that every eye colors loaded successfully. .Alternative representation **** @@ -631,7 +631,7 @@ and it won't cost bandwidth in data transfer. TIP: The example here describes a typical invocation: load the objects you operate on and make sure every object is valid. Then use <> in the `_Ctx` to check if the desired operation is -valid on the object. Next, execute a transaction and if succesful, +valid on the object. Next, execute a transaction and if successful, return all the loaded objects in the *...Payload* result. This load-then-auth-then-txn invocation is very common in GraphQL. diff --git a/doc/schema.asciidoc b/doc/schema.asciidoc index f5d24fc..369e239 100644 --- a/doc/schema.asciidoc +++ b/doc/schema.asciidoc @@ -203,7 +203,7 @@ result type of the given field is the return value of the mutation. This return type, often called the _payload_ contains other objects in the graph. It allows a client to run a mutation on the server side and then query on the data it just changed. This corresponds to the -situtation in RESTful models where a POST provides a `location:` +situation in RESTful models where a POST provides a `location:` response header containing the URI of the newly created object. But as we want to avoid a roundtrip, we "`bake`" the query into the mutation in GraphQL. diff --git a/doc/system_tour.asciidoc b/doc/system_tour.asciidoc index 26c4f47..48e9b1c 100644 --- a/doc/system_tour.asciidoc +++ b/doc/system_tour.asciidoc @@ -130,7 +130,7 @@ connections as we are here: include::{sw_test}/sw_SUITE_data/advanced.query[] ---- -The `fragment` parts allows your queries to re-use different subsets +The `fragment` parts allows your queries to reuse different subsets of a larger query again and again. We use this here to show off that capability of GraphQL. The result follows the structure of the query: @@ -174,7 +174,7 @@ include::{sw_test}/sw_SUITE_data/mutation.result[] ---- <1> The server reflects back the unique client-generated Id for correlation purposes. -<2> The Id migth be different depending on how many *Faction* objects +<2> The Id might be different depending on how many *Faction* objects you created. <3> We have yet to assign any starships to the faction, so the count is currently 0. diff --git a/doc/tricks.asciidoc b/doc/tricks.asciidoc index 3b6ddd1..c94ecde 100644 --- a/doc/tricks.asciidoc +++ b/doc/tricks.asciidoc @@ -168,7 +168,7 @@ TBD TBD [[lazy-evaluation]] -=== Lazy Evalution +=== Lazy Evaluation If you have data where computation is circular, you will have to make sure you don't build an infinite loop in the data. This system has diff --git a/index.html b/index.html index 579e464..64d4a86 100644 --- a/index.html +++ b/index.html @@ -2,30 +2,26 @@ - + - + Erlang GraphQL Tutorial - + +
-
query PlanetQuery {
+
query PlanetQuery {
   node(id:"UGxhbmV0OjE=") { (1)
     ... on Planet { (2)
       id (3)
@@ -1055,7 +1064,7 @@ 

First query

-
{
+
{
   "data": {
     "node": {
       "climate": "arid",
@@ -1088,7 +1097,7 @@ 

More advanced queries

-
query Q {
+
query Q {
   node(id:"UGxhbmV0OjE=") {
     ... on Planet {
       id
@@ -1138,13 +1147,13 @@ 

More advanced queries

-

The fragment parts allows your queries to re-use different subsets +

The fragment parts allows your queries to reuse different subsets of a larger query again and again. We use this here to show off that capability of GraphQL. The result follows the structure of the query:

-
{
+
{
   "data": {
     "node": {
       "climate": "arid",
@@ -1225,7 +1234,7 @@ 

Simple Mutations

-
mutation IntroduceFaction($input: IntroduceFactionInput!) {
+
mutation IntroduceFaction($input: IntroduceFactionInput!) {
   introduceFaction(input: $input) {
     clientMutationId
     faction {
@@ -1248,7 +1257,7 @@ 

Simple Mutations

-
{
+
{
   "input": {
     "clientMutationId": "D9A5939A-DF75-4C78-9B32-04C1C64F9D9C", (1)
     "name": "Rebels"
@@ -1271,7 +1280,7 @@ 

Simple Mutations

-
{
+
{
   "data": {
     "introduceFaction": {
       "clientMutationId": "D9A5939A-DF75-4C78-9B32-04C1C64F9D9C", (1)
@@ -1296,7 +1305,7 @@ 

Simple Mutations

2 -The Id migth be different depending on how many Faction objects +The Id might be different depending on how many Faction objects you created. @@ -1312,7 +1321,7 @@

Simple Mutations

-
query FactionQuery {
+
query FactionQuery {
   node(id: "RmFjdGlvbjoxMDAx") {
      ... on Faction {
        id
@@ -1355,11 +1364,11 @@ 

More complex mutations

-
mutation IntroduceBWing {
+
mutation IntroduceBWing {
   introduceStarship(input:
     { costInCredits: 5.0, (1)
-      length: 20.0,
-      crew: "1",
+      length: 20.0, 
+      crew: "1", 
       name: "B-Wing",
       faction: "RmFjdGlvbjoxMDAx", (2)
       starshipClass: "fighter"}) {
@@ -1372,7 +1381,7 @@ 

More complex mutations

name ships { totalCount - + edges { node { id name @@ -1410,7 +1419,7 @@

More complex mutations

-
{
+
{
   "data": {
     "introduceStarship": {
       "faction": {
@@ -1751,7 +1760,7 @@ 

Setting up an initial Mnesia schem

-
create_schema() ->
+
create_schema() ->
     mnesia:create_schema([node()]),
     application:ensure_all_started(mnesia),
     ok = create_fixture(disc_copies, "fixtures"),
@@ -1772,7 +1781,7 @@ 

Setting up an initial Mnesia schem

-
create_tables(Type) ->
+
create_tables(Type) ->
     {atomic, ok} =
         mnesia:create_table(
           starship,
@@ -1795,7 +1804,7 @@ 

Setting up an initial Mnesia schem

-
-record(planet,
+
-record(planet,
         {id :: integer(),
          edited :: calendar:datetime(),
          climate :: binary(),
@@ -1835,7 +1844,7 @@ 

Populating the database

-
json_to_planet(
+
json_to_planet(
   #{ <<"pk">> := ID,
      <<"fields">> := #{
          <<"edited">> := Edited,
@@ -1873,7 +1882,7 @@ 

Populating the database

-
populate_planets(Terms) ->
+
populate_planets(Terms) ->
     Planets = [json_to_planet(P) || P <- Terms],
     Txn = fun() ->
                   [mnesia:write(P) || P <- Planets],
@@ -1890,7 +1899,7 @@ 

Populating the database

-
populate(File, Fun) ->
+
populate(File, Fun) ->
     {ok, Data} = file:read_file(File),
     Terms = jsx:decode(Data, [return_maps]),
     Fun(Terms).
@@ -1929,7 +1938,7 @@ 

Creating a FALLBACK for the datab

-
$ cp FALLBACK.BUP db/FALLBACK.BUP
+
$ cp FALLBACK.BUP db/FALLBACK.BUP
@@ -2016,7 +2025,7 @@

Identity encoding

-
encode({Tag, ID}) ->
+
encode({Tag, ID}) ->
     BinTag = atom_to_binary(Tag, utf8),
     IDStr = integer_to_binary(ID),
     base64:encode(<<BinTag/binary, ":", IDStr/binary>>).
@@ -2030,7 +2039,7 @@

Identity encoding

-
decode(Input) ->
+
decode(Input) ->
     try
         Decoded = base64:decode(Input),
         case binary:split(Decoded, <<":">>) of
@@ -2069,7 +2078,7 @@ 

The Node Interface

-
+description(text: "Relay Modern Node Interface")
+
+description(text: "Relay Modern Node Interface")
 interface Node {
   +description(text: "Unique Identity of a Node")
   id : ID!
@@ -2135,7 +2144,7 @@ 

Planets

-
type Planet implements Node {
+
type Planet implements Node {
   name : String
   diameter : Int
   rotationPeriod : Int
@@ -2164,14 +2173,14 @@ 

Queries & Mutations

Query Object

-

All GraphQL queries are either a query or a mutation.[1] +

All GraphQL queries are either a query or a mutation.[1] Correspondingly, the schema specification contains entries for two (output) objects, which are commonly called Query and Mutation respectively. For example, the query object looks like:

-
type Query {
+
type Query {
   +description(text: "Relay Modern specification Node fetcher")
   node(id : ID!) : Node
   +description(text: "Fetch a starship with a given Id")
@@ -2217,7 +2226,7 @@ 

Query Object

-
query StarShipQuery($id : ID!) {
+
query StarShipQuery($id : ID!) {
     node(id: $id) {
        __typename
        id
@@ -2264,7 +2273,7 @@ 

Mutation Object

This return type, often called the payload contains other objects in the graph. It allows a client to run a mutation on the server side and then query on the data it just changed. This corresponds to the -situtation in RESTful models where a POST provides a location: +situation in RESTful models where a POST provides a location: response header containing the URI of the newly created object. But as we want to avoid a roundtrip, we “bake” the query into the mutation in GraphQL.

@@ -2272,7 +2281,7 @@

Mutation Object

sw.schema
-
type Mutation {
+
type Mutation {
   introduceFaction(input: IntroduceFactionInput!)
     : IntroduceFactionPayload
   introduceStarship(input: IntroduceStarshipInput!)
@@ -2294,7 +2303,7 @@ 

Mutation Object

-
type IntroduceStarshipPayload {
+
type IntroduceStarshipPayload {
   clientMutationId : String
   faction : Faction
   starship : Starship
@@ -2313,7 +2322,7 @@ 

Mutation Object

-
mutation IF($input : IntroduceStarshipInput!) {
+
mutation IF($input : IntroduceStarshipInput!) {
   introduceStarship(input: $input) {
     starship {
       id
@@ -2382,7 +2391,7 @@ 

Input objects

-
type Mutation {
+
type Mutation {
     introduceStarship(name: String,
                       class: String,
                       manufacturers: [String], ....)
@@ -2397,7 +2406,7 @@ 

Input objects

-
input IntroduceStarshipInput {
+
input IntroduceStarshipInput {
   clientMutationId : String
 
   name : String
@@ -2507,7 +2516,7 @@ 

Interfaces & Unions

-
interface Transport {
+
interface Transport {
   id : ID!
   edited : DateTime
   consumables : String
@@ -2530,7 +2539,7 @@ 

Interfaces & Unions

-
+description(text: "Representation of Star Ships")
+
+description(text: "Representation of Star Ships")
 type Starship implements Node, Transport {
   id : ID!
   name : String
@@ -2623,7 +2632,7 @@ 

Loading the Schema

-
load_schema() ->
+
load_schema() ->
     {ok, SchemaFile} = application:get_env(sw_core, schema_file),
     PrivDir = code:priv_dir(sw_core),
     {ok, SchemaData} = file:read_file(
@@ -2699,7 +2708,7 @@ 

Root setup

-
setup_root() ->
+
setup_root() ->
     Root = {root,
             #{ query => 'Query',
                mutation => 'Mutation',
@@ -2725,7 +2734,7 @@ 

Mapping rules

-
mapping_rules() ->
+
mapping_rules() ->
     #{
        scalars => #{ default => sw_core_scalar },
        interfaces => #{ default => sw_core_type },
@@ -2777,7 +2786,7 @@ 

Interfaces & Unions

-
query Q($nid : ID!) {
+
query Q($nid : ID!) {
   node(id: $nid) {
     ... on Starship {
       model
@@ -2925,7 +2934,7 @@ 

Scalar Resolution

Schema Definition (sw.schema)
-
scalar DateTime
+
scalar DateTime
@@ -2939,7 +2948,7 @@

Scalar Resolution

Coercion code (sw_core_scalar.erl)
-
-module(sw_core_scalar).
+
-module(sw_core_scalar).
 
 -export([input/2, output/2]).
 
@@ -3000,7 +3009,7 @@ 

Scalar Resolution

Built-in scalars such as Int, Float, String, Bool are handled by the system internally and do not currently undergo Scalar conversion. A special case exists for Int and Float. These are coerced -between automatically if it is safe to do so.[2] +between automatically if it is safe to do so.[2] @@ -3012,7 +3021,7 @@

Example

-
query SpeciesQ {
+
query SpeciesQ {
   node(id: "U3BlY2llczoxNQ==") {
     id
     ... on Species {
@@ -3028,7 +3037,7 @@ 

Example

-
{
+
{
   "data": {
     "node": {
       "created": "2014-12-20T09:48:02Z",
@@ -3152,7 +3161,7 @@ 

Defining enums

-
enum Episode {
+
enum Episode {
   PHANTOM
   CLONES
   SITH
@@ -3194,7 +3203,7 @@ 

Coercion

-
-module(sw_core_enum).
+
-module(sw_core_enum).
 
 -export([input/2, output/2]).
 
@@ -3260,7 +3269,7 @@ 

Usage Example

-
query FilmQuery {
+
query FilmQuery {
   filmByEpisode(episode: JEDI) {
     id
     title
@@ -3280,7 +3289,7 @@ 

Usage Example

-
{ "data" :
+
{ "data" :
   { "filmByEpisode" :
       { "episode" : "JEDI",
         "episodeID" : 6,
@@ -3300,7 +3309,7 @@ 

Usage Example

-
{ "episode" : "EMPIRE",
+
{ "episode" : "EMPIRE",
   ...
 }
@@ -3332,7 +3341,7 @@

Type Resolution

-
-spec execute(Term) -> {ok, Type} | {error, Reason}
+
-spec execute(Term) -> {ok, Type} | {error, Reason}
   when
     Term :: term(),
     Type :: atom(),
@@ -3349,7 +3358,7 @@ 

Type Resolution

-
execute(#film{}) -> {ok, 'Film'};
+
execute(#film{}) -> {ok, 'Film'};
 execute(#person{}) -> {ok, 'Person'};
 execute(#planet{}) -> {ok, 'Planet'};
 execute(#species{}) -> {ok, 'Species'};
@@ -3432,7 +3441,7 @@ 

Object Resolution

-
type Query {
+
type Query {
   +description(text: "Relay Modern specification Node fetcher")
   node(id : ID!) : Node
   +description(text: "Fetch a starship with a given Id")
@@ -3512,7 +3521,7 @@ 

Execution

-
-spec execute(Ctx, Obj, Field, Args) ->
+
-spec execute(Ctx, Obj, Field, Args) ->
                 {ok, Result} | {error, Reason}
   when
     Ctx :: context(), % (1)
@@ -3626,7 +3635,7 @@ 

Execution

-
query {
+
query {
   node(id: "SOMEID") {
      __typename
      id
@@ -3708,7 +3717,7 @@ 

Handling Planets

-
execute(_Ctx, #planet { id = PlanetId } = Planet, Field, Args) ->
+
execute(_Ctx, #planet { id = PlanetId } = Planet, Field, Args) ->
     case Field of
         <<"id">> -> {ok, sw_core_id:encode({'Planet', Planet#planet.id})};
         <<"edited">> -> {ok, Planet#planet.edited};
@@ -3733,7 +3742,7 @@ 

Handling Planets

-
execute(Ctx, Obj, Field, Args) ->
+
execute(Ctx, Obj, Field, Args) ->
     maps:get(Field, Obj, {ok, null}).
@@ -3758,7 +3767,7 @@

Handling Starships

-
execute(_Ctx, #{ starship := #starship { id = StarshipId } = Starship,
+
execute(_Ctx, #{ starship := #starship { id = StarshipId } = Starship,
                  transport := Transport }, Field, Args) ->
     case Field of
         <<"id">> ->
@@ -3812,7 +3821,7 @@ 

Handling Starships

Starships would be to return a map() which had all the relevant fields from the #transport{} and #starship{} rows merged. In short, it would be the same as if you had executed SELECT * FROM -Starship INNER JOIN Tranport USING (Id). It is worth examining if a +Starship INNER JOIN Transport USING (Id)
. It is worth examining if a different representation will help in a given situation. @@ -3857,7 +3866,7 @@

Loading Data

-
execute(_Ctx, _DummyObj, <<"node">>, #{ <<"id">> := ID }) ->
+
execute(_Ctx, _DummyObj, <<"node">>, #{ <<"id">> := ID }) ->
     load_node(any, ID);
 ...
@@ -3867,7 +3876,7 @@

Loading Data

-
load_node(Types, ID) when is_binary(ID) ->
+
load_node(Types, ID) when is_binary(ID) ->
     case sw_core_id:decode(ID) of
         {ok, Decoded} ->
             load_node_(Types, Decoded);
@@ -3902,7 +3911,7 @@ 

DB Loading

-
record_of('Film') -> film;
+
record_of('Film') -> film;
 record_of('Person') -> person;
 record_of('Planet') -> planet;
 record_of('Species') -> species;
@@ -3927,7 +3936,7 @@ 

DB Loading

-
...
+
...
 load(Type, ID) ->
     MType = record_of(Type),
     F = fun() ->
@@ -3978,7 +3987,7 @@ 

Loading Complex objects

-
load('Starship', ID) ->
+
load('Starship', ID) ->
     F = fun() ->
                 [Transport] = mnesia:read(transport, ID, read),
                 [Starship]  = mnesia:read(starship, ID, read),
@@ -4026,7 +4035,7 @@ 

Walking in the Graph

-
execute(_, _, Field, _) ->
+
execute(_, _, Field, _) ->
     case Field of
         ...;
         <<"residentConnection">> ->
@@ -4078,7 +4087,7 @@ 

Default Mapping

-
-module(sw_core_object).
+
-module(sw_core_object).
 -export([execute/4]).
 
 %% Assume we are given a map(). Look up the field in the map. If not
@@ -4135,11 +4144,11 @@ 

Resolving lists

-
{ok, [{ok, A}, {error, Reason}, {ok, C}]}
+
{ok, [{ok, A}, {error, Reason}, {ok, C}]}
-

to signify that the two values A and C suceeded but B failed. If +

to signify that the two values A and C succeeded but B failed. If the data fetch fails as a whole you can of course fail the full field by returning {error, Reason}.

@@ -4153,7 +4162,7 @@

Resolving lists

-
execute(_Ctx, #species { id = Id } = Species, Field, Args) ->
+
execute(_Ctx, #species { id = Id } = Species, Field, Args) ->
     case Field of
         <<"id">> -> {ok, sw_core_id:encode({'Species', Id})};
         <<"name">> -> {ok, Species#species.name};
@@ -4164,7 +4173,7 @@ 

Resolving lists

Since the result can’t fail, we wrap every result in an {ok, EC} to -state that every eye colors loaded succesfully.

+state that every eye colors loaded successfully.

@@ -4191,13 +4200,13 @@

Mutations

Object resolution of mutations works the same way as object resolution for every other kind of type in the graph. When the Mutation object -is request in a mutation, the field resovler will run and call an +is request in a mutation, the field resolver will run and call an execute/4 function for the mutation. In our example, our mutation code looks like:

-
execute(Ctx, _, Field, #{ <<"input">> := Input}) ->
+
execute(Ctx, _, Field, #{ <<"input">> := Input}) ->
     with_client_mutation(Ctx, Field, Input).
 
 with_client_mutation(Ctx, Field, Input) ->
@@ -4245,7 +4254,7 @@ 

Mutations

-
execute_mutation(Ctx, <<"introduceFaction">>, Input) ->
+
execute_mutation(Ctx, <<"introduceFaction">>, Input) ->
     {ok, Faction} = sw_core_faction:introduce(Ctx, Input),
     {ok, #{ <<"faction">> => Faction }};
 execute_mutation(Ctx, <<"introduceStarship">>, Input) ->
@@ -4311,7 +4320,7 @@ 

Introducing Factions

-
introduce(_Ctx, #{ <<"name">> := Name }) ->
+
introduce(_Ctx, #{ <<"name">> := Name }) ->
     ID = sw_core_db:nextval(faction), % (1)
     Faction = #faction { id = ID, name = Name }, % (2)
     Txn = fun() ->
@@ -4355,7 +4364,7 @@ 

Introducing Starships

-
introduce(_Ctx, #{ <<"name">> := Name,
+
introduce(_Ctx, #{ <<"name">> := Name,
                    <<"model">> := Model,
                    <<"starshipClass">> := Class,
                    <<"manufacturers">> := Manufacturers,
@@ -4463,7 +4472,7 @@ 

Introducing Starships

The example here describes a typical invocation: load the objects you operate on and make sure every object is valid. Then use Authorization in the _Ctx to check if the desired operation is -valid on the object. Next, execute a transaction and if succesful, +valid on the object. Next, execute a transaction and if successful, return all the loaded objects in the …​Payload result. This load-then-auth-then-txn invocation is very common in GraphQL. @@ -4484,7 +4493,7 @@

Anatomy of a query

-
query PlanetQuery {
+
query PlanetQuery {
   node(id: "UGxhbmV0OjI=") {
     ...PlanetFragment
   }
@@ -4512,7 +4521,7 @@ 

Anatomy of a query

-
sw_core_query:execute(Ctx, Obj,
+
sw_core_query:execute(Ctx, Obj,
   <<"node">>, #{ <<"id">> => <<"UGxhbmV0OjI=">> }),
@@ -4529,7 +4538,7 @@

Anatomy of a query

-
sw_core_type:execute(#planet{} = Planet),
+
sw_core_type:execute(#planet{} = Planet),
@@ -4571,7 +4580,7 @@

Transports

However, newer parts of GraphQL which is currently being tried out has -support for delayed and streamed responses.[3] Because +support for delayed and streamed responses.[3] Because of this, GraphQL will need a more powerful transport for those kinds of features.

@@ -4600,7 +4609,7 @@

Cowboy Handler

-
    Dispatch =
+
    Dispatch =
         cowboy_router:compile(
           [{'_',
             [{"/assets/[...]", cowboy_static,
@@ -4646,7 +4655,7 @@ 

Handler code

-
-module(sw_web_graphql_handler).
+
-module(sw_web_graphql_handler).
 
 %% Cowboy Handler Interface
 -export([init/2]).
@@ -4679,7 +4688,7 @@ 

Initialization & REST handling

init/2
-
init(Req, {priv_file, _, _} = PrivFile) ->
+
init(Req, {priv_file, _, _} = PrivFile) ->
     {cowboy_rest,
      Req,
      #{ index_location => PrivFile }}.
@@ -4703,7 +4712,7 @@

Initialization & REST handling

allowed_methods/2
-
allowed_methods(Req, State) ->
+
allowed_methods(Req, State) ->
     {[<<"GET">>, <<"POST">>], Req, State}.
@@ -4716,7 +4725,7 @@

Initialization & REST handling

content_types_accepted/2
-
content_types_accepted(Req, State) ->
+
content_types_accepted(Req, State) ->
     {[
         {{<<"application">>, <<"json">>, []}, from_json}
     ], Req, State}.
@@ -4730,7 +4739,7 @@

Initialization & REST handling

content_types_provided/2
-
content_types_provided(Req, State) ->
+
content_types_provided(Req, State) ->
     {[
         {{<<"application">>, <<"json">>, []}, to_json},
         {{<<"text">>, <<"html">>, []}, to_html}
@@ -4762,7 +4771,7 @@ 

Initialization & REST handling

charsets_provided/2
-
charsets_provided(Req, State) ->
+
charsets_provided(Req, State) ->
     {[<<"utf-8">>], Req, State}.
@@ -4777,7 +4786,7 @@

Initialization & REST handling

resource_exists/2
-
resource_exists(#{ method := <<"GET">> } = Req, State) ->
+
resource_exists(#{ method := <<"GET">> } = Req, State) ->
     {true, Req, State};
 resource_exists(#{ method := <<"POST">> } = Req, State) ->
     {false, Req, State}.
@@ -4807,7 +4816,7 @@

Processing

to_html/2
-
to_html(Req, #{ index_location :=
+
to_html(Req, #{ index_location :=
                     {priv_file, App, FileLocation}} = State) ->
     Filename = filename:join(code:priv_dir(App), FileLocation),
     {ok, Data} = file:read_file(Filename),
@@ -4849,7 +4858,7 @@ 

Processing

to_json/2 & from_json/2
-
json_request(Req, State) ->
+
json_request(Req, State) ->
     case gather(Req) of
         {error, Reason} ->
             err(400, Reason, Req, State);
@@ -4881,7 +4890,7 @@ 

Processing

-
gather(Req) ->
+
gather(Req) ->
     {ok, Body, Req2} = cowboy_req:read_body(Req),
     Bindings = cowboy_req:bindings(Req2),
     try jsx:decode(Body, [return_maps]) of
@@ -4912,7 +4921,7 @@ 

Processing

-
document([#{ <<"query">> := Q }|_]) -> Q;
+
document([#{ <<"query">> := Q }|_]) -> Q;
 document([_|Next]) -> document(Next);
 document([]) -> undefined.
@@ -4923,7 +4932,7 @@

Processing

-
variables([#{ <<"variables">> := Vars} | _]) ->
+
variables([#{ <<"variables">> := Vars} | _]) ->
   if
       is_binary(Vars) ->
           try jsx:decode(Vars, [return_maps]) of
@@ -4960,11 +4969,11 @@ 

Processing

The request processing starts by a parsing step. If that step fails, we can exit with an error. If it succeeds, we proceed by running pre-processing. The output of the parsing step is an abstract syntax -tree.[4]

+tree.[4]

-
run_request(#{ document := undefined }, Req, State) ->
+
run_request(#{ document := undefined }, Req, State) ->
     err(400, no_query_supplied, Req, State);
 run_request(#{ document := Doc} = ReqCtx, Req, State) ->
     case graphql:parse(Doc) of
@@ -4984,7 +4993,7 @@ 

Processing

-
run_preprocess(#{ document := AST } = ReqCtx, Req, State) ->
+
run_preprocess(#{ document := AST } = ReqCtx, Req, State) ->
     try
         Elaborated = graphql:elaborate(AST), % (1)
         {ok, #{
@@ -5018,7 +5027,7 @@ 

Processing

type checking is a new syntax tree, AST2 on which scalar conversion has been run (for static variables) as an optimization; and a FunEnv which is the type scheme for each operation in the -query.[5] +query.[5] 3 @@ -5040,7 +5049,7 @@

Processing

-
run_execute(#{ document := AST,
+
run_execute(#{ document := AST,
                fun_env := FunEnv,
                vars := Vars,
                operation_name := OpName }, Req, State) ->
@@ -5119,7 +5128,7 @@ 

Errors

-
err(Code, Msg, Req, State) ->
+
err(Code, Msg, Req, State) ->
     Formatted = iolist_to_binary(io_lib:format("~p", [Msg])),
     Err = #{ type => error,
              message => Formatted },
@@ -5164,7 +5173,7 @@ 

GraphiQL

-
$ make release
+
$ make release
 $ _build/default/rel/sw/bin/sw console
@@ -5210,7 +5219,7 @@

GraphiQL

-
query PlanetQuery {
+
query PlanetQuery {
     node(id: "UGxhbmV0OjM=") {
       ... on planet {
         id
@@ -5284,7 +5293,7 @@ 

Node Interface

Taken together, this implements the object identification -specification.[6]

+specification.[6]

@@ -5333,7 +5342,7 @@

Pagination

-
select_(Elements,
+
select_(Elements,
          #{ <<"first">> := F,
             <<"last">> := L,
             <<"after">> := After,
@@ -5346,7 +5355,7 @@ 

Pagination

Sliced = apply_cursors_to_edges(After, Before, lists:zip(Elements, Positions)), Window = edges_to_return(First, Last, Sliced), % (4) - Edges = format(Window), + Edges = format(Window), %% Build PageInfo (5) PageInfo = #{ @@ -5404,7 +5413,7 @@

Pagination

-
apply_cursors_to_edges(null, null, Elements) ->
+
apply_cursors_to_edges(null, null, Elements) ->
     Elements;
 apply_cursors_to_edges(null, Before, Elements) ->
     Pos = unpack_cursor(Before),
@@ -5443,7 +5452,7 @@ 

Pagination

-
edges_to_return(First, null, Window) ->
+
edges_to_return(First, null, Window) ->
     Sz = length(Window),
     case Sz - First of
         K when K =< 0 -> Window;
@@ -5464,9 +5473,9 @@ 

Pagination

-
has_previous(_Sliced, null) -> false;
+
has_previous(_Sliced, null) -> false;
 has_previous(Sliced, Last) -> length(Sliced) > Last.
-
+    
 has_next(_Sliced, null) -> false;
 has_next(Sliced, First) -> length(Sliced) > First.
@@ -5477,7 +5486,7 @@

Pagination

-
pack_cursor(Pos) ->
+
pack_cursor(Pos) ->
     base64:encode(integer_to_binary(Pos)).
@@ -5513,7 +5522,7 @@

Security

large.

-

Limiting Clients—​Stored Procedures

+

Limiting Clients—​Stored Procedures

Work in Progress
@@ -5635,7 +5644,7 @@

Object Representation

A common convention is to use a special atom such as -'$tag'.[7] You can then add data under that key in the map which +'$tag'.[7] You can then add data under that key in the map which is useful to the GraphQL backend only.

@@ -5662,7 +5671,7 @@

Avoid Isomorphic representations

continual translation of backend data to the GraphQL schema. A common solution to this problem is to make the database schema 1-1 with the GraphQL schema, often called an isomorphic -representation.[8] However, our experience is that such a 1-1 mapping is +representation.[8] However, our experience is that such a 1-1 mapping is detrimental to the development of the system. It is common the GraphQL schema and the underlying data evolve at different paces and that new data sources are added as you go along.

@@ -5803,7 +5812,7 @@

The Mutations

-
access(Obj, Field) ->
+
access(Obj, Field) ->
     case maps:get(Obj, Field, not_found) of
       not_found -> {ok, null}; % Or appropriate answer
       access_denied -> {error, access_denied}; % or perhaps {ok, null}
@@ -5830,7 +5839,7 @@ 

Fragments

-

Lazy Evalution

+

Lazy Evaluation

If you have data where computation is circular, you will have to make sure you don’t build an infinite loop in the data. This system has @@ -5845,7 +5854,7 @@

Lazy Evalution

-
{ok, #{ <<"fieldName">> => {'$lazy', fun() -> Expr end}, ...}}
+
{ok, #{ <<"fieldName">> => {'$lazy', fun() -> Expr end}, ...}}
@@ -5856,7 +5865,7 @@

Lazy Evalution

-
execute(Ctx, #{ <<"fieldName">> := Field }, <<"fieldName">>, Args) ->
+
execute(Ctx, #{ <<"fieldName">> := Field }, <<"fieldName">>, Args) ->
     {'$lazy', Thunk} = Field,
     Thunk();
     ...
@@ -6083,9 +6092,9 @@

Root

-

Application sw_core

+

Application sw_core

-
priv/sw.schmea
+
priv/sw.schema

The schema definition file which can be read by the Erlang GraphQL system. It defines the schema rules for the Star Wars API.

@@ -6217,7 +6226,7 @@

Application sw_core

-

Application sw_web

+

Application sw_web

This application implements the web UI and the HTTP transport on top of the Core application.

@@ -6321,38 +6330,36 @@

Appendix C: Changelog


-
+
1. The spec has toyed with the idea of adding more classes in addition to queries and mutations. Most notably the concept of a subscription
-
+
2. Some people call this weak typing, but type promotion is probably a more apt term
-
+
3. Through the use of the @streamed and @defer directives among other things
-
+
4. A later version of GraphQL is likely to use abstract binding trees instead, but for now, we run with the classic structure
-
+
5. We call operations for functions in the GraphQL system because that is what they really are, from a language perspective
-
+
6. You must also do “Node plurality” correct, through this tutorial doesn’t have a place where that gets used. Node plurality is the idea that if you load objects by a list, the return value retains the order in the list.
-
+
7. In Erlang/OTP 20.0 and onward, atoms can be arbitrary Unicode data, so you can pick a special symbol to mark special atoms
-
+
8. Isomorphic stems from ancient greek and means “equal shape”
- - - + \ No newline at end of file