diff --git a/lib/absinthe/blueprint/schema/enum_type_definition.ex b/lib/absinthe/blueprint/schema/enum_type_definition.ex index 49b75998cc..a44e95be4f 100644 --- a/lib/absinthe/blueprint/schema/enum_type_definition.ex +++ b/lib/absinthe/blueprint/schema/enum_type_definition.ex @@ -36,7 +36,8 @@ defmodule Absinthe.Blueprint.Schema.EnumTypeDefinition do values: values_by(type_def, :identifier), values_by_internal_value: values_by(type_def, :value), values_by_name: values_by(type_def, :name), - applied_directives: Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(type_def.directives), + applied_directives: + Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(type_def.directives), definition: type_def.module, description: type_def.description } @@ -54,7 +55,8 @@ defmodule Absinthe.Blueprint.Schema.EnumTypeDefinition do __private__: value_def.__private__, description: value_def.description, deprecation: value_def.deprecation, - applied_directives: Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(value_def.directives) + applied_directives: + Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(value_def.directives) } {Map.fetch!(value_def, key), value} diff --git a/lib/absinthe/blueprint/schema/input_object_type_definition.ex b/lib/absinthe/blueprint/schema/input_object_type_definition.ex index bd5a756464..587d3139cb 100644 --- a/lib/absinthe/blueprint/schema/input_object_type_definition.ex +++ b/lib/absinthe/blueprint/schema/input_object_type_definition.ex @@ -37,7 +37,8 @@ defmodule Absinthe.Blueprint.Schema.InputObjectTypeDefinition do name: type_def.name, fields: build_fields(type_def, schema), description: type_def.description, - applied_directives: Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(type_def.directives), + applied_directives: + Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(type_def.directives), definition: type_def.module } end @@ -50,7 +51,8 @@ defmodule Absinthe.Blueprint.Schema.InputObjectTypeDefinition do description: field_def.description, name: field_def.name, type: Blueprint.TypeReference.to_type(field_def.type, schema), - applied_directives: Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(field_def.directives), + applied_directives: + Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(field_def.directives), definition: type_def.module, __reference__: field_def.__reference__, __private__: field_def.__private__, diff --git a/lib/absinthe/blueprint/schema/interface_type_definition.ex b/lib/absinthe/blueprint/schema/interface_type_definition.ex index 89afaa8e02..9de7dc4754 100644 --- a/lib/absinthe/blueprint/schema/interface_type_definition.ex +++ b/lib/absinthe/blueprint/schema/interface_type_definition.ex @@ -44,7 +44,8 @@ defmodule Absinthe.Blueprint.Schema.InterfaceTypeDefinition do fields: Blueprint.Schema.ObjectTypeDefinition.build_fields(type_def, schema), identifier: type_def.identifier, resolve_type: type_def.resolve_type, - applied_directives: Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(type_def.directives), + applied_directives: + Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(type_def.directives), definition: type_def.module, interfaces: type_def.interfaces } diff --git a/lib/absinthe/blueprint/schema/object_type_definition.ex b/lib/absinthe/blueprint/schema/object_type_definition.ex index d9993a5423..838eade31a 100644 --- a/lib/absinthe/blueprint/schema/object_type_definition.ex +++ b/lib/absinthe/blueprint/schema/object_type_definition.ex @@ -103,34 +103,51 @@ defmodule Absinthe.Blueprint.Schema.ObjectTypeDefinition do Enum.map(directives, fn directive -> %{ name: directive.name, - args: Enum.map(directive.arguments, fn arg -> - %{ - name: arg.name, - value: serialize_argument_value(arg.input_value) - } - end) + args: + Enum.map(directive.arguments, fn arg -> + %{ + name: arg.name, + value: serialize_argument_value(arg.input_value) + } + end) } end) end def build_applied_directives(_), do: [] - defp serialize_argument_value(%Absinthe.Blueprint.Input.String{value: value}), do: inspect(value) - defp serialize_argument_value(%Absinthe.Blueprint.Input.Integer{value: value}), do: to_string(value) - defp serialize_argument_value(%Absinthe.Blueprint.Input.Float{value: value}), do: to_string(value) - defp serialize_argument_value(%Absinthe.Blueprint.Input.Boolean{value: value}), do: to_string(value) + defp serialize_argument_value(%Absinthe.Blueprint.Input.String{value: value}), + do: inspect(value) + + defp serialize_argument_value(%Absinthe.Blueprint.Input.Integer{value: value}), + do: to_string(value) + + defp serialize_argument_value(%Absinthe.Blueprint.Input.Float{value: value}), + do: to_string(value) + + defp serialize_argument_value(%Absinthe.Blueprint.Input.Boolean{value: value}), + do: to_string(value) + defp serialize_argument_value(%Absinthe.Blueprint.Input.Null{}), do: "null" defp serialize_argument_value(%Absinthe.Blueprint.Input.Enum{value: value}), do: value + defp serialize_argument_value(%Absinthe.Blueprint.Input.List{items: items}) do "[" <> Enum.map_join(items, ", ", &serialize_argument_value/1) <> "]" end + defp serialize_argument_value(%Absinthe.Blueprint.Input.Object{fields: fields}) do - "{" <> Enum.map_join(fields, ", ", fn field -> - "#{field.name}: #{serialize_argument_value(field.input_value)}" - end) <> "}" + "{" <> + Enum.map_join(fields, ", ", fn field -> + "#{field.name}: #{serialize_argument_value(field.input_value)}" + end) <> "}" end - defp serialize_argument_value(%Absinthe.Blueprint.Input.RawValue{content: content}), do: serialize_argument_value(content) - defp serialize_argument_value(%Absinthe.Blueprint.Input.Value{raw: raw}), do: serialize_argument_value(raw) + + defp serialize_argument_value(%Absinthe.Blueprint.Input.RawValue{content: content}), + do: serialize_argument_value(content) + + defp serialize_argument_value(%Absinthe.Blueprint.Input.Value{raw: raw}), + do: serialize_argument_value(raw) + defp serialize_argument_value(value), do: inspect(value) defimpl Inspect do diff --git a/lib/absinthe/blueprint/schema/scalar_type_definition.ex b/lib/absinthe/blueprint/schema/scalar_type_definition.ex index 9c29c741a6..142054184d 100644 --- a/lib/absinthe/blueprint/schema/scalar_type_definition.ex +++ b/lib/absinthe/blueprint/schema/scalar_type_definition.ex @@ -36,7 +36,10 @@ defmodule Absinthe.Blueprint.Schema.ScalarTypeDefinition do identifier: type_def.identifier, name: type_def.name, description: type_def.description, - applied_directives: Absinthe.Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(type_def.directives), + applied_directives: + Absinthe.Blueprint.Schema.ObjectTypeDefinition.build_applied_directives( + type_def.directives + ), definition: type_def.module, serialize: type_def.serialize, parse: type_def.parse, diff --git a/lib/absinthe/blueprint/schema/union_type_definition.ex b/lib/absinthe/blueprint/schema/union_type_definition.ex index 321470cd24..2a64df328f 100644 --- a/lib/absinthe/blueprint/schema/union_type_definition.ex +++ b/lib/absinthe/blueprint/schema/union_type_definition.ex @@ -39,7 +39,8 @@ defmodule Absinthe.Blueprint.Schema.UnionTypeDefinition do identifier: type_def.identifier, types: type_def.types |> atomize_types(schema), fields: build_fields(type_def, schema), - applied_directives: Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(type_def.directives), + applied_directives: + Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(type_def.directives), definition: type_def.module, resolve_type: type_def.resolve_type } @@ -64,7 +65,8 @@ defmodule Absinthe.Blueprint.Schema.UnionTypeDefinition do name: field_def.name, type: Blueprint.TypeReference.to_type(field_def.type, schema), args: build_args(field_def, schema), - applied_directives: Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(field_def.directives), + applied_directives: + Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(field_def.directives), definition: field_def.module, __reference__: field_def.__reference__, __private__: field_def.__private__ @@ -83,7 +85,8 @@ defmodule Absinthe.Blueprint.Schema.UnionTypeDefinition do type: Blueprint.TypeReference.to_type(arg_def.type, schema), default_value: arg_def.default_value, deprecation: arg_def.deprecation, - applied_directives: Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(arg_def.directives), + applied_directives: + Blueprint.Schema.ObjectTypeDefinition.build_applied_directives(arg_def.directives), __reference__: arg_def.__reference__, __private__: arg_def.__private__ } diff --git a/lib/absinthe/incremental/complexity.ex b/lib/absinthe/incremental/complexity.ex index 1b468a9f22..bf0f52f6cd 100644 --- a/lib/absinthe/incremental/complexity.ex +++ b/lib/absinthe/incremental/complexity.ex @@ -368,7 +368,13 @@ defmodule Absinthe.Incremental.Complexity do end end - defp analyze_node(%Blueprint.Document.Fragment.Spread{} = node, schema, config, analysis, depth) do + defp analyze_node( + %Blueprint.Document.Fragment.Spread{} = node, + _schema, + config, + analysis, + depth + ) do {analysis, _in_defer} = check_defer_directive(node, config, analysis, depth) # Would need to look up the fragment definition for full analysis analysis diff --git a/lib/absinthe/incremental/dataloader.ex b/lib/absinthe/incremental/dataloader.ex index 97a320cbce..187ebb924f 100644 --- a/lib/absinthe/incremental/dataloader.ex +++ b/lib/absinthe/incremental/dataloader.ex @@ -127,7 +127,9 @@ defmodule Absinthe.Incremental.Dataloader do This allows existing Dataloader resolvers to work with incremental delivery. """ - @spec streaming_dataloader(atom(), any()) :: Resolution.resolver() + @spec streaming_dataloader(atom(), any()) :: + (Resolution.source(), Resolution.arguments(), Resolution.t() -> + {:ok, any()} | {:error, any()} | {:middleware, module(), any()}) def streaming_dataloader(source, batch_key \\ nil) do fn parent, args, %{context: context} = resolution -> # Check if we're in a streaming context @@ -305,7 +307,7 @@ defmodule Absinthe.Incremental.Dataloader do } # Add to the batch queue in the resolution context - resolution = + _resolution = update_in( resolution.context[:__dataloader_batch_queue__], &[batch_data | &1 || []] diff --git a/lib/absinthe/incremental/error_handler.ex b/lib/absinthe/incremental/error_handler.ex index 28bba898b3..8c154d9208 100644 --- a/lib/absinthe/incremental/error_handler.ex +++ b/lib/absinthe/incremental/error_handler.ex @@ -6,7 +6,6 @@ defmodule Absinthe.Incremental.ErrorHandler do streaming operations, ensuring robust behavior even when things go wrong. """ - alias Absinthe.Incremental.Response require Logger @type error_type :: @@ -315,18 +314,11 @@ defmodule Absinthe.Incremental.ErrorHandler do } end - defp format_exception(exception, stacktrace \\ nil) do - formatted_stacktrace = - if stacktrace do - Exception.format_stacktrace(stacktrace) - else - "stacktrace not available" - end - + defp format_exception(exception, stacktrace) do %{ message: Exception.message(exception), type: exception.__struct__, - stacktrace: formatted_stacktrace + stacktrace: Exception.format_stacktrace(stacktrace) } end diff --git a/lib/absinthe/incremental/resource_manager.ex b/lib/absinthe/incremental/resource_manager.ex index b00256c65a..a36e20f4fd 100644 --- a/lib/absinthe/incremental/resource_manager.ex +++ b/lib/absinthe/incremental/resource_manager.ex @@ -258,6 +258,16 @@ defmodule Absinthe.Incremental.ResourceManager do update_in(state.stream_stats.total_count, &(&1 + 1)) end + defp update_stats(state, :stream_timeout) do + state + |> update_in([:stream_stats, :timeout_count], &(&1 + 1)) + |> update_in([:stream_stats, :failed_count], &(&1 + 1)) + end + + defp update_stats(state, :stream_crashed) do + update_in(state.stream_stats.failed_count, &(&1 + 1)) + end + defp update_stats(state, :stream_released, duration) do state |> update_in([:stream_stats, :completed_count], &(&1 + 1)) @@ -269,16 +279,6 @@ defmodule Absinthe.Incremental.ResourceManager do end) end - defp update_stats(state, :stream_timeout) do - state - |> update_in([:stream_stats, :timeout_count], &(&1 + 1)) - |> update_in([:stream_stats, :failed_count], &(&1 + 1)) - end - - defp update_stats(state, :stream_crashed) do - update_in(state.stream_stats.failed_count, &(&1 + 1)) - end - defp schedule_stream_timeout(operation_id, timeout_ms) do Process.send_after(self(), {:stream_timeout, operation_id}, timeout_ms) end diff --git a/lib/absinthe/incremental/response.ex b/lib/absinthe/incremental/response.ex index 8d016ab501..d5d47cd580 100644 --- a/lib/absinthe/incremental/response.ex +++ b/lib/absinthe/incremental/response.ex @@ -8,34 +8,35 @@ defmodule Absinthe.Incremental.Response do alias Absinthe.Blueprint @type initial_response :: %{ - data: map(), - pending: list(pending_item()), - hasNext: boolean(), - errors: list(map()) | nil + required(:data) => map(), + required(:pending) => [pending_item()], + required(:hasNext) => boolean(), + optional(:errors) => [map()] } @type incremental_response :: %{ - incremental: list(incremental_item()), - hasNext: boolean(), - completed: list(completed_item()) | nil + required(:hasNext) => boolean(), + optional(:incremental) => [incremental_item()], + optional(:completed) => [completed_item()] } @type pending_item :: %{ - id: String.t(), - path: list(String.t() | integer()), - label: String.t() | nil + required(:id) => String.t(), + required(:path) => [String.t() | integer()], + optional(:label) => String.t() } @type incremental_item :: %{ - data: any(), - path: list(String.t() | integer()), - label: String.t() | nil, - errors: list(map()) | nil + optional(:data) => any(), + optional(:items) => [any()], + required(:path) => [String.t() | integer()], + optional(:label) => String.t(), + optional(:errors) => [map()] } @type completed_item :: %{ - id: String.t(), - errors: list(map()) | nil + required(:id) => String.t(), + optional(:errors) => [map()] } @doc """ @@ -72,7 +73,7 @@ defmodule Absinthe.Incremental.Response do - A hasNext flag indicating if more payloads are coming - Optional completed items to signal completion of specific operations """ - @spec build_incremental(any(), list(), String.t() | nil, boolean()) :: incremental_response() + @spec build_incremental(any(), [any()], String.t() | nil, boolean()) :: incremental_response() def build_incremental(data, path, label, has_next) do incremental_item = %{ data: data, @@ -95,7 +96,7 @@ defmodule Absinthe.Incremental.Response do @doc """ Build an incremental response for streamed list items. """ - @spec build_stream_incremental(list(), list(), String.t() | nil, boolean()) :: + @spec build_stream_incremental([any()], [any()], String.t() | nil, boolean()) :: incremental_response() def build_stream_incremental(items, path, label, has_next) do incremental_item = %{ @@ -119,7 +120,7 @@ defmodule Absinthe.Incremental.Response do @doc """ Build a completion response to signal the end of incremental delivery. """ - @spec build_completed(list(String.t())) :: incremental_response() + @spec build_completed([String.t()]) :: incremental_response() def build_completed(completed_ids) do completed_items = Enum.map(completed_ids, fn id -> @@ -135,7 +136,7 @@ defmodule Absinthe.Incremental.Response do @doc """ Build an error response for a failed incremental operation. """ - @spec build_error(list(map()), list(), String.t() | nil, boolean()) :: incremental_response() + @spec build_error([map()], [any()], String.t() | nil, boolean()) :: incremental_response() def build_error(errors, path, label, has_next) do incremental_item = %{ errors: errors, @@ -193,7 +194,7 @@ defmodule Absinthe.Incremental.Response do end) end - defp remove_at_path(data, []), do: nil + defp remove_at_path(_data, []), do: nil defp remove_at_path(data, [key | rest]) when is_map(data) do case Map.get(data, key) do diff --git a/lib/absinthe/incremental/transport.ex b/lib/absinthe/incremental/transport.ex index 3bfc63034e..96eae1c3b9 100644 --- a/lib/absinthe/incremental/transport.ex +++ b/lib/absinthe/incremental/transport.ex @@ -71,7 +71,9 @@ defmodule Absinthe.Incremental.Transport do alias Absinthe.Incremental.{Config, Response} alias Absinthe.Streaming.Executor - @type conn_or_socket :: Plug.Conn.t() | Phoenix.Socket.t() | any() + # Plug.Conn.t() | Phoenix.Socket.t() — optional dependencies, kept as any() + # so this module can be used without Plug or Phoenix. + @type conn_or_socket :: any() @type state :: any() @type response :: map() @@ -234,6 +236,7 @@ defmodule Absinthe.Incremental.Transport do # Get configurable executor (defaults to TaskExecutor) executor = Absinthe.Streaming.Executor.get_executor(schema, options) + executor_opts = [ timeout: timeout, max_concurrency: System.schedulers_online() * 2 @@ -488,6 +491,7 @@ defmodule Absinthe.Incremental.Transport do # Use configurable executor (defaults to TaskExecutor) executor = Executor.get_executor(schema, options) + incremental_results = all_tasks |> executor.execute(timeout: timeout) diff --git a/lib/absinthe/lexer.ex b/lib/absinthe/lexer.ex index 7477d022e9..d69785c604 100644 --- a/lib/absinthe/lexer.ex +++ b/lib/absinthe/lexer.ex @@ -260,7 +260,9 @@ defmodule Absinthe.Lexer do {:error, message, _rest, _context, {line, line_offset}, byte_offset} when is_binary(message) -> byte_column = byte_offset - line_offset + 1 - {:error, :invalid_unicode_escape, message, byte_loc_to_char_loc({line, byte_column}, lines)} + + {:error, :invalid_unicode_escape, message, + byte_loc_to_char_loc({line, byte_column}, lines)} {:ok, tokens, "", _, _, _} -> tokens = convert_token_columns_from_byte_to_char(tokens, lines) @@ -399,7 +401,7 @@ defmodule Absinthe.Lexer do # Decode a surrogate pair to a Unicode scalar value defp decode_surrogate_pair(high, low) do - 0x10000 + ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000 + (high - 0xD800) * 0x400 + (low - 0xDC00) end # Variable-width Unicode escape: \u{XXXXXX} diff --git a/lib/absinthe/middleware/auto_defer_stream.ex b/lib/absinthe/middleware/auto_defer_stream.ex index cc332be899..17462854e9 100644 --- a/lib/absinthe/middleware/auto_defer_stream.ex +++ b/lib/absinthe/middleware/auto_defer_stream.ex @@ -334,7 +334,7 @@ defmodule Absinthe.Middleware.AutoDeferStream do } end - defp apply_defer(resolution, config) do + defp apply_defer(resolution, _config) do # Add defer flag to the field field = put_in( diff --git a/lib/absinthe/phase/document/execution/streaming_resolution.ex b/lib/absinthe/phase/document/execution/streaming_resolution.ex index 65971a3661..8b61ef0950 100644 --- a/lib/absinthe/phase/document/execution/streaming_resolution.ex +++ b/lib/absinthe/phase/document/execution/streaming_resolution.ex @@ -1,451 +1,135 @@ defmodule Absinthe.Phase.Document.Execution.StreamingResolution do @moduledoc """ - Resolution phase with support for @defer and @stream directives. + Resolution phase with support for @defer directive. Replaces standard resolution when incremental delivery is enabled. - This phase detects @defer and @stream directives in the query and sets up - the execution context for incremental delivery. The actual streaming happens - through the transport layer. + Strategy: run standard resolution (resolves everything), then store defer + metadata in the streaming context. The transport layer (plug) splits the + final result into initial/incremental payloads after the Result phase runs. """ use Absinthe.Phase alias Absinthe.{Blueprint, Phase} alias Absinthe.Phase.Document.Execution.Resolution - @doc """ - Run the streaming resolution phase. - - If no streaming directives are detected, falls back to standard resolution. - Otherwise, sets up the blueprint for incremental delivery. - """ @spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t() def run(blueprint, options \\ []) do - case detect_streaming_directives(blueprint) do - true -> - run_streaming(blueprint, options) - - false -> - # No streaming directives, use standard resolution - Resolution.run(blueprint, options) - end - end - - # Detect if the query contains @defer or @stream directives - defp detect_streaming_directives(blueprint) do - blueprint - |> Blueprint.prewalk(false, fn - %{flags: %{defer: _}}, _acc -> {nil, true} - %{flags: %{stream: _}}, _acc -> {nil, true} - node, acc -> {node, acc} - end) - |> elem(1) - end - - defp run_streaming(blueprint, options) do - blueprint - |> init_streaming_context() - |> collect_and_prepare_streaming_nodes() - |> run_initial_resolution(options) - |> setup_deferred_execution(options) - end - - # Initialize the streaming context in the blueprint - defp init_streaming_context(blueprint) do - streaming_context = %{ - deferred_fragments: [], - streamed_fields: [], - deferred_tasks: [], - stream_tasks: [], - operation_id: generate_operation_id(), - schema: blueprint.schema, - # Store original operations for deferred re-resolution - original_operations: blueprint.operations - } - - updated_context = Map.put(blueprint.execution.context, :__streaming__, streaming_context) - updated_execution = %{blueprint.execution | context: updated_context} - %{blueprint | execution: updated_execution} - end - - # Collect deferred/streamed nodes and prepare blueprint for initial resolution - defp collect_and_prepare_streaming_nodes(blueprint) do - # Track current path during traversal - initial_acc = %{ - deferred_fragments: [], - streamed_fields: [], - path: [] - } - - {updated_blueprint, collected} = - Blueprint.prewalk(blueprint, initial_acc, &collect_streaming_node/2) - - # Store collected nodes in streaming context - streaming_context = get_streaming_context(updated_blueprint) - - updated_streaming_context = %{ - streaming_context - | deferred_fragments: Enum.reverse(collected.deferred_fragments), - streamed_fields: Enum.reverse(collected.streamed_fields) - } - - put_streaming_context(updated_blueprint, updated_streaming_context) - end - - # Collect streaming nodes during prewalk and mark them appropriately - defp collect_streaming_node(node, acc) do - case node do - # Handle deferred fragments (inline or spread) - %{flags: %{defer: %{enabled: true} = defer_config}} = fragment_node -> - # Build path for this fragment - path = build_node_path(fragment_node, acc.path) - - # Collect the deferred fragment info - deferred_info = %{ - node: fragment_node, - path: path, - label: defer_config[:label], - selections: get_selections(fragment_node) - } - - # Mark the node to skip in initial resolution - updated_node = mark_for_skip(fragment_node) - updated_acc = %{acc | deferred_fragments: [deferred_info | acc.deferred_fragments]} - - {updated_node, updated_acc} - - # Handle streamed list fields - %{flags: %{stream: %{enabled: true} = stream_config}} = field_node -> - # Build path for this field - path = build_node_path(field_node, acc.path) - - # Collect the streamed field info - streamed_info = %{ - node: field_node, - path: path, - label: stream_config[:label], - initial_count: stream_config[:initial_count] || 0 - } - - # Keep the field but mark it with stream config for partial resolution - updated_node = mark_for_streaming(field_node, stream_config) - updated_acc = %{acc | streamed_fields: [streamed_info | acc.streamed_fields]} - - {updated_node, updated_acc} - - # Track path through fields for accurate path building - %Absinthe.Blueprint.Document.Field{name: name} = field_node -> - updated_acc = %{acc | path: acc.path ++ [name]} - {field_node, updated_acc} - - # Pass through other nodes - other -> - {other, acc} - end - end - - # Mark a node to be skipped in initial resolution - defp mark_for_skip(node) do - flags = - node.flags - |> Map.delete(:defer) - |> Map.put(:__skip_initial__, true) - - %{node | flags: flags} - end - - # Mark a field for streaming (partial resolution) - defp mark_for_streaming(node, stream_config) do - flags = - node.flags - |> Map.delete(:stream) - |> Map.put(:__stream_config__, stream_config) - - %{node | flags: flags} - end - - # Build the path for a node - defp build_node_path(%{name: name}, parent_path) when is_binary(name) do - parent_path ++ [name] - end - - defp build_node_path(%Absinthe.Blueprint.Document.Fragment.Spread{name: name}, parent_path) do - parent_path ++ [name] - end - - defp build_node_path(_node, parent_path) do - parent_path - end - - # Get selections from a fragment node - defp get_selections(%{selections: selections}) when is_list(selections), do: selections - defp get_selections(_), do: [] - - # Run initial resolution, skipping deferred content - defp run_initial_resolution(blueprint, options) do - # Filter out deferred nodes before resolution - filtered_blueprint = filter_deferred_selections(blueprint) - - # Run standard resolution on filtered blueprint - Resolution.run(filtered_blueprint, options) - end - - # Filter out selections that are marked for skipping - defp filter_deferred_selections(blueprint) do - Blueprint.prewalk(blueprint, fn - # Skip nodes marked for deferral - %{flags: %{__skip_initial__: true}} -> - nil - - # For streamed fields, limit the resolution to initial_count - %{flags: %{__stream_config__: config}} = node -> - # The stream config is preserved, resolution middleware will handle limiting - node + defer_info = collect_defer_info(blueprint) - node -> - node - end) - end - - # Setup deferred execution after initial resolution - defp setup_deferred_execution({:ok, blueprint}, options) do - streaming_context = get_streaming_context(blueprint) - - if has_pending_operations?(streaming_context) do - blueprint - |> create_deferred_tasks(options) - |> create_stream_tasks(options) - |> mark_as_streaming() + if Enum.empty?(defer_info) do + Resolution.run(blueprint, options) else - {:ok, blueprint} - end - end - - defp setup_deferred_execution(error, _options), do: error - - # Create executable tasks for deferred fragments - defp create_deferred_tasks(blueprint, options) do - streaming_context = get_streaming_context(blueprint) - - deferred_tasks = - Enum.map(streaming_context.deferred_fragments, fn fragment_info -> - create_deferred_task(fragment_info, blueprint, options) - end) - - updated_context = %{streaming_context | deferred_tasks: deferred_tasks} - put_streaming_context(blueprint, updated_context) - end - - # Create executable tasks for streamed fields - defp create_stream_tasks(blueprint, options) do - streaming_context = get_streaming_context(blueprint) - - stream_tasks = - Enum.map(streaming_context.streamed_fields, fn field_info -> - create_stream_task(field_info, blueprint, options) - end) - - updated_context = %{streaming_context | stream_tasks: stream_tasks} - put_streaming_context(blueprint, updated_context) - end - - defp create_deferred_task(fragment_info, blueprint, options) do - %{ - id: generate_task_id(), - type: :defer, - label: fragment_info.label, - path: fragment_info.path, - status: :pending, - execute: fn -> - resolve_deferred_fragment(fragment_info, blueprint, options) - end - } - end - - defp create_stream_task(field_info, blueprint, options) do - %{ - id: generate_task_id(), - type: :stream, - label: field_info.label, - path: field_info.path, - initial_count: field_info.initial_count, - status: :pending, - execute: fn -> - resolve_streamed_field(field_info, blueprint, options) + # Run standard resolution — resolves everything including deferred fields. + # The result split happens later in the transport layer. + case Resolution.run(blueprint, options) do + {:ok, blueprint} -> + streaming_context = %{ + defer_info: defer_info, + operation_id: generate_operation_id(), + # Backwards-compat keys expected by Absinthe.Incremental.* + deferred_fragments: defer_info, + streamed_fields: [], + deferred_tasks: [], + stream_tasks: [] + } + + updated_context = + Map.put(blueprint.execution.context, :__streaming__, streaming_context) + + updated_execution = %{blueprint.execution | context: updated_context} + updated_execution = Map.put(updated_execution, :incremental_delivery, true) + + {:ok, %{blueprint | execution: updated_execution}} + + error -> + error end - } - end - - # Resolve a deferred fragment by re-running resolution on just that fragment - defp resolve_deferred_fragment(fragment_info, blueprint, options) do - # Restore the original node without skip flag - node = restore_deferred_node(fragment_info.node) - - # Get the parent data at this path from the initial result - parent_data = get_parent_data(blueprint, fragment_info.path) - - # Create a focused blueprint for just this fragment's fields - sub_blueprint = build_sub_blueprint(blueprint, node, parent_data, fragment_info.path) - - # Run resolution - case Resolution.run(sub_blueprint, options) do - {:ok, resolved_blueprint} -> - {:ok, extract_fragment_result(resolved_blueprint, fragment_info)} - - {:error, _} = error -> - error - end - rescue - e -> - {:error, - %{ - message: Exception.message(e), - path: fragment_info.path, - extensions: %{code: "DEFERRED_RESOLUTION_ERROR"} - }} - end - - # Resolve remaining items for a streamed field - defp resolve_streamed_field(field_info, blueprint, options) do - # Get the full list by re-resolving without the limit - node = restore_streamed_node(field_info.node) - - parent_data = get_parent_data(blueprint, Enum.drop(field_info.path, -1)) - - sub_blueprint = build_sub_blueprint(blueprint, node, parent_data, field_info.path) - - case Resolution.run(sub_blueprint, options) do - {:ok, resolved_blueprint} -> - {:ok, extract_stream_result(resolved_blueprint, field_info)} - - {:error, _} = error -> - error end - rescue - e -> - {:error, - %{ - message: Exception.message(e), - path: field_info.path, - extensions: %{code: "STREAM_RESOLUTION_ERROR"} - }} end - # Restore a deferred node for resolution - defp restore_deferred_node(node) do - flags = Map.delete(node.flags, :__skip_initial__) - %{node | flags: flags} + # Walk operations to find @defer fragments with their parent field path. + defp collect_defer_info(blueprint) do + blueprint.operations + |> Enum.flat_map(fn op -> + walk_selections(op.selections, [], blueprint) + end) end - # Restore a streamed node for full resolution - defp restore_streamed_node(node) do - flags = Map.delete(node.flags, :__stream_config__) - %{node | flags: flags} + defp walk_selections(selections, parent_path, blueprint) when is_list(selections) do + Enum.flat_map(selections, fn sel -> walk_selection(sel, parent_path, blueprint) end) end - # Get parent data from the result at a given path - defp get_parent_data(blueprint, []) do - blueprint.result[:data] || %{} - end + defp walk_selections(_, _, _blueprint), do: [] - defp get_parent_data(blueprint, path) do - parent_path = Enum.drop(path, -1) - get_in(blueprint.result, [:data | parent_path]) || %{} + defp walk_selection( + %Blueprint.Document.Field{name: name, selections: sels}, + parent_path, + blueprint + ) do + walk_selections(sels, parent_path ++ [name], blueprint) end - # Build a sub-blueprint for resolving deferred/streamed content - defp build_sub_blueprint(blueprint, node, parent_data, path) do - # Create execution context with parent data - execution = %{blueprint.execution | root_value: parent_data, path: path} - - # Create a minimal blueprint with just the node to resolve - %{blueprint | execution: execution, operations: [wrap_in_operation(node, blueprint)]} - end + defp walk_selection( + %Blueprint.Document.Fragment.Inline{ + flags: %{defer: %{enabled: true} = config}, + selections: sels + }, + parent_path, + blueprint + ) do + field_names = Enum.flat_map(sels, &extract_field_names/1) - # Wrap a node in a minimal operation structure - defp wrap_in_operation(node, blueprint) do - %Absinthe.Blueprint.Document.Operation{ - name: "__deferred__", - type: :query, - selections: get_node_selections(node), - schema_node: get_query_type(blueprint) - } + [ + %{label: config[:label], field_names: field_names, parent_path: parent_path} + | walk_selections(sels, parent_path, blueprint) + ] end - defp get_node_selections(%{selections: selections}), do: selections - defp get_node_selections(node), do: [node] - - defp get_query_type(blueprint) do - Absinthe.Schema.lookup_type(blueprint.schema, :query) + defp walk_selection( + %Blueprint.Document.Fragment.Inline{selections: sels}, + parent_path, + blueprint + ) do + walk_selections(sels, parent_path, blueprint) end - # Extract result from a resolved deferred fragment - defp extract_fragment_result(blueprint, fragment_info) do - data = blueprint.result[:data] || %{} - errors = blueprint.result[:errors] || [] - - result = %{ - data: data, - path: fragment_info.path, - label: fragment_info.label - } - - if Enum.empty?(errors) do - result - else - Map.put(result, :errors, errors) - end - end + # Handle Fragment.Spread — resolve the named fragment and check for @defer + # on both the spread itself and nested inline fragments within the fragment. + defp walk_selection( + %Blueprint.Document.Fragment.Spread{name: name, flags: flags}, + parent_path, + blueprint + ) do + case Blueprint.fragment(blueprint, name) do + nil -> + [] - # Extract remaining items from a resolved stream - defp extract_stream_result(blueprint, field_info) do - full_list = get_in(blueprint.result, [:data | [List.last(field_info.path)]]) || [] - remaining_items = Enum.drop(full_list, field_info.initial_count) - errors = blueprint.result[:errors] || [] + %{selections: sels} -> + spread_defers = + case flags do + %{defer: %{enabled: true} = config} -> + field_names = Enum.flat_map(sels, &extract_field_names/1) + [%{label: config[:label], field_names: field_names, parent_path: parent_path}] - result = %{ - items: remaining_items, - path: field_info.path, - label: field_info.label - } + _ -> + [] + end - if Enum.empty?(errors) do - result - else - Map.put(result, :errors, errors) + spread_defers ++ walk_selections(sels, parent_path, blueprint) end end - defp mark_as_streaming(blueprint) do - updated_execution = Map.put(blueprint.execution, :incremental_delivery, true) - {:ok, %{blueprint | execution: updated_execution}} - end + defp walk_selection(_, _parent_path, _blueprint), do: [] - defp has_pending_operations?(streaming_context) do - not Enum.empty?(streaming_context.deferred_fragments) or - not Enum.empty?(streaming_context.streamed_fields) - end + defp extract_field_names(%Blueprint.Document.Field{name: name}), do: [name] - defp get_streaming_context(blueprint) do - get_in(blueprint.execution.context, [:__streaming__]) || - %{ - deferred_fragments: [], - streamed_fields: [], - deferred_tasks: [], - stream_tasks: [] - } + defp extract_field_names(%{selections: sels}) when is_list(sels) do + Enum.flat_map(sels, &extract_field_names/1) end - defp put_streaming_context(blueprint, context) do - updated_context = Map.put(blueprint.execution.context, :__streaming__, context) - updated_execution = %{blueprint.execution | context: updated_context} - %{blueprint | execution: updated_execution} - end + defp extract_field_names(_), do: [] defp generate_operation_id do :crypto.strong_rand_bytes(16) |> Base.encode16(case: :lower) end - - defp generate_task_id do - :crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower) - end end diff --git a/lib/absinthe/phase/parse.ex b/lib/absinthe/phase/parse.ex index 1660687022..68ac4c8563 100644 --- a/lib/absinthe/phase/parse.ex +++ b/lib/absinthe/phase/parse.ex @@ -116,7 +116,9 @@ defmodule Absinthe.Phase.Parse do %Phase.Error{message: message, locations: [%{line: line, column: column}], phase: __MODULE__} end - @spec format_raw_parse_error({:unicode_escape, String.t(), {line :: pos_integer, column :: pos_integer}}) :: + @spec format_raw_parse_error( + {:unicode_escape, String.t(), {line :: pos_integer, column :: pos_integer}} + ) :: Phase.Error.t() defp format_raw_parse_error({:unicode_escape, message, {line, column}}) do %Phase.Error{message: message, locations: [%{line: line, column: column}], phase: __MODULE__} diff --git a/lib/absinthe/phase/schema.ex b/lib/absinthe/phase/schema.ex index 9a7f54281d..525c2a96d8 100644 --- a/lib/absinthe/phase/schema.ex +++ b/lib/absinthe/phase/schema.ex @@ -98,7 +98,13 @@ defmodule Absinthe.Phase.Schema do %{node | schema_node: schema_node, type_condition: %{condition | schema_node: schema_node}} end - defp set_schema_node(%Blueprint.Directive{name: name} = node, _parent, schema, adapter, blueprint_directives) do + defp set_schema_node( + %Blueprint.Directive{name: name} = node, + _parent, + schema, + adapter, + blueprint_directives + ) do %{node | schema_node: find_schema_directive(name, schema, adapter, blueprint_directives)} end @@ -168,11 +174,23 @@ defmodule Absinthe.Phase.Schema do ) end - defp set_schema_node(%Blueprint.Document.Field{} = node, parent, schema, adapter, _blueprint_directives) do + defp set_schema_node( + %Blueprint.Document.Field{} = node, + parent, + schema, + adapter, + _blueprint_directives + ) do %{node | schema_node: find_schema_field(parent.schema_node, node.name, schema, adapter)} end - defp set_schema_node(%Blueprint.Input.Argument{name: name} = node, parent, _schema, adapter, _blueprint_directives) do + defp set_schema_node( + %Blueprint.Input.Argument{name: name} = node, + parent, + _schema, + adapter, + _blueprint_directives + ) do schema_node = find_schema_argument(parent.schema_node, name, adapter) %{node | schema_node: schema_node} end @@ -181,7 +199,13 @@ defmodule Absinthe.Phase.Schema do node end - defp set_schema_node(%Blueprint.Input.Field{} = node, parent, schema, adapter, _blueprint_directives) do + defp set_schema_node( + %Blueprint.Input.Field{} = node, + parent, + schema, + adapter, + _blueprint_directives + ) do case node.name do "__" <> _ -> %{node | schema_node: nil} @@ -191,7 +215,13 @@ defmodule Absinthe.Phase.Schema do end end - defp set_schema_node(%Blueprint.Input.List{} = node, parent, _schema, _adapter, _blueprint_directives) do + defp set_schema_node( + %Blueprint.Input.List{} = node, + parent, + _schema, + _adapter, + _blueprint_directives + ) do case Type.unwrap_non_null(parent.schema_node) do %{of_type: internal_type} -> %{node | schema_node: internal_type} @@ -214,7 +244,13 @@ defmodule Absinthe.Phase.Schema do end end - defp set_schema_node(%{schema_node: nil} = node, %Blueprint.Input.Value{} = parent, _schema, _, _blueprint_directives) do + defp set_schema_node( + %{schema_node: nil} = node, + %Blueprint.Input.Value{} = parent, + _schema, + _, + _blueprint_directives + ) do %{node | schema_node: parent.schema_node} end diff --git a/lib/absinthe/schema/coordinate.ex b/lib/absinthe/schema/coordinate.ex index 2c6fbcddfc..a80e968111 100644 --- a/lib/absinthe/schema/coordinate.ex +++ b/lib/absinthe/schema/coordinate.ex @@ -292,7 +292,8 @@ defmodule Absinthe.Schema.Coordinate do end defp resolve_parsed(schema, {:argument, type_name, field_name, arg_name}, coordinate) do - with {:ok, field} <- resolve_parsed(schema, {:field, type_name, field_name}, "#{type_name}.#{field_name}"), + with {:ok, field} <- + resolve_parsed(schema, {:field, type_name, field_name}, "#{type_name}.#{field_name}"), {:ok, arg} <- get_argument(field, arg_name) do {:ok, arg} else @@ -300,24 +301,6 @@ defmodule Absinthe.Schema.Coordinate do end end - defp resolve_parsed(schema, {:enum_value, enum_name, value_name}, coordinate) do - with {:ok, enum_type} <- resolve_parsed(schema, {:type, enum_name}, enum_name), - {:ok, value} <- get_enum_value(enum_type, value_name) do - {:ok, value} - else - {:error, _} -> {:error, "Enum value not found: #{coordinate}"} - end - end - - defp resolve_parsed(schema, {:input_field, type_name, field_name}, coordinate) do - with {:ok, input_type} <- resolve_parsed(schema, {:type, type_name}, type_name), - {:ok, field} <- get_input_field(input_type, field_name) do - {:ok, field} - else - {:error, _} -> {:error, "Input field not found: #{coordinate}"} - end - end - defp resolve_parsed(schema, {:directive, directive_name}, coordinate) do case lookup_directive_by_name(schema, directive_name) do nil -> {:error, "Directive not found: #{coordinate}"} @@ -326,7 +309,8 @@ defmodule Absinthe.Schema.Coordinate do end defp resolve_parsed(schema, {:directive_argument, directive_name, arg_name}, coordinate) do - with {:ok, directive} <- resolve_parsed(schema, {:directive, directive_name}, "@#{directive_name}"), + with {:ok, directive} <- + resolve_parsed(schema, {:directive, directive_name}, "@#{directive_name}"), {:ok, arg} <- get_directive_argument(directive, arg_name) do {:ok, arg} else @@ -393,38 +377,6 @@ defmodule Absinthe.Schema.Coordinate do defp get_argument(_, _), do: {:error, :not_found} - defp get_enum_value(%Absinthe.Type.Enum{values: values}, value_name) when is_map(values) do - result = - Enum.find_value(values, fn {_, value} -> - if value.name == value_name || Atom.to_string(value.value) == value_name do - value - end - end) - - case result do - nil -> {:error, :not_found} - value -> {:ok, value} - end - end - - defp get_enum_value(_, _), do: {:error, :not_found} - - defp get_input_field(%Absinthe.Type.InputObject{fields: fields}, field_name) when is_map(fields) do - result = - Enum.find_value(fields, fn {_, field} -> - if field.name == field_name || Atom.to_string(field.identifier) == field_name do - field - end - end) - - case result do - nil -> {:error, :not_found} - field -> {:ok, field} - end - end - - defp get_input_field(_, _), do: {:error, :not_found} - defp get_directive_argument(%{args: args}, arg_name) when is_map(args) do result = Enum.find_value(args, fn {_, arg} -> diff --git a/lib/absinthe/type/built_ins/incremental_directives.ex b/lib/absinthe/type/built_ins/incremental_directives.ex index ae9a32773b..93dfd01b04 100644 --- a/lib/absinthe/type/built_ins/incremental_directives.ex +++ b/lib/absinthe/type/built_ins/incremental_directives.ex @@ -42,8 +42,6 @@ defmodule Absinthe.Type.BuiltIns.IncrementalDirectives do use Absinthe.Schema.Notation - alias Absinthe.Blueprint - directive :defer do description """ Directs the executor to defer this fragment spread or inline fragment, @@ -76,7 +74,7 @@ defmodule Absinthe.Type.BuiltIns.IncrementalDirectives do enabled: true } - Blueprint.put_flag(node, :defer, defer_config) + update_in(node.flags, &Map.put(&1, :defer, defer_config)) end end @@ -116,7 +114,7 @@ defmodule Absinthe.Type.BuiltIns.IncrementalDirectives do enabled: true } - Blueprint.put_flag(node, :stream, stream_config) + update_in(node.flags, &Map.put(&1, :stream, stream_config)) end end end diff --git a/lib/absinthe/type/semantic_nullability.ex b/lib/absinthe/type/semantic_nullability.ex index e7e4e8a39a..a79e257be0 100644 --- a/lib/absinthe/type/semantic_nullability.ex +++ b/lib/absinthe/type/semantic_nullability.ex @@ -155,7 +155,8 @@ defmodule Absinthe.Type.SemanticNullability do cond do max_level > type_depth -> - {:error, "level #{max_level} requires #{max_level} nested list(s), but type only has #{type_depth}"} + {:error, + "level #{max_level} requires #{max_level} nested list(s), but type only has #{type_depth}"} Enum.any?(levels, &(&1 < 0)) -> {:error, "levels must be non-negative integers"} diff --git a/test/absinthe/incremental/complexity_test.exs b/test/absinthe/incremental/complexity_test.exs index b647f7d624..60620b379e 100644 --- a/test/absinthe/incremental/complexity_test.exs +++ b/test/absinthe/incremental/complexity_test.exs @@ -10,7 +10,7 @@ defmodule Absinthe.Incremental.ComplexityTest do use ExUnit.Case, async: true - alias Absinthe.{Pipeline, Blueprint} + alias Absinthe.Pipeline alias Absinthe.Incremental.Complexity defmodule TestSchema do diff --git a/test/absinthe/schema/coordinate/error_helpers_test.exs b/test/absinthe/schema/coordinate/error_helpers_test.exs index 32ed6f510d..fa9a8d4f8f 100644 --- a/test/absinthe/schema/coordinate/error_helpers_test.exs +++ b/test/absinthe/schema/coordinate/error_helpers_test.exs @@ -21,7 +21,8 @@ defmodule Absinthe.Schema.Coordinate.ErrorHelpersTest do end test "generates directive argument coordinate" do - assert ErrorHelpers.coordinate_for(:directive, "deprecated", "reason") == "@deprecated(reason:)" + assert ErrorHelpers.coordinate_for(:directive, "deprecated", "reason") == + "@deprecated(reason:)" end end diff --git a/test/absinthe/schema/coordinate_test.exs b/test/absinthe/schema/coordinate_test.exs index 8be05f3d24..56c9248357 100644 --- a/test/absinthe/schema/coordinate_test.exs +++ b/test/absinthe/schema/coordinate_test.exs @@ -104,7 +104,9 @@ defmodule Absinthe.Schema.CoordinateTest do test "parse/1 parses argument coordinates" do assert Coordinate.parse("Query.user(id:)") == {:ok, {:argument, "Query", "user", "id"}} - assert Coordinate.parse("User.posts(limit:)") == {:ok, {:argument, "User", "posts", "limit"}} + + assert Coordinate.parse("User.posts(limit:)") == + {:ok, {:argument, "User", "posts", "limit"}} end test "parse/1 parses directive coordinates" do @@ -113,7 +115,9 @@ defmodule Absinthe.Schema.CoordinateTest do end test "parse/1 parses directive argument coordinates" do - assert Coordinate.parse("@deprecated(reason:)") == {:ok, {:directive_argument, "deprecated", "reason"}} + assert Coordinate.parse("@deprecated(reason:)") == + {:ok, {:directive_argument, "deprecated", "reason"}} + assert Coordinate.parse("@include(if:)") == {:ok, {:directive_argument, "include", "if"}} end @@ -167,11 +171,13 @@ defmodule Absinthe.Schema.CoordinateTest do end test "resolve/2 returns error for non-existent type" do - assert {:error, "Type not found: NonExistent"} = Coordinate.resolve(TestSchema, "NonExistent") + assert {:error, "Type not found: NonExistent"} = + Coordinate.resolve(TestSchema, "NonExistent") end test "resolve/2 returns error for non-existent field" do - assert {:error, "Field not found: User.nonexistent"} = Coordinate.resolve(TestSchema, "User.nonexistent") + assert {:error, "Field not found: User.nonexistent"} = + Coordinate.resolve(TestSchema, "User.nonexistent") end test "resolve/2 returns error for non-existent argument" do @@ -180,11 +186,13 @@ defmodule Absinthe.Schema.CoordinateTest do end test "resolve/2 returns error for non-existent directive" do - assert {:error, "Directive not found: @nonexistent"} = Coordinate.resolve(TestSchema, "@nonexistent") + assert {:error, "Directive not found: @nonexistent"} = + Coordinate.resolve(TestSchema, "@nonexistent") end test "resolve/2 returns error for invalid coordinate" do - assert {:error, "Invalid schema coordinate: not valid!"} = Coordinate.resolve(TestSchema, "not valid!") + assert {:error, "Invalid schema coordinate: not valid!"} = + Coordinate.resolve(TestSchema, "not valid!") end end diff --git a/test/absinthe/schema/typesystem_directives_test.exs b/test/absinthe/schema/typesystem_directives_test.exs index 2e61a9c64d..8ef3bf4bf1 100644 --- a/test/absinthe/schema/typesystem_directives_test.exs +++ b/test/absinthe/schema/typesystem_directives_test.exs @@ -154,10 +154,11 @@ defmodule Absinthe.Schema.TypesystemDirectivesTest do arg :query, non_null(:string) resolve fn _, _ -> - {:ok, [ - %{id: "1", name: "Test User", type: :user}, - %{id: "2", title: "Test Post", type: :post} - ]} + {:ok, + [ + %{id: "1", name: "Test User", type: :user}, + %{id: "2", title: "Test Post", type: :post} + ]} end end end @@ -480,7 +481,11 @@ defmodule Absinthe.Schema.TypesystemDirectivesTest do applied = name_field["appliedDirectives"] feature = Enum.find(applied, &(&1["name"] == "feature")) assert feature != nil - assert Enum.find(feature["args"], &(&1["name"] == "name" && &1["value"] == "\"user_name_field\"")) + + assert Enum.find( + feature["args"], + &(&1["name"] == "name" && &1["value"] == "\"user_name_field\"") + ) end test "introspection shows applied directives on enum values" do @@ -545,7 +550,11 @@ defmodule Absinthe.Schema.TypesystemDirectivesTest do applied = format_arg["appliedDirectives"] feature = Enum.find(applied, &(&1["name"] == "feature")) assert feature != nil - assert Enum.find(feature["args"], &(&1["name"] == "name" && &1["value"] == "\"format_arg\"")) + + assert Enum.find( + feature["args"], + &(&1["name"] == "name" && &1["value"] == "\"format_arg\"") + ) end test "introspection shows applied directives on input object fields" do @@ -575,7 +584,11 @@ defmodule Absinthe.Schema.TypesystemDirectivesTest do applied = name_field["appliedDirectives"] feature = Enum.find(applied, &(&1["name"] == "feature")) assert feature != nil - assert Enum.find(feature["args"], &(&1["name"] == "name" && &1["value"] == "\"name_input_field\"")) + + assert Enum.find( + feature["args"], + &(&1["name"] == "name" && &1["value"] == "\"name_input_field\"") + ) end end end