From fb7cae3d6678930241a09bfadbbd99edacfacb7c Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 23 Apr 2016 16:23:40 +1000 Subject: [PATCH 01/10] Extracted ExecutionContext module from Executor --- lib/graphql/execution/execution_context.ex | 51 ++++++++++++++++++ lib/graphql/execution/executor.ex | 62 ++++------------------ 2 files changed, 62 insertions(+), 51 deletions(-) create mode 100644 lib/graphql/execution/execution_context.ex diff --git a/lib/graphql/execution/execution_context.ex b/lib/graphql/execution/execution_context.ex new file mode 100644 index 0000000..347b72a --- /dev/null +++ b/lib/graphql/execution/execution_context.ex @@ -0,0 +1,51 @@ + +defmodule GraphQL.Execution.ExecutionContext do + + @type operation :: %{ + kind: :OperationDefintion, + operation: atom + } + + defstruct [:schema, :fragments, :root_value, :operation, :variable_values, :errors] + @type t :: %__MODULE__{ + schema: GraphQL.Schema.t, + fragments: struct, + root_value: Map, + operation: Map, + variable_values: Map, + errors: list(GraphQL.Error.t) + } + + @spec new(GraphQL.Schema.t, GraphQL.Document.t, map, map, String.t) :: __MODULE__.t + def new(schema, document, root_value, variable_values, operation_name) do + Enum.reduce document.definitions, %__MODULE__{ + schema: schema, + fragments: %{}, + root_value: root_value, + operation: nil, + variable_values: variable_values || %{}, # TODO: We need to deeply set keys as strings or atoms. not allow both. + errors: [] + }, fn(definition, context) -> + + case definition do + %{kind: :OperationDefinition} -> + cond do + !operation_name && context.operation -> + report_error(context, "Must provide operation name if query contains multiple operations.") + !operation_name || definition.name.value === operation_name -> + context = %{context | operation: definition} + %{context | variable_values: GraphQL.Execution.Variables.extract(context) } + true -> context + end + %{kind: :FragmentDefinition} -> + put_in(context.fragments[definition.name.value], definition) + end + end + end + + @spec report_error(__MODULE__.t, String.t) :: __MODULE__.t + def report_error(context, msg) do + put_in(context.errors, [%{"message" => msg} | context.errors]) + end +end + diff --git a/lib/graphql/execution/executor.ex b/lib/graphql/execution/executor.ex index fa386ff..1bc412d 100644 --- a/lib/graphql/execution/executor.ex +++ b/lib/graphql/execution/executor.ex @@ -6,6 +6,7 @@ defmodule GraphQL.Execution.Executor do # {:ok, %{hello: "world"}} """ + alias GraphQL.Execution.ExecutionContext alias GraphQL.Type.ObjectType alias GraphQL.Type.List alias GraphQL.Type.Interface @@ -26,60 +27,19 @@ defmodule GraphQL.Execution.Executor do """ @spec execute(GraphQL.Schema.t, GraphQL.Document.t, map, map, String.t) :: result_data | {:error, %{errors: list}} def execute(schema, document, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do - context = build_execution_context(schema, document, root_value, variable_values, operation_name) + context = ExecutionContext.new(schema, document, root_value, variable_values, operation_name) case context.errors do [] -> execute_operation(context, context.operation, root_value) _ -> {:error, %{errors: Enum.dedup(context.errors)}} end end - @spec report_error(context, String.t) :: context - defp report_error(context, msg) do - put_in(context.errors, [%{"message" => msg} | context.errors]) - end - - @type context :: %{ - schema: GraphQL.Schema.t, - fragments: struct, - root_value: Map, - operation: Map, - variable_values: Map, - errors: list(GraphQL.Error.t) - } - @type operation :: %{ kind: :OperationDefintion, operation: atom } - @spec build_execution_context(GraphQL.Schema.t, GraphQL.Document.t, map, map, String.t) :: context - defp build_execution_context(schema, document, root_value, variable_values, operation_name) do - Enum.reduce document.definitions, %{ - schema: schema, - fragments: %{}, - root_value: root_value, - operation: nil, - variable_values: variable_values || %{}, # TODO: We need to deeply set keys as strings or atoms. not allow both. - errors: [] - }, fn(definition, context) -> - - case definition do - %{kind: :OperationDefinition} -> - cond do - !operation_name && context.operation -> - report_error(context, "Must provide operation name if query contains multiple operations.") - !operation_name || definition.name.value === operation_name -> - context = %{context | operation: definition} - %{context | variable_values: GraphQL.Execution.Variables.extract(context) } - true -> context - end - %{kind: :FragmentDefinition} -> - put_in(context.fragments[definition.name.value], definition) - end - end - end - - @spec execute_operation(context, operation, map) :: result_data | {:error, String.t} + @spec execute_operation(ExecutionContext.t, operation, map) :: result_data | {:error, String.t} defp execute_operation(context, operation, root_value) do type = operation_root_type(context.schema, operation) %{fields: fields} = collect_fields(context, type, operation.selectionSet) @@ -118,12 +78,12 @@ defmodule GraphQL.Execution.Executor do end end - @spec execute_fields(context, atom | Map, any, any) :: any + @spec execute_fields(ExecutionContext.t, atom | Map, any, any) :: any defp execute_fields(context, parent_type, source_value, fields) when is_atom(parent_type) do execute_fields(context, apply(parent_type, :type, []), source_value, fields) end - @spec execute_fields(context, atom | Map, any, any) :: any + @spec execute_fields(ExecutionContext.t, atom | Map, any, any) :: any defp execute_fields(context, parent_type, source_value, fields) do Enum.reduce fields, %{}, fn({field_name_ast, field_asts}, results) -> case resolve_field(context, parent_type, source_value, field_asts) do @@ -133,7 +93,7 @@ defmodule GraphQL.Execution.Executor do end end - @spec execute_fields_serially(context, atom, map, any) :: any + @spec execute_fields_serially(ExecutionContext.t, atom, map, any) :: any defp execute_fields_serially(context, parent_type, source_value, fields) do # call execute_fields because no async operations yet execute_fields(context, parent_type, source_value, fields) @@ -188,7 +148,7 @@ defmodule GraphQL.Execution.Executor do defp complete_value(_, _, _, _, nil), do: nil - @spec complete_value(context, %ObjectType{}, GraphQL.Document.t, any, map) :: map + @spec complete_value(ExecutionContext.t, %ObjectType{}, GraphQL.Document.t, any, map) :: map defp complete_value(context, %ObjectType{} = return_type, field_asts, _info, result) do sub_field_asts = collect_sub_fields(context, return_type, field_asts) execute_fields(context, return_type, result, sub_field_asts.fields) @@ -198,20 +158,20 @@ defmodule GraphQL.Execution.Executor do complete_value(context, %NonNull{ofType: apply(inner_type, :type, [])}, field_asts, info, result) end - @spec complete_value(context, %NonNull{}, GraphQL.Document.t, any, any) :: map + @spec complete_value(ExecutionContext.t, %NonNull{}, GraphQL.Document.t, any, any) :: map defp complete_value(context, %NonNull{ofType: inner_type}, field_asts, info, result) do # TODO: Null Checking complete_value(context, inner_type, field_asts, info, result) end - @spec complete_value(context, %Interface{}, GraphQL.Document.t, any, any) :: map + @spec complete_value(ExecutionContext.t, %Interface{}, GraphQL.Document.t, any, any) :: map defp complete_value(context, %Interface{} = return_type, field_asts, info, result) do runtime_type = AbstractType.get_object_type(return_type, result, info.schema) sub_field_asts = collect_sub_fields(context, runtime_type, field_asts) execute_fields(context, runtime_type, result, sub_field_asts.fields) end - @spec complete_value(context, %Union{}, GraphQL.Document.t, any, any) :: map + @spec complete_value(ExecutionContext.t, %Union{}, GraphQL.Document.t, any, any) :: map defp complete_value(context, %Union{} = return_type, field_asts, info, result) do runtime_type = AbstractType.get_object_type(return_type, result, info.schema) sub_field_asts = collect_sub_fields(context, runtime_type, field_asts) @@ -222,7 +182,7 @@ defmodule GraphQL.Execution.Executor do complete_value(context, %List{ofType: apply(list_type, :type, [])}, field_asts, info, result) end - @spec complete_value(context, %List{}, GraphQL.Document.t, any, any) :: map + @spec complete_value(ExecutionContext.t, %List{}, GraphQL.Document.t, any, any) :: map defp complete_value(context, %List{ofType: list_type}, field_asts, info, result) do Enum.map result, fn(item) -> complete_value_catching_error(context, list_type, field_asts, info, item) From f6027f932fc4618c606861210c5c91af7eb2073d Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 23 Apr 2016 17:51:39 +1000 Subject: [PATCH 02/10] Refactor: pass and return `ExecutionContext` everywhere This will allow us to report errors from anywhere in the flow and accumulate them in the ExecutionContext. --- lib/graphql/execution/executor.ex | 67 ++++++++++++++++++------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/lib/graphql/execution/executor.ex b/lib/graphql/execution/executor.ex index 1bc412d..c676d70 100644 --- a/lib/graphql/execution/executor.ex +++ b/lib/graphql/execution/executor.ex @@ -42,10 +42,14 @@ defmodule GraphQL.Execution.Executor do @spec execute_operation(ExecutionContext.t, operation, map) :: result_data | {:error, String.t} defp execute_operation(context, operation, root_value) do type = operation_root_type(context.schema, operation) - %{fields: fields} = collect_fields(context, type, operation.selectionSet) + {context, %{fields: fields}} = collect_fields(context, type, operation.selectionSet) case operation.operation do - :query -> {:ok, execute_fields(context, type, root_value, fields)} - :mutation -> {:ok, execute_fields_serially(context, type, root_value, fields)} + :query -> + {_, result} = execute_fields(context, type, root_value, fields) + {:ok, result} + :mutation -> + {_, result} = execute_fields_serially(context, type, root_value, fields) + {:ok, result} :subscription -> {:error, "Subscriptions not currently supported"} _ -> {:error, "Can only execute queries, mutations and subscriptions"} end @@ -57,12 +61,12 @@ defmodule GraphQL.Execution.Executor do end defp collect_fields(context, runtime_type, selection_set, field_fragment_map \\ %{fields: %{}, fragments: %{}}) do - Enum.reduce selection_set[:selections], field_fragment_map, fn(selection, field_fragment_map) -> + Enum.reduce selection_set[:selections], {context, field_fragment_map}, fn(selection, {context, field_fragment_map}) -> case selection do %{kind: :Field} -> field_name = field_entry_key(selection) fields = field_fragment_map.fields[field_name] || [] - put_in(field_fragment_map.fields[field_name], [selection | fields]) + {context, put_in(field_fragment_map.fields[field_name], [selection | fields])} %{kind: :InlineFragment} -> collect_fragment(context, runtime_type, selection, field_fragment_map) %{kind: :FragmentSpread} -> @@ -71,29 +75,29 @@ defmodule GraphQL.Execution.Executor do field_fragment_map = put_in(field_fragment_map.fragments[fragment_name], true) collect_fragment(context, runtime_type, context.fragments[fragment_name], field_fragment_map) else - field_fragment_map + {context, field_fragment_map} end - _ -> field_fragment_map + _ -> {context, field_fragment_map} end end end - @spec execute_fields(ExecutionContext.t, atom | Map, any, any) :: any + @spec execute_fields(ExecutionContext.t, atom | Map, any, any) :: {ExecutionContext.t, map} defp execute_fields(context, parent_type, source_value, fields) when is_atom(parent_type) do execute_fields(context, apply(parent_type, :type, []), source_value, fields) end - @spec execute_fields(ExecutionContext.t, atom | Map, any, any) :: any + @spec execute_fields(ExecutionContext.t, atom | Map, any, any) :: {ExecutionContext.t, map} defp execute_fields(context, parent_type, source_value, fields) do - Enum.reduce fields, %{}, fn({field_name_ast, field_asts}, results) -> + Enum.reduce fields, {context, %{}}, fn({field_name_ast, field_asts}, {context, results}) -> case resolve_field(context, parent_type, source_value, field_asts) do - :undefined -> results - value -> Map.put(results, field_name_ast.value, value) + {context, :undefined} -> {context, results} + {context, value} -> {context, Map.put(results, field_name_ast.value, value)} end end end - @spec execute_fields_serially(ExecutionContext.t, atom, map, any) :: any + @spec execute_fields_serially(ExecutionContext.t, atom, map, any) :: {ExecutionContext.t, map} defp execute_fields_serially(context, parent_type, source_value, fields) do # call execute_fields because no async operations yet execute_fields(context, parent_type, source_value, fields) @@ -101,6 +105,7 @@ defmodule GraphQL.Execution.Executor do defp resolve_field(context, parent_type, source, field_asts) do field_ast = hd(field_asts) + # FIXME: possible memory leak field_name = String.to_atom(field_ast.name.value) if field_def = field_definition(parent_type, field_name) do @@ -137,20 +142,22 @@ defmodule GraphQL.Execution.Executor do end complete_value_catching_error(context, return_type, field_asts, info, result) else - :undefined + {context, :undefined} end end + @spec complete_value_catching_error(ExecutionContext.t, any, GraphQL.Document.t, any, map) :: {ExecutionContext.t, map | nil} defp complete_value_catching_error(context, return_type, field_asts, info, result) do # TODO lots of error checking complete_value(context, return_type, field_asts, info, result) end - defp complete_value(_, _, _, _, nil), do: nil + @spec complete_value(ExecutionContext.t, any, any, any, nil) :: {ExecutionContext.t, nil} + defp complete_value(context, _, _, _, nil), do: {context, nil} - @spec complete_value(ExecutionContext.t, %ObjectType{}, GraphQL.Document.t, any, map) :: map + @spec complete_value(ExecutionContext.t, %ObjectType{}, GraphQL.Document.t, any, map) :: {ExecutionContext.t, map} defp complete_value(context, %ObjectType{} = return_type, field_asts, _info, result) do - sub_field_asts = collect_sub_fields(context, return_type, field_asts) + {context, sub_field_asts} = collect_sub_fields(context, return_type, field_asts) execute_fields(context, return_type, result, sub_field_asts.fields) end @@ -158,23 +165,23 @@ defmodule GraphQL.Execution.Executor do complete_value(context, %NonNull{ofType: apply(inner_type, :type, [])}, field_asts, info, result) end - @spec complete_value(ExecutionContext.t, %NonNull{}, GraphQL.Document.t, any, any) :: map + @spec complete_value(ExecutionContext.t, %NonNull{}, GraphQL.Document.t, any, any) :: {ExecutionContext.t, map} defp complete_value(context, %NonNull{ofType: inner_type}, field_asts, info, result) do # TODO: Null Checking complete_value(context, inner_type, field_asts, info, result) end - @spec complete_value(ExecutionContext.t, %Interface{}, GraphQL.Document.t, any, any) :: map + @spec complete_value(ExecutionContext.t, %Interface{}, GraphQL.Document.t, any, any) :: {ExecutionContext.t, map} defp complete_value(context, %Interface{} = return_type, field_asts, info, result) do runtime_type = AbstractType.get_object_type(return_type, result, info.schema) - sub_field_asts = collect_sub_fields(context, runtime_type, field_asts) + {context, sub_field_asts} = collect_sub_fields(context, runtime_type, field_asts) execute_fields(context, runtime_type, result, sub_field_asts.fields) end - @spec complete_value(ExecutionContext.t, %Union{}, GraphQL.Document.t, any, any) :: map + @spec complete_value(ExecutionContext.t, %Union{}, GraphQL.Document.t, any, any) :: {ExecutionContext.t, map} defp complete_value(context, %Union{} = return_type, field_asts, info, result) do runtime_type = AbstractType.get_object_type(return_type, result, info.schema) - sub_field_asts = collect_sub_fields(context, runtime_type, field_asts) + {context, sub_field_asts} = collect_sub_fields(context, runtime_type, field_asts) execute_fields(context, runtime_type, result, sub_field_asts.fields) end @@ -184,9 +191,11 @@ defmodule GraphQL.Execution.Executor do @spec complete_value(ExecutionContext.t, %List{}, GraphQL.Document.t, any, any) :: map defp complete_value(context, %List{ofType: list_type}, field_asts, info, result) do - Enum.map result, fn(item) -> - complete_value_catching_error(context, list_type, field_asts, info, item) + {context, result} = Enum.reduce result, {context, []}, fn(item, {context, acc}) -> + {context, value} = complete_value_catching_error(context, list_type, field_asts, info, item) + {context, [value] ++ acc} end + {context, Enum.reverse(result)} end defp complete_value(context, return_type, field_asts, info, result) when is_atom(return_type) do @@ -194,16 +203,16 @@ defmodule GraphQL.Execution.Executor do complete_value(context, type, field_asts, info, result) end - defp complete_value(_context, return_type, _field_asts, _info, result) do - GraphQL.Types.serialize(return_type, result) + defp complete_value(context, return_type, _field_asts, _info, result) do + {context, GraphQL.Types.serialize(return_type, result)} end defp collect_sub_fields(context, return_type, field_asts) do - Enum.reduce field_asts, %{fields: %{}, fragments: %{}}, fn(field_ast, field_fragment_map) -> + Enum.reduce field_asts, {context, %{fields: %{}, fragments: %{}}}, fn(field_ast, {context, field_fragment_map}) -> if selection_set = Map.get(field_ast, :selectionSet) do collect_fields(context, return_type, selection_set, field_fragment_map) else - field_fragment_map + {context, field_fragment_map} end end end @@ -293,7 +302,7 @@ defmodule GraphQL.Execution.Executor do if condition_matches do collect_fields(context, runtime_type, selection.selectionSet, field_fragment_map) else - field_fragment_map + {context, field_fragment_map} end end From 9919be00cb4c2d47ca34578bc8c304417335b9da Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 23 Apr 2016 18:13:36 +1000 Subject: [PATCH 03/10] Refactor: prefer a multiple function heads over case --- lib/graphql/execution/executor.ex | 48 +++++++++++++++++-------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/lib/graphql/execution/executor.ex b/lib/graphql/execution/executor.ex index c676d70..ea3b34b 100644 --- a/lib/graphql/execution/executor.ex +++ b/lib/graphql/execution/executor.ex @@ -42,7 +42,7 @@ defmodule GraphQL.Execution.Executor do @spec execute_operation(ExecutionContext.t, operation, map) :: result_data | {:error, String.t} defp execute_operation(context, operation, root_value) do type = operation_root_type(context.schema, operation) - {context, %{fields: fields}} = collect_fields(context, type, operation.selectionSet) + {context, %{fields: fields}} = collect_selections(context, type, operation.selectionSet) case operation.operation do :query -> {_, result} = execute_fields(context, type, root_value, fields) @@ -60,28 +60,34 @@ defmodule GraphQL.Execution.Executor do Map.get(schema, operation.operation) end - defp collect_fields(context, runtime_type, selection_set, field_fragment_map \\ %{fields: %{}, fragments: %{}}) do + defp collect_selections(context, runtime_type, selection_set, field_fragment_map \\ %{fields: %{}, fragments: %{}}) do Enum.reduce selection_set[:selections], {context, field_fragment_map}, fn(selection, {context, field_fragment_map}) -> - case selection do - %{kind: :Field} -> - field_name = field_entry_key(selection) - fields = field_fragment_map.fields[field_name] || [] - {context, put_in(field_fragment_map.fields[field_name], [selection | fields])} - %{kind: :InlineFragment} -> - collect_fragment(context, runtime_type, selection, field_fragment_map) - %{kind: :FragmentSpread} -> - fragment_name = selection.name.value - if !field_fragment_map.fragments[fragment_name] do - field_fragment_map = put_in(field_fragment_map.fragments[fragment_name], true) - collect_fragment(context, runtime_type, context.fragments[fragment_name], field_fragment_map) - else - {context, field_fragment_map} - end - _ -> {context, field_fragment_map} - end + collect_selection(context, runtime_type, selection, field_fragment_map) + end + end + + defp collect_selection(context, _, %{kind: :Field} = selection, field_fragment_map) do + field_name = field_entry_key(selection) + fields = field_fragment_map.fields[field_name] || [] + {context, put_in(field_fragment_map.fields[field_name], [selection | fields])} + end + + defp collect_selection(context, runtime_type, %{kind: :InlineFragment} = selection, field_fragment_map) do + collect_fragment(context, runtime_type, selection, field_fragment_map) + end + + defp collect_selection(context, runtime_type, %{kind: :FragmentSpread} = selection, field_fragment_map) do + fragment_name = selection.name.value + if !field_fragment_map.fragments[fragment_name] do + field_fragment_map = put_in(field_fragment_map.fragments[fragment_name], true) + collect_fragment(context, runtime_type, context.fragments[fragment_name], field_fragment_map) + else + {context, field_fragment_map} end end + defp collect_selection(context, _, _, field_fragment_map), do: {context, field_fragment_map} + @spec execute_fields(ExecutionContext.t, atom | Map, any, any) :: {ExecutionContext.t, map} defp execute_fields(context, parent_type, source_value, fields) when is_atom(parent_type) do execute_fields(context, apply(parent_type, :type, []), source_value, fields) @@ -210,7 +216,7 @@ defmodule GraphQL.Execution.Executor do defp collect_sub_fields(context, return_type, field_asts) do Enum.reduce field_asts, {context, %{fields: %{}, fragments: %{}}}, fn(field_ast, {context, field_fragment_map}) -> if selection_set = Map.get(field_ast, :selectionSet) do - collect_fields(context, return_type, selection_set, field_fragment_map) + collect_selections(context, return_type, selection_set, field_fragment_map) else {context, field_fragment_map} end @@ -300,7 +306,7 @@ defmodule GraphQL.Execution.Executor do defp collect_fragment(context, runtime_type, selection, field_fragment_map) do condition_matches = typecondition_matches?(context, selection, runtime_type) if condition_matches do - collect_fields(context, runtime_type, selection.selectionSet, field_fragment_map) + collect_selections(context, runtime_type, selection.selectionSet, field_fragment_map) else {context, field_fragment_map} end From 09084bc897cd5f71591d5ce5c0cb146e3be21c2d Mon Sep 17 00:00:00 2001 From: James Sadler Date: Thu, 28 Apr 2016 16:30:40 +1000 Subject: [PATCH 04/10] Create `operation_node` type and move it to `Nodes` --- lib/graphql/execution/execution_context.ex | 5 ----- lib/graphql/execution/executor.ex | 15 ++++----------- lib/graphql/lang/ast/nodes.ex | 5 +++++ lib/graphql/type/schema.ex | 6 ++++++ 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/graphql/execution/execution_context.ex b/lib/graphql/execution/execution_context.ex index 347b72a..3c44188 100644 --- a/lib/graphql/execution/execution_context.ex +++ b/lib/graphql/execution/execution_context.ex @@ -1,11 +1,6 @@ defmodule GraphQL.Execution.ExecutionContext do - @type operation :: %{ - kind: :OperationDefintion, - operation: atom - } - defstruct [:schema, :fragments, :root_value, :operation, :variable_values, :errors] @type t :: %__MODULE__{ schema: GraphQL.Schema.t, diff --git a/lib/graphql/execution/executor.ex b/lib/graphql/execution/executor.ex index ea3b34b..226e517 100644 --- a/lib/graphql/execution/executor.ex +++ b/lib/graphql/execution/executor.ex @@ -6,6 +6,7 @@ defmodule GraphQL.Execution.Executor do # {:ok, %{hello: "world"}} """ + alias GraphQL.Schema alias GraphQL.Execution.ExecutionContext alias GraphQL.Type.ObjectType alias GraphQL.Type.List @@ -16,6 +17,7 @@ defmodule GraphQL.Execution.Executor do alias GraphQL.Type.NonNull alias GraphQL.Type.CompositeType alias GraphQL.Type.AbstractType + alias GraphQL.Lang.AST.Nodes @type result_data :: {:ok, Map} @@ -34,14 +36,9 @@ defmodule GraphQL.Execution.Executor do end end - @type operation :: %{ - kind: :OperationDefintion, - operation: atom - } - - @spec execute_operation(ExecutionContext.t, operation, map) :: result_data | {:error, String.t} + @spec execute_operation(ExecutionContext.t, Nodes.operation_node, map) :: result_data | {:error, String.t} defp execute_operation(context, operation, root_value) do - type = operation_root_type(context.schema, operation) + type = Schema.operation_root_type(context.schema, operation) {context, %{fields: fields}} = collect_selections(context, type, operation.selectionSet) case operation.operation do :query -> @@ -55,10 +52,6 @@ defmodule GraphQL.Execution.Executor do end end - @spec operation_root_type(GraphQL.Schema.t, operation) :: atom - defp operation_root_type(schema, operation) do - Map.get(schema, operation.operation) - end defp collect_selections(context, runtime_type, selection_set, field_fragment_map \\ %{fields: %{}, fragments: %{}}) do Enum.reduce selection_set[:selections], {context, field_fragment_map}, fn(selection, {context, field_fragment_map}) -> diff --git a/lib/graphql/lang/ast/nodes.ex b/lib/graphql/lang/ast/nodes.ex index 5342ed2..9e5576d 100644 --- a/lib/graphql/lang/ast/nodes.ex +++ b/lib/graphql/lang/ast/nodes.ex @@ -37,5 +37,10 @@ defmodule GraphQL.Lang.AST.Nodes do } def kinds, do: @kinds + + @type operation_node :: %{ + kind: :OperationDefinition, + operation: atom + } end diff --git a/lib/graphql/type/schema.ex b/lib/graphql/type/schema.ex index 30afb26..f978389 100644 --- a/lib/graphql/type/schema.ex +++ b/lib/graphql/type/schema.ex @@ -11,6 +11,7 @@ defmodule GraphQL.Schema do alias GraphQL.Type.ObjectType alias GraphQL.Type.Introspection alias GraphQL.Type.CompositeType + alias GraphQL.Lang.AST.Nodes defstruct query: nil, mutation: nil, types: [] @@ -71,6 +72,11 @@ defmodule GraphQL.Schema do reduce_types(typemap, apply(type_module, :type, [])) end + @spec operation_root_type(GraphQL.Schema.t, Nodes.operation_node) :: atom + def operation_root_type(schema, operation) do + Map.get(schema, operation.operation) + end + defp _reduce_arguments(typemap, %{args: args}) do field_arg_types = Enum.map(args, fn{_,v} -> v.type end) Enum.reduce(field_arg_types, typemap, fn(fieldtype,typemap) -> From 3a1e400ee3655a9d540f053518ed05a17fc8d5f6 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 30 Apr 2016 14:47:10 +1000 Subject: [PATCH 05/10] Refactor: create Resolvable protocol --- lib/graphql/execution/executor.ex | 29 +++--- lib/graphql/execution/field_resolver.ex | 16 ++++ lib/graphql/execution/resolvable.ex | 49 ++++++++++ test/graphql/execution/mutations_test.exs | 111 ++++++++++++++++++++++ 4 files changed, 188 insertions(+), 17 deletions(-) create mode 100644 lib/graphql/execution/field_resolver.ex create mode 100644 lib/graphql/execution/resolvable.ex create mode 100644 test/graphql/execution/mutations_test.exs diff --git a/lib/graphql/execution/executor.ex b/lib/graphql/execution/executor.ex index 226e517..1983e8b 100644 --- a/lib/graphql/execution/executor.ex +++ b/lib/graphql/execution/executor.ex @@ -8,6 +8,7 @@ defmodule GraphQL.Execution.Executor do alias GraphQL.Schema alias GraphQL.Execution.ExecutionContext + alias GraphQL.Execution.FieldResolver alias GraphQL.Type.ObjectType alias GraphQL.Type.List alias GraphQL.Type.Interface @@ -110,7 +111,12 @@ defmodule GraphQL.Execution.Executor do if field_def = field_definition(parent_type, field_name) do return_type = field_def.type - args = argument_values(Map.get(field_def, :args, %{}), Map.get(field_ast, :arguments, %{}), context.variable_values) + args = argument_values( + Map.get(field_def, :args, %{}), + Map.get(field_ast, :arguments, %{}), + context.variable_values + ) + info = %{ field_name: field_name, field_asts: field_asts, @@ -123,23 +129,12 @@ defmodule GraphQL.Execution.Executor do variable_values: context.variable_values } - resolution = Map.get(field_def, :resolve) - if !is_nil(source) && is_atom(source) do - source = apply(source, :type, []) - end - result = case resolution do - {mod, fun} -> apply(mod, fun, [source, args, info]) - {mod, fun, _} -> apply(mod, fun, [source, args, info]) - resolve when is_function(resolve) -> - apply(resolve, [source, args, info]) - _ -> - cond do - resolution -> resolution - Map.has_key?(source, field_name) -> Map.get(source, field_name) - true -> Map.get(source, Atom.to_string(field_name)) - end + case FieldResolver.resolve(field_def, source, args, info) do + {:ok, result} -> + complete_value_catching_error(context, return_type, field_asts, info, result) + {:error, message} -> + {ExecutionContext.report_error(context, message), nil} end - complete_value_catching_error(context, return_type, field_asts, info, result) else {context, :undefined} end diff --git a/lib/graphql/execution/field_resolver.ex b/lib/graphql/execution/field_resolver.ex new file mode 100644 index 0000000..9b7551d --- /dev/null +++ b/lib/graphql/execution/field_resolver.ex @@ -0,0 +1,16 @@ + +defmodule GraphQL.Execution.FieldResolver do + alias GraphQL.Execution.Resolvable + + def resolve(field_def, source, args, info) do + # TODO: move this if statement to inside the resolvers? + source = if !is_nil(source) && is_atom(source) do + apply(source, :type, []) + else + source + end + Resolvable.resolve(Map.get(field_def, :resolve), source, args, info) + end +end + + diff --git a/lib/graphql/execution/resolvable.ex b/lib/graphql/execution/resolvable.ex new file mode 100644 index 0000000..bda904a --- /dev/null +++ b/lib/graphql/execution/resolvable.ex @@ -0,0 +1,49 @@ + +defprotocol GraphQL.Execution.Resolvable do + @fallback_to_any true + + def resolve(resolvable, source, args, info) +end + +defmodule GraphQL.Execution.ResolveWrapper do + def wrap(fun) do + try do + {:ok, fun.()} + rescue + e in RuntimeError -> {:error, e.message} + end + end +end + +alias GraphQL.Execution.ResolveWrapper + +defimpl GraphQL.Execution.Resolvable, for: Function do + def resolve(fun, source, args, info) do + ResolveWrapper.wrap fn() -> + fun.(source, args, info) + end + end +end + +defimpl GraphQL.Execution.Resolvable, for: Tuple do + def resolve({mod, fun}, source, args, info), do: do_resolve(mod, fun, source, args, info) + def resolve({mod, fun, _}, source, args, info), do: do_resolve(mod, fun, source, args, info) + + defp do_resolve(mod, fun, source, args, info) do + ResolveWrapper.wrap fn() -> + apply(mod, fun, [source, args, info]) + end + end +end + +defimpl GraphQL.Execution.Resolvable, for: Atom do + def resolve(nil, source, _args, info) do + # NOTE: data keys and field names should be normalized to strings when we load the schema + # and then we wouldn't need this Atom or String logic. + {:ok, Map.get(source, info.field_name, Map.get(source, Atom.to_string(info.field_name)))} + end +end + +defimpl GraphQL.Execution.Resolvable, for: Any do + def resolve(resolution, _source, _args, _info), do: {:ok, resolution} +end diff --git a/test/graphql/execution/mutations_test.exs b/test/graphql/execution/mutations_test.exs new file mode 100644 index 0000000..98f7f7e --- /dev/null +++ b/test/graphql/execution/mutations_test.exs @@ -0,0 +1,111 @@ + +defmodule GraphQL.Execution.Executor.MutationsTest do + use ExUnit.Case, async: true + + import ExUnit.TestHelpers + + alias GraphQL + alias GraphQL.Schema + alias GraphQL.Type.ObjectType + alias GraphQL.Type.Int + + defmodule NumberHolder do + def type do + %ObjectType{ + name: "NumberHolder", + fields: %{ + theNumber: %{type: %Int{}} + } + } + end + end + + defmodule TestSchema do + def schema do + %Schema{ + query: %ObjectType{ + name: "Query", + fields: %{ + theNumber: %{type: NumberHolder.type} + } + }, + mutation: %ObjectType{ + name: "Mutation", + fields: %{ + changeTheNumber: %{ + type: NumberHolder.type, + args: %{ newNumber: %{ type: %Int{} }}, + resolve: fn(source, %{ newNumber: newNumber }, _) -> + Map.put(source, :theNumber, newNumber) + end + }, + failToChangeTheNumber: %{ + type: NumberHolder.type, + args: %{ newNumber: %{ type: %Int{} }}, + resolve: fn(_, %{ newNumber: _ }, _) -> + raise "Cannot change the number" + end + } + } + } + } + end + end + + test "evaluates mutations serially" do + doc = """ + mutation M { + first: changeTheNumber(newNumber: 1) { + theNumber + }, + second: changeTheNumber(newNumber: 2) { + theNumber + }, + third: changeTheNumber(newNumber: 3) { + theNumber + } + } + """ + + assert_execute {doc, TestSchema.schema}, %{ + first: %{theNumber: 1}, + second: %{theNumber: 2}, + third: %{theNumber: 3}, + } + end + + test "evaluates mutations correctly in the presense of a failed mutation" do + doc = """ + mutation M { + first: changeTheNumber(newNumber: 1) { + theNumber + }, + second: failToChangeTheNumber(newNumber: 2) { + theNumber + } + third: changeTheNumber(newNumber: 3) { + theNumber + } + } + """ + + assert_execute {doc, TestSchema.schema}, %{ + first: %{ + theNumber: 1 + }, + second: nil, + third: %{ + theNumber: 3 + } + } + + #expect(result.errors).to.have.length(2); + #expect(result.errors).to.containSubset([ + # { message: 'Cannot change the number', + # locations: [ { line: 8, column: 7 } ] }, + # { message: 'Cannot change the number', + # locations: [ { line: 17, column: 7 } ] } + #]); + end +end + From 605ce20d87edb2f45bd81018fb1b4df5fa954b22 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 30 Apr 2016 15:02:22 +1000 Subject: [PATCH 06/10] Refactor: pattern match source dereferencing --- lib/graphql/execution/field_resolver.ex | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/graphql/execution/field_resolver.ex b/lib/graphql/execution/field_resolver.ex index 9b7551d..4ae1c5f 100644 --- a/lib/graphql/execution/field_resolver.ex +++ b/lib/graphql/execution/field_resolver.ex @@ -3,14 +3,14 @@ defmodule GraphQL.Execution.FieldResolver do alias GraphQL.Execution.Resolvable def resolve(field_def, source, args, info) do - # TODO: move this if statement to inside the resolvers? - source = if !is_nil(source) && is_atom(source) do - apply(source, :type, []) - else - source - end - Resolvable.resolve(Map.get(field_def, :resolve), source, args, info) + Resolvable.resolve(Map.get(field_def, :resolve), deref_source(source), args, info) end + + defp deref_source(nil), do: nil + defp deref_source(source) when is_atom(source) do + apply(source, :type, []) + end + defp deref_source(source), do: source end From 11bde20259540f1c445f1a000e1d667baffb3617 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 30 Apr 2016 16:00:13 +1000 Subject: [PATCH 07/10] Refactor: use keyword list for optional args --- lib/graphql.ex | 18 ++++++++--------- lib/graphql/execution/executor.ex | 15 ++++++++------ test/graphql/execution/mutations_test.exs | 8 -------- test/test_helper.exs | 24 ++++++++++++++++++----- 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/lib/graphql.ex b/lib/graphql.ex index 01be9ec..f1cc0c0 100644 --- a/lib/graphql.ex +++ b/lib/graphql.ex @@ -22,8 +22,8 @@ defmodule GraphQL do # iex> GraphQL.execute(schema, "{ hello }") # {:ok, %{hello: world}} """ - def execute(schema, query, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do - execute_with_optional_validation(true, schema, query, root_value, variable_values, operation_name) + def execute(schema, query, opts) do + execute_with_optional_validation(true, schema, query, opts) end @doc """ @@ -32,19 +32,19 @@ defmodule GraphQL do # iex> GraphQL.execute(schema, "{ hello }") # {:ok, %{hello: world}} """ - def execute_without_validation(schema, query, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do - execute_with_optional_validation(false, schema, query, root_value, variable_values, operation_name) + def execute_without_validation(schema, query, opts) do + execute_with_optional_validation(false, schema, query, opts) end - defp execute_with_optional_validation(should_validate, schema, query, root_value, variable_values, operation_name) do - # NOTE: it would be nice if we could compose functions together in a chain (with ot without validation step). - # See: http://www.zohaib.me/railway-programming-pattern-in-elixir/ + defp execute_with_optional_validation(should_validate, schema, query, opts) do + # TODO: use the `with` statement to compose write this in a nicer way case GraphQL.Lang.Parser.parse(query) do {:ok, document} -> case optionally_validate(should_validate, schema, document) do :ok -> - case Executor.execute(schema, document, root_value, variable_values, operation_name) do - {:ok, response} -> {:ok, %{data: response}} + case Executor.execute(schema, document, opts) do + {:ok, data, []} -> {:ok, %{data: data}} + {:ok, data, errors} -> {:ok, %{data: data, errors: errors}} {:error, errors} -> {:error, errors} end {:error, errors} -> diff --git a/lib/graphql/execution/executor.ex b/lib/graphql/execution/executor.ex index 1983e8b..2b02ef0 100644 --- a/lib/graphql/execution/executor.ex +++ b/lib/graphql/execution/executor.ex @@ -28,8 +28,11 @@ defmodule GraphQL.Execution.Executor do # iex> GraphQL.execute(schema, "{ hello }") # {:ok, %{hello: world}} """ - @spec execute(GraphQL.Schema.t, GraphQL.Document.t, map, map, String.t) :: result_data | {:error, %{errors: list}} - def execute(schema, document, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do + @spec execute(GraphQL.Schema.t, GraphQL.Document.t, list) :: result_data | {:error, %{errors: list}} + def execute(schema, document, opts \\ []) do + root_value = Keyword.get(opts, :root_value, %{}) + variable_values = Keyword.get(opts, :variable_values, %{}) + operation_name = Keyword.get(opts, :operation_name, nil) context = ExecutionContext.new(schema, document, root_value, variable_values, operation_name) case context.errors do [] -> execute_operation(context, context.operation, root_value) @@ -43,11 +46,11 @@ defmodule GraphQL.Execution.Executor do {context, %{fields: fields}} = collect_selections(context, type, operation.selectionSet) case operation.operation do :query -> - {_, result} = execute_fields(context, type, root_value, fields) - {:ok, result} + {context, result} = execute_fields(context, type, root_value, fields) + {:ok, result, context.errors} :mutation -> - {_, result} = execute_fields_serially(context, type, root_value, fields) - {:ok, result} + {context, result} = execute_fields_serially(context, type, root_value, fields) + {:ok, result, context.errors} :subscription -> {:error, "Subscriptions not currently supported"} _ -> {:error, "Can only execute queries, mutations and subscriptions"} end diff --git a/test/graphql/execution/mutations_test.exs b/test/graphql/execution/mutations_test.exs index 98f7f7e..1c0c0ea 100644 --- a/test/graphql/execution/mutations_test.exs +++ b/test/graphql/execution/mutations_test.exs @@ -98,14 +98,6 @@ defmodule GraphQL.Execution.Executor.MutationsTest do theNumber: 3 } } - - #expect(result.errors).to.have.length(2); - #expect(result.errors).to.containSubset([ - # { message: 'Cannot change the number', - # locations: [ { line: 8, column: 7 } ] }, - # { message: 'Cannot change the number', - # locations: [ { line: 17, column: 7 } ] } - #]); end end diff --git a/test/test_helper.exs b/test/test_helper.exs index a93f3ff..49d2be5 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -33,8 +33,15 @@ defmodule ExUnit.TestHelpers do end def assert_execute({query, schema, data, variables, operation}, expected_output) do - assert(GraphQL.execute(schema, query, data, variables, operation) == - {:ok, %{data: stringify_keys(expected_output)}}) + assert(GraphQL.execute( + schema, + query, + [ + root_value: data, + variable_values: variables, + operation_name: operation + ] + ) == {:ok, %{data: stringify_keys(expected_output)}}) end def assert_execute_without_validation({query, schema}, expected_output) do @@ -50,8 +57,15 @@ defmodule ExUnit.TestHelpers do end def assert_execute_without_validation({query, schema, data, variables, operation}, expected_output) do - assert(GraphQL.execute_without_validation(schema, query, data, variables, operation) == - {:ok, %{data: stringify_keys(expected_output)}}) + assert(GraphQL.execute_without_validation( + schema, + query, + [ + root_value: data, + variable_values: variables, + operation_name: operation + ] + ) == {:ok, %{data: stringify_keys(expected_output)}}) end def assert_execute_error({query, schema}, expected_output) do @@ -59,6 +73,6 @@ defmodule ExUnit.TestHelpers do end def assert_execute_error({query, schema, data}, expected_output) do - assert GraphQL.execute(schema, query, data) == {:error, %{errors: stringify_keys(expected_output)}} + assert GraphQL.execute(schema, query, [root_value: data]) == {:error, %{errors: stringify_keys(expected_output)}} end end From 441a25266f1b9da16f62071430ca9e636842db4d Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 30 Apr 2016 16:12:42 +1000 Subject: [PATCH 08/10] Refactor: assert_execute ignores errors --- test/test_helper.exs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/test_helper.exs b/test/test_helper.exs index 49d2be5..6fdee2c 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -33,7 +33,7 @@ defmodule ExUnit.TestHelpers do end def assert_execute({query, schema, data, variables, operation}, expected_output) do - assert(GraphQL.execute( + {:ok, result} = GraphQL.execute( schema, query, [ @@ -41,7 +41,9 @@ defmodule ExUnit.TestHelpers do variable_values: variables, operation_name: operation ] - ) == {:ok, %{data: stringify_keys(expected_output)}}) + ) + + assert result[:data] == stringify_keys(expected_output) end def assert_execute_without_validation({query, schema}, expected_output) do @@ -57,7 +59,7 @@ defmodule ExUnit.TestHelpers do end def assert_execute_without_validation({query, schema, data, variables, operation}, expected_output) do - assert(GraphQL.execute_without_validation( + {:ok, result} = GraphQL.execute_without_validation( schema, query, [ @@ -65,14 +67,22 @@ defmodule ExUnit.TestHelpers do variable_values: variables, operation_name: operation ] - ) == {:ok, %{data: stringify_keys(expected_output)}}) + ) + + assert result[:data] == stringify_keys(expected_output) end def assert_execute_error({query, schema}, expected_output) do assert_execute_error({query, schema, %{}}, expected_output) end - def assert_execute_error({query, schema, data}, expected_output) do - assert GraphQL.execute(schema, query, [root_value: data]) == {:error, %{errors: stringify_keys(expected_output)}} + def assert_execute_error({query, schema, data}, errors) do + {_, result} = GraphQL.execute( + schema, + query, + [root_value: data] + ) + + assert result[:errors] == stringify_keys(errors) end end From 5fc3cfa4de98d031d9d62da33a7b9d43798f230e Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 30 Apr 2016 16:14:40 +1000 Subject: [PATCH 09/10] Assert correct error in mutations test --- test/graphql/execution/mutations_test.exs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/graphql/execution/mutations_test.exs b/test/graphql/execution/mutations_test.exs index 1c0c0ea..0798c10 100644 --- a/test/graphql/execution/mutations_test.exs +++ b/test/graphql/execution/mutations_test.exs @@ -98,6 +98,10 @@ defmodule GraphQL.Execution.Executor.MutationsTest do theNumber: 3 } } + + assert_execute_error {doc, TestSchema.schema}, [ + %{"message" => "Cannot change the number"} + ] end end From 4d7c28d91d4dc8a5bc4458d28a4e502154173ef3 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sun, 1 May 2016 15:24:37 +1000 Subject: [PATCH 10/10] Retain old execute function for GraphQL plug compatibility After new version of plug is released that uses the new execute function, the old execute function will be deleted. --- lib/graphql.ex | 24 ++++++++++++++++++++++-- lib/graphql/execution/executor.ex | 8 +------- test/test_helper.exs | 4 ++-- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/lib/graphql.ex b/lib/graphql.ex index f1cc0c0..df57227 100644 --- a/lib/graphql.ex +++ b/lib/graphql.ex @@ -19,13 +19,33 @@ defmodule GraphQL do @doc """ Execute a query against a schema (with validation) - # iex> GraphQL.execute(schema, "{ hello }") + # iex> GraphQL.execute_with_opts(schema, "{ hello }") # {:ok, %{hello: world}} """ - def execute(schema, query, opts) do + # FIXME: when the execute/5 form is removed (after updating the plug) + # then rename this to `execute`. + def execute_with_opts(schema, query, opts) do execute_with_optional_validation(true, schema, query, opts) end + @doc """ + Execute a query against a schema (with validation) + + # iex> GraphQL.execute(schema, "{ hello }") + # {:ok, %{hello: world}} + """ + # TODO: delete this when a new plug is released. + def execute(schema, query, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do + execute_with_optional_validation( + true, + schema, + query, + root_value: root_value, + variable_values: variable_values, + operation_name: operation_name + ) + end + @doc """ Execute a query against a schema (without validation) diff --git a/lib/graphql/execution/executor.ex b/lib/graphql/execution/executor.ex index 2b02ef0..3676b89 100644 --- a/lib/graphql/execution/executor.ex +++ b/lib/graphql/execution/executor.ex @@ -1,10 +1,4 @@ defmodule GraphQL.Execution.Executor do - @moduledoc """ - Execute a GraphQL query against a given schema / datastore. - - # iex> GraphQL.execute(schema, "{ hello }") - # {:ok, %{hello: "world"}} - """ alias GraphQL.Schema alias GraphQL.Execution.ExecutionContext @@ -25,7 +19,7 @@ defmodule GraphQL.Execution.Executor do @doc """ Execute a query against a schema. - # iex> GraphQL.execute(schema, "{ hello }") + # iex> Executor.execute(schema, "{ hello }") # {:ok, %{hello: world}} """ @spec execute(GraphQL.Schema.t, GraphQL.Document.t, list) :: result_data | {:error, %{errors: list}} diff --git a/test/test_helper.exs b/test/test_helper.exs index 6fdee2c..1975fba 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -33,7 +33,7 @@ defmodule ExUnit.TestHelpers do end def assert_execute({query, schema, data, variables, operation}, expected_output) do - {:ok, result} = GraphQL.execute( + {:ok, result} = GraphQL.execute_with_opts( schema, query, [ @@ -77,7 +77,7 @@ defmodule ExUnit.TestHelpers do end def assert_execute_error({query, schema, data}, errors) do - {_, result} = GraphQL.execute( + {_, result} = GraphQL.execute_with_opts( schema, query, [root_value: data]