graphql_includable 0.2.0 → 0.2.1
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 +176 -0
- data/lib/graphql_includable/edge.rb +55 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1d376f28ad0b33963bc79eb820a3301964e621d
|
4
|
+
data.tar.gz: a6685ec899a6a3615d5fb1fafd366cb9e611c438
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57475ac341e200bbecca1f8bcfe554074e9dd9e5db39b325a3e252c77f9292e7a0583633119ab95b50c3277c554c7c83cec90ed8ff33ae2e51859f9efe50fc56
|
7
|
+
data.tar.gz: 84b171467d90d4e531c3f38e26f4103763d9aa05e330bbe9e89a73f4d76eb05fe1cd43bee70896a75def7dc1292f66fd5f8674a207aae6faa8011d325cdd4bda
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module GraphQLIncludable
|
4
|
+
module Concern
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def includes_from_graphql(ctx)
|
9
|
+
node = GraphQLIncludable::Concern.first_child_by_return_type(ctx.irep_node, model_name.to_s)
|
10
|
+
generated_includes = Concern.includes_from_graphql_node(node)
|
11
|
+
includes(generated_includes)
|
12
|
+
end
|
13
|
+
|
14
|
+
def delegate_cache
|
15
|
+
@delegate_cache ||= {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# hook ActiveSupport's delegate method to track models and where their delegated methods go
|
19
|
+
def delegate(*methods, args)
|
20
|
+
methods.each do |method|
|
21
|
+
delegate_cache[method] = args[:to]
|
22
|
+
end
|
23
|
+
super(*methods, args) if defined?(super)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.first_child_by_return_type(node, model_name)
|
28
|
+
matching_node = nil
|
29
|
+
return_type = node.return_type.unwrap
|
30
|
+
if return_type.to_s == model_name
|
31
|
+
matching_node = node
|
32
|
+
elsif node.respond_to?(:scoped_children)
|
33
|
+
node.scoped_children[return_type].each_value do |child_node|
|
34
|
+
matching_node = first_child_by_return_type(child_node, model_name)
|
35
|
+
break if matching_node
|
36
|
+
end
|
37
|
+
end
|
38
|
+
matching_node
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.children_through_connection(node, return_model)
|
42
|
+
includes = {}
|
43
|
+
# if node_is_relay_connection?(node)
|
44
|
+
# all_connection_children = node.scoped_children[node.return_type.unwrap]
|
45
|
+
# connection_children = all_connection_children.except('edges')
|
46
|
+
# edge_node = all_connection_children['edges']
|
47
|
+
# all_edge_children = edge_node.scoped_children[edge_node.return_type.unwrap]
|
48
|
+
# edge_children = all_edge_children.except('node')
|
49
|
+
# target_node = all_edge_children['node']
|
50
|
+
# children = target_node.scoped_children[target_node.return_type.unwrap]
|
51
|
+
|
52
|
+
# target_association = return_model.reflect_on_association(node_return_class(target_node).name.underscore)
|
53
|
+
# includes[target_association.name] = includes_from_graphql_node(target_node) if target_association
|
54
|
+
# else
|
55
|
+
children = node.scoped_children[node.return_type.unwrap]
|
56
|
+
# end
|
57
|
+
|
58
|
+
[children, includes]
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.includes_from_graphql_node(node)
|
62
|
+
return_model = node_return_class(node)
|
63
|
+
return [] unless return_model
|
64
|
+
|
65
|
+
includes = []
|
66
|
+
children, nested_includes = children_through_connection(node, return_model)
|
67
|
+
children.each_value do |child_node|
|
68
|
+
child_includes = includes_from_graphql_child(child_node, return_model)
|
69
|
+
|
70
|
+
if child_includes.is_a?(Hash)
|
71
|
+
nested_includes.merge!(child_includes)
|
72
|
+
else
|
73
|
+
includes += child_includes.is_a?(Array) ? child_includes : [child_includes]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
includes << nested_includes unless nested_includes.blank?
|
78
|
+
includes.uniq
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.includes_from_graphql_child(child_node, return_model)
|
82
|
+
specified_includes = child_node.definitions[0].metadata[:includes]
|
83
|
+
attribute_name = node_predicted_association_name(child_node)
|
84
|
+
includes_chain = delegated_includes_chain(return_model, attribute_name)
|
85
|
+
association = get_model_association(return_model, attribute_name, includes_chain)
|
86
|
+
|
87
|
+
if association
|
88
|
+
child_includes = includes_from_graphql_node(child_node)
|
89
|
+
join_name = (specified_includes || attribute_name)
|
90
|
+
|
91
|
+
# if node_is_relay_connection?(child_node)
|
92
|
+
# join_name = association.options[:through]
|
93
|
+
# edge_includes_chain = [association.name]
|
94
|
+
# edge_includes_chain << child_includes.pop[association.name.to_s.singularize.to_sym] if child_includes.last&.is_a?(Hash)
|
95
|
+
# edge_includes = array_to_nested_hash(edge_includes_chain)
|
96
|
+
# end
|
97
|
+
|
98
|
+
includes_chain << join_name
|
99
|
+
includes_chain << child_includes unless child_includes.blank?
|
100
|
+
# byebug if node_is_relay_connection?(child_node)
|
101
|
+
edge_includes=nil
|
102
|
+
[edge_includes, array_to_nested_hash(includes_chain)].reject(&:blank?)
|
103
|
+
else
|
104
|
+
includes = []
|
105
|
+
includes << array_to_nested_hash(includes_chain) unless includes_chain.blank?
|
106
|
+
includes << specified_includes if specified_includes
|
107
|
+
includes
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.node_return_class(node)
|
112
|
+
# rubocop:disable Lint/HandleExceptions, Style/RedundantBegin
|
113
|
+
begin
|
114
|
+
Object.const_get(node.return_type.unwrap.name.gsub(/(^SquareFoot|Edge$|Connection$)/, ''))
|
115
|
+
rescue NameError
|
116
|
+
end
|
117
|
+
# rubocop:enable Lint/HandleExceptions, Style/RedundantBegin
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.node_is_relay_connection?(node)
|
121
|
+
node.return_type.unwrap.name =~ /Connection$/
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.node_returns_active_record?(node)
|
125
|
+
klass = node_return_class(node)
|
126
|
+
klass && klass < ActiveRecord::Base
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.node_predicted_association_name(node)
|
130
|
+
definition = node.definitions[0]
|
131
|
+
specified_includes = definition.metadata[:includes]
|
132
|
+
if specified_includes.is_a?(Symbol)
|
133
|
+
specified_includes
|
134
|
+
else
|
135
|
+
(definition.property || definition.name).to_sym
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.get_model_association(model, association_name, includes_chain = nil)
|
140
|
+
delegated_model = model_name_to_class(includes_chain.last) unless includes_chain.blank?
|
141
|
+
(delegated_model || model).reflect_on_association(association_name)
|
142
|
+
end
|
143
|
+
|
144
|
+
# get a 1d array of the chain of delegated model names,
|
145
|
+
# so if model A delegates method B to model C, which delegates method B to model D,
|
146
|
+
# delegated_includes_chain(A, :B) => [:C, :D]
|
147
|
+
def self.delegated_includes_chain(base_model, method_name)
|
148
|
+
chain = []
|
149
|
+
method = method_name.to_sym
|
150
|
+
model_name = base_model.instance_variable_get('@delegate_cache').try(:[], method)
|
151
|
+
while model_name
|
152
|
+
chain << model_name
|
153
|
+
model = model_name_to_class(model_name)
|
154
|
+
model_name = model.instance_variable_get('@delegate_cache').try(:[], method)
|
155
|
+
end
|
156
|
+
chain
|
157
|
+
end
|
158
|
+
|
159
|
+
# convert a 1d array into a nested hash
|
160
|
+
# e.g. [:foo, :bar, :baz] => { :foo => { :bar => :baz }}
|
161
|
+
def self.array_to_nested_hash(arr)
|
162
|
+
arr.reverse.inject { |acc, item| { item => acc } }
|
163
|
+
end
|
164
|
+
|
165
|
+
# convert a model name into a class variable,
|
166
|
+
# e.g. :search_parameters -> SearchParameters
|
167
|
+
def self.model_name_to_class(model_name)
|
168
|
+
begin
|
169
|
+
model = model_name.to_s.camelize.constantize
|
170
|
+
rescue NameError
|
171
|
+
model = model_name.to_s.singularize.camelize.constantize
|
172
|
+
end
|
173
|
+
model
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module GraphQLIncludable
|
2
|
+
class Edge < GraphQL::Relay::Edge
|
3
|
+
def edge
|
4
|
+
join_chain = joins_along_edge
|
5
|
+
edge_class_name = join_chain.shift
|
6
|
+
edge_class = str_to_class(edge_class_name)
|
7
|
+
|
8
|
+
root_node = { class_to_str(parent.class).to_s.singularize => parent }
|
9
|
+
terminal_node = { class_to_str(node.class).singularize => node }
|
10
|
+
join_chain.reverse.each do |rel_name|
|
11
|
+
terminal_node = { rel_name.to_s.pluralize => terminal_node }
|
12
|
+
end
|
13
|
+
search_hash = root_node.merge(terminal_node)
|
14
|
+
|
15
|
+
@edge ||= edge_class.includes(*join_chain.map { |s| s.to_s.singularize }).find_by(search_hash)
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing(method_name, *args, &block)
|
19
|
+
if edge.respond_to?(method_name)
|
20
|
+
edge.send(method_name, *args, &block)
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond_to_missing(method_name, include_private = false)
|
27
|
+
edge.respond_to?(method_name) || super
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def str_to_class(str)
|
33
|
+
str.to_s.singularize.camelize.constantize
|
34
|
+
rescue
|
35
|
+
end
|
36
|
+
|
37
|
+
def class_to_str(klass)
|
38
|
+
klass.name.pluralize.downcase
|
39
|
+
end
|
40
|
+
|
41
|
+
def joins_along_edge
|
42
|
+
join_chain = []
|
43
|
+
starting_class = parent.class
|
44
|
+
node_relationship_name = class_to_str(node.class)
|
45
|
+
while starting_class
|
46
|
+
reflection = starting_class.reflect_on_association(node_relationship_name)
|
47
|
+
association_name = reflection&.options&.try(:[], :through)
|
48
|
+
join_chain << association_name if association_name
|
49
|
+
starting_class = str_to_class(association_name)
|
50
|
+
node_relationship_name = node_relationship_name.singularize
|
51
|
+
end
|
52
|
+
join_chain
|
53
|
+
end
|
54
|
+
end
|
55
|
+
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.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Rouse
|
@@ -20,6 +20,8 @@ extensions: []
|
|
20
20
|
extra_rdoc_files: []
|
21
21
|
files:
|
22
22
|
- lib/graphql_includable.rb
|
23
|
+
- lib/graphql_includable/concern.rb
|
24
|
+
- lib/graphql_includable/edge.rb
|
23
25
|
homepage: https://github.com/thesquarefoot/graphql_includable
|
24
26
|
licenses:
|
25
27
|
- MIT
|