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_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.
- .define_has_many_array(graph_type, base_model_type, model_type, path, association, object_to_model, options) ⇒ Object
- .define_has_many_connection(graph_type, base_model_type, model_type, path, association, object_to_model, options) ⇒ Object
-
.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.
- .define_proxy(graph_type, base_model_type, model_type, path, association, object_to_model, &block) ⇒ Object
-
.detect_inclusion_values(model_type, attribute) ⇒ Object
Detects the values that are valid for an attribute by looking at the inclusion validators.
- .detect_is_required(model_type, attr_or_assoc) ⇒ Object
- .get_column(model_type, name) ⇒ Object
- .get_column!(model_type, name) ⇒ 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
- .range_to_graphql(value) ⇒ 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
- .traverse_path(base_model, path, context) ⇒ Object
- .type_to_graphql_type(type) ⇒ 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.
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..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.
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, ) column = get_column!(model_type, attribute) field_name = [: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 [:description] if .include?(:description) deprecation_reason [:deprecation_reason] if .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, ) 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 = [:type] || -> { types[!"#{reflection.klass.name}Graph".constantize] } camel_name = [: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 [: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 types[!types.ID] 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) ⇒ 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, ) 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 = [: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, ) 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 = [: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 [: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" 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 [: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, &block) ⇒ Object
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..include?(:if) || v..include?(:unless) || v.[:in].blank? } return nil unless validators.any? return validators.map { |v| v.[: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..include?(:if) || v..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
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.
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, {}) unless [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 |
.types ⇒ Object
6 7 8 |
# File 'lib/graphql/models/definition_helpers.rb', line 6 def self.types GraphQL::Define::TypeDefiner.instance end |