graphql 0.12.1 → 0.13.0
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 +31 -41
- data/lib/graphql/argument.rb +23 -21
- data/lib/graphql/base_type.rb +5 -8
- data/lib/graphql/define/assign_argument.rb +5 -2
- data/lib/graphql/define/type_definer.rb +2 -1
- data/lib/graphql/directive.rb +34 -36
- data/lib/graphql/directive/include_directive.rb +3 -7
- data/lib/graphql/directive/skip_directive.rb +3 -7
- data/lib/graphql/enum_type.rb +78 -76
- data/lib/graphql/execution_error.rb +1 -3
- data/lib/graphql/field.rb +99 -95
- data/lib/graphql/input_object_type.rb +49 -47
- data/lib/graphql/interface_type.rb +31 -34
- data/lib/graphql/introspection.rb +19 -18
- data/lib/graphql/introspection/directive_location_enum.rb +8 -0
- data/lib/graphql/introspection/directive_type.rb +1 -3
- data/lib/graphql/introspection/field_type.rb +1 -1
- data/lib/graphql/introspection/fields_field.rb +1 -1
- data/lib/graphql/introspection/introspection_query.rb +1 -3
- data/lib/graphql/introspection/possible_types_field.rb +7 -1
- data/lib/graphql/introspection/schema_field.rb +13 -9
- data/lib/graphql/introspection/type_by_name_field.rb +13 -17
- data/lib/graphql/introspection/typename_field.rb +12 -8
- data/lib/graphql/language.rb +5 -9
- data/lib/graphql/language/lexer.rb +668 -0
- data/lib/graphql/language/lexer.rl +149 -0
- data/lib/graphql/language/parser.rb +842 -116
- data/lib/graphql/language/parser.y +264 -0
- data/lib/graphql/language/token.rb +21 -0
- data/lib/graphql/list_type.rb +33 -31
- data/lib/graphql/non_null_type.rb +33 -31
- data/lib/graphql/object_type.rb +52 -55
- data/lib/graphql/query.rb +83 -80
- data/lib/graphql/query/context.rb +5 -1
- data/lib/graphql/query/directive_resolution.rb +16 -0
- data/lib/graphql/query/executor.rb +3 -3
- data/lib/graphql/query/input_validation_result.rb +17 -15
- data/lib/graphql/query/serial_execution.rb +5 -5
- data/lib/graphql/query/serial_execution/execution_context.rb +4 -3
- data/lib/graphql/query/serial_execution/selection_resolution.rb +19 -21
- data/lib/graphql/query/serial_execution/value_resolution.rb +1 -1
- data/lib/graphql/query/type_resolver.rb +22 -18
- data/lib/graphql/query/variable_validation_error.rb +14 -12
- data/lib/graphql/schema.rb +87 -77
- data/lib/graphql/schema/each_item_validator.rb +16 -12
- data/lib/graphql/schema/field_validator.rb +14 -10
- data/lib/graphql/schema/implementation_validator.rb +26 -22
- data/lib/graphql/schema/middleware_chain.rb +2 -1
- data/lib/graphql/schema/possible_types.rb +34 -0
- data/lib/graphql/schema/printer.rb +122 -120
- data/lib/graphql/schema/type_expression.rb +1 -0
- data/lib/graphql/schema/type_map.rb +3 -10
- data/lib/graphql/schema/type_reducer.rb +65 -81
- data/lib/graphql/schema/type_validator.rb +45 -41
- data/lib/graphql/static_validation.rb +7 -9
- data/lib/graphql/static_validation/all_rules.rb +29 -24
- data/lib/graphql/static_validation/arguments_validator.rb +39 -35
- data/lib/graphql/static_validation/literal_validator.rb +44 -40
- data/lib/graphql/static_validation/message.rb +30 -26
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +15 -11
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +14 -10
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +16 -12
- data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +59 -0
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +25 -21
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +28 -24
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +84 -80
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +49 -43
- data/lib/graphql/static_validation/rules/fragment_types_exist.rb +22 -17
- data/lib/graphql/static_validation/rules/fragments_are_finite.rb +19 -15
- data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +25 -20
- data/lib/graphql/static_validation/rules/fragments_are_used.rb +36 -23
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +29 -25
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +21 -17
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +79 -70
- data/lib/graphql/static_validation/rules/variables_are_input_types.rb +24 -20
- data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +122 -119
- data/lib/graphql/static_validation/type_stack.rb +138 -129
- data/lib/graphql/static_validation/validator.rb +29 -25
- data/lib/graphql/type_kinds.rb +42 -40
- data/lib/graphql/union_type.rb +22 -16
- data/lib/graphql/version.rb +1 -1
- data/readme.md +12 -27
- data/spec/graphql/base_type_spec.rb +3 -3
- data/spec/graphql/directive_spec.rb +10 -18
- data/spec/graphql/enum_type_spec.rb +7 -7
- data/spec/graphql/execution_error_spec.rb +1 -1
- data/spec/graphql/field_spec.rb +14 -13
- data/spec/graphql/id_type_spec.rb +6 -6
- data/spec/graphql/input_object_type_spec.rb +39 -39
- data/spec/graphql/interface_type_spec.rb +16 -32
- data/spec/graphql/introspection/directive_type_spec.rb +5 -9
- data/spec/graphql/introspection/input_value_type_spec.rb +10 -4
- data/spec/graphql/introspection/introspection_query_spec.rb +2 -2
- data/spec/graphql/introspection/schema_type_spec.rb +2 -2
- data/spec/graphql/introspection/type_type_spec.rb +34 -6
- data/spec/graphql/language/parser_spec.rb +299 -105
- data/spec/graphql/language/visitor_spec.rb +4 -4
- data/spec/graphql/list_type_spec.rb +11 -11
- data/spec/graphql/object_type_spec.rb +10 -10
- data/spec/graphql/query/arguments_spec.rb +7 -7
- data/spec/graphql/query/context_spec.rb +11 -3
- data/spec/graphql/query/executor_spec.rb +26 -19
- data/spec/graphql/query/serial_execution/execution_context_spec.rb +6 -6
- data/spec/graphql/query/serial_execution/value_resolution_spec.rb +2 -2
- data/spec/graphql/query/type_resolver_spec.rb +3 -3
- data/spec/graphql/query_spec.rb +6 -38
- data/spec/graphql/scalar_type_spec.rb +28 -19
- data/spec/graphql/schema/field_validator_spec.rb +1 -1
- data/spec/graphql/schema/middleware_chain_spec.rb +12 -1
- data/spec/graphql/schema/printer_spec.rb +12 -4
- data/spec/graphql/schema/rescue_middleware_spec.rb +1 -1
- data/spec/graphql/schema/type_expression_spec.rb +2 -2
- data/spec/graphql/schema/type_reducer_spec.rb +21 -36
- data/spec/graphql/schema/type_validator_spec.rb +9 -9
- data/spec/graphql/schema_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +4 -4
- data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +4 -4
- data/spec/graphql/static_validation/rules/directives_are_defined_spec.rb +5 -5
- data/spec/graphql/static_validation/rules/directives_are_in_valid_locations_spec.rb +39 -0
- data/spec/graphql/static_validation/rules/fields_are_defined_on_type_spec.rb +5 -5
- data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +4 -4
- data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +2 -2
- data/spec/graphql/static_validation/rules/fragment_spreads_are_possible_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragment_types_exist_spec.rb +2 -2
- data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +2 -2
- data/spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb +2 -2
- data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +3 -3
- data/spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb +3 -3
- data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +5 -5
- data/spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb +3 -1
- data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +4 -4
- data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +3 -3
- data/spec/graphql/static_validation/type_stack_spec.rb +3 -2
- data/spec/graphql/static_validation/validator_spec.rb +26 -6
- data/spec/graphql/union_type_spec.rb +5 -4
- data/spec/spec_helper.rb +2 -5
- data/spec/support/dairy_app.rb +30 -9
- data/spec/support/dairy_data.rb +1 -1
- data/spec/support/star_wars_data.rb +26 -26
- data/spec/support/star_wars_schema.rb +1 -1
- metadata +40 -21
- data/lib/graphql/language/transform.rb +0 -113
- data/lib/graphql/query/directive_chain.rb +0 -44
- data/lib/graphql/repl.rb +0 -27
- data/spec/graphql/language/transform_spec.rb +0 -156
@@ -1,48 +1,52 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
1
|
+
module GraphQL
|
2
|
+
module StaticValidation
|
3
|
+
# Test whether `ast_value` is a valid input for `type`
|
4
|
+
class LiteralValidator
|
5
|
+
def validate(ast_value, type)
|
6
|
+
if type.kind.non_null?
|
7
|
+
(!ast_value.nil?) && validate(ast_value, type.of_type)
|
8
|
+
elsif type.kind.list?
|
9
|
+
item_type = type.of_type
|
10
|
+
ensure_array(ast_value).all? { |val| validate(val, item_type) }
|
11
|
+
elsif type.kind.scalar? && !ast_value.is_a?(GraphQL::Language::Nodes::AbstractNode) && !ast_value.is_a?(Array)
|
12
|
+
type.valid_input?(ast_value)
|
13
|
+
elsif type.kind.enum? && ast_value.is_a?(GraphQL::Language::Nodes::Enum)
|
14
|
+
type.valid_input?(ast_value.name)
|
15
|
+
elsif type.kind.input_object? && ast_value.is_a?(GraphQL::Language::Nodes::InputObject)
|
16
|
+
required_input_fields_are_present(type, ast_value) &&
|
17
|
+
present_input_field_values_are_valid(type, ast_value)
|
18
|
+
elsif ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
19
|
+
true
|
20
|
+
else
|
21
|
+
false
|
22
|
+
end
|
23
|
+
end
|
22
24
|
|
23
25
|
|
24
|
-
|
26
|
+
private
|
25
27
|
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
def required_input_fields_are_present(type, ast_node)
|
30
|
+
required_field_names = type.input_fields
|
31
|
+
.values
|
32
|
+
.select { |f| f.type.kind.non_null? }
|
33
|
+
.map(&:name)
|
34
|
+
present_field_names = ast_node.arguments.map(&:name)
|
35
|
+
missing_required_field_names = required_field_names - present_field_names
|
36
|
+
missing_required_field_names.none?
|
37
|
+
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
def present_input_field_values_are_valid(type, ast_node)
|
40
|
+
fields = type.input_fields
|
41
|
+
ast_node.arguments.all? do |value|
|
42
|
+
field = fields[value.name]
|
43
|
+
field ? validate(value.value, field.type) : true
|
44
|
+
end
|
45
|
+
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
+
def ensure_array(value)
|
48
|
+
value.is_a?(Array) ? value : [value]
|
49
|
+
end
|
50
|
+
end
|
47
51
|
end
|
48
52
|
end
|
@@ -1,32 +1,36 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
1
|
+
module GraphQL
|
2
|
+
module StaticValidation
|
3
|
+
# Generates GraphQL-compliant validation message.
|
4
|
+
# Only supports one "location", too bad :(
|
5
|
+
class Message
|
6
|
+
# Convenience for validators
|
7
|
+
module MessageHelper
|
8
|
+
# Error `message` is located at `node`
|
9
|
+
def message(message, node)
|
10
|
+
GraphQL::StaticValidation::Message.new(message, line: node.line, col: node.col)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
attr_reader :message, :line, :col
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
def initialize(message, line: nil, col: nil)
|
16
|
+
@message = message
|
17
|
+
@line = line
|
18
|
+
@col = col
|
19
|
+
end
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
# A hash representation of this Message
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
"message" => message,
|
25
|
+
"locations" => locations
|
26
|
+
}
|
27
|
+
end
|
26
28
|
|
27
|
-
|
29
|
+
private
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
+
def locations
|
32
|
+
@line.nil? && @col.nil? ? [] : [{"line" => @line, "column" => @col}]
|
33
|
+
end
|
34
|
+
end
|
31
35
|
end
|
32
36
|
end
|
@@ -1,14 +1,18 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
1
|
+
module GraphQL
|
2
|
+
module StaticValidation
|
3
|
+
class ArgumentLiteralsAreCompatible < GraphQL::StaticValidation::ArgumentsValidator
|
4
|
+
def validate_node(parent, node, defn, context)
|
5
|
+
return if node.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
6
|
+
validator = GraphQL::StaticValidation::LiteralValidator.new
|
7
|
+
arg_defn = defn.arguments[node.name]
|
8
|
+
return unless arg_defn
|
9
|
+
valid = validator.validate(node.value, arg_defn.type)
|
10
|
+
if !valid
|
11
|
+
kind_of_node = node_type(parent)
|
12
|
+
error_arg_name = parent_name(parent, defn)
|
13
|
+
context.errors << message("Argument '#{node.name}' on #{kind_of_node} '#{error_arg_name}' has an invalid value", parent)
|
14
|
+
end
|
15
|
+
end
|
12
16
|
end
|
13
17
|
end
|
14
18
|
end
|
@@ -1,13 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
1
|
+
module GraphQL
|
2
|
+
module StaticValidation
|
3
|
+
class ArgumentsAreDefined < GraphQL::StaticValidation::ArgumentsValidator
|
4
|
+
def validate_node(parent, node, defn, context)
|
5
|
+
argument_defn = defn.arguments[node.name]
|
6
|
+
if argument_defn.nil?
|
7
|
+
kind_of_node = node_type(parent)
|
8
|
+
error_arg_name = parent_name(parent, defn)
|
9
|
+
context.errors << message("#{kind_of_node} '#{error_arg_name}' doesn't accept argument '#{node.name}'", parent)
|
10
|
+
GraphQL::Language::Visitor::SKIP
|
11
|
+
else
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
11
15
|
end
|
12
16
|
end
|
13
17
|
end
|
@@ -1,18 +1,22 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module GraphQL
|
2
|
+
module StaticValidation
|
3
|
+
class DirectivesAreDefined
|
4
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
def validate(context)
|
7
|
+
directive_names = context.schema.directives.keys
|
8
|
+
context.visitor[GraphQL::Language::Nodes::Directive] << -> (node, parent) {
|
9
|
+
validate_directive(node, directive_names, context.errors)
|
10
|
+
}
|
11
|
+
end
|
10
12
|
|
11
|
-
|
13
|
+
private
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
15
|
+
def validate_directive(ast_directive, directive_names, errors)
|
16
|
+
if !directive_names.include?(ast_directive.name)
|
17
|
+
errors << message("Directive @#{ast_directive.name} is not defined", ast_directive)
|
18
|
+
end
|
19
|
+
end
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module StaticValidation
|
3
|
+
class DirectivesAreInValidLocations
|
4
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
5
|
+
include GraphQL::Language
|
6
|
+
|
7
|
+
def validate(context)
|
8
|
+
directives = context.schema.directives
|
9
|
+
|
10
|
+
context.visitor[Nodes::Directive] << -> (node, parent) {
|
11
|
+
validate_location(node, parent, directives, context.errors)
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
LOCATION_MESSAGE_NAMES = {
|
18
|
+
GraphQL::Directive::QUERY => "queries",
|
19
|
+
GraphQL::Directive::MUTATION => "mutations",
|
20
|
+
GraphQL::Directive::SUBSCRIPTION => "subscriptions",
|
21
|
+
GraphQL::Directive::FIELD => "fields",
|
22
|
+
GraphQL::Directive::FRAGMENT_DEFINITION => "fragment definitions",
|
23
|
+
GraphQL::Directive::FRAGMENT_SPREAD => "fragment spreads",
|
24
|
+
GraphQL::Directive::INLINE_FRAGMENT => "inline fragments",
|
25
|
+
}
|
26
|
+
|
27
|
+
SIMPLE_LOCATIONS = {
|
28
|
+
Nodes::Field => GraphQL::Directive::FIELD,
|
29
|
+
Nodes::InlineFragment => GraphQL::Directive::INLINE_FRAGMENT,
|
30
|
+
Nodes::FragmentSpread => GraphQL::Directive::FRAGMENT_SPREAD,
|
31
|
+
Nodes::FragmentDefinition => GraphQL::Directive::FRAGMENT_DEFINITION,
|
32
|
+
}
|
33
|
+
|
34
|
+
SIMPLE_LOCATION_NODES = SIMPLE_LOCATIONS.keys
|
35
|
+
|
36
|
+
def validate_location(ast_directive, ast_parent, directives, errors)
|
37
|
+
directive_defn = directives[ast_directive.name]
|
38
|
+
case ast_parent
|
39
|
+
when Nodes::OperationDefinition
|
40
|
+
required_location = GraphQL::Directive.const_get(ast_parent.operation_type.upcase)
|
41
|
+
assert_includes_location(directive_defn, ast_directive, required_location, errors)
|
42
|
+
when *SIMPLE_LOCATION_NODES
|
43
|
+
required_location = SIMPLE_LOCATIONS[ast_parent.class]
|
44
|
+
assert_includes_location(directive_defn, ast_directive, required_location, errors)
|
45
|
+
else
|
46
|
+
errors << message("Directives can't be applied to #{ast_parent.class.name}s", ast_directive)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def assert_includes_location(directive_defn, directive_ast, required_location, errors)
|
51
|
+
if !directive_defn.locations.include?(required_location)
|
52
|
+
location_name = LOCATION_MESSAGE_NAMES[required_location]
|
53
|
+
allowed_location_names = directive_defn.locations.map { |loc| LOCATION_MESSAGE_NAMES[loc] }
|
54
|
+
errors << message("'@#{directive_defn.name}' can't be applied to #{location_name} (allowed: #{allowed_location_names.join(", ")})", directive_ast)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,28 +1,32 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module GraphQL
|
2
|
+
module StaticValidation
|
3
|
+
class FieldsAreDefinedOnType
|
4
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
def validate(context)
|
7
|
+
visitor = context.visitor
|
8
|
+
visitor[GraphQL::Language::Nodes::Field] << -> (node, parent) {
|
9
|
+
return if context.skip_field?(node.name)
|
10
|
+
parent_type = context.object_types[-2]
|
11
|
+
parent_type = parent_type.unwrap
|
12
|
+
validate_field(context.errors, node, parent_type, parent)
|
13
|
+
}
|
14
|
+
end
|
13
15
|
|
14
|
-
|
16
|
+
private
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
def validate_field(errors, ast_field, parent_type, parent)
|
19
|
+
if parent_type.kind.union?
|
20
|
+
errors << message("Selections can't be made directly on unions (see selections on #{parent_type.name})", parent)
|
21
|
+
return GraphQL::Language::Visitor::SKIP
|
22
|
+
end
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
field = parent_type.get_field(ast_field.name)
|
25
|
+
if field.nil?
|
26
|
+
errors << message("Field '#{ast_field.name}' doesn't exist on type '#{parent_type.name}'", parent)
|
27
|
+
return GraphQL::Language::Visitor::SKIP
|
28
|
+
end
|
29
|
+
end
|
26
30
|
end
|
27
31
|
end
|
28
32
|
end
|
@@ -1,32 +1,36 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module GraphQL
|
2
|
+
module StaticValidation
|
3
|
+
# Scalars _can't_ have selections
|
4
|
+
# Objects _must_ have selections
|
5
|
+
class FieldsHaveAppropriateSelections
|
6
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
def validate(context)
|
9
|
+
context.visitor[GraphQL::Language::Nodes::Field] << -> (node, parent) {
|
10
|
+
return if context.skip_field?(node.name)
|
11
|
+
field_defn = context.field_definition
|
12
|
+
validate_field_selections(node, field_defn, context.errors)
|
13
|
+
}
|
14
|
+
end
|
13
15
|
|
14
|
-
|
16
|
+
private
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
+
def validate_field_selections(ast_field, field_defn, errors)
|
19
|
+
resolved_type = field_defn.type.unwrap
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
if resolved_type.kind.scalar? && ast_field.selections.any?
|
22
|
+
error = message("Selections can't be made on scalars (field '#{ast_field.name}' returns #{resolved_type.name} but has selections [#{ast_field.selections.map(&:name).join(", ")}])", ast_field)
|
23
|
+
elsif resolved_type.kind.object? && ast_field.selections.none?
|
24
|
+
error = message("Objects must have selections (field '#{ast_field.name}' returns #{resolved_type.name} but has no selections)", ast_field)
|
25
|
+
else
|
26
|
+
error = nil
|
27
|
+
end
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
|
29
|
+
if !error.nil?
|
30
|
+
errors << error
|
31
|
+
GraphQL::Language::Visitor::SKIP
|
32
|
+
end
|
33
|
+
end
|
30
34
|
end
|
31
35
|
end
|
32
36
|
end
|
@@ -1,100 +1,104 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
1
|
+
module GraphQL
|
2
|
+
module StaticValidation
|
3
|
+
class FieldsWillMerge
|
4
|
+
def validate(context)
|
5
|
+
fragments = {}
|
6
|
+
has_selections = []
|
7
|
+
visitor = context.visitor
|
8
|
+
visitor[GraphQL::Language::Nodes::OperationDefinition] << -> (node, parent) {
|
9
|
+
if node.selections.any?
|
10
|
+
has_selections << node
|
11
|
+
end
|
12
|
+
}
|
13
|
+
visitor[GraphQL::Language::Nodes::Document].leave << -> (node, parent) {
|
14
|
+
has_selections.each { |node|
|
15
|
+
field_map = gather_fields_by_name(node.selections, {}, [], context)
|
16
|
+
find_conflicts(field_map, context)
|
17
|
+
}
|
18
|
+
}
|
9
19
|
end
|
10
|
-
}
|
11
|
-
visitor[GraphQL::Language::Nodes::Document].leave << -> (node, parent) {
|
12
|
-
has_selections.each { |node|
|
13
|
-
field_map = gather_fields_by_name(node.selections, {}, [], context)
|
14
|
-
find_conflicts(field_map, context)
|
15
|
-
}
|
16
|
-
}
|
17
|
-
end
|
18
20
|
|
19
|
-
|
21
|
+
private
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
def find_conflicts(field_map, context)
|
24
|
+
field_map.each do |name, ast_fields|
|
25
|
+
comparison = FieldDefinitionComparison.new(name, ast_fields)
|
26
|
+
context.errors.push(*comparison.errors)
|
25
27
|
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
subfield_map = {}
|
30
|
+
visited_fragments = []
|
31
|
+
ast_fields.each do |defn|
|
32
|
+
gather_fields_by_name(defn.selections, subfield_map, visited_fragments, context)
|
33
|
+
end
|
34
|
+
find_conflicts(subfield_map, context)
|
35
|
+
end
|
31
36
|
end
|
32
|
-
find_conflicts(subfield_map, context)
|
33
|
-
end
|
34
|
-
end
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
38
|
+
def gather_fields_by_name(fields, field_map, visited_fragments, context)
|
39
|
+
fields.each do |field|
|
40
|
+
if field.is_a?(GraphQL::Language::Nodes::InlineFragment)
|
41
|
+
next_fields = field.selections
|
42
|
+
elsif field.is_a?(GraphQL::Language::Nodes::FragmentSpread)
|
43
|
+
if visited_fragments.include?(field.name)
|
44
|
+
next
|
45
|
+
else
|
46
|
+
visited_fragments << field.name
|
47
|
+
end
|
48
|
+
fragment_defn = context.fragments[field.name]
|
49
|
+
next_fields = fragment_defn ? fragment_defn.selections : []
|
50
|
+
else
|
51
|
+
name_in_selection = field.alias || field.name
|
52
|
+
field_map[name_in_selection] ||= []
|
53
|
+
field_map[name_in_selection].push(field)
|
54
|
+
next_fields = []
|
55
|
+
end
|
56
|
+
gather_fields_by_name(next_fields, field_map, visited_fragments, context)
|
45
57
|
end
|
46
|
-
|
47
|
-
next_fields = fragment.selections
|
48
|
-
else
|
49
|
-
name_in_selection = field.alias || field.name
|
50
|
-
field_map[name_in_selection] ||= []
|
51
|
-
field_map[name_in_selection].push(field)
|
52
|
-
next_fields = []
|
58
|
+
field_map
|
53
59
|
end
|
54
|
-
gather_fields_by_name(next_fields, field_map, visited_fragments, context)
|
55
|
-
end
|
56
|
-
field_map
|
57
|
-
end
|
58
60
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
61
|
+
# Compare two field definitions, add errors to the list if there are any
|
62
|
+
class FieldDefinitionComparison
|
63
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
64
|
+
NAMED_VALUES = [GraphQL::Language::Nodes::Enum, GraphQL::Language::Nodes::VariableIdentifier]
|
65
|
+
attr_reader :errors
|
66
|
+
def initialize(name, defs)
|
67
|
+
errors = []
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
69
|
+
names = defs.map(&:name).uniq
|
70
|
+
if names.length != 1
|
71
|
+
errors << message("Field '#{name}' has a field conflict: #{names.join(" or ")}?", defs.first)
|
72
|
+
end
|
71
73
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
74
|
+
args = defs.map { |defn| reduce_list(defn.arguments)}.uniq
|
75
|
+
if args.length != 1
|
76
|
+
errors << message("Field '#{name}' has an argument conflict: #{args.map {|a| JSON.dump(a) }.join(" or ")}?", defs.first)
|
77
|
+
end
|
76
78
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
79
|
+
directive_names = defs.map { |defn| defn.directives.map(&:name) }.uniq
|
80
|
+
if directive_names.length != 1
|
81
|
+
errors << message("Field '#{name}' has a directive conflict: #{directive_names.map {|names| "[#{names.join(", ")}]"}.join(" or ")}?", defs.first)
|
82
|
+
end
|
81
83
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
84
|
+
directive_args = defs.map {|defn| defn.directives.map {|d| reduce_list(d.arguments) } }.uniq
|
85
|
+
if directive_args.length != 1
|
86
|
+
errors << message("Field '#{name}' has a directive argument conflict: #{directive_args.map {|args| JSON.dump(args)}.join(" or ")}?", defs.first)
|
87
|
+
end
|
86
88
|
|
87
|
-
|
88
|
-
|
89
|
+
@errors = errors
|
90
|
+
end
|
89
91
|
|
90
|
-
|
92
|
+
private
|
91
93
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
94
|
+
# Turn AST tree into a hash
|
95
|
+
# can't look up args, the names just have to match
|
96
|
+
def reduce_list(args)
|
97
|
+
args.reduce({}) do |memo, a|
|
98
|
+
memo[a.name] = NAMED_VALUES.include?(a.value.class) ? a.value.name : a.value
|
99
|
+
memo
|
100
|
+
end
|
101
|
+
end
|
98
102
|
end
|
99
103
|
end
|
100
104
|
end
|