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