graphql 1.11.3 → 1.11.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/graphql/core.rb +8 -0
- data/lib/generators/graphql/templates/base_argument.erb +2 -0
- data/lib/generators/graphql/templates/base_enum.erb +2 -0
- data/lib/generators/graphql/templates/base_field.erb +2 -0
- data/lib/generators/graphql/templates/base_input_object.erb +2 -0
- data/lib/generators/graphql/templates/base_interface.erb +2 -0
- data/lib/generators/graphql/templates/base_mutation.erb +2 -0
- data/lib/generators/graphql/templates/base_object.erb +2 -0
- data/lib/generators/graphql/templates/base_scalar.erb +2 -0
- data/lib/generators/graphql/templates/base_union.erb +2 -0
- data/lib/generators/graphql/templates/enum.erb +2 -0
- data/lib/generators/graphql/templates/graphql_controller.erb +2 -0
- data/lib/generators/graphql/templates/interface.erb +2 -0
- data/lib/generators/graphql/templates/loader.erb +2 -0
- data/lib/generators/graphql/templates/mutation.erb +2 -0
- data/lib/generators/graphql/templates/mutation_type.erb +2 -0
- data/lib/generators/graphql/templates/object.erb +2 -0
- data/lib/generators/graphql/templates/query_type.erb +2 -0
- data/lib/generators/graphql/templates/scalar.erb +2 -0
- data/lib/generators/graphql/templates/schema.erb +2 -0
- data/lib/generators/graphql/templates/union.erb +2 -0
- data/lib/graphql/execution/interpreter/runtime.rb +24 -25
- data/lib/graphql/query/context.rb +20 -1
- data/lib/graphql/query/fingerprint.rb +2 -0
- data/lib/graphql/query/validation_pipeline.rb +3 -0
- data/lib/graphql/schema.rb +4 -0
- data/lib/graphql/schema/field/connection_extension.rb +1 -1
- data/lib/graphql/schema/subscription.rb +2 -12
- data/lib/graphql/schema/warden.rb +2 -3
- data/lib/graphql/subscriptions.rb +31 -19
- data/lib/graphql/tracing/appoptics_tracing.rb +10 -2
- data/lib/graphql/types/iso_8601_date_time.rb +2 -1
- data/lib/graphql/types/relay/base_connection.rb +6 -5
- data/lib/graphql/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5bf42de25927dab863bd600879d33d204c54bdf124b2b4cc5b27c4c16d4f28b
|
4
|
+
data.tar.gz: 58b21a4dd5ba80ec0f6c3362013a6adb07c2f1af243e5a851d4c3f08554fe09d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f199d24a190910e44b189340b6a3d2a3c121ab56ba10ec6cf44d8801107db8d73541f24788b00b13ee7dd357f7bfe6ecff7001f053fe417c777bca59db5851db
|
7
|
+
data.tar.gz: fd0955c7f67b20370ea376d6a286fdf0ac7272ac5b243d6c309d2159a780cd0b8f92fd0e06ca312a69a16b7c8ad0ebd7bb4587feac9dc77b27da507c9a44cd8f
|
@@ -1,3 +1,4 @@
|
|
1
|
+
<% module_namespacing_when_supported do -%>
|
1
2
|
class GraphqlController < ApplicationController
|
2
3
|
# If accessing from outside this domain, nullify the session
|
3
4
|
# This allows for outside API access while preventing CSRF attacks,
|
@@ -48,3 +49,4 @@ class GraphqlController < ApplicationController
|
|
48
49
|
render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: 500
|
49
50
|
end
|
50
51
|
end
|
52
|
+
<% end -%>
|
@@ -1,6 +1,8 @@
|
|
1
|
+
<% module_namespacing_when_supported do -%>
|
1
2
|
module Types
|
2
3
|
class <%= type_ruby_name.split('::')[-1] %> < Types::BaseObject
|
3
4
|
<% if options.node %> implements GraphQL::Relay::Node.interface
|
4
5
|
<% end %><% normalized_fields.each do |f| %> <%= f.to_ruby %>
|
5
6
|
<% end %> end
|
6
7
|
end
|
8
|
+
<% end -%>
|
@@ -170,13 +170,13 @@ module GraphQL
|
|
170
170
|
begin
|
171
171
|
kwarg_arguments = arguments(object, field_defn, ast_node)
|
172
172
|
rescue GraphQL::ExecutionError => e
|
173
|
-
continue_value(next_path, e, field_defn, return_type.non_null?, ast_node)
|
173
|
+
continue_value(next_path, e, owner_type, field_defn, return_type.non_null?, ast_node)
|
174
174
|
next
|
175
175
|
end
|
176
176
|
|
177
177
|
after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |resolved_arguments|
|
178
178
|
if resolved_arguments.is_a? GraphQL::ExecutionError
|
179
|
-
continue_value(next_path, resolved_arguments, field_defn, return_type.non_null?, ast_node)
|
179
|
+
continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
|
180
180
|
next
|
181
181
|
end
|
182
182
|
|
@@ -228,12 +228,12 @@ module GraphQL
|
|
228
228
|
err
|
229
229
|
end
|
230
230
|
after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |inner_result|
|
231
|
-
continue_value = continue_value(next_path, inner_result, field_defn, return_type.non_null?, ast_node)
|
231
|
+
continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
|
232
232
|
if RawValue === continue_value
|
233
233
|
# Write raw value directly to the response without resolving nested objects
|
234
234
|
write_in_response(next_path, continue_value.resolve)
|
235
235
|
elsif HALT != continue_value
|
236
|
-
continue_field(next_path, continue_value, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
|
236
|
+
continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
|
237
237
|
end
|
238
238
|
end
|
239
239
|
end
|
@@ -251,10 +251,9 @@ module GraphQL
|
|
251
251
|
end
|
252
252
|
|
253
253
|
HALT = Object.new
|
254
|
-
def continue_value(path, value, field, is_non_null, ast_node)
|
254
|
+
def continue_value(path, value, parent_type, field, is_non_null, ast_node)
|
255
255
|
if value.nil?
|
256
256
|
if is_non_null
|
257
|
-
parent_type = field.owner_type
|
258
257
|
err = parent_type::InvalidNullError.new(parent_type, field, value)
|
259
258
|
write_invalid_null_in_response(path, err)
|
260
259
|
else
|
@@ -282,7 +281,7 @@ module GraphQL
|
|
282
281
|
err
|
283
282
|
end
|
284
283
|
|
285
|
-
continue_value(path, next_value, field, is_non_null, ast_node)
|
284
|
+
continue_value(path, next_value, parent_type, field, is_non_null, ast_node)
|
286
285
|
elsif GraphQL::Execution::Execute::SKIP == value
|
287
286
|
HALT
|
288
287
|
else
|
@@ -298,49 +297,49 @@ module GraphQL
|
|
298
297
|
# Location information from `path` and `ast_node`.
|
299
298
|
#
|
300
299
|
# @return [Lazy, Array, Hash, Object] Lazy, Array, and Hash are all traversed to resolve lazy values later
|
301
|
-
def continue_field(path, value, field,
|
302
|
-
case
|
300
|
+
def continue_field(path, value, owner_type, field, current_type, ast_node, next_selections, is_non_null, owner_object, arguments) # rubocop:disable Metrics/ParameterLists
|
301
|
+
case current_type.kind.name
|
303
302
|
when "SCALAR", "ENUM"
|
304
|
-
r =
|
303
|
+
r = current_type.coerce_result(value, context)
|
305
304
|
write_in_response(path, r)
|
306
305
|
r
|
307
306
|
when "UNION", "INTERFACE"
|
308
|
-
resolved_type_or_lazy, resolved_value = resolve_type(
|
307
|
+
resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
|
309
308
|
resolved_value ||= value
|
310
309
|
|
311
|
-
after_lazy(resolved_type_or_lazy, owner:
|
312
|
-
possible_types = query.possible_types(
|
310
|
+
after_lazy(resolved_type_or_lazy, owner: current_type, path: path, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |resolved_type|
|
311
|
+
possible_types = query.possible_types(current_type)
|
313
312
|
|
314
313
|
if !possible_types.include?(resolved_type)
|
315
314
|
parent_type = field.owner_type
|
316
|
-
err_class =
|
315
|
+
err_class = current_type::UnresolvedTypeError
|
317
316
|
type_error = err_class.new(resolved_value, field, parent_type, resolved_type, possible_types)
|
318
317
|
schema.type_error(type_error, context)
|
319
318
|
write_in_response(path, nil)
|
320
319
|
nil
|
321
320
|
else
|
322
|
-
continue_field(path, resolved_value, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments)
|
321
|
+
continue_field(path, resolved_value, owner_type, field, resolved_type, ast_node, next_selections, is_non_null, owner_object, arguments)
|
323
322
|
end
|
324
323
|
end
|
325
324
|
when "OBJECT"
|
326
325
|
object_proxy = begin
|
327
|
-
authorized_new(
|
326
|
+
authorized_new(current_type, value, context, path)
|
328
327
|
rescue GraphQL::ExecutionError => err
|
329
328
|
err
|
330
329
|
end
|
331
|
-
after_lazy(object_proxy, owner:
|
332
|
-
continue_value = continue_value(path, inner_object, field, is_non_null, ast_node)
|
330
|
+
after_lazy(object_proxy, owner: current_type, path: path, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |inner_object|
|
331
|
+
continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node)
|
333
332
|
if HALT != continue_value
|
334
333
|
response_hash = {}
|
335
334
|
write_in_response(path, response_hash)
|
336
|
-
evaluate_selections(path, context.scoped_context, continue_value,
|
335
|
+
evaluate_selections(path, context.scoped_context, continue_value, current_type, next_selections)
|
337
336
|
response_hash
|
338
337
|
end
|
339
338
|
end
|
340
339
|
when "LIST"
|
341
340
|
response_list = []
|
342
341
|
write_in_response(path, response_list)
|
343
|
-
inner_type =
|
342
|
+
inner_type = current_type.of_type
|
344
343
|
idx = 0
|
345
344
|
scoped_context = context.scoped_context
|
346
345
|
begin
|
@@ -352,9 +351,9 @@ module GraphQL
|
|
352
351
|
set_type_at_path(next_path, inner_type)
|
353
352
|
# This will update `response_list` with the lazy
|
354
353
|
after_lazy(inner_value, owner: inner_type, path: next_path, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |inner_inner_value|
|
355
|
-
continue_value = continue_value(next_path, inner_inner_value, field, inner_type.non_null?, ast_node)
|
354
|
+
continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node)
|
356
355
|
if HALT != continue_value
|
357
|
-
continue_field(next_path, continue_value, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
|
356
|
+
continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
|
358
357
|
end
|
359
358
|
end
|
360
359
|
end
|
@@ -371,12 +370,12 @@ module GraphQL
|
|
371
370
|
|
372
371
|
response_list
|
373
372
|
when "NON_NULL"
|
374
|
-
inner_type =
|
373
|
+
inner_type = current_type.of_type
|
375
374
|
# Don't `set_type_at_path` because we want the static type,
|
376
375
|
# we're going to use that to determine whether a `nil` should be propagated or not.
|
377
|
-
continue_field(path, value, field, inner_type, ast_node, next_selections, true, owner_object, arguments)
|
376
|
+
continue_field(path, value, owner_type, field, inner_type, ast_node, next_selections, true, owner_object, arguments)
|
378
377
|
else
|
379
|
-
raise "Invariant: Unhandled type kind #{
|
378
|
+
raise "Invariant: Unhandled type kind #{current_type.kind} (#{current_type})"
|
380
379
|
end
|
381
380
|
end
|
382
381
|
|
@@ -168,7 +168,6 @@ module GraphQL
|
|
168
168
|
attr_accessor :scoped_context
|
169
169
|
|
170
170
|
def_delegators :@provided_values, :[]=
|
171
|
-
def_delegators :to_h, :fetch, :dig
|
172
171
|
def_delegators :@query, :trace, :interpreter?
|
173
172
|
|
174
173
|
# @!method []=(key, value)
|
@@ -180,6 +179,26 @@ module GraphQL
|
|
180
179
|
@provided_values[key]
|
181
180
|
end
|
182
181
|
|
182
|
+
UNSPECIFIED_FETCH_DEFAULT = Object.new
|
183
|
+
|
184
|
+
def fetch(key, default = UNSPECIFIED_FETCH_DEFAULT)
|
185
|
+
if @scoped_context.key?(key)
|
186
|
+
@scoped_context[key]
|
187
|
+
elsif @provided_values.key?(key)
|
188
|
+
@provided_values[key]
|
189
|
+
elsif default != UNSPECIFIED_FETCH_DEFAULT
|
190
|
+
default
|
191
|
+
elsif block_given?
|
192
|
+
yield(self, key)
|
193
|
+
else
|
194
|
+
raise KeyError.new(key: key)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def dig(key, *other_keys)
|
199
|
+
@scoped_context.key?(key) ? @scoped_context.dig(key, *other_keys) : @provided_values.dig(key, *other_keys)
|
200
|
+
end
|
201
|
+
|
183
202
|
def to_h
|
184
203
|
@provided_values.merge(@scoped_context)
|
185
204
|
end
|
data/lib/graphql/schema.rb
CHANGED
@@ -44,7 +44,7 @@ module GraphQL
|
|
44
44
|
if field.has_max_page_size? && !value.has_max_page_size_override?
|
45
45
|
value.max_page_size = field.max_page_size
|
46
46
|
end
|
47
|
-
if (custom_t = context.schema.connections.edge_class_for_field(@field))
|
47
|
+
if context.schema.new_connections? && (custom_t = context.schema.connections.edge_class_for_field(@field))
|
48
48
|
value.edge_class = custom_t
|
49
49
|
end
|
50
50
|
value
|
@@ -12,16 +12,6 @@ module GraphQL
|
|
12
12
|
#
|
13
13
|
# Also, `#unsubscribe` terminates the subscription.
|
14
14
|
class Subscription < GraphQL::Schema::Resolver
|
15
|
-
class EarlyTerminationError < StandardError
|
16
|
-
end
|
17
|
-
|
18
|
-
# Raised when `unsubscribe` is called; caught by `subscriptions.rb`
|
19
|
-
class UnsubscribedError < EarlyTerminationError
|
20
|
-
end
|
21
|
-
|
22
|
-
# Raised when `no_update` is returned; caught by `subscriptions.rb`
|
23
|
-
class NoUpdateError < EarlyTerminationError
|
24
|
-
end
|
25
15
|
extend GraphQL::Schema::Resolver::HasPayloadType
|
26
16
|
extend GraphQL::Schema::Member::HasFields
|
27
17
|
|
@@ -65,7 +55,7 @@ module GraphQL
|
|
65
55
|
def resolve_update(**args)
|
66
56
|
ret_val = args.any? ? update(**args) : update
|
67
57
|
if ret_val == :no_update
|
68
|
-
|
58
|
+
throw :graphql_no_subscription_update
|
69
59
|
else
|
70
60
|
ret_val
|
71
61
|
end
|
@@ -90,7 +80,7 @@ module GraphQL
|
|
90
80
|
|
91
81
|
# Call this to halt execution and remove this subscription from the system
|
92
82
|
def unsubscribe
|
93
|
-
|
83
|
+
throw :graphql_subscription_unsubscribed
|
94
84
|
end
|
95
85
|
|
96
86
|
READING_SCOPE = ::Object.new
|
@@ -40,7 +40,6 @@ module GraphQL
|
|
40
40
|
# @param filter [<#call(member)>] Objects are hidden when `.call(member, ctx)` returns true
|
41
41
|
# @param context [GraphQL::Query::Context]
|
42
42
|
# @param schema [GraphQL::Schema]
|
43
|
-
# @param deep_check [Boolean]
|
44
43
|
def initialize(filter, context:, schema:)
|
45
44
|
@schema = schema.interpreter? ? schema : schema.graphql_definition
|
46
45
|
# Cache these to avoid repeated hits to the inheritance chain when one isn't present
|
@@ -51,7 +50,7 @@ module GraphQL
|
|
51
50
|
@visibility_cache = read_through { |m| filter.call(m, context) }
|
52
51
|
end
|
53
52
|
|
54
|
-
# @return [
|
53
|
+
# @return [Hash<String, GraphQL::BaseType>] Visible types in the schema
|
55
54
|
def types
|
56
55
|
@types ||= begin
|
57
56
|
vis_types = {}
|
@@ -199,7 +198,7 @@ module GraphQL
|
|
199
198
|
if (iface_field_defn = interface_type.get_field(field_defn.graphql_name))
|
200
199
|
any_interface_has_field = true
|
201
200
|
|
202
|
-
if
|
201
|
+
if interfaces(type_defn).include?(interface_type) && visible_field?(interface_type, iface_field_defn)
|
203
202
|
any_interface_has_visible_field = true
|
204
203
|
end
|
205
204
|
end
|
@@ -100,31 +100,43 @@ module GraphQL
|
|
100
100
|
# Lookup the saved data for this subscription
|
101
101
|
query_data = read_subscription(subscription_id)
|
102
102
|
if query_data.nil?
|
103
|
-
|
104
|
-
|
103
|
+
delete_subscription(subscription_id)
|
104
|
+
return nil
|
105
105
|
end
|
106
|
+
|
106
107
|
# Fetch the required keys from the saved data
|
107
108
|
query_string = query_data.fetch(:query_string)
|
108
109
|
variables = query_data.fetch(:variables)
|
109
110
|
context = query_data.fetch(:context)
|
110
111
|
operation_name = query_data.fetch(:operation_name)
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
112
|
+
result = nil
|
113
|
+
# this will be set to `false` unless `.execute` is terminated
|
114
|
+
# with a `throw :graphql_subscription_unsubscribed`
|
115
|
+
unsubscribed = true
|
116
|
+
catch(:graphql_subscription_unsubscribed) do
|
117
|
+
catch(:graphql_no_subscription_update) do
|
118
|
+
# Re-evaluate the saved query,
|
119
|
+
# but if it terminates early with a `throw`,
|
120
|
+
# it will stay `nil`
|
121
|
+
result = @schema.execute(
|
122
|
+
query: query_string,
|
123
|
+
context: context,
|
124
|
+
subscription_topic: event.topic,
|
125
|
+
operation_name: operation_name,
|
126
|
+
variables: variables,
|
127
|
+
root_value: object,
|
128
|
+
)
|
129
|
+
end
|
130
|
+
unsubscribed = false
|
131
|
+
end
|
132
|
+
|
133
|
+
if unsubscribed
|
134
|
+
# `unsubscribe` was called, clean up on our side
|
135
|
+
# TODO also send `{more: false}` to client?
|
136
|
+
delete_subscription(subscription_id)
|
137
|
+
end
|
138
|
+
|
139
|
+
result
|
128
140
|
end
|
129
141
|
|
130
142
|
# Run the update query for this subscription and deliver it
|
@@ -55,7 +55,15 @@ module GraphQL
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def platform_field_key(type, field)
|
58
|
-
"graphql.#{type.
|
58
|
+
"graphql.#{type.graphql_name}.#{field.graphql_name}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def platform_authorized_key(type)
|
62
|
+
"graphql.authorized.#{type.graphql_name}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def platform_resolve_type_key(type)
|
66
|
+
"graphql.resolve_type.#{type.graphql_name}"
|
59
67
|
end
|
60
68
|
|
61
69
|
private
|
@@ -107,7 +115,7 @@ module GraphQL
|
|
107
115
|
else
|
108
116
|
[key, data[key]]
|
109
117
|
end
|
110
|
-
end.flatten.each_slice(2).to_h.merge(Spec: 'graphql')
|
118
|
+
end.flatten(2).each_slice(2).to_h.merge(Spec: 'graphql')
|
111
119
|
end
|
112
120
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
113
121
|
|
@@ -48,7 +48,7 @@ module GraphQL
|
|
48
48
|
# It's called when you subclass this base connection, trying to use the
|
49
49
|
# class name to set defaults. You can call it again in the class definition
|
50
50
|
# to override the default (or provide a value, if the default lookup failed).
|
51
|
-
def edge_type(edge_type_class, edge_class: GraphQL::Relay::Edge, node_type: edge_type_class.node_type, nodes_field: true)
|
51
|
+
def edge_type(edge_type_class, edge_class: GraphQL::Relay::Edge, node_type: edge_type_class.node_type, nodes_field: true, node_nullable: true)
|
52
52
|
# Set this connection's graphql name
|
53
53
|
node_type_name = node_type.graphql_name
|
54
54
|
|
@@ -61,7 +61,7 @@ module GraphQL
|
|
61
61
|
description: "A list of edges.",
|
62
62
|
edge_class: edge_class
|
63
63
|
|
64
|
-
define_nodes_field if nodes_field
|
64
|
+
define_nodes_field(node_nullable) if nodes_field
|
65
65
|
|
66
66
|
description("The connection type for #{node_type_name}.")
|
67
67
|
end
|
@@ -90,9 +90,10 @@ module GraphQL
|
|
90
90
|
|
91
91
|
private
|
92
92
|
|
93
|
-
def define_nodes_field
|
94
|
-
|
95
|
-
|
93
|
+
def define_nodes_field(nullable = true)
|
94
|
+
type = nullable ? [@node_type, null: true] : [@node_type]
|
95
|
+
field :nodes, type,
|
96
|
+
null: nullable,
|
96
97
|
description: "A list of nodes."
|
97
98
|
end
|
98
99
|
end
|
data/lib/graphql/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.11.
|
4
|
+
version: 1.11.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Mosolgo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-08-
|
11
|
+
date: 2020-08-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: benchmark-ips
|