graphql 1.9.17 → 1.11.7
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 +18 -2
- data/lib/generators/graphql/install_generator.rb +27 -0
- data/lib/generators/graphql/object_generator.rb +52 -8
- 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 +14 -10
- 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 +10 -0
- data/lib/generators/graphql/templates/union.erb +3 -1
- data/lib/graphql/analysis/ast/field_usage.rb +1 -1
- data/lib/graphql/analysis/ast/query_complexity.rb +178 -67
- data/lib/graphql/analysis/ast/visitor.rb +3 -3
- data/lib/graphql/analysis/ast.rb +12 -11
- data/lib/graphql/argument.rb +10 -38
- data/lib/graphql/backtrace/table.rb +10 -2
- data/lib/graphql/backtrace/tracer.rb +2 -1
- data/lib/graphql/base_type.rb +4 -0
- data/lib/graphql/compatibility/execution_specification/specification_schema.rb +2 -2
- data/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb +5 -9
- data/lib/graphql/define/assign_enum_value.rb +1 -1
- data/lib/graphql/define/assign_global_id_field.rb +2 -2
- data/lib/graphql/define/assign_object_field.rb +3 -3
- data/lib/graphql/define/defined_object_proxy.rb +3 -0
- data/lib/graphql/define/instance_definable.rb +18 -108
- data/lib/graphql/directive/deprecated_directive.rb +1 -12
- data/lib/graphql/directive.rb +8 -1
- data/lib/graphql/enum_type.rb +5 -71
- data/lib/graphql/execution/directive_checks.rb +2 -2
- data/lib/graphql/execution/errors.rb +2 -3
- data/lib/graphql/execution/execute.rb +1 -1
- data/lib/graphql/execution/instrumentation.rb +1 -1
- data/lib/graphql/execution/interpreter/argument_value.rb +28 -0
- data/lib/graphql/execution/interpreter/arguments.rb +51 -0
- data/lib/graphql/execution/interpreter/arguments_cache.rb +79 -0
- data/lib/graphql/execution/interpreter/handles_raw_value.rb +25 -0
- data/lib/graphql/execution/interpreter/runtime.rb +227 -254
- data/lib/graphql/execution/interpreter.rb +34 -11
- data/lib/graphql/execution/lazy/lazy_method_map.rb +4 -0
- data/lib/graphql/execution/lookahead.rb +39 -114
- data/lib/graphql/execution/multiplex.rb +14 -5
- data/lib/graphql/field.rb +14 -118
- data/lib/graphql/filter.rb +1 -1
- data/lib/graphql/function.rb +1 -30
- data/lib/graphql/input_object_type.rb +6 -24
- data/lib/graphql/integer_decoding_error.rb +17 -0
- data/lib/graphql/interface_type.rb +7 -23
- data/lib/graphql/internal_representation/scope.rb +2 -2
- data/lib/graphql/internal_representation/visit.rb +2 -2
- data/lib/graphql/introspection/base_object.rb +2 -5
- data/lib/graphql/introspection/directive_type.rb +1 -1
- data/lib/graphql/introspection/entry_points.rb +7 -7
- data/lib/graphql/introspection/field_type.rb +7 -3
- data/lib/graphql/introspection/input_value_type.rb +33 -9
- data/lib/graphql/introspection/introspection_query.rb +6 -92
- data/lib/graphql/introspection/schema_type.rb +4 -9
- data/lib/graphql/introspection/type_type.rb +11 -7
- data/lib/graphql/introspection.rb +96 -0
- data/lib/graphql/invalid_null_error.rb +18 -0
- data/lib/graphql/language/block_string.rb +24 -5
- data/lib/graphql/language/definition_slice.rb +21 -10
- data/lib/graphql/language/document_from_schema_definition.rb +89 -64
- data/lib/graphql/language/lexer.rb +7 -3
- data/lib/graphql/language/lexer.rl +7 -3
- data/lib/graphql/language/nodes.rb +52 -91
- data/lib/graphql/language/parser.rb +719 -717
- data/lib/graphql/language/parser.y +104 -98
- data/lib/graphql/language/printer.rb +1 -1
- data/lib/graphql/language/sanitized_printer.rb +222 -0
- data/lib/graphql/language/visitor.rb +2 -2
- data/lib/graphql/language.rb +2 -1
- data/lib/graphql/name_validator.rb +6 -7
- data/lib/graphql/non_null_type.rb +0 -10
- data/lib/graphql/object_type.rb +45 -56
- data/lib/graphql/pagination/active_record_relation_connection.rb +41 -0
- data/lib/graphql/pagination/array_connection.rb +77 -0
- data/lib/graphql/pagination/connection.rb +208 -0
- data/lib/graphql/pagination/connections.rb +145 -0
- data/lib/graphql/pagination/mongoid_relation_connection.rb +25 -0
- data/lib/graphql/pagination/relation_connection.rb +185 -0
- data/lib/graphql/pagination/sequel_dataset_connection.rb +28 -0
- data/lib/graphql/pagination.rb +6 -0
- data/lib/graphql/query/arguments.rb +4 -2
- data/lib/graphql/query/context.rb +36 -9
- data/lib/graphql/query/fingerprint.rb +26 -0
- data/lib/graphql/query/input_validation_result.rb +23 -6
- data/lib/graphql/query/literal_input.rb +30 -10
- data/lib/graphql/query/null_context.rb +5 -1
- data/lib/graphql/query/validation_pipeline.rb +4 -1
- data/lib/graphql/query/variable_validation_error.rb +1 -1
- data/lib/graphql/query/variables.rb +16 -7
- data/lib/graphql/query.rb +64 -15
- data/lib/graphql/rake_task/validate.rb +3 -0
- data/lib/graphql/rake_task.rb +9 -9
- data/lib/graphql/relay/array_connection.rb +10 -12
- data/lib/graphql/relay/base_connection.rb +23 -13
- data/lib/graphql/relay/connection_type.rb +2 -1
- data/lib/graphql/relay/edge_type.rb +1 -0
- data/lib/graphql/relay/edges_instrumentation.rb +1 -1
- data/lib/graphql/relay/mutation.rb +1 -86
- data/lib/graphql/relay/node.rb +2 -2
- data/lib/graphql/relay/range_add.rb +14 -5
- data/lib/graphql/relay/relation_connection.rb +8 -10
- data/lib/graphql/scalar_type.rb +15 -59
- data/lib/graphql/schema/argument.rb +113 -11
- data/lib/graphql/schema/base_64_encoder.rb +2 -0
- data/lib/graphql/schema/build_from_definition/resolve_map/default_resolve.rb +1 -1
- data/lib/graphql/schema/build_from_definition/resolve_map.rb +13 -5
- data/lib/graphql/schema/build_from_definition.rb +212 -190
- data/lib/graphql/schema/built_in_types.rb +5 -5
- data/lib/graphql/schema/default_type_error.rb +2 -0
- data/lib/graphql/schema/directive/deprecated.rb +18 -0
- data/lib/graphql/schema/directive/include.rb +1 -1
- data/lib/graphql/schema/directive/skip.rb +1 -1
- data/lib/graphql/schema/directive.rb +34 -3
- data/lib/graphql/schema/enum.rb +52 -4
- data/lib/graphql/schema/enum_value.rb +6 -1
- data/lib/graphql/schema/field/connection_extension.rb +44 -20
- data/lib/graphql/schema/field/scope_extension.rb +1 -1
- data/lib/graphql/schema/field.rb +200 -129
- data/lib/graphql/schema/find_inherited_value.rb +13 -0
- data/lib/graphql/schema/finder.rb +13 -11
- data/lib/graphql/schema/input_object.rb +131 -22
- data/lib/graphql/schema/interface.rb +26 -8
- data/lib/graphql/schema/introspection_system.rb +108 -37
- data/lib/graphql/schema/late_bound_type.rb +3 -2
- data/lib/graphql/schema/list.rb +47 -0
- data/lib/graphql/schema/loader.rb +134 -96
- data/lib/graphql/schema/member/base_dsl_methods.rb +29 -12
- data/lib/graphql/schema/member/build_type.rb +19 -5
- data/lib/graphql/schema/member/cached_graphql_definition.rb +5 -0
- data/lib/graphql/schema/member/has_arguments.rb +105 -5
- data/lib/graphql/schema/member/has_ast_node.rb +20 -0
- data/lib/graphql/schema/member/has_fields.rb +20 -10
- data/lib/graphql/schema/member/has_unresolved_type_error.rb +15 -0
- data/lib/graphql/schema/member/type_system_helpers.rb +2 -2
- data/lib/graphql/schema/member/validates_input.rb +33 -0
- data/lib/graphql/schema/member.rb +6 -0
- data/lib/graphql/schema/mutation.rb +5 -1
- data/lib/graphql/schema/non_null.rb +30 -0
- data/lib/graphql/schema/object.rb +65 -12
- data/lib/graphql/schema/possible_types.rb +9 -4
- data/lib/graphql/schema/printer.rb +0 -15
- data/lib/graphql/schema/relay_classic_mutation.rb +5 -3
- data/lib/graphql/schema/resolver/has_payload_type.rb +5 -2
- data/lib/graphql/schema/resolver.rb +26 -18
- data/lib/graphql/schema/scalar.rb +27 -3
- data/lib/graphql/schema/subscription.rb +8 -18
- data/lib/graphql/schema/timeout.rb +29 -15
- data/lib/graphql/schema/traversal.rb +1 -1
- data/lib/graphql/schema/type_expression.rb +21 -13
- data/lib/graphql/schema/type_membership.rb +2 -2
- data/lib/graphql/schema/union.rb +37 -3
- data/lib/graphql/schema/unique_within_type.rb +1 -2
- data/lib/graphql/schema/validation.rb +10 -2
- data/lib/graphql/schema/warden.rb +115 -29
- data/lib/graphql/schema.rb +903 -195
- data/lib/graphql/static_validation/all_rules.rb +1 -0
- data/lib/graphql/static_validation/base_visitor.rb +10 -6
- data/lib/graphql/static_validation/literal_validator.rb +52 -27
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +43 -83
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +17 -5
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +33 -25
- data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +1 -1
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -4
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +5 -5
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +29 -21
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
- data/lib/graphql/static_validation/rules/input_object_names_are_unique.rb +30 -0
- data/lib/graphql/static_validation/rules/input_object_names_are_unique_error.rb +30 -0
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -2
- data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +4 -5
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +12 -13
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +5 -6
- data/lib/graphql/static_validation/rules/variables_are_input_types.rb +1 -1
- data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +5 -3
- data/lib/graphql/static_validation/type_stack.rb +2 -2
- data/lib/graphql/static_validation/validation_context.rb +1 -1
- data/lib/graphql/static_validation/validation_timeout_error.rb +25 -0
- data/lib/graphql/static_validation/validator.rb +30 -8
- data/lib/graphql/static_validation.rb +1 -0
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +89 -19
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +84 -0
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +21 -0
- data/lib/graphql/subscriptions/event.rb +23 -5
- data/lib/graphql/subscriptions/instrumentation.rb +10 -5
- data/lib/graphql/subscriptions/serialize.rb +22 -4
- data/lib/graphql/subscriptions/subscription_root.rb +15 -5
- data/lib/graphql/subscriptions.rb +108 -35
- data/lib/graphql/tracing/active_support_notifications_tracing.rb +14 -10
- data/lib/graphql/tracing/appoptics_tracing.rb +171 -0
- data/lib/graphql/tracing/appsignal_tracing.rb +8 -0
- data/lib/graphql/tracing/data_dog_tracing.rb +8 -0
- data/lib/graphql/tracing/new_relic_tracing.rb +9 -12
- data/lib/graphql/tracing/platform_tracing.rb +53 -9
- data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +4 -1
- data/lib/graphql/tracing/prometheus_tracing.rb +8 -0
- data/lib/graphql/tracing/scout_tracing.rb +19 -0
- data/lib/graphql/tracing/skylight_tracing.rb +8 -0
- data/lib/graphql/tracing/statsd_tracing.rb +42 -0
- data/lib/graphql/tracing.rb +14 -34
- data/lib/graphql/types/big_int.rb +1 -1
- data/lib/graphql/types/int.rb +9 -2
- data/lib/graphql/types/iso_8601_date.rb +3 -3
- data/lib/graphql/types/iso_8601_date_time.rb +25 -10
- data/lib/graphql/types/relay/base_connection.rb +11 -7
- data/lib/graphql/types/relay/base_edge.rb +2 -1
- data/lib/graphql/types/string.rb +7 -1
- data/lib/graphql/unauthorized_error.rb +1 -1
- data/lib/graphql/union_type.rb +13 -28
- data/lib/graphql/unresolved_type_error.rb +2 -2
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +31 -6
- data/readme.md +1 -1
- metadata +34 -9
- data/lib/graphql/literal_validation_error.rb +0 -6
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
module StaticValidation
|
4
|
+
module InputObjectNamesAreUnique
|
5
|
+
def on_input_object(node, parent)
|
6
|
+
validate_input_fields(node)
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def validate_input_fields(node)
|
13
|
+
input_field_defns = node.arguments
|
14
|
+
input_fields_by_name = Hash.new { |h, k| h[k] = [] }
|
15
|
+
input_field_defns.each { |a| input_fields_by_name[a.name] << a }
|
16
|
+
|
17
|
+
input_fields_by_name.each do |name, defns|
|
18
|
+
if defns.size > 1
|
19
|
+
error = GraphQL::StaticValidation::InputObjectNamesAreUniqueError.new(
|
20
|
+
"There can be only one input field named \"#{name}\"",
|
21
|
+
nodes: defns,
|
22
|
+
name: name
|
23
|
+
)
|
24
|
+
add_error(error)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
module StaticValidation
|
4
|
+
class InputObjectNamesAreUniqueError < StaticValidation::Error
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(message, path: nil, nodes: [], name:)
|
8
|
+
super(message, path: path, nodes: nodes)
|
9
|
+
@name = name
|
10
|
+
end
|
11
|
+
|
12
|
+
# A hash representation of this Message
|
13
|
+
def to_h
|
14
|
+
extensions = {
|
15
|
+
"code" => code,
|
16
|
+
"name" => name
|
17
|
+
}
|
18
|
+
|
19
|
+
super.merge({
|
20
|
+
"extensions" => extensions
|
21
|
+
})
|
22
|
+
end
|
23
|
+
|
24
|
+
def code
|
25
|
+
"inputFieldNotUnique"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -17,8 +17,8 @@ module GraphQL
|
|
17
17
|
|
18
18
|
def assert_required_args(ast_node, defn)
|
19
19
|
present_argument_names = ast_node.arguments.map(&:name)
|
20
|
-
required_argument_names =
|
21
|
-
.select { |a| a.type.kind.non_null? && !a.default_value? }
|
20
|
+
required_argument_names = defn.arguments.each_value
|
21
|
+
.select { |a| a.type.kind.non_null? && !a.default_value? && context.warden.get_argument(defn, a.name) }
|
22
22
|
.map(&:name)
|
23
23
|
|
24
24
|
missing_names = required_argument_names - present_argument_names
|
@@ -26,8 +26,7 @@ module GraphQL
|
|
26
26
|
context.field_definition
|
27
27
|
end
|
28
28
|
|
29
|
-
parent_type = context.warden.
|
30
|
-
.find{|f| f.name == parent_name(parent, defn) }
|
29
|
+
parent_type = context.warden.get_argument(defn, parent_name(parent, defn))
|
31
30
|
parent_type ? parent_type.type.unwrap : nil
|
32
31
|
end
|
33
32
|
|
@@ -46,10 +45,10 @@ module GraphQL
|
|
46
45
|
path = [*context.path, missing_field]
|
47
46
|
missing_field_type = parent_type.arguments[missing_field].type
|
48
47
|
add_error(RequiredInputObjectAttributesArePresentError.new(
|
49
|
-
"Argument '#{missing_field}' on InputObject '#{parent_type}' is required. Expected type #{missing_field_type}",
|
48
|
+
"Argument '#{missing_field}' on InputObject '#{parent_type.to_type_signature}' is required. Expected type #{missing_field_type.to_type_signature}",
|
50
49
|
argument_name: missing_field,
|
51
|
-
argument_type: missing_field_type.
|
52
|
-
input_object_type: parent_type.
|
50
|
+
argument_type: missing_field_type.to_type_signature,
|
51
|
+
input_object_type: parent_type.to_type_signature,
|
53
52
|
path: path,
|
54
53
|
nodes: ast_node,
|
55
54
|
))
|
@@ -13,27 +13,26 @@ module GraphQL
|
|
13
13
|
error_type: VariableDefaultValuesAreCorrectlyTypedError::VIOLATIONS[:INVALID_ON_NON_NULL]
|
14
14
|
))
|
15
15
|
else
|
16
|
-
type = context.schema.type_from_ast(node.type)
|
16
|
+
type = context.schema.type_from_ast(node.type, context: context)
|
17
17
|
if type.nil?
|
18
18
|
# This is handled by another validator
|
19
19
|
else
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
20
|
+
validation_result = context.validate_literal(value, type)
|
21
|
+
|
22
|
+
if !validation_result.valid?
|
23
|
+
problems = validation_result.problems
|
24
|
+
first_problem = problems && problems.first
|
25
|
+
if first_problem
|
26
|
+
error_message = first_problem["message"]
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
error_message ||= "Default value for $#{node.name} doesn't match type #{type}"
|
30
|
-
VariableDefaultValuesAreCorrectlyTypedError
|
29
|
+
error_message ||= "Default value for $#{node.name} doesn't match type #{type.to_type_signature}"
|
31
30
|
add_error(GraphQL::StaticValidation::VariableDefaultValuesAreCorrectlyTypedError.new(
|
32
31
|
error_message,
|
33
32
|
nodes: node,
|
34
33
|
name: node.name,
|
35
|
-
type: type.
|
36
|
-
error_type: VariableDefaultValuesAreCorrectlyTypedError::VIOLATIONS[:INVALID_TYPE]
|
34
|
+
type: type.to_type_signature,
|
35
|
+
error_type: VariableDefaultValuesAreCorrectlyTypedError::VIOLATIONS[:INVALID_TYPE],
|
37
36
|
))
|
38
37
|
end
|
39
38
|
end
|
@@ -52,17 +52,16 @@ module GraphQL
|
|
52
52
|
private
|
53
53
|
|
54
54
|
def validate_usage(arguments, arg_node, ast_var)
|
55
|
-
var_type = context.schema.type_from_ast(ast_var.type)
|
55
|
+
var_type = context.schema.type_from_ast(ast_var.type, context: context)
|
56
56
|
if var_type.nil?
|
57
57
|
return
|
58
58
|
end
|
59
59
|
if !ast_var.default_value.nil?
|
60
|
-
unless var_type.
|
60
|
+
unless var_type.kind.non_null?
|
61
61
|
# If the value is required, but the argument is not,
|
62
62
|
# and yet there's a non-nil default, then we impliclty
|
63
63
|
# make the argument also a required type.
|
64
|
-
|
65
|
-
var_type = GraphQL::NonNullType.new(of_type: var_type)
|
64
|
+
var_type = var_type.to_non_null_type
|
66
65
|
end
|
67
66
|
end
|
68
67
|
|
@@ -85,10 +84,10 @@ module GraphQL
|
|
85
84
|
|
86
85
|
def create_error(error_message, var_type, ast_var, arg_defn, arg_node)
|
87
86
|
add_error(GraphQL::StaticValidation::VariableUsagesAreAllowedError.new(
|
88
|
-
"#{error_message} on variable $#{ast_var.name} and argument #{arg_node.name} (#{var_type.
|
87
|
+
"#{error_message} on variable $#{ast_var.name} and argument #{arg_node.name} (#{var_type.to_type_signature} / #{arg_defn.type.to_type_signature})",
|
89
88
|
nodes: arg_node,
|
90
89
|
name: ast_var.name,
|
91
|
-
type: var_type.
|
90
|
+
type: var_type.to_type_signature,
|
92
91
|
argument: arg_node.name,
|
93
92
|
error: error_message
|
94
93
|
))
|
@@ -15,7 +15,7 @@ module GraphQL
|
|
15
15
|
))
|
16
16
|
elsif !type.kind.input?
|
17
17
|
add_error(GraphQL::StaticValidation::VariablesAreInputTypesError.new(
|
18
|
-
"#{type.
|
18
|
+
"#{type.graphql_name} isn't a valid input type (on $#{node.name})",
|
19
19
|
nodes: node,
|
20
20
|
name: node.name,
|
21
21
|
type: type_name
|
@@ -2,7 +2,7 @@
|
|
2
2
|
module GraphQL
|
3
3
|
module StaticValidation
|
4
4
|
# The problem is
|
5
|
-
# - Variable usage must be determined at the OperationDefinition level
|
5
|
+
# - Variable $usage must be determined at the OperationDefinition level
|
6
6
|
# - You can't tell how fragments use variables until you visit FragmentDefinitions (which may be at the end of the document)
|
7
7
|
#
|
8
8
|
# So, this validator includes some crazy logic to follow fragment spreads recursively, while avoiding infinite loops.
|
@@ -126,8 +126,9 @@ module GraphQL
|
|
126
126
|
node_variables
|
127
127
|
.select { |name, usage| usage.declared? && !usage.used? }
|
128
128
|
.each { |var_name, usage|
|
129
|
+
declared_by_error_name = usage.declared_by.name || "anonymous #{usage.declared_by.operation_type}"
|
129
130
|
add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new(
|
130
|
-
"Variable $#{var_name} is declared by #{
|
131
|
+
"Variable $#{var_name} is declared by #{declared_by_error_name} but not used",
|
131
132
|
nodes: usage.declared_by,
|
132
133
|
path: usage.path,
|
133
134
|
name: var_name,
|
@@ -139,8 +140,9 @@ module GraphQL
|
|
139
140
|
node_variables
|
140
141
|
.select { |name, usage| usage.used? && !usage.declared? }
|
141
142
|
.each { |var_name, usage|
|
143
|
+
used_by_error_name = usage.used_by.name || "anonymous #{usage.used_by.operation_type}"
|
142
144
|
add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new(
|
143
|
-
"Variable $#{var_name} is used by #{
|
145
|
+
"Variable $#{var_name} is used by #{used_by_error_name} but not declared",
|
144
146
|
nodes: usage.ast_node,
|
145
147
|
path: usage.path,
|
146
148
|
name: var_name,
|
@@ -55,7 +55,7 @@ module GraphQL
|
|
55
55
|
module FragmentWithTypeStrategy
|
56
56
|
def push(stack, node)
|
57
57
|
object_type = if node.type
|
58
|
-
stack.schema.
|
58
|
+
stack.schema.get_type(node.type.name)
|
59
59
|
else
|
60
60
|
stack.object_types.last
|
61
61
|
end
|
@@ -148,7 +148,7 @@ module GraphQL
|
|
148
148
|
if stack.argument_definitions.last
|
149
149
|
arg_type = stack.argument_definitions.last.type.unwrap
|
150
150
|
if arg_type.kind.input_object?
|
151
|
-
argument_defn = arg_type.
|
151
|
+
argument_defn = arg_type.arguments[node.name]
|
152
152
|
else
|
153
153
|
argument_defn = nil
|
154
154
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
module StaticValidation
|
4
|
+
class ValidationTimeoutError < StaticValidation::Error
|
5
|
+
def initialize(message, path: nil, nodes: [])
|
6
|
+
super(message, path: path, nodes: nodes)
|
7
|
+
end
|
8
|
+
|
9
|
+
# A hash representation of this Message
|
10
|
+
def to_h
|
11
|
+
extensions = {
|
12
|
+
"code" => code
|
13
|
+
}
|
14
|
+
|
15
|
+
super.merge({
|
16
|
+
"extensions" => extensions
|
17
|
+
})
|
18
|
+
end
|
19
|
+
|
20
|
+
def code
|
21
|
+
"validationTimeout"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -20,10 +20,12 @@ module GraphQL
|
|
20
20
|
|
21
21
|
# Validate `query` against the schema. Returns an array of message hashes.
|
22
22
|
# @param query [GraphQL::Query]
|
23
|
+
# @param validate [Boolean]
|
24
|
+
# @param timeout [Float] Number of seconds to wait before aborting validation. Any positive number may be used, including Floats to specify fractional seconds.
|
23
25
|
# @return [Array<Hash>]
|
24
|
-
def validate(query, validate: true)
|
26
|
+
def validate(query, validate: true, timeout: nil)
|
25
27
|
query.trace("validate", { validate: validate, query: query }) do
|
26
|
-
can_skip_rewrite = query.context.interpreter? && query.schema.using_ast_analysis?
|
28
|
+
can_skip_rewrite = query.context.interpreter? && query.schema.using_ast_analysis? && query.schema.is_a?(Class)
|
27
29
|
errors = if validate == false && can_skip_rewrite
|
28
30
|
[]
|
29
31
|
else
|
@@ -32,18 +34,29 @@ module GraphQL
|
|
32
34
|
|
33
35
|
context = GraphQL::StaticValidation::ValidationContext.new(query, visitor_class)
|
34
36
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
begin
|
38
|
+
# CAUTION: Usage of the timeout module makes the assumption that validation rules are stateless Ruby code that requires no cleanup if process was interrupted. This means no blocking IO calls, native gems, locks, or `rescue` clauses that must be reached.
|
39
|
+
# A timeout value of 0 or nil will execute the block without any timeout.
|
40
|
+
Timeout::timeout(timeout) do
|
41
|
+
# Attach legacy-style rules.
|
42
|
+
# Only loop through rules if it has legacy-style rules
|
43
|
+
unless (legacy_rules = rules_to_use - GraphQL::StaticValidation::ALL_RULES).empty?
|
44
|
+
legacy_rules.each do |rule_class_or_module|
|
45
|
+
if rule_class_or_module.method_defined?(:validate)
|
46
|
+
rule_class_or_module.new.validate(context)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context.visitor.visit
|
39
52
|
end
|
53
|
+
rescue Timeout::Error
|
54
|
+
handle_timeout(query, context)
|
40
55
|
end
|
41
56
|
|
42
|
-
context.visitor.visit
|
43
57
|
context.errors
|
44
58
|
end
|
45
59
|
|
46
|
-
|
47
60
|
irep = if errors.empty? && context
|
48
61
|
# Only return this if there are no errors and validation was actually run
|
49
62
|
context.visitor.rewrite_document
|
@@ -57,6 +70,15 @@ module GraphQL
|
|
57
70
|
}
|
58
71
|
end
|
59
72
|
end
|
73
|
+
|
74
|
+
# Invoked when static validation times out.
|
75
|
+
# @param query [GraphQL::Query]
|
76
|
+
# @param context [GraphQL::StaticValidation::ValidationContext]
|
77
|
+
def handle_timeout(query, context)
|
78
|
+
context.errors << GraphQL::StaticValidation::ValidationTimeoutError.new(
|
79
|
+
"Timeout on validation of query"
|
80
|
+
)
|
81
|
+
end
|
60
82
|
end
|
61
83
|
end
|
62
84
|
end
|
@@ -4,6 +4,7 @@ require "graphql/static_validation/definition_dependencies"
|
|
4
4
|
require "graphql/static_validation/type_stack"
|
5
5
|
require "graphql/static_validation/validator"
|
6
6
|
require "graphql/static_validation/validation_context"
|
7
|
+
require "graphql/static_validation/validation_timeout_error"
|
7
8
|
require "graphql/static_validation/literal_validator"
|
8
9
|
require "graphql/static_validation/base_visitor"
|
9
10
|
require "graphql/static_validation/no_validate_visitor"
|
@@ -4,13 +4,14 @@ module GraphQL
|
|
4
4
|
# A subscriptions implementation that sends data
|
5
5
|
# as ActionCable broadcastings.
|
6
6
|
#
|
7
|
-
#
|
7
|
+
# Some things to keep in mind:
|
8
8
|
#
|
9
9
|
# - No queueing system; ActiveJob should be added
|
10
10
|
# - Take care to reload context when re-delivering the subscription. (see {Query#subscription_update?})
|
11
|
+
# - Avoid the async ActionCable adapter and use the redis or PostgreSQL adapters instead. Otherwise calling #trigger won't work from background jobs or the Rails console.
|
11
12
|
#
|
12
13
|
# @example Adding ActionCableSubscriptions to your schema
|
13
|
-
# MySchema
|
14
|
+
# class MySchema < GraphQL::Schema
|
14
15
|
# # ...
|
15
16
|
# use GraphQL::Subscriptions::ActionCableSubscriptions
|
16
17
|
# end
|
@@ -26,7 +27,7 @@ module GraphQL
|
|
26
27
|
# variables = ensure_hash(data["variables"])
|
27
28
|
# operation_name = data["operationName"]
|
28
29
|
# context = {
|
29
|
-
# # Re-implement whatever context methods you need
|
30
|
+
# # Re-implement whatever context methods you need
|
30
31
|
# # in this channel or ApplicationCable::Channel
|
31
32
|
# # current_user: current_user,
|
32
33
|
# # Make sure the channel is in the context
|
@@ -41,7 +42,7 @@ module GraphQL
|
|
41
42
|
# })
|
42
43
|
#
|
43
44
|
# payload = {
|
44
|
-
# result: result.
|
45
|
+
# result: result.to_h,
|
45
46
|
# more: result.subscription?,
|
46
47
|
# }
|
47
48
|
#
|
@@ -85,27 +86,32 @@ module GraphQL
|
|
85
86
|
EVENT_PREFIX = "graphql-event:"
|
86
87
|
|
87
88
|
# @param serializer [<#dump(obj), #load(string)] Used for serializing messages before handing them to `.broadcast(msg)`
|
88
|
-
|
89
|
+
# @param namespace [string] Used to namespace events and subscriptions (default: '')
|
90
|
+
def initialize(serializer: Serialize, namespace: '', action_cable: ActionCable, action_cable_coder: ActiveSupport::JSON, **rest)
|
89
91
|
# A per-process map of subscriptions to deliver.
|
90
92
|
# This is provided by Rails, so let's use it
|
91
93
|
@subscriptions = Concurrent::Map.new
|
94
|
+
@events = Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new { |h2, k2| h2[k2] = Concurrent::Array.new } }
|
95
|
+
@action_cable = action_cable
|
96
|
+
@action_cable_coder = action_cable_coder
|
92
97
|
@serializer = serializer
|
98
|
+
@transmit_ns = namespace
|
93
99
|
super
|
94
100
|
end
|
95
101
|
|
96
102
|
# An event was triggered; Push the data over ActionCable.
|
97
103
|
# Subscribers will re-evaluate locally.
|
98
104
|
def execute_all(event, object)
|
99
|
-
stream =
|
105
|
+
stream = stream_event_name(event)
|
100
106
|
message = @serializer.dump(object)
|
101
|
-
|
107
|
+
@action_cable.server.broadcast(stream, message)
|
102
108
|
end
|
103
109
|
|
104
110
|
# This subscription was re-evaluated.
|
105
111
|
# Send it to the specific stream where this client was waiting.
|
106
112
|
def deliver(subscription_id, result)
|
107
113
|
payload = { result: result.to_h, more: true }
|
108
|
-
|
114
|
+
@action_cable.server.broadcast(stream_subscription_name(subscription_id), payload)
|
109
115
|
end
|
110
116
|
|
111
117
|
# A query was run where these events were subscribed to.
|
@@ -115,31 +121,95 @@ module GraphQL
|
|
115
121
|
def write_subscription(query, events)
|
116
122
|
channel = query.context.fetch(:channel)
|
117
123
|
subscription_id = query.context[:subscription_id] ||= build_id
|
118
|
-
stream =
|
124
|
+
stream = stream_subscription_name(subscription_id)
|
119
125
|
channel.stream_from(stream)
|
120
126
|
@subscriptions[subscription_id] = query
|
121
127
|
events.each do |event|
|
122
|
-
|
123
|
-
|
124
|
-
|
128
|
+
# Setup a new listener to run all events with this topic in this process
|
129
|
+
setup_stream(channel, event)
|
130
|
+
# Add this event to the list of events to be updated
|
131
|
+
@events[event.topic][event.fingerprint] << event
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Every subscribing channel is listening here, but only one of them takes any action.
|
136
|
+
# This is so we can reuse payloads when possible, and make one payload to send to
|
137
|
+
# all subscribers.
|
138
|
+
#
|
139
|
+
# But the problem is, any channel could close at any time, so each channel has to
|
140
|
+
# be ready to take over the primary position.
|
141
|
+
#
|
142
|
+
# To make sure there's always one-and-only-one channel building payloads,
|
143
|
+
# let the listener belonging to the first event on the list be
|
144
|
+
# the one to build and publish payloads.
|
145
|
+
#
|
146
|
+
def setup_stream(channel, initial_event)
|
147
|
+
topic = initial_event.topic
|
148
|
+
channel.stream_from(stream_event_name(initial_event), coder: @action_cable_coder) do |message|
|
149
|
+
object = @serializer.load(message)
|
150
|
+
events_by_fingerprint = @events[topic]
|
151
|
+
events_by_fingerprint.each do |_fingerprint, events|
|
152
|
+
if events.any? && events.first == initial_event
|
153
|
+
# The fingerprint has told us that this response should be shared by all subscribers,
|
154
|
+
# so just run it once, then deliver the result to every subscriber
|
155
|
+
first_event = events.first
|
156
|
+
first_subscription_id = first_event.context.fetch(:subscription_id)
|
157
|
+
result = execute_update(first_subscription_id, first_event, object)
|
158
|
+
# Having calculated the result _once_, send the same payload to all subscribers
|
159
|
+
events.each do |event|
|
160
|
+
subscription_id = event.context.fetch(:subscription_id)
|
161
|
+
deliver(subscription_id, result)
|
162
|
+
end
|
163
|
+
end
|
125
164
|
end
|
165
|
+
nil
|
126
166
|
end
|
127
167
|
end
|
128
168
|
|
129
169
|
# Return the query from "storage" (in memory)
|
130
170
|
def read_subscription(subscription_id)
|
131
171
|
query = @subscriptions[subscription_id]
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
172
|
+
if query.nil?
|
173
|
+
# This can happen when a subscription is triggered from an unsubscribed channel,
|
174
|
+
# see https://github.com/rmosolgo/graphql-ruby/issues/2478.
|
175
|
+
# (This `nil` is handled by `#execute_update`)
|
176
|
+
nil
|
177
|
+
else
|
178
|
+
{
|
179
|
+
query_string: query.query_string,
|
180
|
+
variables: query.provided_variables,
|
181
|
+
context: query.context.to_h,
|
182
|
+
operation_name: query.operation_name,
|
183
|
+
}
|
184
|
+
end
|
138
185
|
end
|
139
186
|
|
140
187
|
# The channel was closed, forget about it.
|
141
188
|
def delete_subscription(subscription_id)
|
142
|
-
@subscriptions.delete(subscription_id)
|
189
|
+
query = @subscriptions.delete(subscription_id)
|
190
|
+
# This can be `nil` when `.trigger` happens inside an unsubscribed ActionCable channel,
|
191
|
+
# see https://github.com/rmosolgo/graphql-ruby/issues/2478
|
192
|
+
if query
|
193
|
+
events = query.context.namespace(:subscriptions)[:events]
|
194
|
+
events.each do |event|
|
195
|
+
ev_by_fingerprint = @events[event.topic]
|
196
|
+
ev_for_fingerprint = ev_by_fingerprint[event.fingerprint]
|
197
|
+
ev_for_fingerprint.delete(event)
|
198
|
+
if ev_for_fingerprint.empty?
|
199
|
+
ev_by_fingerprint.delete(event.fingerprint)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
def stream_subscription_name(subscription_id)
|
208
|
+
[SUBSCRIPTION_PREFIX, @transmit_ns, subscription_id].join
|
209
|
+
end
|
210
|
+
|
211
|
+
def stream_event_name(event)
|
212
|
+
[EVENT_PREFIX, @transmit_ns, event.topic].join
|
143
213
|
end
|
144
214
|
end
|
145
215
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
class Subscriptions
|
5
|
+
# Detect whether the current operation:
|
6
|
+
# - Is a subscription operation
|
7
|
+
# - Is completely broadcastable
|
8
|
+
#
|
9
|
+
# Assign the result to `context.namespace(:subscriptions)[:subscription_broadcastable]`
|
10
|
+
# @api private
|
11
|
+
# @see Subscriptions#broadcastable? for a public API
|
12
|
+
class BroadcastAnalyzer < GraphQL::Analysis::AST::Analyzer
|
13
|
+
def initialize(subject)
|
14
|
+
super
|
15
|
+
@default_broadcastable = subject.schema.subscriptions.default_broadcastable
|
16
|
+
# Maybe this will get set to false while analyzing
|
17
|
+
@subscription_broadcastable = true
|
18
|
+
end
|
19
|
+
|
20
|
+
# Only analyze subscription operations
|
21
|
+
def analyze?
|
22
|
+
@query.subscription?
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_enter_field(node, parent, visitor)
|
26
|
+
if (@subscription_broadcastable == false) || visitor.skipping?
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
current_field = visitor.field_definition
|
31
|
+
apply_broadcastable(current_field)
|
32
|
+
|
33
|
+
current_type = visitor.parent_type_definition
|
34
|
+
if current_type.kind.interface?
|
35
|
+
pt = @query.possible_types(current_type)
|
36
|
+
pt.each do |object_type|
|
37
|
+
ot_field = @query.get_field(object_type, current_field.graphql_name)
|
38
|
+
if !ot_field
|
39
|
+
binding.pry
|
40
|
+
end
|
41
|
+
# Inherited fields would be exactly the same object;
|
42
|
+
# only check fields that are overrides of the inherited one
|
43
|
+
if ot_field && ot_field != current_field
|
44
|
+
apply_broadcastable(ot_field)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Assign the result to context.
|
51
|
+
# (This method is allowed to return an error, but we don't need to)
|
52
|
+
# @return [void]
|
53
|
+
def result
|
54
|
+
query.context.namespace(:subscriptions)[:subscription_broadcastable] = @subscription_broadcastable
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Modify `@subscription_broadcastable` based on `field_defn`'s configuration (and/or the default value)
|
61
|
+
def apply_broadcastable(field_defn)
|
62
|
+
current_field_broadcastable = field_defn.introspection? || field_defn.broadcastable?
|
63
|
+
case current_field_broadcastable
|
64
|
+
when nil
|
65
|
+
# If the value wasn't set, mix in the default value:
|
66
|
+
# - If the default is false and the current value is true, make it false
|
67
|
+
# - If the default is true and the current value is true, it stays true
|
68
|
+
# - If the default is false and the current value is false, keep it false
|
69
|
+
# - If the default is true and the current value is false, keep it false
|
70
|
+
@subscription_broadcastable = @subscription_broadcastable && @default_broadcastable
|
71
|
+
when false
|
72
|
+
# One non-broadcastable field is enough to make the whole subscription non-broadcastable
|
73
|
+
@subscription_broadcastable = false
|
74
|
+
when true
|
75
|
+
# Leave `@broadcastable_query` true if it's already true,
|
76
|
+
# but don't _set_ it to true if it was set to false by something else.
|
77
|
+
# Actually, just leave it!
|
78
|
+
else
|
79
|
+
raise ArgumentError, "Unexpected `.broadcastable?` value for #{field_defn.path}: #{current_field_broadcastable}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
class Subscriptions
|
4
|
+
class DefaultSubscriptionResolveExtension < GraphQL::Subscriptions::SubscriptionRoot::Extension
|
5
|
+
def resolve(context:, object:, arguments:)
|
6
|
+
has_override_implementation = @field.resolver ||
|
7
|
+
object.respond_to?(@field.resolver_method)
|
8
|
+
|
9
|
+
if !has_override_implementation
|
10
|
+
if context.query.subscription_update?
|
11
|
+
object.object
|
12
|
+
else
|
13
|
+
context.skip
|
14
|
+
end
|
15
|
+
else
|
16
|
+
yield(object, arguments)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|