graphql 1.6.8 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +5 -0
  3. data/lib/graphql/analysis/analyze_query.rb +21 -17
  4. data/lib/graphql/argument.rb +6 -2
  5. data/lib/graphql/backtrace.rb +50 -0
  6. data/lib/graphql/backtrace/inspect_result.rb +51 -0
  7. data/lib/graphql/backtrace/table.rb +120 -0
  8. data/lib/graphql/backtrace/traced_error.rb +55 -0
  9. data/lib/graphql/backtrace/tracer.rb +50 -0
  10. data/lib/graphql/enum_type.rb +1 -10
  11. data/lib/graphql/execution.rb +1 -2
  12. data/lib/graphql/execution/execute.rb +98 -89
  13. data/lib/graphql/execution/flatten.rb +40 -0
  14. data/lib/graphql/execution/lazy/resolve.rb +7 -7
  15. data/lib/graphql/execution/multiplex.rb +29 -29
  16. data/lib/graphql/field.rb +5 -1
  17. data/lib/graphql/internal_representation/node.rb +16 -0
  18. data/lib/graphql/invalid_name_error.rb +11 -0
  19. data/lib/graphql/language/parser.rb +11 -5
  20. data/lib/graphql/language/parser.y +11 -5
  21. data/lib/graphql/name_validator.rb +16 -0
  22. data/lib/graphql/object_type.rb +5 -0
  23. data/lib/graphql/query.rb +28 -7
  24. data/lib/graphql/query/context.rb +155 -52
  25. data/lib/graphql/query/literal_input.rb +36 -9
  26. data/lib/graphql/query/null_context.rb +7 -1
  27. data/lib/graphql/query/result.rb +63 -0
  28. data/lib/graphql/query/serial_execution/field_resolution.rb +3 -4
  29. data/lib/graphql/query/serial_execution/value_resolution.rb +3 -4
  30. data/lib/graphql/query/variables.rb +1 -1
  31. data/lib/graphql/schema.rb +31 -0
  32. data/lib/graphql/schema/traversal.rb +16 -1
  33. data/lib/graphql/schema/warden.rb +40 -4
  34. data/lib/graphql/static_validation/validator.rb +20 -18
  35. data/lib/graphql/subscriptions.rb +129 -0
  36. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +122 -0
  37. data/lib/graphql/subscriptions/event.rb +52 -0
  38. data/lib/graphql/subscriptions/instrumentation.rb +68 -0
  39. data/lib/graphql/tracing.rb +80 -0
  40. data/lib/graphql/tracing/active_support_notifications_tracing.rb +31 -0
  41. data/lib/graphql/version.rb +1 -1
  42. data/readme.md +1 -1
  43. data/spec/graphql/analysis/analyze_query_spec.rb +19 -0
  44. data/spec/graphql/argument_spec.rb +28 -0
  45. data/spec/graphql/backtrace_spec.rb +144 -0
  46. data/spec/graphql/define/assign_argument_spec.rb +12 -0
  47. data/spec/graphql/enum_type_spec.rb +1 -1
  48. data/spec/graphql/execution/execute_spec.rb +66 -0
  49. data/spec/graphql/execution/lazy_spec.rb +4 -3
  50. data/spec/graphql/language/parser_spec.rb +16 -0
  51. data/spec/graphql/object_type_spec.rb +14 -0
  52. data/spec/graphql/query/context_spec.rb +134 -27
  53. data/spec/graphql/query/result_spec.rb +29 -0
  54. data/spec/graphql/query/variables_spec.rb +13 -0
  55. data/spec/graphql/query_spec.rb +22 -0
  56. data/spec/graphql/schema/build_from_definition_spec.rb +2 -0
  57. data/spec/graphql/schema/traversal_spec.rb +70 -12
  58. data/spec/graphql/schema/warden_spec.rb +67 -1
  59. data/spec/graphql/schema_spec.rb +29 -0
  60. data/spec/graphql/static_validation/validator_spec.rb +16 -0
  61. data/spec/graphql/subscriptions_spec.rb +331 -0
  62. data/spec/graphql/tracing/active_support_notifications_tracing_spec.rb +57 -0
  63. data/spec/graphql/tracing_spec.rb +47 -0
  64. data/spec/spec_helper.rb +32 -0
  65. data/spec/support/star_wars/schema.rb +39 -0
  66. metadata +27 -4
  67. data/lib/graphql/execution/field_result.rb +0 -54
  68. data/lib/graphql/execution/selection_result.rb +0 -90
@@ -37,6 +37,18 @@ describe GraphQL::Define::AssignArgument do
37
37
  assert_equal "GraphQL::Argument can't define 'blah'", err.message
38
38
  end
39
39
 
40
+ it "accepts an existing argument" do
41
+ existing = GraphQL::Argument.define do
42
+ name "bar"
43
+ type GraphQL::STRING_TYPE
44
+ end
45
+
46
+ arg = define_argument(:foo, existing)
47
+
48
+ assert_equal "foo", arg.name
49
+ assert_equal GraphQL::STRING_TYPE, arg.type
50
+ end
51
+
40
52
  def define_argument(*args)
41
53
  type = GraphQL::ObjectType.define do
42
54
  field :a, types.String do
@@ -39,7 +39,7 @@ describe GraphQL::EnumType do
39
39
 
40
40
  describe "invalid names" do
41
41
  it "rejects names with a space" do
42
- assert_raises(GraphQL::EnumType::InvalidEnumNameError) {
42
+ assert_raises(GraphQL::InvalidNameError) {
43
43
  InvalidEnumTest = GraphQL::EnumType.define do
44
44
  name "InvalidEnumTest"
45
45
 
@@ -137,4 +137,70 @@ describe GraphQL::Execution::Execute do
137
137
  assert_equal "👻", res["errors"].first["message"]
138
138
  end
139
139
  end
140
+
141
+ describe "tracing" do
142
+ if rails_should_be_installed?
143
+ it "emits traces" do
144
+ query_string = <<-GRAPHQL
145
+ query Bases($id1: ID!, $id2: ID!){
146
+ b1: batchedBase(id: $id1) { name }
147
+ b2: batchedBase(id: $id2) { name }
148
+ }
149
+ GRAPHQL
150
+ first_id = StarWars::Base.first.id
151
+ last_id = StarWars::Base.last.id
152
+
153
+ traces = TestTracing.with_trace do
154
+ star_wars_query(query_string, {
155
+ "id1" => first_id,
156
+ "id2" => last_id,
157
+ })
158
+ end
159
+
160
+ exec_traces = traces[5..-1]
161
+ expected_traces = [
162
+ "execute_field",
163
+ "execute_field",
164
+ "execute_query",
165
+ "lazy_loader",
166
+ "execute_field",
167
+ "execute_field_lazy",
168
+ "execute_field",
169
+ "execute_field_lazy",
170
+ "execute_field_lazy",
171
+ "execute_field_lazy",
172
+ "execute_query_lazy",
173
+ "execute_multiplex",
174
+ ]
175
+ assert_equal expected_traces, exec_traces.map { |t| t[:key] }
176
+
177
+ field_1_eager, field_2_eager,
178
+ query_eager, lazy_loader,
179
+ # field 3 is eager-resolved _during_ field 1's lazy resolve
180
+ field_3_eager, field_1_lazy,
181
+ field_4_eager, field_2_lazy,
182
+ # field 3 didn't finish above, it's resolved in the next round
183
+ field_3_lazy, field_4_lazy,
184
+ query_lazy, multiplex = exec_traces
185
+
186
+ assert_equal ["b1"], field_1_eager[:context].path
187
+ assert_equal ["b2"], field_2_eager[:context].path
188
+ assert_instance_of GraphQL::Query, query_eager[:query]
189
+
190
+ assert_equal [first_id.to_s, last_id.to_s], lazy_loader[:ids]
191
+ assert_equal StarWars::Base, lazy_loader[:model]
192
+
193
+ assert_equal ["b1", "name"], field_3_eager[:context].path
194
+ assert_equal ["b1"], field_1_lazy[:context].path
195
+ assert_equal ["b2", "name"], field_4_eager[:context].path
196
+ assert_equal ["b2"], field_2_lazy[:context].path
197
+
198
+ assert_equal ["b1", "name"], field_3_lazy[:context].path
199
+ assert_equal ["b2", "name"], field_4_lazy[:context].path
200
+ assert_instance_of GraphQL::Query, query_lazy[:query]
201
+
202
+ assert_instance_of GraphQL::Execution::Multiplex, multiplex[:multiplex]
203
+ end
204
+ end
205
+ end
140
206
  end
@@ -70,7 +70,7 @@ describe GraphQL::Execution::Lazy do
70
70
  assert_equal expected_data, res["data"]
71
71
  end
72
72
 
73
- it "propagates nulls" do
73
+ it "propagates nulls to the root" do
74
74
  res = run_query %|
75
75
  {
76
76
  nestedSum(value: 1) {
@@ -85,14 +85,15 @@ describe GraphQL::Execution::Lazy do
85
85
 
86
86
  assert_equal(nil, res["data"])
87
87
  assert_equal 1, res["errors"].length
88
+ end
88
89
 
89
-
90
+ it "propagates partial nulls" do
90
91
  res = run_query %|
91
92
  {
92
93
  nullableNestedSum(value: 1) {
93
94
  value
94
95
  nullableNestedSum(value: 2) {
95
- nestedSum(value: 13) {
96
+ ns: nestedSum(value: 13) {
96
97
  value
97
98
  }
98
99
  }
@@ -71,4 +71,20 @@ describe GraphQL::Language::Parser do
71
71
 
72
72
  end
73
73
  end
74
+
75
+ it "serves traces" do
76
+ traces = TestTracing.with_trace do
77
+ GraphQL.parse("{ t: __typename }")
78
+ end
79
+ assert_equal 2, traces.length
80
+ lex_trace, parse_trace = traces
81
+
82
+ assert_equal "{ t: __typename }", lex_trace[:query_string]
83
+ assert_equal "lex", lex_trace[:key]
84
+ assert_instance_of Array, lex_trace[:result]
85
+
86
+ assert_equal "{ t: __typename }", parse_trace[:query_string]
87
+ assert_equal "parse", parse_trace[:key]
88
+ assert_instance_of GraphQL::Language::Nodes::Document, parse_trace[:result]
89
+ end
74
90
  end
@@ -17,6 +17,20 @@ describe GraphQL::ObjectType do
17
17
  }
18
18
  end
19
19
 
20
+ it "doesn't allow invalid name" do
21
+ exception = assert_raises(GraphQL::InvalidNameError) {
22
+ InvalidNameObject = GraphQL::ObjectType.define do
23
+ name "Three Word Query"
24
+
25
+ field :id, !types.Int, "id field"
26
+ end
27
+
28
+ # Force evaluation
29
+ InvalidNameObject.name
30
+ }
31
+ assert_equal("Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but 'Three Word Query' does not", exception.message)
32
+ end
33
+
20
34
  it "has a name" do
21
35
  assert_equal("Cheese", type.name)
22
36
  type.name = "Fromage"
@@ -5,35 +5,102 @@ describe GraphQL::Query::Context do
5
5
  CTX = []
6
6
  before { CTX.clear }
7
7
 
8
- let(:query_type) { GraphQL::ObjectType.define {
9
- name "Query"
10
- field :context, types.String do
11
- argument :key, !types.String
12
- resolve ->(target, args, ctx) { ctx[args[:key]] }
13
- end
14
- field :contextAstNodeName, types.String do
15
- resolve ->(target, args, ctx) { ctx.ast_node.class.name }
16
- end
17
- field :contextIrepNodeName, types.String do
18
- resolve ->(target, args, ctx) { ctx.irep_node.class.name }
19
- end
20
- field :queryName, types.String do
21
- resolve ->(target, args, ctx) { ctx.query.class.name }
8
+ let(:parent_info_type) {
9
+ GraphQL::ObjectType.define {
10
+ name "ParentInfo"
11
+ field :object, types.String do
12
+ resolve ->(o, a, c) { c.parent.parent.object }
13
+ end
14
+ field :objectClassName, types.String do
15
+ resolve ->(o, a, c) { c.parent.parent.object.class.name }
16
+ end
17
+ field :valueClassName, types.String do
18
+ resolve ->(o, a, c) { c.parent.parent.value.class.name }
19
+ end
20
+ field :value, types.String do
21
+ resolve ->(o, a, c) { c.parent.parent.value.to_s }
22
+ end
23
+ }
24
+ }
25
+ let(:backtrace_type) {
26
+ GraphQL::ObjectType.define do
27
+ name "Backtrace"
28
+ field :backtraceEntry, types.String do
29
+ argument :idx, !types.Int
30
+ resolve ->(o, a, c) { c.backtrace[a[:idx]] }
31
+ end
32
+ field :backtraceArray, types[types.String] do
33
+ resolve ->(o, a, c) { c.backtrace.to_a }
34
+ end
35
+ field :backtraceTable, types.String do
36
+ resolve ->(o, a, c) { c.backtrace.inspect }
37
+ end
22
38
  end
39
+ }
40
+ let(:query_type) {
41
+ parent_info = parent_info_type
42
+ backtrace = backtrace_type
43
+ GraphQL::ObjectType.define {
44
+ name "Query"
45
+ field :context, types.String do
46
+ argument :key, !types.String
47
+ resolve ->(target, args, ctx) { ctx[args[:key]] }
48
+ end
49
+ field :contextAstNodeName, types.String do
50
+ resolve ->(target, args, ctx) { ctx.ast_node.class.name }
51
+ end
52
+ field :contextIrepNodeName, types.String do
53
+ resolve ->(target, args, ctx) { ctx.irep_node.class.name }
54
+ end
55
+ field :queryName, types.String do
56
+ resolve ->(target, args, ctx) { ctx.query.class.name }
57
+ end
23
58
 
24
- field :pushContext, types.Int do
25
- resolve ->(t,a,c) { CTX << c; 1 }
26
- end
59
+ field :pushContext, types.Int do
60
+ resolve ->(t,a,c) { CTX << c; 1 }
61
+ end
62
+
63
+ field :pushQueryError, types.Int do
64
+ resolve ->(t,a,c) {
65
+ c.query.context.add_error(GraphQL::ExecutionError.new("Query-level error"))
66
+ 1
67
+ }
68
+ end
27
69
 
28
- field :pushQueryError, types.Int do
29
- resolve ->(t,a,c) {
30
- c.query.context.add_error(GraphQL::ExecutionError.new("Query-level error"))
31
- 1
70
+ field :parentInfo, parent_info, resolve: ->(o,a,c) { :noop }
71
+ field :backtrace, backtrace, resolve: Proc.new { :noop }
72
+ }
73
+ }
74
+
75
+ let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}
76
+ let(:result) { schema.execute(query_string, root_value: "rootval", context: {"some_key" => "some value"})}
77
+
78
+ describe "access to parent context" do
79
+ let(:query_string) { %|
80
+ {
81
+ parentInfo {
82
+ value
83
+ valueClassName
84
+ object
85
+ objectClassName
86
+ }
32
87
  }
88
+ |}
89
+
90
+ it "exposes the parent object" do
91
+ expected = {
92
+ "data" => {
93
+ "parentInfo" => {
94
+ "objectClassName" => "String",
95
+ "object" => "rootval",
96
+ "value" => "{}",
97
+ "valueClassName" => "Hash",
98
+ }
99
+ }
100
+ }
101
+ assert_equal(expected, result)
33
102
  end
34
- }}
35
- let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}
36
- let(:result) { schema.execute(query_string, context: {"some_key" => "some value"})}
103
+ end
37
104
 
38
105
  describe "access to passed-in values" do
39
106
  let(:query_string) { %|
@@ -57,6 +124,46 @@ describe GraphQL::Query::Context do
57
124
  end
58
125
  end
59
126
 
127
+ describe "#backtrace" do
128
+ let(:query_string) { %|
129
+ query {
130
+ backtrace {
131
+ b1: backtraceEntry(idx: 0)
132
+ b2: backtraceEntry(idx: 1)
133
+ b3: backtraceEntry(idx: 2)
134
+ backtraceArray
135
+ backtraceTable
136
+ }
137
+ pushContext
138
+ }
139
+ |}
140
+
141
+ it "exposes the GraphQL backtrace" do
142
+ backtrace_result = result.fetch("data").fetch("backtrace")
143
+ assert_equal "4:11: Backtrace.backtraceEntry as b1", backtrace_result.fetch("b1")
144
+ assert_equal "3:9: Query.backtrace", backtrace_result.fetch("b2")
145
+ assert_equal "2:7: query", backtrace_result.fetch("b3")
146
+ assert_equal ["7:11: Backtrace.backtraceArray", "3:9: Query.backtrace", "2:7: query"], backtrace_result.fetch("backtraceArray")
147
+ expected_table = [
148
+ 'Loc | Field | Object | Arguments | Result',
149
+ '8:11 | Backtrace.backtraceTable | :noop | {} | nil',
150
+ '3:9 | Query.backtrace | "rootval" | {} | {b1: "4:11: Backtrace.backtraceEntry as b1", b2: "3:9: Query.backtrace", b3: "2:7: query", backtr...',
151
+ '2:7 | query | "rootval" | {} | {}',
152
+ '',
153
+ ].join("\n")
154
+ assert_equal expected_table, backtrace_result.fetch("backtraceTable")
155
+
156
+ expected_table_2 = <<-TABLE
157
+ Loc | Field | Object | Arguments | Result
158
+ 10:9 | Query.pushContext | "rootval" | {} | 1
159
+ 2:7 | query | "rootval" | {} | {backtrace: {...}, pushContext: 1}
160
+ TABLE
161
+
162
+ ctx = CTX.last
163
+ assert_equal expected_table_2, ctx.backtrace.to_s
164
+ end
165
+ end
166
+
60
167
  describe "access to the InternalRepresentation node" do
61
168
  let(:query_string) { %|
62
169
  query getCtx { contextIrepNodeName }
@@ -80,7 +187,7 @@ describe GraphQL::Query::Context do
80
187
  end
81
188
 
82
189
  describe "empty values" do
83
- let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil) }
190
+ let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil, object: nil) }
84
191
 
85
192
  it "returns returns nil and reports key? => false" do
86
193
  assert_equal(nil, context[:some_key])
@@ -90,7 +197,7 @@ describe GraphQL::Query::Context do
90
197
  end
91
198
 
92
199
  describe "assigning values" do
93
- let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil) }
200
+ let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil, object: nil) }
94
201
 
95
202
  it "allows you to assign new contexts" do
96
203
  assert_equal(nil, context[:some_key])
@@ -99,7 +206,7 @@ describe GraphQL::Query::Context do
99
206
  end
100
207
 
101
208
  describe "namespaces" do
102
- let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: {a: 1}) }
209
+ let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: {a: 1}, object: nil) }
103
210
 
104
211
  it "doesn't conflict with base values" do
105
212
  ns = context.namespace(:stuff)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ describe GraphQL::Query::Result do
5
+ let(:query_string) { '{ __type(name: "Cheese") { name } }' }
6
+ let(:schema) { Dummy::Schema }
7
+ let(:result) { schema.execute(query_string, context: { a: :b }) }
8
+
9
+ it "exposes hash-like methods" do
10
+ assert_equal "Cheese", result["data"]["__type"]["name"]
11
+ refute result.key?("errors")
12
+ assert_equal ["data"], result.keys
13
+ end
14
+
15
+ it "is equal with hashes" do
16
+ hash_result = {"data" => { "__type" => { "name" => "Cheese" } } }
17
+ assert_equal hash_result, result
18
+ end
19
+
20
+ it "tells the kind of operation" do
21
+ assert result.query?
22
+ refute result.mutation?
23
+ end
24
+
25
+ it "exposes the context" do
26
+ assert_instance_of GraphQL::Query::Context, result.context
27
+ assert_equal({a: :b}, result.context.to_h)
28
+ end
29
+ end
@@ -26,6 +26,19 @@ describe GraphQL::Query::Variables do
26
26
  provided_variables)
27
27
  }
28
28
 
29
+ describe "#to_h" do
30
+ let(:provided_variables) { { "animals" => "YAK" } }
31
+
32
+ it "returns a hash representation including default values" do
33
+ expected_hash = {
34
+ "animals" => ["YAK"],
35
+ "intDefaultNull" => nil,
36
+ "intWithDefault" => 10,
37
+ }
38
+ assert_equal expected_hash, variables.to_h
39
+ end
40
+ end
41
+
29
42
  describe "#initialize" do
30
43
  describe "coercing inputs" do
31
44
  let(:provided_variables) { { "animals" => "YAK" } }
@@ -44,6 +44,28 @@ describe GraphQL::Query do
44
44
  )}
45
45
  let(:result) { query.result }
46
46
 
47
+ describe "when passed both a query string and a document" do
48
+ it "returns an error to the client when query kwarg is used" do
49
+ assert_raises ArgumentError do
50
+ GraphQL::Query.new(
51
+ schema,
52
+ query: "{ fromSource(source: COW) { id } }",
53
+ document: document
54
+ )
55
+ end
56
+ end
57
+
58
+ it "returns an error to the client" do
59
+ assert_raises ArgumentError do
60
+ GraphQL::Query.new(
61
+ schema,
62
+ "{ fromSource(source: COW) { id } }",
63
+ document: document
64
+ )
65
+ end
66
+ end
67
+ end
68
+
47
69
  describe "when passed no query string or document" do
48
70
  it 'returns an error to the client' do
49
71
  res = GraphQL::Query.new(