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.
- checksums.yaml +4 -4
- data/lib/generators/graphql/core.rb +13 -3
- data/lib/generators/graphql/enum_generator.rb +1 -1
- data/lib/generators/graphql/function_generator.rb +4 -1
- data/lib/generators/graphql/install_generator.rb +3 -3
- data/lib/generators/graphql/interface_generator.rb +1 -1
- data/lib/generators/graphql/loader_generator.rb +4 -1
- data/lib/generators/graphql/mutation_generator.rb +3 -3
- data/lib/generators/graphql/object_generator.rb +1 -1
- data/lib/generators/graphql/type_generator.rb +3 -0
- data/lib/generators/graphql/union_generator.rb +1 -1
- data/lib/graphql/backtrace/inspect_result.rb +1 -1
- data/lib/graphql/backtrace/table.rb +2 -2
- data/lib/graphql/backtrace/tracer.rb +7 -1
- data/lib/graphql/base_type.rb +10 -0
- data/lib/graphql/compatibility/query_parser_specification.rb +15 -0
- data/lib/graphql/execution/multiplex.rb +4 -0
- data/lib/graphql/language/parser.rb +595 -514
- data/lib/graphql/language/parser.y +21 -5
- data/lib/graphql/list_type.rb +4 -0
- data/lib/graphql/non_null_type.rb +5 -1
- data/lib/graphql/query.rb +3 -0
- data/lib/graphql/static_validation/all_rules.rb +1 -0
- data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +23 -0
- data/lib/graphql/subscriptions.rb +6 -0
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +2 -4
- data/lib/graphql/subscriptions/serialize.rb +36 -10
- data/lib/graphql/tracing/scout_tracing.rb +5 -3
- data/lib/graphql/version.rb +1 -1
- data/spec/dummy/Gemfile +4 -2
- data/spec/dummy/config/application.rb +0 -1
- data/spec/generators/graphql/install_generator_spec.rb +7 -0
- data/spec/generators/graphql/mutation_generator_spec.rb +25 -2
- data/spec/generators/graphql/union_generator_spec.rb +14 -0
- data/spec/graphql/backtrace_spec.rb +21 -1
- data/spec/graphql/base_type_spec.rb +42 -0
- data/spec/graphql/execution/multiplex_spec.rb +48 -0
- data/spec/graphql/field_spec.rb +14 -2
- data/spec/graphql/static_validation/rules/variable_names_are_unique_spec.rb +23 -0
- data/spec/graphql/subscriptions/serialize_spec.rb +53 -0
- data/spec/graphql/subscriptions_spec.rb +9 -2
- data/spec/graphql/tracing/scout_tracing_spec.rb +17 -0
- 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 */
|
82
|
-
| EQUALS
|
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
|
-
|
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
|
-
|
|
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:
|
data/lib/graphql/list_type.rb
CHANGED
@@ -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
|
data/lib/graphql/query.rb
CHANGED
@@ -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] ||=
|
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
|
-
|
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
|
-
|
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
|
data/lib/graphql/version.rb
CHANGED
data/spec/dummy/Gemfile
CHANGED
@@ -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(
|
8
|
+
gem 'graphql', path: File.expand_path('../../', __dir__)
|
7
9
|
|
8
10
|
group :development do
|
9
|
-
gem
|
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
|
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:
|
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
|