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
-
.attempt_cache_load(model, association, context) ⇒ Object
Attempts to retrieve the model from the query’s cache.
- .cache_model(context, model) ⇒ Object
- .define_attribute(graph_type, base_model_class, model_class, path, attribute, object_to_model, options, detect_nulls, &block) ⇒ Object
- .define_has_many_array(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls) ⇒ Object
- .define_has_many_connection(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls) ⇒ Object
-
.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.
- .define_proxy(graph_type, base_model_type, model_type, path, association, object_to_model, detect_nulls, &block) ⇒ Object
-
.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.
- .mark_association_loaded(association, target) ⇒ Object
-
.register_field_metadata(graph_type, field_name, metadata) ⇒ Object
Stores metadata about GraphQL fields that are available on this model’s GraphQL type.
- .resolve_has_one_type(reflection) ⇒ Object
- .resolve_nullability(graphql_type, model_class, attribute_or_association, detect_nulls, options) ⇒ Object
- .traverse_path(base_model, path, _context) ⇒ Object
- .types ⇒ Object
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..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, , 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, ) field_name = [: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 [:description] if .include?(:description) deprecation_reason [:deprecation_reason] if .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
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, , 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 = [: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 ([:nullable].nil? && detect_nulls) || [:nullable] == false association_type = association_type.to_non_null_type id_field_type = id_field_type.to_non_null_type end camel_name = [: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 [:description] if .include?(:description) deprecation_reason [:deprecation_reason] if .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 [:deprecation_reason] if .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
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, , 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 ([:nullable].nil? && detect_nulls) || [:nullable] == false connection_type = connection_type.to_non_null_type end camel_name = [: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 [:description] if .include?(:description) deprecation_reason [:deprecation_reason] if .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.
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, , 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 = [:name] association_graphql_type = resolve_has_one_type(reflection) association_graphql_type = resolve_nullability(association_graphql_type, model_type, association, detect_nulls, ) 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 [:description] if .include?(:description) deprecation_reason [:deprecation_reason] if .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, ) 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 [:deprecation_reason] if .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
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.[:through] # First step, load the :through association (ie, the :open_enrollments) through = association.reflection.[: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.
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 = graph_type.instance_variable_get(:@field_metadata) ||= graph_type.instance_variable_set(:@field_metadata, {}) [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, ) # 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 = [: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 |
.types ⇒ Object
8 9 10 |
# File 'lib/graphql/models/definition_helpers.rb', line 8 def self.types GraphQL::Define::TypeDefiner.instance end |