Skip to content

Commit

Permalink
Add @defer directive support
Browse files Browse the repository at this point in the history
  • Loading branch information
Bernard Duggan committed Feb 1, 2019
1 parent 8cc869d commit 2afe7be
Show file tree
Hide file tree
Showing 15 changed files with 481 additions and 16 deletions.
28 changes: 24 additions & 4 deletions lib/absinthe.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ defmodule Absinthe do
%{message: String.t()}
| %{message: String.t(), locations: [%{line: pos_integer, column: integer}]}

@type continuation_t :: nil | [Continuation.t()]

@type result_t ::
%{data: nil | result_selection_t}
| %{data: nil | result_selection_t, errors: [result_error_t]}
%{required(:data) => nil | result_selection_t,
optional(:continuation) => continuation_t,
optional(:errors) => [result_error_t]}
| %{errors: [result_error_t]}

@doc """
Expand Down Expand Up @@ -87,7 +90,7 @@ defmodule Absinthe do
max_complexity: non_neg_integer | :infinity
]

@type run_result :: {:ok, result_t} | {:error, String.t()}
@type run_result :: {:ok, result_t} | {:more, result_t} | {:error, String.t()}

@spec run(
binary | Absinthe.Language.Source.t() | Absinthe.Language.Document.t(),
Expand All @@ -99,7 +102,23 @@ defmodule Absinthe do
schema
|> Absinthe.Pipeline.for_document(options)

case Absinthe.Pipeline.run(document, pipeline) do
document
|> Absinthe.Pipeline.run(pipeline)
|> build_result()
end

@spec continue([Continuation.t()]) :: run_result()
def continue(continuation) do
continuation
|> Absinthe.Pipeline.continue()
|> build_result()
end

defp build_result(output) do
case output do
{:ok, %{result: %{continuation: c} = result}, _phases} when c != [] ->
{:more, result}

{:ok, %{result: result}, _phases} ->
{:ok, result}

Expand All @@ -123,6 +142,7 @@ defmodule Absinthe do
def run!(input, schema, options \\ []) do
case run(input, schema, options) do
{:ok, result} -> result
{:more, result} -> result
{:error, err} -> raise ExecutionError, message: err
end
end
Expand Down
19 changes: 19 additions & 0 deletions lib/absinthe/blueprint/continuation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Absinthe.Blueprint.Continuation do
@moduledoc false

# Continuations allow further resolutions after the initial result is
# returned

alias Absinthe.Pipeline

defstruct [
:phase_input,
:pipeline
]

@type t :: %__MODULE__{
phase_input: Pipeline.data_t,
pipeline: Pipeline.t()
}

end
7 changes: 5 additions & 2 deletions lib/absinthe/blueprint/document/field.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ defmodule Absinthe.Blueprint.Document.Field do
source_location: nil,
type_conditions: [],
schema_node: nil,
complexity: nil
complexity: nil,
# Added by directives
dynamic_middleware: []
]

@type t :: %__MODULE__{
Expand All @@ -30,6 +32,7 @@ defmodule Absinthe.Blueprint.Document.Field do
source_location: nil | Blueprint.SourceLocation.t(),
type_conditions: [Blueprint.TypeReference.Name],
schema_node: Type.t(),
complexity: nil | non_neg_integer
complexity: nil | non_neg_integer,
dynamic_middleware: []
}
end
6 changes: 4 additions & 2 deletions lib/absinthe/blueprint/result/list.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ defmodule Absinthe.Blueprint.Result.List do
:values,
errors: [],
flags: %{},
extensions: %{}
extensions: %{},
continuations: []
]

@type t :: %__MODULE__{
emitter: Blueprint.Document.Field.t(),
values: [Blueprint.Document.Resolution.node_t()],
errors: [Phase.Error.t()],
flags: Blueprint.flags_t(),
extensions: %{any => any}
extensions: %{any => any},
continuations: [Continuation.t()]
}
end
6 changes: 4 additions & 2 deletions lib/absinthe/blueprint/result/object.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ defmodule Absinthe.Blueprint.Result.Object do
:fields,
errors: [],
flags: %{},
extensions: %{}
extensions: %{},
continuations: []
]

@type t :: %__MODULE__{
emitter: Blueprint.Document.Field.t(),
fields: [Blueprint.Document.Resolution.node_t()],
errors: [Phase.Error.t()],
flags: Blueprint.flags_t(),
extensions: %{any => any}
extensions: %{any => any},
continuations: [Continuation.t()]
}
end
12 changes: 12 additions & 0 deletions lib/absinthe/middleware/defer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Absinthe.Middleware.Defer do
@moduledoc false

# Suspends deferred fields so that they are not immediately processed

@behaviour Absinthe.Middleware

def call(%{state: :unresolved} = res, _),
do: %{res | state: :suspended, acc: Map.put(res.acc, :deferred_res, res)}

def call(res, _), do: res
end
88 changes: 88 additions & 0 deletions lib/absinthe/phase/document/execution/defer_fields.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
defmodule Absinthe.Phase.Document.Execution.DeferFields do
@moduledoc false

# Strips out deferred fields from the current result and places them
# in continuations.

alias Absinthe.Phase.Document.Execution.Resolution
alias Absinthe.{Blueprint, Phase, Resolution}
alias Blueprint.Continuation
alias Blueprint.Result.List

use Absinthe.Phase

@spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t()
def run(bp_root, _options \\ []) do
result = strip_deferred(bp_root, bp_root.execution.result)

{:ok, %{bp_root | execution: %{bp_root.execution | result: result}}}
end

defp strip_deferred(bp_root, %{fields: _} = object) do
strip_nested(bp_root, object, :fields)
end

defp strip_deferred(bp_root, %List{} = object) do
strip_nested(bp_root, object, :values)
end

defp strip_deferred(bp_root, fields) when is_list(fields) do
fields
|> Enum.reduce(
{[], []},
fn f, acc -> do_strip_deferred(bp_root, f, acc) end
)
end

defp strip_deferred(_bp_root, other), do: other

defp strip_nested(bp_root, object, sub_object_field) do
{continuations, remaining} = strip_deferred(bp_root, Map.get(object, sub_object_field))

object
|> Map.put(sub_object_field, Enum.reverse(remaining))
|> Map.put(:continuations, object.continuations ++ Enum.reverse(continuations))
end

defp do_strip_deferred(
bp_root,
%Resolution{state: :suspended, acc: %{deferred_res: res}},
{deferred, remaining}
) do
continuation = %Continuation{
phase_input: %{
resolution: %{res | state: :unresolved},
execution: bp_root.execution
},
pipeline: [
Phase.Document.Execution.DeferredResolution,
Phase.Document.Execution.DeferFields,
Phase.Document.Result
]
}

{[continuation | deferred], remaining}
end

defp do_strip_deferred(_bp_root, %Resolution{} = r, {deferred, remaining}) do
{deferred, [r | remaining]}
end

defp do_strip_deferred(bp_root, %{fields: _} = object, acc) do
do_strip_nested(bp_root, object, :fields, acc)
end

defp do_strip_deferred(bp_root, %List{} = object, acc) do
do_strip_nested(bp_root, object, :values, acc)
end

defp do_strip_deferred(_bp_root, object, {deferred, remaining}) do
{deferred, [object | remaining]}
end

defp do_strip_nested(bp_root, object, sub_object_field, {deferred, remaining}) do
{d, r} = strip_deferred(bp_root, Map.get(object, sub_object_field))
object = Map.put(object, sub_object_field, Enum.reverse(r))
{d ++ deferred, [object | remaining]}
end
end
42 changes: 42 additions & 0 deletions lib/absinthe/phase/document/execution/deferred_resolution.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule Absinthe.Phase.Document.Execution.DeferredResolution do
@moduledoc false

# Perform resolution on previously deferred fields

alias Absinthe.{Blueprint, Phase}
alias Phase.Document.Execution.Resolution

use Absinthe.Phase

@spec run(map(), Keyword.t()) :: Phase.result_t()
def run(input, _options \\ []) do
{:ok, resolve_field(input)}
end

defp resolve_field(input) do
{result, _} = perform_deferred_resolution(input)

%Blueprint{
execution: %{input.execution | result: result},
result: %{path: make_path(input.resolution.path)}}
end

defp perform_deferred_resolution(input) do
# Perform resolution using the standard resolver pipeline functionality
Resolution.do_resolve_field(
input.resolution,
input.resolution.source,
input.resolution.path
)
end

defp make_path(path) do
path
|> Enum.map(&to_path_field/1)
|> Enum.filter(fn e -> e != nil end)
|> Enum.reverse()
end

defp to_path_field(index) when is_integer(index), do: index
defp to_path_field(%{name: name}), do: name
end
4 changes: 2 additions & 2 deletions lib/absinthe/phase/document/execution/resolution.ex
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ defmodule Absinthe.Phase.Document.Execution.Resolution do
end

# bp_field needs to have a concrete schema node, AKA no unions or interfaces
defp do_resolve_field(res, source, path) do
def do_resolve_field(res, source, path) do
res
|> reduce_resolution
|> case do
Expand Down Expand Up @@ -217,7 +217,7 @@ defmodule Absinthe.Phase.Document.Execution.Resolution do
errors: [],
source: source,
parent_type: parent_type,
middleware: middleware,
middleware: bp_field.dynamic_middleware ++ middleware,
definition: bp_field,
arguments: args
}
Expand Down
9 changes: 8 additions & 1 deletion lib/absinthe/phase/document/result.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ defmodule Absinthe.Phase.Document.Result do
{:validation_failed, errors}
end

format_result(result)
result
|> format_result()
|> maybe_add_continuations(blueprint.execution.result)
end

defp format_result(:execution_failed) do
Expand Down Expand Up @@ -126,4 +128,9 @@ defmodule Absinthe.Phase.Document.Result do
end

defp format_location(_), do: []

defp maybe_add_continuations(result, %{continuations: continuations}) when continuations != [],
do: Map.put(result, :continuation, continuations)

defp maybe_add_continuations(result, _), do: result
end
25 changes: 22 additions & 3 deletions lib/absinthe/pipeline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,41 @@ defmodule Absinthe.Pipeline do
modifying, and executing pipelines of phases.
"""

alias Absinthe.Blueprint.Continuation
alias Absinthe.Phase

require Logger

@type data_t :: any

@type run_result_t :: {:ok, data_t, [Phase.t()]} | {:error, String.t(), [Phase.t()]}

@type phase_config_t :: Phase.t() | {Phase.t(), Keyword.t()}

@type t :: [phase_config_t | [phase_config_t]]

@spec run(data_t, t) :: {:ok, data_t, [Phase.t()]} | {:error, String.t(), [Phase.t()]}
@spec run(data_t, t) :: run_result_t
def run(input, pipeline) do
pipeline
|> List.flatten()
|> run_phase(input)
end

@spec continue([Continuation.t()]) :: run_result_t
def continue([continuation | rest]) do
result = run_phase(continuation.pipeline, continuation.phase_input)

case result do
{:ok, blueprint, phases} when rest == [] ->
{:ok, blueprint, phases}
{:ok, blueprint, phases} ->
bp_result = Map.put(blueprint.result, :continuation, rest)
blueprint = Map.put(blueprint, :result, bp_result)
{:ok, blueprint, phases}
error -> error
end
end

@defaults [
adapter: Absinthe.Adapter.LanguageConventions,
operation_name: nil,
Expand Down Expand Up @@ -103,6 +121,7 @@ defmodule Absinthe.Pipeline do
# Execution
{Phase.Subscription.SubscribeSelf, options},
{Phase.Document.Execution.Resolution, options},
Phase.Document.Execution.DeferFields,
# Format Result
Phase.Document.Result
]
Expand Down Expand Up @@ -334,8 +353,8 @@ defmodule Absinthe.Pipeline do
end)
end

@spec run_phase(t, data_t, [Phase.t()]) ::
{:ok, data_t, [Phase.t()]} | {:error, String.t(), [Phase.t()]}
@spec run_phase(t, data_t, [Phase.t()]) :: run_result_t

def run_phase(pipeline, input, done \\ [])

def run_phase([], input, done) do
Expand Down
Loading

0 comments on commit 2afe7be

Please sign in to comment.