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.



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

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

  reflection = association.reflection
  return false unless [: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
    mark_association_loaded(association, nil) and return true if target_id.nil?

    # 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



74
75
76
77
# File 'lib/graphql/models/definition_helpers.rb', line 74

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

.define_attribute(graph_type, base_model_type, model_type, path, attribute, object_to_model, options) ⇒ Object

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

Parameters:

  • graph_type

    The GraphQL::ObjectType that the field is being added to

  • model_type

    The class object for the model that defines the attribute

  • path

    The associations (in order) that need to be loaded, starting from the graph_type’s model

  • attribute

    The name of the attribute that is accessed on the target model_type



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

def self.define_attribute(graph_type, base_model_type, model_type, path, attribute, object_to_model, options)
  column = get_column!(model_type, attribute)
  field_name = options[:name] || column.camel_name

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

  graph_type.fields[field_name.to_s] = GraphQL::Field.define do
    name field_name.to_s
    type column.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

      if column.is_range
        DefinitionHelpers.range_to_graphql(model.public_send(attribute))
      else
        model.public_send(attribute)
      end
    end
  end
end

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



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

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

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

  type_lambda = options[:type] || -> { types[!"#{reflection.klass.name}Graph".constantize] }
  camel_name = options[:name] || association.to_s.camelize(:lower).to_sym

  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 type_lambda
    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 types[!types.ID]
    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) ⇒ Object



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

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

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

  type_lambda = -> { "#{reflection.klass.name}Graph".constantize.connection_type }
  camel_name = options[:name] || association.to_s.camelize(:lower).to_sym

  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
  })

  GraphQL::Relay::Define::AssignConnection.call(graph_type, camel_name, type_lambda) do
    resolve -> (model, args, context) do
      return nil unless model
      return GraphSupport.secure(model.public_send(association), context)
    end
  end
end

.define_has_one(graph_type, base_model_type, model_type, path, association, object_to_model, options) ⇒ 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.



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

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

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

  # Define the field for the association itself

  camel_name = options[:name] || association.to_s.camelize(:lower)
  camel_name = camel_name.to_sym if camel_name.is_a?(String)

  type_lambda = resolve_has_one_type(reflection)

  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 type_lambda
    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"

  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 types.ID
    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, &block) ⇒ Object

Raises:

  • (ArgumentError)


4
5
6
7
8
9
10
11
12
13
14
# File 'lib/graphql/models/definition_helpers/associations.rb', line 4

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

  return unless block_given?

  proxy = ProxyBlock.new(graph_type, base_model_type, reflection.klass, [*path, association], object_to_model)
  proxy.instance_exec(&block)
end

.detect_inclusion_values(model_type, attribute) ⇒ Object

Detects the values that are valid for an attribute by looking at the inclusion validators



96
97
98
99
100
101
102
103
104
# File 'lib/graphql/models/definition_helpers.rb', line 96

def self.detect_inclusion_values(model_type, attribute)
  # Get all of the inclusion validators
  validators = model_type.validators_on(attribute).select { |v| v.is_a?(ActiveModel::Validations::InclusionValidator) }

  # Ignore any inclusion validators that are using the 'if' or 'unless' options
  validators = validators.reject { |v| v.options.include?(:if) || v.options.include?(:unless) || v.options[:in].blank? }
  return nil unless validators.any?
  return validators.map { |v| v.options[:in] }.reduce(:&)
end

.detect_is_required(model_type, attr_or_assoc) ⇒ Object



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

def self.detect_is_required(model_type, attr_or_assoc)
  col = model_type.columns.detect { |c| c.name == attr_or_assoc.to_s }
  return true if col && !col.null

  validators = model_type.validators_on(attr_or_assoc)
    .select { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) }
    .reject { |v| v.options.include?(:if) || v.options.include?(:unless) }

  return true if validators.any?

  # The column is nullable, and there are no unconditional presence validators,
  # so it's at least sometimes optional
  false
end

.get_column(model_type, name) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/graphql/models/definition_helpers/attributes.rb', line 28

def self.get_column(model_type, name)
  col = model_type.columns.detect { |c| c.name == name.to_s }
  return nil unless col

  if model_type.graphql_enum_types.include?(name)
    graphql_type = model_type.graphql_enum_types[name]
  else
    graphql_type = type_to_graphql_type(col.type)
  end

  if col.array
    graphql_type = types[graphql_type]
  end

  return OpenStruct.new({
    is_range: /range\z/ === col.type.to_s,
    camel_name: name.to_s.camelize(:lower).to_sym,
    graphql_type: graphql_type,
    nullable: col.null
  })
end

.get_column!(model_type, name) ⇒ Object

Raises:

  • (ArgumentError)


50
51
52
53
54
# File 'lib/graphql/models/definition_helpers/attributes.rb', line 50

def self.get_column!(model_type, name)
  col = get_column(model_type, name)
  raise ArgumentError.new("The attribute #{name} wasn't found on model #{model_type.name}.") unless col
  col
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



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

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

  association = current_model.association(path[0])

  while path.length > 0 && (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.length == 0 || current_model.nil?

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

  request = AssociationLoadRequest.new(current_model, path[0], context)
  request.load.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



79
80
81
82
83
# File 'lib/graphql/models/definition_helpers.rb', line 79

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

.range_to_graphql(value) ⇒ Object



56
57
58
59
60
61
62
63
64
# File 'lib/graphql/models/definition_helpers/attributes.rb', line 56

def self.range_to_graphql(value)
  return nil unless value

  begin
    [value.first, value.last_included]
  rescue TypeError
    [value.first, value.last]
  end
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, {}) unless field_meta
  field_meta[field_name] = OpenStruct.new().freeze
end

.resolve_has_one_type(reflection) ⇒ Object



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

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

  return -> { "#{reflection.klass.name}Graph".constantize } if !reflection.polymorphic?

  ############################################
  ## Polymorphic associations
  ############################################

  # 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 = detect_inclusion_values(model_type, reflection.foreign_type)

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

  return ->() do
    graph_types = valid_types.map { |t| "#{t}Graph".safe_constantize }.compact

    GraphQL::UnionType.define do
      name "#{model_type.name}#{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
  end
end

.traverse_path(base_model, path, context) ⇒ Object



85
86
87
88
89
90
91
92
93
# File 'lib/graphql/models/definition_helpers.rb', line 85

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

  return model
end

.type_to_graphql_type(type) ⇒ Object



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

def self.type_to_graphql_type(type)
  registered_type = ScalarTypes.registered_type(type)
  if registered_type
    return registered_type.is_a?(Proc) ? registered_type.call : registered_type
  end

  case type
  when :boolean
    types.Boolean
  when :integer
    types.Int
  when :float
    types.Float
  when :daterange
    inner_type = type_to_graphql_type(:date)
    types[!inner_type]
  when :tsrange
    inner_type = type_to_graphql_type(:datetime)
    types[!inner_type]
  else
    types.String
  end
end

.typesObject



6
7
8
# File 'lib/graphql/models/definition_helpers.rb', line 6

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