graphql 1.6.8 → 1.7.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 (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(