graphql 1.8.4 → 1.8.5

Sign up to get free protection for your applications and to get access to all the features.
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