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
@@ -131,7 +131,7 @@ module GraphQL
131
131
  }
132
132
 
133
133
  SCHEMA_CAN_RESOLVE_TYPES = ->(schema) {
134
- if schema.types.values.any? { |type| type.kind.resolves? } && schema.resolve_type_proc.nil?
134
+ if schema.types.values.any? { |type| type.kind.abstract? } && schema.resolve_type_proc.nil?
135
135
  "schema contains Interfaces or Unions, so you must define a `resolve_type -> (obj, ctx) { ... }` function"
136
136
  else
137
137
  # :+1:
@@ -33,7 +33,11 @@ module GraphQL
33
33
  private
34
34
 
35
35
  def locations
36
- @nodes.map{|node| {"line" => node.line, "column" => node.col}}
36
+ @nodes.map do |node|
37
+ h = {"line" => node.line, "column" => node.col}
38
+ h["filename"] = node.filename if node.filename
39
+ h
40
+ end
37
41
  end
38
42
  end
39
43
  end
@@ -12,6 +12,7 @@ module GraphQL
12
12
  }
13
13
 
14
14
  visitor = context.visitor
15
+
15
16
  visitor[GraphQL::Language::Nodes::DirectiveDefinition] << register_node
16
17
  visitor[GraphQL::Language::Nodes::SchemaDefinition] << register_node
17
18
  visitor[GraphQL::Language::Nodes::ScalarTypeDefinition] << register_node
@@ -21,6 +22,14 @@ module GraphQL
21
22
  visitor[GraphQL::Language::Nodes::UnionTypeDefinition] << register_node
22
23
  visitor[GraphQL::Language::Nodes::EnumTypeDefinition] << register_node
23
24
 
25
+ visitor[GraphQL::Language::Nodes::SchemaExtension] << register_node
26
+ visitor[GraphQL::Language::Nodes::ScalarTypeExtension] << register_node
27
+ visitor[GraphQL::Language::Nodes::ObjectTypeExtension] << register_node
28
+ visitor[GraphQL::Language::Nodes::InputObjectTypeExtension] << register_node
29
+ visitor[GraphQL::Language::Nodes::InterfaceTypeExtension] << register_node
30
+ visitor[GraphQL::Language::Nodes::UnionTypeExtension] << register_node
31
+ visitor[GraphQL::Language::Nodes::EnumTypeExtension] << register_node
32
+
24
33
  visitor[GraphQL::Language::Nodes::Document].leave << ->(node, _p) {
25
34
  if schema_definition_nodes.any?
26
35
  context.errors << message(%|Query cannot contain schema definitions|, schema_definition_nodes, context: context)
@@ -5,18 +5,21 @@ module GraphQL
5
5
  # These objects are singletons, eg `GraphQL::TypeKinds::UNION`, `GraphQL::TypeKinds::SCALAR`.
6
6
  class TypeKind
7
7
  attr_reader :name, :description
8
- def initialize(name, resolves: false, fields: false, wraps: false, input: false, description: nil)
8
+ def initialize(name, abstract: false, fields: false, wraps: false, input: false, description: nil)
9
9
  @name = name
10
- @resolves = resolves
10
+ @abstract = abstract
11
11
  @fields = fields
12
12
  @wraps = wraps
13
13
  @input = input
14
- @composite = fields? || resolves?
14
+ @composite = fields? || abstract?
15
15
  @description = description
16
16
  end
17
17
 
18
18
  # Does this TypeKind have multiple possible implementors?
19
- def resolves?; @resolves; end
19
+ # @deprecated Use `abstract?` instead of `resolves?`.
20
+ def resolves?; @abstract; end
21
+ # Is this TypeKind abstract?
22
+ def abstract?; @abstract; end
20
23
  # Does this TypeKind have queryable fields?
21
24
  def fields?; @fields; end
22
25
  # Does this TypeKind modify another type?
@@ -31,8 +34,8 @@ module GraphQL
31
34
  TYPE_KINDS = [
32
35
  SCALAR = TypeKind.new("SCALAR", input: true, description: 'Indicates this type is a scalar.'),
33
36
  OBJECT = TypeKind.new("OBJECT", fields: true, description: 'Indicates this type is an object. `fields` and `interfaces` are valid fields.'),
34
- INTERFACE = TypeKind.new("INTERFACE", resolves: true, fields: true, description: 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.'),
35
- UNION = TypeKind.new("UNION", resolves: true, description: 'Indicates this type is a union. `possibleTypes` is a valid field.'),
37
+ INTERFACE = TypeKind.new("INTERFACE", abstract: true, fields: true, description: 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.'),
38
+ UNION = TypeKind.new("UNION", abstract: true, description: 'Indicates this type is a union. `possibleTypes` is a valid field.'),
36
39
  ENUM = TypeKind.new("ENUM", input: true, description: 'Indicates this type is an enum. `enumValues` is a valid field.'),
37
40
  INPUT_OBJECT = TypeKind.new("INPUT_OBJECT", input: true, description: 'Indicates this type is an input object. `inputFields` is a valid field.'),
38
41
  LIST = TypeKind.new("LIST", wraps: true, description: 'Indicates this type is a list. `ofType` is a valid field.'),
@@ -3,5 +3,6 @@ require "graphql/types/boolean"
3
3
  require "graphql/types/float"
4
4
  require "graphql/types/id"
5
5
  require "graphql/types/int"
6
+ require "graphql/types/iso_8601_date_time"
6
7
  require "graphql/types/string"
7
8
  require "graphql/types/relay"
@@ -4,11 +4,7 @@ module GraphQL
4
4
  # This scalar takes `DateTime`s and transmits them as strings,
5
5
  # using ISO 8601 format.
6
6
  #
7
- # To use it, require it in your project:
8
- #
9
- # require "graphql/types/iso_8601_date_time"
10
- #
11
- # Then use it for fields or arguments:
7
+ # Use it for fields or arguments as follows:
12
8
  #
13
9
  # field :created_at, GraphQL::Types::ISO8601DateTime, null: false
14
10
  #
@@ -10,11 +10,16 @@ module GraphQL
10
10
  # @return [GraphQL::Query::Context] the context for the current query
11
11
  attr_reader :context
12
12
 
13
- def initialize(object:, type:, context:)
13
+ def initialize(message = nil, object: nil, type: nil, context: nil)
14
+ if message.nil? && object.nil?
15
+ raise ArgumentError, "#{self.class.name} requires either a message or keywords"
16
+ end
17
+
14
18
  @object = object
15
19
  @type = type
16
20
  @context = context
17
- super("An instance of #{object.class} failed #{type.name}'s authorization check")
21
+ message ||= "An instance of #{object.class} failed #{type.name}'s authorization check"
22
+ super(message)
18
23
  end
19
24
  end
20
25
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.8.4"
3
+ VERSION = "1.8.5"
4
4
  end
@@ -129,7 +129,7 @@ RUBY
129
129
  assert_file "app/controllers/graphql_controller.rb", /CustomSchema\.execute/
130
130
  end
131
131
 
132
- EXPECTED_GRAPHQLS_CONTROLLER = <<-RUBY
132
+ EXPECTED_GRAPHQLS_CONTROLLER = <<-'RUBY'
133
133
  class GraphqlController < ApplicationController
134
134
  def execute
135
135
  variables = ensure_hash(params[:variables])
@@ -141,6 +141,9 @@ class GraphqlController < ApplicationController
141
141
  }
142
142
  result = DummySchema.execute(query, variables: variables, context: context, operation_name: operation_name)
143
143
  render json: result
144
+ rescue => e
145
+ raise e unless Rails.env.development?
146
+ handle_error_in_development e
144
147
  end
145
148
 
146
149
  private
@@ -159,9 +162,16 @@ class GraphqlController < ApplicationController
159
162
  when nil
160
163
  {}
161
164
  else
162
- raise ArgumentError, "Unexpected parameter: \#{ambiguous_param}"
165
+ raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
163
166
  end
164
167
  end
168
+
169
+ def handle_error_in_development(e)
170
+ logger.error e.message
171
+ logger.error e.backtrace.join("\n")
172
+
173
+ render json: { error: { message: e.message, backtrace: e.backtrace }, data: {} }, status: 500
174
+ end
165
175
  end
166
176
  RUBY
167
177
 
@@ -208,6 +208,26 @@ describe GraphQL::Authorization do
208
208
  edge_type(IntegerObjectEdge)
209
209
  end
210
210
 
211
+ # This object responds with `replaced => false`,
212
+ # but if its replacement value is used, it gives `replaced => true`
213
+ class Replaceable
214
+ def replacement
215
+ { replaced: true }
216
+ end
217
+
218
+ def replaced
219
+ false
220
+ end
221
+ end
222
+
223
+ class ReplacedObject < BaseObject
224
+ def self.authorized?(obj, ctx)
225
+ super && !ctx[:replace_me]
226
+ end
227
+
228
+ field :replaced, Boolean, null: false
229
+ end
230
+
211
231
  class LandscapeFeature < BaseEnum
212
232
  value "MOUNTAIN"
213
233
  value "STREAM", role: :unauthorized
@@ -284,6 +304,12 @@ describe GraphQL::Authorization do
284
304
  argument :value, String, required: true
285
305
  end
286
306
 
307
+ field :unauthorized_lazy_list_interface, [UnauthorizedInterface, null: true], null: true
308
+
309
+ def unauthorized_lazy_list_interface
310
+ ["z", Box.new(value: Box.new(value: "z2")), "a", Box.new(value: "a")]
311
+ end
312
+
287
313
  field :integers, IntegerObjectConnection, null: false
288
314
 
289
315
  def integers
@@ -295,6 +321,11 @@ describe GraphQL::Authorization do
295
321
  def lazy_integers
296
322
  Box.new(value: Box.new(value: [1,2,3]))
297
323
  end
324
+
325
+ field :replaced_object, ReplacedObject, null: false
326
+ def replaced_object
327
+ Replaceable.new
328
+ end
298
329
  end
299
330
 
300
331
  class DoHiddenStuff < GraphQL::Schema::RelayClassicMutation
@@ -303,6 +334,12 @@ describe GraphQL::Authorization do
303
334
  end
304
335
  end
305
336
 
337
+ class DoHiddenStuff2 < GraphQL::Schema::Mutation
338
+ def self.visible?(ctx)
339
+ super && !ctx[:hidden_mutation]
340
+ end
341
+ end
342
+
306
343
  class DoInaccessibleStuff < GraphQL::Schema::RelayClassicMutation
307
344
  def self.accessible?(ctx)
308
345
  super && (ctx[:inaccessible_mutation] ? false : true)
@@ -317,6 +354,7 @@ describe GraphQL::Authorization do
317
354
 
318
355
  class Mutation < BaseObject
319
356
  field :do_hidden_stuff, mutation: DoHiddenStuff
357
+ field :do_hidden_stuff2, mutation: DoHiddenStuff2
320
358
  field :do_inaccessible_stuff, mutation: DoInaccessibleStuff
321
359
  field :do_unauthorized_stuff, mutation: DoUnauthorizedStuff
322
360
  end
@@ -328,7 +366,11 @@ describe GraphQL::Authorization do
328
366
  lazy_resolve(Box, :value)
329
367
 
330
368
  def self.unauthorized_object(err)
331
- raise GraphQL::ExecutionError, "Unauthorized #{err.type.graphql_name}: #{err.object}"
369
+ if err.object.respond_to?(:replacement)
370
+ err.object.replacement
371
+ else
372
+ raise GraphQL::ExecutionError, "Unauthorized #{err.type.graphql_name}: #{err.object}"
373
+ end
332
374
  end
333
375
 
334
376
  # use GraphQL::Backtrace
@@ -387,6 +429,17 @@ describe GraphQL::Authorization do
387
429
  assert_equal "DoHiddenStuffPayload", visible_introspection_res["data"]["t2"]["name"]
388
430
  end
389
431
 
432
+ it "works with Schema::Mutation" do
433
+ query = "mutation { doHiddenStuff2 { __typename } }"
434
+ res = auth_execute(query, context: { hidden_mutation: true })
435
+ assert_equal ["Field 'doHiddenStuff2' doesn't exist on type 'Mutation'"], res["errors"].map { |e| e["message"] }
436
+
437
+ # `#resolve` isn't implemented, so this errors out:
438
+ assert_raises NotImplementedError do
439
+ auth_execute(query)
440
+ end
441
+ end
442
+
390
443
  it "uses the base type for edges and connections" do
391
444
  query = <<-GRAPHQL
392
445
  {
@@ -680,5 +733,31 @@ describe GraphQL::Authorization do
680
733
  res2 = auth_execute(query, variables: { value: "b"})
681
734
  assert_equal "b", res2["data"]["unauthorizedInterface"]["value"]
682
735
  end
736
+
737
+ it "works with lazy values / lists of interfaces" do
738
+ query = <<-GRAPHQL
739
+ {
740
+ unauthorizedLazyListInterface {
741
+ ... on UnauthorizedCheckBox {
742
+ value
743
+ }
744
+ }
745
+ }
746
+ GRAPHQL
747
+
748
+ res = auth_execute(query)
749
+ # An error from two, values from the others
750
+ assert_equal ["Unauthorized UnauthorizedCheckBox: a", "Unauthorized UnauthorizedCheckBox: a"], res["errors"].map { |e| e["message"] }
751
+ assert_equal [{"value" => "z"}, {"value" => "z2"}, nil, nil], res["data"]["unauthorizedLazyListInterface"]
752
+ end
753
+
754
+ it "replaces objects from the unauthorized_object hook" do
755
+ query = "{ replacedObject { replaced } }"
756
+ res = auth_execute(query, context: { replace_me: true })
757
+ assert_equal true, res["data"]["replacedObject"]["replaced"]
758
+
759
+ res = auth_execute(query, context: { replace_me: false })
760
+ assert_equal false, res["data"]["replacedObject"]["replaced"]
761
+ end
683
762
  end
684
763
  end
@@ -48,6 +48,48 @@ describe GraphQL::Directive do
48
48
  end
49
49
  end
50
50
 
51
+ describe "when directive uses argument with default value" do
52
+ describe "with false" do
53
+ let(:query_string) { <<-GRAPHQL
54
+ query($f: Boolean = false) {
55
+ cheese(id: 1) {
56
+ dontIncludeFlavor: flavor @include(if: $f)
57
+ dontSkipFlavor: flavor @skip(if: $f)
58
+ }
59
+ }
60
+ GRAPHQL
61
+ }
62
+
63
+ it "is not included" do
64
+ assert !result["data"]["cheese"].key?("dontIncludeFlavor")
65
+ end
66
+
67
+ it "is not skipped" do
68
+ assert result["data"]["cheese"].key?("dontSkipFlavor")
69
+ end
70
+ end
71
+
72
+ describe "with true" do
73
+ let(:query_string) { <<-GRAPHQL
74
+ query($t: Boolean = true) {
75
+ cheese(id: 1) {
76
+ includeFlavor: flavor @include(if: $t)
77
+ skipFlavor: flavor @skip(if: $t)
78
+ }
79
+ }
80
+ GRAPHQL
81
+ }
82
+
83
+ it "is included" do
84
+ assert result["data"]["cheese"].key?("includeFlavor")
85
+ end
86
+
87
+ it "is skipped" do
88
+ assert !result["data"]["cheese"].key?("skipFlavor")
89
+ end
90
+ end
91
+ end
92
+
51
93
  it "intercepts fields" do
52
94
  expected = { "data" =>{
53
95
  "cheese" => {
@@ -137,7 +137,7 @@ describe GraphQL::ExecutionError do
137
137
  },
138
138
  ]
139
139
  }
140
- assert_equal(expected_result, result)
140
+ assert_equal(expected_result, result.to_h)
141
141
  end
142
142
  end
143
143
 
@@ -638,6 +638,20 @@ describe GraphQL::Query do
638
638
  end
639
639
  end
640
640
 
641
+ describe "validating with optional arguments and variables: nil" do
642
+ it "works" do
643
+ query_str = <<-GRAPHQL
644
+ query($expiresAfter: Time) {
645
+ searchDairy(expiresAfter: $expiresAfter) {
646
+ __typename
647
+ }
648
+ }
649
+ GRAPHQL
650
+ query = GraphQL::Query.new(schema, query_str, variables: nil)
651
+ assert query.valid?
652
+ end
653
+ end
654
+
641
655
  describe 'NullValue type arguments' do
642
656
  let(:schema_definition) {
643
657
  <<-GRAPHQL
@@ -161,6 +161,12 @@ describe GraphQL::Schema::Interface do
161
161
  describe ':DefinitionMethods' do
162
162
  module InterfaceA
163
163
  include GraphQL::Schema::Interface
164
+
165
+ definition_methods do
166
+ def some_method
167
+ 42
168
+ end
169
+ end
164
170
  end
165
171
 
166
172
  module InterfaceB
@@ -171,6 +177,10 @@ describe GraphQL::Schema::Interface do
171
177
  include GraphQL::Schema::Interface
172
178
  end
173
179
 
180
+ class ObjectA < GraphQL::Schema::Object
181
+ implements InterfaceA
182
+ end
183
+
174
184
  it "doesn't overwrite them when including multiple interfaces" do
175
185
  def_methods = InterfaceC::DefinitionMethods
176
186
 
@@ -181,5 +191,9 @@ describe GraphQL::Schema::Interface do
181
191
 
182
192
  assert_equal(InterfaceC::DefinitionMethods, def_methods)
183
193
  end
194
+
195
+ it "extends classes with the defined methods" do
196
+ assert_equal(ObjectA.some_method, InterfaceA.some_method)
197
+ end
184
198
  end
185
199
  end
@@ -41,6 +41,30 @@ describe GraphQL::Schema::Object do
41
41
  assert_equal object_class.description, new_subclass_2.description
42
42
  end
43
43
 
44
+ it "does not inherit singleton methods from base interface when implementing base interface" do
45
+ object_type = Class.new(GraphQL::Schema::Object)
46
+ methods = object_type.singleton_methods
47
+ method_defs = Hash[methods.zip(methods.map{|method| object_type.method(method.to_sym)})]
48
+
49
+ object_type.implements(GraphQL::Schema::Interface)
50
+ new_method_defs = Hash[methods.zip(methods.map{|method| object_type.method(method.to_sym)})]
51
+ assert_equal method_defs, new_method_defs
52
+ end
53
+
54
+ it "does not inherit singleton methods from base interface when implementing another interface" do
55
+ object_type = Class.new(GraphQL::Schema::Object)
56
+ methods = object_type.singleton_methods
57
+ method_defs = Hash[methods.zip(methods.map{|method| object_type.method(method.to_sym)})]
58
+
59
+ module InterfaceType
60
+ include GraphQL::Schema::Interface
61
+ end
62
+
63
+ object_type.implements(InterfaceType)
64
+ new_method_defs = Hash[methods.zip(methods.map{|method| object_type.method(method.to_sym)})]
65
+ assert_equal method_defs, new_method_defs
66
+ end
67
+
44
68
  it "should take Ruby name (without Type suffix) as default graphql name" do
45
69
  TestingClassType = Class.new(GraphQL::Schema::Object)
46
70
  assert_equal "TestingClass", TestingClassType.graphql_name
@@ -83,7 +107,23 @@ describe GraphQL::Schema::Object do
83
107
  end
84
108
  end
85
109
 
86
- describe ".to_graphql_type" do
110
+ describe "wrapping `nil`" do
111
+ it "doesn't wrap nil in lists" do
112
+ query_str = <<-GRAPHQL
113
+ {
114
+ namedEntities {
115
+ name
116
+ }
117
+ }
118
+ GRAPHQL
119
+
120
+ res = Jazz::Schema.execute(query_str)
121
+ expected_items = [{"name" => "Bela Fleck and the Flecktones"}, nil]
122
+ assert_equal expected_items, res["data"]["namedEntities"]
123
+ end
124
+ end
125
+
126
+ describe ".to_graphql" do
87
127
  let(:obj_type) { Jazz::Ensemble.to_graphql }
88
128
  it "returns a matching GraphQL::ObjectType" do
89
129
  assert_equal "Ensemble", obj_type.name