graphql_includable 0.5.0 → 1.0.0.beta.0

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.
@@ -1,69 +0,0 @@
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 dig(*args)
34
- args = args[0] if args.length == 1 && args[0].is_a?(Array)
35
- return @included_children if args.empty?
36
- @included_children.dig(*args)
37
- end
38
-
39
- def empty?
40
- @included_children.empty?
41
- end
42
-
43
- def active_record_includes
44
- child_includes = {}
45
- child_includes_arr = []
46
- @included_children.each do |key, value|
47
- if value.empty?
48
- child_includes_arr << key
49
- else
50
- active_record_includes = value.active_record_includes
51
- if active_record_includes.is_a?(Array)
52
- child_includes_arr += active_record_includes
53
- else
54
- child_includes.merge!(active_record_includes)
55
- end
56
- end
57
- end
58
-
59
- if child_includes_arr.present?
60
- child_includes_arr << child_includes if child_includes.present?
61
- child_includes = child_includes_arr
62
- end
63
-
64
- return child_includes if @parent_attribute.nil?
65
- { @parent_attribute => child_includes }
66
- end
67
- end
68
- end
69
- end
@@ -1,110 +0,0 @@
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
- @includes.dig(included_path)
22
- end
23
-
24
- def path(*symbols, &block)
25
- raise ArgumentError, 'Can only add path once' if @included_path.present? && @only_one_path
26
-
27
- if symbols.present?
28
- first, *rest = symbols
29
- includes = @includes.add_child(first)
30
- rest.each do |key|
31
- includes = includes.add_child(key)
32
- end
33
- else
34
- includes = @includes
35
- end
36
-
37
- if block_given?
38
- nested = GraphQLIncludable::New::IncludesBuilder.new
39
- nested.instance_eval(&block)
40
- symbols += nested.included_path
41
- includes.merge_includes(nested.includes)
42
- end
43
- @included_path = symbols
44
- end
45
-
46
- def sibling_path(*symbols, &block)
47
- if symbols.present?
48
- first, *rest = symbols
49
- includes = @includes.add_child(first)
50
- rest.each do |key|
51
- includes = includes.add_child(key)
52
- end
53
- else
54
- includes = @includes
55
- end
56
-
57
- return unless block_given?
58
- nested = GraphQLIncludable::New::IncludesBuilder.new(only_one_path: false)
59
- nested.instance_eval(&block)
60
- includes.merge_includes(nested.includes)
61
- end
62
- end
63
-
64
- class ConnectionIncludesBuilder
65
- attr_reader :nodes_builder, :edges_builder
66
-
67
- def initialize
68
- @nodes_builder = IncludesBuilder.new
69
- @edges_builder = ConnectionEdgesIncludesBuilder.new
70
- end
71
-
72
- def includes?
73
- @nodes_builder.includes? || @edges_builder.includes?
74
- end
75
-
76
- def nodes(*symbols, &block)
77
- @nodes_builder.path(*symbols, &block)
78
- end
79
-
80
- def edges(&block)
81
- @edges_builder.instance_eval(&block)
82
- end
83
- end
84
-
85
- class ConnectionEdgesIncludesBuilder
86
- attr_reader :builder, :node_builder
87
-
88
- def initialize
89
- @builder = IncludesBuilder.new
90
- @node_builder = IncludesBuilder.new
91
- end
92
-
93
- def includes?
94
- @builder.includes? && @node_builder.includes?
95
- end
96
-
97
- def path(*symbols, &block)
98
- @builder.path(*symbols, &block)
99
- end
100
-
101
- def sibling_path(*symbols, &block)
102
- @builder.sibling_path(*symbols, &block)
103
- end
104
-
105
- def node(*symbols, &block)
106
- @node_builder.path(*symbols, &block)
107
- end
108
- end
109
- end
110
- end
@@ -1,48 +0,0 @@
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
@@ -1,101 +0,0 @@
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
@@ -1,37 +0,0 @@
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
@@ -1,79 +0,0 @@
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
@@ -1,203 +0,0 @@
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
- definition_override = node_definition_override(node, child_node)
20
- includes_for_child(child_node, includes, definition_override)
21
- end
22
- end
23
-
24
- private
25
-
26
- def includes_for_child(node, includes, definition_override)
27
- return includes_for_connection(node, includes, definition_override) if node.definition.connection?
28
-
29
- builder = build_includes(node, definition_override)
30
- return unless builder.present?
31
- includes.merge_includes(builder.includes) unless builder.includes.empty?
32
-
33
- return unless builder.includes?
34
-
35
- # Determine which [nested] child Includes manager to send to the children
36
- child_includes = includes.dig(builder.included_path)
37
-
38
- children = node.scoped_children[node.return_type.unwrap]
39
- children.each_value do |child_node|
40
- definition_override = node_definition_override(node, child_node)
41
- includes_for_child(child_node, child_includes, definition_override)
42
- end
43
- end
44
-
45
- # rubocop:disable Metrics/AbcSize
46
- # rubocop:disable Metrics/MethodLength
47
- def includes_for_connection(node, includes, definition_override)
48
- builder = build_connection_includes(node, definition_override)
49
- return unless builder&.includes?
50
-
51
- connection_children = node.scoped_children[node.return_type.unwrap]
52
- connection_children.each_value do |connection_node|
53
- # connection_field {
54
- # totalCount
55
- # pageInfo {...}
56
- # nodes {
57
- # node_model_field ...
58
- # }
59
- # edges {
60
- # edge_model_field ...
61
- # node {
62
- # node_model_field ...
63
- # }
64
- # }
65
- # }
66
-
67
- if connection_node.name == 'edges'
68
- edges_includes_builder = builder.edges_builder.builder
69
- includes.merge_includes(edges_includes_builder.includes)
70
- edges_includes = edges_includes_builder.path_leaf_includes
71
-
72
- edge_children = connection_node.scoped_children[connection_node.return_type.unwrap]
73
- edge_children.each_value do |edge_child_node|
74
- if edge_child_node.name == 'node'
75
- node_includes_builder = builder.edges_builder.node_builder
76
- edges_includes.merge_includes(node_includes_builder.includes)
77
- edge_node_includes = node_includes_builder.path_leaf_includes
78
-
79
- node_children = edge_child_node.scoped_children[edge_child_node.return_type.unwrap]
80
- node_children.each_value do |node_child_node|
81
- definition_override = node_definition_override(edge_child_node, node_child_node)
82
- includes_for_child(node_child_node, edge_node_includes, definition_override)
83
- end
84
- else
85
- definition_override = node_definition_override(connection_node, edge_child_node)
86
- includes_for_child(edge_child_node, edges_includes, definition_override)
87
- end
88
- end
89
- elsif connection_node.name == 'nodes'
90
- nodes_includes_builder = builder.nodes_builder
91
- includes.merge_includes(nodes_includes_builder.includes)
92
- nodes_includes = nodes_includes_builder.path_leaf_includes
93
-
94
- node_children = connection_node.scoped_children[connection_node.return_type.unwrap]
95
- node_children.each_value do |node_child_node|
96
- definition_override = node_definition_override(connection_node, node_child_node)
97
- includes_for_child(node_child_node, nodes_includes, definition_override)
98
- end
99
- elsif connection_node.name == 'totalCount'
100
- # Handled using `.size`
101
- end
102
- end
103
- end
104
-
105
- # Special case:
106
- # When includes_for_node is called within a connection resolver, there is no need to use that field's nodes/edges
107
- # includes, only edge_to_node includes
108
- def includes_for_top_level_connection(node, includes)
109
- connection_children = node.scoped_children[node.return_type.unwrap]
110
- top_level_being_resolved = @root_ctx.namespace(:gql_includable)[:resolving]
111
-
112
- if top_level_being_resolved == :edges
113
- builder = build_connection_includes(node, nil)
114
- return unless builder&.edges_builder&.node_builder&.includes?
115
-
116
- edges_node = connection_children['edges']
117
- edges_includes = includes
118
-
119
- edge_children = edges_node.scoped_children[edges_node.return_type.unwrap]
120
- edge_children.each_value do |edge_child_node|
121
- if edge_child_node.name == 'node'
122
- node_includes_builder = builder.edges_builder.node_builder
123
- edges_includes.merge_includes(node_includes_builder.includes)
124
- edge_node_includes = node_includes_builder.path_leaf_includes
125
-
126
- node_children = edge_child_node.scoped_children[edge_child_node.return_type.unwrap]
127
- node_children.each_value do |node_child_node|
128
- definition_override = node_definition_override(edge_child_node, node_child_node)
129
- includes_for_child(node_child_node, edge_node_includes, definition_override)
130
- end
131
- else
132
- definition_override = node_definition_override(edges_node, edge_child_node)
133
- includes_for_child(edge_child_node, edges_includes, definition_override)
134
- end
135
- end
136
- else
137
- nodes_node = connection_children['nodes']
138
- return unless nodes_node.present?
139
- nodes_includes = includes
140
-
141
- node_children = nodes_node.scoped_children[nodes_node.return_type.unwrap]
142
- node_children.each_value do |node_child_node|
143
- definition_override = node_definition_override(nodes_node, node_child_node)
144
- includes_for_child(node_child_node, nodes_includes, definition_override)
145
- end
146
- end
147
- end
148
- # rubocop:enable Metrics/MethodLength
149
- # rubocop:enable Metrics/AbcSize
150
-
151
- def build_includes(node, definition_override)
152
- definition = definition_override || node.definition
153
- includes_meta = definition.metadata[:new_includes]
154
- return nil if includes_meta.blank?
155
-
156
- builder = GraphQLIncludable::New::IncludesBuilder.new
157
-
158
- if includes_meta.is_a?(Proc)
159
- if includes_meta.arity == 2
160
- args_for_field = @root_ctx.query.arguments_for(node, node.definition)
161
- builder.instance_exec(args_for_field, @root_ctx, &includes_meta)
162
- else
163
- builder.instance_exec(&includes_meta)
164
- end
165
- else
166
- builder.path(includes_meta)
167
- end
168
-
169
- builder
170
- end
171
-
172
- def build_connection_includes(node, definition_override)
173
- definition = definition_override || node.definition
174
- includes_meta = definition.metadata[:new_includes]
175
- return nil if includes_meta.blank?
176
-
177
- builder = GraphQLIncludable::New::ConnectionIncludesBuilder.new
178
- if includes_meta.arity == 2
179
- args_for_field = @root_ctx.query.arguments_for(node, node.definition)
180
- builder.instance_exec(args_for_field, @root_ctx, &includes_meta)
181
- else
182
- builder.instance_exec(&includes_meta)
183
- end
184
- builder
185
- end
186
-
187
- def node_definition_override(parent_node, child_node)
188
- node_return_type = parent_node.return_type.unwrap
189
- child_node_parent_type = child_node.parent.return_type.unwrap
190
-
191
- return nil unless child_node_parent_type != node_return_type
192
- child_node_definition_override = nil
193
- # Handle GraphQL interface with overridden fields
194
- # GraphQL makes child_node.return_type the interface instance
195
- # and therefore takes the metadata from the interface rather than the
196
- # implementing object's overridden field instance
197
- is_interface = node_return_type.interfaces.include?(child_node_parent_type)
198
- child_node_definition_override = node_return_type.fields[child_node.name] if is_interface
199
- child_node_definition_override
200
- end
201
- end
202
- end
203
- end