graphql 1.8.4 → 1.8.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/templates/graphql_controller.erb +10 -0
  3. data/lib/graphql/compatibility/schema_parser_specification.rb +398 -0
  4. data/lib/graphql/execution/execute.rb +24 -18
  5. data/lib/graphql/execution/lazy.rb +14 -1
  6. data/lib/graphql/introspection/type_type.rb +1 -1
  7. data/lib/graphql/language/lexer.rb +68 -42
  8. data/lib/graphql/language/lexer.rl +2 -0
  9. data/lib/graphql/language/nodes.rb +98 -0
  10. data/lib/graphql/language/parser.rb +1050 -770
  11. data/lib/graphql/language/parser.y +50 -2
  12. data/lib/graphql/object_type.rb +4 -0
  13. data/lib/graphql/query.rb +3 -1
  14. data/lib/graphql/query/variables.rb +1 -1
  15. data/lib/graphql/schema.rb +10 -2
  16. data/lib/graphql/schema/input_object.rb +1 -1
  17. data/lib/graphql/schema/interface.rb +6 -0
  18. data/lib/graphql/schema/member/has_arguments.rb +1 -0
  19. data/lib/graphql/schema/member/instrumentation.rb +21 -16
  20. data/lib/graphql/schema/mutation.rb +1 -1
  21. data/lib/graphql/schema/object.rb +7 -2
  22. data/lib/graphql/schema/possible_types.rb +1 -1
  23. data/lib/graphql/schema/resolver.rb +210 -1
  24. data/lib/graphql/schema/validation.rb +1 -1
  25. data/lib/graphql/static_validation/message.rb +5 -1
  26. data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +9 -0
  27. data/lib/graphql/type_kinds.rb +9 -6
  28. data/lib/graphql/types.rb +1 -0
  29. data/lib/graphql/types/iso_8601_date_time.rb +1 -5
  30. data/lib/graphql/unauthorized_error.rb +7 -2
  31. data/lib/graphql/version.rb +1 -1
  32. data/spec/generators/graphql/install_generator_spec.rb +12 -2
  33. data/spec/graphql/authorization_spec.rb +80 -1
  34. data/spec/graphql/directive_spec.rb +42 -0
  35. data/spec/graphql/execution_error_spec.rb +1 -1
  36. data/spec/graphql/query_spec.rb +14 -0
  37. data/spec/graphql/schema/interface_spec.rb +14 -0
  38. data/spec/graphql/schema/object_spec.rb +41 -1
  39. data/spec/graphql/schema/relay_classic_mutation_spec.rb +47 -0
  40. data/spec/graphql/schema/resolver_spec.rb +182 -8
  41. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +6 -5
  42. data/spec/graphql/static_validation/rules/no_definitions_are_present_spec.rb +34 -0
  43. data/spec/graphql/static_validation/validator_spec.rb +15 -0
  44. data/spec/support/jazz.rb +36 -1
  45. metadata +2 -2
@@ -51,4 +51,51 @@ describe GraphQL::Schema::RelayClassicMutation do
51
51
  assert_equal "Sitar", res["data"]["addSitar"]["instrument"]["name"]
52
52
  end
53
53
  end
54
+
55
+ describe "loading application objects" do
56
+ let(:query_str) {
57
+ <<-GRAPHQL
58
+ mutation($id: ID!, $newName: String!) {
59
+ renameEnsemble(input: {ensembleId: $id, newName: $newName}) {
60
+ ensemble {
61
+ name
62
+ }
63
+ }
64
+ }
65
+ GRAPHQL
66
+ }
67
+
68
+ it "loads arguments as objects of the given type" do
69
+ res = Jazz::Schema.execute(query_str, variables: { id: "Ensemble/Robert Glasper Experiment", newName: "August Greene"})
70
+ assert_equal "August Greene", res["data"]["renameEnsemble"]["ensemble"]["name"]
71
+ end
72
+
73
+ it "returns an error instead when the ID resolves to nil" do
74
+ res = Jazz::Schema.execute(query_str, variables: {
75
+ id: "Ensemble/Nonexistant Name",
76
+ newName: "August Greene"
77
+ })
78
+ assert_nil res["data"].fetch("renameEnsemble")
79
+ assert_equal ['No object found for `ensembleId: "Ensemble/Nonexistant Name"`'], res["errors"].map { |e| e["message"] }
80
+ end
81
+
82
+ it "returns an error instead when the ID resolves to an object of the wrong type" do
83
+ res = Jazz::Schema.execute(query_str, variables: {
84
+ id: "Instrument/Organ",
85
+ newName: "August Greene"
86
+ })
87
+ assert_nil res["data"].fetch("renameEnsemble")
88
+ assert_equal ["No object found for `ensembleId: \"Instrument/Organ\"`"], res["errors"].map { |e| e["message"] }
89
+ end
90
+
91
+ it "raises an authorization error when the type's auth fails" do
92
+ res = Jazz::Schema.execute(query_str, variables: {
93
+ id: "Ensemble/Spinal Tap",
94
+ newName: "August Greene"
95
+ })
96
+ assert_nil res["data"].fetch("renameEnsemble")
97
+ # Failed silently
98
+ refute res.key?("errors")
99
+ end
100
+ end
54
101
  end
@@ -3,6 +3,16 @@ require "spec_helper"
3
3
 
4
4
  describe GraphQL::Schema::Resolver do
5
5
  module ResolverTest
6
+ class LazyBlock
7
+ def initialize
8
+ @get_value = Proc.new
9
+ end
10
+
11
+ def value
12
+ @get_value.call
13
+ end
14
+ end
15
+
6
16
  class BaseResolver < GraphQL::Schema::Resolver
7
17
  end
8
18
 
@@ -64,6 +74,90 @@ describe GraphQL::Schema::Resolver do
64
74
  class Resolver8 < Resolver7
65
75
  end
66
76
 
77
+ class PrepResolver1 < BaseResolver
78
+ argument :int, Integer, required: true
79
+
80
+ def load_int(i)
81
+ i * 10
82
+ end
83
+
84
+ type Integer, null: false
85
+
86
+ def resolve(int:)
87
+ int
88
+ end
89
+
90
+ private
91
+
92
+ def check_for_magic_number(int)
93
+ if int == 13
94
+ raise GraphQL::ExecutionError, "13 is unlucky!"
95
+ elsif int > 99
96
+ raise GraphQL::UnauthorizedError, "Top secret big number: #{int}"
97
+ else
98
+ int
99
+ end
100
+ end
101
+ end
102
+
103
+ class PrepResolver2 < PrepResolver1
104
+ def load_int(i)
105
+ LazyBlock.new {
106
+ super - 35
107
+ }
108
+ end
109
+ end
110
+
111
+ class PrepResolver3 < PrepResolver1
112
+ type Integer, null: true
113
+
114
+ def load_int(i)
115
+ check_for_magic_number(i)
116
+ end
117
+ end
118
+
119
+ class PrepResolver4 < PrepResolver3
120
+ def load_int(i)
121
+ LazyBlock.new {
122
+ super
123
+ }
124
+ end
125
+ end
126
+
127
+ class PrepResolver5 < PrepResolver1
128
+ type Integer, null: true
129
+
130
+ def before_prepare(int:)
131
+ check_for_magic_number(int)
132
+ end
133
+ end
134
+
135
+ class PrepResolver6 < PrepResolver5
136
+ def before_prepare(**args)
137
+ LazyBlock.new {
138
+ super
139
+ }
140
+ end
141
+ end
142
+
143
+ class PrepResolver7 < PrepResolver1
144
+ type Integer, null: true
145
+
146
+ def load_int(int)
147
+ int
148
+ end
149
+
150
+ def validate_int(int)
151
+ check_for_magic_number(int)
152
+ end
153
+ end
154
+
155
+ class PrepResolver8 < PrepResolver7
156
+ def validate_int(int)
157
+ LazyBlock.new { super }
158
+ end
159
+ end
160
+
67
161
  class Query < GraphQL::Schema::Object
68
162
  class CustomField < GraphQL::Schema::Field
69
163
  def resolve_field(*args)
@@ -86,47 +180,61 @@ describe GraphQL::Schema::Resolver do
86
180
  field :resolver_6, resolver: Resolver6
87
181
  field :resolver_7, resolver: Resolver7
88
182
  field :resolver_8, resolver: Resolver8
183
+
184
+ field :prep_resolver_1, resolver: PrepResolver1
185
+ field :prep_resolver_2, resolver: PrepResolver2
186
+ field :prep_resolver_3, resolver: PrepResolver3
187
+ field :prep_resolver_4, resolver: PrepResolver4
188
+ field :prep_resolver_5, resolver: PrepResolver5
189
+ field :prep_resolver_6, resolver: PrepResolver6
190
+ field :prep_resolver_7, resolver: PrepResolver7
191
+ field :prep_resolver_8, resolver: PrepResolver8
89
192
  end
90
193
 
91
194
  class Schema < GraphQL::Schema
92
195
  query(Query)
196
+ lazy_resolve LazyBlock, :value
93
197
  end
94
198
  end
95
199
 
200
+ def exec_query(*args)
201
+ ResolverTest::Schema.execute(*args)
202
+ end
203
+
96
204
  it "gets initialized for each resolution" do
97
205
  # State isn't shared between calls:
98
- res = ResolverTest::Schema.execute " { r1: resolver1(value: 1) r2: resolver1 }"
206
+ res = exec_query " { r1: resolver1(value: 1) r2: resolver1 }"
99
207
  assert_equal [100, 1], res["data"]["r1"]
100
208
  assert_equal [100, nil], res["data"]["r2"]
101
209
  end
102
210
 
103
211
  it "inherits type and arguments" do
104
- res = ResolverTest::Schema.execute " { r1: resolver2(value: 1, extraValue: 2) r2: resolver2(extraValue: 3) }"
212
+ res = exec_query " { r1: resolver2(value: 1, extraValue: 2) r2: resolver2(extraValue: 3) }"
105
213
  assert_equal [100, 1, 2], res["data"]["r1"]
106
214
  assert_equal [100, nil, 3], res["data"]["r2"]
107
215
  end
108
216
 
109
217
  it "uses the object's field_class" do
110
- res = ResolverTest::Schema.execute " { r1: resolver3(value: 1) r2: resolver3 }"
218
+ res = exec_query " { r1: resolver3(value: 1) r2: resolver3 }"
111
219
  assert_equal [100, 1, -1], res["data"]["r1"]
112
220
  assert_equal [100, nil, -1], res["data"]["r2"]
113
221
  end
114
222
 
115
223
  describe "resolve method" do
116
224
  it "has access to the application object" do
117
- res = ResolverTest::Schema.execute " { resolver4 } ", root_value: OpenStruct.new(value: 4)
225
+ res = exec_query " { resolver4 } ", root_value: OpenStruct.new(value: 4)
118
226
  assert_equal 13, res["data"]["resolver4"]
119
227
  end
120
228
 
121
229
  it "gets extras" do
122
- res = ResolverTest::Schema.execute " { resolver4 } ", root_value: OpenStruct.new(value: 0)
230
+ res = exec_query " { resolver4 } ", root_value: OpenStruct.new(value: 0)
123
231
  assert_equal 9, res["data"]["resolver4"]
124
232
  end
125
233
  end
126
234
 
127
235
  describe "extras" do
128
236
  it "is inherited" do
129
- res = ResolverTest::Schema.execute " { resolver4 resolver5 } ", root_value: OpenStruct.new(value: 0)
237
+ res = exec_query " { resolver4 resolver5 } ", root_value: OpenStruct.new(value: 0)
130
238
  assert_equal 9, res["data"]["resolver4"]
131
239
  assert_equal 9, res["data"]["resolver5"]
132
240
  end
@@ -134,12 +242,12 @@ describe GraphQL::Schema::Resolver do
134
242
 
135
243
  describe "complexity" do
136
244
  it "has default values" do
137
- res = ResolverTest::Schema.execute " { resolver6 } ", root_value: OpenStruct.new(value: 0)
245
+ res = exec_query " { resolver6 } ", root_value: OpenStruct.new(value: 0)
138
246
  assert_equal 1, res["data"]["resolver6"]
139
247
  end
140
248
 
141
249
  it "is inherited" do
142
- res = ResolverTest::Schema.execute " { resolver7 resolver8 } ", root_value: OpenStruct.new(value: 0)
250
+ res = exec_query " { resolver7 resolver8 } ", root_value: OpenStruct.new(value: 0)
143
251
  assert_equal 2, res["data"]["resolver7"]
144
252
  assert_equal 2, res["data"]["resolver8"]
145
253
  end
@@ -159,4 +267,70 @@ describe GraphQL::Schema::Resolver do
159
267
  assert ResolverTest::Schema.find("Query.resolver3Again")
160
268
  end
161
269
  end
270
+
271
+ describe "preparing inputs" do
272
+ # Add assertions for a given field, assuming the behavior of `check_for_magic_number`
273
+ def add_error_assertions(field_name, description)
274
+ res = exec_query("{ int: #{field_name}(int: 13) }")
275
+ assert_nil res["data"].fetch("int"), "#{description}: no result for execution error"
276
+ assert_equal ["13 is unlucky!"], res["errors"].map { |e| e["message"] }, "#{description}: top-level error is added"
277
+
278
+ res = exec_query("{ int: #{field_name}(int: 200) }")
279
+ assert_nil res["data"].fetch("int"), "#{description}: No result for authorization error"
280
+ refute res.key?("errors"), "#{description}: silent auth failure (no top-level error)"
281
+ end
282
+
283
+ describe "before_prepare" do
284
+ it "can raise errors" do
285
+ res = exec_query("{ int: prepResolver5(int: 5) }")
286
+ assert_equal 50, res["data"]["int"]
287
+ add_error_assertions("prepResolver5", "before_prepare")
288
+ end
289
+
290
+ it "can raise errors in lazy sync" do
291
+ res = exec_query("{ int: prepResolver6(int: 5) }")
292
+ assert_equal 50, res["data"]["int"]
293
+ add_error_assertions("prepResolver6", "lazy before_prepare")
294
+ end
295
+ end
296
+
297
+ describe "loading arguments" do
298
+ it "calls load methods and injects the return value" do
299
+ res = exec_query("{ prepResolver1(int: 5) }")
300
+ assert_equal 50, res["data"]["prepResolver1"], "The load multiplier was called"
301
+ end
302
+
303
+ it "supports lazy values" do
304
+ res = exec_query("{ prepResolver2(int: 5) }")
305
+ assert_equal 15, res["data"]["prepResolver2"], "The load multiplier was called"
306
+ end
307
+
308
+ it "supports raising GraphQL::UnauthorizedError and GraphQL::ExecutionError" do
309
+ res = exec_query("{ prepResolver3(int: 5) }")
310
+ assert_equal 5, res["data"]["prepResolver3"]
311
+ add_error_assertions("prepResolver3", "load_ hook")
312
+ end
313
+
314
+ it "supports raising errors from promises" do
315
+ res = exec_query("{ prepResolver4(int: 5) }")
316
+ assert_equal 5, res["data"]["prepResolver4"]
317
+ add_error_assertions("prepResolver4", "lazy load_ hook")
318
+ end
319
+ end
320
+
321
+ describe "validating arguments" do
322
+ test_cases = {
323
+ "eager" => "prepResolver7",
324
+ "lazy" => "prepResolver8",
325
+ }
326
+
327
+ test_cases.each do |mode, field_name|
328
+ it "supports raising #{mode} errors" do
329
+ res = exec_query("{ validatedInt: #{field_name}(int: 5) }")
330
+ assert_equal 5, res["data"]["validatedInt"]
331
+ add_error_assertions(field_name, "#{mode} validation")
332
+ end
333
+ end
334
+ end
335
+ end
162
336
  end
@@ -222,7 +222,8 @@ describe GraphQL::StaticValidation::ArgumentLiteralsAreCompatible do
222
222
 
223
223
  describe "custom error messages" do
224
224
  let(:schema) {
225
- TimeType = GraphQL::ScalarType.define do
225
+
226
+ CoerceTestTimeType = GraphQL::ScalarType.define do
226
227
  name "Time"
227
228
  description "Time since epoch in seconds"
228
229
 
@@ -237,19 +238,19 @@ describe GraphQL::StaticValidation::ArgumentLiteralsAreCompatible do
237
238
  coerce_result ->(value, ctx) { value.to_f }
238
239
  end
239
240
 
240
- QueryType = GraphQL::ObjectType.define do
241
+ CoerceTestQueryType = GraphQL::ObjectType.define do
241
242
  name "Query"
242
243
  description "The query root of this schema"
243
244
 
244
245
  field :time do
245
- type TimeType
246
- argument :value, !TimeType
246
+ type CoerceTestTimeType
247
+ argument :value, !CoerceTestTimeType
247
248
  resolve ->(obj, args, ctx) { args[:value] }
248
249
  end
249
250
  end
250
251
 
251
252
  GraphQL::Schema.define do
252
- query QueryType
253
+ query CoerceTestQueryType
253
254
  end
254
255
  }
255
256
 
@@ -25,4 +25,38 @@ describe GraphQL::StaticValidation::NoDefinitionsArePresent do
25
25
  assert_equal [{"line"=>5, "column"=>7}, {"line"=>9, "column"=>7}], err["locations"]
26
26
  end
27
27
  end
28
+
29
+ describe "when schema extensions are present in the query" do
30
+ let(:query_string) {
31
+ <<-GRAPHQL
32
+ {
33
+ cheese(id: 1) { flavor }
34
+ }
35
+
36
+ extend schema {
37
+ subscription: Query
38
+ }
39
+
40
+ extend scalar TracingScalar @deprecated
41
+ extend type Dairy @deprecated
42
+ extend interface Edible @deprecated
43
+ extend union Beverage @deprecated
44
+ extend enum DairyAnimal @deprecated
45
+ extend input ResourceOrderType @deprecated
46
+ GRAPHQL
47
+ }
48
+
49
+ it "adds an error" do
50
+ assert_equal 1, errors.length
51
+ err = errors[0]
52
+ assert_equal "Query cannot contain schema definitions", err["message"]
53
+ assert_equal [{"line"=>5, "column"=>7},
54
+ {"line"=>9, "column"=>7},
55
+ {"line"=>10, "column"=>7},
56
+ {"line"=>11, "column"=>7},
57
+ {"line"=>12, "column"=>7},
58
+ {"line"=>13, "column"=>7},
59
+ {"line"=>14, "column"=>7}], err["locations"]
60
+ end
61
+ end
28
62
  end
@@ -25,6 +25,21 @@ describe GraphQL::StaticValidation::Validator do
25
25
  end
26
26
  end
27
27
 
28
+ describe "error format" do
29
+ let(:query_string) { "{ cheese(id: $undefinedVar) { source } }" }
30
+ let(:document) { GraphQL.parse_with_racc(query_string, filename: "not_a_real.graphql") }
31
+ let(:query) { GraphQL::Query.new(Dummy::Schema, nil, document: document) }
32
+
33
+ it "includes message, locations, and fields keys" do
34
+ expected_errors = [{
35
+ "message" => "Variable $undefinedVar is used by but not declared",
36
+ "locations" => [{"line" => 1, "column" => 14, "filename" => "not_a_real.graphql"}],
37
+ "fields" => ["query", "cheese", "id"]
38
+ }]
39
+ assert_equal expected_errors, errors
40
+ end
41
+ end
42
+
28
43
  describe "validation order" do
29
44
  let(:document) { GraphQL.parse(query_string)}
30
45
 
@@ -32,6 +32,7 @@ module Jazz
32
32
  "Ensemble" => [
33
33
  Models::Ensemble.new("Bela Fleck and the Flecktones"),
34
34
  Models::Ensemble.new("Robert Glasper Experiment"),
35
+ Models::Ensemble.new("Spinal Tap"),
35
36
  ],
36
37
  "Musician" => [
37
38
  Models::Musician.new("Herbie Hancock", Models::Key.from_notation("B♭")),
@@ -190,6 +191,12 @@ module Jazz
190
191
  def overridden_name
191
192
  @object.name.sub("Robert Glasper", "ROBERT GLASPER")
192
193
  end
194
+
195
+ def self.authorized?(object, context)
196
+ # Spinal Tap is top-secret, don't show it to anyone.
197
+ obj_name = object.is_a?(Hash) ? object[:name] : object.name
198
+ obj_name != "Spinal Tap"
199
+ end
193
200
  end
194
201
 
195
202
  class Family < BaseEnum
@@ -344,7 +351,8 @@ module Jazz
344
351
  end
345
352
 
346
353
  def ensembles
347
- Models.data["Ensemble"]
354
+ # Filter out the unauthorized one to avoid an error later
355
+ Models.data["Ensemble"].select { |e| e.name != "Spinal Tap" }
348
356
  end
349
357
 
350
358
  def find(id:)
@@ -419,6 +427,12 @@ module Jazz
419
427
  def hash_by_sym
420
428
  { falsey: false }
421
429
  end
430
+
431
+ field :named_entities, [NamedEntity, null: true], null: false
432
+
433
+ def named_entities
434
+ [Models.data["Ensemble"].first, nil]
435
+ end
422
436
  end
423
437
 
424
438
  class EnsembleInput < GraphQL::Schema::InputObject
@@ -458,6 +472,22 @@ module Jazz
458
472
  end
459
473
  end
460
474
 
475
+ class RenameEnsemble < GraphQL::Schema::RelayClassicMutation
476
+ argument :ensemble_id, ID, required: true, loads: Ensemble
477
+ argument :new_name, String, required: true
478
+
479
+ field :ensemble, Ensemble, null: false
480
+
481
+ def resolve(ensemble:, new_name:)
482
+ # doesn't actually update the "database"
483
+ dup_ensemble = ensemble.dup
484
+ dup_ensemble.name = new_name
485
+ {
486
+ ensemble: dup_ensemble
487
+ }
488
+ end
489
+ end
490
+
461
491
  class Mutation < BaseObject
462
492
  field :add_ensemble, Ensemble, null: false do
463
493
  argument :input, EnsembleInput, required: true
@@ -465,6 +495,7 @@ module Jazz
465
495
 
466
496
  field :add_instrument, mutation: AddInstrument
467
497
  field :add_sitar, mutation: AddSitar
498
+ field :rename_ensemble, mutation: RenameEnsemble
468
499
 
469
500
  def add_ensemble(input:)
470
501
  ens = Models::Ensemble.new(input.name)
@@ -553,5 +584,9 @@ module Jazz
553
584
  class_name = obj.class.name.split("::").last
554
585
  ctx.schema.types[class_name] || raise("No type for #{obj.inspect}")
555
586
  end
587
+
588
+ def self.object_from_id(id, ctx)
589
+ GloballyIdentifiableType.find(id)
590
+ end
556
591
  end
557
592
  end