graphql_includable 0.2.11 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8c4d33679dac4d67ec32b21e75fec07de5880b01
4
- data.tar.gz: 9740f56824351816c832548ceec61a64181d53a0
3
+ metadata.gz: 7e15a080b64989dcfc9dfb9e9df39cf8f37c96ee
4
+ data.tar.gz: c0c53dcc1a61dbaab44b9c585bb5a486d0d1223b
5
5
  SHA512:
6
- metadata.gz: 66b0ff3db10a0f536d400082e1645a3ebececc22e44868a89a9edb1f4dddf5f7fb32f1d3b96cd20e57ff4fc06ab8eee600706e0567129e6cafe3aee023e8f873
7
- data.tar.gz: cadeae8bb2ccfdff4f5de8d03e48294278204c9138d0e92f5c1f998fb1a407bca81fcb43d7551e53a75a8b9f93a1f15825e69b340fc5974fbc8dcb11c554ddf7
6
+ metadata.gz: c7d535ba4e020ff2eac12c1aa81f63de740af241dfc3b1669167667afb8bf8ed6ca1edd8d761b37f847555d03a64e8c23691a91b857a93e0c985d561a0a091b1
7
+ data.tar.gz: 320b95f48de65e16b42033b2ef45cd921d86c57e70e98ada5600155d7204a912665db781203f090d011d4b83ed8e871825a751c43491efc427a79232f2507387
@@ -1,24 +1,17 @@
1
1
  require 'active_support/concern'
2
- require 'logger'
3
2
 
4
3
  module GraphQLIncludable
5
- # ActiveSupport::Concern to include onto GraphQL-mapped models
6
4
  module Concern
7
5
  extend ActiveSupport::Concern
8
6
 
9
7
  module ClassMethods
10
- # Main entry point of the concern, to be called from top-level fields
11
- # Accepts a graphql-ruby query context, preloads, and returns itself
12
- # @param ctx GraphQL::Query::Context
13
8
  def includes_from_graphql(ctx)
14
- node = GraphQLIncludable::Resolver.find_node_by_return_type(ctx.irep_node, name)
15
- generated_includes = GraphQLIncludable::Resolver.includes_for_node(node)
16
- preload(generated_includes)
9
+ node = Resolver.find_node_by_return_type(ctx.irep_node, name)
10
+ generated_includes = Resolver.includes_for_node(node)
11
+ puts("INCLUDES!!!: #{generated_includes}")
12
+ includes(generated_includes)
17
13
  rescue => e
18
- # As this feature is just for a performance gain, it should never
19
- # fail destructively, so catch and log all exceptions, but continue
20
- raise e if Rails && Rails.env.development?
21
- Rails.logger.debug("#{e.message}\n#{e.backtrace.join('\n')}")
14
+ Rails.logger.error(e)
22
15
  self
23
16
  end
24
17
 
@@ -26,8 +19,6 @@ module GraphQLIncludable
26
19
  @delegate_cache ||= {}
27
20
  end
28
21
 
29
- # Hook and cache all calls to ActiveRecord's `delegate` method,
30
- # so that preloading can take place through delegated models
31
22
  def delegate(*methods, args)
32
23
  methods.each do |method|
33
24
  delegate_cache[method] = args[:to]
@@ -1,107 +1,90 @@
1
1
  module GraphQLIncludable
2
2
  class Edge < GraphQL::Relay::Edge
3
- # edge_record represents the data in between `node` and `parent`,
4
- # basically the `through` record in a has-many-through association
5
- # @returns record ActiveRecord::Base
6
- def edge_record
7
- @edge_record ||= edge_record_from_memory || edge_record_from_database
8
- end
9
-
10
- # attempt to query the edge record freshly from the database
11
- # @returns record ActiveRecord::Base
12
- def edge_record_from_database
13
- first_association, *nested_associations = associations_between_node_and_parent
14
- edge_class = self.class.str_to_class(first_association)
15
- root_association_key = root_association_key(edge_class)
3
+ def edge
4
+ return @edge if @edge
5
+ join_chain = joins_along_edge
6
+ edge_class_name = join_chain.shift
7
+ edge_class = str_to_class(edge_class_name)
16
8
 
17
- selector = edge_class
18
-
19
- if nested_associations.present?
20
- nested_association_names = nested_associations.map { |s| s.to_s.singularize }
21
- selector = selector.merge(edge_class.includes(*nested_association_names))
9
+ root_association_key = class_to_str(parent.class)
10
+ unless edge_class.reflections.keys.include?(root_association_key)
11
+ is_polymorphic = true
12
+ root_association_key = edge_class.reflections.select { |k, r| r.polymorphic? }.keys.first
22
13
  end
23
14
 
24
- selector.find_by(
25
- where_hash_for_edge(root_association_key, nested_associations)
26
- )
27
- end
15
+ if parent.class.delegate_cache&.key?(edge_class_name)
16
+ root_association_search_value = parent.send(parent.class.delegate_cache[edge_class_name])
17
+ else
18
+ root_association_search_value = parent
19
+ end
28
20
 
29
- # attempt to pull the preloaded edge record out of the associated object in memory
30
- # @returns record ActiveRecord::Base
31
- def edge_record_from_memory
32
- associations = associations_between_node_and_parent
33
- records = associations.reverse.reduce(parent) { |acc, cur| acc.send(cur) }
34
- return unless records.loaded?
35
- records.first do |rec|
36
- child_association_name = node.class.name.downcase.to_sym
37
- rec.send(root_association_key) == parent && rec.send(child_association_name) == node
21
+ root_node = { root_association_key.to_sym => [root_association_search_value] }
22
+ terminal_node = { class_to_str(node.class) => node }
23
+ join_chain.reverse.each do |rel_name|
24
+ terminal_node = { rel_name.to_s.pluralize => terminal_node }
38
25
  end
26
+
27
+ search_hash = root_node.merge(terminal_node)
28
+ edge_includes = join_chain.map { |s| s.to_s.singularize }
29
+ edge_class = edge_class.includes(*edge_includes) unless edge_includes.empty?
30
+ edge_class = edge_class.joins(root_association_key.to_sym) unless is_polymorphic
31
+ @edge ||= edge_class.find_by(search_hash)
39
32
  end
40
33
 
41
- # Delegate method calls on this Edge instance to the ActiveRecord instance
42
34
  def method_missing(method_name, *args, &block)
43
- return super unless edge_record.respond_to?(method_name)
44
- edge_record.send(method_name, *args, &block)
35
+ if edge.respond_to?(method_name)
36
+ edge.send(method_name, *args, &block)
37
+ else
38
+ super
39
+ end
45
40
  end
46
41
 
47
- def respond_to_missing?(method_name, include_private = false)
48
- edge_record.respond_to?(method_name) || super
42
+ def respond_to_missing(method_name, include_private = false)
43
+ edge.respond_to?(method_name) || super
49
44
  end
50
45
 
51
46
  private
52
47
 
53
- # List all HasManyThrough associations between node and parent models
54
- # @returns associations Array<Symbol>
55
- def associations_between_node_and_parent
56
- return @associations if @associations.present?
57
-
58
- associations = []
59
- association_name = node.class.name.pluralize.downcase.to_sym
60
- association = parent.class.reflect_on_association(association_name)
61
- while association.is_a?(ActiveRecord::Reflection::ThroughReflection)
62
- associations.unshift(association.options[:through])
63
- association = parent.class.reflect_on_association(association.options[:through])
64
- end
65
-
66
- @associations = associations
48
+ def str_to_class(str)
49
+ str.to_s.singularize.camelize.constantize
50
+ rescue
67
51
  end
68
52
 
69
- # List the key:value criteria for finding the edge record in the database
70
- # @param root_key Symbol
71
- # @param nested_associations Array<Symbol>
72
- # @returns where_hash Hash<?>
73
- def where_hash_for_edge(root_key, nested_associations)
74
- root_include = { root_key => [parent] }
75
- terminal_include = { self.class.class_to_str(node.class) => node }
76
- inner_includes = nested_associations.reverse.reduce(terminal_include) do |acc, rel_name|
77
- { rel_name.to_s.pluralize => acc }
78
- end
79
- root_include.merge(inner_includes)
53
+ def class_to_str(klass)
54
+ klass.name.downcase
80
55
  end
81
56
 
82
- # @param edge_class ActiveRecord::Base
83
- # @returns key Symbol
84
- def root_association_key(edge_class)
85
- key = self.class.class_to_str(parent.class)
86
- unless edge_class.reflections.keys.include?(key)
87
- key = edge_class.reflections.select { |_k, r| r.polymorphic? }.keys.first
57
+ def joins_along_edge
58
+ # node.edges
59
+ # edge.node
60
+ # edge.parent
61
+ # parent.edges
62
+ # node.parents
63
+ # parent.nodes
64
+ edge_association_name = node.class.name.pluralize.downcase.to_sym
65
+ if parent.class.delegate_cache&.key?(edge_association_name)
66
+ parent_class = str_to_class(parent.class.delegate_cache[edge_association_name])
67
+ else
68
+ parent_class = parent.class
88
69
  end
89
- key.to_sym
90
- end
91
-
92
- class << self
93
- # @param str String
94
- # @returns klass Class
95
- def str_to_class(str)
96
- str.to_s.singularize.camelize.constantize
97
- rescue # rubocop:disable Lint/HandleExceptions
98
- end
99
-
100
- # @param klass Class
101
- # @returns str String
102
- def class_to_str(klass)
103
- klass.name.downcase
70
+ edge_association = parent_class.reflect_on_association(edge_association_name)
71
+ edge_joins = []
72
+ while edge_association.is_a? ActiveRecord::Reflection::ThroughReflection
73
+ edge_joins.unshift edge_association.options[:through]
74
+ edge_association = parent_class.reflect_on_association(edge_association.options[:through])
104
75
  end
76
+ edge_joins
77
+ # join_chain = []
78
+ # starting_class = parent.class
79
+ # node_relationship_name = class_to_str(node.class)
80
+ # while starting_class
81
+ # reflection = starting_class.reflect_on_association(node_relationship_name)
82
+ # association_name = reflection&.options&.try(:[], :through)
83
+ # join_chain << association_name if association_name
84
+ # starting_class = str_to_class(association_name)
85
+ # node_relationship_name = node_relationship_name.singularize
86
+ # end
87
+ # join_chain
105
88
  end
106
89
  end
107
90
  end
@@ -1,15 +1,10 @@
1
- # Includes = Symbol | Array<Symbol> | Hash<Symbol, Includes>
2
1
  module GraphQLIncludable
3
2
  class Resolver
4
- class << self
5
- # Returns the first node in the tree which returns a specific type
6
- # @param node GraphQL::InternalRepresentation::Node
7
- # @param desired_return_type String
8
- # @returns matching_node GraphQL::InternalRepresentation::Node
9
- def find_node_by_return_type(node, desired_return_type)
10
- return_type = node.return_type.unwrap.to_s
11
- return node if return_type == desired_return_type
12
- return unless node.respond_to?(:scoped_children)
3
+ # Returns the first node in the tree which returns a specific type
4
+ def self.find_node_by_return_type(node, desired_return_type)
5
+ return_type = node.return_type.unwrap.to_s
6
+ return node if return_type == desired_return_type
7
+ if node.respond_to?(:scoped_children)
13
8
  matching_node = nil
14
9
  node.scoped_children.values.each do |selections|
15
10
  matching_node = selections.values.find do |child_node|
@@ -19,185 +14,102 @@ module GraphQLIncludable
19
14
  end
20
15
  matching_node
21
16
  end
17
+ end
22
18
 
23
- # Collect preloadable relationships for the resolution of `node`
24
- #
25
- # @param node GraphQL::InternalRepresentation::Node
26
- # @returns includes Includes
27
- def includes_for_node(node)
28
- return_model = node_return_model(node)
29
- return [] if return_model.blank?
30
- children = node.scoped_children[node.return_type.unwrap]
31
- child_includes = children.map { |_key, child| includes_for_node_child(child, return_model) }
32
- combine_child_includes(child_includes)
33
- end
34
-
35
- # Collect preloadable relationships for all selections on `node`,
36
- # as accessed from `parent_model`.
37
- #
38
- # @param node GraphQL::InternalRepresentation::Node
39
- # @param parent_model ActiveRecord::Base
40
- # @returns child_includes Includes
41
- def includes_for_node_child(node, parent_model)
42
- included_attributes = node_includes_source_from_metadata(node)
43
- return included_attributes if included_attributes.is_a?(Hash)
44
- included_attributes = [included_attributes] unless included_attributes.is_a?(Array)
45
- includes = included_attributes.map do |attribute_name|
46
- includes_for_node_child_attribute(node, parent_model, attribute_name)
47
- end
48
- includes.size == 1 ? includes.first : includes
49
- end
50
-
51
- # Collect preloadable relationships for a single selection on `node`,
52
- # as accessed from `parent_model`
53
- #
54
- # @param node GraphQL::InternalRepresentation::Node
55
- # @param parent_model ActiveRecord::Base
56
- # @param attribute_name Symbol
57
- # @returns includes Includes
58
- def includes_for_node_child_attribute(node, parent_model, attribute_name)
59
- includes_array = includes_delegated_through(parent_model, attribute_name)
60
- target_model = model_name_to_class(includes_array.last) || parent_model
61
- association = target_model.reflect_on_association(attribute_name)
62
-
63
- if association && node_is_relay_connection?(node)
64
- includes_array << includes_for_node_relay_child_attribute(
65
- node, attribute_name, association
66
- )
67
- elsif association
68
- includes_array += [attribute_name, includes_for_node(node)]
69
- end
70
- array_to_nested_hash(includes_array)
71
- end
72
-
73
- # Collect preloadable relationships for a single selection on `node`,
74
- # as accessed from `parent_model`, through the Relay specification's
75
- # named `edges` and `nodes` selections.
76
- #
77
- # @param node GraphQL::InternalRepresentation::Node
78
- # @param attribute_name Symbol
79
- # @param association ActiveRecord::HasManyThroughAssociation
80
- def includes_for_node_relay_child_attribute(node, attribute_name, association)
81
- children = node.scoped_children[node.return_type.unwrap]
82
-
83
- edge_data = includes_for_node_relay_child_edge_attribute(
84
- children['edges'], attribute_name, association
85
- )
86
- leaf_data = [attribute_name, includes_for_node(node)] if children['nodes']
87
-
88
- [array_to_nested_hash(edge_data), array_to_nested_hash(leaf_data)].reject(&:blank?)
89
- end
90
-
91
- # Collect preloadable relationships for a single selection on `node`,
92
- # a Relay Edge node whose parent is a Connection being accessed from `parent_model`
93
- #
94
- # @param node GraphQL::InternalRepresentation::Node
95
- # @param attribute_name Symbol
96
- # @param association ActiveRecord::HasManyThroughAssociation
97
- def includes_for_node_relay_child_edge_attribute(node, attribute_name, association)
98
- return unless node
99
- leaf_includes = [attribute_name.to_s.singularize.to_sym]
100
- edge_children = node.scoped_children[node.return_type.unwrap]
101
- if edge_children['node']
102
- leaf_includes << includes_for_node(edge_children['node'])
19
+ # Translate a node's selections into `includes` values
20
+ # Combine and format children values
21
+ # Noop on nodes that don't return AR (so no associations to include)
22
+ def self.includes_for_node(node)
23
+ return_model = node_return_model(node)
24
+ return [] if return_model.blank?
25
+
26
+ includes = []
27
+ children = node.scoped_children[node.return_type.unwrap]
28
+ nested_includes = {}
29
+
30
+ children.each_value do |child_node|
31
+ child_includes = includes_for_child(child_node, return_model)
32
+
33
+ if child_includes.is_a?(Hash)
34
+ nested_includes.merge!(child_includes)
35
+ elsif child_includes.is_a?(Array)
36
+ includes += child_includes
37
+ else
38
+ includes << child_includes
103
39
  end
104
- leaf_includes = array_to_nested_hash(leaf_includes)
105
-
106
- [association.options[:through], [*includes_for_node(node), leaf_includes]]
107
40
  end
108
41
 
109
- # Format child includes into a data structure that can be preloaded
110
- # Singular terminal nodes become symbols,
111
- # multiple terminal nodes become arrays of symbols,
112
- # and branching nodes become hashes.
113
- # The result can be passed directly into ActiveModel::Base.includes
114
- #
115
- # @param child_includes Includes
116
- # @returns child_includes Includes
117
- def combine_child_includes(child_includes)
118
- includes = []
119
- nested_includes = {}
120
- child_includes.each do |child|
121
- if child.is_a?(Hash)
122
- nested_includes.merge!(child)
123
- elsif child.is_a?(Array)
124
- includes += child
125
- else
126
- includes << child
127
- end
128
- end
129
- includes << nested_includes if nested_includes.present?
130
- includes.uniq.reject(&:blank?)
131
- end
42
+ includes << nested_includes if nested_includes.present?
43
+ includes.uniq
44
+ end
132
45
 
133
- # Retrieve the Ruby class for a model by name
134
- # Attempts to singularize the name if not found
135
- #
136
- # @param model_name Symbol | String
137
- # @returns klass Class
138
- def model_name_to_class(model_name)
139
- begin
140
- model_name.to_s.camelize.constantize
141
- rescue NameError
142
- model_name.to_s.singularize.camelize.constantize
143
- end
144
- rescue # rubocop:disable Lint/HandleExceptions
46
+ def self.includes_for_child(node, parent_model)
47
+ attribute_name = node_predicted_association_name(node)
48
+ delegated_through = includes_delegated_through(parent_model, attribute_name)
49
+ delegated_model = model_name_to_class(delegated_through.last) if delegated_through.present?
50
+ association = (delegated_model || parent_model).reflect_on_association(attribute_name)
51
+ interceding_includes = []
52
+ # association = get_model_association(parent_model, attribute_name, interceding_includes)
53
+
54
+ if association
55
+ child_includes = includes_for_node(node)
56
+ child_includes.delete(association.plural_name.singularize.to_sym) if association.options[:through]
57
+
58
+ array_to_nested_hash(interceding_includes + [attribute_name, child_includes].reject(&:blank?))
59
+ # if node_is_relay_connection?(node)
60
+ # join_name = association.options[:through]
61
+ # edge_includes_chain = [association.name]
62
+ # edge_includes_chain << child_includes.pop[association.name.to_s.singularize.to_sym] if child_includes.last&.is_a?(Hash)
63
+ # edge_includes = array_to_nested_hash(edge_includes_chain)
64
+ # end
65
+ else
66
+ # TODO: specified includes?
67
+ [array_to_nested_hash(interceding_includes)].reject(&:blank?)
145
68
  end
69
+ end
146
70
 
147
- # Translate a node's return type to an ActiveRecord model
148
- #
149
- # @param node GraphQL::InternalRepresentation::Node
150
- # @returns model ActiveRecord::Base | nil
151
- REGEX_CLEAN_GQL_TYPE_NAME = /(^SquareFoot|Edge$|Connection$)/
152
- def node_return_model(node)
153
- model = Object.const_get(node.return_type.unwrap.name.gsub(REGEX_CLEAN_GQL_TYPE_NAME, ''))
154
- model if model < ActiveRecord::Base
155
- rescue NameError # rubocop:disable Lint/HandleExceptions
71
+ def self.model_name_to_class(model_name)
72
+ begin
73
+ model_name.to_s.camelize.constantize
74
+ rescue NameError
75
+ model_name.to_s.singularize.camelize.constantize
156
76
  end
77
+ rescue
78
+ end
157
79
 
158
- # @param node GraphQL::InternalRepresentation::Node
159
- # @returns is_relay_connection Boolean
160
- REGEX_RELAY_CONNECTION = /Connection$/
161
- def node_is_relay_connection?(node)
162
- node.return_type.unwrap.name =~ REGEX_RELAY_CONNECTION
163
- end
80
+ # Translate a node's return type to an ActiveRecord model
81
+ def self.node_return_model(node)
82
+ model = Object.const_get(node.return_type.unwrap.name.gsub(/(^SquareFoot|Edge$|Connection$)/, ''))
83
+ model if model < ActiveRecord::Base
84
+ rescue NameError
85
+ end
164
86
 
165
- # Predict the association name to include from a field's metadata
166
- #
167
- # @param node GraphQL::InternalRepresentation::Node
168
- # @returns possible_includes Includes
169
- def node_includes_source_from_metadata(node)
170
- definition = node.definitions.first
171
- definition.metadata[:includes] || (definition.property || definition.name).to_sym
172
- end
87
+ def self.node_predicted_association_name(node)
88
+ definition = node.definitions.first
89
+ definition.metadata[:includes] || (definition.property || definition.name).to_sym
90
+ end
173
91
 
174
- # If method_name is delegated from base_model, return an array of
175
- # associations through which those methods can be delegated
176
- #
177
- # @param base_model ActiveRecord::Base
178
- # @param method_name String | Symbol
179
- # @returns chain Array<Symbol>
180
- def includes_delegated_through(base_model, method_name)
181
- chain = []
182
- method = method_name.to_sym
183
- model_name = base_model.instance_variable_get(:@delegate_cache).try(:[], method)
184
- while model_name
185
- chain << model_name
186
- model = model_name_to_class(model_name)
187
- model_name = model.instance_variable_get(:@delegate_cache).try(:[], method)
188
- end
189
- chain
92
+ # If method_name is delegated from base_model, return an array of
93
+ # associations through which those methods can be delegated
94
+ def self.includes_delegated_through(base_model, method_name)
95
+ chain = []
96
+ method = method_name.to_sym
97
+ model_name = base_model.instance_variable_get(:@delegate_cache).try(:[], method)
98
+ while model_name
99
+ chain << model_name
100
+ model = model_name_to_class(model_name)
101
+ model_name = model.instance_variable_get(:@delegate_cache).try(:[], method)
190
102
  end
103
+ chain
104
+ end
191
105
 
192
- # Right-reduce an array into a nested hash
193
- #
194
- # @param arr Array<Includes>
195
- # @returns nested_hash Includes
196
- def array_to_nested_hash(arr)
197
- (arr || []).reject(&:blank?)
198
- .reverse
199
- .inject { |acc, item| { item => acc } } || {}
200
- end
106
+ # Right-reduce an array into a nested hash
107
+ def self.array_to_nested_hash(arr)
108
+ arr.reverse.inject { |acc, item| { item => acc } } || {}
201
109
  end
110
+
111
+ # def self.node_is_relay_connection?(node)
112
+ # node.return_type.unwrap.name =~ /Connection$/
113
+ # end
202
114
  end
203
115
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql_includable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.11
4
+ version: 0.2.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Rouse
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-09-11 00:00:00.000000000 Z
12
+ date: 2019-06-25 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description:
15
15
  email: