diff --git a/lib/absinthe/blueprint.ex b/lib/absinthe/blueprint.ex index fdfadc5b50..27d78463ab 100644 --- a/lib/absinthe/blueprint.ex +++ b/lib/absinthe/blueprint.ex @@ -139,4 +139,70 @@ defmodule Absinthe.Blueprint do %{blueprint | operations: ops} end + + @doc """ + Append the given field or fields to the given type + """ + def extend_fields(blueprint = %Blueprint{}, ext_blueprint = %Blueprint{}) do + ext_types = types_by_name(ext_blueprint) + + schema_defs = + for schema_def = %{type_definitions: type_defs} <- blueprint.schema_definitions do + type_defs = + for type_def <- type_defs do + case ext_types[type_def.name] do + nil -> + type_def + + %{fields: new_fields} -> + %{type_def | fields: type_def.fields ++ new_fields} + end + end + + %{schema_def | type_definitions: type_defs} + end + + %{blueprint | schema_definitions: schema_defs} + end + + def extend_fields(blueprint, ext_blueprint) when is_atom(ext_blueprint) do + extend_fields(blueprint, ext_blueprint.__absinthe_blueprint__) + end + + def add_field(blueprint = %Blueprint{}, type_def_name, new_field) do + schema_defs = + for schema_def = %{type_definitions: type_defs} <- blueprint.schema_definitions do + type_defs = + for type_def <- type_defs do + if type_def.name == type_def_name do + %{type_def | fields: type_def.fields ++ List.wrap(new_field)} + else + type_def + end + end + + %{schema_def | type_definitions: type_defs} + end + + %{blueprint | schema_definitions: schema_defs} + end + + def find_field(%{fields: fields}, name) do + Enum.find(fields, fn field = %{name: field_name} -> field_name == name end) + end + + @doc """ + Index the types by their name + """ + def types_by_name(blueprint = %Blueprint{}) do + for schema_def = %{type_definitions: type_defs} <- blueprint.schema_definitions, + type_def <- type_defs, + into: %{} do + {type_def.name, type_def} + end + end + + def types_by_name(module) when is_atom(module) do + types_by_name(module.__absinthe_blueprint__) + end end diff --git a/lib/absinthe/blueprint/schema/enum_type_definition.ex b/lib/absinthe/blueprint/schema/enum_type_definition.ex index 56f7eb57c4..a7141d4445 100644 --- a/lib/absinthe/blueprint/schema/enum_type_definition.ex +++ b/lib/absinthe/blueprint/schema/enum_type_definition.ex @@ -46,6 +46,7 @@ defmodule Absinthe.Blueprint.Schema.EnumTypeDefinition do value = %Absinthe.Type.Enum.Value{ name: value_def.name, value: value_def.value, + enum_identifier: type_def.identifier, __reference__: value_def.__reference__, description: value_def.description, deprecation: value_def.deprecation diff --git a/lib/absinthe/blueprint/schema/union_type_definition.ex b/lib/absinthe/blueprint/schema/union_type_definition.ex index 86cba8505a..71427fe17a 100644 --- a/lib/absinthe/blueprint/schema/union_type_definition.ex +++ b/lib/absinthe/blueprint/schema/union_type_definition.ex @@ -1,7 +1,7 @@ defmodule Absinthe.Blueprint.Schema.UnionTypeDefinition do @moduledoc false - alias Absinthe.Blueprint + alias Absinthe.{Blueprint, Type} @enforce_keys [:name] defstruct [ @@ -10,6 +10,7 @@ defmodule Absinthe.Blueprint.Schema.UnionTypeDefinition do :module, description: nil, resolve_type: nil, + fields: [], directives: [], types: [], source_location: nil, @@ -31,17 +32,55 @@ defmodule Absinthe.Blueprint.Schema.UnionTypeDefinition do errors: [Absinthe.Phase.Error.t()] } - def build(type_def, _schema) do - %Absinthe.Type.Union{ + def build(type_def, schema) do + %Type.Union{ name: type_def.name, description: type_def.description, identifier: type_def.identifier, types: type_def.types |> Enum.sort(), + fields: build_fields(type_def, schema), definition: type_def.module, resolve_type: type_def.resolve_type } end + def build_fields(type_def, schema) do + for field_def <- type_def.fields, into: %{} do + field = %Type.Field{ + identifier: field_def.identifier, + middleware: field_def.middleware, + deprecation: field_def.deprecation, + description: field_def.description, + complexity: field_def.complexity, + config: field_def.complexity, + triggers: field_def.triggers, + name: field_def.name, + type: Blueprint.TypeReference.to_type(field_def.type, schema), + args: build_args(field_def, schema), + definition: field_def.module, + __reference__: field_def.__reference__, + __private__: field_def.__private__ + } + + {field.identifier, field} + end + end + + def build_args(field_def, schema) do + Map.new(field_def.arguments, fn arg_def -> + arg = %Type.Argument{ + identifier: arg_def.identifier, + name: arg_def.name, + description: arg_def.description, + type: Blueprint.TypeReference.to_type(arg_def.type, schema), + default_value: arg_def.default_value, + deprecation: arg_def.deprecation + } + + {arg_def.identifier, arg} + end) + end + @doc false def functions(), do: [:resolve_type] end diff --git a/lib/absinthe/phase/document/execution/resolution.ex b/lib/absinthe/phase/document/execution/resolution.ex index 8368a70d57..376ec731b4 100644 --- a/lib/absinthe/phase/document/execution/resolution.ex +++ b/lib/absinthe/phase/document/execution/resolution.ex @@ -364,13 +364,16 @@ defmodule Absinthe.Phase.Document.Execution.Resolution do {[], _} -> raise Absinthe.Resolution.result_error(error_value, bp_field, source) + {[message: message, path: error_path], extra} -> + put_error(result, error(bp_field, message, Enum.reverse(error_path) ++ path, Map.new(extra))) + {[message: message], extra} -> put_error(result, error(bp_field, message, path, Map.new(extra))) end end defp split_error_value(error_value) when is_list(error_value) or is_map(error_value) do - Keyword.split(Enum.to_list(error_value), [:message]) + Keyword.split(Enum.to_list(error_value), [:message, :path]) end defp split_error_value(error_value) when is_binary(error_value) do diff --git a/lib/absinthe/phase/schema.ex b/lib/absinthe/phase/schema.ex index a53c6fa590..21c6335545 100644 --- a/lib/absinthe/phase/schema.ex +++ b/lib/absinthe/phase/schema.ex @@ -217,9 +217,6 @@ defmodule Absinthe.Phase.Schema do # Given a schema type, lookup a child field definition @spec find_schema_field(nil | Type.t(), String.t(), Absinthe.Schema.t(), Absinthe.Adapter.t()) :: nil | Type.Field.t() - defp find_schema_field(_, "__" <> introspection_field, _, _) do - Absinthe.Introspection.Field.meta(introspection_field) - end defp find_schema_field(%{of_type: type}, name, schema, adapter) do find_schema_field(type, name, schema, adapter) diff --git a/lib/absinthe/phase/schema/decorate.ex b/lib/absinthe/phase/schema/decorate.ex index 74bbb6d267..f1ce183d4b 100644 --- a/lib/absinthe/phase/schema/decorate.ex +++ b/lib/absinthe/phase/schema/decorate.ex @@ -75,6 +75,7 @@ defmodule Absinthe.Phase.Schema.Decorate do end @impl __MODULE__.Decorator + def apply_decoration(node, {:description, text}) do %{node | description: text} end @@ -82,4 +83,109 @@ defmodule Absinthe.Phase.Schema.Decorate do def apply_decoration(node, {:resolve, resolver}) do %{node | middleware: [{Absinthe.Resolution, resolver}]} end + + def apply_decoration( + node = %{fields: fields}, + {:add_fields, new_fields} + ) + when is_list(new_fields) do + new_fields = new_fields |> List.wrap() + + new_field_names = Enum.map(new_fields, & &1.name) + + filtered_fields = + fields + |> Enum.reject(fn %{name: field_name} -> field_name in new_field_names end) + + %{node | fields: filtered_fields ++ new_fields} + end + + def apply_decoration( + node = %{fields: fields}, + {:del_fields, del_field_name} + ) do + filtered_fields = + fields + |> Enum.reject(fn %{name: field_name} -> field_name == del_field_name end) + + %{node | fields: filtered_fields} + end + + @decoration_level1 [ + Blueprint.Schema.DirectiveDefinition, + Blueprint.Schema.EnumTypeDefinition, + Blueprint.Schema.InputObjectTypeDefinition, + Blueprint.Schema.InterfaceTypeDefinition, + Blueprint.Schema.ObjectTypeDefinition, + Blueprint.Schema.ScalarTypeDefinition, + Blueprint.Schema.UnionTypeDefinition + ] + + @decoration_level2 [ + Blueprint.Schema.FieldDefinition, + Blueprint.Schema.EnumValueDefinition + ] + + @decoration_level3 [ + Blueprint.Schema.InputValueDefinition + ] + + def apply_decoration(%Absinthe.Blueprint{} = root, %{} = sub_decorations) do + {root, _} = + Blueprint.prewalk(root, nil, fn + %module{identifier: ident} = node, nil when module in @decoration_level1 -> + case Map.fetch(sub_decorations, ident) do + :error -> + {node, nil} + + {:ok, type_decorations} -> + {apply_decorations(node, type_decorations, __MODULE__), nil} + end + + node, nil -> + {node, nil} + end) + + root + end + + def apply_decoration(%module{} = root, %{} = sub_decorations) + when module in @decoration_level1 do + {root, _} = + Blueprint.prewalk(root, nil, fn + %module{identifier: ident} = node, nil when module in @decoration_level2 -> + case Map.fetch(sub_decorations, ident) do + :error -> + {node, nil} + + {:ok, type_decorations} -> + {apply_decorations(node, type_decorations, __MODULE__), nil} + end + + node, nil -> + {node, nil} + end) + + root + end + + def apply_decoration(%module{} = root, %{} = sub_decorations) + when module in @decoration_level2 do + {root, _} = + Blueprint.prewalk(root, nil, fn + %module{identifier: ident} = node, nil when module in @decoration_level3 -> + case Map.fetch(sub_decorations, ident) do + :error -> + {node, nil} + + {:ok, type_decorations} -> + {apply_decorations(node, type_decorations, __MODULE__), nil} + end + + node, nil -> + {node, nil} + end) + + root + end end diff --git a/lib/absinthe/phase/schema/inline_functions.ex b/lib/absinthe/phase/schema/inline_functions.ex index b9d96c5cc0..221179c3de 100644 --- a/lib/absinthe/phase/schema/inline_functions.ex +++ b/lib/absinthe/phase/schema/inline_functions.ex @@ -46,7 +46,8 @@ defmodule Absinthe.Phase.Schema.InlineFunctions do end end - def inline_middleware(%Type.Object{} = type, schema) do + def inline_middleware(%type_name{} = type, schema) + when type_name in [Type.Object, Type.Union, Type.Interface] do Map.update!(type, :fields, fn fields -> fields = Enum.map(fields, fn {field_ident, field} -> diff --git a/lib/absinthe/phase/schema/introspection.ex b/lib/absinthe/phase/schema/introspection.ex new file mode 100644 index 0000000000..04a6bb19fd --- /dev/null +++ b/lib/absinthe/phase/schema/introspection.ex @@ -0,0 +1,172 @@ +defmodule Absinthe.Phase.Schema.Introspection do + @moduledoc false + + use Absinthe.Phase + alias Absinthe.Blueprint + alias Absinthe.Blueprint.Schema + + alias Absinthe.Blueprint.Schema.FieldDefinition + alias Absinthe.Blueprint.Schema.InputValueDefinition + alias Absinthe.Blueprint.TypeReference.NonNull + alias Absinthe.Blueprint.Schema.ScalarTypeDefinition + alias Absinthe.Blueprint.Schema.ObjectTypeDefinition + alias Absinthe.Blueprint.Schema.ListTypeDefinition + alias Absinthe.Blueprint.Schema.UnionTypeDefinition + alias Absinthe.Blueprint.Schema.InterfaceTypeDefinition + + def __absinthe_function__(identifier, :middleware) do + [{{Absinthe.Resolution, :call}, resolve_fn(identifier)}] + end + + def run(blueprint, _opts) do + blueprint = attach_introspection_fields(blueprint) + {:ok, blueprint} + end + + @doc """ + Append the given field or fields to the given type + """ + def attach_introspection_fields(blueprint = %Blueprint{}) do + %{blueprint | schema_definitions: update_schema_defs(blueprint.schema_definitions)} + end + + def update_schema_defs(schema_definitions) do + for schema_def = %{type_definitions: type_defs} <- schema_definitions do + %{schema_def | type_definitions: update_type_defs(type_defs)} + end + end + + def update_type_defs(type_defs) do + for type_def = %struct_type{} <- type_defs do + cond do + type_def.name in ["RootQueryType", "Query"] -> + type_field = field_def(:type) + schema_field = field_def(:schema) + typename_field = field_def(:typename) + %{type_def | fields: [type_field, schema_field, typename_field | type_def.fields]} + + struct_type in [ + ObjectTypeDefinition, + ListTypeDefinition, + UnionTypeDefinition, + InterfaceTypeDefinition + ] -> + typename_field = field_def(:typename) + %{type_def | fields: [typename_field | type_def.fields]} + + true -> + type_def + end + end + end + + def field_def(:typename) do + %FieldDefinition{ + name: "__typename", + identifier: :__typename, + module: __MODULE__, + type: :string, + description: "The name of the object type currently being queried.", + triggers: %{}, + middleware: [ + {:ref, __MODULE__, :typename} + ], + flags: %{reserved_name: true}, + __reference__: Absinthe.Schema.Notation.build_reference(__ENV__) + } + end + + def field_def(:type) do + %FieldDefinition{ + __reference__: Absinthe.Schema.Notation.build_reference(__ENV__), + name: "__type", + identifier: :__type, + type: :__type, + module: __MODULE__, + description: "Represents scalars, interfaces, object types, unions, enums in the system", + triggers: %{}, + arguments: [ + %InputValueDefinition{ + __reference__: Absinthe.Schema.Notation.build_reference(__ENV__), + module: __MODULE__, + identifier: :name, + name: "name", + type: %NonNull{of_type: :string}, + description: "The name of the type to introspect" + } + ], + middleware: [ + {:ref, __MODULE__, :type} + ], + flags: %{reserved_name: true} + } + end + + def field_def(:schema) do + %FieldDefinition{ + name: "__schema", + identifier: :__schema, + type: :__schema, + module: __MODULE__, + description: "Represents the schema", + triggers: %{}, + middleware: [ + {:ref, __MODULE__, :schema} + ], + flags: %{reserved_name: true}, + __reference__: Absinthe.Schema.Notation.build_reference(__ENV__) + } + end + + def resolve_fn(:schema) do + fn _, %{schema: schema} -> + {:ok, schema} + end + end + + def resolve_fn(:type) do + fn %{name: name}, %{schema: schema} -> + type_def = + case Absinthe.Schema.lookup_type(schema, name) do + type_def = %{fields: fields} -> + %{type_def | fields: filter_fields(fields)} + + type_def -> + type_def + end + + {:ok, type_def} + end + end + + def resolve_fn(:typename) do + fn + _, %{parent_type: %Absinthe.Type.Object{} = type} -> + {:ok, type.name} + + _, %{source: source, parent_type: %Absinthe.Type.Interface{} = iface} = env -> + case Absinthe.Type.Interface.resolve_type(iface, source, env) do + nil -> + {:error, "Could not resolve type of concrete " <> iface.name} + + type -> + {:ok, type.name} + end + + _, %{source: source, parent_type: %Absinthe.Type.Union{} = union} = env -> + case Absinthe.Type.Union.resolve_type(union, source, env) do + nil -> + {:error, "Could not resolve type of concrete " <> union.name} + + type -> + {:ok, type.name} + end + end + end + + def filter_fields(fields) do + for {key, field = %{name: name}} <- fields, not String.starts_with?(name, "__"), into: %{} do + {key, field} + end + end +end diff --git a/lib/absinthe/phase/schema/validation/input_output_types_correctly_placed.ex b/lib/absinthe/phase/schema/validation/input_output_types_correctly_placed.ex index ad09fd60dc..8139582867 100644 --- a/lib/absinthe/phase/schema/validation/input_output_types_correctly_placed.ex +++ b/lib/absinthe/phase/schema/validation/input_output_types_correctly_placed.ex @@ -61,6 +61,7 @@ defmodule Absinthe.Phase.Schema.Validation.InputOuputTypesCorrectlyPlaced do @output_types [ Blueprint.Schema.ObjectTypeDefinition, + Blueprint.Schema.UnionTypeDefinition, Blueprint.Schema.InterfaceTypeDefinition ] defp wrong_type?(type, field_type) when type in @output_types do diff --git a/lib/absinthe/phase/schema/validation/type_names_are_reserved.ex b/lib/absinthe/phase/schema/validation/type_names_are_reserved.ex index 100e9bf7be..98bc732ed4 100644 --- a/lib/absinthe/phase/schema/validation/type_names_are_reserved.ex +++ b/lib/absinthe/phase/schema/validation/type_names_are_reserved.ex @@ -10,8 +10,39 @@ defmodule Absinthe.Phase.Schema.Validation.TypeNamesAreReserved do {:ok, bp} end + def allow_reserved(node = %{flags: nil}) do + allow_reserved(%{node | flags: %{}}) + end + + def allow_reserved(node = %{flags: flags}) do + flags = + flags + |> Map.put(:reserved_name, true) + + %{node | flags: flags} + end + + def make_reserved(node = %{name: "__" <> _}) do + allow_reserved(node) + end + + def make_reserved(node = %{name: name, identifier: identifier}) do + node = %{ + node + | name: name |> String.replace_prefix("", "__"), + identifier: + identifier |> to_string() |> String.replace_prefix("", "__") |> String.to_atom() + } + + allow_reserved(node) + end + defp validate_reserved(%struct{name: "__" <> _} = entity) do - if Absinthe.Type.built_in_module?(entity.__reference__.module) do + reserved_ok = + Absinthe.Type.built_in_module?(entity.__reference__.module) || + reserved_name_ok_flag?(entity) + + if reserved_ok do entity else kind = struct_to_kind(struct) @@ -22,6 +53,14 @@ defmodule Absinthe.Phase.Schema.Validation.TypeNamesAreReserved do end end + defp reserved_name_ok_flag?(%{flags: flags}) do + flags[:reserved_name] + end + + defp reserved_name_ok_flag?(_) do + false + end + defp validate_reserved(entity) do entity end diff --git a/lib/absinthe/phase/schema/validation/type_references_exist.ex b/lib/absinthe/phase/schema/validation/type_references_exist.ex index 203f7523bb..2712966616 100644 --- a/lib/absinthe/phase/schema/validation/type_references_exist.ex +++ b/lib/absinthe/phase/schema/validation/type_references_exist.ex @@ -86,9 +86,13 @@ defmodule Absinthe.Phase.Schema.Validation.TypeReferencesExist do end defp unwrap(type) do - type - |> Absinthe.Blueprint.TypeReference.unwrap() - |> unwrap + unwrap_type = Absinthe.Blueprint.TypeReference.unwrap(type) + + if unwrap_type == type do + type + else + unwrap(unwrap_type) + end end defp error(thing, type) do diff --git a/lib/absinthe/pipeline.ex b/lib/absinthe/pipeline.ex index 2f119f2c53..6ab15c2a23 100644 --- a/lib/absinthe/pipeline.ex +++ b/lib/absinthe/pipeline.ex @@ -112,9 +112,10 @@ defmodule Absinthe.Pipeline do @spec for_schema(nil | Absinthe.Schema.t(), Keyword.t()) :: t def for_schema(schema, _options \\ []) do [ - Phase.Schema.NormalizeReferences, - {Phase.Schema.Decorate, [schema: schema]}, Phase.Schema.TypeImports, + Phase.Schema.Introspection, + {Phase.Schema.Decorate, [schema: schema]}, + Phase.Schema.NormalizeReferences, Phase.Schema.Validation.TypeNamesAreUnique, Phase.Schema.Validation.TypeReferencesExist, Phase.Schema.Validation.TypeNamesAreReserved, diff --git a/lib/absinthe/schema.ex b/lib/absinthe/schema.ex index 9ddd5f7b65..51b03b345f 100644 --- a/lib/absinthe/schema.ex +++ b/lib/absinthe/schema.ex @@ -405,6 +405,7 @@ defmodule Absinthe.Schema do |> Enum.flat_map(&Type.referenced_types(&1, schema)) |> MapSet.new() |> Enum.map(&Schema.lookup_type(schema, &1)) + |> Enum.filter(&(!Type.introspection?(&1))) end @doc """ diff --git a/lib/absinthe/schema/notation/sdl.ex b/lib/absinthe/schema/notation/sdl.ex index cdb0b58215..cc40a954db 100644 --- a/lib/absinthe/schema/notation/sdl.ex +++ b/lib/absinthe/schema/notation/sdl.ex @@ -4,8 +4,11 @@ defmodule Absinthe.Schema.Notation.SDL do @doc """ Parse definitions from SDL source """ - @spec parse(sdl :: String.t(), module, map(), Keyword.t()) :: - {:ok, [Absinthe.Blueprint.Schema.t()]} | {:error, String.t()} + + alias Absinthe.Blueprint + + @spec parse(sdl :: String.t(), Module.t(), map(), Keyword.t()) :: + {:ok, [Absinthe.Blueprint.Schema.type_t()]} | {:error, String.t()} def parse(sdl, module, ref, opts) do with {:ok, doc} <- Absinthe.Phase.Parse.run(sdl) do definitions = @@ -46,17 +49,45 @@ defmodule Absinthe.Schema.Notation.SDL do defp put_ref(node, ref, opts), do: do_put_ref(node, ref, opts) - defp do_put_ref(%{__reference__: nil} = node, ref, opts) do + @field_types [ + Blueprint.Schema.FieldDefinition, + Blueprint.Schema.EnumValueDefinition, + Blueprint.Schema.InputValueDefinition + ] + + # TODO: Which else of these need the conversions? + # Blueprint.Schema.DirectiveDefinition, + # Blueprint.Schema.EnumTypeDefinition, + # Blueprint.Schema.InputObjectTypeDefinition, + # Blueprint.Schema.InterfaceTypeDefinition, + # Blueprint.Schema.ObjectTypeDefinition, + # Blueprint.Schema.ScalarTypeDefinition, + # Blueprint.Schema.UnionTypeDefinition + # Blueprint.Schema.EnumValueDefinition + # Blueprint.Schema.InputValueDefinition + + defp do_put_ref(%node_type{__reference__: nil, name: name} = node, ref, opts) do + adapter = Keyword.get(opts, :adapter, Absinthe.Adapter.LanguageConventions) + ref = case opts[:path] do nil -> ref path -> - put_in(ref.location, %{file: path, line: node.source_location.line}) + put_in(ref.location, %{file: {:unquote, [], [path]}, line: node.source_location.line}) + end + + name = + cond do + node_type in @field_types -> + adapter.to_internal_name(name, :field) + + true -> + name end - %{node | __reference__: ref} + %{node | __reference__: ref, name: name} end defp do_put_ref(node, _ref, _opts), do: node diff --git a/lib/absinthe/type/built_ins/introspection.ex b/lib/absinthe/type/built_ins/introspection.ex index 2ed14a0170..47208fc403 100644 --- a/lib/absinthe/type/built_ins/introspection.ex +++ b/lib/absinthe/type/built_ins/introspection.ex @@ -110,10 +110,15 @@ defmodule Absinthe.Type.BuiltIns.Introspection do result = fields |> Enum.flat_map(fn {_, %{deprecation: is_deprecated} = field} -> - if !is_deprecated || (is_deprecated && show_deprecated) do - [field] - else - [] + cond do + Absinthe.Type.introspection?(field) -> + [] + + !is_deprecated || (is_deprecated && show_deprecated) -> + [field] + + true -> + [] end end) diff --git a/lib/absinthe/type/enum/value.ex b/lib/absinthe/type/enum/value.ex index fac8a0bb2c..07b9b7721d 100644 --- a/lib/absinthe/type/enum/value.ex +++ b/lib/absinthe/type/enum/value.ex @@ -26,8 +26,9 @@ defmodule Absinthe.Type.Enum.Value do name: binary, description: binary, value: any, + enum_identifier: atom, deprecation: Type.Deprecation.t() | nil, __reference__: Type.Reference.t() } - defstruct name: nil, description: nil, value: nil, deprecation: nil, __reference__: nil + defstruct name: nil, description: nil, value: nil, deprecation: nil, enum_identifier: nil, __reference__: nil end diff --git a/lib/absinthe/type/union.ex b/lib/absinthe/type/union.ex index aceb2753fa..87f60dc966 100644 --- a/lib/absinthe/type/union.ex +++ b/lib/absinthe/type/union.ex @@ -43,6 +43,7 @@ defmodule Absinthe.Type.Union do description: binary, types: [Type.identifier_t()], identifier: atom, + fields: map, __private__: Keyword.t(), definition: module, __reference__: Type.Reference.t() @@ -53,6 +54,7 @@ defmodule Absinthe.Type.Union do identifier: nil, resolve_type: nil, types: [], + fields: nil, __private__: [], definition: nil, __reference__: nil diff --git a/test/absinthe/schema/manipulation_test.exs b/test/absinthe/schema/manipulation_test.exs new file mode 100644 index 0000000000..002cc2917d --- /dev/null +++ b/test/absinthe/schema/manipulation_test.exs @@ -0,0 +1,289 @@ +defmodule Absinthe.Schema.ManipulationTest do + use Absinthe.Case, async: true + require IEx + + alias Absinthe.Phase.Schema.Validation.TypeNamesAreReserved + + defmodule ExtTypes do + use Absinthe.Schema.Notation + + object :some_dyn_obj do + field :some_dyn_integer, :integer do + meta :some_string_meta, "some_dyn_integer meta" + end + + field :some_dyn_string, :string do + meta :some_string_meta, "some_dyn_string meta" + resolve fn _, _ -> {:ok, "some_string_val"} end + end + end + end + + defmodule CustomIntrospectionTypes do + use Absinthe.Schema.Notation + + object :custom_introspection_helper do + description "Simple Helper Object used to define blueprint fields" + + field :simple_string, :string do + description "customer introspection field" + + resolve fn _, %{schema: schema} -> + {:ok, "This is a new introspection type on #{inspect(schema)}"} + end + end + + field :some_string_meta, :string do + description "Expose some_string_meta" + + resolve fn _, + %{ + source: source + } -> + private = source[:__private__] || [] + meta_items = private[:meta] || [] + + {:ok, meta_items[:some_string_meta]} + end + end + end + end + + defmodule MyAppWeb.CustomSchemaPhase do + alias Absinthe.{Phase, Pipeline, Blueprint} + + # Add this module to the pipeline of phases + # to run on the schema + def pipeline(pipeline) do + Pipeline.insert_after(pipeline, Phase.Schema.TypeImports, __MODULE__) + end + + # Here's the blueprint of the schema, let's do whatever we want with it. + def run(blueprint = %Blueprint{}, _) do + custom_introspection_types = Blueprint.types_by_name(CustomIntrospectionTypes) + custom_introspection_fields = custom_introspection_types["CustomIntrospectionHelper"] + + simple_string_field = + Blueprint.find_field(custom_introspection_fields, "simple_string") + |> TypeNamesAreReserved.make_reserved() + + some_string_meta_field = + Blueprint.find_field(custom_introspection_fields, "some_string_meta") + |> TypeNamesAreReserved.make_reserved() + + blueprint = + blueprint + |> Blueprint.extend_fields(ExtTypes) + |> Blueprint.add_field("__Type", simple_string_field) + |> Blueprint.add_field("__Field", simple_string_field) + |> Blueprint.add_field("__Field", some_string_meta_field) + + {:ok, blueprint} + end + end + + defmodule MyAppWeb.Schema do + use Absinthe.Schema + + @pipeline_modifier MyAppWeb.CustomSchemaPhase + + object :some_obj do + field :some_integer, :integer do + meta :some_string_meta, "some_integer meta" + end + + field :some_string, :string do + meta :some_string_meta, "some_string meta" + resolve fn _, _ -> {:ok, "some_string_val"} end + end + end + + object :some_dyn_obj do + field :non_dyn_integer, :integer do + meta :some_string_meta, "non_dyn_integer meta" + end + + field :non_dyn_string, :string do + meta :some_string_meta, "non_dyn_string meta" + resolve fn _, _ -> {:ok, "some_string_val"} end + end + end + + query do + field :some_field, :some_obj do + meta :some_field_meta, "some field meta" + resolve fn _, _ -> {:ok, %{some_integer: 1}} end + end + end + end + + test "Schema works" do + q = """ + query { + some_field { + some_integer + some_string + } + } + """ + + expected = %{ + data: %{"some_field" => %{"some_integer" => 1, "some_string" => "some_string_val"}} + } + + actual = Absinthe.run!(q, MyAppWeb.Schema) + + assert expected == actual + end + + test "Introspection works" do + q = """ + query { + __type(name: "SomeObj") { + fields { + name + type { + name + } + } + } + } + """ + + expected = %{ + data: %{ + "__type" => %{ + "fields" => [ + %{"name" => "someInteger", "type" => %{"name" => "Int"}}, + %{"name" => "someString", "type" => %{"name" => "String"}} + ] + } + } + } + + actual = Absinthe.run!(q, MyAppWeb.Schema) + + assert expected == actual + end + + test "Custom introspection works" do + q = """ + query { + __type(name: "SomeObj") { + __simple_string + fields { + name + type { + name + } + } + } + } + """ + + expected = %{ + data: %{ + "__type" => %{ + "__simple_string" => + "This is a new introspection type on Absinthe.Schema.ManipulationTest.MyAppWeb.Schema", + "fields" => [ + %{"name" => "someInteger", "type" => %{"name" => "Int"}}, + %{"name" => "someString", "type" => %{"name" => "String"}} + ] + } + } + } + + actual = Absinthe.run!(q, MyAppWeb.Schema) + + assert expected == actual + end + + test "Exposing meta data via introspection works" do + q = """ + query { + __type(name: "SomeObj") { + fields { + name + type { + name + } + __some_string_meta + } + } + } + """ + + expected = %{ + data: %{ + "__type" => %{ + "fields" => [ + %{ + "name" => "someInteger", + "type" => %{"name" => "Int"}, + "__some_string_meta" => "some_integer meta" + }, + %{ + "name" => "someString", + "type" => %{"name" => "String"}, + "__some_string_meta" => "some_string meta" + } + ] + } + } + } + + actual = Absinthe.run!(q, MyAppWeb.Schema) + + assert expected == actual + end + + test "Extending Objects works" do + q = """ + query { + __type(name: "SomeDynObj") { + fields { + name + type { + name + } + __some_string_meta + } + } + } + """ + + expected = %{ + data: %{ + "__type" => %{ + "fields" => [ + %{ + "name" => "nonDynInteger", + "type" => %{"name" => "Int"}, + "__some_string_meta" => "non_dyn_integer meta" + }, + %{ + "name" => "nonDynString", + "type" => %{"name" => "String"}, + "__some_string_meta" => "non_dyn_string meta" + }, + %{ + "name" => "someDynInteger", + "type" => %{"name" => "Int"}, + "__some_string_meta" => "some_dyn_integer meta" + }, + %{ + "name" => "someDynString", + "type" => %{"name" => "String"}, + "__some_string_meta" => "some_dyn_string meta" + } + ] + } + } + } + + actual = Absinthe.run!(q, MyAppWeb.Schema) + + assert expected == actual + end +end diff --git a/test/absinthe/schema/notation/experimental/import_sdl_test.exs b/test/absinthe/schema/notation/experimental/import_sdl_test.exs index 16b747d878..58f720e5fa 100644 --- a/test/absinthe/schema/notation/experimental/import_sdl_test.exs +++ b/test/absinthe/schema/notation/experimental/import_sdl_test.exs @@ -5,6 +5,17 @@ defmodule Absinthe.Schema.Notation.Experimental.ImportSdlTest do @moduletag :experimental @moduletag :sdl + defmodule ExtTypes do + use Absinthe.Schema.Notation + + # Extend a Post Object + import_sdl """ + type User { + upVotes: Int + } + """ + end + defmodule Definition do use Absinthe.Schema @@ -15,8 +26,9 @@ defmodule Absinthe.Schema.Notation.Experimental.ImportSdlTest do type Query { "A list of posts" - posts(filter: PostFilter): [Post] + posts(filter: PostFilter, reverse: Boolean): [Post] admin: User! + droppedField: String } type Comment { @@ -66,6 +78,10 @@ defmodule Absinthe.Schema.Notation.Experimental.ImportSdlTest do {:ok, posts} end + def upcase_title(post, _, _) do + {:ok, Map.get(post, :title) |> String.upcase()} + end + def decorations(%{identifier: :admin}, [%{identifier: :query} | _]) do {:description, "The admin"} end @@ -78,11 +94,50 @@ defmodule Absinthe.Schema.Notation.Experimental.ImportSdlTest do {:resolve, &__MODULE__.get_posts/3} end + def decorations(%{identifier: :user}, _ancestors) do + user_ext = Absinthe.Blueprint.types_by_name(ExtTypes)["User"] + + {:add_fields, user_ext.fields} + end + + def decorations(%{identifier: :query}, _ancestors) do + {:del_fields, "dropped_field"} + end + + def decorations(%Absinthe.Blueprint{}, _) do + %{ + query: %{ + posts: %{ + reverse: {:description, "Just reverse the list, if you want"} + } + }, + post: %{ + upcased_title: [ + {:description, "The title, but upcased"}, + {:resolve, &__MODULE__.upcase_title/3} + ] + } + } + end + def decorations(_node, _ancestors) do [] end end + describe "locations" do + test "have evaluated file values" do + Absinthe.Blueprint.prewalk(Definition.__absinthe_blueprint__(), nil, fn + %{__reference__: %{location: %{file: file}}} = node, _ -> + assert is_binary(file) + {node, nil} + + node, _ -> + {node, nil} + end) + end + end + describe "directives" do test "can be defined" do assert %{name: "foo", identifier: :foo, locations: [:object, :scalar]} = lookup_compiled_directive(Definition, :foo) @@ -120,6 +175,16 @@ defmodule Absinthe.Schema.Notation.Experimental.ImportSdlTest do assert %{description: "A list of posts"} = lookup_field(Definition, :query, :posts) end + test "work on fields, defined deeply" do + assert %{description: "The title, but upcased"} = + lookup_compiled_field(Definition, :post, :upcased_title) + end + + test "work on arguments, defined deeply" do + assert %{description: "Just reverse the list, if you want"} = + lookup_compiled_argument(Definition, :query, :posts, :reverse) + end + test "can be multiline" do assert %{description: "The post author\n(is a user)"} = lookup_field(Definition, :post, :author) @@ -135,6 +200,13 @@ defmodule Absinthe.Schema.Notation.Experimental.ImportSdlTest do end end + describe "resolve" do + test "work on fields, defined deeply" do + assert %{middleware: mw} = lookup_compiled_field(Definition, :post, :upcased_title) + assert length(mw) > 0 + end + end + describe "multiple invocations" do test "can add definitions" do assert %{name: "User", identifier: :user} = lookup_type(Definition, :user) @@ -163,9 +235,46 @@ defmodule Absinthe.Schema.Notation.Experimental.ImportSdlTest do end end + @query """ + { posts { upcasedTitle } } + """ + describe "execution with deeply decoration-defined resolvers" do + test "works" do + assert {:ok, + %{data: %{"posts" => [%{"upcasedTitle" => "FOO"}, %{"upcasedTitle" => "BAR"}]}}} = + Absinthe.run(@query, Definition) + end + end + describe "Absinthe.Schema.used_types/1" do test "works" do assert Absinthe.Schema.used_types(Definition) end end + + @query """ + { admin { upVotes } } + """ + describe "decorator can append fields" do + test "works" do + assert {:ok, %{data: %{"admin" => %{"upVotes" => 99}}}} = + Absinthe.run(@query, Definition, root_value: %{admin: %{up_votes: 99}}) + end + end + + @query """ + { droppedField } + """ + test "decorator can remove fields" do + assert {:ok, + %{ + errors: [ + %{ + locations: [%{column: 3, line: 1}], + message: "Cannot query field \"droppedField\" on type \"Query\"." + } + ] + }} = + Absinthe.run(@query, Definition, root_value: %{dropped_field: "Should be ignored"}) + end end diff --git a/test/absinthe/schema/notation/import_test.exs b/test/absinthe/schema/notation/import_test.exs index 98041e1053..9a393979f3 100644 --- a/test/absinthe/schema/notation/import_test.exs +++ b/test/absinthe/schema/notation/import_test.exs @@ -1,8 +1,20 @@ defmodule Absinthe.Schema.Notation.ImportTest do use ExUnit.Case, async: true + defp field_list(module, name) do + module.__absinthe_type__(name).fields + |> Enum.filter(&(!introspection?(&1))) + |> Keyword.keys() + |> Enum.sort() + end + + defp introspection?({_, field}) do + Absinthe.Type.introspection?(field) + end + alias Absinthe.Phase + describe "import fields" do test "fields can be imported" do defmodule Foo do @@ -22,7 +34,7 @@ defmodule Absinthe.Schema.Notation.ImportTest do end end - assert [:email, :name] = Foo.__absinthe_type__(:bar).fields |> Map.keys() |> Enum.sort() + assert [:email, :name] = field_list(Foo, :bar) end test "works for input objects" do @@ -71,11 +83,9 @@ defmodule Absinthe.Schema.Notation.ImportTest do end end - interface_fields = InterfaceFoo.__absinthe_type__(:foo).fields - assert [:name] = interface_fields |> Map.keys() |> Enum.sort() + assert [:name] = field_list(InterfaceFoo, :foo) - object_fields = InterfaceFoo.__absinthe_type__(:real_foo).fields - assert [:name] = object_fields |> Map.keys() |> Enum.sort() + assert [:name] = field_list(InterfaceFoo, :real_foo) end test "can work transitively" do @@ -101,8 +111,7 @@ defmodule Absinthe.Schema.Notation.ImportTest do end end - assert [:age, :email, :name] == - Bar.__absinthe_type__(:baz).fields |> Map.keys() |> Enum.sort() + assert [:age, :email, :name] == field_list(Bar, :baz) end test "raises errors nicely" do @@ -195,8 +204,7 @@ defmodule Absinthe.Schema.Notation.ImportTest do end end - assert [:age, :email, :name] == - Multiples.__absinthe_type__(:query).fields |> Map.keys() |> Enum.sort() + assert [:age, :email, :name] == field_list(Multiples, :query) end test "can import fields from imported types" do @@ -240,7 +248,7 @@ defmodule Absinthe.Schema.Notation.ImportTest do end end - assert [:email, :name] = Dest.__absinthe_type__(:baz).fields |> Map.keys() |> Enum.sort() + assert [:email, :name] = field_list(Dest, :baz) end end diff --git a/test/support/experimental_notation_helpers.ex b/test/support/experimental_notation_helpers.ex index 54a917fa34..593821eb46 100644 --- a/test/support/experimental_notation_helpers.ex +++ b/test/support/experimental_notation_helpers.ex @@ -29,6 +29,22 @@ defmodule ExperimentalNotationHelpers do end) end + def lookup_argument(mod, type_ident, field_ident, arg_ident) do + case lookup_field(mod, type_ident, field_ident) do + nil -> + nil + + field -> + Enum.find(field.arguments, fn + %{identifier: ^arg_ident} -> + true + + _ -> + false + end) + end + end + def lookup_compiled_field(mod, type_ident, field_ident) do case Absinthe.Schema.lookup_type(mod, type_ident) do nil -> @@ -39,6 +55,16 @@ defmodule ExperimentalNotationHelpers do end end + def lookup_compiled_argument(mod, type_ident, field_ident, arg_ident) do + case lookup_compiled_field(mod, type_ident, field_ident) do + nil -> + nil + + field -> + field.args[arg_ident] + end + end + def type_count(mod) do mod.__absinthe_blueprint__().schema_definitions |> List.first() diff --git a/test/support/fixtures/import_sdl_path_option_fn.graphql b/test/support/fixtures/import_sdl_path_option_fn.graphql index d93bedeb75..3b3ef33a78 100644 --- a/test/support/fixtures/import_sdl_path_option_fn.graphql +++ b/test/support/fixtures/import_sdl_path_option_fn.graphql @@ -1,6 +1,7 @@ "A submitted post" type Post { title: String! + upcasedTitle: String! body: String! """ The post author