File: field_extension.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 (153 lines) | stat: -rw-r--r-- 6,054 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
# frozen_string_literal: true
module GraphQL
  class Schema
    # Extend this class to make field-level customizations to resolve behavior.
    #
    # When a extension is added to a field with `extension(MyExtension)`, a `MyExtension` instance
    # is created, and its hooks are applied whenever that field is called.
    #
    # The instance is frozen so that instance variables aren't modified during query execution,
    # which could cause all kinds of issues due to race conditions.
    class FieldExtension
      # @return [GraphQL::Schema::Field]
      attr_reader :field

      # @return [Object]
      attr_reader :options

      # @return [Array<Symbol>, nil] `default_argument`s added, if any were added (otherwise, `nil`)
      attr_reader :added_default_arguments

      # Called when the extension is mounted with `extension(name, options)`.
      # The instance will be frozen to avoid improper use of state during execution.
      # @param field [GraphQL::Schema::Field] The field where this extension was mounted
      # @param options [Object] The second argument to `extension`, or `{}` if nothing was passed.
      def initialize(field:, options:)
        @field = field
        @options = options || {}
        @added_default_arguments = nil
        apply
      end

      class << self
        # @return [Array(Array, Hash), nil] A list of default argument configs, or `nil` if there aren't any
        def default_argument_configurations
          args = superclass.respond_to?(:default_argument_configurations) ? superclass.default_argument_configurations : nil
          if @own_default_argument_configurations
            if args
              args.concat(@own_default_argument_configurations)
            else
              args = @own_default_argument_configurations.dup
            end
          end
          args
        end

        # @see Argument#initialize
        # @see HasArguments#argument
        def default_argument(*argument_args, **argument_kwargs)
          configs = @own_default_argument_configurations ||= []
          configs << [argument_args, argument_kwargs]
        end

        # If configured, these `extras` will be added to the field if they aren't already present,
        # but removed by from `arguments` before the field's `resolve` is called.
        # (The extras _will_ be present for other extensions, though.)
        #
        # @param new_extras [Array<Symbol>] If provided, assign extras used by this extension
        # @return [Array<Symbol>] any extras assigned to this extension
        def extras(new_extras = nil)
          if new_extras
            @own_extras = new_extras
          end

          inherited_extras = self.superclass.respond_to?(:extras) ? superclass.extras : nil
          if @own_extras
            if inherited_extras
              inherited_extras + @own_extras
            else
              @own_extras
            end
          elsif inherited_extras
            inherited_extras
          else
            GraphQL::EmptyObjects::EMPTY_ARRAY
          end
        end
      end

      # Called when this extension is attached to a field.
      # The field definition may be extended during this method.
      # @return [void]
      def apply
      end

      # Called after the field's definition block has been executed.
      # (Any arguments from the block are present on `field`)
      # @return [void]
      def after_define
      end

      # @api private
      def after_define_apply
        after_define
        if (configs = self.class.default_argument_configurations)
          existing_keywords = field.all_argument_definitions.map(&:keyword)
          existing_keywords.uniq!
          @added_default_arguments = []
          configs.each do |config|
            argument_args, argument_kwargs = config
            arg_name = argument_args[0]
            if !existing_keywords.include?(arg_name)
              @added_default_arguments << arg_name
              field.argument(*argument_args, **argument_kwargs)
            end
          end
        end
        if (extras = self.class.extras).any?
          @added_extras = extras - field.extras
          field.extras(@added_extras)
        else
          @added_extras = nil
        end
        freeze
      end

      # @api private
      attr_reader :added_extras

      # Called before resolving {#field}. It should either:
      #
      # - `yield` values to continue execution; OR
      # - return something else to shortcut field execution.
      #
      # Whatever this method returns will be used for execution.
      #
      # @param object [Object] The object the field is being resolved on
      # @param arguments [Hash] Ruby keyword arguments for resolving this field
      # @param context [Query::Context] the context for this query
      # @yieldparam object [Object] The object to continue resolving the field on
      # @yieldparam arguments [Hash] The keyword arguments to continue resolving with
      # @yieldparam memo [Object] Any extension-specific value which will be passed to {#after_resolve} later
      # @return [Object] The return value for this field.
      def resolve(object:, arguments:, context:)
        yield(object, arguments, nil)
      end

      # Called after {#field} was resolved, and after any lazy values (like `Promise`s) were synced,
      # but before the value was added to the GraphQL response.
      #
      # Whatever this hook returns will be used as the return value.
      #
      # @param object [Object] The object the field is being resolved on
      # @param arguments [Hash] Ruby keyword arguments for resolving this field
      # @param context [Query::Context] the context for this query
      # @param value [Object] Whatever the field previously returned
      # @param memo [Object] The third value yielded by {#resolve}, or `nil` if there wasn't one
      # @return [Object] The return value for this field.
      def after_resolve(object:, arguments:, context:, value:, memo:)
        value
      end
    end
  end
end