File: resolver.rb

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (412 lines) | stat: -rw-r--r-- 16,115 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# frozen_string_literal: true
require "graphql/schema/resolver/has_payload_type"

module GraphQL
  class Schema
    # A class-based container for field configuration and resolution logic. It supports:
    #
    # - Arguments, via `.argument(...)` helper, which will be applied to the field.
    # - Return type, via `.type(..., null: ...)`, which will be applied to the field.
    # - Description, via `.description(...)`, which will be applied to the field
    # - Resolution, via `#resolve(**args)` method, which will be called to resolve the field.
    # - `#object` and `#context` accessors for use during `#resolve`.
    #
    # Resolvers can be attached with the `resolver:` option in a `field(...)` call.
    #
    # A resolver's configuration may be overridden with other keywords in the `field(...)` call.
    #
    # @see {GraphQL::Schema::Mutation} for a concrete subclass of `Resolver`.
    # @see {GraphQL::Function} `Resolver` is a replacement for `GraphQL::Function`
    class Resolver
      include Schema::Member::GraphQLTypeNames
      # Really we only need description from here, but:
      extend Schema::Member::BaseDSLMethods
      extend GraphQL::Schema::Member::HasArguments
      extend GraphQL::Schema::Member::HasValidators
      include Schema::Member::HasPath
      extend Schema::Member::HasPath

      # @param object [Object] The application object that this field is being resolved on
      # @param context [GraphQL::Query::Context]
      # @param field [GraphQL::Schema::Field]
      def initialize(object:, context:, field:)
        @object = object
        @context = context
        @field = field
        # Since this hash is constantly rebuilt, cache it for this call
        @arguments_by_keyword = {}
        self.class.arguments(context).each do |name, arg|
          @arguments_by_keyword[arg.keyword] = arg
        end
        @prepared_arguments = nil
      end

      # @return [Object] The application object this field is being resolved on
      attr_reader :object

      # @return [GraphQL::Query::Context]
      attr_reader :context

      # @return [GraphQL::Dataloader]
      def dataloader
        context.dataloader
      end

      # @return [GraphQL::Schema::Field]
      attr_reader :field

      def arguments
        @prepared_arguments || raise("Arguments have not been prepared yet, still waiting for #load_arguments to resolve. (Call `.arguments` later in the code.)")
      end

      # This method is _actually_ called by the runtime,
      # it does some preparation and then eventually calls
      # the user-defined `#resolve` method.
      # @api private
      def resolve_with_support(**args)
        # First call the ready? hook which may raise
        raw_ready_val = if args.any?
          ready?(**args)
        else
          ready?
        end
        context.query.after_lazy(raw_ready_val) do |ready_val|
          if ready_val.is_a?(Array)
            is_ready, ready_early_return = ready_val
            if is_ready != false
              raise "Unexpected result from #ready? (expected `true`, `false` or `[false, {...}]`): [#{is_ready.inspect}, #{ready_early_return.inspect}]"
            else
              ready_early_return
            end
          elsif ready_val
            # Then call each prepare hook, which may return a different value
            # for that argument, or may return a lazy object
            load_arguments_val = load_arguments(args)
            context.query.after_lazy(load_arguments_val) do |loaded_args|
              @prepared_arguments = loaded_args
              Schema::Validator.validate!(self.class.validators, object, context, loaded_args, as: @field)
              # Then call `authorized?`, which may raise or may return a lazy object
              raw_authorized_val = if loaded_args.any?
                authorized?(**loaded_args)
              else
                authorized?
              end
              context.query.after_lazy(raw_authorized_val) do |authorized_val|
                # If the `authorized?` returned two values, `false, early_return`,
                # then use the early return value instead of continuing
                if authorized_val.is_a?(Array)
                  authorized_result, early_return = authorized_val
                  if authorized_result == false
                    early_return
                  else
                    raise "Unexpected result from #authorized? (expected `true`, `false` or `[false, {...}]`): [#{authorized_result.inspect}, #{early_return.inspect}]"
                  end
                elsif authorized_val
                  # Finally, all the hooks have passed, so resolve it
                  if loaded_args.any?
                    public_send(self.class.resolve_method, **loaded_args)
                  else
                    public_send(self.class.resolve_method)
                  end
                else
                  raise GraphQL::UnauthorizedFieldError.new(context: context, object: object, type: field.owner, field: field)
                end
              end
            end
          end
        end
      end

      # Do the work. Everything happens here.
      # @return [Object] An object corresponding to the return type
      def resolve(**args)
        raise GraphQL::RequiredImplementationMissingError, "#{self.class.name}#resolve should execute the field's logic"
      end

      # Called before arguments are prepared.
      # Implement this hook to make checks before doing any work.
      #
      # If it returns a lazy object (like a promise), it will be synced by GraphQL
      # (but the resulting value won't be used).
      #
      # @param args [Hash] The input arguments, if there are any
      # @raise [GraphQL::ExecutionError] To add an error to the response
      # @raise [GraphQL::UnauthorizedError] To signal an authorization failure
      # @return [Boolean, early_return_data] If `false`, execution will stop (and `early_return_data` will be returned instead, if present.)
      def ready?(**args)
        true
      end

      # Called after arguments are loaded, but before resolving.
      #
      # Override it to check everything before calling the mutation.
      # @param inputs [Hash] The input arguments
      # @raise [GraphQL::ExecutionError] To add an error to the response
      # @raise [GraphQL::UnauthorizedError] To signal an authorization failure
      # @return [Boolean, early_return_data] If `false`, execution will stop (and `early_return_data` will be returned instead, if present.)
      def authorized?(**inputs)
        arg_owner = @field # || self.class
        args = arg_owner.arguments(context)
        authorize_arguments(args, inputs)
      end

      # Called when an object loaded by `loads:` fails the `.authorized?` check for its resolved GraphQL object type.
      #
      # By default, the error is re-raised and passed along to {{Schema.unauthorized_object}}.
      #
      # Any value returned here will be used _instead of_ of the loaded object.
      # @param err [GraphQL::UnauthorizedError]
      def unauthorized_object(err)
        raise err
      end

      private

      def authorize_arguments(args, inputs)
        args.each_value do |argument|
          arg_keyword = argument.keyword
          if inputs.key?(arg_keyword) && !(arg_value = inputs[arg_keyword]).nil? && (arg_value != argument.default_value)
            auth_result = argument.authorized?(self, arg_value, context)
            if auth_result.is_a?(Array)
              # only return this second value if the application returned a second value
              arg_auth, err = auth_result
              if !arg_auth
                return arg_auth, err
              end
            elsif auth_result == false
              return auth_result
            end
          else
            true
          end
        end
      end

      def load_arguments(args)
        prepared_args = {}
        prepare_lazies = []

        args.each do |key, value|
          arg_defn = @arguments_by_keyword[key]
          if arg_defn
            prepped_value = prepared_args[key] = arg_defn.load_and_authorize_value(self, value, context)
            if context.schema.lazy?(prepped_value)
              prepare_lazies << context.query.after_lazy(prepped_value) do |finished_prepped_value|
                prepared_args[key] = finished_prepped_value
              end
            end
          else
            # these are `extras:`
            prepared_args[key] = value
          end
        end

        # Avoid returning a lazy if none are needed
        if prepare_lazies.any?
          GraphQL::Execution::Lazy.all(prepare_lazies).then { prepared_args }
        else
          prepared_args
        end
      end

      def get_argument(name, context = GraphQL::Query::NullContext.instance)
        self.class.get_argument(name, context)
      end

      class << self
        def field_arguments(context = GraphQL::Query::NullContext.instance)
          arguments(context)
        end

        def any_field_arguments?
          any_arguments?
        end

        def get_field_argument(name, context = GraphQL::Query::NullContext.instance)
          get_argument(name, context)
        end

        def all_field_argument_definitions
          all_argument_definitions
        end

        # Default `:resolve` set below.
        # @return [Symbol] The method to call on instances of this object to resolve the field
        def resolve_method(new_method = nil)
          if new_method
            @resolve_method = new_method
          end
          @resolve_method || (superclass.respond_to?(:resolve_method) ? superclass.resolve_method : :resolve)
        end

        # Additional info injected into {#resolve}
        # @see {GraphQL::Schema::Field#extras}
        def extras(new_extras = nil)
          if new_extras
            @own_extras = new_extras
          end
          own_extras = @own_extras || []
          own_extras + (superclass.respond_to?(:extras) ? superclass.extras : [])
        end

        # If `true` (default), then the return type for this resolver will be nullable.
        # If `false`, then the return type is non-null.
        #
        # @see #type which sets the return type of this field and accepts a `null:` option
        # @param allow_null [Boolean] Whether or not the response can be null
        def null(allow_null = nil)
          if !allow_null.nil?
            @null = allow_null
          end

          @null.nil? ? (superclass.respond_to?(:null) ? superclass.null : true) : @null
        end

        def resolver_method(new_method_name = nil)
          if new_method_name
            @resolver_method = new_method_name
          else
            @resolver_method || :resolve_with_support
          end
        end

        # Call this method to get the return type of the field,
        # or use it as a configuration method to assign a return type
        # instead of generating one.
        # TODO unify with {#null}
        # @param new_type [Class, Array<Class>, nil] If a type definition class is provided, it will be used as the return type of the field
        # @param null [true, false] Whether or not the field may return `nil`
        # @return [Class] The type which this field returns.
        def type(new_type = nil, null: nil)
          if new_type
            if null.nil?
              raise ArgumentError, "required argument `null:` is missing"
            end
            @type_expr = new_type
            @null = null
          else
            if type_expr
              GraphQL::Schema::Member::BuildType.parse_type(type_expr, null: self.null)
            elsif superclass.respond_to?(:type)
              superclass.type
            else
              nil
            end
          end
        end

        # Specifies the complexity of the field. Defaults to `1`
        # @return [Integer, Proc]
        def complexity(new_complexity = nil)
          if new_complexity
            @complexity = new_complexity
          end
          @complexity || (superclass.respond_to?(:complexity) ? superclass.complexity : 1)
        end

        def broadcastable(new_broadcastable)
          @broadcastable = new_broadcastable
        end

        # @return [Boolean, nil]
        def broadcastable?
          if defined?(@broadcastable)
            @broadcastable
          else
            (superclass.respond_to?(:broadcastable?) ? superclass.broadcastable? : nil)
          end
        end

        # Get or set the `max_page_size:` which will be configured for fields using this resolver
        # (`nil` means "unlimited max page size".)
        # @param max_page_size [Integer, nil] Set a new value
        # @return [Integer, nil] The `max_page_size` assigned to fields that use this resolver
        def max_page_size(new_max_page_size = NOT_CONFIGURED)
          if new_max_page_size != NOT_CONFIGURED
            @max_page_size = new_max_page_size
          elsif defined?(@max_page_size)
            @max_page_size
          elsif superclass.respond_to?(:max_page_size)
            superclass.max_page_size
          else
            nil
          end
        end

        # @return [Boolean] `true` if this resolver or a superclass has an assigned `max_page_size`
        def has_max_page_size?
          (!!defined?(@max_page_size)) || (superclass.respond_to?(:has_max_page_size?) && superclass.has_max_page_size?)
        end

        # Get or set the `default_page_size:` which will be configured for fields using this resolver
        # (`nil` means "unlimited default page size".)
        # @param default_page_size [Integer, nil] Set a new value
        # @return [Integer, nil] The `default_page_size` assigned to fields that use this resolver
        def default_page_size(new_default_page_size = NOT_CONFIGURED)
          if new_default_page_size != NOT_CONFIGURED
            @default_page_size = new_default_page_size
          elsif defined?(@default_page_size)
            @default_page_size
          elsif superclass.respond_to?(:default_page_size)
            superclass.default_page_size
          else
            nil
          end
        end

        # @return [Boolean] `true` if this resolver or a superclass has an assigned `default_page_size`
        def has_default_page_size?
          (!!defined?(@default_page_size)) || (superclass.respond_to?(:has_default_page_size?) && superclass.has_default_page_size?)
        end

        # A non-normalized type configuration, without `null` applied
        def type_expr
          @type_expr || (superclass.respond_to?(:type_expr) ? superclass.type_expr : nil)
        end

        # Add an argument to this field's signature, but
        # also add some preparation hook methods which will be used for this argument
        # @see {GraphQL::Schema::Argument#initialize} for the signature
        def argument(*args, **kwargs, &block)
          # Use `from_resolver: true` to short-circuit the InputObject's own `loads:` implementation
          # so that we can support `#load_{x}` methods below.
          super(*args, from_resolver: true, **kwargs)
        end

        # Registers new extension
        # @param extension [Class] Extension class
        # @param options [Hash] Optional extension options
        def extension(extension, **options)
          @own_extensions ||= []
          @own_extensions << {extension => options}
        end

        # @api private
        def extensions
          own_exts = @own_extensions
          # Jump through some hoops to avoid creating arrays when we don't actually need them
          if superclass.respond_to?(:extensions)
            s_exts = superclass.extensions
            if own_exts
              if s_exts.any?
                own_exts + s_exts
              else
                own_exts
              end
            else
              s_exts
            end
          else
            own_exts || EMPTY_ARRAY
          end
        end

        private

        def own_extensions
          @own_extensions
        end
      end
    end
  end
end