graphql 0.13.0 → 0.14.0

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/define/assign_argument.rb +3 -5
  3. data/lib/graphql/field.rb +1 -3
  4. data/lib/graphql/input_object_type.rb +4 -1
  5. data/lib/graphql/language.rb +1 -0
  6. data/lib/graphql/language/generation.rb +94 -0
  7. data/lib/graphql/language/lexer.rb +229 -201
  8. data/lib/graphql/language/lexer.rl +24 -7
  9. data/lib/graphql/language/nodes.rb +4 -0
  10. data/lib/graphql/language/parser.rb +195 -187
  11. data/lib/graphql/language/parser.y +11 -4
  12. data/lib/graphql/object_type.rb +21 -0
  13. data/lib/graphql/query.rb +8 -3
  14. data/lib/graphql/query/serial_execution/operation_resolution.rb +2 -2
  15. data/lib/graphql/query/serial_execution/value_resolution.rb +5 -0
  16. data/lib/graphql/schema.rb +11 -19
  17. data/lib/graphql/schema/invalid_type_error.rb +6 -0
  18. data/lib/graphql/schema/reduce_types.rb +68 -0
  19. data/lib/graphql/schema/type_expression.rb +6 -16
  20. data/lib/graphql/schema/type_map.rb +1 -5
  21. data/lib/graphql/schema/validation.rb +164 -0
  22. data/lib/graphql/version.rb +1 -1
  23. data/readme.md +3 -4
  24. data/spec/graphql/argument_spec.rb +20 -0
  25. data/spec/graphql/field_spec.rb +16 -0
  26. data/spec/graphql/introspection/schema_type_spec.rb +1 -0
  27. data/spec/graphql/language/generation_spec.rb +42 -0
  28. data/spec/graphql/language/parser_spec.rb +136 -26
  29. data/spec/graphql/query/serial_execution/value_resolution_spec.rb +23 -0
  30. data/spec/graphql/query_spec.rb +51 -0
  31. data/spec/graphql/schema/{type_reducer_spec.rb → reduce_types_spec.rb} +16 -14
  32. data/spec/graphql/schema/type_expression_spec.rb +4 -4
  33. data/spec/graphql/schema/validation_spec.rb +219 -0
  34. data/spec/support/dairy_app.rb +3 -0
  35. metadata +14 -13
  36. data/lib/graphql/schema/each_item_validator.rb +0 -21
  37. data/lib/graphql/schema/field_validator.rb +0 -17
  38. data/lib/graphql/schema/implementation_validator.rb +0 -31
  39. data/lib/graphql/schema/type_reducer.rb +0 -79
  40. data/lib/graphql/schema/type_validator.rb +0 -58
  41. data/spec/graphql/schema/field_validator_spec.rb +0 -21
  42. data/spec/graphql/schema/type_validator_spec.rb +0 -81
@@ -1,3 +1,3 @@
1
1
  module GraphQL
2
- VERSION = "0.13.0"
2
+ VERSION = "0.14.0"
3
3
  end
data/readme.md CHANGED
@@ -119,12 +119,11 @@ If you're building a backend for [Relay](http://facebook.github.io/relay/), you'
119
119
 
120
120
  ## To Do
121
121
 
122
- - Type lookup should be by type name (to support reloaded constants in Rails code)
122
+ - __1.0 items:__
123
+ - Support type name for field types
124
+ - Revisit error handling & `debug:` option
123
125
  - Add a complexity validator (reject queries if they're too big)
124
126
  - Add docs for shared behaviors & DRY code
125
- - Revamp the fixture Schema to be more useful (better names, more extensible)
126
- - Fix when a field's type is left out `field :name, "This is the name field"`
127
- - Revisit error handling & `debug:` option
128
127
  - __Subscriptions__
129
128
  - This is a good chance to make an `Operation` abstraction of which `query`, `mutation` and `subscription` are members
130
129
  - For a subscription, `graphql` would send an outbound message to the system (allow the host application to manage its own subscriptions via Pusher, ActionCable, whatever)
@@ -0,0 +1,20 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Argument do
4
+ it "is validated at schema build-time" do
5
+ query_type = GraphQL::ObjectType.define do
6
+ name "Query"
7
+ field :invalid, types.Boolean do
8
+ argument :invalid, types.Float, default_value: ["123"]
9
+ end
10
+ end
11
+
12
+ err = assert_raises {
13
+ schema = GraphQL::Schema.new(query: query_type)
14
+ schema.types
15
+ }
16
+
17
+ expected_error = %|Query is invalid: field "invalid" argument "invalid" default value ["123"] is not valid for type Float|
18
+ assert_equal expected_error, err.message
19
+ end
20
+ end
@@ -72,5 +72,21 @@ describe GraphQL::Field do
72
72
  assert_raises { field.name = "somethingelse" }
73
73
  assert_equal "something", field.name
74
74
  end
75
+
76
+ it "must be a string" do
77
+ dummy_query = GraphQL::ObjectType.define do
78
+ name "QueryType"
79
+ end
80
+
81
+ invalid_field = GraphQL::Field.new
82
+ invalid_field.type = dummy_query
83
+ invalid_field.name = :symbol_name
84
+
85
+ dummy_query.fields["symbol_name"] = invalid_field
86
+ dummy_schema = GraphQL::Schema.new(query: dummy_query)
87
+
88
+ err = assert_raises(GraphQL::Schema::InvalidTypeError) { dummy_schema.types }
89
+ assert_equal "QueryType is invalid: field :symbol_name name must return String, not Symbol (:symbol_name)", err.message
90
+ end
75
91
  end
76
92
  end
@@ -27,6 +27,7 @@ describe GraphQL::Introspection::SchemaType do
27
27
  {"name"=>"fromSource"},
28
28
  {"name"=>"maybeNull"},
29
29
  {"name"=>"milk"},
30
+ {"name"=>"root"},
30
31
  {"name"=>"searchDairy"},
31
32
  ]
32
33
  },
@@ -0,0 +1,42 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Language::Generation do
4
+ let(:document) { GraphQL::Language::Parser.parse(query_string) }
5
+ let(:query_string) {%|
6
+ query getStuff($someVar: Int = 1, $anotherVar: [String!]) @skip(if: false) {
7
+ myField: someField(someArg: $someVar, ok: 1.4) @skip(if: $anotherVar) @thing(or: "Whatever")
8
+ anotherField(someArg: [1, 2, 3]) {
9
+ nestedField
10
+ ... moreNestedFields @skip(if: true)
11
+ }
12
+ ... on OtherType @include(unless: false) {
13
+ field(arg: [{key: "value", anotherKey: 0.9, anotherAnotherKey: WHATEVER}])
14
+ anotherField
15
+ }
16
+ ... {
17
+ id
18
+ }
19
+ }
20
+ fragment moreNestedFields on NestedType @or(something: "ok") {
21
+ anotherNestedField
22
+ }
23
+ |}
24
+
25
+ describe ".generate" do
26
+ it "generates query string" do
27
+ assert_equal query_string.gsub(/^ /, "").strip, document.to_query_string
28
+ end
29
+
30
+ describe "inputs" do
31
+ let(:query_string) {%|
32
+ query {
33
+ field(int: 3, float: 4.7e-24, bool: false, string: "☀︎🏆\\n escaped \\" unicode ¶ /", enum: ENUM_NAME, array: [7, 8, 9], object: {a: [1, 2, 3], b: {c: "4"}}, unicode_bom: "\xef\xbb\xbfquery")
34
+ }
35
+ |}
36
+
37
+ it "generate" do
38
+ assert_equal query_string.gsub(/^ /, "").strip, document.to_query_string
39
+ end
40
+ end
41
+ end
42
+ end
@@ -27,32 +27,6 @@ describe GraphQL::Language::Parser do
27
27
  |}
28
28
 
29
29
  describe ".parse" do
30
- let(:_query_string) { '
31
- query getStuff($fragment: Int!, $false: String = "h\"😸i") @skip(ok: 1) {
32
- myField(
33
- arg1: 4.5,
34
- arg2: -3,
35
- arg3: "hello ☀︎ \uD83C\uDF40",
36
- arg4: 4.5e-12,
37
- arg5: true
38
- arg6: $false
39
- arg7: [true, false],
40
- arg8: {key: "val", ok: true, whatever: $fragment}
41
- arg9: ENUM_VALUE
42
- ) {
43
- aliasName: childField @skip(on: true)
44
- ... description
45
- },
46
- # Comment!
47
- #
48
- otherField
49
- }
50
-
51
- fragment thingStuff on Thing {
52
- whatever
53
- }
54
- '}
55
-
56
30
  it "parses queries" do
57
31
  assert document
58
32
  end
@@ -215,6 +189,7 @@ describe GraphQL::Language::Parser do
215
189
  enum: ENUM_NAME,
216
190
  array: [7, 8, 9]
217
191
  object: {a: [1,2,3], b: {c: "4"}}
192
+ unicode_bom: "\xef\xbb\xbfquery"
218
193
  )
219
194
  }
220
195
  |}
@@ -254,6 +229,11 @@ describe GraphQL::Language::Parser do
254
229
  assert_equal "c", obj.arguments[1].value.arguments[0].name
255
230
  assert_equal "4", obj.arguments[1].value.arguments[0].value
256
231
  end
232
+
233
+ it "parses unicode bom" do
234
+ obj = inputs[7].value
235
+ assert_equal %|\xef\xbb\xbfquery|, inputs[7].value
236
+ end
257
237
  end
258
238
  end
259
239
 
@@ -304,6 +284,136 @@ describe GraphQL::Language::Parser do
304
284
  err = assert_raises { GraphQL.parse("{ ") }
305
285
  assert_equal "Unexpected end of document", err.message
306
286
  end
287
+
288
+ it "rejects unsupported characters" do
289
+ e = assert_raises(GraphQL::ParseError) do
290
+ GraphQL.parse("{ field; }")
291
+ end
292
+
293
+ assert_includes(e.message, "Parse error on \";\"")
294
+ end
295
+
296
+ it "rejects control characters" do
297
+ e = assert_raises(GraphQL::ParseError) do
298
+ GraphQL.parse("{ \afield }")
299
+ end
300
+
301
+ assert_includes(e.message, "Parse error on \"\\a\"")
302
+ end
303
+
304
+ it "rejects partial BOM" do
305
+ e = assert_raises(GraphQL::ParseError) do
306
+ GraphQL.parse("{ \xeffield }")
307
+ end
308
+
309
+ assert_includes(e.message, "Parse error on \"\\xEF\"")
310
+ end
311
+
312
+ it "rejects vertical tabs" do
313
+ e = assert_raises(GraphQL::ParseError) do
314
+ GraphQL.parse("{ \vfield }")
315
+ end
316
+
317
+ assert_includes(e.message, "Parse error on \"\\v\"")
318
+ end
319
+
320
+ it "rejects form feed" do
321
+ e = assert_raises(GraphQL::ParseError) do
322
+ GraphQL.parse("{ \ffield }")
323
+ end
324
+
325
+ assert_includes(e.message, "Parse error on \"\\f\"")
326
+ end
327
+
328
+ it "rejects no break space" do
329
+ e = assert_raises(GraphQL::ParseError) do
330
+ GraphQL.parse("{ \xa0field }")
331
+ end
332
+
333
+ assert_includes(e.message, "Parse error on \"\\xA0\"")
334
+ end
335
+
336
+ it "rejects unterminated strings" do
337
+ e = assert_raises(GraphQL::ParseError) do
338
+ GraphQL.parse("\"")
339
+ end
340
+
341
+ assert_includes(e.message, "Parse error on \"\\\"\"")
342
+
343
+ e = assert_raises(GraphQL::ParseError) do
344
+ GraphQL.parse("\"\n\"")
345
+ end
346
+
347
+ assert_includes(e.message, "Parse error on \"\\n\"")
348
+ end
349
+
350
+ it "rejects bad escape sequence in strings" do
351
+ e = assert_raises(GraphQL::ParseError) do
352
+ GraphQL.parse("{ field(arg:\"\\x\") }")
353
+ end
354
+
355
+ assert_includes(e.message, "Parse error on bad Unicode escape sequence")
356
+ end
357
+
358
+ it "rejects incomplete escape sequence in strings" do
359
+ e = assert_raises(GraphQL::ParseError) do
360
+ GraphQL.parse("{ field(arg:\"\\u1\") }")
361
+ end
362
+
363
+ assert_includes(e.message, "bad Unicode escape sequence")
364
+ end
365
+
366
+ it "rejects unicode escape with bad chars" do
367
+ e = assert_raises(GraphQL::ParseError) do
368
+ GraphQL.parse("{ field(arg:\"\\u0XX1\") }")
369
+ end
370
+
371
+ assert_includes(e.message, "bad Unicode escape sequence")
372
+
373
+ e = assert_raises(GraphQL::ParseError) do
374
+ GraphQL.parse("{ field(arg:\"\\uXXXX\") }")
375
+ end
376
+
377
+ assert_includes(e.message, "bad Unicode escape sequence")
378
+
379
+
380
+ e = assert_raises(GraphQL::ParseError) do
381
+ GraphQL.parse("{ field(arg:\"\\uFXXX\") }")
382
+ end
383
+
384
+ assert_includes(e.message, "bad Unicode escape sequence")
385
+
386
+
387
+ e = assert_raises(GraphQL::ParseError) do
388
+ GraphQL.parse("{ field(arg:\"\\uXXXF\") }")
389
+ end
390
+
391
+ assert_includes(e.message, "bad Unicode escape sequence")
392
+ end
393
+
394
+ it "rejects fragments named 'on'" do
395
+ e = assert_raises(GraphQL::ParseError) do
396
+ GraphQL.parse("fragment on on on { on }")
397
+ end
398
+
399
+ assert_includes(e.message, "Parse error on \"on\"")
400
+ end
401
+
402
+ it "rejects fragment spread of 'on'" do
403
+ e = assert_raises(GraphQL::ParseError) do
404
+ GraphQL.parse("{ ...on }")
405
+ end
406
+
407
+ assert_includes(e.message, "Parse error on \"}\"")
408
+ end
409
+
410
+ it "rejects null value" do
411
+ e = assert_raises(GraphQL::ParseError) do
412
+ GraphQL.parse("{ fieldWithNullableStringInput(input: null) }")
413
+ end
414
+
415
+ assert_includes(e.message, "Parse error on \"null\"")
416
+ end
307
417
  end
308
418
 
309
419
 
@@ -13,12 +13,22 @@ describe GraphQL::Query::SerialExecution::ValueResolution do
13
13
  value("SATURDAY", value: 5)
14
14
  value("SUNDAY", value: 6)
15
15
  end
16
+
17
+ interface = GraphQL::InterfaceType.define do
18
+ name "SomeInterface"
19
+ field :someField, !types.Int
20
+ resolve_type ->(obj, ctx) { nil }
21
+ end
22
+
16
23
  GraphQL::ObjectType.define do
17
24
  name "Query"
18
25
  field :tomorrow, day_of_week_enum do
19
26
  argument :today, day_of_week_enum
20
27
  resolve ->(obj, args, ctx) { (args["today"] + 1) % 7 }
21
28
  end
29
+ field :misbehavedInterface, interface do
30
+ resolve ->(obj, args, ctx) { Object.new }
31
+ end
22
32
  end
23
33
  }
24
34
  let(:schema) { GraphQL::Schema.new(query: query_root) }
@@ -43,4 +53,17 @@ describe GraphQL::Query::SerialExecution::ValueResolution do
43
53
  assert_equal(expected, result)
44
54
  end
45
55
  end
56
+
57
+ describe "interface type resolution" do
58
+ let(:debug) { true }
59
+ let(:query_string) { %|
60
+ {
61
+ misbehavedInterface { someField }
62
+ }
63
+ |}
64
+
65
+ it "raises an error if it cannot resolve the type of an interface" do
66
+ assert_raises(GraphQL::ObjectType::UnresolvedTypeError) { result }
67
+ end
68
+ end
46
69
  end
@@ -33,6 +33,8 @@ describe GraphQL::Query do
33
33
  let(:max_depth) { nil }
34
34
  let(:query_variables) { {"cheeseId" => 2} }
35
35
  let(:schema) { DummySchema }
36
+ let(:document) { GraphQL.parse(query_string) }
37
+
36
38
  let(:query) { GraphQL::Query.new(
37
39
  schema,
38
40
  query_string,
@@ -42,6 +44,50 @@ describe GraphQL::Query do
42
44
  max_depth: max_depth,
43
45
  )}
44
46
  let(:result) { query.result }
47
+
48
+ describe "when passed no query string or document" do
49
+ it 'fails with an ArgumentError' do
50
+ -> {
51
+ GraphQL::Query.new(
52
+ schema,
53
+ variables: query_variables,
54
+ debug: debug,
55
+ operation_name: operation_name,
56
+ max_depth: max_depth,
57
+ )
58
+ }.must_raise ArgumentError
59
+ end
60
+ end
61
+
62
+ describe "when passed a document instance" do
63
+ let(:query) { GraphQL::Query.new(
64
+ schema,
65
+ document: document,
66
+ variables: query_variables,
67
+ debug: debug,
68
+ operation_name: operation_name,
69
+ max_depth: max_depth,
70
+ )}
71
+
72
+ it "runs the query using the already parsed document" do
73
+ expected = {"data"=> {
74
+ "brie" => { "flavor" => "Brie", "taste" => "Brie" },
75
+ "cheese" => {
76
+ "__typename" => "Cheese",
77
+ "id" => 2,
78
+ "flavor" => "Gouda",
79
+ "fatContent" => 0.3,
80
+ "cheeseKind" => "Gouda",
81
+ },
82
+ "fromSource" => [{ "id" => 1 }, {"id" => 2}],
83
+ "fromSheep"=>[{"id"=>3}],
84
+ "firstSheep" => { "__typename" => "Cheese", "flavor" => "Manchego" },
85
+ "favoriteEdible"=>{"__typename"=>"Milk", "fatContent"=>0.04},
86
+ }}
87
+ assert_equal(expected, result)
88
+ end
89
+ end
90
+
45
91
  describe '#result' do
46
92
  it "returns fields on objects" do
47
93
  expected = {"data"=> {
@@ -82,6 +128,11 @@ describe GraphQL::Query do
82
128
  end
83
129
  end
84
130
 
131
+ it "uses root_value as the object for the root type" do
132
+ result = GraphQL::Query.new(schema, '{ root }', root_value: "I am root").result
133
+ assert_equal 'I am root', result.fetch('data').fetch('root')
134
+ end
135
+
85
136
  it "exposes fragments" do
86
137
  assert_equal(GraphQL::Language::Nodes::FragmentDefinition, query.fragments["cheeseFields"].class)
87
138
  end
@@ -1,8 +1,11 @@
1
1
  require "spec_helper"
2
2
 
3
- describe GraphQL::Schema::TypeReducer do
3
+ describe GraphQL::Schema::ReduceTypes do
4
+ def reduce_types(types)
5
+ GraphQL::Schema::ReduceTypes.reduce(types)
6
+ end
7
+
4
8
  it "finds types from a single type and its fields" do
5
- reducer = GraphQL::Schema::TypeReducer.new(CheeseType, {})
6
9
  expected = {
7
10
  "Cheese" => CheeseType,
8
11
  "Float" => GraphQL::FLOAT_TYPE,
@@ -12,13 +15,14 @@ describe GraphQL::Schema::TypeReducer do
12
15
  "Edible" => EdibleInterface,
13
16
  "AnimalProduct" => AnimalProductInterface,
14
17
  }
15
- assert_equal(expected.keys, reducer.result.keys)
16
- assert_equal(expected, reducer.result)
18
+ result = reduce_types([CheeseType])
19
+ assert_equal(expected.keys, result.keys)
20
+ assert_equal(expected, result.to_h)
17
21
  end
18
22
 
19
23
  it "finds type from arguments" do
20
- reducer = GraphQL::Schema::TypeReducer.new(QueryType, {})
21
- assert_equal(DairyProductInputType, reducer.result["DairyProductInput"])
24
+ result = reduce_types([QueryType])
25
+ assert_equal(DairyProductInputType, result["DairyProductInput"])
22
26
  end
23
27
 
24
28
  it "finds types from nested InputObjectTypes" do
@@ -32,13 +36,13 @@ describe GraphQL::Schema::TypeReducer do
32
36
  input_field :child, type_child
33
37
  end
34
38
 
35
- reducer = GraphQL::Schema::TypeReducer.new(type_parent, {})
39
+ result = reduce_types([type_parent])
36
40
  expected = {
37
41
  "InputTypeParent" => type_parent,
38
42
  "InputTypeChild" => type_child,
39
43
  "String" => GraphQL::STRING_TYPE
40
44
  }
41
- assert_equal(expected, reducer.result)
45
+ assert_equal(expected, result.to_h)
42
46
  end
43
47
 
44
48
  describe "when a type is invalid" do
@@ -57,13 +61,11 @@ describe GraphQL::Schema::TypeReducer do
57
61
  }
58
62
 
59
63
  it "raises an InvalidTypeError when passed nil" do
60
- reducer = GraphQL::Schema::TypeReducer.new(invalid_type, {})
61
- assert_raises(GraphQL::Schema::InvalidTypeError) { reducer.result }
64
+ assert_raises(GraphQL::Schema::InvalidTypeError) { reduce_types([invalid_type]) }
62
65
  end
63
66
 
64
67
  it "raises an InvalidTypeError when passed an object that isnt a GraphQL::BaseType" do
65
- reducer = GraphQL::Schema::TypeReducer.new(another_invalid_type, {})
66
- assert_raises(GraphQL::Schema::InvalidTypeError) { reducer.result }
68
+ assert_raises(GraphQL::Schema::InvalidTypeError) { reduce_types([another_invalid_type]) }
67
69
  end
68
70
  end
69
71
 
@@ -80,14 +82,14 @@ describe GraphQL::Schema::TypeReducer do
80
82
  }
81
83
  it "raises an error" do
82
84
  assert_raises(RuntimeError) {
83
- GraphQL::Schema::TypeReducer.find_all([type_1, type_2])
85
+ reduce_types([type_1, type_2])
84
86
  }
85
87
  end
86
88
  end
87
89
 
88
90
  describe "when getting a type which doesnt exist" do
89
91
  it "raises an error" do
90
- type_map = GraphQL::Schema::TypeReducer.find_all([])
92
+ type_map = reduce_types([])
91
93
  assert_raises(RuntimeError) { type_map["SomeType"] }
92
94
  end
93
95
  end