graphql 1.7.4 → 1.7.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +13 -3
  3. data/lib/generators/graphql/enum_generator.rb +1 -1
  4. data/lib/generators/graphql/function_generator.rb +4 -1
  5. data/lib/generators/graphql/install_generator.rb +3 -3
  6. data/lib/generators/graphql/interface_generator.rb +1 -1
  7. data/lib/generators/graphql/loader_generator.rb +4 -1
  8. data/lib/generators/graphql/mutation_generator.rb +3 -3
  9. data/lib/generators/graphql/object_generator.rb +1 -1
  10. data/lib/generators/graphql/type_generator.rb +3 -0
  11. data/lib/generators/graphql/union_generator.rb +1 -1
  12. data/lib/graphql/backtrace/inspect_result.rb +1 -1
  13. data/lib/graphql/backtrace/table.rb +2 -2
  14. data/lib/graphql/backtrace/tracer.rb +7 -1
  15. data/lib/graphql/base_type.rb +10 -0
  16. data/lib/graphql/compatibility/query_parser_specification.rb +15 -0
  17. data/lib/graphql/execution/multiplex.rb +4 -0
  18. data/lib/graphql/language/parser.rb +595 -514
  19. data/lib/graphql/language/parser.y +21 -5
  20. data/lib/graphql/list_type.rb +4 -0
  21. data/lib/graphql/non_null_type.rb +5 -1
  22. data/lib/graphql/query.rb +3 -0
  23. data/lib/graphql/static_validation/all_rules.rb +1 -0
  24. data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +23 -0
  25. data/lib/graphql/subscriptions.rb +6 -0
  26. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +2 -4
  27. data/lib/graphql/subscriptions/serialize.rb +36 -10
  28. data/lib/graphql/tracing/scout_tracing.rb +5 -3
  29. data/lib/graphql/version.rb +1 -1
  30. data/spec/dummy/Gemfile +4 -2
  31. data/spec/dummy/config/application.rb +0 -1
  32. data/spec/generators/graphql/install_generator_spec.rb +7 -0
  33. data/spec/generators/graphql/mutation_generator_spec.rb +25 -2
  34. data/spec/generators/graphql/union_generator_spec.rb +14 -0
  35. data/spec/graphql/backtrace_spec.rb +21 -1
  36. data/spec/graphql/base_type_spec.rb +42 -0
  37. data/spec/graphql/execution/multiplex_spec.rb +48 -0
  38. data/spec/graphql/field_spec.rb +14 -2
  39. data/spec/graphql/static_validation/rules/variable_names_are_unique_spec.rb +23 -0
  40. data/spec/graphql/subscriptions/serialize_spec.rb +53 -0
  41. data/spec/graphql/subscriptions_spec.rb +9 -2
  42. data/spec/graphql/tracing/scout_tracing_spec.rb +17 -0
  43. metadata +23 -2
@@ -78,8 +78,8 @@ rule
78
78
  | LBRACKET type RBRACKET { return make_node(:ListType, of_type: val[1]) }
79
79
 
80
80
  default_value_opt:
81
- /* none */ { return nil }
82
- | EQUALS input_value { return val[1] }
81
+ /* none */ { return nil }
82
+ | EQUALS literal_value { return val[1] }
83
83
 
84
84
  selection_set:
85
85
  LCURLY RCURLY { return [] }
@@ -176,17 +176,21 @@ rule
176
176
  argument:
177
177
  name COLON input_value { return make_node(:Argument, name: val[0], value: val[2], position_source: val[0])}
178
178
 
179
- input_value:
179
+ literal_value:
180
180
  FLOAT { return val[0].to_f }
181
181
  | INT { return val[0].to_i }
182
182
  | STRING { return val[0].to_s }
183
183
  | TRUE { return true }
184
184
  | FALSE { return false }
185
185
  | null_value
186
- | variable
186
+ | enum_value
187
187
  | list_value
188
+ | object_literal_value
189
+
190
+ input_value:
191
+ | literal_value
192
+ | variable
188
193
  | object_value
189
- | enum_value
190
194
 
191
195
  null_value: NULL { return make_node(:NullValue, name: val[0], position_source: val[0]) }
192
196
  variable: VAR_SIGN name { return make_node(:VariableIdentifier, name: val[1], position_source: val[0]) }
@@ -210,6 +214,18 @@ rule
210
214
  object_value_field:
211
215
  name COLON input_value { return make_node(:Argument, name: val[0], value: val[2], position_source: val[0])}
212
216
 
217
+ /* like the previous, but with literals only: */
218
+ object_literal_value:
219
+ LCURLY RCURLY { return make_node(:InputObject, arguments: [], position_source: val[0])}
220
+ | LCURLY object_literal_value_list RCURLY { return make_node(:InputObject, arguments: val[1], position_source: val[0])}
221
+
222
+ object_literal_value_list:
223
+ object_literal_value_field { return [val[0]] }
224
+ | object_literal_value_list object_literal_value_field { val[0] << val[1] }
225
+
226
+ object_literal_value_field:
227
+ name COLON literal_value { return make_node(:Argument, name: val[0], value: val[2], position_source: val[0])}
228
+
213
229
  enum_value: enum_name { return make_node(:Enum, name: val[0], position_source: val[0]) }
214
230
 
215
231
  directives_list_opt:
@@ -49,6 +49,10 @@ module GraphQL
49
49
  ensure_array(value).map { |item| item.nil? ? nil : of_type.coerce_result(item, ctx) }
50
50
  end
51
51
 
52
+ def list?
53
+ true
54
+ end
55
+
52
56
  private
53
57
 
54
58
  def coerce_non_null_input(value, ctx)
@@ -62,7 +62,7 @@ module GraphQL
62
62
  end
63
63
  end
64
64
 
65
- def_delegators :@of_type, :coerce_input, :coerce_result
65
+ def_delegators :@of_type, :coerce_input, :coerce_result, :list?
66
66
 
67
67
  def kind
68
68
  GraphQL::TypeKinds::NON_NULL
@@ -72,5 +72,9 @@ module GraphQL
72
72
  "#{of_type.to_s}!"
73
73
  end
74
74
  alias_method :inspect, :to_s
75
+
76
+ def non_null?
77
+ true
78
+ end
75
79
  end
76
80
  end
@@ -137,6 +137,9 @@ module GraphQL
137
137
  end
138
138
  end
139
139
 
140
+ # @api private
141
+ attr_reader :result_values
142
+
140
143
  def fragments
141
144
  with_prepared_ast { @fragments }
142
145
  end
@@ -24,6 +24,7 @@ module GraphQL
24
24
  GraphQL::StaticValidation::ArgumentsAreDefined,
25
25
  GraphQL::StaticValidation::ArgumentLiteralsAreCompatible,
26
26
  GraphQL::StaticValidation::RequiredArgumentsArePresent,
27
+ GraphQL::StaticValidation::VariableNamesAreUnique,
27
28
  GraphQL::StaticValidation::VariablesAreInputTypes,
28
29
  GraphQL::StaticValidation::VariableDefaultValuesAreCorrectlyTyped,
29
30
  GraphQL::StaticValidation::VariablesAreUsedAndDefined,
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ class VariableNamesAreUnique
5
+ include GraphQL::StaticValidation::Message::MessageHelper
6
+
7
+ def validate(context)
8
+ context.visitor[GraphQL::Language::Nodes::OperationDefinition] << ->(node, parent) {
9
+ var_defns = node.variables
10
+ if var_defns.any?
11
+ vars_by_name = Hash.new { |h, k| h[k] = [] }
12
+ var_defns.each { |v| vars_by_name[v.name] << v }
13
+ vars_by_name.each do |name, defns|
14
+ if defns.size > 1
15
+ context.errors << message("There can only be one variable named \"#{name}\"", defns, context: context)
16
+ end
17
+ end
18
+ end
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require "securerandom"
2
3
  require "graphql/subscriptions/event"
3
4
  require "graphql/subscriptions/instrumentation"
4
5
  require "graphql/subscriptions/serialize"
@@ -133,5 +134,10 @@ module GraphQL
133
134
  def write_subscription(query, events)
134
135
  raise NotImplementedError
135
136
  end
137
+
138
+ # @return [String] A new unique identifier for a subscription
139
+ def build_id
140
+ SecureRandom.uuid
141
+ end
136
142
  end
137
143
  end
@@ -1,6 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require "securerandom"
3
-
4
2
  module GraphQL
5
3
  class Subscriptions
6
4
  # A subscriptions implementation that sends data
@@ -41,7 +39,7 @@ module GraphQL
41
39
  # })
42
40
  #
43
41
  # payload = {
44
- # result: result.to_h,
42
+ # result: result.subscription? ? nil : result.to_h,
45
43
  # more: result.subscription?,
46
44
  # }
47
45
  #
@@ -93,7 +91,7 @@ module GraphQL
93
91
  # and re-evaluate the query locally.
94
92
  def write_subscription(query, events)
95
93
  channel = query.context[:channel]
96
- subscription_id = query.context[:subscription_id] ||= SecureRandom.uuid
94
+ subscription_id = query.context[:subscription_id] ||= build_id
97
95
  stream = query.context[:action_cable_stream] ||= SUBSCRIPTION_PREFIX + subscription_id
98
96
  channel.stream_from(stream)
99
97
  @subscriptions[subscription_id] = query
@@ -12,21 +12,13 @@ module GraphQL
12
12
  # @return [Object] An object equivalent to the one passed to {.dump}
13
13
  def load(str)
14
14
  parsed_obj = JSON.parse(str)
15
- if parsed_obj.is_a?(Hash) && parsed_obj.size == 1 && parsed_obj.key?(GLOBALID_KEY)
16
- GlobalID::Locator.locate(parsed_obj[GLOBALID_KEY])
17
- else
18
- parsed_obj
19
- end
15
+ load_value(parsed_obj)
20
16
  end
21
17
 
22
18
  # @param obj [Object] Some subscription-related data to dump
23
19
  # @return [String] The stringified object
24
20
  def dump(obj)
25
- if obj.respond_to?(:to_gid_param)
26
- JSON.dump(GLOBALID_KEY => obj.to_gid_param)
27
- else
28
- JSON.generate(obj, quirks_mode: true)
29
- end
21
+ JSON.generate(dump_value(obj), quirks_mode: true)
30
22
  end
31
23
 
32
24
  # This is for turning objects into subscription scopes.
@@ -47,6 +39,40 @@ module GraphQL
47
39
  obj.to_s
48
40
  end
49
41
  end
42
+
43
+ class << self
44
+ private
45
+
46
+ # @param value [Object] A parsed JSON object
47
+ # @return [Object] An object that load Global::Identification recursive
48
+ def load_value(value)
49
+ if value.is_a?(Array)
50
+ value.map{|item| load_value(item)}
51
+ elsif value.is_a?(Hash)
52
+ if value.size == 1 && value.key?(GLOBALID_KEY)
53
+ GlobalID::Locator.locate(value[GLOBALID_KEY])
54
+ else
55
+ Hash[value.map{|k, v| [k, load_value(v)]}]
56
+ end
57
+ else
58
+ value
59
+ end
60
+ end
61
+
62
+ # @param obj [Object] Some subscription-related data to dump
63
+ # @return [Object] The object that converted Global::Identification
64
+ def dump_value(obj)
65
+ if obj.is_a?(Array)
66
+ obj.map{|item| dump_value(item)}
67
+ elsif obj.is_a?(Hash)
68
+ Hash[obj.map{|k, v| [k, dump_value(v)]}]
69
+ elsif obj.respond_to?(:to_gid_param)
70
+ {GLOBALID_KEY => obj.to_gid_param}
71
+ else
72
+ obj
73
+ end
74
+ end
75
+ end
50
76
  end
51
77
  end
52
78
  end
@@ -3,9 +3,6 @@
3
3
  module GraphQL
4
4
  module Tracing
5
5
  class ScoutTracing < PlatformTracing
6
- if defined?(ScoutApm)
7
- include ScoutApm::Tracer
8
- end
9
6
  INSTRUMENT_OPTS = { scope: true }
10
7
 
11
8
  self.platform_keys = {
@@ -19,6 +16,11 @@ module GraphQL
19
16
  "execute_query_lazy" => "execute.graphql",
20
17
  }
21
18
 
19
+ def initialize
20
+ self.class.include ScoutApm::Tracer
21
+ super
22
+ end
23
+
22
24
  def platform_trace(platform_key, key, data)
23
25
  self.class.instrument("GraphQL", platform_key, INSTRUMENT_OPTS) do
24
26
  yield
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.7.4"
3
+ VERSION = "1.7.5"
4
4
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
2
4
  gem 'rails'
3
5
  gem 'puma'
4
6
  gem 'capybara'
5
7
  gem 'selenium-webdriver'
6
- gem 'graphql', path: File.expand_path("../../", __dir__)
8
+ gem 'graphql', path: File.expand_path('../../', __dir__)
7
9
 
8
10
  group :development do
9
- gem "listen"
11
+ gem 'listen'
10
12
  end
@@ -10,7 +10,6 @@ require "action_view/railtie"
10
10
  require "action_cable/engine"
11
11
  require "sprockets/railtie"
12
12
  require "rails/test_unit/railtie"
13
- require "puma"
14
13
 
15
14
  # Require the gems listed in Gemfile, including any gems
16
15
  # you've limited to :test, :development, or :production.
@@ -64,6 +64,13 @@ RUBY
64
64
  assert_file "app/controllers/graphql_controller.rb", EXPECTED_GRAPHQLS_CONTROLLER
65
65
  end
66
66
 
67
+ test "it allows for a user-specified install directory" do
68
+ run_generator(["--directory", "app/mydirectory"])
69
+
70
+ assert_file "app/mydirectory/types/.keep"
71
+ assert_file "app/mydirectory/mutations/.keep"
72
+ end
73
+
67
74
  test "it generates graphql-batch and relay boilerplate" do
68
75
  run_generator(["--batch", "--relay"])
69
76
  assert_file "app/graphql/loaders/.keep"
@@ -7,7 +7,7 @@ class GraphQLGeneratorsMutationGeneratorTest < BaseGeneratorTest
7
7
 
8
8
  destination File.expand_path("../../../tmp/dummy", File.dirname(__FILE__))
9
9
 
10
- setup do
10
+ def setup(directory = "app/graphql")
11
11
  prepare_destination
12
12
  FileUtils.cd(File.expand_path("../../../tmp", File.dirname(__FILE__))) do
13
13
  `rm -rf dummy`
@@ -15,11 +15,12 @@ class GraphQLGeneratorsMutationGeneratorTest < BaseGeneratorTest
15
15
  end
16
16
 
17
17
  FileUtils.cd(destination_root) do
18
- `rails g graphql:install`
18
+ `rails g graphql:install --directory #{directory}`
19
19
  end
20
20
  end
21
21
 
22
22
  test "it generates an empty resolver by name" do
23
+ setup
23
24
  run_generator(["UpdateName"])
24
25
 
25
26
  expected_content = <<-RUBY
@@ -39,4 +40,26 @@ RUBY
39
40
 
40
41
  assert_file "app/graphql/mutations/update_name.rb", expected_content
41
42
  end
43
+
44
+ test "it allows for user-specified directory" do
45
+ setup "app/mydirectory"
46
+ run_generator(["UpdateName", "--directory", "app/mydirectory"])
47
+
48
+ expected_content = <<-RUBY
49
+ Mutations::UpdateName = GraphQL::Relay::Mutation.define do
50
+ name "UpdateName"
51
+ # TODO: define return fields
52
+ # return_field :post, Types::PostType
53
+
54
+ # TODO: define arguments
55
+ # input_field :name, !types.String
56
+
57
+ resolve ->(obj, args, ctx) {
58
+ # TODO: define resolve function
59
+ }
60
+ end
61
+ RUBY
62
+
63
+ assert_file "app/mydirectory/mutations/update_name.rb", expected_content
64
+ end
42
65
  end
@@ -47,4 +47,18 @@ RUBY
47
47
  assert_file "app/graphql/types/winged_creature_type.rb", expected_content
48
48
  end
49
49
  end
50
+
51
+ test "it accepts a user-specified directory" do
52
+ command = ["WingedCreature", "--directory", "app/mydirectory"]
53
+
54
+ expected_content = <<-RUBY
55
+ Types::WingedCreatureType = GraphQL::UnionType.define do
56
+ name "WingedCreature"
57
+ end
58
+ RUBY
59
+
60
+ prepare_destination
61
+ run_generator(command)
62
+ assert_file "app/mydirectory/types/winged_creature_type.rb", expected_content
63
+ end
50
64
  end
@@ -21,6 +21,15 @@ describe GraphQL::Backtrace do
21
21
  def inspect; nil; end
22
22
  end
23
23
 
24
+ class ErrorInstrumentation
25
+ def self.before_query(_query)
26
+ end
27
+
28
+ def self.after_query(query)
29
+ raise "Instrumentation Boom"
30
+ end
31
+ end
32
+
24
33
  let(:resolvers) {
25
34
  {
26
35
  "Query" => {
@@ -113,7 +122,7 @@ describe GraphQL::Backtrace do
113
122
  assert_includes err.message, rendered_table
114
123
  # The message includes the original error message
115
124
  assert_includes err.message, "This is broken: Boom"
116
- assert_includes err.message, "spec/graphql/backtrace_spec.rb:33", "It includes the original backtrace"
125
+ assert_includes err.message, "spec/graphql/backtrace_spec.rb:42", "It includes the original backtrace"
117
126
  assert_includes err.message, "more lines"
118
127
  end
119
128
 
@@ -165,6 +174,17 @@ describe GraphQL::Backtrace do
165
174
 
166
175
  assert_includes(err.message, rendered_table)
167
176
  end
177
+
178
+
179
+ it "raises original exception instead of a TracedError when error does not occur during resolving" do
180
+ instrumentation_schema = schema.redefine do
181
+ instrument(:query, ErrorInstrumentation)
182
+ end
183
+
184
+ assert_raises(RuntimeError) {
185
+ instrumentation_schema.execute(GraphQL::Introspection::INTROSPECTION_QUERY, context: { backtrace: true })
186
+ }
187
+ end
168
188
  end
169
189
 
170
190
  # This will get brittle when execution code moves between files
@@ -82,4 +82,46 @@ TYPE
82
82
  assert_equal expected.chomp, post_type.to_definition(schema)
83
83
  end
84
84
  end
85
+
86
+ describe 'non_null?' do
87
+ let(:type) do
88
+ GraphQL::EnumType.define do
89
+ name "Hello"
90
+ value 'WORLD'
91
+ end
92
+ end
93
+
94
+ it "returns false for nullable types" do
95
+ assert_equal(type.non_null?, false)
96
+ end
97
+
98
+ it "returns true for non-nulls" do
99
+ assert_equal(type.to_non_null_type.non_null?, true)
100
+ end
101
+
102
+ it "returns false for a nullable list of non-nulls" do
103
+ assert_equal(type.to_non_null_type.to_list_type.non_null?, false)
104
+ end
105
+ end
106
+
107
+ describe 'list?' do
108
+ let(:type) do
109
+ GraphQL::EnumType.define do
110
+ name "Hello"
111
+ value 'WORLD'
112
+ end
113
+ end
114
+
115
+ it "returns false for non-list types" do
116
+ assert_equal(type.list?, false)
117
+ end
118
+
119
+ it "returns true for lists" do
120
+ assert_equal(type.to_list_type.list?, true)
121
+ end
122
+
123
+ it "returns true for a non-nullable list" do
124
+ assert_equal(type.to_list_type.to_non_null_type.list?, true)
125
+ end
126
+ end
85
127
  end