graph_weaver 0.5.1 → 0.6.0
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/CHANGELOG.md +409 -0
- data/Gemfile.lock +19 -19
- data/README.md +74 -53
- data/docs/cassettes.md +6 -1
- data/docs/editors.md +3 -1
- data/docs/errors.md +73 -16
- data/docs/federation.md +201 -151
- data/docs/generated_modules.md +222 -165
- data/docs/getting_started.md +105 -81
- data/docs/logging.md +34 -4
- data/docs/scalars.md +119 -24
- data/docs/testing.md +191 -151
- data/docs/transports.md +47 -19
- data/docs/upgrading.md +210 -11
- data/lib/generators/graph_weaver/install_generator.rb +16 -1
- data/lib/graph_weaver/client.rb +46 -13
- data/lib/graph_weaver/codegen/aliases.rb +5 -4
- data/lib/graph_weaver/codegen/emit.rb +96 -39
- data/lib/graph_weaver/codegen/enum_type.rb +3 -0
- data/lib/graph_weaver/codegen/nodes.rb +42 -21
- data/lib/graph_weaver/codegen/scalar_type.rb +82 -79
- data/lib/graph_weaver/codegen/type_helpers.rb +1 -0
- data/lib/graph_weaver/codegen.rb +284 -84
- data/lib/graph_weaver/coerce.rb +113 -0
- data/lib/graph_weaver/errors.rb +30 -7
- data/lib/graph_weaver/federation.rb +6 -5
- data/lib/graph_weaver/hints.rb +76 -2
- data/lib/graph_weaver/in_process.rb +11 -8
- data/lib/graph_weaver/inflect.rb +2 -0
- data/lib/graph_weaver/input_struct.rb +115 -12
- data/lib/graph_weaver/internal/overrides.rb +101 -0
- data/lib/graph_weaver/internal/planner.rb +868 -0
- data/lib/graph_weaver/internal/schemas.rb +50 -0
- data/lib/graph_weaver/internal/selection.rb +127 -0
- data/lib/graph_weaver/{testing → internal}/subgraphs.rb +39 -41
- data/lib/graph_weaver/internal/values.rb +181 -0
- data/lib/graph_weaver/internal.rb +206 -0
- data/lib/graph_weaver/logging.rb +108 -20
- data/lib/graph_weaver/parsing.rb +5 -4
- data/lib/graph_weaver/query_module.rb +2 -0
- data/lib/graph_weaver/railtie.rb +113 -14
- data/lib/graph_weaver/representation.rb +30 -2
- data/lib/graph_weaver/response.rb +15 -0
- data/lib/graph_weaver/retry.rb +54 -22
- data/lib/graph_weaver/rspec.rb +50 -11
- data/lib/graph_weaver/schema_diff.rb +293 -0
- data/lib/graph_weaver/schema_loader.rb +96 -29
- data/lib/graph_weaver/tasks.rb +78 -29
- data/lib/graph_weaver/testing/cassette.rb +49 -65
- data/lib/graph_weaver/testing/coverage.rb +5 -4
- data/lib/graph_weaver/testing/failure.rb +10 -6
- data/lib/graph_weaver/testing/fake_client.rb +253 -60
- data/lib/graph_weaver/testing/fake_subgraph.rb +19 -8
- data/lib/graph_weaver/testing/router.rb +94 -808
- data/lib/graph_weaver/testing.rb +35 -84
- data/lib/graph_weaver/transport/faraday.rb +1 -1
- data/lib/graph_weaver/transport/http.rb +29 -12
- data/lib/graph_weaver/transport.rb +11 -34
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +188 -110
- metadata +10 -5
- data/lib/graph_weaver/schemas.rb +0 -48
- data/lib/graph_weaver/selection.rb +0 -120
- data/lib/graph_weaver/testing/values.rb +0 -98
data/lib/graph_weaver/testing.rb
CHANGED
|
@@ -19,27 +19,32 @@ end
|
|
|
19
19
|
# GraphWeaver::Testing.configure do |config|
|
|
20
20
|
# config.schema = MySchema # overrides the derived schema
|
|
21
21
|
# config.router = { subgraphs: { "reviews" => :fake } } # or supergraph:,
|
|
22
|
-
# # when it isn't the dump
|
|
22
|
+
# # when it isn't the dump, or
|
|
23
|
+
# # fake: for how those fabricate
|
|
23
24
|
# config.context = { current_user: } # baseline GraphQL context
|
|
24
25
|
# config.default_mode = :fake # untagged examples (graph_weaver/rspec)
|
|
25
26
|
# config.seed = 42 # reproducible fakes
|
|
26
|
-
# config.mode = :faker # or :literal; nil = auto
|
|
27
27
|
# config.overrides = { "Person.name" => "Daniel" }
|
|
28
28
|
# config.list_size = 2..4
|
|
29
|
-
# config.null_chance = 0.1 # nullable fields go nil sometimes
|
|
30
29
|
# config.cassette_dir = "spec/cassettes"
|
|
31
30
|
# end
|
|
32
31
|
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
32
|
+
# Anything whose honest answer differs per example belongs on the fake, not
|
|
33
|
+
# here: graphql_fake(null_chance: 1.0) for the empty-state example,
|
|
34
|
+
# graphql_fake(values: :literal) for the one that reads better without
|
|
35
|
+
# faker's prose, and overrides:/list_size: to say what one example's data is.
|
|
37
36
|
#
|
|
38
37
|
# rspec users: require "graph_weaver/rspec" instead — it hooks the suite
|
|
39
38
|
# (seed from rspec, a client per example from its `graphql:` tag).
|
|
40
39
|
module GraphWeaver
|
|
41
40
|
module Testing
|
|
42
|
-
|
|
41
|
+
# How a fake writes its values, named by the `values:` a fake takes —
|
|
42
|
+
# graphql_fake(values: :literal), FakeClient.new(values:):
|
|
43
|
+
#
|
|
44
|
+
# :faker semantic, matched on the field name (needs the faker gem)
|
|
45
|
+
# :literal plain type-derived values ("name-1", seeded numbers)
|
|
46
|
+
# nil auto: :faker when the gem is loaded, else :literal
|
|
47
|
+
VALUE_STYLES = %i[faker literal].freeze
|
|
43
48
|
|
|
44
49
|
# What an example can run against, named by the rspec tag that selects
|
|
45
50
|
# it — `it "…", graphql: :in_process` (see graph_weaver/rspec):
|
|
@@ -50,19 +55,17 @@ module GraphWeaver
|
|
|
50
55
|
CLIENT_MODES = %i[fake in_process router].freeze
|
|
51
56
|
|
|
52
57
|
class Config
|
|
53
|
-
attr_accessor :overrides, :seed, :list_size, :
|
|
58
|
+
attr_accessor :overrides, :seed, :list_size, :cassette_dir, :context,
|
|
54
59
|
:record, :anonymize
|
|
55
60
|
# #schema is written plainly and read with a fallback (below), the way
|
|
56
|
-
# #
|
|
61
|
+
# #router and #default_mode are read plainly and written with a check
|
|
57
62
|
attr_writer :schema
|
|
58
|
-
attr_reader :
|
|
63
|
+
attr_reader :router, :default_mode
|
|
59
64
|
|
|
60
65
|
def initialize
|
|
61
66
|
@overrides = {}
|
|
62
67
|
@seed = nil
|
|
63
68
|
@list_size = 1..3
|
|
64
|
-
@null_chance = 0.0
|
|
65
|
-
@mode = nil # auto
|
|
66
69
|
@schema = nil
|
|
67
70
|
@located = nil # the committed dump, once located
|
|
68
71
|
# not under spec/fixtures: `fixtures :all` globs that path for
|
|
@@ -100,14 +103,6 @@ module GraphWeaver
|
|
|
100
103
|
# that never asks for one.
|
|
101
104
|
def explicit_schema = @schema
|
|
102
105
|
|
|
103
|
-
def mode=(mode)
|
|
104
|
-
unless mode.nil? || MODES.include?(mode)
|
|
105
|
-
raise ArgumentError, "mode: must be one of #{MODES.inspect} (or nil for auto), got #{mode.inspect}"
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
@mode = mode
|
|
109
|
-
end
|
|
110
|
-
|
|
111
106
|
def default_mode=(mode)
|
|
112
107
|
unless mode.nil? || CLIENT_MODES.include?(mode)
|
|
113
108
|
raise ArgumentError,
|
|
@@ -118,13 +113,14 @@ module GraphWeaver
|
|
|
118
113
|
@default_mode = mode
|
|
119
114
|
end
|
|
120
115
|
|
|
121
|
-
# Router arguments —
|
|
116
|
+
# Router arguments — every key optional, and each answers a different
|
|
122
117
|
# question. `supergraph:` is for one derivation can't find; without it
|
|
123
118
|
# the conventional dump is used, when that dump is itself a supergraph.
|
|
124
119
|
# `subgraphs:` is for what derivation can't settle, or for `"reviews"
|
|
125
120
|
# => :fake`, which fabricates a subgraph this process doesn't serve —
|
|
126
121
|
# the commonest reason to configure a router at all, and no reason to
|
|
127
|
-
# have to restate where the supergraph is.
|
|
122
|
+
# have to restate where the supergraph is. `fake:` says how those
|
|
123
|
+
# fabricate; graphql_router(fake: …) says it for one example.
|
|
128
124
|
def router=(arguments)
|
|
129
125
|
unless arguments.nil? || arguments.is_a?(Hash)
|
|
130
126
|
raise ArgumentError, "router: must be the arguments to build one, e.g. " \
|
|
@@ -137,7 +133,7 @@ module GraphWeaver
|
|
|
137
133
|
raise ArgumentError, "router: context: is set as config.context — the baseline every " \
|
|
138
134
|
":in_process and :router example starts from"
|
|
139
135
|
end
|
|
140
|
-
unknown = (arguments&.keys || []) - %i[supergraph subgraphs]
|
|
136
|
+
unknown = (arguments&.keys || []) - %i[supergraph subgraphs fake]
|
|
141
137
|
raise ArgumentError, "router: doesn't take #{unknown.join(", ")}" if unknown.any?
|
|
142
138
|
|
|
143
139
|
@router = arguments
|
|
@@ -148,14 +144,18 @@ module GraphWeaver
|
|
|
148
144
|
# #context is settable, so an example that runs as someone else sets
|
|
149
145
|
# that rather than rebuilding — the rspec hook resets it each time.
|
|
150
146
|
def built_router
|
|
151
|
-
@built_router ||= Router.new(
|
|
147
|
+
@built_router ||= Router.new(
|
|
148
|
+
supergraph: supergraph!,
|
|
149
|
+
subgraphs: @router && @router[:subgraphs],
|
|
150
|
+
fake: (@router && @router[:fake]) || {},
|
|
151
|
+
)
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
# The composed supergraph :router plans against — named, or the
|
|
155
155
|
# conventional dump when that's what it is. A client can't supply
|
|
156
156
|
# one: its schema is the API schema a router serves, with the
|
|
157
157
|
# @join__* routing table stripped out.
|
|
158
|
-
def supergraph!
|
|
158
|
+
private def supergraph!
|
|
159
159
|
return @router[:supergraph] if @router&.key?(:supergraph)
|
|
160
160
|
|
|
161
161
|
path = GraphWeaver::SchemaLoader.locate_path
|
|
@@ -175,7 +175,7 @@ module GraphWeaver
|
|
|
175
175
|
# explicit_schema, not schema: the latter falls back to the committed
|
|
176
176
|
# dump, which loads as an anonymous GraphQL::Schema subclass — runnable
|
|
177
177
|
# by every test that matters, and holding not one resolver.
|
|
178
|
-
runnable(explicit_schema) || GraphWeaver.live_schema ||
|
|
178
|
+
runnable(explicit_schema) || GraphWeaver::Internal::Util.live_schema ||
|
|
179
179
|
raise(GraphWeaver::Error, ":in_process runs your resolvers, so it needs the live " \
|
|
180
180
|
"GraphQL::Schema class — and GraphWeaver.client isn't running one in-process to " \
|
|
181
181
|
"borrow. Name it in the example — graphql_in_process(MySchema) — or set " \
|
|
@@ -219,81 +219,32 @@ module GraphWeaver
|
|
|
219
219
|
yield config
|
|
220
220
|
# a typo'd override key pins nothing and the test still passes, so
|
|
221
221
|
# catch it here — while the block that set it is still on the stack
|
|
222
|
-
|
|
222
|
+
Internal::Overrides.validate!(config.explicit_schema, config.overrides) if config.explicit_schema
|
|
223
223
|
config
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
-
# Override keys name schema coordinates: "Type.field", or a bare field
|
|
227
|
-
# name matching that field on any type. Anything else is a typo that
|
|
228
|
-
# would silently fabricate random data instead of pinning a value.
|
|
229
|
-
def validate_overrides!(schema, overrides)
|
|
230
|
-
overrides.each_key { |key| validate_override_key!(schema, key.to_s) }
|
|
231
|
-
end
|
|
232
|
-
|
|
233
226
|
# back to defaults — between tests, or to undo an experiment
|
|
234
227
|
def reset!
|
|
235
228
|
@config = nil
|
|
236
229
|
end
|
|
237
230
|
|
|
238
231
|
# resolve a cassette name ("github") against cassette_dir; paths
|
|
239
|
-
# with separators or extensions
|
|
232
|
+
# with separators or extensions name the file themselves
|
|
240
233
|
def cassette_path(name)
|
|
241
|
-
return name if name.include?("/") || name.end_with?(".yml", ".yaml")
|
|
234
|
+
return Internal::Util.resolve(name) if name.include?("/") || name.end_with?(".yml", ".yaml")
|
|
242
235
|
|
|
243
236
|
File.join(cassette_dir, "#{name}.yml")
|
|
244
237
|
end
|
|
245
238
|
|
|
246
|
-
# The configured directory
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
# gem doesn't depend on.
|
|
250
|
-
def cassette_dir
|
|
251
|
-
dir = config.cassette_dir
|
|
252
|
-
root = (Object.const_get(:Rails).root if Object.const_defined?(:Rails))
|
|
253
|
-
root ? File.join(root.to_s, dir) : dir
|
|
254
|
-
rescue NoMethodError
|
|
255
|
-
dir # something else named Rails
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
private
|
|
259
|
-
|
|
260
|
-
def validate_override_key!(schema, key)
|
|
261
|
-
type_name, field_name = key.split(".", 2)
|
|
262
|
-
# introspection fields (__typename) are real but absent from #fields
|
|
263
|
-
return if (field_name || type_name).start_with?("__")
|
|
264
|
-
|
|
265
|
-
if field_name.nil?
|
|
266
|
-
known = field_names(schema)
|
|
267
|
-
return if known.include?(type_name)
|
|
268
|
-
|
|
269
|
-
bad_override!(key, "matches no field in this schema", known, type_name)
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
type = schema.get_type(type_name)
|
|
273
|
-
unless type.respond_to?(:fields)
|
|
274
|
-
bad_override!(key, "names no object type in this schema", schema.types.keys, type_name)
|
|
275
|
-
end
|
|
276
|
-
return if type.fields.key?(field_name)
|
|
277
|
-
|
|
278
|
-
bad_override!(key, "is not a field of #{type_name}", type.fields.keys, field_name)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def bad_override!(key, problem, dictionary, term)
|
|
282
|
-
suggestion = GraphWeaver.did_you_mean(dictionary, term)
|
|
283
|
-
hint = suggestion ? " — did you mean '#{suggestion}'?" : ""
|
|
284
|
-
raise GraphWeaver::Error, "override key #{key.inspect} #{problem}#{hint}"
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Every output field name in the schema — walked only when a bare key
|
|
288
|
-
# asks for it.
|
|
289
|
-
def field_names(schema)
|
|
290
|
-
schema.types.each_value.flat_map { |type| type.respond_to?(:fields) ? type.fields.keys : [] }.uniq
|
|
291
|
-
end
|
|
239
|
+
# The configured directory as a real path — a rake task or an rspec run
|
|
240
|
+
# starts from wherever it starts from; the cassettes don't move.
|
|
241
|
+
def cassette_dir = Internal::Util.resolve(config.cassette_dir)
|
|
292
242
|
end
|
|
293
243
|
end
|
|
294
244
|
end
|
|
295
245
|
|
|
296
|
-
require_relative "
|
|
246
|
+
require_relative "internal/overrides"
|
|
247
|
+
require_relative "internal/values"
|
|
297
248
|
require_relative "testing/fake_client"
|
|
298
249
|
require_relative "testing/fake_subgraph"
|
|
299
250
|
require_relative "testing/failure"
|
|
@@ -61,7 +61,7 @@ module GraphWeaver
|
|
|
61
61
|
# which adapter got picked decides socket reuse — Faraday's
|
|
62
62
|
# default net_http one opens a connection per request. Naming it
|
|
63
63
|
# is the cheapest way to make that discoverable.
|
|
64
|
-
GraphWeaver.log(:info) { "faraday transport #{@url} (adapter: #{@connection.builder.adapter})" }
|
|
64
|
+
GraphWeaver::Internal::Log.log(:info) { "faraday transport #{@url} (adapter: #{@connection.builder.adapter})" }
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
private
|
|
@@ -39,7 +39,18 @@ module GraphWeaver
|
|
|
39
39
|
raise ArgumentError, "pool_size: must be >= 1" unless pool_size >= 1
|
|
40
40
|
|
|
41
41
|
@url = url
|
|
42
|
-
|
|
42
|
+
# a scheme-less url parses as a *path* with no host, and the failure
|
|
43
|
+
# then lands as far away as Net::HTTP.start(nil, nil)
|
|
44
|
+
uri = begin
|
|
45
|
+
URI(url)
|
|
46
|
+
rescue URI::InvalidURIError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
unless uri && %w[http https].include?(uri.scheme)
|
|
50
|
+
raise ArgumentError, "expected an http(s) url, got #{url.inspect}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@uri = uri
|
|
43
54
|
@headers = headers
|
|
44
55
|
@open_timeout = open_timeout
|
|
45
56
|
@read_timeout = read_timeout
|
|
@@ -63,7 +74,7 @@ module GraphWeaver
|
|
|
63
74
|
# live connections, LIFO — a warm socket beats opening a cold one,
|
|
64
75
|
# so a single-threaded caller keeps reusing the same one
|
|
65
76
|
@idle = []
|
|
66
|
-
@
|
|
77
|
+
@lock = Mutex.new # guards @idle and @saturated
|
|
67
78
|
end
|
|
68
79
|
|
|
69
80
|
private
|
|
@@ -84,12 +95,14 @@ module GraphWeaver
|
|
|
84
95
|
# is the concurrency ceiling.
|
|
85
96
|
def with_connection
|
|
86
97
|
acquire_permit
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
# nothing between acquiring the permit and the ensure that returns it:
|
|
99
|
+
# an async interrupt (Rack::Timeout, a fiber cancel) landing in that
|
|
100
|
+
# gap would leak a permit, shrinking the pool for the process's life
|
|
89
101
|
begin
|
|
90
|
-
http =
|
|
102
|
+
http = nil
|
|
103
|
+
http = @lock.synchronize { @idle.pop } || connect
|
|
91
104
|
result = yield http
|
|
92
|
-
@
|
|
105
|
+
@lock.synchronize { @idle.push(http) }
|
|
93
106
|
result
|
|
94
107
|
rescue Exception
|
|
95
108
|
# socket state is unknown — drop it, leaving the slot empty so
|
|
@@ -107,14 +120,19 @@ module GraphWeaver
|
|
|
107
120
|
# when idle past keep_alive_timeout, so a server-closed keep-alive
|
|
108
121
|
# socket doesn't produce spurious failures.
|
|
109
122
|
def connect
|
|
110
|
-
GraphWeaver.log(:debug) { "connecting to #{@uri.hostname}:#{@uri.port}" }
|
|
111
|
-
Net::HTTP.start(
|
|
123
|
+
GraphWeaver::Internal::Log.log(:debug) { "connecting to #{@uri.hostname}:#{@uri.port}" }
|
|
124
|
+
http = Net::HTTP.start(
|
|
112
125
|
@uri.hostname, @uri.port,
|
|
113
126
|
use_ssl: @uri.scheme == "https",
|
|
114
127
|
open_timeout: @open_timeout, read_timeout: @read_timeout,
|
|
115
128
|
keep_alive_timeout: @keep_alive_timeout,
|
|
116
129
|
**@ssl,
|
|
117
130
|
)
|
|
131
|
+
# net/http otherwise returns the bytes that did arrive, so a
|
|
132
|
+
# connection dying mid-body reads as a server answering with garbage —
|
|
133
|
+
# a permanent ServerError — instead of the retriable EOFError it is
|
|
134
|
+
http.ignore_eof = false
|
|
135
|
+
http
|
|
118
136
|
end
|
|
119
137
|
|
|
120
138
|
# Take a permit, saying so when none is free. A queued request is
|
|
@@ -129,9 +147,8 @@ module GraphWeaver
|
|
|
129
147
|
@permits.pop
|
|
130
148
|
waited = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round
|
|
131
149
|
|
|
132
|
-
first = !@saturated
|
|
133
|
-
|
|
134
|
-
GraphWeaver.log(first ? :warn : :debug) do
|
|
150
|
+
first = @lock.synchronize { !@saturated && (@saturated = true) }
|
|
151
|
+
GraphWeaver::Internal::Log.log(first ? :warn : :debug) do
|
|
135
152
|
"connection pool saturated: waited #{waited}ms for 1 of #{@pool_size} connections to " \
|
|
136
153
|
"#{@uri.hostname} — raise pool_size: to this process's concurrency"
|
|
137
154
|
end
|
|
@@ -140,7 +157,7 @@ module GraphWeaver
|
|
|
140
157
|
def disconnect(http)
|
|
141
158
|
return unless http
|
|
142
159
|
|
|
143
|
-
GraphWeaver.log(:debug) { "dropping connection to #{@uri.hostname}:#{@uri.port}" }
|
|
160
|
+
GraphWeaver::Internal::Log.log(:debug) { "dropping connection to #{@uri.hostname}:#{@uri.port}" }
|
|
144
161
|
http.finish if http.started?
|
|
145
162
|
rescue IOError
|
|
146
163
|
# already closed
|
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require "sorbet-runtime"
|
|
6
6
|
|
|
7
7
|
require_relative "errors"
|
|
8
|
+
require_relative "internal"
|
|
8
9
|
require_relative "version"
|
|
9
10
|
|
|
10
11
|
# Base class for the bundled network transports — Transport::HTTP
|
|
@@ -48,10 +49,10 @@ class GraphWeaver::Transport
|
|
|
48
49
|
# slow-query reports on. Generated modules pass their OPERATION_NAME;
|
|
49
50
|
# a raw query string falls back to the name in the document itself.
|
|
50
51
|
def execute(query, variables: {}, operation_name: nil)
|
|
51
|
-
operation_name ||= GraphWeaver::
|
|
52
|
+
operation_name ||= GraphWeaver::Internal::Wire.operation_name(query)
|
|
52
53
|
payload = { url:, operation: operation_name }
|
|
53
54
|
|
|
54
|
-
GraphWeaver.instrument(GraphWeaver::EXECUTE_EVENT, payload) do
|
|
55
|
+
GraphWeaver::Internal::Log.instrument(GraphWeaver::EXECUTE_EVENT, payload) do
|
|
55
56
|
perform(query, variables, operation_name, payload)
|
|
56
57
|
end
|
|
57
58
|
end
|
|
@@ -61,11 +62,13 @@ class GraphWeaver::Transport
|
|
|
61
62
|
private def perform(query, variables, operation_name, payload)
|
|
62
63
|
# tag pairs this request's log lines (threads interleave), and names
|
|
63
64
|
# the operation so the log says WHICH query, not just the url
|
|
64
|
-
tag = GraphWeaver.logger && GraphWeaver::
|
|
65
|
+
tag = GraphWeaver.logger && GraphWeaver::Internal::Wire.log_tag(operation_name)
|
|
65
66
|
|
|
66
|
-
# full query + variables at debug only — they can carry PII
|
|
67
|
-
GraphWeaver.
|
|
68
|
-
|
|
67
|
+
# full query + variables at debug only — they can carry PII, and the
|
|
68
|
+
# sensitive keys are scrubbed even there (GraphWeaver.filter_parameters)
|
|
69
|
+
GraphWeaver::Internal::Log.log(:debug) do
|
|
70
|
+
filtered = JSON.generate(GraphWeaver::Internal::Log.filter_variables(variables))
|
|
71
|
+
"POST #{url} #{tag} variables=#{filtered}\n#{GraphWeaver::Internal::Wire.truncate_for_log(query)}"
|
|
69
72
|
end
|
|
70
73
|
|
|
71
74
|
# camelCase because it's the graphql-over-http request field, not a
|
|
@@ -84,7 +87,7 @@ class GraphWeaver::Transport
|
|
|
84
87
|
# headers is optional: a third-party subclass returning the
|
|
85
88
|
# documented [status, body] pair simply has none
|
|
86
89
|
status, body, headers = begin
|
|
87
|
-
GraphWeaver.log_timed(:debug, "POST #{url} #{tag} completed") do
|
|
90
|
+
GraphWeaver::Internal::Log.log_timed(:debug, "POST #{url} #{tag} completed") do
|
|
88
91
|
post(encoded)
|
|
89
92
|
end
|
|
90
93
|
rescue *GraphWeaver.transport_errors.to_a => e
|
|
@@ -93,7 +96,7 @@ class GraphWeaver::Transport
|
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
payload[:status] = status
|
|
96
|
-
GraphWeaver.log(:debug) { "HTTP #{status} #{tag} from #{url} (#{body.to_s.bytesize} bytes)" }
|
|
99
|
+
GraphWeaver::Internal::Log.log(:debug) { "HTTP #{status} #{tag} from #{url} (#{body.to_s.bytesize} bytes)" }
|
|
97
100
|
|
|
98
101
|
parsed = parse_body(body)
|
|
99
102
|
|
|
@@ -139,32 +142,6 @@ class GraphWeaver::Transport
|
|
|
139
142
|
end
|
|
140
143
|
alias to_s inspect
|
|
141
144
|
|
|
142
|
-
# The name of the document's FIRST operation, nil when anonymous. Only
|
|
143
|
-
# the fallback for a raw query string handed straight to a transport —
|
|
144
|
-
# generated modules pass their OPERATION_NAME, parsed properly.
|
|
145
|
-
OPERATION_NAME_PATTERN = /\A\s*(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/
|
|
146
|
-
def self.operation_name(query)
|
|
147
|
-
query[OPERATION_NAME_PATTERN, 1]
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# "[req 3 FilteredPokemon]" — a per-process request id plus the
|
|
151
|
-
# operation name, when there is one
|
|
152
|
-
REQUEST_MUTEX = Mutex.new
|
|
153
|
-
|
|
154
|
-
def self.log_tag(operation_name = nil)
|
|
155
|
-
id = REQUEST_MUTEX.synchronize { @request_count = (@request_count || 0) + 1 }
|
|
156
|
-
"[req #{id}#{" #{operation_name}" if operation_name}]"
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# keep debug readable: a 100-line introspection query would drown the
|
|
160
|
-
# log — the INFO introspection line already carries the timing
|
|
161
|
-
LOG_QUERY_LIMIT = 600
|
|
162
|
-
def self.truncate_for_log(query)
|
|
163
|
-
return query if query.length <= LOG_QUERY_LIMIT
|
|
164
|
-
|
|
165
|
-
"#{query[0, LOG_QUERY_LIMIT]}... (truncated, #{query.bytesize} bytes total)"
|
|
166
|
-
end
|
|
167
|
-
|
|
168
145
|
private
|
|
169
146
|
|
|
170
147
|
# POST the JSON body to the endpoint; return [status code, raw body]
|
data/lib/graph_weaver/version.rb
CHANGED