lib/graphql/execution/execute.rb



# frozen_string_literal: true
module GraphQL
  module Execution
    # A valid execution strategy
    # @api private
    class Execute
      PROPAGATE_NULL = :__graphql_propagate_null__

      def execute(ast_operation, root_type, query)
        result = resolve_selection(
          query.root_value,
          root_type,
          query.irep_selection,
          query.context,
          mutation: query.mutation?
        )

        GraphQL::Execution::Lazy.resolve(result)

        result.to_h
      end

      private

      def resolve_selection(object, current_type, selection, query_ctx, mutation: false )
        selection_result = SelectionResult.new

        selection.typed_children[current_type].each do |name, subselection|
          field_result = resolve_field(
            selection_result,
            subselection,
            current_type,
            subselection.definition,
            object,
            query_ctx
          )

          if mutation
            GraphQL::Execution::Lazy.resolve(field_result)
          end

          selection_result.set(name, field_result)

          # If the last subselection caused a null to propagate to _this_ selection,
          # then we may as well quit executing fields because they
          # won't be in the response
          if selection_result.invalid_null?
            break
          end
        end

        selection_result
      end

      def resolve_field(owner, selection, parent_type, field, object, query_ctx)
        query = query_ctx.query
        field_ctx = query_ctx.spawn(
          parent_type: parent_type,
          field: field,
          key: selection.name,
          selection: selection,
        )

        arguments = query.arguments_for(selection, field)
        raw_value = begin
          query_ctx.schema.middleware.invoke([parent_type, object, field, arguments, field_ctx])
        rescue GraphQL::ExecutionError => err
          err
        end

        result = if query.schema.lazy?(raw_value)
          field.prepare_lazy(raw_value, arguments, field_ctx).then { |inner_value|
            continue_resolve_field(owner, selection, parent_type, field, inner_value, field_ctx)
          }
        elsif raw_value.is_a?(GraphQL::Execution::Lazy)
          # It came from a connection resolve, assume it was already instrumented
          raw_value.then { |inner_value|
            continue_resolve_field(owner, selection, parent_type, field, inner_value, field_ctx)
          }
        else
          continue_resolve_field(owner, selection, parent_type, field, raw_value, field_ctx)
        end

        case result
        when PROPAGATE_NULL, GraphQL::Execution::Lazy, SelectionResult
          FieldResult.new(
            owner: owner,
            type: field.type,
            value: result,
          )
        else
          result
        end
      end

      def continue_resolve_field(owner, selection, parent_type, field, raw_value, field_ctx)
        query = field_ctx.query

        case raw_value
        when GraphQL::ExecutionError
          raw_value.ast_node = field_ctx.ast_node
          raw_value.path = field_ctx.path
          query.context.errors.push(raw_value)
        when Array
          list_errors = raw_value.each_with_index.select { |value, _| value.is_a?(GraphQL::ExecutionError) }
          if list_errors.any?
            list_errors.each do |error, index|
              error.ast_node = field_ctx.ast_node
              error.path = field_ctx.path + [index]
              query.context.errors.push(error)
            end
          end
        end

        resolve_value(
          owner,
          parent_type,
          field,
          field.type,
          raw_value,
          selection,
          field_ctx,
        )
      end

      def resolve_value(owner, parent_type, field_defn, field_type, value, selection, field_ctx)
        if value.nil?
          if field_type.kind.non_null?
            type_error = GraphQL::InvalidNullError.new(parent_type, field_defn, value)
            field_ctx.schema.type_error(type_error, field_ctx)
            PROPAGATE_NULL
          else
            nil
          end
        elsif value.is_a?(GraphQL::ExecutionError)
          if field_type.kind.non_null?
            PROPAGATE_NULL
          else
            nil
          end
        else
          case field_type.kind
          when GraphQL::TypeKinds::SCALAR
            field_type.coerce_result(value, field_ctx)
          when GraphQL::TypeKinds::ENUM
            field_type.coerce_result(value, field_ctx)
          when GraphQL::TypeKinds::LIST
            inner_type = field_type.of_type
            i = 0
            result = []
            value.each do |inner_value|
              inner_ctx = field_ctx.spawn(
                key: i,
                selection: selection,
                parent_type: parent_type,
                field: field_defn,
              )

              inner_result = resolve_value(
                owner,
                parent_type,
                field_defn,
                inner_type,
                inner_value,
                selection,
                inner_ctx,
              )

              result << GraphQL::Execution::FieldResult.new(type: inner_type, owner: owner, value: inner_result)
              i += 1
            end
            result
          when GraphQL::TypeKinds::NON_NULL
            wrapped_type = field_type.of_type
            resolve_value(
              owner,
              parent_type,
              field_defn,
              wrapped_type,
              value,
              selection,
              field_ctx,
            )
          when GraphQL::TypeKinds::OBJECT
            resolve_selection(
              value,
              field_type,
              selection,
              field_ctx
            )
          when GraphQL::TypeKinds::UNION, GraphQL::TypeKinds::INTERFACE
            query = field_ctx.query
            resolved_type = query.resolve_type(value)
            possible_types = query.possible_types(field_type)

            if !possible_types.include?(resolved_type)
              type_error = GraphQL::UnresolvedTypeError.new(value, field_defn, parent_type, resolved_type, possible_types)
              field_ctx.schema.type_error(type_error, field_ctx)
              PROPAGATE_NULL
            else
              resolve_value(
                owner,
                parent_type,
                field_defn,
                resolved_type,
                value,
                selection,
                field_ctx,
              )
            end
          else
            raise("Unknown type kind: #{field_type.kind}")
          end
        end
      end

      # A `.call`-able suitable to be the last step in a middleware chain
      module FieldResolveStep
        # Execute the field's resolve method
        def self.call(_parent_type, parent_object, field_definition, field_args, context, _next = nil)
          field_definition.resolve(parent_object, field_args, context)
        end
      end
    end
  end
end