graphql 1.2.4 → 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
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