Class: Rails::GraphQL::TypeMap

Inherits:
Object
  • Object
show all
Extended by:
ActiveSupport::Autoload
Defined in:
lib/rails/graphql/type_map.rb

Overview

GraphQL Type Map

Inspired by ActiveRecord::Type::TypeMap, this class stores all the things defined, their unique name, their basic settings, and correctly index them so they are easy to find whenever necessary.

Items are stored as procs because aliases should fetch whatever the base object is, even if they change in the another point.

The cache stores in the following structure: Namespace -> BaseClass -> ItemKey -> Item

Constant Summary collapse

FILTER_REGISTER_TRACE =
/((inherited|initialize)'$|schema\.rb:\d+)/.freeze
NESTED_MODULE =
Type::Creator::NESTED_MODULE

Instance Method Summary collapse

Instance Method Details

#add_dependencies(*list, to:) ⇒ Object

Add a list of dependencies to the type map, so it can lazy load them



74
75
76
# File 'lib/rails/graphql/type_map.rb', line 74

def add_dependencies(*list, to:)
  @dependencies[to].concat(list.flatten.compact)
end

#after_register(name_or_key, base_class: :Type, **xargs, &block) ⇒ Object

Add a callback that will trigger when a type is registered under the given set of settings of this method



289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/rails/graphql/type_map.rb', line 289

def after_register(name_or_key, base_class: :Type, **xargs, &block)
  item = fetch(name_or_key, prevent_register: true, base_class: base_class, **xargs)
  return block.call(item) unless item.nil?

  namespaces = sanitize_namespaces(**xargs)
  callback = ->(n, b, result) do
    return unless b === base_class && (n === :base || namespaces.include?(n))
    block.call(result)
    true
  end

  callbacks[name_or_key].unshift(callback)
end

#associate(namespace, mod) ⇒ Object

Associate the given module to a given namespace. If registered objects have no namespaces, but its module_parents have been associated, then use the value TODO: Maybe turn this into a 1-to-Many association



89
90
91
# File 'lib/rails/graphql/type_map.rb', line 89

def associate(namespace, mod)
  @module_namespaces[mod] = namespace
end

#associated_namespace_of(object) ⇒ Object

Grab all the module_parents from the object and try to return the first matching result



95
96
97
98
99
100
101
102
103
# File 'lib/rails/graphql/type_map.rb', line 95

def associated_namespace_of(object)
  return if @module_namespaces.empty?
  object.module_parents.find do |mod|
    ns = @module_namespaces[mod]
    break ns unless ns.nil?
  end
rescue ::NameError
  # If any module parent can't be found, there is no much we can do
end

#each_from(namespaces, base_class: nil, exclusive: false, base_classes: nil, &block) ⇒ Object

Iterate over the types of the given base_class that are defined on the given namespaces.



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/rails/graphql/type_map.rb', line 254

def each_from(namespaces, base_class: nil, exclusive: false, base_classes: nil, &block)
  namespaces = sanitize_namespaces(namespaces: namespaces, exclusive: exclusive)
  load_dependencies!(_ns: namespaces)
  register_pending!

  iterated = Set.new
  base_classes = GraphQL.enumerate(base_class || base_classes || :Type)
  enumerator = Enumerator::Lazy.new(namespaces) do |yielder, namespace|
    next unless @index.key?(namespace)

    base_classes.each do |a_base_class|
      @index[namespace][a_base_class]&.each do |key, value|
        value = value.is_a?(Proc) ? value.call : value
        next if value.blank? || iterated.include?(value.gql_name)

        iterated << value.gql_name
        yielder << value
      end
    end
  end

  block.present? ? enumerator.each(&block) : enumerator
end

#exist?(name_or_key, **xargs) ⇒ Boolean

Checks if a given key or name is already defined under the same base class and namespace. If exclusive is set to false, then it won’t check the :base namespace when not found on the given namespace.

Returns:

  • (Boolean)


240
241
242
# File 'lib/rails/graphql/type_map.rb', line 240

def exist?(name_or_key, **xargs)
  !fetch(name_or_key, **xargs, prevent_register: true).nil?
end

#fetch(key_or_name, prevent_register: nil, **xargs) ⇒ Object

Find the given key or name inside the base class either on the given namespace or in the base :base namespace



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/rails/graphql/type_map.rb', line 214

def fetch(key_or_name, prevent_register: nil, **xargs)
  prevent_register = true if @pending.blank?
  if prevent_register != true
    items = prevent_register == true ? nil : ::Array.wrap(prevent_register)
    skip_register << items.to_set
    register_pending!
  end

  possibilities = ::Array.wrap(key_or_name)
  possibilities << xargs[:fallback] if xargs.key?(:fallback)

  base_class = xargs.fetch(:base_class, :Type)
  sanitize_namespaces(**xargs).find do |namespace|
    possibilities.find do |item|
      next if (result = dig(namespace, base_class, item)).nil?
      next if (result.is_a?(Proc) && (result = result.call).nil?)
      return result
    end
  end
ensure
  skip_register.pop if prevent_register != true
end

#fetch!(key_or_name, base_class: :Type, fallback: nil, **xargs) ⇒ Object

Same as fetch but it will raise an exception or retry depending if the base type was already loaded or not

Raises:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/rails/graphql/type_map.rb', line 191

def fetch!(key_or_name, base_class: :Type, fallback: nil, **xargs)
  xargs[:base_class] = base_class

  result = fetch(key_or_name, **xargs)
  return result unless result.nil?

  new_loads = load_dependencies!(**xargs)
  result = fetch(key_or_name, **xargs) if new_loads

  if result.nil? && fallback
    result = fetch(fallback, **xargs)
    report_fallback(key_or_name, result, base_class)
  end

  raise NotFoundError, (+<<~MSG).squish if result.nil?
    Unable to find #{key_or_name.inspect} #{base_class} object.
  MSG

  result
end

#inspectObject



303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/rails/graphql/type_map.rb', line 303

def inspect
  dependencies = @dependencies.each_pair.map do |key, list|
    +("#{key}: #{list.size}")
  end.join(', ')

  (+<<~INFO).squish << '>'
    #<Rails::GraphQL::TypeMap [index]
    @namespaces=#{@index.size}
    @base_classes=#{base_classes.size}
    @objects=#{@objects}
    @pending=#{@pending.size}
    @dependencies={#{dependencies}}
  INFO
end

#object_exist?(object, **xargs) ⇒ Boolean

Find if a given object is already defined. If exclusive is set to false, then it won’t check the :base namespace

Returns:

  • (Boolean)


246
247
248
249
250
# File 'lib/rails/graphql/type_map.rb', line 246

def object_exist?(object, **xargs)
  xargs[:base_class] = find_base_class(object)
  xargs[:namespaces] ||= object.namespaces
  exist?(object, **xargs)
end

#objects(base_classes: nil, namespaces: nil) ⇒ Object

Get the list of all registered objects TODO: Maybe keep it as a lazy enumerator



280
281
282
283
284
285
# File 'lib/rails/graphql/type_map.rb', line 280

def objects(base_classes: nil, namespaces: nil)
  base_classes ||= self.class.base_classes
  each_from(namespaces || @index.keys, base_classes: base_classes).select do |obj|
    obj.is_a?(Helpers::Registerable)
  end.force
end

#postpone_registration(object) ⇒ Object

Mark the given object to be registered later, when a fetch is triggered TODO: Improve this with a Backtrace Cleaner



80
81
82
83
# File 'lib/rails/graphql/type_map.rb', line 80

def postpone_registration(object)
  source = caller(3).find { |item| item !~ FILTER_REGISTER_TRACE }
  @pending << [object, source]
end

#register(object) ⇒ Object

Register a given object, which must be a class where the namespaces and the base class can be inferred



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
# File 'lib/rails/graphql/type_map.rb', line 107

def register(object)
  namespaces = sanitize_namespaces(namespaces: object.namespaces, exclusive: true)
  namespaces << :base if namespaces.empty?

  base_class = find_base_class(object)
  ensure_base_class!(base_class)

  # Cache the name, the key, and the alias proc
  object_base = namespaces.first
  object_name = object.gql_name
  object_key = object.to_sym
  alias_proc = -> do
    value = dig(object_base, base_class, object_key)
    value.is_a?(Proc) ? value.call : value
  end

  # TODO Warn when the base key is being assigned to a different object
  # Register the main type object for both key and name
  add(object_base, base_class, object_key, object)
  add(object_base, base_class, object_name, alias_proc)

  # Register all the aliases plus the object name
  aliases = object.try(:aliases)
  aliases&.each do |alias_name|
    add(object_base, base_class, alias_name, alias_proc)
  end

  # For each remaining namespace, register a key and a name alias
  if namespaces.size > 1
    keys_and_names = [object_key, object_name, *aliases]
    namespaces.drop(1).product(keys_and_names) do |(namespace, key_or_name)|
      add(namespace, base_class, key_or_name, alias_proc)
    end
  end

  # Return the object for chain purposes
  @objects += 1
  object
end

#register_alias(name_or_key, key = nil, **xargs, &block) ⇒ Object

Register an item alias. Either provide a block that trigger the fetch method to return that item, or a key from the same namespace and base class

Raises:



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/rails/graphql/type_map.rb', line 171

def register_alias(name_or_key, key = nil, **xargs, &block)
  raise ArgumentError, (+<<~MSG).squish unless key.nil? ^ block.nil?
    Provide either a key or a block in order to register an alias.
  MSG

  base_class = xargs.delete(:base_class) || :Type
  ensure_base_class!(base_class)

  namespaces = sanitize_namespaces(**xargs, exclusive: true)
  namespaces << :base if namespaces.empty?

  block ||= -> do
    fetch(key, base_class: base_class, namespaces: namespaces, exclusive: true)
  end

  namespaces.each { |ns| add(ns, base_class, name_or_key, block) }
end

#reset!Object Also known as: initialize

Reset the state of the type mapper



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
# File 'lib/rails/graphql/type_map.rb', line 35

def reset!
  @objects = 0 # Number of types and directives defined
  @version = nil # Make sure to not keep the same version
  @skip_register = nil

  @pending = Concurrent::Array.new
  @reported_fallbacks = Concurrent::Set.new

  # Initialize the callbacks
  @callbacks = Concurrent::Map.new do |hc, key|
    hc.fetch_or_store(key, Concurrent::Array.new)
  end

  # Initialize the dependencies
  @dependencies = Concurrent::Map.new do |hd, key|
    hd.fetch_or_store(key, Concurrent::Array.new)
  end

  # A registered list of modules and to which namespaces they are
  # associated with
  @module_namespaces = Concurrent::Map.new

  # Initialize the index structure
  @index = Concurrent::Map.new do |h1, key1|                # Namespaces
    base_class = Concurrent::Map.new do |h2, key2|          # Base classes
      ensure_base_class!(key2)
      h2.fetch_or_store(key2, Concurrent::Map.new)          # Items
    end

    h1.fetch_or_store(key1, base_class)
  end

  # Provide the first dependencies
  seed_dependencies!
end

#unregister(*objects) ⇒ Object

Unregister all the provided objects by simply assigning nil to their final value on the index



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/rails/graphql/type_map.rb', line 149

def unregister(*objects)
  objects.each do |object|
    namespaces = sanitize_namespaces(namespaces: object.namespaces, exclusive: true)
    namespaces << :base if namespaces.empty?
    base_class = find_base_class(object)

    if object.kind != :source
      @index[namespaces.first][base_class][object.to_sym] = nil
      @objects -= 1
    end

    return unless object.const_defined?(NESTED_MODULE, false)

    nested_mod = object.const_get(NESTED_MODULE, false)
    nested_mod.constants.each { |name| nested_mod.const_get(name, false).unregister! }
    object.send(:remove_const, NESTED_MODULE)
  end
end

#versionObject

Get the current version of the Type Map. On each reset, the version is changed and can be used to invalidate cache and similar things



30
31
32
# File 'lib/rails/graphql/type_map.rb', line 30

def version
  @version ||= GraphQL.config.version&.first(8) || SecureRandom.hex(8)
end