graphql 2.5.9 → 2.5.26
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/generators/graphql/detailed_trace_generator.rb +77 -0
- data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
- data/lib/graphql/analysis.rb +20 -13
- data/lib/graphql/dashboard/application_controller.rb +41 -0
- data/lib/graphql/dashboard/landings_controller.rb +9 -0
- data/lib/graphql/dashboard/statics_controller.rb +31 -0
- data/lib/graphql/dashboard/subscriptions.rb +2 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
- data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
- data/lib/graphql/dashboard.rb +11 -73
- data/lib/graphql/dataloader/active_record_association_source.rb +14 -2
- data/lib/graphql/dataloader/async_dataloader.rb +22 -11
- data/lib/graphql/dataloader/null_dataloader.rb +54 -9
- data/lib/graphql/dataloader.rb +75 -23
- data/lib/graphql/date_encoding_error.rb +1 -1
- data/lib/graphql/execution/field_resolve_step.rb +631 -0
- data/lib/graphql/execution/finalize.rb +217 -0
- data/lib/graphql/execution/input_values.rb +261 -0
- data/lib/graphql/execution/interpreter/handles_raw_value.rb +6 -0
- data/lib/graphql/execution/interpreter/resolve.rb +10 -16
- data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
- data/lib/graphql/execution/interpreter/runtime.rb +28 -33
- data/lib/graphql/execution/interpreter.rb +8 -22
- data/lib/graphql/execution/lazy.rb +1 -1
- data/lib/graphql/execution/load_argument_step.rb +64 -0
- data/lib/graphql/execution/multiplex.rb +1 -1
- data/lib/graphql/execution/next.rb +90 -0
- data/lib/graphql/execution/prepare_object_step.rb +128 -0
- data/lib/graphql/execution/runner.rb +410 -0
- data/lib/graphql/execution/selections_step.rb +91 -0
- data/lib/graphql/execution.rb +8 -4
- data/lib/graphql/execution_error.rb +17 -10
- data/lib/graphql/introspection/directive_type.rb +7 -3
- data/lib/graphql/introspection/dynamic_fields.rb +5 -1
- data/lib/graphql/introspection/entry_points.rb +11 -3
- data/lib/graphql/introspection/enum_value_type.rb +5 -5
- data/lib/graphql/introspection/field_type.rb +13 -5
- data/lib/graphql/introspection/input_value_type.rb +21 -13
- data/lib/graphql/introspection/type_type.rb +64 -28
- data/lib/graphql/invalid_null_error.rb +11 -5
- data/lib/graphql/language/document_from_schema_definition.rb +2 -1
- data/lib/graphql/language/lexer.rb +20 -9
- data/lib/graphql/language/nodes.rb +5 -1
- data/lib/graphql/language/parser.rb +1 -0
- data/lib/graphql/language.rb +21 -12
- data/lib/graphql/pagination/connection.rb +2 -0
- data/lib/graphql/pagination/connections.rb +32 -0
- data/lib/graphql/query/context.rb +11 -4
- data/lib/graphql/query/null_context.rb +9 -3
- data/lib/graphql/query/partial.rb +18 -3
- data/lib/graphql/query.rb +10 -1
- data/lib/graphql/runtime_error.rb +6 -0
- data/lib/graphql/schema/addition.rb +3 -1
- data/lib/graphql/schema/argument.rb +17 -0
- data/lib/graphql/schema/build_from_definition.rb +15 -2
- data/lib/graphql/schema/directive.rb +45 -13
- data/lib/graphql/schema/field/connection_extension.rb +4 -37
- data/lib/graphql/schema/field/scope_extension.rb +18 -13
- data/lib/graphql/schema/field.rb +87 -48
- data/lib/graphql/schema/field_extension.rb +11 -8
- data/lib/graphql/schema/interface.rb +26 -0
- data/lib/graphql/schema/list.rb +5 -1
- data/lib/graphql/schema/member/base_dsl_methods.rb +1 -11
- data/lib/graphql/schema/member/has_arguments.rb +43 -14
- data/lib/graphql/schema/member/has_authorization.rb +35 -0
- data/lib/graphql/schema/member/has_dataloader.rb +37 -0
- data/lib/graphql/schema/member/has_fields.rb +86 -5
- data/lib/graphql/schema/member/has_interfaces.rb +2 -2
- data/lib/graphql/schema/member/type_system_helpers.rb +16 -2
- data/lib/graphql/schema/member.rb +5 -0
- data/lib/graphql/schema/non_null.rb +1 -1
- data/lib/graphql/schema/object.rb +1 -0
- data/lib/graphql/schema/ractor_shareable.rb +79 -0
- data/lib/graphql/schema/resolver.rb +60 -1
- data/lib/graphql/schema/subscription.rb +0 -2
- data/lib/graphql/schema/validator/required_validator.rb +45 -5
- data/lib/graphql/schema/visibility/migration.rb +2 -2
- data/lib/graphql/schema/visibility/profile.rb +140 -56
- data/lib/graphql/schema/visibility.rb +31 -18
- data/lib/graphql/schema/wrapper.rb +7 -1
- data/lib/graphql/schema.rb +108 -32
- data/lib/graphql/static_validation/all_rules.rb +1 -1
- data/lib/graphql/static_validation/base_visitor.rb +90 -66
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
- data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
- data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
- data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
- data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +6 -2
- data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
- data/lib/graphql/static_validation/validation_context.rb +1 -1
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +34 -10
- data/lib/graphql/subscriptions/event.rb +1 -0
- data/lib/graphql/subscriptions.rb +36 -1
- data/lib/graphql/testing/helpers.rb +12 -9
- data/lib/graphql/testing/mock_action_cable.rb +111 -0
- data/lib/graphql/testing.rb +1 -0
- data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
- data/lib/graphql/tracing/detailed_trace.rb +70 -7
- data/lib/graphql/tracing/null_trace.rb +1 -1
- data/lib/graphql/tracing/perfetto_trace.rb +209 -79
- data/lib/graphql/tracing/sentry_trace.rb +3 -1
- data/lib/graphql/tracing/trace.rb +6 -0
- data/lib/graphql/type_kinds.rb +1 -0
- data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
- data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
- data/lib/graphql/types/relay/has_node_field.rb +13 -8
- data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
- data/lib/graphql/types/relay/node_behaviors.rb +13 -2
- data/lib/graphql/unauthorized_error.rb +9 -1
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +7 -3
- metadata +21 -3
|
@@ -28,10 +28,10 @@ module GraphQL
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def add_conflict(node, conflict_str)
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
if nodes.any? { |n| n
|
|
31
|
+
# Check if we already have an error for this exact node.
|
|
32
|
+
# Use object identity first (fast path), then fall back to
|
|
33
|
+
# value + location comparison for duplicate AST nodes.
|
|
34
|
+
if nodes.any? { |n| n.equal?(node) || (n.line == node.line && n.col == node.col && n == node) }
|
|
35
35
|
# already have an error for this node
|
|
36
36
|
return
|
|
37
37
|
end
|
|
@@ -8,8 +8,8 @@ module GraphQL
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def on_inline_fragment(node, parent)
|
|
11
|
-
fragment_parent =
|
|
12
|
-
fragment_child =
|
|
11
|
+
fragment_parent = @parent_object_type
|
|
12
|
+
fragment_child = @current_object_type
|
|
13
13
|
if fragment_child
|
|
14
14
|
validate_fragment_in_scope(fragment_parent, fragment_child, node, context, context.path)
|
|
15
15
|
end
|
|
@@ -17,7 +17,7 @@ module GraphQL
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def on_fragment_spread(node, parent)
|
|
20
|
-
fragment_parent =
|
|
20
|
+
fragment_parent = @current_object_type
|
|
21
21
|
@spreads_to_validate << FragmentSpread.new(node: node, parent_type: fragment_parent, path: context.path)
|
|
22
22
|
super
|
|
23
23
|
end
|
|
@@ -23,18 +23,21 @@ module GraphQL
|
|
|
23
23
|
type_name = fragment_node.type.name
|
|
24
24
|
type = @types.type(type_name)
|
|
25
25
|
if type.nil?
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
suggestion = if @schema.did_you_mean
|
|
27
|
+
@all_possible_fragment_type_names ||= begin
|
|
28
|
+
names = []
|
|
29
|
+
context.types.all_types.each do |type|
|
|
30
|
+
if type.kind.fields?
|
|
31
|
+
names << type.graphql_name
|
|
32
|
+
end
|
|
31
33
|
end
|
|
34
|
+
names
|
|
32
35
|
end
|
|
33
|
-
|
|
36
|
+
context.did_you_mean_suggestion(type_name, @all_possible_fragment_type_names)
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
add_error(GraphQL::StaticValidation::FragmentTypesExistError.new(
|
|
37
|
-
"No such type #{type_name}, so it can't be a fragment condition#{
|
|
40
|
+
"No such type #{type_name}, so it can't be a fragment condition#{suggestion}",
|
|
38
41
|
nodes: fragment_node,
|
|
39
42
|
type: type_name
|
|
40
43
|
))
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
module GraphQL
|
|
3
3
|
module StaticValidation
|
|
4
4
|
module RequiredArgumentsArePresent
|
|
5
|
+
def initialize(*)
|
|
6
|
+
super
|
|
7
|
+
@required_args_cache = {}.compare_by_identity
|
|
8
|
+
end
|
|
9
|
+
|
|
5
10
|
def on_field(node, _parent)
|
|
6
|
-
assert_required_args(node,
|
|
11
|
+
assert_required_args(node, @current_field_definition)
|
|
7
12
|
super
|
|
8
13
|
end
|
|
9
14
|
|
|
@@ -16,13 +21,28 @@ module GraphQL
|
|
|
16
21
|
private
|
|
17
22
|
|
|
18
23
|
def assert_required_args(ast_node, defn)
|
|
19
|
-
|
|
20
|
-
return if args.empty?
|
|
21
|
-
present_argument_names = ast_node.arguments.map(&:name)
|
|
22
|
-
required_argument_names = context.query.types.arguments(defn)
|
|
23
|
-
.select { |a| a.type.kind.non_null? && !a.default_value? && context.query.types.argument(defn, a.name) }
|
|
24
|
-
.map!(&:name)
|
|
24
|
+
return unless defn
|
|
25
25
|
|
|
26
|
+
# Cache required argument names per definition to avoid re-iterating
|
|
27
|
+
# arguments for the same definition across field instances
|
|
28
|
+
if @required_args_cache.key?(defn)
|
|
29
|
+
required_argument_names = @required_args_cache[defn]
|
|
30
|
+
else
|
|
31
|
+
args = @types.arguments(defn)
|
|
32
|
+
required_argument_names = nil
|
|
33
|
+
if !args.empty?
|
|
34
|
+
args.each do |a|
|
|
35
|
+
if a.type.kind.non_null? && !a.default_value? && @types.argument(defn, a.name)
|
|
36
|
+
(required_argument_names ||= []) << a.graphql_name
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
@required_args_cache[defn] = required_argument_names
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
return if required_argument_names.nil?
|
|
44
|
+
|
|
45
|
+
present_argument_names = ast_node.arguments.map(&:name)
|
|
26
46
|
missing_names = required_argument_names - present_argument_names
|
|
27
47
|
if !missing_names.empty?
|
|
28
48
|
add_error(GraphQL::StaticValidation::RequiredArgumentsArePresentError.new(
|
|
@@ -19,13 +19,17 @@ module GraphQL
|
|
|
19
19
|
:on_field,
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
VALIDATE_DIRECTIVE_LOCATION_ON_NODE = <<~RUBY
|
|
23
|
+
def %{method_name}(node, parent)
|
|
24
24
|
if !node.directives.empty?
|
|
25
25
|
validate_directive_location(node)
|
|
26
26
|
end
|
|
27
27
|
super(node, parent)
|
|
28
28
|
end
|
|
29
|
+
RUBY
|
|
30
|
+
DIRECTIVE_NODE_HOOKS.each do |method_name|
|
|
31
|
+
# Can't use `define_method {...}` here because the proc can't be isolated for use in non-main Ractors
|
|
32
|
+
module_eval(VALIDATE_DIRECTIVE_LOCATION_ON_NODE % { method_name: method_name }) # rubocop:disable Development/NoEvalCop
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
private
|
|
@@ -7,17 +7,20 @@ module GraphQL
|
|
|
7
7
|
type = context.query.types.type(type_name)
|
|
8
8
|
|
|
9
9
|
if type.nil?
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
suggestion = if @schema.did_you_mean
|
|
11
|
+
@all_possible_input_type_names ||= begin
|
|
12
|
+
names = []
|
|
13
|
+
context.types.all_types.each { |(t)|
|
|
14
|
+
if t.kind.input?
|
|
15
|
+
names << t.graphql_name
|
|
16
|
+
end
|
|
17
|
+
}
|
|
18
|
+
names
|
|
19
|
+
end
|
|
20
|
+
context.did_you_mean_suggestion(type_name, @all_possible_input_type_names)
|
|
18
21
|
end
|
|
19
22
|
add_error(GraphQL::StaticValidation::VariablesAreInputTypesError.new(
|
|
20
|
-
"#{type_name} isn't a defined input type (on $#{node.name})#{
|
|
23
|
+
"#{type_name} isn't a defined input type (on $#{node.name})#{suggestion}",
|
|
21
24
|
nodes: node,
|
|
22
25
|
name: node.name,
|
|
23
26
|
type: type_name
|
|
@@ -32,7 +32,7 @@ module GraphQL
|
|
|
32
32
|
# TODO stop using def_delegators because of Array allocations
|
|
33
33
|
def_delegators :@visitor,
|
|
34
34
|
:path, :type_definition, :field_definition, :argument_definition,
|
|
35
|
-
:parent_type_definition, :directive_definition, :
|
|
35
|
+
:parent_type_definition, :directive_definition, :dependencies
|
|
36
36
|
|
|
37
37
|
def on_dependency_resolve(&handler)
|
|
38
38
|
@on_dependency_resolve_handlers << handler
|
|
@@ -2,25 +2,49 @@
|
|
|
2
2
|
module GraphQL
|
|
3
3
|
class Subscriptions
|
|
4
4
|
class DefaultSubscriptionResolveExtension < GraphQL::Schema::FieldExtension
|
|
5
|
-
def resolve(context:, object
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
def resolve(context:, object: nil, objects: nil, arguments:)
|
|
6
|
+
if objects
|
|
7
|
+
has_override_implementation = @field.execution_mode != :direct_send
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
if !has_override_implementation
|
|
10
|
+
if context.query.subscription_update?
|
|
11
|
+
objects
|
|
12
|
+
else
|
|
13
|
+
objects.map { |o| context.skip }
|
|
14
|
+
end
|
|
12
15
|
else
|
|
13
|
-
|
|
16
|
+
yield(objects, arguments)
|
|
14
17
|
end
|
|
15
18
|
else
|
|
16
|
-
|
|
19
|
+
has_override_implementation = @field.resolver ||
|
|
20
|
+
object.respond_to?(@field.resolver_method)
|
|
21
|
+
|
|
22
|
+
if !has_override_implementation
|
|
23
|
+
if context.query.subscription_update?
|
|
24
|
+
object.object
|
|
25
|
+
else
|
|
26
|
+
context.skip
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
yield(object, arguments)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def after_resolve(values: nil, value: nil, context:, objects: nil, object: nil, arguments:, **rest)
|
|
35
|
+
if values
|
|
36
|
+
values.map do |value|
|
|
37
|
+
self.class.write_subscription(@field, value, arguments, context)
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
self.class.write_subscription(@field, value, arguments, context)
|
|
17
41
|
end
|
|
18
42
|
end
|
|
19
43
|
|
|
20
|
-
def
|
|
44
|
+
def self.write_subscription(field, value, arguments, context)
|
|
21
45
|
if value.is_a?(GraphQL::ExecutionError)
|
|
22
46
|
value
|
|
23
|
-
elsif
|
|
47
|
+
elsif field.resolver&.method_defined?(:subscription_written?) &&
|
|
24
48
|
(subscription_namespace = context.namespace(:subscriptions)) &&
|
|
25
49
|
(subscriptions_by_path = subscription_namespace[:subscriptions])
|
|
26
50
|
(subscription_instance = subscriptions_by_path[context.current_path])
|
|
@@ -80,7 +80,7 @@ module GraphQL
|
|
|
80
80
|
|
|
81
81
|
# Normalize symbol-keyed args to strings, try camelizing them
|
|
82
82
|
# Should this accept a real context somehow?
|
|
83
|
-
normalized_args = normalize_arguments(normalized_event_name, field, args,
|
|
83
|
+
normalized_args = normalize_arguments(normalized_event_name, field, args, @schema.null_context)
|
|
84
84
|
|
|
85
85
|
event = Subscriptions::Event.new(
|
|
86
86
|
name: normalized_event_name,
|
|
@@ -239,6 +239,41 @@ module GraphQL
|
|
|
239
239
|
query.context.namespace(:subscriptions)[:subscription_broadcastable]
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
+
# Called during execution when a new `subscription ...` operation is received
|
|
243
|
+
# @param query [GraphQL::Query]
|
|
244
|
+
# @return [void]
|
|
245
|
+
def initialize_subscriptions(query)
|
|
246
|
+
subs_namespace = query.context.namespace(:subscriptions)
|
|
247
|
+
subs_namespace[:events] = []
|
|
248
|
+
subs_namespace[:subscriptions] = {}
|
|
249
|
+
nil
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Called during execution when a subscription operation has finished
|
|
253
|
+
# @param query [GraphQL::Query]
|
|
254
|
+
# @return [void]
|
|
255
|
+
def finish_subscriptions(query)
|
|
256
|
+
if (events = query.context.namespace(:subscriptions)[:events]) && !events.empty?
|
|
257
|
+
write_subscription(query, events)
|
|
258
|
+
end
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def finalizer
|
|
263
|
+
Finalizer.new(self)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
class Finalizer
|
|
267
|
+
include Execution::Finalizer
|
|
268
|
+
def initialize(subscriptions)
|
|
269
|
+
@subscriptions = subscriptions
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def finalize_graphql_result(query, result_data, result_key)
|
|
273
|
+
@subscriptions.finish_subscriptions(query)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
242
277
|
private
|
|
243
278
|
|
|
244
279
|
# Recursively normalize `args` as belonging to `arg_owner`:
|
|
@@ -39,9 +39,9 @@ module GraphQL
|
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def run_graphql_field(schema, field_path, object, arguments: {}, context: {}, ast_node: nil, lookahead: nil)
|
|
42
|
+
def run_graphql_field(schema, field_path, object, arguments: {}, context: {}, ast_node: nil, lookahead: nil, visibility_profile: nil)
|
|
43
43
|
type_name, *field_names = field_path.split(".")
|
|
44
|
-
dummy_query = GraphQL::Query.new(schema, "{ __typename }", context: context)
|
|
44
|
+
dummy_query = GraphQL::Query.new(schema, "{ __typename }", context: context, visibility_profile: visibility_profile)
|
|
45
45
|
query_context = dummy_query.context
|
|
46
46
|
dataloader = query_context.dataloader
|
|
47
47
|
object_type = dummy_query.types.type(type_name) # rubocop:disable Development/ContextIsPassedCop
|
|
@@ -104,32 +104,35 @@ module GraphQL
|
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
def with_resolution_context(schema, type:, object:, context:{})
|
|
107
|
+
def with_resolution_context(schema, type:, object:, context:{}, visibility_profile: nil)
|
|
108
108
|
resolution_context = ResolutionAssertionContext.new(
|
|
109
109
|
self,
|
|
110
110
|
schema: schema,
|
|
111
111
|
type_name: type,
|
|
112
112
|
object: object,
|
|
113
|
-
context: context
|
|
113
|
+
context: context,
|
|
114
|
+
visibility_profile: visibility_profile,
|
|
114
115
|
)
|
|
115
116
|
yield(resolution_context)
|
|
116
117
|
end
|
|
117
118
|
|
|
118
119
|
class ResolutionAssertionContext
|
|
119
|
-
def initialize(test, type_name:, object:, schema:, context:)
|
|
120
|
+
def initialize(test, type_name:, object:, schema:, context:, visibility_profile:)
|
|
120
121
|
@test = test
|
|
121
122
|
@type_name = type_name
|
|
122
123
|
@object = object
|
|
123
124
|
@schema = schema
|
|
124
125
|
@context = context
|
|
126
|
+
@visibility_profile = visibility_profile
|
|
125
127
|
end
|
|
126
128
|
|
|
129
|
+
attr_reader :visibility_profile
|
|
127
130
|
|
|
128
131
|
def run_graphql_field(field_name, arguments: {})
|
|
129
132
|
if @schema
|
|
130
|
-
@test.run_graphql_field(@schema, "#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context)
|
|
133
|
+
@test.run_graphql_field(@schema, "#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context, visibility_profile: @visibility_profile)
|
|
131
134
|
else
|
|
132
|
-
@test.run_graphql_field("#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context)
|
|
135
|
+
@test.run_graphql_field("#{@type_name}.#{field_name}", @object, arguments: arguments, context: @context, visibility_profile: @visibility_profile)
|
|
133
136
|
end
|
|
134
137
|
end
|
|
135
138
|
end
|
|
@@ -137,8 +140,8 @@ module GraphQL
|
|
|
137
140
|
module SchemaHelpers
|
|
138
141
|
include Helpers
|
|
139
142
|
|
|
140
|
-
def run_graphql_field(field_path, object, arguments: {}, context: {})
|
|
141
|
-
super(@@schema_class_for_helpers, field_path, object, arguments: arguments, context: context)
|
|
143
|
+
def run_graphql_field(field_path, object, arguments: {}, context: {}, visibility_profile: nil)
|
|
144
|
+
super(@@schema_class_for_helpers, field_path, object, arguments: arguments, context: context, visibility_profile: visibility_profile)
|
|
142
145
|
end
|
|
143
146
|
|
|
144
147
|
def with_resolution_context(*args, **kwargs, &block)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
module Testing
|
|
4
|
+
# A stub implementation of ActionCable.
|
|
5
|
+
# Any methods to support the mock backend have `mock` in the name.
|
|
6
|
+
#
|
|
7
|
+
# @example Configuring your schema to use MockActionCable in the test environment
|
|
8
|
+
# class MySchema < GraphQL::Schema
|
|
9
|
+
# # Use MockActionCable in test:
|
|
10
|
+
# use GraphQL::Subscriptions::ActionCableSubscriptions,
|
|
11
|
+
# action_cable: Rails.env.test? ? GraphQL::Testing::MockActionCable : ActionCable
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Clearing old data before each test
|
|
15
|
+
# setup do
|
|
16
|
+
# GraphQL::Testing::MockActionCable.clear_mocks
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Using MockActionCable in a test case
|
|
20
|
+
# # Create a channel to use in the test, pass it to GraphQL
|
|
21
|
+
# mock_channel = GraphQL::Testing::MockActionCable.get_mock_channel
|
|
22
|
+
# ActionCableTestSchema.execute("subscription { newsFlash { text } }", context: { channel: mock_channel })
|
|
23
|
+
#
|
|
24
|
+
# # Trigger a subscription update
|
|
25
|
+
# ActionCableTestSchema.subscriptions.trigger(:news_flash, {}, {text: "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"})
|
|
26
|
+
#
|
|
27
|
+
# # Check messages on the channel
|
|
28
|
+
# expected_msg = {
|
|
29
|
+
# result: {
|
|
30
|
+
# "data" => {
|
|
31
|
+
# "newsFlash" => {
|
|
32
|
+
# "text" => "After yesterday's rain, someone stopped on Rio Road to help a box turtle across five lanes of traffic"
|
|
33
|
+
# }
|
|
34
|
+
# }
|
|
35
|
+
# },
|
|
36
|
+
# more: true,
|
|
37
|
+
# }
|
|
38
|
+
# assert_equal [expected_msg], mock_channel.mock_broadcasted_messages
|
|
39
|
+
#
|
|
40
|
+
class MockActionCable
|
|
41
|
+
class MockChannel
|
|
42
|
+
def initialize
|
|
43
|
+
@mock_broadcasted_messages = []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Array<Hash>] Payloads "sent" to this channel by GraphQL-Ruby
|
|
47
|
+
attr_reader :mock_broadcasted_messages
|
|
48
|
+
|
|
49
|
+
# Called by ActionCableSubscriptions. Implements a Rails API.
|
|
50
|
+
def stream_from(stream_name, coder: nil, &block)
|
|
51
|
+
# Rails uses `coder`, we don't
|
|
52
|
+
block ||= ->(msg) { @mock_broadcasted_messages << msg }
|
|
53
|
+
MockActionCable.mock_stream_for(stream_name).add_mock_channel(self, block)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Used by mock code
|
|
58
|
+
# @api private
|
|
59
|
+
class MockStream
|
|
60
|
+
def initialize
|
|
61
|
+
@mock_channels = {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def add_mock_channel(channel, handler)
|
|
65
|
+
@mock_channels[channel] = handler
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def mock_broadcast(message)
|
|
69
|
+
@mock_channels.each do |channel, handler|
|
|
70
|
+
handler && handler.call(message)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
# Call this before each test run to make sure that MockActionCable's data is empty
|
|
77
|
+
def clear_mocks
|
|
78
|
+
@mock_streams = {}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Implements Rails API
|
|
82
|
+
def server
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Implements Rails API
|
|
87
|
+
def broadcast(stream_name, message)
|
|
88
|
+
stream = @mock_streams[stream_name]
|
|
89
|
+
stream && stream.mock_broadcast(message)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Used by mock code
|
|
93
|
+
def mock_stream_for(stream_name)
|
|
94
|
+
@mock_streams[stream_name] ||= MockStream.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Use this as `context[:channel]` to simulate an ActionCable channel
|
|
98
|
+
#
|
|
99
|
+
# @return [GraphQL::Testing::MockActionCable::MockChannel]
|
|
100
|
+
def get_mock_channel
|
|
101
|
+
MockChannel.new
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [Array<String>] Streams that currently have subscribers
|
|
105
|
+
def mock_stream_names
|
|
106
|
+
@mock_streams.keys
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/graphql/testing.rb
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphQL
|
|
4
|
+
module Tracing
|
|
5
|
+
class DetailedTrace
|
|
6
|
+
class ActiveRecordBackend
|
|
7
|
+
class GraphqlDetailedTrace < ActiveRecord::Base
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(limit: nil, model_class: nil)
|
|
11
|
+
@limit = limit
|
|
12
|
+
@model_class = model_class || GraphqlDetailedTrace
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def traces(last:, before:)
|
|
16
|
+
gdts = @model_class.all.order("begin_ms DESC")
|
|
17
|
+
if before
|
|
18
|
+
gdts = gdts.where("begin_ms < ?", before)
|
|
19
|
+
end
|
|
20
|
+
if last
|
|
21
|
+
gdts = gdts.limit(last)
|
|
22
|
+
end
|
|
23
|
+
gdts.map { |gdt| record_to_stored_trace(gdt) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def delete_trace(id)
|
|
27
|
+
@model_class.where(id: id).destroy_all
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete_all_traces
|
|
32
|
+
@model_class.all.destroy_all
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def find_trace(id)
|
|
36
|
+
gdt = @model_class.find_by(id: id)
|
|
37
|
+
if gdt
|
|
38
|
+
record_to_stored_trace(gdt)
|
|
39
|
+
else
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def save_trace(operation_name, duration_ms, begin_ms, trace_data)
|
|
45
|
+
gdt = @model_class.create!(
|
|
46
|
+
begin_ms: begin_ms,
|
|
47
|
+
operation_name: operation_name,
|
|
48
|
+
duration_ms: duration_ms,
|
|
49
|
+
trace_data: trace_data,
|
|
50
|
+
)
|
|
51
|
+
if @limit
|
|
52
|
+
@model_class
|
|
53
|
+
.where("id NOT IN(SELECT id FROM graphql_detailed_traces ORDER BY begin_ms DESC LIMIT ?)", @limit)
|
|
54
|
+
.delete_all
|
|
55
|
+
end
|
|
56
|
+
gdt.id
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def record_to_stored_trace(gdt)
|
|
62
|
+
StoredTrace.new(
|
|
63
|
+
id: gdt.id,
|
|
64
|
+
begin_ms: gdt.begin_ms,
|
|
65
|
+
operation_name: gdt.operation_name,
|
|
66
|
+
duration_ms: gdt.duration_ms,
|
|
67
|
+
trace_data: gdt.trace_data
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|