graphql 1.8.3 → 1.8.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql.rb +4 -1
- data/lib/graphql/argument.rb +1 -0
- data/lib/graphql/authorization.rb +81 -0
- data/lib/graphql/boolean_type.rb +0 -1
- data/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb +2 -1
- data/lib/graphql/execution/execute.rb +34 -10
- data/lib/graphql/execution/lazy.rb +5 -1
- data/lib/graphql/field.rb +7 -1
- data/lib/graphql/float_type.rb +0 -1
- data/lib/graphql/id_type.rb +0 -1
- data/lib/graphql/int_type.rb +0 -1
- data/lib/graphql/introspection/entry_points.rb +2 -2
- data/lib/graphql/object_type.rb +3 -3
- data/lib/graphql/query.rb +6 -0
- data/lib/graphql/query/arguments.rb +2 -0
- data/lib/graphql/query/context.rb +6 -0
- data/lib/graphql/query/variables.rb +7 -1
- data/lib/graphql/relay/connection_instrumentation.rb +2 -2
- data/lib/graphql/relay/connection_resolve.rb +7 -27
- data/lib/graphql/relay/connection_type.rb +1 -0
- data/lib/graphql/relay/edge_type.rb +1 -0
- data/lib/graphql/relay/edges_instrumentation.rb +9 -25
- data/lib/graphql/relay/mutation/instrumentation.rb +1 -2
- data/lib/graphql/relay/mutation/resolve.rb +2 -4
- data/lib/graphql/relay/node.rb +1 -6
- data/lib/graphql/relay/page_info.rb +1 -9
- data/lib/graphql/schema.rb +84 -11
- data/lib/graphql/schema/argument.rb +13 -0
- data/lib/graphql/schema/enum.rb +1 -1
- data/lib/graphql/schema/enum_value.rb +4 -0
- data/lib/graphql/schema/field.rb +44 -11
- data/lib/graphql/schema/interface.rb +20 -0
- data/lib/graphql/schema/introspection_system.rb +1 -1
- data/lib/graphql/schema/member/base_dsl_methods.rb +25 -3
- data/lib/graphql/schema/member/instrumentation.rb +15 -17
- data/lib/graphql/schema/mutation.rb +4 -0
- data/lib/graphql/schema/object.rb +33 -0
- data/lib/graphql/schema/possible_types.rb +2 -0
- data/lib/graphql/schema/resolver.rb +10 -0
- data/lib/graphql/schema/traversal.rb +9 -2
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +11 -2
- data/lib/graphql/string_type.rb +0 -1
- data/lib/graphql/types.rb +7 -0
- data/lib/graphql/types/relay.rb +31 -0
- data/lib/graphql/types/relay/base_connection.rb +87 -0
- data/lib/graphql/types/relay/base_edge.rb +51 -0
- data/lib/graphql/types/relay/base_field.rb +22 -0
- data/lib/graphql/types/relay/base_interface.rb +29 -0
- data/lib/graphql/types/relay/base_object.rb +26 -0
- data/lib/graphql/types/relay/node.rb +18 -0
- data/lib/graphql/types/relay/page_info.rb +23 -0
- data/lib/graphql/unauthorized_error.rb +20 -0
- data/lib/graphql/version.rb +1 -1
- data/spec/graphql/authorization_spec.rb +684 -0
- data/spec/graphql/query/variables_spec.rb +20 -0
- data/spec/graphql/relay/connection_instrumentation_spec.rb +1 -1
- data/spec/graphql/schema/resolver_spec.rb +31 -0
- data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +52 -0
- data/spec/support/dummy/schema.rb +16 -0
- data/spec/support/star_wars/schema.rb +28 -17
- metadata +15 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 879e9096d14bb82f2c28f57c60d34e66c6706181
|
4
|
+
data.tar.gz: c39e6b2c81303fba7c2dfa1527932af7d8eb9d31
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79d69e540d7cb3063ce6fea65286f0e66634edeced066930cbc63de169eb9f2a53412f606aacfbf02e28fccfd11781122e95cc31ee234073c6b0f3246c7ac26e
|
7
|
+
data.tar.gz: 4fe1822b9f021f064a2ba383c65a8c889a4acd08927f0c99ed9d9e473890474c3529afa97ed5cb95fd4f67f5d7754b595cdd654734a2fa595ecfebebeb4ff2a8
|
data/lib/graphql.rb
CHANGED
@@ -67,8 +67,9 @@ require "graphql/language"
|
|
67
67
|
require "graphql/analysis"
|
68
68
|
require "graphql/tracing"
|
69
69
|
require "graphql/execution"
|
70
|
-
require "graphql/relay"
|
71
70
|
require "graphql/schema"
|
71
|
+
require "graphql/types"
|
72
|
+
require "graphql/relay"
|
72
73
|
require "graphql/boolean_type"
|
73
74
|
require "graphql/float_type"
|
74
75
|
require "graphql/id_type"
|
@@ -98,3 +99,5 @@ require "graphql/parse_error"
|
|
98
99
|
require "graphql/backtrace"
|
99
100
|
|
100
101
|
require "graphql/deprecated_dsl"
|
102
|
+
require "graphql/authorization"
|
103
|
+
require "graphql/unauthorized_error"
|
data/lib/graphql/argument.rb
CHANGED
@@ -38,6 +38,7 @@ module GraphQL
|
|
38
38
|
accepts_definitions :name, :type, :description, :default_value, :as, :prepare
|
39
39
|
attr_accessor :type, :description, :default_value, :name, :as
|
40
40
|
attr_accessor :ast_node
|
41
|
+
alias :graphql_name :name
|
41
42
|
|
42
43
|
ensure_defined(:name, :description, :default_value, :type=, :type, :as, :expose_as, :prepare)
|
43
44
|
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
module Authorization
|
4
|
+
class InaccessibleFieldsError < GraphQL::AnalysisError
|
5
|
+
# @return [Array<Schema::Field, GraphQL::Field>] Fields that failed `.accessible?` checks
|
6
|
+
attr_reader :fields
|
7
|
+
|
8
|
+
# @return [GraphQL::Query::Context] The current query's context
|
9
|
+
attr_reader :context
|
10
|
+
|
11
|
+
# @return [Array<GraphQL::InternalRepresentation::Node>] The visited nodes that failed `.accessible?` checks
|
12
|
+
# @see {#fields} for the Field definitions
|
13
|
+
attr_reader :irep_nodes
|
14
|
+
|
15
|
+
def initialize(fields:, irep_nodes:, context:)
|
16
|
+
@fields = fields
|
17
|
+
@irep_nodes = irep_nodes
|
18
|
+
@context = context
|
19
|
+
super("Some fields in this query are not accessible: #{fields.map(&:graphql_name).join(", ")}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Analyzer
|
24
|
+
module_function
|
25
|
+
def initial_value(query)
|
26
|
+
{
|
27
|
+
schema: query.schema,
|
28
|
+
context: query.context,
|
29
|
+
inaccessible_nodes: [],
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def call(memo, visit_type, irep_node)
|
34
|
+
if visit_type == :enter
|
35
|
+
field = irep_node.definition
|
36
|
+
if field
|
37
|
+
schema = memo[:schema]
|
38
|
+
ctx = memo[:context]
|
39
|
+
next_field_accessible = schema.accessible?(field, ctx)
|
40
|
+
if !next_field_accessible
|
41
|
+
memo[:inaccessible_nodes] << irep_node
|
42
|
+
else
|
43
|
+
arg_accessible = true
|
44
|
+
irep_node.arguments.argument_values.each do |name, arg_value|
|
45
|
+
arg_accessible = schema.accessible?(arg_value.definition, ctx)
|
46
|
+
if !arg_accessible
|
47
|
+
memo[:inaccessible_nodes] << irep_node
|
48
|
+
break
|
49
|
+
end
|
50
|
+
end
|
51
|
+
if arg_accessible
|
52
|
+
return_type = field.type.unwrap
|
53
|
+
next_type_accessible = schema.accessible?(return_type, ctx)
|
54
|
+
if !next_type_accessible
|
55
|
+
memo[:inaccessible_nodes] << irep_node
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
memo
|
62
|
+
end
|
63
|
+
|
64
|
+
def final_value(memo)
|
65
|
+
nodes = memo[:inaccessible_nodes]
|
66
|
+
if nodes.any?
|
67
|
+
fields = nodes.map do |node|
|
68
|
+
field_inst = node.definition
|
69
|
+
# Get the "source of truth" for this field
|
70
|
+
field_inst.metadata[:type_class] || field_inst
|
71
|
+
end
|
72
|
+
context = memo[:context]
|
73
|
+
err = InaccessibleFieldsError.new(fields: fields, irep_nodes: nodes, context: context)
|
74
|
+
context.schema.inaccessible_fields(err)
|
75
|
+
else
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/graphql/boolean_type.rb
CHANGED
@@ -110,23 +110,47 @@ module GraphQL
|
|
110
110
|
err
|
111
111
|
end
|
112
112
|
|
113
|
-
|
114
|
-
# assign the lazy object to `.value=` so we can resolve it later.
|
115
|
-
# When we resolve it later, reassign it to `.value=` so that
|
116
|
-
# the finished value replaces the unfinished one.
|
117
|
-
#
|
118
|
-
# If the returned object is finished, continue to coerce
|
119
|
-
# and resolve child fields
|
120
|
-
if query.schema.lazy?(raw_value)
|
113
|
+
if field_ctx.schema.lazy?(raw_value)
|
121
114
|
field_ctx.value = Execution::Lazy.new {
|
122
115
|
inner_value = field_ctx.trace("execute_field_lazy", {context: field_ctx}) {
|
123
116
|
begin
|
124
|
-
|
117
|
+
begin
|
118
|
+
field_ctx.field.lazy_resolve(raw_value, arguments, field_ctx)
|
119
|
+
rescue GraphQL::UnauthorizedError => err
|
120
|
+
field_ctx.schema.unauthorized_object(err)
|
121
|
+
end
|
125
122
|
rescue GraphQL::ExecutionError => err
|
126
123
|
err
|
127
124
|
end
|
128
125
|
}
|
129
|
-
|
126
|
+
continue_or_wait(inner_value, arguments, field_ctx)
|
127
|
+
}
|
128
|
+
else
|
129
|
+
continue_or_wait(raw_value, arguments, field_ctx)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# If the returned object is lazy (unfinished),
|
134
|
+
# assign the lazy object to `.value=` so we can resolve it later.
|
135
|
+
# When we resolve it later, reassign it to `.value=` so that
|
136
|
+
# the finished value replaces the unfinished one.
|
137
|
+
#
|
138
|
+
# If the returned object is finished, continue to coerce
|
139
|
+
# and resolve child fields
|
140
|
+
def continue_or_wait(raw_value, arguments, field_ctx)
|
141
|
+
if (lazy_method = field_ctx.schema.lazy_method_name(raw_value))
|
142
|
+
field_ctx.value = Execution::Lazy.new {
|
143
|
+
inner_value = begin
|
144
|
+
begin
|
145
|
+
raw_value.public_send(lazy_method)
|
146
|
+
rescue GraphQL::UnauthorizedError => err
|
147
|
+
field_ctx.schema.unauthorized_object(err)
|
148
|
+
end
|
149
|
+
rescue GraphQL::ExecutionError => err
|
150
|
+
err
|
151
|
+
end
|
152
|
+
|
153
|
+
field_ctx.value = continue_or_wait(inner_value, arguments, field_ctx)
|
130
154
|
}
|
131
155
|
else
|
132
156
|
field_ctx.value = continue_resolve_field(raw_value, field_ctx)
|
data/lib/graphql/field.rb
CHANGED
@@ -157,6 +157,7 @@ module GraphQL
|
|
157
157
|
|
158
158
|
# @return [String] The name of this field on its {GraphQL::ObjectType} (or {GraphQL::InterfaceType})
|
159
159
|
attr_accessor :name
|
160
|
+
alias :graphql_name :name
|
160
161
|
|
161
162
|
# @return [String, nil] The client-facing description of this field
|
162
163
|
attr_accessor :description
|
@@ -323,7 +324,12 @@ module GraphQL
|
|
323
324
|
module DefaultLazyResolve
|
324
325
|
def self.call(obj, args, ctx)
|
325
326
|
method_name = ctx.schema.lazy_method_name(obj)
|
326
|
-
obj.public_send(method_name)
|
327
|
+
next_obj = obj.public_send(method_name)
|
328
|
+
if ctx.schema.lazy?(next_obj)
|
329
|
+
call(next_obj, args, ctx)
|
330
|
+
else
|
331
|
+
next_obj
|
332
|
+
end
|
327
333
|
end
|
328
334
|
end
|
329
335
|
end
|
data/lib/graphql/float_type.rb
CHANGED
data/lib/graphql/id_type.rb
CHANGED
data/lib/graphql/int_type.rb
CHANGED
@@ -11,7 +11,7 @@ module GraphQL
|
|
11
11
|
# Apply wrapping manually since this field isn't wrapped by instrumentation
|
12
12
|
schema = @context.query.schema
|
13
13
|
schema_type = schema.introspection_system.schema_type
|
14
|
-
schema_type.metadata[:type_class].
|
14
|
+
schema_type.metadata[:type_class].authorized_new(schema, @context)
|
15
15
|
end
|
16
16
|
|
17
17
|
def __type(name:)
|
@@ -19,7 +19,7 @@ module GraphQL
|
|
19
19
|
if type
|
20
20
|
# Apply wrapping manually since this field isn't wrapped by instrumentation
|
21
21
|
type_type = @context.schema.introspection_system.type_type
|
22
|
-
type_type.metadata[:type_class].
|
22
|
+
type_type.metadata[:type_class].authorized_new(type, @context)
|
23
23
|
else
|
24
24
|
nil
|
25
25
|
end
|
data/lib/graphql/object_type.rb
CHANGED
@@ -22,11 +22,11 @@ module GraphQL
|
|
22
22
|
# end
|
23
23
|
#
|
24
24
|
class ObjectType < GraphQL::BaseType
|
25
|
-
accepts_definitions :interfaces, :fields, :mutation, field: GraphQL::Define::AssignObjectField
|
25
|
+
accepts_definitions :interfaces, :fields, :mutation, :relay_node_type, field: GraphQL::Define::AssignObjectField
|
26
26
|
accepts_definitions implements: ->(type, *interfaces, inherit: false) { type.implements(interfaces, inherit: inherit) }
|
27
27
|
|
28
|
-
attr_accessor :fields, :mutation
|
29
|
-
ensure_defined(:fields, :mutation, :interfaces)
|
28
|
+
attr_accessor :fields, :mutation, :relay_node_type
|
29
|
+
ensure_defined(:fields, :mutation, :interfaces, :relay_node_type)
|
30
30
|
|
31
31
|
# @!attribute fields
|
32
32
|
# @return [Hash<String => GraphQL::Field>] Map String fieldnames to their {GraphQL::Field} implementations
|
data/lib/graphql/query.rb
CHANGED
@@ -128,6 +128,12 @@ module GraphQL
|
|
128
128
|
|
129
129
|
@result_values = nil
|
130
130
|
@executed = false
|
131
|
+
|
132
|
+
# TODO add a general way to define schema-level filters
|
133
|
+
# TODO also add this to schema dumps
|
134
|
+
if @schema.respond_to?(:visible?)
|
135
|
+
merge_filters(only: @schema.method(:visible?))
|
136
|
+
end
|
131
137
|
end
|
132
138
|
|
133
139
|
def subscription_update?
|
@@ -207,8 +207,14 @@ module GraphQL
|
|
207
207
|
@query = context.query
|
208
208
|
@schema = context.schema
|
209
209
|
@tracers = @query.tracers
|
210
|
+
# This hack flag is required by ConnectionResolve
|
211
|
+
@wrapped_connection = false
|
212
|
+
@wrapped_object = false
|
210
213
|
end
|
211
214
|
|
215
|
+
# @api private
|
216
|
+
attr_accessor :wrapped_connection, :wrapped_object
|
217
|
+
|
212
218
|
def path
|
213
219
|
@path ||= @parent.path.dup << @key
|
214
220
|
end
|
@@ -30,7 +30,13 @@ module GraphQL
|
|
30
30
|
provided_value = @provided_variables[variable_name]
|
31
31
|
value_was_provided = @provided_variables.key?(variable_name)
|
32
32
|
|
33
|
-
|
33
|
+
begin
|
34
|
+
validation_result = variable_type.validate_input(provided_value, ctx)
|
35
|
+
rescue GraphQL::CoercionError => ex
|
36
|
+
validation_result = GraphQL::Query::InputValidationResult.new
|
37
|
+
validation_result.add_problem(ex.message)
|
38
|
+
end
|
39
|
+
|
34
40
|
if !validation_result.valid?
|
35
41
|
# This finds variables that were required but not provided
|
36
42
|
@errors << GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, provided_value, validation_result)
|
@@ -36,8 +36,8 @@ module GraphQL
|
|
36
36
|
connection_arguments = default_arguments.merge(field.arguments)
|
37
37
|
original_resolve = field.resolve_proc
|
38
38
|
original_lazy_resolve = field.lazy_resolve_proc
|
39
|
-
connection_resolve = GraphQL::Relay::ConnectionResolve.new(field, original_resolve
|
40
|
-
connection_lazy_resolve = GraphQL::Relay::ConnectionResolve.new(field, original_lazy_resolve
|
39
|
+
connection_resolve = GraphQL::Relay::ConnectionResolve.new(field, original_resolve)
|
40
|
+
connection_lazy_resolve = GraphQL::Relay::ConnectionResolve.new(field, original_lazy_resolve)
|
41
41
|
field.redefine(
|
42
42
|
resolve: connection_resolve,
|
43
43
|
lazy_resolve: connection_lazy_resolve,
|
@@ -2,34 +2,24 @@
|
|
2
2
|
module GraphQL
|
3
3
|
module Relay
|
4
4
|
class ConnectionResolve
|
5
|
-
def initialize(field, underlying_resolve
|
5
|
+
def initialize(field, underlying_resolve)
|
6
6
|
@field = field
|
7
7
|
@underlying_resolve = underlying_resolve
|
8
8
|
@max_page_size = field.connection_max_page_size
|
9
|
-
@lazy = lazy
|
10
9
|
end
|
11
10
|
|
12
11
|
def call(obj, args, ctx)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
parent = obj
|
18
|
-
end
|
12
|
+
# in a lazy ressolve hook, obj is the promise,
|
13
|
+
# get the object that the promise was
|
14
|
+
# originally derived from
|
15
|
+
parent = ctx.object
|
19
16
|
|
20
17
|
nodes = @underlying_resolve.call(obj, args, ctx)
|
21
18
|
|
22
|
-
if nodes.nil?
|
23
|
-
nil
|
24
|
-
elsif ctx.schema.lazy?(nodes)
|
25
|
-
if !@lazy
|
26
|
-
LazyNodesWrapper.new(obj, nodes)
|
27
|
-
else
|
28
|
-
nodes
|
29
|
-
end
|
30
|
-
elsif nodes.is_a?(GraphQL::Execution::Execute::Skip)
|
19
|
+
if nodes.nil? || ctx.schema.lazy?(nodes) || nodes.is_a?(GraphQL::Execution::Execute::Skip) || ctx.wrapped_connection
|
31
20
|
nodes
|
32
21
|
else
|
22
|
+
ctx.wrapped_connection = true
|
33
23
|
build_connection(nodes, args, parent, ctx)
|
34
24
|
end
|
35
25
|
end
|
@@ -48,16 +38,6 @@ module GraphQL
|
|
48
38
|
connection_class.new(nodes, args, field: @field, max_page_size: @max_page_size, parent: parent, context: ctx)
|
49
39
|
end
|
50
40
|
end
|
51
|
-
|
52
|
-
# A container for the proper `parent` of connection nodes.
|
53
|
-
# Without this wrapper, the lazy object _itself_ is passed into `build_connection`
|
54
|
-
# and it becomes the parent, which is wrong.
|
55
|
-
#
|
56
|
-
# We can get away with it because we know that this instrumentation will be applied last.
|
57
|
-
# That means its code after `underlying_resolve` will be _last_ on the way in.
|
58
|
-
# And, its code before `underlying_resolve` will be _first_ during lazy resolution.
|
59
|
-
# @api private
|
60
|
-
LazyNodesWrapper = Struct.new(:parent, :lazy_object)
|
61
41
|
end
|
62
42
|
end
|
63
43
|
end
|
@@ -9,6 +9,7 @@ module GraphQL
|
|
9
9
|
description "An edge in a connection."
|
10
10
|
field :node, wrapped_type, "The item at the end of the edge."
|
11
11
|
field :cursor, !types.String, "A cursor for use in pagination."
|
12
|
+
relay_node_type(wrapped_type)
|
12
13
|
block && instance_eval(&block)
|
13
14
|
end
|
14
15
|
end
|
@@ -4,15 +4,12 @@ module GraphQL
|
|
4
4
|
module EdgesInstrumentation
|
5
5
|
def self.instrument(type, field)
|
6
6
|
if field.edges?
|
7
|
-
edges_resolve = EdgesResolve.new(
|
8
|
-
|
9
|
-
resolve: field.resolve_proc,
|
10
|
-
lazy_resolve: field.lazy_resolve_proc,
|
11
|
-
)
|
7
|
+
edges_resolve = EdgesResolve.new(edge_class: field.edge_class, resolve: field.resolve_proc)
|
8
|
+
edges_lazy_resolve = EdgesResolve.new(edge_class: field.edge_class, resolve: field.lazy_resolve_proc)
|
12
9
|
|
13
10
|
field.redefine(
|
14
|
-
resolve: edges_resolve
|
15
|
-
lazy_resolve:
|
11
|
+
resolve: edges_resolve,
|
12
|
+
lazy_resolve: edges_lazy_resolve,
|
16
13
|
)
|
17
14
|
else
|
18
15
|
field
|
@@ -21,35 +18,22 @@ module GraphQL
|
|
21
18
|
|
22
19
|
|
23
20
|
class EdgesResolve
|
24
|
-
def initialize(edge_class:, resolve
|
21
|
+
def initialize(edge_class:, resolve:)
|
25
22
|
@edge_class = edge_class
|
26
23
|
@resolve_proc = resolve
|
27
|
-
@lazy_resolve_proc = lazy_resolve
|
28
24
|
end
|
29
25
|
|
30
26
|
# A user's custom Connection may return a lazy object,
|
31
27
|
# if so, handle it later.
|
32
|
-
def
|
28
|
+
def call(obj, args, ctx)
|
29
|
+
parent = ctx.object
|
33
30
|
nodes = @resolve_proc.call(obj, args, ctx)
|
34
31
|
if ctx.schema.lazy?(nodes)
|
35
|
-
|
32
|
+
nodes
|
36
33
|
else
|
37
|
-
|
34
|
+
nodes.map { |item| @edge_class.new(item, parent) }
|
38
35
|
end
|
39
36
|
end
|
40
|
-
|
41
|
-
# If we get this far, unwrap the wrapper,
|
42
|
-
# resolve the lazy object and make the edges as usual
|
43
|
-
def lazy_resolve(obj, args, ctx)
|
44
|
-
items = @lazy_resolve_proc.call(obj.lazy_object, args, ctx)
|
45
|
-
build_edges(items, obj.parent)
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
|
50
|
-
def build_edges(items, connection)
|
51
|
-
items.map { |item| @edge_class.new(item, connection) }
|
52
|
-
end
|
53
37
|
end
|
54
38
|
end
|
55
39
|
end
|