graphql_includable 0.2.12 → 0.3.0.alpha.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 +3 -3
- data/lib/graphql_includable/relay/edge_with_node.rb +24 -0
- data/lib/graphql_includable/relay/edge_with_node_connection.rb +62 -0
- data/lib/graphql_includable/relay/edge_with_node_connection_type.rb +25 -0
- data/lib/graphql_includable/relay/instrumentation/connection.rb +31 -0
- data/lib/graphql_includable/resolver.rb +134 -46
- data/lib/graphql_includable.rb +17 -1
- metadata +11 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e012854a25b8c65723c9903dda037159e41999e1
|
4
|
+
data.tar.gz: bb0ae8ce407dc874d6ae8c9361cab4ca6010af0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2aee7ee36e05bc87d308f4576fb48ac0205105e612ee2ee140865bb54dae043376bec46af4d237992fce32eec5a9ecb248b70e7db8aeafb653b0c299bd53690
|
7
|
+
data.tar.gz: a014ad366b299f89037d0fad3e0d6b1a87d614f71c691696b55bbd7ea1a4a34ab36969ed79b0db5fdb93be50d11427c5126a477c9f8b2e7db199c3495cc59869
|
@@ -7,9 +7,9 @@ module GraphQLIncludable
|
|
7
7
|
module ClassMethods
|
8
8
|
def includes_from_graphql(ctx)
|
9
9
|
node = Resolver.find_node_by_return_type(ctx.irep_node, name)
|
10
|
-
|
11
|
-
|
12
|
-
includes(
|
10
|
+
manager = IncludesManager.new(nil)
|
11
|
+
Resolver.includes_for_node(node, manager)
|
12
|
+
includes(manager.includes)
|
13
13
|
rescue => e
|
14
14
|
Rails.logger.error(e)
|
15
15
|
self
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module GraphQLIncludable
|
2
|
+
module Relay
|
3
|
+
class EdgeWithNode < GraphQL::Relay::Edge
|
4
|
+
def initialize(node, connection)
|
5
|
+
@edge = node
|
6
|
+
edge_to_node_property = connection.field.type.fields['edges'].metadata[:edge_to_node_property]
|
7
|
+
node = @edge.public_send(edge_to_node_property)
|
8
|
+
super(node, connection)
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(method_name, *args, &block)
|
12
|
+
if @edge.respond_to?(method_name)
|
13
|
+
@edge.send(method_name, *args, &block)
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to_missing(method_name, include_private = false)
|
20
|
+
@edge.respond_to?(method_name) || super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module GraphQLIncludable
|
2
|
+
module Relay
|
3
|
+
class ConnectionEdgesAndNodes
|
4
|
+
attr_reader :parent, :edges_property, :nodes_property
|
5
|
+
|
6
|
+
def initialize(parent, edges_property, nodes_property)
|
7
|
+
@parent = parent
|
8
|
+
@edges_property = edges_property
|
9
|
+
@nodes_property = nodes_property
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class EdgeWithNodeConnection < GraphQL::Relay::RelationConnection
|
14
|
+
def initialize(nodes, *args, &block)
|
15
|
+
@connection_edges_and_nodes = nodes
|
16
|
+
@loaded_nodes = nil
|
17
|
+
@loaded_edges = nil
|
18
|
+
super(nil, *args, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def edge_nodes
|
22
|
+
raise 'This should not be called from a EdgeWithNodeConnectionType'
|
23
|
+
end
|
24
|
+
|
25
|
+
def fetch_edges
|
26
|
+
@loaded_edges ||= @connection_edges_and_nodes.parent.public_send(@connection_edges_and_nodes.edges_property)
|
27
|
+
# Set nodes to make underlying BaseConnection work
|
28
|
+
@nodes = @loaded_edges
|
29
|
+
@loaded_edges
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch_nodes
|
33
|
+
@loaded_nodes ||= @connection_edges_and_nodes.parent.public_send(@connection_edges_and_nodes.nodes_property)
|
34
|
+
# Set nodes to make underlying BaseConnection work
|
35
|
+
@nodes = @loaded_nodes
|
36
|
+
@loaded_nodes
|
37
|
+
end
|
38
|
+
|
39
|
+
def page_info
|
40
|
+
@nodes = determin_page_info_nodes
|
41
|
+
super
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def determin_page_info_nodes
|
47
|
+
# If the query asks for `pageInfo` before `edges` or `nodes`, we dont directly know which to use most efficently.
|
48
|
+
# We can have a guess by checking if either of the associations are preloaded
|
49
|
+
return @loaded_nodes if @loaded_nodes.present?
|
50
|
+
return @loaded_edges if @loaded_edges.present?
|
51
|
+
|
52
|
+
nodes_preloaded = @connection_edges_and_nodes.parent.association(@connection_edges_and_nodes.nodes_property).loaded?
|
53
|
+
return fetch_nodes if nodes_preloaded
|
54
|
+
|
55
|
+
edges_preloaded = @connection_edges_and_nodes.parent.association(@connection_edges_and_nodes.edges_property).loaded?
|
56
|
+
return fetch_edges if edges_preloaded
|
57
|
+
|
58
|
+
fetch_nodes
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module GraphQLIncludable
|
2
|
+
module Relay
|
3
|
+
class EdgeWithNodeConnectionType
|
4
|
+
def self.create_type(
|
5
|
+
wrapped_type, edge_to_node_property:,
|
6
|
+
edge_type: wrapped_type.edge_type, edge_class: EdgeWithNode, nodes_field: GraphQL::Relay::ConnectionType.default_nodes_field, &block
|
7
|
+
)
|
8
|
+
custom_edge_class = edge_class
|
9
|
+
|
10
|
+
GraphQL::ObjectType.define do
|
11
|
+
name("#{wrapped_type.name}Connection")
|
12
|
+
description("The connection type for #{wrapped_type.name}.")
|
13
|
+
field :edges, types[edge_type], "A list of edges.", edge_class: custom_edge_class, property: :fetch_edges, edge_to_node_property: edge_to_node_property
|
14
|
+
|
15
|
+
if nodes_field
|
16
|
+
field :nodes, types[wrapped_type], "A list of nodes.", property: :fetch_nodes
|
17
|
+
end
|
18
|
+
|
19
|
+
field :pageInfo, !GraphQL::Relay::PageInfo, "Information to aid in pagination.", property: :page_info
|
20
|
+
block && instance_eval(&block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module GraphQLIncludable
|
2
|
+
module Relay
|
3
|
+
module Instrumentation
|
4
|
+
class Connection
|
5
|
+
def instrument(_type, field)
|
6
|
+
return field unless field.connection?
|
7
|
+
|
8
|
+
required_metadata = [:edges_property, :nodes_property]
|
9
|
+
requires_instrumentation = required_metadata.any? { |key| field.metadata.key?(key) }
|
10
|
+
|
11
|
+
return field unless requires_instrumentation
|
12
|
+
raise ArgumentError unless required_metadata.all? { |key| field.metadata.key?(key) }
|
13
|
+
|
14
|
+
raise ArgumentError if field.property.present? # TODO: Check for resolve proc too
|
15
|
+
|
16
|
+
edges_prop = field.metadata[:edges_property]
|
17
|
+
nodes_prop = field.metadata[:nodes_property]
|
18
|
+
|
19
|
+
_original_resolve = field.resolve_proc
|
20
|
+
new_resolve_proc = ->(obj, args, ctx) do
|
21
|
+
ConnectionEdgesAndNodes.new(obj, edges_prop, nodes_prop)
|
22
|
+
end
|
23
|
+
|
24
|
+
field.redefine { resolve(new_resolve_proc) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
GraphQL::Relay::BaseConnection.register_connection_implementation(GraphQLIncludable::Relay::ConnectionEdgesAndNodes, GraphQLIncludable::Relay::EdgeWithNodeConnection)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,4 +1,48 @@
|
|
1
1
|
module GraphQLIncludable
|
2
|
+
class IncludesManager
|
3
|
+
def initialize(parent_attribute)
|
4
|
+
@parent_attribute = parent_attribute
|
5
|
+
@included_children = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_child_include(association)
|
9
|
+
return @included_children[association.name] if @included_children.key?(association.name)
|
10
|
+
|
11
|
+
manager = IncludesManager.new(association.name)
|
12
|
+
@included_children[association.name] = manager
|
13
|
+
manager
|
14
|
+
end
|
15
|
+
|
16
|
+
def empty?
|
17
|
+
@included_children.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def includes
|
21
|
+
child_includes = {}
|
22
|
+
child_includes_arr = []
|
23
|
+
@included_children.each do |key, value|
|
24
|
+
if value.empty?
|
25
|
+
child_includes_arr << key
|
26
|
+
else
|
27
|
+
includes = value.includes
|
28
|
+
if includes.is_a?(Array)
|
29
|
+
child_includes_arr += includes
|
30
|
+
else
|
31
|
+
child_includes.merge!(includes)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if child_includes_arr.present?
|
37
|
+
child_includes_arr << child_includes if child_includes.present?
|
38
|
+
child_includes = child_includes_arr
|
39
|
+
end
|
40
|
+
|
41
|
+
return child_includes if @parent_attribute.nil?
|
42
|
+
{ @parent_attribute => child_includes }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
2
46
|
class Resolver
|
3
47
|
# Returns the first node in the tree which returns a specific type
|
4
48
|
def self.find_node_by_return_type(node, desired_return_type)
|
@@ -19,52 +63,80 @@ module GraphQLIncludable
|
|
19
63
|
# Translate a node's selections into `includes` values
|
20
64
|
# Combine and format children values
|
21
65
|
# Noop on nodes that don't return AR (so no associations to include)
|
22
|
-
def self.includes_for_node(node)
|
66
|
+
def self.includes_for_node(node, includes_manager)
|
23
67
|
return_model = node_return_model(node)
|
24
68
|
return [] if return_model.blank?
|
25
69
|
|
26
|
-
includes = []
|
27
70
|
children = node.scoped_children[node.return_type.unwrap]
|
28
|
-
nested_includes = {}
|
29
71
|
|
30
72
|
children.each_value do |child_node|
|
31
|
-
|
73
|
+
includes_for_child(child_node, return_model, includes_manager)
|
74
|
+
end
|
75
|
+
end
|
32
76
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
77
|
+
def self.includes_from_connection(node, parent_model, associations_from_parent_model, includes_manager)
|
78
|
+
return unless node.return_type.fields['edges'].edge_class <= GraphQLIncludable::Relay::EdgeWithNode # TODO: Possibly basic support for connections with only nodes
|
79
|
+
|
80
|
+
edges_association = associations_from_parent_model[:edges]
|
81
|
+
nodes_association = associations_from_parent_model[:nodes]
|
82
|
+
|
83
|
+
edge_node_attribute = node.return_type.fields['edges'].metadata[:edge_to_node_property]
|
84
|
+
edge_model = edges_association.klass
|
85
|
+
edge_to_node_association = edge_model.reflect_on_association(edge_node_attribute)
|
86
|
+
node_model = edge_to_node_association.klass
|
87
|
+
|
88
|
+
connection_children = node.scoped_children[node.return_type.unwrap]
|
89
|
+
connection_children.each_value do |connection_node|
|
90
|
+
# connection_field {
|
91
|
+
# pageInfo {...}
|
92
|
+
# nodes {
|
93
|
+
# node_model_field ...
|
94
|
+
# }
|
95
|
+
# edges {
|
96
|
+
# edge_model_field ...
|
97
|
+
# node {
|
98
|
+
# node_model_field ...
|
99
|
+
# }
|
100
|
+
# }
|
101
|
+
# }
|
102
|
+
|
103
|
+
if connection_node.name == 'edges'
|
104
|
+
edges_includes_manager = includes_manager.add_child_include(edges_association)
|
105
|
+
|
106
|
+
edge_children = connection_node.scoped_children[connection_node.return_type.unwrap]
|
107
|
+
edge_children.each_value do |edge_child_node|
|
108
|
+
if edge_child_node.name == 'node'
|
109
|
+
node_includes_manager = edges_includes_manager.add_child_include(edge_to_node_association)
|
110
|
+
|
111
|
+
node_children = edge_child_node.scoped_children[edge_child_node.return_type.unwrap]
|
112
|
+
node_children.each_value do |node_child_node|
|
113
|
+
includes_for_child(node_child_node, node_model, node_includes_manager)
|
114
|
+
end
|
115
|
+
else
|
116
|
+
includes_for_child(edge_child_node, edge_model, edges_includes_manager)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
elsif connection_node.name == 'nodes'
|
120
|
+
nodes_includes_manager = includes_manager.add_child_include(nodes_association)
|
121
|
+
node_children = connection_node.scoped_children[connection_node.return_type.unwrap]
|
122
|
+
node_children.each_value do |node_child_node|
|
123
|
+
includes_for_child(node_child_node, node_model, nodes_includes_manager)
|
124
|
+
end
|
39
125
|
end
|
40
126
|
end
|
41
|
-
|
42
|
-
includes << nested_includes if nested_includes.present?
|
43
|
-
includes.uniq
|
44
127
|
end
|
45
128
|
|
46
|
-
def self.includes_for_child(node, parent_model)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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?)
|
129
|
+
def self.includes_for_child(node, parent_model, includes_manager)
|
130
|
+
associations = possible_associations(node, parent_model)
|
131
|
+
|
132
|
+
if associations.present?
|
133
|
+
if node_is_relay_connection?(node)
|
134
|
+
includes_from_connection(node, parent_model, associations, includes_manager)
|
135
|
+
else
|
136
|
+
association = associations[:default] # should only be one
|
137
|
+
child_includes_manager = includes_manager.add_child_include(association)
|
138
|
+
includes_for_node(node, child_includes_manager)
|
139
|
+
end
|
68
140
|
end
|
69
141
|
end
|
70
142
|
|
@@ -84,9 +156,34 @@ module GraphQLIncludable
|
|
84
156
|
rescue NameError
|
85
157
|
end
|
86
158
|
|
87
|
-
def self.
|
159
|
+
def self.node_is_relay_connection?(node)
|
160
|
+
node.return_type.unwrap.name =~ /Connection$/
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.possible_associations(node, parent_model)
|
164
|
+
attribute_names = node_possible_association_names(node)
|
165
|
+
attribute_names.transform_values { |attribute_name| figure_out_association(attribute_name, parent_model) }.compact
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.node_possible_association_names(node)
|
88
169
|
definition = node.definitions.first
|
89
|
-
|
170
|
+
|
171
|
+
association_names = {}
|
172
|
+
if node_is_relay_connection?(node)
|
173
|
+
association_names[:edges] = definition.metadata[:edges_property] if definition.metadata.key?(:edges_property)
|
174
|
+
association_names[:nodes] = definition.metadata[:nodes_property] if definition.metadata.key?(:nodes_property)
|
175
|
+
return association_names if association_names.present? # This should be an includable connection with no :property or name fallback.
|
176
|
+
end
|
177
|
+
|
178
|
+
association_names[:default] = definition.metadata[:includes] || (definition.property || definition.name).to_sym
|
179
|
+
association_names
|
180
|
+
end
|
181
|
+
|
182
|
+
def self.figure_out_association(attribute_name, parent_model)
|
183
|
+
delegated_through = includes_delegated_through(parent_model, attribute_name)
|
184
|
+
delegated_model = model_name_to_class(delegated_through.last) if delegated_through.present?
|
185
|
+
association = (delegated_model || parent_model).reflect_on_association(attribute_name)
|
186
|
+
association
|
90
187
|
end
|
91
188
|
|
92
189
|
# If method_name is delegated from base_model, return an array of
|
@@ -102,14 +199,5 @@ module GraphQLIncludable
|
|
102
199
|
end
|
103
200
|
chain
|
104
201
|
end
|
105
|
-
|
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 } } || {}
|
109
|
-
end
|
110
|
-
|
111
|
-
# def self.node_is_relay_connection?(node)
|
112
|
-
# node.return_type.unwrap.name =~ /Connection$/
|
113
|
-
# end
|
114
202
|
end
|
115
203
|
end
|
data/lib/graphql_includable.rb
CHANGED
@@ -2,19 +2,35 @@ require 'graphql'
|
|
2
2
|
require 'graphql_includable/resolver'
|
3
3
|
require 'graphql_includable/concern'
|
4
4
|
require 'graphql_includable/edge'
|
5
|
+
require 'graphql_includable/relay/edge_with_node'
|
6
|
+
require 'graphql_includable/relay/edge_with_node_connection'
|
7
|
+
require 'graphql_includable/relay/edge_with_node_connection_type'
|
8
|
+
require 'graphql_includable/relay/instrumentation/connection'
|
5
9
|
|
6
10
|
GraphQL::Field.accepts_definitions(
|
7
|
-
includes: GraphQL::Define.assign_metadata_key(:includes)
|
11
|
+
includes: GraphQL::Define.assign_metadata_key(:includes),
|
12
|
+
edges_property: GraphQL::Define.assign_metadata_key(:edges_property),
|
13
|
+
nodes_property: GraphQL::Define.assign_metadata_key(:nodes_property),
|
14
|
+
edge_to_node_property: GraphQL::Define.assign_metadata_key(:edge_to_node_property)
|
8
15
|
)
|
9
16
|
|
10
17
|
module GraphQL
|
11
18
|
class BaseType
|
12
19
|
def define_includable_connection(**kwargs, &block)
|
20
|
+
warn '[DEPRECATION] `define_includable_connection` is deprecated. Please use `define_connection_with_fetched_edge` instead.'
|
13
21
|
define_connection(
|
14
22
|
edge_class: GraphQLIncludable::Edge,
|
15
23
|
**kwargs,
|
16
24
|
&block
|
17
25
|
)
|
18
26
|
end
|
27
|
+
|
28
|
+
def define_connection_with_fetched_edge(**kwargs, &block)
|
29
|
+
GraphQLIncludable::Relay::EdgeWithNodeConnectionType.create_type(
|
30
|
+
self,
|
31
|
+
**kwargs,
|
32
|
+
&block
|
33
|
+
)
|
34
|
+
end
|
19
35
|
end
|
20
36
|
end
|
metadata
CHANGED
@@ -1,20 +1,22 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql_includable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0.alpha.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Rouse
|
8
8
|
- Josh Vickery
|
9
|
+
- Jordan Hamill
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2019-
|
13
|
+
date: 2019-08-05 00:00:00.000000000 Z
|
13
14
|
dependencies: []
|
14
15
|
description:
|
15
16
|
email:
|
16
17
|
- dan.rouse@squarefoot.com
|
17
18
|
- jvickery@squarefoot.com
|
19
|
+
- jordan@squarefoot.com
|
18
20
|
executables: []
|
19
21
|
extensions: []
|
20
22
|
extra_rdoc_files: []
|
@@ -22,6 +24,10 @@ files:
|
|
22
24
|
- lib/graphql_includable.rb
|
23
25
|
- lib/graphql_includable/concern.rb
|
24
26
|
- lib/graphql_includable/edge.rb
|
27
|
+
- lib/graphql_includable/relay/edge_with_node.rb
|
28
|
+
- lib/graphql_includable/relay/edge_with_node_connection.rb
|
29
|
+
- lib/graphql_includable/relay/edge_with_node_connection_type.rb
|
30
|
+
- lib/graphql_includable/relay/instrumentation/connection.rb
|
25
31
|
- lib/graphql_includable/resolver.rb
|
26
32
|
homepage: https://github.com/thesquarefoot/graphql_includable
|
27
33
|
licenses:
|
@@ -38,12 +44,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
44
|
version: '0'
|
39
45
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
46
|
requirements:
|
41
|
-
- - "
|
47
|
+
- - ">"
|
42
48
|
- !ruby/object:Gem::Version
|
43
|
-
version:
|
49
|
+
version: 1.3.1
|
44
50
|
requirements: []
|
45
51
|
rubyforge_project:
|
46
|
-
rubygems_version: 2.6.
|
52
|
+
rubygems_version: 2.6.14.1
|
47
53
|
signing_key:
|
48
54
|
specification_version: 4
|
49
55
|
summary: An ActiveSupport::Concern for GraphQL Ruby to eager-load query data
|