| 
 | 1 | +# frozen_string_literal: true  | 
 | 2 | +require "spec_helper"  | 
 | 3 | + | 
 | 4 | +describe "GraphQL::Execution::Interpreter for breadth-first execution" do  | 
 | 5 | +  # A breadth-first interpreter uses the following runtime interface:  | 
 | 6 | +  # - evaluate_selection(result_key, ast_nodes, selections_result)  | 
 | 7 | +  # - exit_with_inner_result?  | 
 | 8 | +  class SimpleBreadthRuntime < GraphQL::Execution::Interpreter::Runtime  | 
 | 9 | +    class BreadthObject < GraphQL::Execution::Interpreter::Runtime::GraphQLResultHash  | 
 | 10 | +      attr_accessor :breadth_index  | 
 | 11 | +    end  | 
 | 12 | + | 
 | 13 | +    def initialize(query:)  | 
 | 14 | +      query.multiplex = GraphQL::Execution::Multiplex.new(  | 
 | 15 | +        schema: query.schema,  | 
 | 16 | +        queries: [query],  | 
 | 17 | +        context: query.context,  | 
 | 18 | +        max_complexity: nil,  | 
 | 19 | +      )  | 
 | 20 | + | 
 | 21 | +      super(query: query, lazies_at_depth: Hash.new { |h, k| h[k] = [] })  | 
 | 22 | +      @breadth_results_by_key = {}  | 
 | 23 | +    end  | 
 | 24 | + | 
 | 25 | +    def run  | 
 | 26 | +      result = nil  | 
 | 27 | +      query.current_trace.execute_multiplex(multiplex: query.multiplex) do  | 
 | 28 | +        query.current_trace.execute_query(query: query) do  | 
 | 29 | +          result = yield  | 
 | 30 | +        end  | 
 | 31 | +      end  | 
 | 32 | +      result  | 
 | 33 | +    ensure  | 
 | 34 | +      delete_all_interpreter_context  | 
 | 35 | +    end  | 
 | 36 | + | 
 | 37 | +    def evaluate_breadth_selection(objects, parent_type, node)  | 
 | 38 | +      result_key = node.alias || node.name  | 
 | 39 | +      @breadth_results_by_key[result_key] = Array.new(objects.size)  | 
 | 40 | +      objects.each_with_index do |object, index|  | 
 | 41 | +        app_value = parent_type.wrap(object, query.context)  | 
 | 42 | +        breadth_object = BreadthObject.new(nil, parent_type, app_value, nil, false, node.selections, false, node, nil, nil)  | 
 | 43 | +        breadth_object.ordered_result_keys = []  | 
 | 44 | +        breadth_object.breadth_index = index  | 
 | 45 | + | 
 | 46 | +        state = get_current_runtime_state  | 
 | 47 | +        state.current_result_name = nil  | 
 | 48 | +        state.current_result = breadth_object  | 
 | 49 | +        @dataloader.append_job { evaluate_selection(result_key, node, breadth_object) }  | 
 | 50 | +      end  | 
 | 51 | + | 
 | 52 | +      @dataloader.run  | 
 | 53 | +      GraphQL::Execution::Interpreter::Resolve.resolve_each_depth(@lazies_at_depth, @dataloader)  | 
 | 54 | + | 
 | 55 | +      @breadth_results_by_key[result_key]  | 
 | 56 | +    end  | 
 | 57 | + | 
 | 58 | +    def exit_with_inner_result?(inner_result, result_key, breadth_object)  | 
 | 59 | +      @breadth_results_by_key[result_key][breadth_object.breadth_index] = inner_result  | 
 | 60 | +      true  | 
 | 61 | +    end  | 
 | 62 | +  end  | 
 | 63 | + | 
 | 64 | +  class PassthroughLoader < GraphQL::Batch::Loader  | 
 | 65 | +    def perform(objects)  | 
 | 66 | +      objects.each { |obj| fulfill(obj, obj) }  | 
 | 67 | +    end  | 
 | 68 | +  end  | 
 | 69 | + | 
 | 70 | +  class SimpleHashBatchLoader < GraphQL::Batch::Loader  | 
 | 71 | +    def initialize(key)  | 
 | 72 | +      super()  | 
 | 73 | +      @key = key  | 
 | 74 | +    end  | 
 | 75 | + | 
 | 76 | +    def perform(objects)  | 
 | 77 | +      objects.each { |obj| fulfill(obj, obj.fetch(@key)) }  | 
 | 78 | +    end  | 
 | 79 | +  end  | 
 | 80 | + | 
 | 81 | +  class UpcaseExtension < GraphQL::Schema::FieldExtension  | 
 | 82 | +    def after_resolve(value:, **rest)  | 
 | 83 | +      value&.upcase  | 
 | 84 | +    end  | 
 | 85 | +  end  | 
 | 86 | + | 
 | 87 | +  class RangeInput < GraphQL::Schema::InputObject  | 
 | 88 | +    argument :min, Int  | 
 | 89 | +    argument :max, Int  | 
 | 90 | + | 
 | 91 | +    def prepare  | 
 | 92 | +      min..max  | 
 | 93 | +    end  | 
 | 94 | +  end  | 
 | 95 | + | 
 | 96 | +  class BaseField < GraphQL::Schema::Field  | 
 | 97 | +    def authorized?(obj, args, ctx)  | 
 | 98 | +      if !ctx[:field_auth].nil?  | 
 | 99 | +        ctx[:field_auth]  | 
 | 100 | +      elsif !ctx[:lazy_field_auth].nil?  | 
 | 101 | +        PassthroughLoader.load(ctx[:lazy_field_auth])  | 
 | 102 | +      elsif !ctx[:field_auth_with_error].nil?  | 
 | 103 | +        raise GraphQL::ExecutionError, "Not authorized" unless ctx[:field_auth_with_error]  | 
 | 104 | +      else  | 
 | 105 | +        true  | 
 | 106 | +      end  | 
 | 107 | +    end  | 
 | 108 | +  end  | 
 | 109 | + | 
 | 110 | +  class BaseObject < GraphQL::Schema::Object  | 
 | 111 | +    field_class BaseField  | 
 | 112 | +  end  | 
 | 113 | + | 
 | 114 | +  class Query < BaseObject  | 
 | 115 | +    field :foo, String  | 
 | 116 | + | 
 | 117 | +    def foo  | 
 | 118 | +      object[:foo]  | 
 | 119 | +    end  | 
 | 120 | + | 
 | 121 | +    field :lazy_foo, String  | 
 | 122 | + | 
 | 123 | +    def lazy_foo  | 
 | 124 | +      SimpleHashBatchLoader.for(:foo).load(object)  | 
 | 125 | +    end  | 
 | 126 | + | 
 | 127 | +    field :maybe_lazy_foo, String  | 
 | 128 | + | 
 | 129 | +    def maybe_lazy_foo  | 
 | 130 | +      if object[:foo] == "beep"  | 
 | 131 | +        SimpleHashBatchLoader.for(:foo).load(object)  | 
 | 132 | +      else  | 
 | 133 | +        object[:foo]  | 
 | 134 | +      end  | 
 | 135 | +    end  | 
 | 136 | + | 
 | 137 | +    field :nested_lazy_foo, String  | 
 | 138 | + | 
 | 139 | +    def nested_lazy_foo  | 
 | 140 | +      PassthroughLoader  | 
 | 141 | +        .load(object)  | 
 | 142 | +        .then { |obj| SimpleHashBatchLoader.for(:foo).load(obj) }  | 
 | 143 | +        .then { |str| str }  | 
 | 144 | +    end  | 
 | 145 | + | 
 | 146 | +    field :upcase_foo, String, extensions: [UpcaseExtension]  | 
 | 147 | + | 
 | 148 | +    def upcase_foo  | 
 | 149 | +      object[:foo]  | 
 | 150 | +    end  | 
 | 151 | + | 
 | 152 | +    field :lazy_upcase_foo, String, extensions: [UpcaseExtension]  | 
 | 153 | + | 
 | 154 | +    def lazy_upcase_foo  | 
 | 155 | +      SimpleHashBatchLoader.for(:foo).load(object)  | 
 | 156 | +    end  | 
 | 157 | + | 
 | 158 | +    field :go_boom, String  | 
 | 159 | + | 
 | 160 | +    def go_boom  | 
 | 161 | +      raise GraphQL::ExecutionError, "boom"  | 
 | 162 | +    end  | 
 | 163 | + | 
 | 164 | +    field :args, String do |f|  | 
 | 165 | +      f.argument :a, String  | 
 | 166 | +      f.argument :b, String  | 
 | 167 | +    end  | 
 | 168 | + | 
 | 169 | +    def args(a:, b:)  | 
 | 170 | +      "#{a}#{b}"  | 
 | 171 | +    end  | 
 | 172 | + | 
 | 173 | +    field :range, String do |f|  | 
 | 174 | +      f.argument :input, RangeInput  | 
 | 175 | +    end  | 
 | 176 | + | 
 | 177 | +    def range(input:)  | 
 | 178 | +      "#{input.min}-#{input.max}"  | 
 | 179 | +    end  | 
 | 180 | + | 
 | 181 | +    field :extras, String, extras: [:lookahead]  | 
 | 182 | + | 
 | 183 | +    def extras(lookahead:)  | 
 | 184 | +      lookahead.field.name  | 
 | 185 | +    end  | 
 | 186 | + | 
 | 187 | +    # uses default resolver...  | 
 | 188 | +    field :fizz, String  | 
 | 189 | +  end  | 
 | 190 | + | 
 | 191 | +  class TestSchema < GraphQL::Schema  | 
 | 192 | +    use(GraphQL::Batch)  | 
 | 193 | +    query Query  | 
 | 194 | +  end  | 
 | 195 | + | 
 | 196 | +  SCHEMA_FROM_DEF = GraphQL::Schema.from_definition(  | 
 | 197 | +    %|type Query { a: String }|,  | 
 | 198 | +    default_resolve: {  | 
 | 199 | +      "Query" => { "a" => ->(obj, _args, _ctx) { obj["a"] } },  | 
 | 200 | +    },  | 
 | 201 | +  )  | 
 | 202 | + | 
 | 203 | +  OBJECTS = [{ foo: "fizz" }, { foo: "buzz" }, { foo: "beep" }, { foo: "boom" }].freeze  | 
 | 204 | +  EXPECTED_RESULTS = ["fizz", "buzz", "beep", "boom"].freeze  | 
 | 205 | + | 
 | 206 | +  def test_maps_sync_results  | 
 | 207 | +    result = map_breadth_objects(OBJECTS, "{ foo }")  | 
 | 208 | +    assert_equal EXPECTED_RESULTS, result  | 
 | 209 | +  end  | 
 | 210 | + | 
 | 211 | +  def test_maps_lazy_results  | 
 | 212 | +    result = map_breadth_objects(OBJECTS, "{ lazyFoo }")  | 
 | 213 | +    assert_equal EXPECTED_RESULTS, result  | 
 | 214 | +  end  | 
 | 215 | + | 
 | 216 | +  def test_maps_sometimes_lazy_results  | 
 | 217 | +    result = map_breadth_objects(OBJECTS, "{ maybeLazyFoo }")  | 
 | 218 | +    assert_equal EXPECTED_RESULTS, result  | 
 | 219 | +  end  | 
 | 220 | + | 
 | 221 | +  def test_maps_nested_lazy_results  | 
 | 222 | +    result = map_breadth_objects(OBJECTS, "{ nestedLazyFoo }")  | 
 | 223 | +    assert_equal EXPECTED_RESULTS, result  | 
 | 224 | +  end  | 
 | 225 | + | 
 | 226 | +  def test_maps_field_extension_results  | 
 | 227 | +    result = map_breadth_objects(OBJECTS, "{ upcaseFoo }")  | 
 | 228 | +    assert_equal ["FIZZ", "BUZZ", "BEEP", "BOOM"], result  | 
 | 229 | +  end  | 
 | 230 | + | 
 | 231 | +  def test_maps_lazy_field_extension_results  | 
 | 232 | +    result = map_breadth_objects(OBJECTS, "{ lazyUpcaseFoo }")  | 
 | 233 | +    assert_equal ["FIZZ", "BUZZ", "BEEP", "BOOM"], result  | 
 | 234 | +  end  | 
 | 235 | + | 
 | 236 | +  def test_maps_fields_with_authorization  | 
 | 237 | +    context = { field_auth: false }  | 
 | 238 | +    result = map_breadth_objects(OBJECTS, "{ foo }", context: context)  | 
 | 239 | +    assert_equal [nil, nil, nil, nil], result  | 
 | 240 | +  end  | 
 | 241 | + | 
 | 242 | +  def test_maps_fields_with_lazy_authorization  | 
 | 243 | +    context = { lazy_field_auth: false }  | 
 | 244 | +    result = map_breadth_objects(OBJECTS, "{ foo }", context: context)  | 
 | 245 | +    assert result.all? { |r| r.is_a?(GraphQL::UnauthorizedFieldError) }  | 
 | 246 | +  end  | 
 | 247 | + | 
 | 248 | +  def test_maps_fields_with_authorization_errors  | 
 | 249 | +    context = { field_auth_with_error: false }  | 
 | 250 | +    result = map_breadth_objects(OBJECTS, "{ foo }", context: context)  | 
 | 251 | +    assert result.all? { |r| r.is_a?(GraphQL::ExecutionError) }  | 
 | 252 | +  end  | 
 | 253 | + | 
 | 254 | +  def test_maps_field_errors  | 
 | 255 | +    result = map_breadth_objects(OBJECTS, "{ goBoom }")  | 
 | 256 | +    assert result.all? { |r| r.is_a?(GraphQL::ExecutionError) }  | 
 | 257 | +    assert_equal ["boom", "boom", "boom", "boom"], result.map(&:message)  | 
 | 258 | +  end  | 
 | 259 | + | 
 | 260 | +  def test_maps_basic_arguments  | 
 | 261 | +    doc = %|{ args(a:"fizz", b:"buzz") }|  | 
 | 262 | +    result = map_breadth_objects([{}], doc)  | 
 | 263 | +    assert_equal ["fizzbuzz"], result  | 
 | 264 | +  end  | 
 | 265 | + | 
 | 266 | +  def test_maps_basic_arguments_with_variables  | 
 | 267 | +    doc = %|query($b: String) { args(a:"fizz", b: $b) }|  | 
 | 268 | +    result = map_breadth_objects([{}], doc, variables: { b: "buzz" })  | 
 | 269 | +    assert_equal ["fizzbuzz"], result  | 
 | 270 | +  end  | 
 | 271 | + | 
 | 272 | +  def test_maps_prepared_input_object  | 
 | 273 | +    doc = %|{ range(input: { min: 1, max: 2 }) }|  | 
 | 274 | +    result = map_breadth_objects([{}], doc)  | 
 | 275 | +    assert_equal ["1-2"], result  | 
 | 276 | +  end  | 
 | 277 | + | 
 | 278 | +  def test_maps_prepared_input_object_with_variables  | 
 | 279 | +    doc = %|query($b: Int) { range(input: { min: 1, max: $b }) }|  | 
 | 280 | +    result = map_breadth_objects([{}], doc, variables: { b: 2 })  | 
 | 281 | +    assert_equal ["1-2"], result  | 
 | 282 | +  end  | 
 | 283 | + | 
 | 284 | +  def test_maps_extras_arguments  | 
 | 285 | +    result = map_breadth_objects([{}], "{ extras }")  | 
 | 286 | +    assert_equal ["extras"], result  | 
 | 287 | +  end  | 
 | 288 | + | 
 | 289 | +  def test_uses_default_resolver_for_hash_keys  | 
 | 290 | +    result = map_breadth_objects([{ fizz: "buzz" }], "{ fizz }")  | 
 | 291 | +    assert_equal ["buzz"], result  | 
 | 292 | +  end  | 
 | 293 | + | 
 | 294 | +  def test_uses_default_resolver_for_method_calls  | 
 | 295 | +    entity = Struct.new(:fizz)  | 
 | 296 | +    result = map_breadth_objects([entity.new("buzz")], "{ fizz }")  | 
 | 297 | +    assert_equal ["buzz"], result  | 
 | 298 | +  end  | 
 | 299 | + | 
 | 300 | +  def test_maps_schemas_from_definition  | 
 | 301 | +    objects = [{ "a" => "1" }, { "a" => "2" }]  | 
 | 302 | +    result = map_breadth_objects(objects, "{ a }", schema: SCHEMA_FROM_DEF)  | 
 | 303 | +    assert_equal ["1", "2"], result  | 
 | 304 | +  end  | 
 | 305 | + | 
 | 306 | +  def test_maps_results_with_multiple_nodes  | 
 | 307 | +    result = map_breadth_objects(OBJECTS, "{ foo foo }")  | 
 | 308 | +    assert_equal EXPECTED_RESULTS, result  | 
 | 309 | +  end  | 
 | 310 | + | 
 | 311 | +  private  | 
 | 312 | + | 
 | 313 | +  def map_breadth_objects(objects, doc, schema: TestSchema, variables: {}, context: {})  | 
 | 314 | +    query = GraphQL::Query.new(  | 
 | 315 | +      schema,  | 
 | 316 | +      document: GraphQL.parse(doc),  | 
 | 317 | +      variables: variables,  | 
 | 318 | +      context: context,  | 
 | 319 | +    )  | 
 | 320 | + | 
 | 321 | +    node = query.document.definitions.first.selections.first  | 
 | 322 | +    runtime = SimpleBreadthRuntime.new(query: query)  | 
 | 323 | +    runtime.run { runtime.evaluate_breadth_selection(objects, schema.query, node) }  | 
 | 324 | +  end  | 
 | 325 | +end  | 
0 commit comments