Skip to content
This repository was archived by the owner on Jul 25, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions lib/graphql/plug.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,31 @@ defmodule GraphQL.Plug do
pass: ["*/*"],
json_decoder: Poison

plug GraphQL.Plug.Endpoint
# TODO extract
# plug GraphQL.Plug.GraphiQL

# TODO remove duplication call GraphQL.Plug.Helper.extract_init_options/1 here
@type init :: %{
schema: GraphQL.Schema.t,
root_value: ConfigurableValue.t,
query: ConfigurableValue.t,
allow_graphiql?: true | false
}

@spec init(Map) :: init
def init(opts) do
schema = case Keyword.get(opts, :schema) do
{mod, func} -> apply(mod, func, [])
s -> s
end
root_value = Keyword.get(opts, :root_value, %{})
%{:schema => schema, :root_value => root_value}
graphiql = GraphQL.Plug.GraphiQL.init(opts)
endpoint = GraphQL.Plug.Endpoint.init(opts)

Keyword.merge(graphiql, endpoint)
|> Enum.dedup
end

def call(conn, opts) do
# TODO use private
conn = assign(conn, :graphql_options, opts)
conn = super(conn, opts)

conn = if GraphQL.Plug.GraphiQL.use_graphiql?(conn, opts) do
GraphQL.Plug.GraphiQL.call(conn, opts)
else
GraphQL.Plug.Endpoint.call(conn, opts)
end

# TODO consider not logging instrospection queries
Logger.debug """
Processed GraphQL query:
Expand Down
40 changes: 40 additions & 0 deletions lib/graphql/plug/configurable_value.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule GraphQL.Plug.ConfigurableValue do
@moduledoc """
This module provides the functions that are used for
evaluating configuration options that can be set as
raw strings, or functions in the form of {_ModuleName_, _:function_}
or in the syntax of _&ModuleName.function/arity_

In order for a function to be callable it needs to be
an arity of 1 accepting a `Plug.Conn`.
"""

@type t :: {module, atom} | (Plug.Conn.t -> Map) | Map | nil
@spec evaluate(Plug.Conn.t, t, any) :: Map

@error_msg "Configured function must only be arity of 1 that accepts a value of Plug.Conn"

def evaluate(conn, {mod, func}, _) do
if :erlang.function_exported(mod, func, 1) do
apply(mod, func, [conn])
else
raise @error_msg
end
end

def evaluate(conn, root_fn, _) when is_function(root_fn, 1) do
apply(root_fn, [conn])
end

def evaluate(_, root_fn, _) when is_function(root_fn) do
raise @error_msg
end

def evaluate(_, nil, default) do
default
end

def evaluate(_, root_value, _) do
root_value
end
end
137 changes: 27 additions & 110 deletions lib/graphql/plug/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,61 +12,44 @@ defmodule GraphQL.Plug.Endpoint do
You may want to look at how `GraphQL.Plug` configures its pipeline.
Specifically note how `Plug.Parsers` are configured, as this is required
for pre-parsing the various POST bodies depending on `content-type`.

This plug currently includes _GraphiQL_ support but this should end
up in it's own plug.
"""

import Plug.Conn
alias Plug.Conn
alias GraphQL.Plug.ConfigurableValue
alias GraphQL.Plug.Parameter

@behaviour Plug

@graphiql_version "0.4.9"
@graphiql_instructions """
# Welcome to GraphQL Elixir!
#
# GraphiQL is an in-browser IDE for writing, validating, and
# testing GraphQL queries.
#
# Type queries into this side of the screen, and you will
# see intelligent typeaheads aware of the current GraphQL type schema and
# live syntax and validation errors highlighted within the text.
#
# To bring up the auto-complete at any point, just press Ctrl-Space.
#
# Press the run button above, or Cmd-Enter to execute the query, and the result
# will appear in the pane to the right.
"""

# Load GraphiQL HTML view
require EEx
EEx.function_from_file :defp, :graphiql_html,
Path.absname(Path.relative_to_cwd("templates/graphiql.eex")),
[:graphiql_version, :query, :variables, :result]

def init(opts) do
# NOTE: This code needs to be kept in sync with
# GraphQL.Plug.GraphiQL and GraphQL.Plugs.Endpoint as the
# returned data structure is shared.
schema = case Keyword.get(opts, :schema) do
{mod, func} -> apply(mod, func, [])
s -> s
s -> s
end
root_value = Keyword.get(opts, :root_value, %{})
%{schema: schema, root_value: root_value}

root_value = Keyword.get(opts, :root_value, %{})
query = Keyword.get(opts, :query, nil)

[
schema: schema,
root_value: root_value,
query: query
]
end

def call(%Conn{method: m} = conn, opts) when m in ["GET", "POST"] do
%{schema: schema, root_value: root_value} = conn.assigns[:graphql_options] || opts

query = query(conn)
variables = variables(conn)
operation_name = operation_name(conn)
evaluated_root_value = evaluate_root_value(conn, root_value)
query = Parameter.query(conn) ||
ConfigurableValue.evaluate(conn, opts[:query], nil)
variables = Parameter.variables(conn)
operation_name = Parameter.operation_name(conn)
root_value = ConfigurableValue.evaluate(conn, opts[:root_value], %{})

cond do
use_graphiql?(conn) ->
handle_graphiql_call(conn, schema, evaluated_root_value, query, variables, operation_name)
query ->
handle_call(conn, schema, evaluated_root_value, query, variables, operation_name)
handle_call(conn, opts[:schema], root_value, query, variables, operation_name)
true ->
handle_error(conn, "Must provide query string.")
end
Expand All @@ -76,43 +59,25 @@ defmodule GraphQL.Plug.Endpoint do
handle_error(conn, "GraphQL only supports GET and POST requests.")
end

defp handle_call(conn, schema, root_value, query, variables, operation_name) do
def handle_error(conn, message) do
{:ok, errors} = Poison.encode(%{errors: [%{message: message}]})
conn
|> put_resp_content_type("application/json")
|> execute(schema, root_value, query, variables, operation_name)
end

defp escape_string(s) do
s
|> String.replace(~r/\n/, "\\n")
|> String.replace(~r/'/, "\\'")
end

defp handle_graphiql_call(conn, schema, root_value, query, variables, operation_name) do
# TODO construct a simple query from the schema (ie `schema.query.fields[0].fields[0..5]`)
query = query || @graphiql_instructions <> "\n{\n\tfield\n}\n"
{_, data} = GraphQL.execute(schema, query, root_value, variables, operation_name)
{:ok, variables} = Poison.encode(variables, pretty: true)
{:ok, result} = Poison.encode(data, pretty: true)
graphiql = graphiql_html(@graphiql_version, escape_string(query), escape_string(variables), escape_string(result))
conn
|> put_resp_content_type("text/html")
|> send_resp(200, graphiql)
|> send_resp(400, errors)
end

defp handle_error(conn, message) do
{:ok, errors} = Poison.encode %{errors: [%{message: message}]}
def handle_call(conn, schema, root_value, query, variables, operation_name) do
conn
|> put_resp_content_type("application/json")
|> send_resp(400, errors)
|> execute(schema, root_value, query, variables, operation_name)
end

defp execute(conn, schema, root_value, query, variables, operation_name) do
case GraphQL.execute(schema, query, root_value, variables, operation_name) do
{:ok, data} ->
case Poison.encode(data) do
{:ok, json} -> send_resp(conn, 200, json)
{:error, errors} -> send_resp(conn, 400, errors)
{:error, errors} -> send_resp(conn, 500, errors)
end
{:error, errors} ->
case Poison.encode(errors) do
Expand All @@ -121,52 +86,4 @@ defmodule GraphQL.Plug.Endpoint do
end
end
end

defp evaluate_root_value(conn, {mod, func}) do
apply(mod, func, [conn])
end

defp evaluate_root_value(conn, root_fn) when is_function(root_fn, 1) do
apply(root_fn, [conn])
end

defp evaluate_root_value(_, nil) do
%{}
end

defp evaluate_root_value(_, root_value) do
root_value
end

defp query(conn) do
query = Map.get(conn.params, "query")
if query && String.strip(query) != "", do: query, else: nil
end

defp variables(conn) do
decode_variables Map.get(conn.params, "variables", %{})
end

defp decode_variables(variables) when is_binary(variables) do
case Poison.decode(variables) do
{:ok, variables} -> variables
{:error, _} -> %{} # express-graphql ignores these errors currently
end
end
defp decode_variables(vars), do: vars

defp operation_name(conn) do
Map.get(conn.params, "operationName") ||
Map.get(conn.params, "operation_name")
end

defp use_graphiql?(conn) do
case get_req_header(conn, "accept") do
[accept_header | _] ->
String.contains?(accept_header, "text/html") &&
!Map.has_key?(conn.params, "raw")
_ ->
false
end
end
end
104 changes: 104 additions & 0 deletions lib/graphql/plug/graphiql.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
defmodule GraphQL.Plug.GraphiQL do
@moduledoc """
This is the GraphiQL plug for mounting a GraphQL server.

You can build your own pipeline by mounting the
`GraphQL.Plug.GraphiQL` plug directly.

```elixir
forward "/graphql", GraphQL.Plug.GraphiQL, schema: {MyApp.Schema, :schema}
```

You may want to look at how `GraphQL.Plug` configures its pipeline.
Specifically note how `Plug.Parsers` are configured, as this is required
for pre-parsing the various POST bodies depending on `content-type`.
"""
import Plug.Conn
alias Plug.Conn
alias GraphQL.Plug.Endpoint
alias GraphQL.Plug.ConfigurableValue
alias GraphQL.Plug.Parameter

@behaviour Plug

@graphiql_version "0.6.1"
@graphiql_instructions """
# Welcome to GraphQL Elixir!
#
# GraphiQL is an in-browser IDE for writing, validating, and
# testing GraphQL queries.
#
# Type queries into this side of the screen, and you will
# see intelligent typeaheads aware of the current GraphQL type schema and
# live syntax and validation errors highlighted within the text.
#
# To bring up the auto-complete at any point, just press Ctrl-Space.
#
# Press the run button above, or Cmd-Enter to execute the query, and the result
# will appear in the pane to the right.
"""

# Load GraphiQL HTML view
require EEx
EEx.function_from_file :defp, :graphiql_html,
Path.absname(Path.relative_to_cwd("templates/graphiql.eex")),
[:graphiql_version, :query, :variables, :result]

def init(opts) do
allow_graphiql? = Keyword.get(opts, :allow_graphiql?, true)

GraphQL.Plug.Endpoint.init(opts) ++ [allow_graphiql?: allow_graphiql?]
end

def call(%Conn{method: m} = conn, opts) when m in ["GET", "POST"] do
query = Parameter.query(conn) ||
ConfigurableValue.evaluate(conn, opts[:query], nil)
variables = Parameter.variables(conn)
operation_name = Parameter.operation_name(conn)
root_value = ConfigurableValue.evaluate(conn, opts[:root_value], %{})

cond do
use_graphiql?(conn, opts) ->
handle_graphiql_call(conn, opts[:schema], root_value, query, variables, operation_name)
query ->
Endpoint.handle_call(conn, opts[:schema], root_value, query, variables, operation_name)
true ->
Endpoint.handle_error(conn, "Must provide query string.")
end
end

def call(%Conn{method: _} = conn, _) do
Endpoint.handle_error(conn, "GraphQL only supports GET and POST requests.")
end

defp escape_string(s) do
s
|> String.replace(~r/\n/, "\\n")
|> String.replace(~r/'/, "\\'")
end

defp handle_graphiql_call(conn, schema, root_value, query, variables, operation_name) do
# TODO construct a simple query from the schema (ie `schema.query.fields[0].fields[0..5]`)
query = query || @graphiql_instructions <> "\n{\n\tfield\n}\n"

{_, data} = GraphQL.execute(schema, query, root_value, variables, operation_name)
{:ok, variables} = Poison.encode(variables, pretty: true)
{:ok, result} = Poison.encode(data, pretty: true)

graphiql = graphiql_html(@graphiql_version, escape_string(query), escape_string(variables), escape_string(result))
conn
|> put_resp_content_type("text/html")
|> send_resp(200, graphiql)
end

def use_graphiql?(%Conn{method: "GET"} = conn, opts) do
case opts[:allow_graphiql?] && get_req_header(conn, "accept") do
[accept_header | _] ->
String.contains?(accept_header, "text/html") &&
!Map.has_key?(conn.params, "raw")
_ ->
false
end
end
def use_graphiql?(_, _), do: false
end
Loading