graphql 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graph_ql/directive.rb +9 -5
- data/lib/graph_ql/directives/include_directive.rb +2 -2
- data/lib/graph_ql/directives/skip_directive.rb +2 -2
- data/lib/graph_ql/enum.rb +5 -8
- data/lib/graph_ql/field.rb +50 -0
- data/lib/graph_ql/interface.rb +4 -0
- data/lib/graph_ql/introspection/arguments_field.rb +2 -2
- data/lib/graph_ql/introspection/directive_type.rb +10 -10
- data/lib/graph_ql/introspection/enum_value_type.rb +11 -8
- data/lib/graph_ql/introspection/enum_values_field.rb +5 -5
- data/lib/graph_ql/introspection/field_type.rb +14 -10
- data/lib/graph_ql/introspection/fields_field.rb +4 -4
- data/lib/graph_ql/introspection/input_fields_field.rb +3 -3
- data/lib/graph_ql/introspection/input_value_type.rb +8 -8
- data/lib/graph_ql/introspection/interfaces_field.rb +5 -0
- data/lib/graph_ql/introspection/of_type_field.rb +3 -9
- data/lib/graph_ql/introspection/possible_types_field.rb +3 -10
- data/lib/graph_ql/introspection/schema_type.rb +8 -14
- data/lib/graph_ql/introspection/type_kind_enum.rb +1 -1
- data/lib/graph_ql/introspection/type_type.rb +17 -19
- data/lib/graph_ql/introspection/typename_field.rb +15 -0
- data/lib/graph_ql/parser.rb +9 -0
- data/lib/graph_ql/parser/nodes.rb +17 -7
- data/lib/graph_ql/parser/parser.rb +7 -7
- data/lib/graph_ql/parser/transform.rb +23 -20
- data/lib/graph_ql/parser/visitor.rb +29 -13
- data/lib/graph_ql/query.rb +39 -16
- data/lib/graph_ql/query/field_resolution_strategy.rb +15 -11
- data/lib/graph_ql/query/type_resolver.rb +4 -2
- data/lib/graph_ql/repl.rb +1 -1
- data/lib/graph_ql/schema.rb +19 -7
- data/lib/graph_ql/schema/each_item_validator.rb +12 -0
- data/lib/graph_ql/schema/field_validator.rb +13 -0
- data/lib/graph_ql/schema/implementation_validator.rb +21 -0
- data/lib/graph_ql/schema/schema_validator.rb +10 -0
- data/lib/graph_ql/schema/type_reducer.rb +6 -6
- data/lib/graph_ql/schema/type_validator.rb +47 -0
- data/lib/graph_ql/static_validation.rb +18 -0
- data/lib/graph_ql/static_validation/argument_literals_are_compatible.rb +13 -0
- data/lib/graph_ql/static_validation/arguments_are_defined.rb +10 -0
- data/lib/graph_ql/static_validation/arguments_validator.rb +16 -0
- data/lib/graph_ql/static_validation/directives_are_defined.rb +18 -0
- data/lib/graph_ql/static_validation/fields_are_defined_on_type.rb +29 -0
- data/lib/graph_ql/static_validation/fields_have_appropriate_selections.rb +31 -0
- data/lib/graph_ql/static_validation/fields_will_merge.rb +93 -0
- data/lib/graph_ql/static_validation/fragment_types_exist.rb +24 -0
- data/lib/graph_ql/static_validation/fragments_are_used.rb +30 -0
- data/lib/graph_ql/static_validation/literal_validator.rb +27 -0
- data/lib/graph_ql/static_validation/message.rb +29 -0
- data/lib/graph_ql/static_validation/required_arguments_are_present.rb +13 -0
- data/lib/graph_ql/static_validation/type_stack.rb +87 -0
- data/lib/graph_ql/static_validation/validator.rb +48 -0
- data/lib/graph_ql/type_kinds.rb +50 -12
- data/lib/graph_ql/types/argument_definer.rb +7 -0
- data/lib/graph_ql/types/boolean_type.rb +3 -3
- data/lib/graph_ql/types/field_definer.rb +19 -0
- data/lib/graph_ql/types/float_type.rb +3 -3
- data/lib/graph_ql/types/input_object_type.rb +4 -0
- data/lib/graph_ql/types/input_value.rb +1 -1
- data/lib/graph_ql/types/int_type.rb +4 -4
- data/lib/graph_ql/types/list_type.rb +5 -1
- data/lib/graph_ql/types/non_null_type.rb +4 -0
- data/lib/graph_ql/types/object_type.rb +9 -20
- data/lib/graph_ql/types/scalar_type.rb +4 -0
- data/lib/graph_ql/types/string_type.rb +3 -3
- data/lib/graph_ql/types/type_definer.rb +5 -9
- data/lib/graph_ql/union.rb +6 -17
- data/lib/graph_ql/version.rb +1 -1
- data/lib/graphql.rb +58 -78
- data/readme.md +80 -7
- data/spec/graph_ql/interface_spec.rb +15 -1
- data/spec/graph_ql/introspection/directive_type_spec.rb +2 -2
- data/spec/graph_ql/introspection/schema_type_spec.rb +2 -1
- data/spec/graph_ql/introspection/type_type_spec.rb +16 -1
- data/spec/graph_ql/parser/parser_spec.rb +3 -1
- data/spec/graph_ql/parser/transform_spec.rb +12 -2
- data/spec/graph_ql/parser/visitor_spec.rb +13 -5
- data/spec/graph_ql/query_spec.rb +25 -13
- data/spec/graph_ql/schema/field_validator_spec.rb +21 -0
- data/spec/graph_ql/schema/type_reducer_spec.rb +2 -2
- data/spec/graph_ql/schema/type_validator_spec.rb +54 -0
- data/spec/graph_ql/static_validation/argument_literals_are_compatible_spec.rb +41 -0
- data/spec/graph_ql/static_validation/arguments_are_defined_spec.rb +40 -0
- data/spec/graph_ql/static_validation/directives_are_defined_spec.rb +33 -0
- data/spec/graph_ql/static_validation/fields_are_defined_on_type_spec.rb +59 -0
- data/spec/graph_ql/static_validation/fields_have_appropriate_selections_spec.rb +30 -0
- data/spec/graph_ql/{validations → static_validation}/fields_will_merge_spec.rb +24 -17
- data/spec/graph_ql/static_validation/fragment_types_exist_spec.rb +38 -0
- data/spec/graph_ql/static_validation/fragments_are_used_spec.rb +24 -0
- data/spec/graph_ql/static_validation/required_arguments_are_present_spec.rb +41 -0
- data/spec/graph_ql/static_validation/type_stack_spec.rb +35 -0
- data/spec/graph_ql/static_validation/validator_spec.rb +28 -0
- data/spec/graph_ql/types/object_type_spec.rb +1 -1
- data/spec/graph_ql/union_spec.rb +1 -14
- data/spec/support/dummy_app.rb +75 -53
- metadata +53 -31
- data/lib/graph_ql/fields/abstract_field.rb +0 -37
- data/lib/graph_ql/fields/access_field.rb +0 -24
- data/lib/graph_ql/fields/field.rb +0 -34
- data/lib/graph_ql/types/abstract_type.rb +0 -14
- data/lib/graph_ql/validations/fields_are_defined_on_type.rb +0 -44
- data/lib/graph_ql/validations/fields_will_merge.rb +0 -80
- data/lib/graph_ql/validations/fragments_are_used.rb +0 -24
- data/lib/graph_ql/validator.rb +0 -29
- data/spec/graph_ql/validations/fields_are_defined_on_type_spec.rb +0 -28
- data/spec/graph_ql/validations/fragments_are_used_spec.rb +0 -28
- data/spec/graph_ql/validator_spec.rb +0 -24
data/lib/graph_ql/schema.rb
CHANGED
@@ -1,20 +1,19 @@
|
|
1
1
|
class GraphQL::Schema
|
2
|
-
extend ActiveSupport::Autoload
|
3
|
-
autoload(:TypeReducer)
|
4
2
|
DIRECTIVES = [GraphQL::SkipDirective, GraphQL::IncludeDirective]
|
5
3
|
|
6
|
-
attr_reader :query, :mutation, :directives
|
4
|
+
attr_reader :query, :mutation, :directives, :static_validator
|
7
5
|
def initialize(query:, mutation:)
|
8
6
|
# Add fields to this query root for introspection:
|
9
7
|
query.fields = query.fields.merge({
|
10
|
-
"__type" => GraphQL::Field.new do |f|
|
8
|
+
"__type" => GraphQL::Field.new do |f, type, field, arg|
|
11
9
|
f.description("A type in the GraphQL system")
|
12
|
-
f.type
|
13
|
-
f.
|
10
|
+
f.arguments({name: arg.build(type: !type.String)})
|
11
|
+
f.type(!GraphQL::Introspection::TypeType)
|
12
|
+
f.resolve -> (o, a, c) { self.types[a["name"]] }
|
14
13
|
end,
|
15
14
|
"__schema" => GraphQL::Field.new do |f|
|
16
15
|
f.description("This GraphQL schema")
|
17
|
-
f.type(!GraphQL::SchemaType)
|
16
|
+
f.type(!GraphQL::Introspection::SchemaType)
|
18
17
|
f.resolve -> (o, a, c) { self }
|
19
18
|
end
|
20
19
|
})
|
@@ -22,9 +21,22 @@ class GraphQL::Schema
|
|
22
21
|
@query = query
|
23
22
|
@mutation = mutation
|
24
23
|
@directives = DIRECTIVES.reduce({}) { |m, d| m[d.name] = d; m }
|
24
|
+
@static_validator = GraphQL::StaticValidation::Validator.new(schema: self)
|
25
|
+
|
26
|
+
errors = SchemaValidator.new.validate(self)
|
27
|
+
if errors.any?
|
28
|
+
raise("Schema is invalid: \n#{errors.join("\n")}")
|
29
|
+
end
|
25
30
|
end
|
26
31
|
|
27
32
|
def types
|
28
33
|
@types ||= TypeReducer.new(query, {}).result
|
29
34
|
end
|
30
35
|
end
|
36
|
+
|
37
|
+
require 'graph_ql/schema/each_item_validator'
|
38
|
+
require 'graph_ql/schema/field_validator'
|
39
|
+
require 'graph_ql/schema/implementation_validator'
|
40
|
+
require 'graph_ql/schema/schema_validator'
|
41
|
+
require 'graph_ql/schema/type_reducer'
|
42
|
+
require 'graph_ql/schema/type_validator'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class GraphQL::Schema::EachItemValidator
|
2
|
+
def initialize(errors)
|
3
|
+
@errors = errors
|
4
|
+
end
|
5
|
+
|
6
|
+
def validate(items, as:, must_be:)
|
7
|
+
invalid_items = items.select {|k| !yield(k) }
|
8
|
+
if invalid_items.any?
|
9
|
+
@errors << "#{as} must be #{must_be}, but some aren't: #{invalid_items.map(&:to_s).join(", ")}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class GraphQL::Schema::FieldValidator
|
2
|
+
def validate(field, errors)
|
3
|
+
implementation = GraphQL::Schema::ImplementationValidator.new(field, as: "Field", errors: errors)
|
4
|
+
implementation.must_respond_to(:name)
|
5
|
+
implementation.must_respond_to(:type)
|
6
|
+
implementation.must_respond_to(:description)
|
7
|
+
implementation.must_respond_to(:arguments) do |arguments|
|
8
|
+
validator = GraphQL::Schema::EachItemValidator.new(errors)
|
9
|
+
validator.validate(arguments.keys, as: "#{field.name}.arguments keys", must_be: "Strings") { |k| k.is_a?(String) }
|
10
|
+
end
|
11
|
+
implementation.must_respond_to(:deprecation_reason)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# A helper to ensure `object` implements the concept `as`
|
2
|
+
class GraphQL::Schema::ImplementationValidator
|
3
|
+
attr_reader :object, :errors, :implementation_as
|
4
|
+
def initialize(object, as:, errors:)
|
5
|
+
@object = object
|
6
|
+
@implementation_as = as
|
7
|
+
@errors = errors
|
8
|
+
end
|
9
|
+
|
10
|
+
# Ensure the object responds to `method_name`.
|
11
|
+
# If `block_given?`, yield the return value of that method
|
12
|
+
# If provided, use `as` in the error message, overriding class-level `as`.
|
13
|
+
def must_respond_to(method_name, args: [], as: nil)
|
14
|
+
if !object.respond_to?(method_name)
|
15
|
+
local_as = as || implementation_as
|
16
|
+
errors << "#{object.to_s} must respond to ##{method_name}(#{args.join(", ")}) to be a #{local_as}"
|
17
|
+
elsif block_given?
|
18
|
+
yield(object.public_send(method_name))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
|
+
# Starting from a given type, discover other types in the system by
|
2
|
+
# traversing that type's fields, possible_types, etc
|
1
3
|
class GraphQL::Schema::TypeReducer
|
2
|
-
FIELDS_TYPE_KINDS = [GraphQL::TypeKinds::OBJECT, GraphQL::TypeKinds::INTERFACE]
|
3
|
-
POSSIBLE_TYPES_TYPE_KINDS = [GraphQL::TypeKinds::INTERFACE, GraphQL::TypeKinds::UNION]
|
4
4
|
attr_reader :type, :result
|
5
5
|
def initialize(type, existing_type_hash)
|
6
6
|
if [GraphQL::TypeKinds::NON_NULL, GraphQL::TypeKinds::LIST].include?(type.kind)
|
@@ -17,20 +17,20 @@ class GraphQL::Schema::TypeReducer
|
|
17
17
|
|
18
18
|
def find_types(type, type_hash)
|
19
19
|
type_hash[type.name] = type
|
20
|
-
if
|
20
|
+
if type.kind.fields?
|
21
21
|
type.fields.each do |name, field|
|
22
22
|
type_hash.merge!(reduce_type(field.type, type_hash))
|
23
23
|
field.arguments.each do |name, argument|
|
24
|
-
type_hash.merge!(reduce_type(argument
|
24
|
+
type_hash.merge!(reduce_type(argument.type, type_hash))
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
28
|
-
if type.kind
|
28
|
+
if type.kind.object?
|
29
29
|
type.interfaces.each do |interface|
|
30
30
|
type_hash.merge!(reduce_type(interface, type_hash))
|
31
31
|
end
|
32
32
|
end
|
33
|
-
if
|
33
|
+
if type.kind.resolves?
|
34
34
|
type.possible_types.each do |possible_type|
|
35
35
|
type_hash.merge!(reduce_type(possible_type, type_hash))
|
36
36
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class GraphQL::Schema::TypeValidator
|
2
|
+
def validate(type, errors)
|
3
|
+
implementation = GraphQL::Schema::ImplementationValidator.new(type, as: "Type", errors: errors)
|
4
|
+
implementation.must_respond_to(:name)
|
5
|
+
implementation.must_respond_to(:kind)
|
6
|
+
type_name = type.name
|
7
|
+
kind_name = type.kind.name
|
8
|
+
|
9
|
+
implementation.must_respond_to(:description, as: kind_name)
|
10
|
+
each_item_validator = GraphQL::Schema::EachItemValidator.new(errors)
|
11
|
+
|
12
|
+
if type.kind.fields?
|
13
|
+
field_validator = GraphQL::Schema::FieldValidator.new
|
14
|
+
implementation.must_respond_to(:fields, as: kind_name) do |fields|
|
15
|
+
each_item_validator.validate(fields.keys, as: "#{type.name}.fields keys", must_be: "Strings") { |k| k.is_a?(String) }
|
16
|
+
|
17
|
+
fields.values.each do |field|
|
18
|
+
field_validator.validate(field, errors)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if type.kind.resolves?
|
24
|
+
implementation.must_respond_to(:resolve_type)
|
25
|
+
implementation.must_respond_to(:possible_types, as: kind_name) do |possible_types|
|
26
|
+
each_item_validator.validate(possible_types, as: "#{type_name}.possible_types", must_be: "objects") { |t| t.kind.object? }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if type.kind.object?
|
31
|
+
implementation.must_respond_to(:interfaces, as: kind_name) do |interfaces|
|
32
|
+
each_item_validator.validate(interfaces, as: "#{type_name}.interfaces", must_be: "interfaces") { |t| t.kind.interface? }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if type.kind.input_object?
|
37
|
+
implementation.must_respond_to(:input_fields, as: kind_name)
|
38
|
+
end
|
39
|
+
|
40
|
+
if type.kind.union?
|
41
|
+
union_types = type.possible_types
|
42
|
+
if union_types.length < 2
|
43
|
+
errors << "Union #{type_name} must be defined with 2 or more types, not #{union_types.length}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module GraphQL::StaticValidation
|
2
|
+
end
|
3
|
+
|
4
|
+
require 'graph_ql/static_validation/message'
|
5
|
+
require 'graph_ql/static_validation/arguments_validator'
|
6
|
+
|
7
|
+
require 'graph_ql/static_validation/argument_literals_are_compatible'
|
8
|
+
require 'graph_ql/static_validation/arguments_are_defined'
|
9
|
+
require 'graph_ql/static_validation/required_arguments_are_present'
|
10
|
+
require 'graph_ql/static_validation/fragment_types_exist'
|
11
|
+
require 'graph_ql/static_validation/directives_are_defined'
|
12
|
+
require 'graph_ql/static_validation/fields_are_defined_on_type'
|
13
|
+
require 'graph_ql/static_validation/fields_have_appropriate_selections'
|
14
|
+
require 'graph_ql/static_validation/fields_will_merge'
|
15
|
+
require 'graph_ql/static_validation/fragments_are_used'
|
16
|
+
require 'graph_ql/static_validation/type_stack'
|
17
|
+
require 'graph_ql/static_validation/validator'
|
18
|
+
require 'graph_ql/static_validation/literal_validator'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class GraphQL::StaticValidation::ArgumentLiteralsAreCompatible < GraphQL::StaticValidation::ArgumentsValidator
|
2
|
+
def validate_node(node, defn, context)
|
3
|
+
args_with_literals = node.arguments.select {|a| !a.value.is_a?(GraphQL::Nodes::VariableIdentifier)}
|
4
|
+
validator = GraphQL::StaticValidation::LiteralValidator.new
|
5
|
+
args_with_literals.each do |arg|
|
6
|
+
arg_defn = defn.arguments[arg.name]
|
7
|
+
valid = validator.validate(arg.value, arg_defn.type)
|
8
|
+
if !valid
|
9
|
+
context.errors << message("#{arg.name} on #{node.class.name.split("::").last} '#{node.name}' has an invalid value", node)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class GraphQL::StaticValidation::ArgumentsAreDefined < GraphQL::StaticValidation::ArgumentsValidator
|
2
|
+
def validate_node(node, defn, context)
|
3
|
+
node.arguments.each do |argument|
|
4
|
+
argument_defn = defn.arguments[argument.name]
|
5
|
+
if argument_defn.nil?
|
6
|
+
context.errors << message("#{node.class.name.split("::").last} '#{node.name}' doesn't accept argument #{argument.name}", node)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Implement validate_node
|
2
|
+
class GraphQL::StaticValidation::ArgumentsValidator
|
3
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
4
|
+
|
5
|
+
def validate(context)
|
6
|
+
visitor = context.visitor
|
7
|
+
visitor[GraphQL::Nodes::Field] << -> (node, parent) {
|
8
|
+
field_defn = context.field_definition
|
9
|
+
validate_node(node, field_defn, context)
|
10
|
+
}
|
11
|
+
visitor[GraphQL::Nodes::Directive] << -> (node, parent) {
|
12
|
+
directive_defn = context.schema.directives[node.name]
|
13
|
+
validate_node(node, directive_defn, context)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class GraphQL::StaticValidation::DirectivesAreDefined
|
2
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
3
|
+
|
4
|
+
def validate(context)
|
5
|
+
directive_names = context.schema.directives.keys
|
6
|
+
context.visitor[GraphQL::Nodes::Directive] << -> (node, parent) {
|
7
|
+
validate_directive(node, directive_names, context.errors)
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def validate_directive(ast_directive, directive_names, errors)
|
14
|
+
if !directive_names.include?(ast_directive.name)
|
15
|
+
errors << message("Directive @#{ast_directive.name} is not defined", ast_directive)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class GraphQL::StaticValidation::FieldsAreDefinedOnType
|
2
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
3
|
+
|
4
|
+
IS_FIELD = Proc.new {|f| f.is_a?(GraphQL::Nodes::Field) }
|
5
|
+
|
6
|
+
def validate(context)
|
7
|
+
visitor = context.visitor
|
8
|
+
visitor[GraphQL::Nodes::Field] << -> (node, parent) {
|
9
|
+
parent_type = context.object_types[-2]
|
10
|
+
parent_type = parent_type.kind.unwrap(parent_type)
|
11
|
+
validate_field(context.errors, node, parent_type, parent)
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_field(errors, ast_field, parent_type, parent)
|
18
|
+
if parent_type.kind.union?
|
19
|
+
errors << message("Selections can't be made directly on unions (see selections on #{parent_type.name})", parent)
|
20
|
+
return GraphQL::Visitor::SKIP
|
21
|
+
end
|
22
|
+
|
23
|
+
field = parent_type.fields[ast_field.name]
|
24
|
+
if field.nil?
|
25
|
+
errors << message("Field '#{ast_field.name}' doesn't exist on type '#{parent_type.name}'", parent)
|
26
|
+
return GraphQL::Visitor::SKIP
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Scalars _can't_ have selections
|
2
|
+
# Objects _must_ have selections
|
3
|
+
class GraphQL::StaticValidation::FieldsHaveAppropriateSelections
|
4
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
5
|
+
|
6
|
+
def validate(context)
|
7
|
+
context.visitor[GraphQL::Nodes::Field] << -> (node, parent) {
|
8
|
+
field_defn = context.field_definition
|
9
|
+
validate_field_selections(node, field_defn, context.errors)
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def validate_field_selections(ast_field, field_defn, errors)
|
16
|
+
resolved_type = field_defn.type.kind.unwrap(field_defn.type)
|
17
|
+
|
18
|
+
if resolved_type.kind.scalar? && ast_field.selections.any?
|
19
|
+
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)
|
20
|
+
elsif resolved_type.kind.object? && ast_field.selections.none?
|
21
|
+
error = message("Objects must have selections (field '#{ast_field.name}' returns #{resolved_type.name} but has no selections)", ast_field)
|
22
|
+
else
|
23
|
+
error = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
if !error.nil?
|
27
|
+
errors << error
|
28
|
+
GraphQL::Visitor::SKIP
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
class GraphQL::StaticValidation::FieldsWillMerge
|
2
|
+
def validate(context)
|
3
|
+
fragments = {}
|
4
|
+
has_selections = []
|
5
|
+
visitor = context.visitor
|
6
|
+
visitor[GraphQL::Nodes::OperationDefinition] << -> (node, parent) {
|
7
|
+
if node.selections.any?
|
8
|
+
has_selections << node
|
9
|
+
end
|
10
|
+
}
|
11
|
+
visitor[GraphQL::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
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def find_conflicts(field_map, context)
|
22
|
+
field_map.each do |name, ast_fields|
|
23
|
+
comparison = FieldDefinitionComparison.new(name, ast_fields)
|
24
|
+
context.errors.push(*comparison.errors)
|
25
|
+
|
26
|
+
subfield_map = ast_fields.reduce({}) do |memo, defn|
|
27
|
+
gather_fields_by_name(defn.selections, memo, context)
|
28
|
+
end
|
29
|
+
find_conflicts(subfield_map, context)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def gather_fields_by_name(fields, field_map, context)
|
34
|
+
fields.each do |field|
|
35
|
+
if field.is_a?(GraphQL::Nodes::InlineFragment)
|
36
|
+
next_fields = field.selections
|
37
|
+
elsif field.is_a?(GraphQL::Nodes::FragmentSpread)
|
38
|
+
fragment = context.fragments[field.name]
|
39
|
+
next_fields = fragment.selections
|
40
|
+
else
|
41
|
+
name_in_selection = field.alias || field.name
|
42
|
+
field_map[name_in_selection] ||= []
|
43
|
+
field_map[name_in_selection].push(field)
|
44
|
+
next_fields = []
|
45
|
+
end
|
46
|
+
gather_fields_by_name(next_fields, field_map, context)
|
47
|
+
end
|
48
|
+
field_map
|
49
|
+
end
|
50
|
+
|
51
|
+
# Compare two field definitions, add errors to the list if there are any
|
52
|
+
class FieldDefinitionComparison
|
53
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
54
|
+
NAMED_VALUES = [GraphQL::Nodes::Enum, GraphQL::Nodes::VariableIdentifier]
|
55
|
+
attr_reader :errors
|
56
|
+
def initialize(name, defs)
|
57
|
+
errors = []
|
58
|
+
|
59
|
+
names = defs.map(&:name).uniq
|
60
|
+
if names.length != 1
|
61
|
+
errors << message("Field '#{name}' has a field conflict: #{names.join(" or ")}?", defs.first)
|
62
|
+
end
|
63
|
+
|
64
|
+
args = defs.map { |defn| reduce_list(defn.arguments)}.uniq
|
65
|
+
if args.length != 1
|
66
|
+
errors << message("Field '#{name}' has an argument conflict: #{args.map {|a| JSON.dump(a) }.join(" or ")}?", defs.first)
|
67
|
+
end
|
68
|
+
|
69
|
+
directive_names = defs.map { |defn| defn.directives.map(&:name) }.uniq
|
70
|
+
if directive_names.length != 1
|
71
|
+
errors << message("Field '#{name}' has a directive conflict: #{directive_names.map {|names| "[#{names.join(", ")}]"}.join(" or ")}?", defs.first)
|
72
|
+
end
|
73
|
+
|
74
|
+
directive_args = defs.map {|defn| defn.directives.map {|d| reduce_list(d.arguments) } }.uniq
|
75
|
+
if directive_args.length != 1
|
76
|
+
errors << message("Field '#{name}' has a directive argument conflict: #{directive_args.map {|args| JSON.dump(args)}.join(" or ")}?", defs.first)
|
77
|
+
end
|
78
|
+
|
79
|
+
@errors = errors
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Turn AST tree into a hash
|
85
|
+
# can't look up args, the names just have to match
|
86
|
+
def reduce_list(args)
|
87
|
+
args.reduce({}) do |memo, a|
|
88
|
+
memo[a.name] = NAMED_VALUES.include?(a.value.class) ? a.value.name : a.value
|
89
|
+
memo
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class GraphQL::StaticValidation::FragmentTypesExist
|
2
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
3
|
+
|
4
|
+
FRAGMENTS_ON_TYPES = [
|
5
|
+
GraphQL::Nodes::FragmentDefinition,
|
6
|
+
GraphQL::Nodes::InlineFragment,
|
7
|
+
]
|
8
|
+
|
9
|
+
def validate(context)
|
10
|
+
FRAGMENTS_ON_TYPES.each do |node_class|
|
11
|
+
context.visitor[node_class] << -> (node, parent) { validate_type_exists(node, context) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_type_exists(node, context)
|
18
|
+
type = context.schema.types[node.type]
|
19
|
+
if type.nil?
|
20
|
+
context.errors << message("No such type #{node.type}, so it can't be a fragment condition", node)
|
21
|
+
GraphQL::Visitor::SKIP
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|