在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
开源软件名称(OpenSource Name):jlouis/graphql-erlang开源软件地址(OpenSource Url):https://github.com/jlouis/graphql-erlang开源编程语言(OpenSource Language):Erlang 99.4%开源软件介绍(OpenSource Introduction):A GraphQL Server library - in ErlangThis project contains the necessary support code to implement GraphQL servers in Erlang. Its major use is on top of some other existing transport library, for instance the cowboy web server. When a request arrives, it can be processed by the GraphQL support library and a GraphQL answer can be given. In a way, this replaces all of your REST endpoints with a single endpoint: one for Graph Queries. This README provides the system overview and its mode of operation. ChangelogSee the file StatusCurrently, the code implements all of the October 2016 GraphQL specification, except for a few areas:
In addition, we are working towards June 2018 compliance. We already implemented many of the changes in the system. But we are still missing some parts. The implementation plan is on a demand driven basis for Shopgun currently, in that we tend to implement things when there is a need for them. DocumentationThis is a big library. In order to ease development, we have provided a complete tutorial for GraphQL Erlang: https://github.com/shopgun/graphql-erlang-tutorial Also, the tutorial has a book which describes how the tutorial example is implemented in detail: https://shopgun.github.io/graphql-erlang-tutorial/ NOTE: Read the tutorial before reading on in this repository if you haven't already. This README gives a very quick overview, but the canonical documentation is the book at the moment. What is GraphQLGraphQL is a query language for the web. It allows a client to tell the server what it wants in a declarative way. The server then materializes a response based on the clients query. This makes your development client-centric and client-driven, which tend to be a lot faster from a development perspective. A project is usually driven from the top-of-the-iceberg and down, so shuffling more onus on the client side is a wise move in modern system design. GraphQL is also a contract. Queries and responses are typed and contract-verified on both the input and output side. That is, GraphQL also acts as a contract-checker. This ensures:
Finally, GraphQL supports introspection of its endpoint. This allows systems to query the server in order to learn what the schema is. In turn, tooling can be built on top of GraphQL servers to provide development-debug user interfaces. Also, languages with static types can use the introspection to derive a type model in code which matches the contract. Either by static code generation, or by type providers. Whirlwind tourThe GraphQL world specifies a typed schema definition. For instance the following taken from the Relay Modern specification: interface Node {
id: ID!
}
type Faction : Node {
id: ID!
name: String
ships: ShipConnection
}
type Ship : Node {
id: ID!
name: String
}
type ShipConnection {
edges: [ShipEdge]
pageInfo: PageInfo!
}
type ShipEdge {
cursor: String!
node: Ship
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
rebels: Faction
empire: Faction
node(id: ID!): Node
}
input IntroduceShipInput {
factionId: String!
shipNamed: String!
clientMutationId: String!
}
type IntroduceShipPayload {
faction: Faction
ship: Ship
clientMutationId: String!
}
type Mutation {
introduceShip(input: IntroduceShipInput!): IntroduceShipPayload
} The schema is a subset of the Star Wars schema given as the typical GraphQL example all over the web. The GraphQL world roughly splits the world into input objects and output objects. Input objects are given as part of a query request by the client. Output objects are sent back from the server to the client. This Erlang implementation contains a schema parser for schemas like the above. Once parsed, a mapping is provided by the programmer which maps an output type in the schema to an Erlang module. This module must implement a function -spec execute(Context, Object, Field, Args) ->
{ok, Response}
| {error, Reason}. which is used to materialize said object. That is, when you request a
field Materialization is thus simply a function call in the Erlang world.
These calls tend to be used in two ways: Either they acquire a piece
of data from a database (e.g., mnesia) and return that data as an
For example, look at the following query: query Q {
node(id: "12098141") {
... on Ship {
id
name
}
}
} When this query executes, it will start by a developer provided
initial object. Typically the empty map -module(query).
...
execute(Ctx, #{}, <<"node">>, #{ <<"id">> := ID }) ->
{ok, Obj} = load_object(ID). Now, since you are requesting the -module(ship).
-record(ship, { id, name }).
execute(Ctx, #ship{ id = Id }, <<"id">>, _Args) ->
{ok, ID};
execute(Ctx, #ship{ name = Name }, <<"name">>, _Args) ->
{ok, Name}. Two materialization calls will be made. One for the field Materilization through derivationA common use of the functions is to derive data from existing data. Suppose we extend the ship in the following way: type Ship {
...
capacity : float!
load : float!
loadRatio : float!
} so a ship has a certain capacity and a current load in its cargo bay.
We could store the -module(ship).
-record(ship,
{ id,
name,
capacity,
load }).
execute(...) ->
...;
execute(Ctx, #ship {
capacity = Cap,
load = Load }, <<"loadRatio">>, _) ->
{ok, Load / Cap };
... This will compute that field if it is requested, but not compute it when it is not requested by a client. Many fields in a data set are derivable in this fashion. Especially when a schema changes and grows over time. Old fields can be derived for backwards compatibility and new fields can be added next to it. In addition, it tends to be more efficient. A sizable portion of modern web work is about moving data around. If you have to move less data, you decrease the memory and network pressure, which can translate to faster service. Materializing JOINsIf we take a look at the type Faction : Node {
id: ID!
name: String
ships: ShipConnection
} in this, execute(Ctx, #faction { id = ID }, <<"ships">>, _Args) ->
{ok, Ships} = ship:lookup_by_faction(ID),
pagination:build_pagination(Ships). where the #{
'$type' => <<"ShipConnection">>,
<<"pageInfo">> => #{
<<"hasNextPage">> => false,
...
},
<<"edges">> => [
#{ <<"cursor">> => base64:encode(<<"edge:1">>),
<<"node">> => #ship{ ... } },
...]
} which can then be processed further by other resources. Note how we are eagerly constructing several objects at once and then exploiting the cursor moves of the GraphQL system to materialize the fields which the client requests. The alternative is to lazily construct materializations on demand, but when data is readily available anyway, it is often more efficient to just pass pointers along. APIThe GraphQL API is defined in the module The system deliberately splits each phase and hands it over to the programmer. This allows you to debug a bit easier and gives the programmer more control over the parts. A typical implementation will start by using the schema loader: inject() ->
{ok, File} = application:get_env(myapp, schema_file),
Priv = code:priv_dir(myapp),
FName = filename:join([Priv, File]),
{ok, SchemaData} = file:read_file(FName),
Map = #{
scalars => #{ default => scalar_resource },
interfaces => #{ default => resolve_resource },
unions => #{ default => resolve_resource },
objects => #{
'Ship' => ship_resource,
'Faction' => faction_resource,
...
'Query' => query_resource,
'Mutation' => mutation_resource
}
},
ok = graphql:load_schema(Map, SchemaData),
Root = {root,
#{
query => 'Query',
mutation => 'Mutation',
interfaces => []
}},
ok = graphql:insert_schema_definition(Root),
ok = graphql:validate_schema(),
ok. This will set up the schema in the code by reading it from a file on
disk. Each of the In order to execute queries on the schema, code such as the following
can be used. We have a query document in run(Doc, OpName, Vars, Req, State) ->
case graphql:parse(Doc) of
{ok, AST} ->
try
{ok, #{fun_env := FunEnv,
ast := AST2 }} = graphql:type_check(AST),
ok = graphql:validate(AST2),
Coerced = graphql:type_check_params(FunEnv, OpName, Vars),
Ctx = #{ params => Coerced, operation_name => OpName },
Response = graphql:execute(Ctx, AST2),
Req2 = cowboy_req:set_resp_body(encode_json(Response), Req),
{ok, Reply} = cowboy_req:reply(200, Req2),
{halt, Reply, State}
catch
throw:Err ->
err(400, Err, Req, State)
end;
{error, Error} ->
err(400, {parser_error, Error}, Req, State)
end. ConventionsIn this GraphQL implementation, the default value for keys are type
However, there are many places where you can input atom values and then have them converted internally by the library into binary values. This greatly simplifies a large number of data entry tasks for the programmer. The general rules are:
MiddlewaresThis GraphQL system does not support middlewares, because it turns out
the systems design is flexible enough middlewares can be implemented
by developers themselves. The observation is that any query runs
through the As a result, you can implement middlewares by using the execute(Ctx, Obj, Field, Args) ->
AnnotCtx = perform_authentication(Ctx),
execute_field(AnnotCtx, Obj, Field, Args). The reason this works so well is because we are able to use pattern
matching on More complex systems will define a stack of middlewares in the list
and run them one by one. As an example, a Schema DefinitionsThis GraphQL implementation follows the Jun2018 specification for
defining a schema. In this format, one writes the schema according to
specification, including doc-strings. What was represented as As an example, you can write something along the lines of: """
A Ship from the Star Wars universe
"""
type Ship : Node {
"Unique identity of the ship"
id: ID!
"The name of the ship"
name: String
} And the schema parser knows how to transform this into documentation for introspection. Resource modulesThe following section documents the layout of resource modules as they are used in GraphQL, and what they are needed for in the implementation. Scalar ResourcesGraphQL contains two major kinds of data: objects and scalars. Objects are product types where each element in the product is a field. Raw data are represented as scalar values. GraphQL defines a number of standard scalar values: boolean, integers, floating point numbers, enumerations, strings, identifiers and so on. But you can extend the set of scalars yourself. The spec will contain something along the lines of scalar Color
scalar DateTime and so on. These are mapped onto resource modules handling scalars. It is often enough to provide a default scalar module in the mapping and then implement two functions to handle the scalars: -module(scalar_resource).
-export(
[input/2,
output/2]).
-spec input(Type, Value) -> {ok, Coerced} | {error, Reason}
when
Type :: binary(),
Value :: binary(),
Coerced :: any(),
Reason :: term().
input(<<"Color">>, C) -> color:coerce(C);
input(<<"DateTime">>, DT) -> datetime:coerce(DT);
input(Ty, V) ->
error_logger:info_report({coercing_generic_scalar, Ty, V}),
{ok, V}.
-spec output(Type, Value) -> {ok, Coerced} | {error, Reason}
when
Type :: binary(),
Value :: binary(),
Coerced :: any(),
Reason :: term().
output(<<"Color">>, C) -> color:as_binary(C);
output(<<"DateTime">>, DT) -> datetime:as_binary(DT);
output(Ty, V) ->
error_logger:info_report({output_generic_scalar, Ty, V}),
{ok, V}. Scalar Mappings allow you to have an internal and external
representation of values. You could for instance read a color such as
Type resolution ResourcesFor GraphQL to function correctly, we must be able to resolve types of
concrete objects. This is because the GraphQL system allows you to
specify abstract interfaces and unions. An example from the above
schema is the -module(resolve_resource).
-export([execute/1]).
%% The following is probably included from a header file in a real
%% implementation
-record(ship, {id, name}).
-record(faction, {id, name}).
execute(#ship{}) -> {ok, <<"Ship">>};
execute(#faction{}) -> {ok, <<"Faction">>};
execute(Obj) ->
{error, unknown_type}. Output object ResourcesEach (output) object is mapped onto an Erlang module responsible for handling field requests in that object. The module looks like: -module(object_resource).
-export([execute/4]).
execute(Ctx, SrcObj, <<"f">>, Args) ->
{ok, 42};
execute(Ctx, SrcObj, Field, Args) ->
default The only function which is needed is the
Field Argument rulesIn GraphQL, field arguments follow a specific pattern:
This pattern means there is a clear way for the client to specify "no value" and a clear way for the server to work with the case where the client specified "no value. It eliminates corner cases where you have to figure out what the client meant. Resolution follows a rather simple pattern in GraphQL. If a client
omits a field and it has a default value, the default value is input.
Otherwise Note: This limitation is lifted in the Jun2018 GraphQL specification, but this server doesn't implement that detail yet. On the server side, we handle arguments by supplying a map of KV pairs to the execute function. Suppose we have an input such as input Point {
x = 4.0 float
y float
} The server can handle this input by matching directly: execute(Ctx, SrcObj, Field,
#{ <<"x">> := XVal, <<"y">> := YVal }) ->
... This will always match. If the client provides the input Tips & TricksThe execute function allows you to make object-level generic handling
of fields. If, for example, your execute(_Ctx, Obj, Field, _Args) ->
case maps:get(Field, Obj, not_found) of
not_found -> {ok, null};
Val -> {ok, Val}
end. Another trick is to use generic execution to handle "middlewares" - See the appropriate section on Middlewares. System ArchitectureMost other GraphQL servers provide no type->module mapping. Rather, they rely on binding of individual functions to fields. The implementation began with the same setup, but it turns out pattern matching is a good fit for the notion of requesting different fields inside an object. Thus, we use pattern matching as a destructuring mechanism for incoming queries. |