graphql 1.4.5 → 1.5.3
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/enum_generator.rb +33 -0
- data/lib/generators/graphql/function_generator.rb +15 -0
- data/lib/generators/graphql/install_generator.rb +118 -0
- data/lib/generators/graphql/interface_generator.rb +27 -0
- data/lib/generators/graphql/loader_generator.rb +17 -0
- data/lib/generators/graphql/mutation_generator.rb +19 -0
- data/lib/generators/graphql/object_generator.rb +34 -0
- data/lib/generators/graphql/templates/enum.erb +4 -0
- data/lib/generators/graphql/templates/function.erb +17 -0
- data/lib/generators/graphql/templates/graphql_controller.erb +32 -0
- data/lib/generators/graphql/templates/interface.erb +4 -0
- data/lib/generators/graphql/templates/loader.erb +15 -0
- data/lib/generators/graphql/templates/mutation.erb +12 -0
- data/lib/generators/graphql/templates/object.erb +5 -0
- data/lib/generators/graphql/templates/query_type.erb +15 -0
- data/lib/generators/graphql/templates/schema.erb +34 -0
- data/lib/generators/graphql/templates/union.erb +4 -0
- data/lib/generators/graphql/type_generator.rb +78 -0
- data/lib/generators/graphql/union_generator.rb +33 -0
- data/lib/graphql.rb +10 -0
- data/lib/graphql/analysis/analyze_query.rb +1 -1
- data/lib/graphql/analysis/query_complexity.rb +6 -50
- data/lib/graphql/analysis/query_depth.rb +1 -1
- data/lib/graphql/argument.rb +21 -0
- data/lib/graphql/compatibility/execution_specification/counter_schema.rb +3 -3
- data/lib/graphql/define.rb +1 -0
- data/lib/graphql/define/assign_argument.rb +3 -19
- data/lib/graphql/define/assign_mutation_function.rb +34 -0
- data/lib/graphql/define/assign_object_field.rb +26 -14
- data/lib/graphql/define/defined_object_proxy.rb +21 -0
- data/lib/graphql/define/instance_definable.rb +61 -11
- data/lib/graphql/directive.rb +6 -1
- data/lib/graphql/execution/directive_checks.rb +1 -0
- data/lib/graphql/execution/execute.rb +14 -9
- data/lib/graphql/execution/field_result.rb +1 -0
- data/lib/graphql/execution/lazy.rb +8 -17
- data/lib/graphql/execution/lazy/lazy_method_map.rb +2 -0
- data/lib/graphql/execution/lazy/resolve.rb +1 -0
- data/lib/graphql/execution/selection_result.rb +1 -0
- data/lib/graphql/execution/typecast.rb +39 -26
- data/lib/graphql/field.rb +15 -3
- data/lib/graphql/field/resolve.rb +3 -3
- data/lib/graphql/function.rb +134 -0
- data/lib/graphql/id_type.rb +1 -1
- data/lib/graphql/input_object_type.rb +1 -1
- data/lib/graphql/internal_representation.rb +1 -1
- data/lib/graphql/internal_representation/node.rb +35 -107
- data/lib/graphql/internal_representation/rewrite.rb +189 -183
- data/lib/graphql/internal_representation/visit.rb +38 -0
- data/lib/graphql/introspection/input_value_type.rb +10 -1
- data/lib/graphql/introspection/schema_type.rb +1 -1
- data/lib/graphql/language/lexer.rb +6 -3
- data/lib/graphql/language/lexer.rl +6 -3
- data/lib/graphql/object_type.rb +53 -13
- data/lib/graphql/query.rb +30 -14
- data/lib/graphql/query/arguments.rb +2 -0
- data/lib/graphql/query/context.rb +2 -2
- data/lib/graphql/query/literal_input.rb +9 -0
- data/lib/graphql/query/serial_execution/field_resolution.rb +2 -2
- data/lib/graphql/query/serial_execution/selection_resolution.rb +1 -1
- data/lib/graphql/relay.rb +1 -0
- data/lib/graphql/relay/array_connection.rb +1 -1
- data/lib/graphql/relay/base_connection.rb +34 -15
- data/lib/graphql/relay/connection_resolve.rb +7 -2
- data/lib/graphql/relay/mutation.rb +45 -4
- data/lib/graphql/relay/node.rb +18 -6
- data/lib/graphql/relay/range_add.rb +45 -0
- data/lib/graphql/relay/relation_connection.rb +17 -2
- data/lib/graphql/runtime_type_error.rb +1 -0
- data/lib/graphql/schema.rb +40 -5
- data/lib/graphql/schema/base_64_encoder.rb +1 -0
- data/lib/graphql/schema/build_from_definition.rb +56 -21
- data/lib/graphql/schema/default_parse_error.rb +10 -0
- data/lib/graphql/schema/loader.rb +8 -1
- data/lib/graphql/schema/null_mask.rb +1 -0
- data/lib/graphql/schema/validation.rb +35 -0
- data/lib/graphql/static_validation.rb +1 -0
- data/lib/graphql/static_validation/all_rules.rb +1 -0
- data/lib/graphql/static_validation/arguments_validator.rb +7 -4
- data/lib/graphql/static_validation/definition_dependencies.rb +183 -0
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +28 -96
- data/lib/graphql/static_validation/rules/fragment_names_are_unique.rb +23 -0
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +8 -5
- data/lib/graphql/static_validation/rules/fragments_are_finite.rb +6 -31
- data/lib/graphql/static_validation/rules/fragments_are_used.rb +11 -41
- data/lib/graphql/static_validation/rules/operation_names_are_valid.rb +2 -2
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +19 -7
- data/lib/graphql/static_validation/validation_context.rb +22 -1
- data/lib/graphql/static_validation/validator.rb +4 -1
- data/lib/graphql/string_type.rb +5 -1
- data/lib/graphql/version.rb +1 -1
- data/readme.md +12 -3
- data/spec/generators/graphql/enum_generator_spec.rb +29 -0
- data/spec/generators/graphql/function_generator_spec.rb +33 -0
- data/spec/generators/graphql/install_generator_spec.rb +185 -0
- data/spec/generators/graphql/interface_generator_spec.rb +32 -0
- data/spec/generators/graphql/loader_generator_spec.rb +31 -0
- data/spec/generators/graphql/mutation_generator_spec.rb +28 -0
- data/spec/generators/graphql/object_generator_spec.rb +42 -0
- data/spec/generators/graphql/union_generator_spec.rb +50 -0
- data/spec/graphql/analysis/query_complexity_spec.rb +2 -1
- data/spec/graphql/define/instance_definable_spec.rb +38 -0
- data/spec/graphql/directive/skip_directive_spec.rb +1 -0
- data/spec/graphql/directive_spec.rb +18 -0
- data/spec/graphql/execution/typecast_spec.rb +41 -46
- data/spec/graphql/field_spec.rb +1 -1
- data/spec/graphql/function_spec.rb +128 -0
- data/spec/graphql/internal_representation/rewrite_spec.rb +166 -129
- data/spec/graphql/introspection/type_type_spec.rb +1 -1
- data/spec/graphql/language/lexer_spec.rb +6 -0
- data/spec/graphql/object_type_spec.rb +73 -2
- data/spec/graphql/query/arguments_spec.rb +28 -0
- data/spec/graphql/query/variables_spec.rb +7 -1
- data/spec/graphql/query_spec.rb +30 -0
- data/spec/graphql/relay/base_connection_spec.rb +26 -8
- data/spec/graphql/relay/connection_resolve_spec.rb +45 -0
- data/spec/graphql/relay/connection_type_spec.rb +21 -0
- data/spec/graphql/relay/node_spec.rb +30 -2
- data/spec/graphql/relay/range_add_spec.rb +113 -0
- data/spec/graphql/schema/build_from_definition_spec.rb +114 -0
- data/spec/graphql/schema/loader_spec.rb +1 -0
- data/spec/graphql/schema/printer_spec.rb +2 -2
- data/spec/graphql/schema/validation_spec.rb +80 -11
- data/spec/graphql/schema/warden_spec.rb +10 -10
- data/spec/graphql/schema_spec.rb +18 -1
- data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +16 -0
- data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +50 -3
- data/spec/graphql/static_validation/rules/fragment_names_are_unique_spec.rb +27 -0
- data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +57 -0
- data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +14 -0
- data/spec/graphql/string_type_spec.rb +7 -0
- data/spec/spec_helper.rb +3 -3
- data/spec/support/base_generator_test.rb +7 -0
- data/spec/support/dummy/schema.rb +32 -30
- data/spec/support/star_wars/schema.rb +81 -23
- metadata +98 -20
- data/lib/graphql/internal_representation/selection.rb +0 -85
@@ -107,7 +107,14 @@ module GraphQL
|
|
107
107
|
)
|
108
108
|
when "ARGUMENT"
|
109
109
|
kwargs = {}
|
110
|
-
|
110
|
+
if type["defaultValue"]
|
111
|
+
kwargs[:default_value] = begin
|
112
|
+
JSON.parse(type["defaultValue"], quirks_mode: true)
|
113
|
+
rescue JSON::ParserError
|
114
|
+
# Enum values are not valid JSON, they're bare identifiers
|
115
|
+
type["default_value"]
|
116
|
+
end
|
117
|
+
end
|
111
118
|
|
112
119
|
Argument.define(
|
113
120
|
name: type["name"],
|
@@ -196,6 +196,40 @@ module GraphQL
|
|
196
196
|
# no worries
|
197
197
|
end
|
198
198
|
}
|
199
|
+
|
200
|
+
INTERFACES_ARE_IMPLEMENTED = ->(obj_type) {
|
201
|
+
field_errors = []
|
202
|
+
obj_type.interfaces.each do |interface_type|
|
203
|
+
interface_type.fields.each do |field_name, field_defn|
|
204
|
+
object_field = obj_type.get_field(field_name)
|
205
|
+
if object_field.nil?
|
206
|
+
field_errors << %|"#{field_name}" is required by #{interface_type.name} but not implemented by #{obj_type.name}|
|
207
|
+
elsif !GraphQL::Execution::Typecast.subtype?(field_defn.type, object_field.type)
|
208
|
+
field_errors << %|"#{field_name}" is required by #{interface_type.name} to return #{field_defn.type} but #{obj_type.name}.#{field_name} returns #{object_field.type}|
|
209
|
+
else
|
210
|
+
field_defn.arguments.each do |arg_name, arg_defn|
|
211
|
+
object_field_arg = object_field.arguments[arg_name]
|
212
|
+
if object_field_arg.nil?
|
213
|
+
field_errors << %|"#{arg_name}" argument is required by #{interface_type.name}.#{field_name} but not accepted by #{obj_type.name}.#{field_name}|
|
214
|
+
elsif arg_defn.type != object_field_arg.type
|
215
|
+
field_errors << %|"#{arg_name}" is required by #{interface_type.name}.#{field_defn.name} to accept #{arg_defn.type} but #{obj_type.name}.#{field_name} accepts #{object_field_arg.type} for "#{arg_name}"|
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
object_field.arguments.each do |arg_name, arg_defn|
|
220
|
+
if field_defn.arguments[arg_name].nil? && arg_defn.type.is_a?(GraphQL::NonNullType)
|
221
|
+
field_errors << %|"#{arg_name}" is not accepted by #{interface_type.name}.#{field_name} but required by #{obj_type.name}.#{field_name}|
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
if field_errors.any?
|
228
|
+
"#{obj_type.name} failed to implement some interfaces: #{field_errors.join(", ")}"
|
229
|
+
else
|
230
|
+
nil
|
231
|
+
end
|
232
|
+
}
|
199
233
|
end
|
200
234
|
|
201
235
|
# A mapping of `{Class => [Proc, Proc...]}` pairs.
|
@@ -227,6 +261,7 @@ module GraphQL
|
|
227
261
|
GraphQL::ObjectType => [
|
228
262
|
Rules.assert_property_list_of(:interfaces, GraphQL::InterfaceType),
|
229
263
|
Rules::FIELDS_ARE_VALID,
|
264
|
+
Rules::INTERFACES_ARE_IMPLEMENTED,
|
230
265
|
],
|
231
266
|
GraphQL::InputObjectType => [
|
232
267
|
Rules::ARGUMENTS_ARE_STRING_TO_ARGUMENT,
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "graphql/static_validation/message"
|
3
3
|
require "graphql/static_validation/arguments_validator"
|
4
|
+
require "graphql/static_validation/definition_dependencies"
|
4
5
|
require "graphql/static_validation/type_stack"
|
5
6
|
require "graphql/static_validation/validator"
|
6
7
|
require "graphql/static_validation/validation_context"
|
@@ -12,6 +12,7 @@ module GraphQL
|
|
12
12
|
GraphQL::StaticValidation::UniqueDirectivesPerLocation,
|
13
13
|
GraphQL::StaticValidation::FragmentsAreFinite,
|
14
14
|
GraphQL::StaticValidation::FragmentsAreNamed,
|
15
|
+
GraphQL::StaticValidation::FragmentNamesAreUnique,
|
15
16
|
GraphQL::StaticValidation::FragmentsAreUsed,
|
16
17
|
GraphQL::StaticValidation::FragmentTypesExist,
|
17
18
|
GraphQL::StaticValidation::FragmentsAreOnCompositeTypes,
|
@@ -8,20 +8,23 @@ module GraphQL
|
|
8
8
|
def validate(context)
|
9
9
|
visitor = context.visitor
|
10
10
|
visitor[GraphQL::Language::Nodes::Argument] << ->(node, parent) {
|
11
|
-
|
11
|
+
case parent
|
12
|
+
when GraphQL::Language::Nodes::InputObject
|
12
13
|
arg_defn = context.argument_definition
|
13
14
|
if arg_defn.nil?
|
14
15
|
return
|
15
16
|
else
|
16
17
|
parent_defn = arg_defn.type.unwrap
|
17
|
-
if parent_defn.is_a?(GraphQL::
|
18
|
+
if !parent_defn.is_a?(GraphQL::InputObjectType)
|
18
19
|
return
|
19
20
|
end
|
20
21
|
end
|
21
|
-
|
22
|
+
when GraphQL::Language::Nodes::Directive
|
22
23
|
parent_defn = context.schema.directives[parent.name]
|
23
|
-
|
24
|
+
when GraphQL::Language::Nodes::Field
|
24
25
|
parent_defn = context.field_definition
|
26
|
+
else
|
27
|
+
raise "Unexpected argument parent: #{parent.class} (##{parent})"
|
25
28
|
end
|
26
29
|
validate_node(parent, node, parent_defn, context)
|
27
30
|
}
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
module StaticValidation
|
4
|
+
# Track fragment dependencies for operations
|
5
|
+
# and expose the fragment definitions which
|
6
|
+
# are used by a given operation
|
7
|
+
class DefinitionDependencies
|
8
|
+
def self.mount(visitor)
|
9
|
+
deps = self.new
|
10
|
+
deps.mount(visitor)
|
11
|
+
deps
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
# { name => node } pairs for fragments
|
16
|
+
@fragment_definitions = {}
|
17
|
+
|
18
|
+
# This tracks dependencies from fragment to Node where it was used
|
19
|
+
# { frag_name => [dependent_node, dependent_node]}
|
20
|
+
@dependent_definitions = Hash.new { |h, k| h[k] = Set.new }
|
21
|
+
|
22
|
+
# First-level usages of spreads within definitions
|
23
|
+
# (When a key has an empty list as its value,
|
24
|
+
# we can resolve that key's depenedents)
|
25
|
+
# { string => [node, node ...] }
|
26
|
+
@immediate_dependencies = Hash.new { |h, k| h[k] = Set.new }
|
27
|
+
end
|
28
|
+
|
29
|
+
# A map of operation definitions to an array of that operation's dependencies
|
30
|
+
# @return [DependencyMap]
|
31
|
+
def dependency_map(&block)
|
32
|
+
@dependency_map ||= resolve_dependencies(&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def mount(context)
|
37
|
+
visitor = context.visitor
|
38
|
+
# When we encounter a spread,
|
39
|
+
# this node is the one who depends on it
|
40
|
+
current_parent = nil
|
41
|
+
|
42
|
+
visitor[GraphQL::Language::Nodes::OperationDefinition] << ->(node, prev_node) {
|
43
|
+
current_parent = NodeWithPath.new(node, context.path)
|
44
|
+
}
|
45
|
+
|
46
|
+
visitor[GraphQL::Language::Nodes::OperationDefinition].leave << ->(node, prev_node) {
|
47
|
+
current_parent = nil
|
48
|
+
}
|
49
|
+
|
50
|
+
visitor[GraphQL::Language::Nodes::FragmentDefinition] << ->(node, prev_node) {
|
51
|
+
current_parent = @fragment_definitions[node.name] = NodeWithPath.new(node, context.path)
|
52
|
+
}
|
53
|
+
|
54
|
+
visitor[GraphQL::Language::Nodes::FragmentDefinition].leave << ->(node, prev_node) {
|
55
|
+
current_parent = nil
|
56
|
+
}
|
57
|
+
|
58
|
+
visitor[GraphQL::Language::Nodes::FragmentSpread] << ->(node, prev_node) {
|
59
|
+
# Track both sides of the dependency
|
60
|
+
@dependent_definitions[node.name] << current_parent
|
61
|
+
@immediate_dependencies[current_parent.name] << NodeWithPath.new(node, context.path)
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
# Map definition AST nodes to the definition AST nodes they depend on.
|
66
|
+
# Expose circular depednencies.
|
67
|
+
class DependencyMap
|
68
|
+
# @return [Array<GraphQL::Language::Nodes::FragmentDefinition>]
|
69
|
+
attr_reader :cyclical_definitions
|
70
|
+
|
71
|
+
# @return [Hash<Node, Array<GraphQL::Language::Nodes::FragmentSpread>>]
|
72
|
+
attr_reader :unmet_dependencies
|
73
|
+
|
74
|
+
# @return [Array<GraphQL::Language::Nodes::FragmentDefinition>]
|
75
|
+
attr_reader :unused_dependencies
|
76
|
+
|
77
|
+
def initialize
|
78
|
+
@dependencies = Hash.new { |h, k| h[k] = [] }
|
79
|
+
@cyclical_definitions = []
|
80
|
+
@unmet_dependencies = Hash.new { |h, k| h[k] = [] }
|
81
|
+
@unused_dependencies = []
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Array<GraphQL::Language::Nodes::AbstractNode>] dependencies for `definition_node`
|
85
|
+
def [](definition_node)
|
86
|
+
@dependencies[definition_node]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class NodeWithPath
|
91
|
+
extend Forwardable
|
92
|
+
attr_reader :node, :path
|
93
|
+
def initialize(node, path)
|
94
|
+
@node = node
|
95
|
+
@path = path
|
96
|
+
end
|
97
|
+
|
98
|
+
def_delegators :@node, :name, :eql?, :hash
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# Return a hash of { node => [node, node ... ]} pairs
|
104
|
+
# Keys are top-level definitions
|
105
|
+
# Values are arrays of flattened dependencies
|
106
|
+
def resolve_dependencies
|
107
|
+
dependency_map = DependencyMap.new
|
108
|
+
# Don't allow the loop to run more times
|
109
|
+
# than the number of fragments in the document
|
110
|
+
max_loops = @fragment_definitions.size
|
111
|
+
loops = 0
|
112
|
+
|
113
|
+
# Instead of tracking independent fragments _as you visit_,
|
114
|
+
# determine them at the end. This way, we can treat fragments with the
|
115
|
+
# same name as if they were the same name. If _any_ of the fragments
|
116
|
+
# with that name has a dependency, we record it.
|
117
|
+
independent_fragment_names = @fragment_definitions.each_key.select { |name| !@immediate_dependencies.key?(name) }
|
118
|
+
|
119
|
+
while fragment_name = independent_fragment_names.pop
|
120
|
+
loops += 1
|
121
|
+
if loops > max_loops
|
122
|
+
raise("Resolution loops exceeded the number of definitions; infinite loop detected.")
|
123
|
+
end
|
124
|
+
fragment_node = @fragment_definitions[fragment_name]
|
125
|
+
# Since it's independent, let's remove it from here.
|
126
|
+
# That way, we can use the remainder to identify cycles
|
127
|
+
@immediate_dependencies.delete(fragment_name)
|
128
|
+
fragment_usages = @dependent_definitions[fragment_name]
|
129
|
+
if fragment_usages.none?
|
130
|
+
# If we didn't record any usages during the visit,
|
131
|
+
# then this fragment is unused.
|
132
|
+
dependency_map.unused_dependencies << fragment_node
|
133
|
+
else
|
134
|
+
fragment_usages.each do |definition_node|
|
135
|
+
# Register the dependency AND second-order dependencies
|
136
|
+
dependency_map[definition_node] << fragment_node
|
137
|
+
dependency_map[definition_node].concat(dependency_map[fragment_node])
|
138
|
+
# Since we've regestered it, remove it from our to-do list
|
139
|
+
deps = @immediate_dependencies[definition_node.name]
|
140
|
+
# Can't find a way to _just_ delete from `deps` and return the deleted entries
|
141
|
+
removed, remaining = deps.partition { |spread| spread.name == fragment_name }
|
142
|
+
@immediate_dependencies[definition_node.name] = remaining
|
143
|
+
if block_given?
|
144
|
+
yield(definition_node, removed, fragment_node)
|
145
|
+
end
|
146
|
+
if remaining.none? && definition_node.node.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
147
|
+
# If all of this definition's dependencies have
|
148
|
+
# been resolved, we can now resolve its
|
149
|
+
# own dependents.
|
150
|
+
independent_fragment_names << definition_node.name
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# If any dependencies were _unmet_
|
157
|
+
# (eg, spreads with no corresponding definition)
|
158
|
+
# then they're still in there
|
159
|
+
@immediate_dependencies.each do |defn_name, deps|
|
160
|
+
deps.each do |spread|
|
161
|
+
if @fragment_definitions[spread.name].nil?
|
162
|
+
defn_node = @fragment_definitions[defn_name]
|
163
|
+
dependency_map.unmet_dependencies[defn_node] << spread
|
164
|
+
deps.delete(spread)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
if deps.none?
|
168
|
+
@immediate_dependencies.delete(defn_name)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Anything left in @immediate_dependencies is cyclical
|
173
|
+
cyclical_nodes = @immediate_dependencies.keys.map { |n| @fragment_definitions[n] }
|
174
|
+
# @immediate_dependencies also includes operation names, but we don't care about
|
175
|
+
# those. They became nil when we looked them up on `@fragment_definitions`, so remove them.
|
176
|
+
cyclical_nodes.compact!
|
177
|
+
dependency_map.cyclical_definitions.concat(cyclical_nodes)
|
178
|
+
|
179
|
+
dependency_map
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -3,105 +3,37 @@ module GraphQL
|
|
3
3
|
module StaticValidation
|
4
4
|
class FieldsWillMerge
|
5
5
|
def validate(context)
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
visitor[GraphQL::Language::Nodes::Document].leave << ->(node, parent) {
|
15
|
-
has_selections.each { |node|
|
16
|
-
field_map = gather_fields_by_name(node.selections, {}, [], context)
|
17
|
-
find_conflicts(field_map, [], context)
|
18
|
-
}
|
19
|
-
}
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
24
|
-
def find_conflicts(field_map, visited_fragments, context)
|
25
|
-
field_map.each do |name, ast_fields|
|
26
|
-
comparison = FieldDefinitionComparison.new(name, ast_fields, context)
|
27
|
-
context.errors.push(*comparison.errors)
|
28
|
-
|
29
|
-
|
30
|
-
subfield_map = {}
|
31
|
-
ast_fields.each do |defn|
|
32
|
-
gather_fields_by_name(defn.selections, subfield_map, visited_fragments, context)
|
33
|
-
end
|
34
|
-
|
35
|
-
find_conflicts(subfield_map, visited_fragments, context)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def gather_fields_by_name(fields, field_map, visited_fragments, context)
|
40
|
-
fields.each do |field|
|
41
|
-
case field
|
42
|
-
when GraphQL::Language::Nodes::InlineFragment
|
43
|
-
next_fields = field.selections
|
44
|
-
when GraphQL::Language::Nodes::FragmentSpread
|
45
|
-
if visited_fragments.include?(field.name)
|
46
|
-
next
|
47
|
-
else
|
48
|
-
visited_fragments << field.name
|
6
|
+
context.each_irep_node do |node|
|
7
|
+
if node.ast_nodes.size > 1
|
8
|
+
|
9
|
+
# Check for more than one GraphQL::Field backing this node:
|
10
|
+
if node.definitions.size > 1
|
11
|
+
defn_names = node.definitions.map { |d| d.name }.sort.join(" or ")
|
12
|
+
msg = "Field '#{node.name}' has a field conflict: #{defn_names}?"
|
13
|
+
context.errors << GraphQL::StaticValidation::Message.new(msg, nodes: node.ast_nodes.to_a)
|
49
14
|
end
|
50
|
-
fragment_defn = context.fragments[field.name]
|
51
|
-
next_fields = fragment_defn ? fragment_defn.selections : []
|
52
|
-
when GraphQL::Language::Nodes::Field
|
53
|
-
name_in_selection = field.alias || field.name
|
54
|
-
field_map[name_in_selection] ||= []
|
55
|
-
field_map[name_in_selection].push(field)
|
56
|
-
next_fields = []
|
57
|
-
else
|
58
|
-
raise "Unexpected field for merging: #{field}"
|
59
|
-
end
|
60
|
-
gather_fields_by_name(next_fields, field_map, visited_fragments, context)
|
61
|
-
end
|
62
|
-
field_map
|
63
|
-
end
|
64
|
-
|
65
|
-
# Compare two field definitions, add errors to the list if there are any
|
66
|
-
class FieldDefinitionComparison
|
67
|
-
include GraphQL::StaticValidation::Message::MessageHelper
|
68
|
-
NAMED_VALUES = [GraphQL::Language::Nodes::Enum, GraphQL::Language::Nodes::VariableIdentifier]
|
69
|
-
attr_reader :errors
|
70
|
-
def initialize(name, defs, context)
|
71
|
-
errors = []
|
72
|
-
|
73
|
-
names = defs.map(&:name).uniq
|
74
|
-
if names.length != 1
|
75
|
-
errors << message("Field '#{name}' has a field conflict: #{names.join(" or ")}?", defs.first, context: context)
|
76
|
-
end
|
77
15
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
else
|
95
|
-
GraphQL::Language.serialize(arg)
|
96
|
-
end
|
97
|
-
end
|
16
|
+
# Check for incompatible / non-identical arguments on this node:
|
17
|
+
args = node.ast_nodes.map do |n|
|
18
|
+
n.arguments.reduce({}) do |memo, a|
|
19
|
+
arg_value = a.value
|
20
|
+
memo[a.name] = case arg_value
|
21
|
+
when GraphQL::Language::Nodes::VariableIdentifier
|
22
|
+
"$#{arg_value.name}"
|
23
|
+
when GraphQL::Language::Nodes::Enum
|
24
|
+
"#{arg_value.name}"
|
25
|
+
else
|
26
|
+
GraphQL::Language.serialize(arg_value)
|
27
|
+
end
|
28
|
+
memo
|
29
|
+
end
|
30
|
+
end
|
31
|
+
args.uniq!
|
98
32
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
memo[a.name] = print_arg(a.value)
|
104
|
-
memo
|
33
|
+
if args.length > 1
|
34
|
+
msg = "Field '#{node.name}' has an argument conflict: #{args.map{ |arg| GraphQL::Language.serialize(arg) }.join(" or ")}?"
|
35
|
+
context.errors << GraphQL::StaticValidation::Message.new(msg, nodes: node.ast_nodes.to_a)
|
36
|
+
end
|
105
37
|
end
|
106
38
|
end
|
107
39
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
module StaticValidation
|
4
|
+
class FragmentNamesAreUnique
|
5
|
+
include GraphQL::StaticValidation::Message::MessageHelper
|
6
|
+
|
7
|
+
def validate(context)
|
8
|
+
fragments_by_name = Hash.new { |h, k| h[k] = [] }
|
9
|
+
context.visitor[GraphQL::Language::Nodes::FragmentDefinition] << ->(node, parent) {
|
10
|
+
fragments_by_name[node.name] << node
|
11
|
+
}
|
12
|
+
|
13
|
+
context.visitor[GraphQL::Language::Nodes::Document].leave << ->(node, parent) {
|
14
|
+
fragments_by_name.each do |name, fragments|
|
15
|
+
if fragments.length > 1
|
16
|
+
context.errors << message(%|Fragment name "#{name}" must be unique|, fragments, context: context)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|