graphql 1.2.4 → 1.2.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f79f91876d8a49ea48c4d551a073491c33eefc31
4
- data.tar.gz: 3e91b2b21c60df562e95879a4a8fd24f214553e2
3
+ metadata.gz: b49f51c993d3f37fb6596a0547175b9cb1e6dfe0
4
+ data.tar.gz: 4e639bf70ca713dd8a795690a30b2eafca16c5be
5
5
  SHA512:
6
- metadata.gz: 86a409aedbc0f2261b80b1f4cbca9e5fcc529abdb609557299de5547ff554e5f98a35188dcf6ba1a82650995f8989d867738c98aeafda4de7686b33932dff371
7
- data.tar.gz: 23135beccd828141bb898bd12514899a0125035f38284146f2941a36e5117449854759e6b202937996e078782123e7f87437f5244be6af392186fd8d1d798cdc
6
+ metadata.gz: 8ba6eec79d60b4e4fac5a3779dce895c9e757e3bd923b64d6d77b2702638ed2dcb6b57ae914a6be2e582cff6cad29a3f3f18662e90b7ff5076f56610c75c2519
7
+ data.tar.gz: 39e999428ae079e15e942852e295518bb242b6c4dcb65ca32f31cdcde7d2518427f34dbd39d5b92a8e72d70762f85d0e85079d4efe4d670bf2ea674f98e542f8
@@ -16,13 +16,13 @@ module GraphQL
16
16
  # Figure out how to find or initialize the field instance:
17
17
  if type_or_field.is_a?(GraphQL::Field)
18
18
  field = type_or_field
19
- field.name ||= name_s
19
+ field = field.redefine(name: name_s)
20
20
  elsif block_given?
21
21
  field = GraphQL::Field.define(kwargs, &block)
22
22
  elsif field.nil?
23
23
  field = GraphQL::Field.define(kwargs)
24
24
  elsif field.is_a?(GraphQL::Field)
25
- field.name ||= name_s
25
+ field = field.redefine(name: name_s)
26
26
  else
27
27
  raise("Couldn't find a field argument, received: #{field || type_or_field}")
28
28
  end
@@ -100,7 +100,7 @@ module GraphQL
100
100
  def redefine(**kwargs, &block)
101
101
  ensure_defined
102
102
  new_instance = self.class.new
103
- applied_definitions.each { |defn| defn.apply(new_instance) }
103
+ applied_definitions.each { |defn| new_instance.define(defn.define_keywords, &defn.define_proc) }
104
104
  new_instance.define(**kwargs, &block)
105
105
  new_instance
106
106
  end
@@ -116,7 +116,16 @@ module GraphQL
116
116
  if @pending_definition
117
117
  defn = @pending_definition
118
118
  @pending_definition = nil
119
- defn.apply(self)
119
+ defn_proxy = DefinedObjectProxy.new(self)
120
+ # Apply definition from `define(...)` kwargs
121
+ defn.define_keywords.each do |keyword, value|
122
+ defn_proxy.public_send(keyword, value)
123
+ end
124
+ # and/or apply definition from `define { ... }` block
125
+ if defn.define_proc
126
+ defn_proxy.instance_eval(&defn.define_proc)
127
+ end
128
+
120
129
  applied_definitions << defn
121
130
  end
122
131
  nil
@@ -126,24 +135,12 @@ module GraphQL
126
135
  @applied_definitions ||= []
127
136
  end
128
137
 
129
-
130
138
  class Definition
139
+ attr_reader :define_keywords, :define_proc
131
140
  def initialize(define_keywords, define_proc)
132
141
  @define_keywords = define_keywords
133
142
  @define_proc = define_proc
134
143
  end
135
-
136
- def apply(instance)
137
- defn_proxy = DefinedObjectProxy.new(instance)
138
-
139
- @define_keywords.each do |keyword, value|
140
- defn_proxy.public_send(keyword, value)
141
- end
142
-
143
- if @define_proc
144
- defn_proxy.instance_eval(&@define_proc)
145
- end
146
- end
147
144
  end
148
145
 
149
146
  module ClassMethods
@@ -123,13 +123,19 @@ module GraphQL
123
123
  accepts_definitions :name, :description, :deprecation_reason,
124
124
  :resolve, :type, :arguments,
125
125
  :property, :hash_key, :complexity, :mutation,
126
+ :relay_node_field,
126
127
  argument: GraphQL::Define::AssignArgument
127
128
 
128
129
 
129
130
  attr_accessor :name, :deprecation_reason, :description, :property, :hash_key, :mutation, :arguments, :complexity
131
+
132
+ # @return [Boolean] True if this is the Relay find-by-id field
133
+ attr_accessor :relay_node_field
134
+
130
135
  ensure_defined(
131
- :name, :deprecation_reason, :description, :property, :hash_key, :mutation, :arguments, :complexity,
132
- :resolve, :resolve=, :type, :type=, :name=, :property=, :hash_key=
136
+ :name, :deprecation_reason, :description, :description=, :property, :hash_key, :mutation, :arguments, :complexity,
137
+ :resolve, :resolve=, :type, :type=, :name=, :property=, :hash_key=,
138
+ :relay_node_field,
133
139
  )
134
140
 
135
141
  # @!attribute [r] resolve_proc
@@ -152,6 +158,7 @@ module GraphQL
152
158
  @complexity = 1
153
159
  @arguments = {}
154
160
  @resolve_proc = build_default_resolver
161
+ @relay_node_field = false
155
162
  end
156
163
 
157
164
  # Get a value for this field
@@ -30,9 +30,8 @@ module GraphQL
30
30
  )
31
31
 
32
32
  attr_accessor :mutation, :arguments
33
- alias :input_fields :arguments
34
-
35
33
  ensure_defined(:mutation, :arguments)
34
+ alias :input_fields :arguments
36
35
 
37
36
  # @!attribute mutation
38
37
  # @return [GraphQL::Relay::Mutation, nil] The mutation this field was derived from, if it was derived from a mutation
@@ -17,16 +17,9 @@ module GraphQL
17
17
  # node right away
18
18
  SKIP = :_skip
19
19
 
20
- # @return [Array<Proc>] Hooks to call when entering _any_ node
21
- attr_reader :enter
22
- # @return [Array<Proc>] Hooks to call when leaving _any_ node
23
- attr_reader :leave
24
-
25
20
  def initialize(document)
26
21
  @document = document
27
22
  @visitors = {}
28
- @enter = []
29
- @leave = []
30
23
  end
31
24
 
32
25
  # Get a {NodeVisitor} for `node_class`
@@ -50,20 +43,18 @@ module GraphQL
50
43
  def visit_node(node, parent)
51
44
  begin_hooks_ok = begin_visit(node, parent)
52
45
  if begin_hooks_ok
53
- node.children.reduce(true) { |memo, child| memo && visit_node(child, node) }
46
+ node.children.each { |child| visit_node(child, node) }
54
47
  end
55
48
  end_visit(node, parent)
56
49
  end
57
50
 
58
51
  def begin_visit(node, parent)
59
- self.class.apply_hooks(enter, node, parent)
60
52
  node_visitor = self[node.class]
61
53
  self.class.apply_hooks(node_visitor.enter, node, parent)
62
54
  end
63
55
 
64
56
  # Should global `leave` visitors come first or last?
65
57
  def end_visit(node, parent)
66
- self.class.apply_hooks(leave, node, parent)
67
58
  node_visitor = self[node.class]
68
59
  self.class.apply_hooks(node_visitor.leave, node, parent)
69
60
  end
@@ -240,6 +240,7 @@ module GraphQL
240
240
  def call
241
241
  @instrumenters.each { |i| i.before_query(@query) }
242
242
  result = get_result
243
+ ensure
243
244
  @instrumenters.each { |i| i.after_query(@query) }
244
245
  result
245
246
  end
@@ -60,19 +60,23 @@ module GraphQL
60
60
  NULL_ARGUMENT_VALUE = ArgumentValue.new(nil, nil, nil)
61
61
 
62
62
  def wrap_value(value, arg_defn_type)
63
- case arg_defn_type
64
- when GraphQL::ListType
65
- value.map { |item| wrap_value(item, arg_defn_type.of_type) }
66
- when GraphQL::NonNullType
67
- wrap_value(value, arg_defn_type.of_type)
68
- when GraphQL::InputObjectType
69
- if value.is_a?(Hash)
70
- self.class.new(value, argument_definitions: arg_defn_type.arguments)
63
+ if value.nil?
64
+ nil
65
+ else
66
+ case arg_defn_type
67
+ when GraphQL::ListType
68
+ value.map { |item| wrap_value(item, arg_defn_type.of_type) }
69
+ when GraphQL::NonNullType
70
+ wrap_value(value, arg_defn_type.of_type)
71
+ when GraphQL::InputObjectType
72
+ if value.is_a?(Hash)
73
+ self.class.new(value, argument_definitions: arg_defn_type.arguments)
74
+ else
75
+ value
76
+ end
71
77
  else
72
78
  value
73
79
  end
74
- else
75
- value
76
80
  end
77
81
  end
78
82
 
@@ -27,7 +27,7 @@ module GraphQL
27
27
  # Turn A GraphQL::Field into a connection by:
28
28
  # - Merging in the default arguments
29
29
  # - Transforming its resolve function to return a connection object
30
- # @param [GraphQL::Field] A field which returns nodes to be wrapped as a connection
30
+ # @param underlying_field [GraphQL::Field] A field which returns nodes to be wrapped as a connection
31
31
  # @param max_page_size [Integer] The maximum number of nodes which may be requested (if a larger page is requested, it is limited to this number)
32
32
  # @return [GraphQL::Field] The same field, modified to resolve to a connection object
33
33
  def self.create(underlying_field, max_page_size: nil)
@@ -148,7 +148,7 @@ module GraphQL
148
148
  attr_reader :client_mutation_id
149
149
  def initialize(client_mutation_id:, result:)
150
150
  @client_mutation_id = client_mutation_id
151
- result.each do |key, value|
151
+ result && result.each do |key, value|
152
152
  self.public_send("#{key}=", value)
153
153
  end
154
154
  end
@@ -175,6 +175,12 @@ module GraphQL
175
175
 
176
176
  def call(obj, args, ctx)
177
177
  mutation_result = @resolve.call(obj, args[:input], ctx)
178
+
179
+ if mutation_result.is_a?(GraphQL::ExecutionError)
180
+ ctx.add_error(mutation_result)
181
+ mutation_result = nil
182
+ end
183
+
178
184
  if @wrap_result
179
185
  @mutation.result_class.new(client_mutation_id: args[:input][:clientMutationId], result: mutation_result)
180
186
  else
@@ -7,17 +7,13 @@ module GraphQL
7
7
  # We have to define it fresh each time because
8
8
  # its name will be modified and its description
9
9
  # _may_ be modified.
10
- node_field = GraphQL::Field.define do
10
+ GraphQL::Field.define do
11
11
  type(GraphQL::Relay::Node.interface)
12
12
  description("Fetches an object given its ID.")
13
13
  argument(:id, !types.ID, "ID of the object.")
14
14
  resolve(GraphQL::Relay::Node::FindNode)
15
+ relay_node_field(true)
15
16
  end
16
-
17
- # This is used to identify generated fields in the schema
18
- node_field.metadata[:relay_node_field] = true
19
-
20
- node_field
21
17
  end
22
18
 
23
19
  # @return [GraphQL::InterfaceType] The interface which all Relay types must implement
@@ -109,27 +109,24 @@ module GraphQL
109
109
  ensure_defined
110
110
  all_types = orphan_types + [query, mutation, subscription, GraphQL::Introspection::SchemaType]
111
111
  @types = GraphQL::Schema::ReduceTypes.reduce(all_types.compact)
112
-
113
- @instrumented_field_map = InstrumentedFieldMap.new(self)
114
- field_instrumenters = @instrumenters[:field]
115
- types.each do |type_name, type|
116
- if type.kind.fields?
117
- type.all_fields.each do |field_defn|
118
-
119
- instrumented_field_defn = field_instrumenters.reduce(field_defn) do |defn, inst|
120
- inst.instrument(type, defn)
121
- end
122
-
123
- @instrumented_field_map.set(type.name, field_defn.name, instrumented_field_defn)
124
- end
125
- end
126
- end
112
+ build_instrumented_field_map
127
113
  # Assert that all necessary configs are present:
128
114
  validation_error = Validation.validate(self)
129
115
  validation_error && raise(NotImplementedError, validation_error)
130
116
  nil
131
117
  end
132
118
 
119
+ # Attach `instrumenter` to this schema for instrumenting events of `instrumentation_type`.
120
+ # @param instrumentation_type [Symbol]
121
+ # @param instrumenter
122
+ # @return [void]
123
+ def instrument(instrumentation_type, instrumenter)
124
+ @instrumenters[instrumentation_type] << instrumenter
125
+ if instrumentation_type == :field
126
+ build_instrumented_field_map
127
+ end
128
+ end
129
+
133
130
 
134
131
  # @see [GraphQL::Schema::Warden] Restricted access to members of a schema
135
132
  # @return [GraphQL::Schema::TypeMap] `{ name => type }` pairs of types in this schema
@@ -289,5 +286,9 @@ module GraphQL
289
286
  middleware
290
287
  end
291
288
  end
289
+
290
+ def build_instrumented_field_map
291
+ @instrumented_field_map = InstrumentedFieldMap.new(self, @instrumenters[:field])
292
+ end
292
293
  end
293
294
  end
@@ -6,8 +6,21 @@ module GraphQL
6
6
  #
7
7
  # The catch is, the fields in this map _may_ have been modified by being instrumented.
8
8
  class InstrumentedFieldMap
9
- def initialize(schema)
9
+ # Build a map using types from `schema` and instrumenters in `field_instrumenters`
10
+ # @param schema [GraphQL::Schema]
11
+ # @param field_instrumenters [Array<#instrument(type, field)>]
12
+ def initialize(schema, field_instrumenters)
10
13
  @storage = Hash.new { |h, k| h[k] = {} }
14
+ schema.types.each do |type_name, type|
15
+ if type.kind.fields?
16
+ type.all_fields.each do |field_defn|
17
+ instrumented_field_defn = field_instrumenters.reduce(field_defn) do |defn, inst|
18
+ inst.instrument(type, defn)
19
+ end
20
+ self.set(type.name, field_defn.name, instrumented_field_defn)
21
+ end
22
+ end
23
+ end
11
24
  end
12
25
 
13
26
  def set(type_name, field_name, field)
@@ -139,7 +139,7 @@ module GraphQL
139
139
  }
140
140
 
141
141
  SCHEMA_CAN_FETCH_IDS = ->(schema) {
142
- has_node_field = schema.query && schema.query.all_fields.any? { |f| f.metadata[:relay_node_field] }
142
+ has_node_field = schema.query && schema.query.all_fields.any?(&:relay_node_field)
143
143
  if has_node_field && schema.object_from_id_proc.nil?
144
144
  "schema contains `node(id:...)` field, so you must define a `object_from_id (id, ctx) -> { ... }` function"
145
145
  else
@@ -8,6 +8,7 @@ module GraphQL
8
8
  ALL_RULES = [
9
9
  GraphQL::StaticValidation::DirectivesAreDefined,
10
10
  GraphQL::StaticValidation::DirectivesAreInValidLocations,
11
+ GraphQL::StaticValidation::UniqueDirectivesPerLocation,
11
12
  GraphQL::StaticValidation::FragmentsAreFinite,
12
13
  GraphQL::StaticValidation::FragmentsAreNamed,
13
14
  GraphQL::StaticValidation::FragmentsAreUsed,
@@ -1,23 +1,22 @@
1
1
  module GraphQL
2
2
  module StaticValidation
3
3
  # Generates GraphQL-compliant validation message.
4
- # Only supports one "location", too bad :(
5
4
  class Message
6
5
  # Convenience for validators
7
6
  module MessageHelper
8
7
  # Error `message` is located at `node`
9
- def message(message, node, context: nil, path: nil)
8
+ def message(message, nodes, context: nil, path: nil)
10
9
  path ||= context.path
11
- GraphQL::StaticValidation::Message.new(message, line: node.line, col: node.col, path: path)
10
+ nodes = Array(nodes)
11
+ GraphQL::StaticValidation::Message.new(message, nodes: nodes, path: path)
12
12
  end
13
13
  end
14
14
 
15
- attr_reader :message, :line, :col, :path
15
+ attr_reader :message, :path
16
16
 
17
- def initialize(message, line: nil, col: nil, path: [])
17
+ def initialize(message, path: [], nodes: [])
18
18
  @message = message
19
- @line = line
20
- @col = col
19
+ @nodes = nodes
21
20
  @path = path
22
21
  end
23
22
 
@@ -33,7 +32,7 @@ module GraphQL
33
32
  private
34
33
 
35
34
  def locations
36
- @line.nil? && @col.nil? ? [] : [{"line" => @line, "column" => @col}]
35
+ @nodes.map{|node| {"line" => node.line, "column" => node.col}}
37
36
  end
38
37
  end
39
38
  end
@@ -0,0 +1,38 @@
1
+ module GraphQL
2
+ module StaticValidation
3
+ class UniqueDirectivesPerLocation
4
+ include GraphQL::StaticValidation::Message::MessageHelper
5
+
6
+ NODES_WITH_DIRECTIVES = GraphQL::Language::Nodes.constants
7
+ .map{|c| GraphQL::Language::Nodes.const_get(c)}
8
+ .select{|c| c.is_a?(Class) && c.instance_methods.include?(:directives)}
9
+
10
+ def validate(context)
11
+ NODES_WITH_DIRECTIVES.each do |node_class|
12
+ context.visitor[node_class] << ->(node, _) {
13
+ validate_directives(node, context) unless node.directives.empty?
14
+ }
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def validate_directives(node, context)
21
+ used_directives = {}
22
+
23
+ node.directives.each do |ast_directive|
24
+ directive_name = ast_directive.name
25
+ if used_directives[directive_name]
26
+ context.errors << message(
27
+ "The directive \"#{directive_name}\" can only be used once at this location.",
28
+ [used_directives[directive_name], ast_directive],
29
+ context: context
30
+ )
31
+ else
32
+ used_directives[directive_name] = ast_directive
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -41,18 +41,15 @@ module GraphQL
41
41
  @directive_definitions = []
42
42
  @argument_definitions = []
43
43
  @path = []
44
- visitor.enter << ->(node, parent) { PUSH_STRATEGIES[node.class].push(self, node) }
45
- visitor.leave << ->(node, parent) { PUSH_STRATEGIES[node.class].pop(self, node) }
44
+
45
+ PUSH_STRATEGIES.each do |node_class, strategy|
46
+ visitor[node_class].enter << EnterWithStrategy.new(self, strategy)
47
+ visitor[node_class].leave << LeaveWithStrategy.new(self, strategy)
48
+ end
46
49
  end
47
50
 
48
51
  private
49
52
 
50
- # Look up strategies by name and use singleton instance to push and pop
51
- PUSH_STRATEGIES = Hash.new do |hash, key|
52
- node_class_name = key.name.split("::").last
53
- strategy_key = "#{node_class_name}Strategy"
54
- hash[key] = const_defined?(strategy_key) ? const_get(strategy_key) : NullStrategy
55
- end
56
53
 
57
54
  module FragmentWithTypeStrategy
58
55
  def push(stack, node)
@@ -182,10 +179,36 @@ module GraphQL
182
179
  end
183
180
  end
184
181
 
185
- # A no-op strategy (don't handle this node)
186
- module NullStrategy
187
- def self.push(stack, node); end
188
- def self.pop(stack, node); end
182
+ PUSH_STRATEGIES = {
183
+ GraphQL::Language::Nodes::FragmentDefinition => FragmentDefinitionStrategy,
184
+ GraphQL::Language::Nodes::InlineFragment => InlineFragmentStrategy,
185
+ GraphQL::Language::Nodes::FragmentSpread => FragmentSpreadStrategy,
186
+ GraphQL::Language::Nodes::Argument => ArgumentStrategy,
187
+ GraphQL::Language::Nodes::Field => FieldStrategy,
188
+ GraphQL::Language::Nodes::Directive => DirectiveStrategy,
189
+ GraphQL::Language::Nodes::OperationDefinition => OperationDefinitionStrategy,
190
+ }
191
+
192
+ class EnterWithStrategy
193
+ def initialize(stack, strategy)
194
+ @stack = stack
195
+ @strategy = strategy
196
+ end
197
+
198
+ def call(node, parent)
199
+ @strategy.push(@stack, node)
200
+ end
201
+ end
202
+
203
+ class LeaveWithStrategy
204
+ def initialize(stack, strategy)
205
+ @stack = stack
206
+ @strategy = strategy
207
+ end
208
+
209
+ def call(node, parent)
210
+ @strategy.pop(@stack, node)
211
+ end
189
212
  end
190
213
  end
191
214
  end
@@ -1,3 +1,3 @@
1
1
  module GraphQL
2
- VERSION = "1.2.4"
2
+ VERSION = "1.2.5"
3
3
  end
@@ -18,6 +18,10 @@ module Garden
18
18
  # definition added later:
19
19
  attr_accessor :height
20
20
  ensure_defined(:height)
21
+
22
+ def color
23
+ metadata[:color]
24
+ end
21
25
  end
22
26
  end
23
27
 
@@ -84,15 +88,24 @@ describe GraphQL::Define::InstanceDefinable do
84
88
  name "Renamed Red Arugula"
85
89
  end
86
90
 
87
- assert_equal :green, arugula.metadata[:color]
91
+ assert_equal :green, arugula.color
88
92
  assert_equal "Arugula", arugula.name
89
93
 
90
- assert_equal :red, red_arugula.metadata[:color]
94
+ assert_equal :red, red_arugula.color
91
95
  assert_equal "Arugula", red_arugula.name
92
96
 
93
- assert_equal :red, renamed_red_arugula.metadata[:color]
97
+ assert_equal :red, renamed_red_arugula.color
94
98
  assert_equal "Renamed Red Arugula", renamed_red_arugula.name
95
99
  end
100
+
101
+ it "can be chained several times" do
102
+ arugula_1 = Garden::Vegetable.define(name: "Arugula") { color :green }
103
+ arugula_2 = arugula_1.redefine { color :red }
104
+ arugula_3 = arugula_2.redefine { plant_between(1..3) }
105
+ assert_equal ["Arugula", :green], [arugula_1.name, arugula_1.color]
106
+ assert_equal ["Arugula", :red], [arugula_2.name, arugula_2.color]
107
+ assert_equal ["Arugula", :red], [arugula_3.name, arugula_3.color]
108
+ end
96
109
  end
97
110
 
98
111
  describe "#metadata" do
@@ -120,4 +120,34 @@ describe GraphQL::Field do
120
120
  assert_equal [:cheeses, :milks], similar_cheese_field.metadata[:joins]
121
121
  end
122
122
  end
123
+
124
+ describe "reusing a GraphQL::Field" do
125
+ let(:schema) {
126
+ int_field = GraphQL::Field.define do
127
+ type types.Int
128
+ argument :value, types.Int
129
+ resolve -> (obj, args, ctx) { args[:value] }
130
+ end
131
+
132
+ query_type = GraphQL::ObjectType.define do
133
+ name "Query"
134
+ field :int, int_field
135
+ field :int2, int_field
136
+ field :int3, field: int_field
137
+ end
138
+
139
+ GraphQL::Schema.define do
140
+ query(query_type)
141
+ end
142
+ }
143
+
144
+ it "can be used in two places" do
145
+ res = schema.execute %|{ int(value: 1) int2(value: 2) int3(value: 3) }|
146
+ assert_equal({ "int" => 1, "int2" => 2, "int3" => 3}, res["data"], "It works in queries")
147
+
148
+ res = schema.execute %|{ __type(name: "Query") { fields { name } } }|
149
+ query_field_names = res["data"]["__type"]["fields"].map { |f| f["name"] }
150
+ assert_equal ["int", "int2", "int3"], query_field_names, "It works in introspection"
151
+ end
152
+ end
123
153
  end
@@ -10,6 +10,18 @@ describe GraphQL::InputObjectType do
10
10
  assert(DairyProductInputType.input_fields["fatContent"])
11
11
  end
12
12
 
13
+ describe "on a type unused by the schema" do
14
+ it "has input fields" do
15
+ UnreachedInputType = GraphQL::InputObjectType.define do
16
+ name 'UnreachedInputType'
17
+ description 'An input object type not directly used in the schema.'
18
+
19
+ input_field :field, types.String
20
+ end
21
+ assert(UnreachedInputType.input_fields['field'])
22
+ end
23
+ end
24
+
13
25
  describe "input validation" do
14
26
  it "Accepts anything that yields key-value pairs to #all?" do
15
27
  values_obj = MinimumInputObject.new({"source" => "COW", "fatContent" => 0.4})
@@ -2,30 +2,69 @@ require "spec_helper"
2
2
 
3
3
  describe GraphQL::Query::Variables do
4
4
  let(:query_string) {%|
5
- query getCheese($animals: [DairyAnimal], $int: Int, $intWithDefault: Int = 10) {
5
+ query getCheese($animals: [DairyAnimal!], $int: Int, $intWithDefault: Int = 10) {
6
6
  cheese(id: 1) {
7
7
  similarCheese(source: $animals)
8
8
  }
9
9
  }
10
10
  |}
11
11
  let(:ast_variables) { GraphQL.parse(query_string).definitions.first.variables }
12
+ let(:schema) { DummySchema }
12
13
  let(:variables) { GraphQL::Query::Variables.new(
13
- DummySchema,
14
- GraphQL::Schema::Warden.new(DummySchema, GraphQL::Query::NullExcept),
14
+ schema,
15
+ GraphQL::Schema::Warden.new(schema, GraphQL::Query::NullExcept),
15
16
  ast_variables,
16
17
  provided_variables)
17
18
  }
18
19
 
19
20
  describe "#initialize" do
20
21
  describe "coercing inputs" do
21
- let(:provided_variables) {
22
- {"animals" => "YAK"}
23
- }
22
+ let(:provided_variables) { { "animals" => "YAK" } }
23
+
24
24
  it "coerces single items into one-element lists" do
25
25
  assert_equal ["YAK"], variables["animals"]
26
26
  end
27
27
  end
28
28
 
29
+ describe "nullable variables" do
30
+ let(:schema) { GraphQL::Schema.from_definition(%|
31
+ type Query {
32
+ thingsCount(ids: [ID!]): Int!
33
+ }
34
+ |)
35
+ }
36
+ let(:query_string) {%|
37
+ query getThingsCount($ids: [ID!]) {
38
+ thingsCount(ids: $ids)
39
+ }
40
+ |}
41
+ let(:result) {
42
+ schema.execute(query_string, variables: provided_variables, root_value: OpenStruct.new(thingsCount: 1))
43
+ }
44
+
45
+ describe "when they are present, but null" do
46
+ let(:provided_variables) { { "ids" => nil } }
47
+ it "ignores them" do
48
+ assert_equal 1, result["data"]["thingsCount"]
49
+ end
50
+ end
51
+
52
+ describe "when they are not present" do
53
+ let(:provided_variables) { {} }
54
+ it "ignores them" do
55
+ assert_equal 1, result["data"]["thingsCount"]
56
+ end
57
+ end
58
+
59
+ describe "when a nullable list has a null in it" do
60
+ let(:provided_variables) { { "ids" => [nil] } }
61
+ it "returns an error" do
62
+ assert_equal 1, result["errors"].length
63
+ assert_equal nil, result["data"]
64
+ end
65
+ end
66
+ end
67
+
29
68
  describe "coercing null" do
30
69
  let(:provided_variables) {
31
70
  {"int" => nil, "intWithDefault" => nil}
@@ -9,4 +9,26 @@ describe GraphQL::Relay::ConnectionField do
9
9
 
10
10
  assert_equal ["tests"], test_type.fields.keys
11
11
  end
12
+
13
+ it "leaves the original field untouched" do
14
+ test_type = nil
15
+
16
+ test_field = GraphQL::Field.define do
17
+ type(test_type.connection_type)
18
+ property(:something)
19
+ end
20
+
21
+ test_type = GraphQL::ObjectType.define do
22
+ name "Test"
23
+ connection :tests, test_field
24
+ end
25
+
26
+ conn_field = test_type.fields["tests"]
27
+
28
+ assert_equal 0, test_field.arguments.length
29
+ assert_equal 4, conn_field.arguments.length
30
+
31
+ assert_instance_of GraphQL::Field::Resolve::MethodResolve, test_field.resolve_proc
32
+ assert_instance_of GraphQL::Relay::ConnectionResolve, conn_field.resolve_proc
33
+ end
12
34
  end
@@ -77,6 +77,23 @@ describe GraphQL::Relay::Mutation do
77
77
  assert_equal IntroduceShipMutation, IntroduceShipMutation.result_class.mutation
78
78
  end
79
79
 
80
+ describe "aliased methods" do
81
+ describe "on an unreached mutation" do
82
+ it 'still ensures definitions' do
83
+ UnreachedMutation = GraphQL::Relay::Mutation.define do
84
+ name 'UnreachedMutation'
85
+ description 'A mutation type not directly used in the schema.'
86
+
87
+ input_field :input, types.String
88
+ return_field :return, types.String
89
+ end
90
+
91
+ assert UnreachedMutation.input_fields['input']
92
+ assert UnreachedMutation.return_fields['return']
93
+ end
94
+ end
95
+ end
96
+
80
97
  describe "providing a return type" do
81
98
  let(:custom_return_type) {
82
99
  GraphQL::ObjectType.define do
@@ -143,4 +160,29 @@ describe GraphQL::Relay::Mutation do
143
160
  assert_equal "String", input.arguments['stringDefault'].default_value
144
161
  end
145
162
  end
163
+
164
+ describe "handling errors" do
165
+ it "supports returning an error in resolve" do
166
+ result = star_wars_query(query_string, "clientMutationId" => "5678", "shipName" => "Millennium Falcon")
167
+
168
+ expected = {
169
+ "data" => {
170
+ "introduceShip" => {
171
+ "clientMutationId" => "5678",
172
+ "shipEdge" => nil,
173
+ "faction" => nil,
174
+ }
175
+ },
176
+ "errors" => [
177
+ {
178
+ "message" => "Sorry, Millennium Falcon ship is reserved",
179
+ "locations" => [ { "line" => 3 , "column" => 7}],
180
+ "path" => ["introduceShip"]
181
+ }
182
+ ]
183
+ }
184
+
185
+ assert_equal(expected, result)
186
+ end
187
+ end
146
188
  end
@@ -217,6 +217,7 @@ type Query {
217
217
  end
218
218
 
219
219
  def after_query(query)
220
+ @counts << :end
220
221
  end
221
222
  end
222
223
 
@@ -228,7 +229,7 @@ type Query {
228
229
  name "Query"
229
230
  field :int, types.Int do
230
231
  argument :value, types.Int
231
- resolve -> (obj, args, ctx) { args[:value] }
232
+ resolve -> (obj, args, ctx) { args[:value] == 13 ? raise("13 is unlucky") : args[:value] }
232
233
  end
233
234
  end
234
235
  }
@@ -250,7 +251,24 @@ type Query {
250
251
  it "can wrap query execution" do
251
252
  schema.execute("query getInt($val: Int = 5){ int(value: $val) } ")
252
253
  schema.execute("query getInt($val: Int = 5, $val2: Int = 3){ int(value: $val) int2: int(value: $val2) } ")
253
- assert_equal [1, 2], variable_counter.counts
254
+ assert_equal [1, :end, 2, :end], variable_counter.counts
255
+ end
256
+
257
+ it "runs even when a runtime error occurs" do
258
+ schema.execute("query getInt($val: Int = 5){ int(value: $val) } ")
259
+ assert_raises(RuntimeError) {
260
+ schema.execute("query getInt($val: Int = 13){ int(value: $val) } ")
261
+ }
262
+ assert_equal [1, :end, 1, :end], variable_counter.counts
263
+ end
264
+
265
+ it "can be applied after the fact" do
266
+ res = schema.execute("query { int(value: 2) } ")
267
+ assert_equal 6, res["data"]["int"]
268
+
269
+ schema.instrument(:field, MultiplyInstrumenter.new(4))
270
+ res = schema.execute("query { int(value: 2) } ")
271
+ assert_equal 24, res["data"]["int"]
254
272
  end
255
273
  end
256
274
  end
@@ -0,0 +1,180 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::StaticValidation::UniqueDirectivesPerLocation do
4
+ include StaticValidationHelpers
5
+
6
+ let(:schema) { GraphQL::Schema.from_definition("
7
+ type Query {
8
+ type: Type
9
+ }
10
+
11
+ type Type {
12
+ field: String
13
+ }
14
+
15
+ directive @A on FIELD
16
+ directive @B on FIELD
17
+ ") }
18
+
19
+ describe "query with no directives" do
20
+ let(:query_string) {"
21
+ {
22
+ type {
23
+ field
24
+ }
25
+ }
26
+ "}
27
+
28
+ it "passes rule" do
29
+ assert_equal [], errors
30
+ end
31
+ end
32
+
33
+ describe "query with unique directives in different locations" do
34
+ let(:query_string) {"
35
+ {
36
+ type @A {
37
+ field @B
38
+ }
39
+ }
40
+ "}
41
+
42
+ it "passes rule" do
43
+ assert_equal [], errors
44
+ end
45
+ end
46
+
47
+ describe "query with unique directives in same locations" do
48
+ let(:query_string) {"
49
+ {
50
+ type @A @B {
51
+ field @A @B
52
+ }
53
+ }
54
+ "}
55
+
56
+ it "passes rule" do
57
+ assert_equal [], errors
58
+ end
59
+ end
60
+
61
+ describe "query with same directives in different locations" do
62
+ let(:query_string) {"
63
+ {
64
+ type @A {
65
+ field @A
66
+ }
67
+ }
68
+ "}
69
+
70
+ it "passes rule" do
71
+ assert_equal [], errors
72
+ end
73
+ end
74
+
75
+ describe "query with same directives in similar locations" do
76
+ let(:query_string) {"
77
+ {
78
+ type {
79
+ field @A
80
+ field @A
81
+ }
82
+ }
83
+ "}
84
+
85
+ it "passes rule" do
86
+ assert_equal [], errors
87
+ end
88
+ end
89
+
90
+ describe "query with duplicate directives in one location" do
91
+ let(:query_string) {"
92
+ {
93
+ type {
94
+ field @A @A
95
+ }
96
+ }
97
+ "}
98
+
99
+ it "fails rule" do
100
+ assert_includes errors, {
101
+ "message" => 'The directive "A" can only be used once at this location.',
102
+ "locations" => [{ "line" => 4, "column" => 17 }, { "line" => 4, "column" => 20 }],
103
+ "fields" => ["query", "type", "field"],
104
+ }
105
+ end
106
+ end
107
+
108
+
109
+ describe "query with many duplicate directives in one location" do
110
+ let(:query_string) {"
111
+ {
112
+ type {
113
+ field @A @A @A
114
+ }
115
+ }
116
+ "}
117
+
118
+ it "fails rule" do
119
+ assert_includes errors, {
120
+ "message" => 'The directive "A" can only be used once at this location.',
121
+ "locations" => [{ "line" => 4, "column" => 17 }, { "line" => 4, "column" => 20 }],
122
+ "fields" => ["query", "type", "field"],
123
+ }
124
+
125
+ assert_includes errors, {
126
+ "message" => 'The directive "A" can only be used once at this location.',
127
+ "locations" => [{ "line" => 4, "column" => 17 }, { "line" => 4, "column" => 23 }],
128
+ "fields" => ["query", "type", "field"],
129
+ }
130
+ end
131
+ end
132
+
133
+ describe "query with different duplicate directives in one location" do
134
+ let(:query_string) {"
135
+ {
136
+ type {
137
+ field @A @B @A @B
138
+ }
139
+ }
140
+ "}
141
+
142
+ it "fails rule" do
143
+ assert_includes errors, {
144
+ "message" => 'The directive "A" can only be used once at this location.',
145
+ "locations" => [{ "line" => 4, "column" => 17 }, { "line" => 4, "column" => 23 }],
146
+ "fields" => ["query", "type", "field"],
147
+ }
148
+
149
+ assert_includes errors, {
150
+ "message" => 'The directive "B" can only be used once at this location.',
151
+ "locations" => [{ "line" => 4, "column" => 20 }, { "line" => 4, "column" => 26 }],
152
+ "fields" => ["query", "type", "field"],
153
+ }
154
+ end
155
+ end
156
+
157
+ describe "query with duplicate directives in many locations" do
158
+ let(:query_string) {"
159
+ {
160
+ type @A @A {
161
+ field @A @A
162
+ }
163
+ }
164
+ "}
165
+
166
+ it "fails rule" do
167
+ assert_includes errors, {
168
+ "message" => 'The directive "A" can only be used once at this location.',
169
+ "locations" => [{ "line" => 3, "column" => 14 }, { "line" => 3, "column" => 17 }],
170
+ "fields" => ["query", "type"],
171
+ }
172
+
173
+ assert_includes errors, {
174
+ "message" => 'The directive "A" can only be used once at this location.',
175
+ "locations" => [{ "line" => 4, "column" => 17 }, { "line" => 4, "column" => 20 }],
176
+ "fields" => ["query", "type", "field"],
177
+ }
178
+ end
179
+ end
180
+ end
@@ -157,6 +157,7 @@ IntroduceShipMutation = GraphQL::Relay::Mutation.define do
157
157
  # Here's the mutation operation:
158
158
  resolve ->(root_obj, inputs, ctx) {
159
159
  faction_id = inputs["factionId"]
160
+ return GraphQL::ExecutionError.new("Sorry, Millennium Falcon ship is reserved") if inputs["shipName"] == 'Millennium Falcon'
160
161
  ship = STAR_WARS_DATA.create_ship(inputs["shipName"], faction_id)
161
162
  faction = STAR_WARS_DATA["Faction"][faction_id]
162
163
  connection_class = GraphQL::Relay::BaseConnection.connection_for_nodes(faction.ships)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.4
4
+ version: 1.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-18 00:00:00.000000000 Z
11
+ date: 2016-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: codeclimate-test-reporter
@@ -407,6 +407,7 @@ files:
407
407
  - lib/graphql/static_validation/rules/mutation_root_exists.rb
408
408
  - lib/graphql/static_validation/rules/required_arguments_are_present.rb
409
409
  - lib/graphql/static_validation/rules/subscription_root_exists.rb
410
+ - lib/graphql/static_validation/rules/unique_directives_per_location.rb
410
411
  - lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb
411
412
  - lib/graphql/static_validation/rules/variable_usages_are_allowed.rb
412
413
  - lib/graphql/static_validation/rules/variables_are_input_types.rb
@@ -504,6 +505,7 @@ files:
504
505
  - spec/graphql/static_validation/rules/mutation_root_exists_spec.rb
505
506
  - spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb
506
507
  - spec/graphql/static_validation/rules/subscription_root_exists_spec.rb
508
+ - spec/graphql/static_validation/rules/unique_directives_per_location_spec.rb
507
509
  - spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb
508
510
  - spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb
509
511
  - spec/graphql/static_validation/rules/variables_are_input_types_spec.rb
@@ -628,6 +630,7 @@ test_files:
628
630
  - spec/graphql/static_validation/rules/mutation_root_exists_spec.rb
629
631
  - spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb
630
632
  - spec/graphql/static_validation/rules/subscription_root_exists_spec.rb
633
+ - spec/graphql/static_validation/rules/unique_directives_per_location_spec.rb
631
634
  - spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb
632
635
  - spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb
633
636
  - spec/graphql/static_validation/rules/variables_are_input_types_spec.rb