Module: GraphQL::Models::DefinitionHelpers

Defined in:
lib/graphql/models/definition_helpers.rb,
lib/graphql/models/definition_helpers/attributes.rb,
lib/graphql/models/definition_helpers/associations.rb

Class Method Summary collapse

Class Method Details

.attempt_cache_load(model, association, context) ⇒ Object

Attempts to retrieve the model from the query’s cache. If it’s found, the association is marked as loaded and the model is added. This only works for belongs_to and has_one associations.



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
# File 'lib/graphql/models/definition_helpers.rb', line 66

def self.attempt_cache_load(model, association, context)
  return false unless context

  reflection = association.reflection
  return false unless %i[has_one belongs_to].include?(reflection.macro)

  if reflection.macro == :belongs_to
    target_id = model.send(reflection.foreign_key)

    # If there isn't an associated model, mark the association loaded and return
    if target_id.nil?
      mark_association_loaded(association, nil)
      return true
    end

    # If the associated model isn't cached, return false
    target = context.cached_models.detect { |m| m.is_a?(association.klass) && m.id == target_id }
    return false unless target

    # Found it!
    mark_association_loaded(association, target)
    return true
  else
    target = context.cached_models.detect do |m|
      m.is_a?(association.klass) && m.send(reflection.foreign_key) == model.id && (!reflection.options.include?(:as) || m.send(reflection.type) == model.class.name)
    end

    return false unless target

    mark_association_loaded(association, target)
    return true
  end
end

.cache_model(context, model) ⇒ Object



100
101
102
103
# File 'lib/graphql/models/definition_helpers.rb', line 100

def self.cache_model(context, model)
  return unless context
  context.cached_models.merge(Array.wrap(model))
end

.define_attribute(graph_type, base_model_class, model_class, path, attribute, object_to_model, options, detect_nulls, &block) ⇒ Object



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
# File 'lib/graphql/models/definition_helpers/attributes.rb', line 24

def self.define_attribute(graph_type, base_model_class, model_class, path, attribute, object_to_model, options, detect_nulls, &block)
  attribute_graphql_type = Reflection.attribute_graphql_type(model_class, attribute).output
  attribute_graphql_type = resolve_nullability(attribute_graphql_type, model_class, attribute, detect_nulls, options)

  field_name = options[:name]

  DefinitionHelpers.(graph_type, field_name, {
    macro: :attr,
    macro_type: :attribute,
    path: path,
    attribute: attribute,
    base_model_class: base_model_class,
    model_class: model_class,
    object_to_base_model: object_to_model,
  })

  graph_type.fields[field_name.to_s] = GraphQL::Field.define do
    name field_name.to_s
    type attribute_graphql_type
    description options[:description] if options.include?(:description)
    deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)

    resolve -> (model, _args, _context) do
      model&.public_send(attribute)
    end

    instance_exec(&block) if block
  end
end

.define_has_many_array(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls) ⇒ Object

Raises:

  • (ArgumentError)


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
# File 'lib/graphql/models/definition_helpers/associations.rb', line 132

def self.define_has_many_array(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls)
  reflection = model_type.reflect_on_association(association)

  raise ArgumentError, "Association #{association} wasn't found on model #{model_type.name}" unless reflection
  raise ArgumentError, "Cannot include #{reflection.macro} association #{association} on model #{model_type.name} with has_many_array" unless [:has_many].include?(reflection.macro)

  association_type = options[:type] || GraphQL::Models.get_graphql_type!(reflection.klass)

  if !association_type.is_a?(GraphQL::ListType)
    association_type = association_type.to_non_null_type.to_list_type
  end

  id_field_type = GraphQL::ID_TYPE.to_non_null_type.to_list_type

  # The has_many associations are a little special. Instead of checking for a presence validator, we instead assume
  # that the outer type should be non-null, unless detect_nulls is false. In other words, we prefer an empty
  # array for the association, rather than null.
  if (options[:nullable].nil? && detect_nulls) || options[:nullable] == false
    association_type = association_type.to_non_null_type
    id_field_type = id_field_type.to_non_null_type
  end

  camel_name = options[:name]

  DefinitionHelpers.(graph_type, camel_name, {
    macro: :has_many_array,
    macro_type: :association,
    path: path,
    association: association,
    base_model_type: base_model_type,
    model_type: model_type,
    object_to_base_model: object_to_model,
  })

  graph_type.fields[camel_name.to_s] = GraphQL::Field.define do
    name camel_name.to_s
    type association_type
    description options[:description] if options.include?(:description)
    deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)

    resolve -> (model, _args, context) do
      return nil unless model
      DefinitionHelpers.load_and_traverse(model, [association], context).then do |result|
        Array.wrap(result)
      end
    end
  end

  # Define the field for the associated model's ID
  id_field_name = :"#{camel_name.to_s.singularize}Ids"

  DefinitionHelpers.(graph_type, id_field_name, {
    macro: :has_one,
    macro_type: :association,
    path: path,
    association: association,
    base_model_type: base_model_type,
    model_type: model_type,
    object_to_base_model: object_to_model,
  })

  graph_type.fields[id_field_name.to_s] = GraphQL::Field.define do
    name id_field_name.to_s
    type id_field_type
    deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)

    resolve -> (model, _args, context) do
      return nil unless model
      DefinitionHelpers.load_and_traverse(model, [association], context).then do |result|
        Array.wrap(result).map(&:gid)
      end
    end
  end
end

.define_has_many_connection(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls) ⇒ Object

Raises:

  • (ArgumentError)


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
# File 'lib/graphql/models/definition_helpers/associations.rb', line 207

def self.define_has_many_connection(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls)
  reflection = model_type.reflect_on_association(association)

  raise ArgumentError, "Association #{association} wasn't found on model #{model_type.name}" unless reflection
  raise ArgumentError, "Cannot include #{reflection.macro} association #{association} on model #{model_type.name} with has_many_connection" unless [:has_many].include?(reflection.macro)

  connection_type = GraphQL::Models.get_graphql_type!(reflection.klass).connection_type

  if (options[:nullable].nil? && detect_nulls) || options[:nullable] == false
    connection_type = connection_type.to_non_null_type
  end

  camel_name = options[:name].to_s

  DefinitionHelpers.(graph_type, camel_name, {
    macro: :has_many_connection,
    macro_type: :association,
    path: path,
    association: association,
    base_model_type: base_model_type,
    model_type: model_type,
    object_to_base_model: object_to_model,
  })

  connection_field = graph_type.fields[camel_name] = GraphQL::Field.define do
    name(camel_name)
    type(connection_type)
    description options[:description] if options.include?(:description)
    deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)

    resolve -> (model, _args, _context) do
      return nil unless model
      model.public_send(association)
    end
  end

  connection_field.connection = true
  connection_field
end

.define_has_one(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls) ⇒ Object

Adds a field to the graph type which is resolved by accessing a has_one association on the model. Traverses across has_one associations specified in the path. The resolver returns a promise.

Raises:

  • (ArgumentError)


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
# File 'lib/graphql/models/definition_helpers/associations.rb', line 56

def self.define_has_one(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls)
  reflection = model_type.reflect_on_association(association)

  raise ArgumentError, "Association #{association} wasn't found on model #{model_type.name}" unless reflection
  raise ArgumentError, "Cannot include #{reflection.macro} association #{association} on model #{model_type.name} with has_one" unless %i[has_one belongs_to].include?(reflection.macro)

  # Define the field for the association itself

  camel_name = options[:name]
  association_graphql_type = resolve_has_one_type(reflection)
  association_graphql_type = resolve_nullability(association_graphql_type, model_type, association, detect_nulls, options)

  DefinitionHelpers.(graph_type, camel_name, {
    macro: :has_one,
    macro_type: :association,
    path: path,
    association: association,
    base_model_type: base_model_type,
    model_type: model_type,
    object_to_base_model: object_to_model,
  })

  graph_type.fields[camel_name.to_s] = GraphQL::Field.define do
    name camel_name.to_s
    type association_graphql_type
    description options[:description] if options.include?(:description)
    deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)

    resolve -> (model, _args, context) do
      return nil unless model
      DefinitionHelpers.load_and_traverse(model, [association], context)
    end
  end

  # Define the field for the associated model's ID
  id_field_name = :"#{camel_name}Id"
  id_field_type = resolve_nullability(GraphQL::ID_TYPE, model_type, association, detect_nulls, options)

  DefinitionHelpers.(graph_type, id_field_name, {
    macro: :has_one,
    macro_type: :association,
    path: path,
    association: association,
    base_model_type: base_model_type,
    model_type: model_type,
    object_to_base_model: object_to_model,
  })

  can_use_optimized = reflection.macro == :belongs_to

  if !reflection.polymorphic? && reflection.klass.column_names.include?('type')
    can_use_optimized = false
  end

  graph_type.fields[id_field_name.to_s] = GraphQL::Field.define do
    name id_field_name.to_s
    type id_field_type
    deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)

    resolve -> (model, _args, context) do
      return nil unless model

      if can_use_optimized
        id = model.public_send(reflection.foreign_key)
        return nil if id.nil?

        type = model.association(association).klass.name
        GraphQL::Models.id_for_model.call(type, id)
      else
        # We have to actually load the model and then get it's ID
        DefinitionHelpers.load_and_traverse(model, [association], context).then(&:gid)
      end
    end
  end
end

.define_proxy(graph_type, base_model_type, model_type, path, association, object_to_model, detect_nulls, &block) ⇒ Object

Raises:

  • (ArgumentError)


6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/graphql/models/definition_helpers/associations.rb', line 6

def self.define_proxy(graph_type, base_model_type, model_type, path, association, object_to_model, detect_nulls, &block)
  reflection = model_type.reflect_on_association(association)
  raise ArgumentError, "Association #{association} wasn't found on model #{model_type.name}" unless reflection
  raise ArgumentError, "Cannot proxy to polymorphic association #{association} on model #{model_type.name}" if reflection.polymorphic?
  raise ArgumentError, "Cannot proxy to #{reflection.macro} association #{association} on model #{model_type.name}" unless %i[has_one belongs_to].include?(reflection.macro)

  return unless block_given?

  proxy = BackedByModel.new(
    graph_type,
    reflection.klass,
    base_model_type: base_model_type,
    path: [*path, association],
    object_to_model: object_to_model,
    detect_nulls: detect_nulls && Reflection.is_required(model_type, association)
  )

  proxy.instance_exec(&block)
end

.load_and_traverse(current_model, path, context) ⇒ Object

Returns a promise that will eventually resolve to the model that is at the end of the path



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
# File 'lib/graphql/models/definition_helpers.rb', line 13

def self.load_and_traverse(current_model, path, context)
  cache_model(context, current_model)
  return Promise.resolve(current_model) if path.empty? || current_model.nil?

  association = current_model.association(path[0])

  while !path.empty? && (association.loaded? || attempt_cache_load(current_model, association, context))
    current_model = association.target
    path = path[1..-1]
    cache_model(context, current_model)

    return Promise.resolve(current_model) if path.empty? || current_model.nil?

    association = current_model.association(path[0])
  end

  # If this is a has_many :through, then we need to load the two associations in sequence
  # (eg: Company has_many :health_lines, through: :open_enrollments => load open enrollments, then health lines)

  promise = if association.reflection.options[:through]
    # First step, load the :through association (ie, the :open_enrollments)
    through = association.reflection.options[:through]
    load_and_traverse(current_model, [through], context).then do |intermediate_models|
      # Now, for each of the intermediate models (OpenEnrollment), load the source association (:health_line)
      sources = intermediate_models.map do |im|
        load_and_traverse(im, [association.reflection.source_reflection_name], context)
      end

      # Once all of the eventual models are loaded, flatten the results
      Promise.all(sources).then do |result|
        result = result.flatten
        Helpers.load_association_with(association, result)
      end
    end
  else
    AssociationLoadRequest.new(current_model, path[0], context).load
  end

  promise.then do |next_model|
    next next_model if next_model.blank?
    cache_model(context, next_model)

    if path.length == 1
      sanity = next_model.is_a?(Array) ? next_model[0] : next_model
      next next_model
    else
      DefinitionHelpers.load_and_traverse(next_model, path[1..-1], context)
    end
  end
end

.mark_association_loaded(association, target) ⇒ Object



105
106
107
108
109
# File 'lib/graphql/models/definition_helpers.rb', line 105

def self.mark_association_loaded(association, target)
  association.loaded!
  association.target = target
  association.set_inverse_instance(target) unless target.nil?
end

.register_field_metadata(graph_type, field_name, metadata) ⇒ Object

Stores metadata about GraphQL fields that are available on this model’s GraphQL type.

Parameters:

  • metadata

    Should be a hash that contains information about the field’s definition, including :macro and :type



123
124
125
126
127
128
129
# File 'lib/graphql/models/definition_helpers.rb', line 123

def self.(graph_type, field_name, )
  field_name = field_name.to_s

  field_meta = graph_type.instance_variable_get(:@field_metadata)
  field_meta ||= graph_type.instance_variable_set(:@field_metadata, {})
  field_meta[field_name] = OpenStruct.new().freeze
end

.resolve_has_one_type(reflection) ⇒ Object



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
# File 'lib/graphql/models/definition_helpers/associations.rb', line 26

def self.resolve_has_one_type(reflection)
  ############################################
  ## Ordinary has_one/belongs_to associations
  ############################################

  if reflection.polymorphic?
    # For polymorphic associations, we look for a validator that limits the types of entities that could be
    # used, and use it to build a union. If we can't find one, raise an error.

    model_type = reflection.active_record
    valid_types = Reflection.possible_values(model_type, reflection.foreign_type)

    if valid_types.blank?
      raise ArgumentError, "Cannot include polymorphic #{reflection.name} association on model #{model_type.name}, because it does not define an inclusion validator on #{reflection.foreign_type}"
    end

    graph_types = valid_types.map { |t| GraphQL::Models.get_graphql_type(t) }.compact

    GraphQL::UnionType.define do
      name "#{model_type.name.demodulize}#{reflection.foreign_type.classify}"
      description "Objects that can be used as #{reflection.foreign_type.titleize.downcase} on #{model_type.name.titleize.downcase}"
      possible_types graph_types
    end
  else
    GraphQL::Models.get_graphql_type!(reflection.klass)
  end
end

.resolve_nullability(graphql_type, model_class, attribute_or_association, detect_nulls, options) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/graphql/models/definition_helpers/attributes.rb', line 6

def self.resolve_nullability(graphql_type, model_class, attribute_or_association, detect_nulls, options)
  # If detect_nulls is true, it means that everything on the path (ie, between base_model_class and model_class) is non null.
  # So for example, if we're five levels deep inside of proxy_to blocks, but every single association along the way has
  # a presence validator, then `detect_nulls` is false. Thus, we can take it one step further and enforce nullability on the
  # attribute itself.
  nullable = options[:nullable]

  if nullable.nil?
    nullable = !(detect_nulls && Reflection.is_required(model_class, attribute_or_association))
  end

  if nullable == false
    graphql_type = graphql_type.to_non_null_type
  else
    graphql_type
  end
end

.traverse_path(base_model, path, _context) ⇒ Object



111
112
113
114
115
116
117
118
119
# File 'lib/graphql/models/definition_helpers.rb', line 111

def self.traverse_path(base_model, path, _context)
  model = base_model
  path.each do |segment|
    return nil unless model
    model = model.public_send(segment)
  end

  model
end

.typesObject



8
9
10
# File 'lib/graphql/models/definition_helpers.rb', line 8

def self.types
  GraphQL::Define::TypeDefiner.instance
end