graphql_includable 0.4.0 → 0.5.0.beta.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 +12 -4
- data/lib/graphql_includable/edge.rb +21 -6
- data/lib/graphql_includable/new/gql_includable.rb +36 -0
- data/lib/graphql_includable/new/includes.rb +63 -0
- data/lib/graphql_includable/new/includes_builder.rb +114 -0
- data/lib/graphql_includable/new/relay/edge_with_node.rb +48 -0
- data/lib/graphql_includable/new/relay/edge_with_node_connection.rb +101 -0
- data/lib/graphql_includable/new/relay/edge_with_node_connection_type.rb +37 -0
- data/lib/graphql_includable/new/relay/instrumentation.rb +79 -0
- data/lib/graphql_includable/new/resolver.rb +177 -0
- data/lib/graphql_includable/relay/edge_with_node_connection.rb +3 -3
- data/lib/graphql_includable.rb +1 -0
- metadata +12 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 21738d44c97c02d307c624f23191471f5701e078
|
4
|
+
data.tar.gz: acacb75291ee29bc5356b577ba2df1e9933ae8ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d63dae811fa795631ab1fc7a996421c7113f72c97151fadd17dc1e76c3c9ce8ca61c8262fde791105cadd2bfb6281a2f4575d6b9645c4e09bc1648d66bd8638
|
7
|
+
data.tar.gz: da84988546493f2decef19b3b8bc92b1d02c27a1876c85b59b49e7d650d8e960500b21e85272f3dd996a9aa798e00b72d71db64062a40bc70dbd1f0b26cdd881
|
@@ -6,10 +6,18 @@ module GraphQLIncludable
|
|
6
6
|
|
7
7
|
module ClassMethods
|
8
8
|
def includes_from_graphql(ctx)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
ActiveSupport::Notifications.instrument('graphql_includable.includes_from_graphql') do |instrument|
|
10
|
+
instrument[:operation_name] = ctx.query&.operation_name
|
11
|
+
instrument[:field_name] = ctx.irep_node.name
|
12
|
+
|
13
|
+
node = Resolver.find_node_by_return_type(ctx.irep_node, name)
|
14
|
+
manager = IncludesManager.new(nil)
|
15
|
+
Resolver.includes_for_node(node, manager)
|
16
|
+
|
17
|
+
generated_includes = manager.includes
|
18
|
+
instrument[:includes] = generated_includes
|
19
|
+
includes(generated_includes)
|
20
|
+
end
|
13
21
|
rescue => e
|
14
22
|
Rails.logger.error(e)
|
15
23
|
self
|
@@ -1,22 +1,33 @@
|
|
1
1
|
# rubocop:disable Style/ConditionalAssignment
|
2
2
|
# rubocop:disable Lint/HandleExceptions
|
3
3
|
# rubocop:disable Metrics/AbcSize
|
4
|
+
# rubocop:disable Style/IfInsideElse
|
5
|
+
# rubocop:disable Metrics/MethodLength
|
4
6
|
module GraphQLIncludable
|
5
7
|
class Edge < GraphQL::Relay::Edge
|
6
8
|
def edge
|
7
9
|
return @edge if @edge
|
10
|
+
|
8
11
|
join_chain = joins_along_edge
|
9
12
|
edge_class_name = join_chain.shift
|
10
13
|
edge_class = str_to_class(edge_class_name)
|
11
14
|
|
12
|
-
|
15
|
+
parent = if self.parent.instance_of?(GraphQLIncludable::New::Relay::LazyEvaluatedNode)
|
16
|
+
parent_class = self.parent._value.class
|
17
|
+
self.parent._value
|
18
|
+
else
|
19
|
+
parent_class = self.parent.class
|
20
|
+
self.parent
|
21
|
+
end
|
22
|
+
|
23
|
+
root_association_key = class_to_str(parent_class)
|
13
24
|
unless edge_class.reflections.keys.include?(root_association_key)
|
14
25
|
is_polymorphic = true
|
15
26
|
root_association_key = edge_class.reflections.select { |_k, r| r.polymorphic? }.keys.first
|
16
27
|
end
|
17
28
|
|
18
|
-
if
|
19
|
-
root_association_search_value = parent.send(
|
29
|
+
if parent_class.delegate_cache&.key?(edge_class_name)
|
30
|
+
root_association_search_value = parent.send(parent_class.delegate_cache[edge_class_name])
|
20
31
|
else
|
21
32
|
root_association_search_value = parent
|
22
33
|
end
|
@@ -65,10 +76,14 @@ module GraphQLIncludable
|
|
65
76
|
# node.parents
|
66
77
|
# parent.nodes
|
67
78
|
edge_association_name = node.class.name.pluralize.downcase.to_sym
|
68
|
-
if parent.
|
69
|
-
parent_class =
|
79
|
+
if parent.instance_of?(GraphQLIncludable::New::Relay::LazyEvaluatedNode)
|
80
|
+
parent_class = parent._value.class
|
70
81
|
else
|
71
|
-
|
82
|
+
if parent.class.delegate_cache&.key?(edge_association_name)
|
83
|
+
parent_class = str_to_class(parent.class.delegate_cache[edge_association_name])
|
84
|
+
else
|
85
|
+
parent_class = parent.class
|
86
|
+
end
|
72
87
|
end
|
73
88
|
edge_association = parent_class.reflect_on_association(edge_association_name)
|
74
89
|
edge_joins = []
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'graphql'
|
2
|
+
require_relative 'includes_builder'
|
3
|
+
require_relative 'includes'
|
4
|
+
require_relative 'resolver'
|
5
|
+
require_relative 'relay/edge_with_node_connection_type'
|
6
|
+
require_relative 'relay/instrumentation'
|
7
|
+
|
8
|
+
module GraphQLIncludable
|
9
|
+
module New
|
10
|
+
def self.includes(ctx)
|
11
|
+
ActiveSupport::Notifications.instrument('graphql_includable.includes') do |instrument|
|
12
|
+
instrument[:operation_name] = ctx.query&.operation_name
|
13
|
+
instrument[:field_name] = ctx.irep_node.name
|
14
|
+
|
15
|
+
includes = Includes.new(nil)
|
16
|
+
resolver = Resolver.new(ctx)
|
17
|
+
resolver.includes_for_node(ctx.irep_node, includes)
|
18
|
+
generated_includes = includes.active_record_includes
|
19
|
+
instrument[:includes] = generated_includes
|
20
|
+
generated_includes
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module GraphQL
|
27
|
+
class BaseType
|
28
|
+
def new_define_connection_with_fetched_edge(**kwargs, &block)
|
29
|
+
GraphQLIncludable::New::Relay::EdgeWithNodeConnectionType.create_type(
|
30
|
+
self,
|
31
|
+
**kwargs,
|
32
|
+
&block
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module GraphQLIncludable
|
2
|
+
module New
|
3
|
+
class Includes
|
4
|
+
attr_reader :included_children
|
5
|
+
|
6
|
+
def initialize(parent_attribute)
|
7
|
+
@parent_attribute = parent_attribute
|
8
|
+
@included_children = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_child(key)
|
12
|
+
return @included_children[key] if @included_children.key?(key)
|
13
|
+
manager = Includes.new(key)
|
14
|
+
@included_children[key] = manager
|
15
|
+
manager
|
16
|
+
end
|
17
|
+
|
18
|
+
def merge_includes(includes_manager)
|
19
|
+
includes_manager.included_children.each do |key, manager|
|
20
|
+
included_children[key] = if included_children.key?(key)
|
21
|
+
included_children[key].merge_includes(manager)
|
22
|
+
else
|
23
|
+
manager
|
24
|
+
end
|
25
|
+
end
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
@included_children[key]
|
31
|
+
end
|
32
|
+
|
33
|
+
def empty?
|
34
|
+
@included_children.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def active_record_includes
|
38
|
+
child_includes = {}
|
39
|
+
child_includes_arr = []
|
40
|
+
@included_children.each do |key, value|
|
41
|
+
if value.empty?
|
42
|
+
child_includes_arr << key
|
43
|
+
else
|
44
|
+
active_record_includes = value.active_record_includes
|
45
|
+
if active_record_includes.is_a?(Array)
|
46
|
+
child_includes_arr += active_record_includes
|
47
|
+
else
|
48
|
+
child_includes.merge!(active_record_includes)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if child_includes_arr.present?
|
54
|
+
child_includes_arr << child_includes if child_includes.present?
|
55
|
+
child_includes = child_includes_arr
|
56
|
+
end
|
57
|
+
|
58
|
+
return child_includes if @parent_attribute.nil?
|
59
|
+
{ @parent_attribute => child_includes }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module GraphQLIncludable
|
2
|
+
module New
|
3
|
+
class IncludesBuilder
|
4
|
+
attr_reader :included_path, :includes
|
5
|
+
|
6
|
+
def initialize(only_one_path: true)
|
7
|
+
@only_one_path = only_one_path
|
8
|
+
@included_path = []
|
9
|
+
@includes = GraphQLIncludable::New::Includes.new(nil)
|
10
|
+
end
|
11
|
+
|
12
|
+
def includes?
|
13
|
+
@included_path.present?
|
14
|
+
end
|
15
|
+
|
16
|
+
def active_record_includes
|
17
|
+
@includes.active_record_includes
|
18
|
+
end
|
19
|
+
|
20
|
+
def path_leaf_includes
|
21
|
+
leaf_includes = @includes
|
22
|
+
included_path.each do |key|
|
23
|
+
leaf_includes = leaf_includes[key]
|
24
|
+
end
|
25
|
+
leaf_includes
|
26
|
+
end
|
27
|
+
|
28
|
+
def path(*symbols, &block)
|
29
|
+
raise ArgumentError, 'Can only add path once' if @included_path.present? && @only_one_path
|
30
|
+
|
31
|
+
if symbols.present?
|
32
|
+
first, *rest = symbols
|
33
|
+
includes = @includes.add_child(first)
|
34
|
+
rest.each do |key|
|
35
|
+
includes = includes.add_child(key)
|
36
|
+
end
|
37
|
+
else
|
38
|
+
includes = @includes
|
39
|
+
end
|
40
|
+
|
41
|
+
if block_given?
|
42
|
+
nested = GraphQLIncludable::New::IncludesBuilder.new
|
43
|
+
nested.instance_eval(&block)
|
44
|
+
symbols += nested.included_path
|
45
|
+
includes.merge_includes(nested.includes)
|
46
|
+
end
|
47
|
+
@included_path = symbols
|
48
|
+
end
|
49
|
+
|
50
|
+
def sibling_path(*symbols, &block)
|
51
|
+
if symbols.present?
|
52
|
+
first, *rest = symbols
|
53
|
+
includes = @includes.add_child(first)
|
54
|
+
rest.each do |key|
|
55
|
+
includes = includes.add_child(key)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
includes = @includes
|
59
|
+
end
|
60
|
+
|
61
|
+
return unless block_given?
|
62
|
+
nested = GraphQLIncludable::New::IncludesBuilder.new(only_one_path: false)
|
63
|
+
nested.instance_eval(&block)
|
64
|
+
includes.merge_includes(nested.includes)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class ConnectionIncludesBuilder
|
69
|
+
attr_reader :nodes_builder, :edges_builder
|
70
|
+
|
71
|
+
def initialize
|
72
|
+
@nodes_builder = IncludesBuilder.new
|
73
|
+
@edges_builder = ConnectionEdgesIncludesBuilder.new
|
74
|
+
end
|
75
|
+
|
76
|
+
def includes?
|
77
|
+
@nodes_builder.includes? || @edges_builder.includes?
|
78
|
+
end
|
79
|
+
|
80
|
+
def nodes(*symbols, &block)
|
81
|
+
@nodes_builder.path(*symbols, &block)
|
82
|
+
end
|
83
|
+
|
84
|
+
def edges(&block)
|
85
|
+
@edges_builder.instance_eval(&block)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class ConnectionEdgesIncludesBuilder
|
90
|
+
attr_reader :builder, :node_builder
|
91
|
+
|
92
|
+
def initialize
|
93
|
+
@builder = IncludesBuilder.new
|
94
|
+
@node_builder = IncludesBuilder.new
|
95
|
+
end
|
96
|
+
|
97
|
+
def includes?
|
98
|
+
@builder.includes? && @node_builder.includes?
|
99
|
+
end
|
100
|
+
|
101
|
+
def path(*symbols, &block)
|
102
|
+
@builder.path(*symbols, &block)
|
103
|
+
end
|
104
|
+
|
105
|
+
def sibling_path(*symbols, &block)
|
106
|
+
@builder.sibling_path(*symbols, &block)
|
107
|
+
end
|
108
|
+
|
109
|
+
def node(*symbols, &block)
|
110
|
+
@node_builder.path(*symbols, &block)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
GraphQL::Field.accepts_definitions(
|
2
|
+
##
|
3
|
+
# Define how to get from an edge Active Record model to the node Active Record model
|
4
|
+
connection_properties: GraphQL::Define.assign_metadata_key(:connection_properties),
|
5
|
+
|
6
|
+
##
|
7
|
+
# Define a resolver for connection edges records
|
8
|
+
resolve_edges: GraphQL::Define.assign_metadata_key(:resolve_edges),
|
9
|
+
|
10
|
+
##
|
11
|
+
# Define a resolver for connection nodes records
|
12
|
+
resolve_nodes: GraphQL::Define.assign_metadata_key(:resolve_nodes),
|
13
|
+
|
14
|
+
##
|
15
|
+
# Internally used to mark a connection type that has a fetched edge
|
16
|
+
_new_includable_connection_marker: GraphQL::Define.assign_metadata_key(:_new_includable_connection_marker)
|
17
|
+
)
|
18
|
+
|
19
|
+
module GraphQLIncludable
|
20
|
+
module New
|
21
|
+
module Relay
|
22
|
+
class EdgeWithNode < GraphQL::Relay::Edge
|
23
|
+
def initialize(node, connection)
|
24
|
+
@edge = node
|
25
|
+
@edge_to_node = ->() { connection.edge_to_node(@edge) }
|
26
|
+
super(nil, connection)
|
27
|
+
end
|
28
|
+
|
29
|
+
def node
|
30
|
+
@node ||= @edge_to_node.call
|
31
|
+
@node
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_missing(method_name, *args, &block)
|
35
|
+
if @edge.respond_to?(method_name)
|
36
|
+
@edge.send(method_name, *args, &block)
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def respond_to_missing?(method_name, include_private = false)
|
43
|
+
@edge.respond_to?(method_name) || super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module GraphQLIncludable
|
2
|
+
module New
|
3
|
+
module Relay
|
4
|
+
class ConnectionEdgesAndNodes
|
5
|
+
attr_reader :parent, :args, :ctx, :edges_property, :nodes_property, :edge_to_node_property
|
6
|
+
attr_reader :edges_resolver, :nodes_resolver
|
7
|
+
|
8
|
+
# rubocop:disable Metrics/ParameterLists
|
9
|
+
def initialize(parent, args, ctx,
|
10
|
+
edges_property, nodes_property, edge_to_node_property,
|
11
|
+
edges_resolver, nodes_resolver)
|
12
|
+
@parent = parent
|
13
|
+
@args = args
|
14
|
+
@ctx = ctx
|
15
|
+
@edges_property = edges_property # optional
|
16
|
+
@nodes_property = nodes_property # optional
|
17
|
+
@edge_to_node_property = edge_to_node_property
|
18
|
+
@edges_resolver = edges_resolver
|
19
|
+
@nodes_resolver = nodes_resolver
|
20
|
+
end
|
21
|
+
# rubocop:enable Metrics/ParameterLists
|
22
|
+
end
|
23
|
+
|
24
|
+
class EdgeWithNodeConnection < GraphQL::Relay::RelationConnection
|
25
|
+
def initialize(nodes, *args, &block)
|
26
|
+
@edges_and_nodes = nodes
|
27
|
+
@loaded_nodes = nil
|
28
|
+
@loaded_edges = nil
|
29
|
+
super(nil, *args, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def edge_nodes
|
33
|
+
raise 'This should not be called from a EdgeWithNodeConnectionType'
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch_edges
|
37
|
+
# This context is used within Resolver for connections
|
38
|
+
ctx.namespace(:gql_includable)[:resolving] = :edges
|
39
|
+
@loaded_edges ||= @edges_and_nodes.edges_resolver.call(@edges_and_nodes.parent, args, ctx)
|
40
|
+
ctx.namespace(:gql_includable)[:resolving] = nil
|
41
|
+
# Set nodes to make underlying BaseConnection work
|
42
|
+
@nodes = @loaded_edges
|
43
|
+
@loaded_edges
|
44
|
+
end
|
45
|
+
|
46
|
+
def fetch_nodes
|
47
|
+
# This context is used within Resolver for connections
|
48
|
+
ctx.namespace(:gql_includable)[:resolving] = :nodes
|
49
|
+
@loaded_nodes ||= @edges_and_nodes.nodes_resolver.call(@edges_and_nodes.parent, args, ctx)
|
50
|
+
ctx.namespace(:gql_includable)[:resolving] = nil
|
51
|
+
# Set nodes to make underlying BaseConnection work
|
52
|
+
@nodes = @loaded_nodes
|
53
|
+
@loaded_nodes
|
54
|
+
end
|
55
|
+
|
56
|
+
def page_info
|
57
|
+
@nodes = determine_page_info_nodes
|
58
|
+
super
|
59
|
+
end
|
60
|
+
|
61
|
+
def edge_to_node(edge)
|
62
|
+
edge.public_send(@edges_and_nodes.edge_to_node_property)
|
63
|
+
end
|
64
|
+
|
65
|
+
def total_count
|
66
|
+
@nodes = determine_page_info_nodes
|
67
|
+
@nodes.size
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def args
|
73
|
+
@edges_and_nodes.args
|
74
|
+
end
|
75
|
+
|
76
|
+
def ctx
|
77
|
+
@edges_and_nodes.ctx
|
78
|
+
end
|
79
|
+
|
80
|
+
def determine_page_info_nodes
|
81
|
+
# If the query asks for `pageInfo` before `edges` or `nodes`, we dont directly know which to use most
|
82
|
+
# efficently. We can have a guess by checking if either of the associations are preloaded
|
83
|
+
return @loaded_nodes if @loaded_nodes.present?
|
84
|
+
return @loaded_edges if @loaded_edges.present?
|
85
|
+
|
86
|
+
if @edges_and_nodes.nodes_property.present?
|
87
|
+
nodes_preloaded = @edges_and_nodes.parent.association(@edges_and_nodes.nodes_property).loaded?
|
88
|
+
return fetch_nodes if nodes_preloaded
|
89
|
+
end
|
90
|
+
|
91
|
+
if @edges_and_nodes.edges_property.present?
|
92
|
+
edges_preloaded = @edges_and_nodes.parent.association(@edges_and_nodes.edges_property).loaded?
|
93
|
+
return fetch_edges if edges_preloaded
|
94
|
+
end
|
95
|
+
|
96
|
+
fetch_nodes
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'edge_with_node'
|
2
|
+
|
3
|
+
module GraphQLIncludable
|
4
|
+
module New
|
5
|
+
module Relay
|
6
|
+
class EdgeWithNodeConnectionType
|
7
|
+
def self.create_type(
|
8
|
+
wrapped_type,
|
9
|
+
edge_type: wrapped_type.edge_type, edge_class: EdgeWithNode,
|
10
|
+
nodes_field: GraphQL::Relay::ConnectionType.default_nodes_field, &block
|
11
|
+
)
|
12
|
+
custom_edge_class = edge_class
|
13
|
+
|
14
|
+
GraphQL::ObjectType.define do
|
15
|
+
name("#{wrapped_type.name}Connection")
|
16
|
+
description("The connection type for #{wrapped_type.name}.")
|
17
|
+
|
18
|
+
field :totalCount, types.Int, 'Total count.', property: :total_count
|
19
|
+
|
20
|
+
field :edges, types[edge_type], 'A list of edges.' do
|
21
|
+
edge_class custom_edge_class
|
22
|
+
property :fetch_edges
|
23
|
+
_new_includable_connection_marker true
|
24
|
+
end
|
25
|
+
|
26
|
+
if nodes_field
|
27
|
+
field :nodes, types[wrapped_type], 'A list of nodes.', property: :fetch_nodes
|
28
|
+
end
|
29
|
+
|
30
|
+
field :pageInfo, !GraphQL::Relay::PageInfo, 'Information to aid in pagination.', property: :page_info
|
31
|
+
block && instance_eval(&block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative 'edge_with_node_connection'
|
2
|
+
|
3
|
+
module GraphQLIncludable
|
4
|
+
module New
|
5
|
+
module Relay
|
6
|
+
class Instrumentation
|
7
|
+
# rubocop:disable Metrics/AbcSize
|
8
|
+
def instrument(_type, field)
|
9
|
+
return field unless edge_with_node_connection?(field)
|
10
|
+
|
11
|
+
raise ArgumentError, 'Connection does not support fetching using :property' if field.property.present?
|
12
|
+
|
13
|
+
is_proc_based = proc_based?(field)
|
14
|
+
|
15
|
+
validate!(field, is_proc_based)
|
16
|
+
properties = field.metadata[:connection_properties]
|
17
|
+
edge_to_node_property = properties[:edge_to_node]
|
18
|
+
edges_prop = properties[:edges]
|
19
|
+
nodes_prop = properties[:nodes]
|
20
|
+
|
21
|
+
if is_proc_based
|
22
|
+
edges_resolver = field.metadata[:resolve_edges]
|
23
|
+
nodes_resolver = field.metadata[:resolve_nodes]
|
24
|
+
else
|
25
|
+
# Use the edges and nodes symbols from the incldues pattern as the propeties to fetch
|
26
|
+
edges_resolver = ->(obj, _args, _ctx) { obj.public_send(edges_prop) }
|
27
|
+
nodes_resolver = ->(obj, _args, _ctx) { obj.public_send(nodes_prop) }
|
28
|
+
end
|
29
|
+
|
30
|
+
_original_resolve = field.resolve_proc
|
31
|
+
new_resolve_proc = ->(obj, args, ctx) do
|
32
|
+
ConnectionEdgesAndNodes.new(obj, args, ctx,
|
33
|
+
edges_prop, nodes_prop, edge_to_node_property,
|
34
|
+
edges_resolver, nodes_resolver)
|
35
|
+
end
|
36
|
+
|
37
|
+
field.redefine { resolve(new_resolve_proc) }
|
38
|
+
end
|
39
|
+
# rubocop:enable Metrics/AbcSize
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def edge_with_node_connection?(field)
|
44
|
+
field.connection? && field.type.fields['edges'].metadata.key?(:_new_includable_connection_marker)
|
45
|
+
end
|
46
|
+
|
47
|
+
def proc_based?(field)
|
48
|
+
required_metadata = [:resolve_edges, :resolve_nodes]
|
49
|
+
has_a_resolver = required_metadata.any? { |key| field.metadata.key?(key) }
|
50
|
+
|
51
|
+
return false unless has_a_resolver
|
52
|
+
unless required_metadata.all? { |key| field.metadata.key?(key) }
|
53
|
+
raise ArgumentError, "Missing one of #{required_metadata}"
|
54
|
+
end
|
55
|
+
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate!(field, is_proc_based)
|
60
|
+
unless field.metadata.key?(:connection_properties)
|
61
|
+
raise ArgumentError, 'Missing connection_properties definition for field'
|
62
|
+
end
|
63
|
+
properties = field.metadata[:connection_properties]
|
64
|
+
unless properties.is_a?(Hash)
|
65
|
+
raise ArgumentError, 'Connection includes must be a hash containing :edges and :nodes keys'
|
66
|
+
end
|
67
|
+
raise ArgumentError, 'Missing :nodes' unless is_proc_based || properties.key?(:nodes)
|
68
|
+
raise ArgumentError, 'Missing :edges' unless is_proc_based || properties.key?(:edges)
|
69
|
+
raise ArgumentError, 'Missing :edge_to_node' unless properties.key?(:edge_to_node)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
GraphQL::Relay::BaseConnection.register_connection_implementation(
|
74
|
+
GraphQLIncludable::New::Relay::ConnectionEdgesAndNodes,
|
75
|
+
GraphQLIncludable::New::Relay::EdgeWithNodeConnection
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
GraphQL::Field.accepts_definitions(
|
2
|
+
##
|
3
|
+
# Define Active Record includes for a field
|
4
|
+
new_includes: GraphQL::Define.assign_metadata_key(:new_includes)
|
5
|
+
)
|
6
|
+
|
7
|
+
module GraphQLIncludable
|
8
|
+
module New
|
9
|
+
class Resolver
|
10
|
+
def initialize(ctx)
|
11
|
+
@root_ctx = ctx
|
12
|
+
end
|
13
|
+
|
14
|
+
def includes_for_node(node, includes)
|
15
|
+
return includes_for_top_level_connection(node, includes) if node.definition.connection?
|
16
|
+
|
17
|
+
children = node.scoped_children[node.return_type.unwrap]
|
18
|
+
children.each_value do |child_node|
|
19
|
+
includes_for_child(child_node, includes)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def includes_for_child(node, includes)
|
26
|
+
return includes_for_connection(node, includes) if node.definition.connection?
|
27
|
+
|
28
|
+
builder = build_includes(node)
|
29
|
+
return unless builder&.includes?
|
30
|
+
|
31
|
+
includes.merge_includes(builder.includes)
|
32
|
+
|
33
|
+
# Determine which [nested] child Includes manager to send to the children
|
34
|
+
child_includes = builder.path_leaf_includes
|
35
|
+
|
36
|
+
children = node.scoped_children[node.return_type.unwrap]
|
37
|
+
children.each_value do |child_node|
|
38
|
+
includes_for_child(child_node, child_includes)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# rubocop:disable Metrics/AbcSize
|
43
|
+
# rubocop:disable Metrics/MethodLength
|
44
|
+
def includes_for_connection(node, includes)
|
45
|
+
builder = build_connection_includes(node)
|
46
|
+
return unless builder&.includes?
|
47
|
+
|
48
|
+
connection_children = node.scoped_children[node.return_type.unwrap]
|
49
|
+
connection_children.each_value do |connection_node|
|
50
|
+
# connection_field {
|
51
|
+
# totalCount
|
52
|
+
# pageInfo {...}
|
53
|
+
# nodes {
|
54
|
+
# node_model_field ...
|
55
|
+
# }
|
56
|
+
# edges {
|
57
|
+
# edge_model_field ...
|
58
|
+
# node {
|
59
|
+
# node_model_field ...
|
60
|
+
# }
|
61
|
+
# }
|
62
|
+
# }
|
63
|
+
|
64
|
+
if connection_node.name == 'edges'
|
65
|
+
edges_includes_builder = builder.edges_builder.builder
|
66
|
+
includes.merge_includes(edges_includes_builder.includes)
|
67
|
+
edges_includes = edges_includes_builder.path_leaf_includes
|
68
|
+
|
69
|
+
edge_children = connection_node.scoped_children[connection_node.return_type.unwrap]
|
70
|
+
edge_children.each_value do |edge_child_node|
|
71
|
+
if edge_child_node.name == 'node'
|
72
|
+
node_includes_builder = builder.edges_builder.node_builder
|
73
|
+
edges_includes.merge_includes(node_includes_builder.includes)
|
74
|
+
edge_node_includes = node_includes_builder.path_leaf_includes
|
75
|
+
|
76
|
+
node_children = edge_child_node.scoped_children[edge_child_node.return_type.unwrap]
|
77
|
+
node_children.each_value do |node_child_node|
|
78
|
+
includes_for_child(node_child_node, edge_node_includes)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
includes_for_child(edge_child_node, edges_includes)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
elsif connection_node.name == 'nodes'
|
85
|
+
nodes_includes_builder = builder.nodes_builder
|
86
|
+
includes.merge_includes(nodes_includes_builder.includes)
|
87
|
+
nodes_includes = nodes_includes_builder.path_leaf_includes
|
88
|
+
|
89
|
+
node_children = connection_node.scoped_children[connection_node.return_type.unwrap]
|
90
|
+
node_children.each_value do |node_child_node|
|
91
|
+
includes_for_child(node_child_node, nodes_includes)
|
92
|
+
end
|
93
|
+
elsif connection_node.name == 'totalCount'
|
94
|
+
# Handled using `.size`
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Special case:
|
100
|
+
# When includes_for_node is called within a connection resolver, there is no need to use that field's nodes/edges
|
101
|
+
# includes, only edge_to_node includes
|
102
|
+
def includes_for_top_level_connection(node, includes)
|
103
|
+
connection_children = node.scoped_children[node.return_type.unwrap]
|
104
|
+
top_level_being_resolved = @root_ctx.namespace(:gql_includable)[:resolving]
|
105
|
+
|
106
|
+
if top_level_being_resolved == :edges
|
107
|
+
builder = build_connection_includes(node)
|
108
|
+
return unless builder&.edges_builder&.node_builder&.includes?
|
109
|
+
|
110
|
+
edges_node = connection_children['edges']
|
111
|
+
edges_includes = includes
|
112
|
+
|
113
|
+
edge_children = edges_node.scoped_children[edges_node.return_type.unwrap]
|
114
|
+
edge_children.each_value do |edge_child_node|
|
115
|
+
if edge_child_node.name == 'node'
|
116
|
+
node_includes_builder = builder.edges_builder.node_builder
|
117
|
+
edges_includes.merge_includes(node_includes_builder.includes)
|
118
|
+
edge_node_includes = node_includes_builder.path_leaf_includes
|
119
|
+
|
120
|
+
node_children = edge_child_node.scoped_children[edge_child_node.return_type.unwrap]
|
121
|
+
node_children.each_value do |node_child_node|
|
122
|
+
includes_for_child(node_child_node, edge_node_includes)
|
123
|
+
end
|
124
|
+
else
|
125
|
+
includes_for_child(edge_child_node, edges_includes)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
else
|
129
|
+
nodes_node = connection_children['nodes']
|
130
|
+
return unless nodes_node.present?
|
131
|
+
nodes_includes = includes
|
132
|
+
|
133
|
+
node_children = nodes_node.scoped_children[nodes_node.return_type.unwrap]
|
134
|
+
node_children.each_value do |node_child_node|
|
135
|
+
includes_for_child(node_child_node, nodes_includes)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
# rubocop:enable Metrics/MethodLength
|
140
|
+
# rubocop:enable Metrics/AbcSize
|
141
|
+
|
142
|
+
def build_includes(node)
|
143
|
+
includes_meta = node.definition.metadata[:new_includes]
|
144
|
+
return nil if includes_meta.blank?
|
145
|
+
|
146
|
+
builder = GraphQLIncludable::New::IncludesBuilder.new
|
147
|
+
|
148
|
+
if includes_meta.is_a?(Proc)
|
149
|
+
if includes_meta.arity == 2
|
150
|
+
args_for_field = @root_ctx.query.arguments_for(node, node.definition)
|
151
|
+
builder.instance_exec(args_for_field, @root_ctx, &includes_meta)
|
152
|
+
else
|
153
|
+
builder.instance_exec(&includes_meta)
|
154
|
+
end
|
155
|
+
else
|
156
|
+
builder.path(includes_meta)
|
157
|
+
end
|
158
|
+
|
159
|
+
builder
|
160
|
+
end
|
161
|
+
|
162
|
+
def build_connection_includes(node)
|
163
|
+
includes_meta = node.definition.metadata[:new_includes]
|
164
|
+
return nil if includes_meta.blank?
|
165
|
+
|
166
|
+
builder = GraphQLIncludable::New::ConnectionIncludesBuilder.new
|
167
|
+
if includes_meta.arity == 2
|
168
|
+
args_for_field = @root_ctx.query.arguments_for(node, node.definition)
|
169
|
+
builder.instance_exec(args_for_field, @root_ctx, &includes_meta)
|
170
|
+
else
|
171
|
+
builder.instance_exec(&includes_meta)
|
172
|
+
end
|
173
|
+
builder
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -47,7 +47,7 @@ module GraphQLIncludable
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def page_info
|
50
|
-
@nodes =
|
50
|
+
@nodes = determine_page_info_nodes
|
51
51
|
super
|
52
52
|
end
|
53
53
|
|
@@ -56,7 +56,7 @@ module GraphQLIncludable
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def total_count
|
59
|
-
@nodes =
|
59
|
+
@nodes = determine_page_info_nodes
|
60
60
|
@nodes.size
|
61
61
|
end
|
62
62
|
|
@@ -70,7 +70,7 @@ module GraphQLIncludable
|
|
70
70
|
@edges_and_nodes.ctx
|
71
71
|
end
|
72
72
|
|
73
|
-
def
|
73
|
+
def determine_page_info_nodes
|
74
74
|
# If the query asks for `pageInfo` before `edges` or `nodes`, we dont directly know which to use most
|
75
75
|
# efficently. We can have a guess by checking if either of the associations are preloaded
|
76
76
|
return @loaded_nodes if @loaded_nodes.present?
|
data/lib/graphql_includable.rb
CHANGED
@@ -6,6 +6,7 @@ require 'graphql_includable/relay/edge_with_node'
|
|
6
6
|
require 'graphql_includable/relay/edge_with_node_connection'
|
7
7
|
require 'graphql_includable/relay/edge_with_node_connection_type'
|
8
8
|
require 'graphql_includable/relay/instrumentation/connection'
|
9
|
+
require 'graphql_includable/new/gql_includable'
|
9
10
|
|
10
11
|
GraphQL::Field.accepts_definitions(
|
11
12
|
includes: GraphQL::Define.assign_metadata_key(:includes),
|
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.
|
4
|
+
version: 0.5.0.beta.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Rouse
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2019-08-
|
13
|
+
date: 2019-08-28 00:00:00.000000000 Z
|
14
14
|
dependencies: []
|
15
15
|
description:
|
16
16
|
email:
|
@@ -24,6 +24,14 @@ files:
|
|
24
24
|
- lib/graphql_includable.rb
|
25
25
|
- lib/graphql_includable/concern.rb
|
26
26
|
- lib/graphql_includable/edge.rb
|
27
|
+
- lib/graphql_includable/new/gql_includable.rb
|
28
|
+
- lib/graphql_includable/new/includes.rb
|
29
|
+
- lib/graphql_includable/new/includes_builder.rb
|
30
|
+
- lib/graphql_includable/new/relay/edge_with_node.rb
|
31
|
+
- lib/graphql_includable/new/relay/edge_with_node_connection.rb
|
32
|
+
- lib/graphql_includable/new/relay/edge_with_node_connection_type.rb
|
33
|
+
- lib/graphql_includable/new/relay/instrumentation.rb
|
34
|
+
- lib/graphql_includable/new/resolver.rb
|
27
35
|
- lib/graphql_includable/relay/edge_with_node.rb
|
28
36
|
- lib/graphql_includable/relay/edge_with_node_connection.rb
|
29
37
|
- lib/graphql_includable/relay/edge_with_node_connection_type.rb
|
@@ -44,9 +52,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
52
|
version: '0'
|
45
53
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
54
|
requirements:
|
47
|
-
- - "
|
55
|
+
- - ">"
|
48
56
|
- !ruby/object:Gem::Version
|
49
|
-
version:
|
57
|
+
version: 1.3.1
|
50
58
|
requirements: []
|
51
59
|
rubyforge_project:
|
52
60
|
rubygems_version: 2.6.14.1
|