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 +4 -4
- data/lib/graphql_includable/concern.rb +5 -14
- data/lib/graphql_includable/edge.rb +65 -82
- data/lib/graphql_includable/resolver.rb +88 -176
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7e15a080b64989dcfc9dfb9e9df39cf8f37c96ee
|
4
|
+
data.tar.gz: c0c53dcc1a61dbaab44b9c585bb5a486d0d1223b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 =
|
15
|
-
generated_includes =
|
16
|
-
|
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
|
-
|
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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
44
|
-
|
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
|
48
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
70
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
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
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
chain
|
182
|
-
|
183
|
-
model_name =
|
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
|
-
|
193
|
-
|
194
|
-
|
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.
|
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:
|
12
|
+
date: 2019-06-25 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description:
|
15
15
|
email:
|