graphql 1.4.5 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/enum_generator.rb +33 -0
  3. data/lib/generators/graphql/function_generator.rb +15 -0
  4. data/lib/generators/graphql/install_generator.rb +118 -0
  5. data/lib/generators/graphql/interface_generator.rb +27 -0
  6. data/lib/generators/graphql/loader_generator.rb +17 -0
  7. data/lib/generators/graphql/mutation_generator.rb +19 -0
  8. data/lib/generators/graphql/object_generator.rb +34 -0
  9. data/lib/generators/graphql/templates/enum.erb +4 -0
  10. data/lib/generators/graphql/templates/function.erb +17 -0
  11. data/lib/generators/graphql/templates/graphql_controller.erb +32 -0
  12. data/lib/generators/graphql/templates/interface.erb +4 -0
  13. data/lib/generators/graphql/templates/loader.erb +15 -0
  14. data/lib/generators/graphql/templates/mutation.erb +12 -0
  15. data/lib/generators/graphql/templates/object.erb +5 -0
  16. data/lib/generators/graphql/templates/query_type.erb +15 -0
  17. data/lib/generators/graphql/templates/schema.erb +34 -0
  18. data/lib/generators/graphql/templates/union.erb +4 -0
  19. data/lib/generators/graphql/type_generator.rb +78 -0
  20. data/lib/generators/graphql/union_generator.rb +33 -0
  21. data/lib/graphql.rb +10 -0
  22. data/lib/graphql/analysis/analyze_query.rb +1 -1
  23. data/lib/graphql/analysis/query_complexity.rb +6 -50
  24. data/lib/graphql/analysis/query_depth.rb +1 -1
  25. data/lib/graphql/argument.rb +21 -0
  26. data/lib/graphql/compatibility/execution_specification/counter_schema.rb +3 -3
  27. data/lib/graphql/define.rb +1 -0
  28. data/lib/graphql/define/assign_argument.rb +3 -19
  29. data/lib/graphql/define/assign_mutation_function.rb +34 -0
  30. data/lib/graphql/define/assign_object_field.rb +26 -14
  31. data/lib/graphql/define/defined_object_proxy.rb +21 -0
  32. data/lib/graphql/define/instance_definable.rb +61 -11
  33. data/lib/graphql/directive.rb +6 -1
  34. data/lib/graphql/execution/directive_checks.rb +1 -0
  35. data/lib/graphql/execution/execute.rb +14 -9
  36. data/lib/graphql/execution/field_result.rb +1 -0
  37. data/lib/graphql/execution/lazy.rb +8 -17
  38. data/lib/graphql/execution/lazy/lazy_method_map.rb +2 -0
  39. data/lib/graphql/execution/lazy/resolve.rb +1 -0
  40. data/lib/graphql/execution/selection_result.rb +1 -0
  41. data/lib/graphql/execution/typecast.rb +39 -26
  42. data/lib/graphql/field.rb +15 -3
  43. data/lib/graphql/field/resolve.rb +3 -3
  44. data/lib/graphql/function.rb +134 -0
  45. data/lib/graphql/id_type.rb +1 -1
  46. data/lib/graphql/input_object_type.rb +1 -1
  47. data/lib/graphql/internal_representation.rb +1 -1
  48. data/lib/graphql/internal_representation/node.rb +35 -107
  49. data/lib/graphql/internal_representation/rewrite.rb +189 -183
  50. data/lib/graphql/internal_representation/visit.rb +38 -0
  51. data/lib/graphql/introspection/input_value_type.rb +10 -1
  52. data/lib/graphql/introspection/schema_type.rb +1 -1
  53. data/lib/graphql/language/lexer.rb +6 -3
  54. data/lib/graphql/language/lexer.rl +6 -3
  55. data/lib/graphql/object_type.rb +53 -13
  56. data/lib/graphql/query.rb +30 -14
  57. data/lib/graphql/query/arguments.rb +2 -0
  58. data/lib/graphql/query/context.rb +2 -2
  59. data/lib/graphql/query/literal_input.rb +9 -0
  60. data/lib/graphql/query/serial_execution/field_resolution.rb +2 -2
  61. data/lib/graphql/query/serial_execution/selection_resolution.rb +1 -1
  62. data/lib/graphql/relay.rb +1 -0
  63. data/lib/graphql/relay/array_connection.rb +1 -1
  64. data/lib/graphql/relay/base_connection.rb +34 -15
  65. data/lib/graphql/relay/connection_resolve.rb +7 -2
  66. data/lib/graphql/relay/mutation.rb +45 -4
  67. data/lib/graphql/relay/node.rb +18 -6
  68. data/lib/graphql/relay/range_add.rb +45 -0
  69. data/lib/graphql/relay/relation_connection.rb +17 -2
  70. data/lib/graphql/runtime_type_error.rb +1 -0
  71. data/lib/graphql/schema.rb +40 -5
  72. data/lib/graphql/schema/base_64_encoder.rb +1 -0
  73. data/lib/graphql/schema/build_from_definition.rb +56 -21
  74. data/lib/graphql/schema/default_parse_error.rb +10 -0
  75. data/lib/graphql/schema/loader.rb +8 -1
  76. data/lib/graphql/schema/null_mask.rb +1 -0
  77. data/lib/graphql/schema/validation.rb +35 -0
  78. data/lib/graphql/static_validation.rb +1 -0
  79. data/lib/graphql/static_validation/all_rules.rb +1 -0
  80. data/lib/graphql/static_validation/arguments_validator.rb +7 -4
  81. data/lib/graphql/static_validation/definition_dependencies.rb +183 -0
  82. data/lib/graphql/static_validation/rules/fields_will_merge.rb +28 -96
  83. data/lib/graphql/static_validation/rules/fragment_names_are_unique.rb +23 -0
  84. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +8 -5
  85. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +6 -31
  86. data/lib/graphql/static_validation/rules/fragments_are_used.rb +11 -41
  87. data/lib/graphql/static_validation/rules/operation_names_are_valid.rb +2 -2
  88. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +19 -7
  89. data/lib/graphql/static_validation/validation_context.rb +22 -1
  90. data/lib/graphql/static_validation/validator.rb +4 -1
  91. data/lib/graphql/string_type.rb +5 -1
  92. data/lib/graphql/version.rb +1 -1
  93. data/readme.md +12 -3
  94. data/spec/generators/graphql/enum_generator_spec.rb +29 -0
  95. data/spec/generators/graphql/function_generator_spec.rb +33 -0
  96. data/spec/generators/graphql/install_generator_spec.rb +185 -0
  97. data/spec/generators/graphql/interface_generator_spec.rb +32 -0
  98. data/spec/generators/graphql/loader_generator_spec.rb +31 -0
  99. data/spec/generators/graphql/mutation_generator_spec.rb +28 -0
  100. data/spec/generators/graphql/object_generator_spec.rb +42 -0
  101. data/spec/generators/graphql/union_generator_spec.rb +50 -0
  102. data/spec/graphql/analysis/query_complexity_spec.rb +2 -1
  103. data/spec/graphql/define/instance_definable_spec.rb +38 -0
  104. data/spec/graphql/directive/skip_directive_spec.rb +1 -0
  105. data/spec/graphql/directive_spec.rb +18 -0
  106. data/spec/graphql/execution/typecast_spec.rb +41 -46
  107. data/spec/graphql/field_spec.rb +1 -1
  108. data/spec/graphql/function_spec.rb +128 -0
  109. data/spec/graphql/internal_representation/rewrite_spec.rb +166 -129
  110. data/spec/graphql/introspection/type_type_spec.rb +1 -1
  111. data/spec/graphql/language/lexer_spec.rb +6 -0
  112. data/spec/graphql/object_type_spec.rb +73 -2
  113. data/spec/graphql/query/arguments_spec.rb +28 -0
  114. data/spec/graphql/query/variables_spec.rb +7 -1
  115. data/spec/graphql/query_spec.rb +30 -0
  116. data/spec/graphql/relay/base_connection_spec.rb +26 -8
  117. data/spec/graphql/relay/connection_resolve_spec.rb +45 -0
  118. data/spec/graphql/relay/connection_type_spec.rb +21 -0
  119. data/spec/graphql/relay/node_spec.rb +30 -2
  120. data/spec/graphql/relay/range_add_spec.rb +113 -0
  121. data/spec/graphql/schema/build_from_definition_spec.rb +114 -0
  122. data/spec/graphql/schema/loader_spec.rb +1 -0
  123. data/spec/graphql/schema/printer_spec.rb +2 -2
  124. data/spec/graphql/schema/validation_spec.rb +80 -11
  125. data/spec/graphql/schema/warden_spec.rb +10 -10
  126. data/spec/graphql/schema_spec.rb +18 -1
  127. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +16 -0
  128. data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +50 -3
  129. data/spec/graphql/static_validation/rules/fragment_names_are_unique_spec.rb +27 -0
  130. data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +57 -0
  131. data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +1 -1
  132. data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +14 -0
  133. data/spec/graphql/string_type_spec.rb +7 -0
  134. data/spec/spec_helper.rb +3 -3
  135. data/spec/support/base_generator_test.rb +7 -0
  136. data/spec/support/dummy/schema.rb +32 -30
  137. data/spec/support/star_wars/schema.rb +81 -23
  138. metadata +98 -20
  139. data/lib/graphql/internal_representation/selection.rb +0 -85
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Schema
4
+ module DefaultParseError
5
+ def self.call(parse_error, ctx)
6
+ ctx.errors.push(parse_error)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -107,7 +107,14 @@ module GraphQL
107
107
  )
108
108
  when "ARGUMENT"
109
109
  kwargs = {}
110
- kwargs[:default_value] = JSON.parse(type["defaultValue"], quirks_mode: true) if type["defaultValue"]
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"],
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GraphQL
2
3
  class Schema
3
4
  # @api private
@@ -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
- if parent.is_a?(GraphQL::Language::Nodes::InputObject)
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::ScalarType)
18
+ if !parent_defn.is_a?(GraphQL::InputObjectType)
18
19
  return
19
20
  end
20
21
  end
21
- elsif parent.is_a?(GraphQL::Language::Nodes::Directive)
22
+ when GraphQL::Language::Nodes::Directive
22
23
  parent_defn = context.schema.directives[parent.name]
23
- else
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
- fragments = {}
7
- has_selections = []
8
- visitor = context.visitor
9
- visitor[GraphQL::Language::Nodes::OperationDefinition] << ->(node, parent) {
10
- if node.selections.any?
11
- has_selections << node
12
- end
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
- args = defs.map { |defn| reduce_list(defn.arguments)}.uniq
79
- if args.length != 1
80
- errors << message("Field '#{name}' has an argument conflict: #{args.map{ |arg| GraphQL::Language.serialize(arg) }.join(" or ")}?", defs.first, context: context)
81
- end
82
-
83
- @errors = errors
84
- end
85
-
86
- private
87
-
88
- def print_arg(arg)
89
- case arg
90
- when GraphQL::Language::Nodes::VariableIdentifier
91
- "$#{arg.name}"
92
- when GraphQL::Language::Nodes::Enum
93
- "#{arg.name}"
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
- # Turn AST tree into a hash
100
- # can't look up args, the names just have to match
101
- def reduce_list(args)
102
- args.reduce({}) do |memo, a|
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