graph_weaver 0.5.0 → 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 +537 -0
- data/Gemfile.lock +19 -19
- data/README.md +74 -53
- data/docs/cassettes.md +29 -4
- data/docs/editors.md +3 -1
- data/docs/errors.md +75 -16
- data/docs/federation.md +206 -155
- data/docs/generated_modules.md +223 -166
- data/docs/getting_started.md +106 -82
- data/docs/logging.md +35 -5
- data/docs/scalars.md +119 -24
- data/docs/testing.md +196 -155
- data/docs/transports.md +47 -19
- data/docs/upgrading.md +243 -22
- data/graph_weaver.gemspec +16 -2
- data/lib/generators/graph_weaver/install_generator.rb +31 -16
- data/lib/graph_weaver/client.rb +52 -15
- data/lib/graph_weaver/codegen/aliases.rb +15 -8
- data/lib/graph_weaver/codegen/emit.rb +107 -42
- data/lib/graph_weaver/codegen/enum_type.rb +4 -3
- data/lib/graph_weaver/codegen/nodes.rb +42 -21
- data/lib/graph_weaver/codegen/scalar_type.rb +87 -83
- data/lib/graph_weaver/codegen/type_helpers.rb +2 -3
- data/lib/graph_weaver/codegen.rb +382 -105
- data/lib/graph_weaver/coerce.rb +113 -0
- data/lib/graph_weaver/errors.rb +57 -13
- data/lib/graph_weaver/federation.rb +10 -22
- 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 +45 -43
- 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 +6 -13
- 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 +63 -18
- data/lib/graph_weaver/schema_diff.rb +293 -0
- data/lib/graph_weaver/schema_loader.rb +126 -35
- data/lib/graph_weaver/tasks.rb +88 -36
- data/lib/graph_weaver/testing/cassette.rb +131 -78
- data/lib/graph_weaver/testing/coverage.rb +11 -15
- data/lib/graph_weaver/testing/failure.rb +14 -8
- 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 +147 -840
- data/lib/graph_weaver/testing.rb +40 -83
- 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 +221 -118
- metadata +17 -13
- data/CLAUDE.md +0 -161
- data/DECISIONS.md +0 -309
- data/Makefile +0 -23
- data/NOTES.md +0 -182
- data/PLAN.md +0 -115
- data/REVIEW.md +0 -946
- data/lib/graph_weaver/schemas.rb +0 -46
- 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,16 +55,17 @@ module GraphWeaver
|
|
|
50
55
|
CLIENT_MODES = %i[fake in_process router].freeze
|
|
51
56
|
|
|
52
57
|
class Config
|
|
53
|
-
attr_accessor :
|
|
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
|
|
61
|
+
# #router and #default_mode are read plainly and written with a check
|
|
62
|
+
attr_writer :schema
|
|
63
|
+
attr_reader :router, :default_mode
|
|
56
64
|
|
|
57
65
|
def initialize
|
|
58
66
|
@overrides = {}
|
|
59
67
|
@seed = nil
|
|
60
68
|
@list_size = 1..3
|
|
61
|
-
@null_chance = 0.0
|
|
62
|
-
@mode = nil # auto
|
|
63
69
|
@schema = nil
|
|
64
70
|
@located = nil # the committed dump, once located
|
|
65
71
|
# not under spec/fixtures: `fixtures :all` globs that path for
|
|
@@ -97,14 +103,6 @@ module GraphWeaver
|
|
|
97
103
|
# that never asks for one.
|
|
98
104
|
def explicit_schema = @schema
|
|
99
105
|
|
|
100
|
-
def mode=(mode)
|
|
101
|
-
unless mode.nil? || MODES.include?(mode)
|
|
102
|
-
raise ArgumentError, "mode: must be one of #{MODES.inspect} (or nil for auto), got #{mode.inspect}"
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
@mode = mode
|
|
106
|
-
end
|
|
107
|
-
|
|
108
106
|
def default_mode=(mode)
|
|
109
107
|
unless mode.nil? || CLIENT_MODES.include?(mode)
|
|
110
108
|
raise ArgumentError,
|
|
@@ -115,13 +113,14 @@ module GraphWeaver
|
|
|
115
113
|
@default_mode = mode
|
|
116
114
|
end
|
|
117
115
|
|
|
118
|
-
# Router arguments —
|
|
116
|
+
# Router arguments — every key optional, and each answers a different
|
|
119
117
|
# question. `supergraph:` is for one derivation can't find; without it
|
|
120
118
|
# the conventional dump is used, when that dump is itself a supergraph.
|
|
121
119
|
# `subgraphs:` is for what derivation can't settle, or for `"reviews"
|
|
122
120
|
# => :fake`, which fabricates a subgraph this process doesn't serve —
|
|
123
121
|
# the commonest reason to configure a router at all, and no reason to
|
|
124
|
-
# 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.
|
|
125
124
|
def router=(arguments)
|
|
126
125
|
unless arguments.nil? || arguments.is_a?(Hash)
|
|
127
126
|
raise ArgumentError, "router: must be the arguments to build one, e.g. " \
|
|
@@ -134,7 +133,7 @@ module GraphWeaver
|
|
|
134
133
|
raise ArgumentError, "router: context: is set as config.context — the baseline every " \
|
|
135
134
|
":in_process and :router example starts from"
|
|
136
135
|
end
|
|
137
|
-
unknown = (arguments&.keys || []) - %i[supergraph subgraphs]
|
|
136
|
+
unknown = (arguments&.keys || []) - %i[supergraph subgraphs fake]
|
|
138
137
|
raise ArgumentError, "router: doesn't take #{unknown.join(", ")}" if unknown.any?
|
|
139
138
|
|
|
140
139
|
@router = arguments
|
|
@@ -145,14 +144,18 @@ module GraphWeaver
|
|
|
145
144
|
# #context is settable, so an example that runs as someone else sets
|
|
146
145
|
# that rather than rebuilding — the rspec hook resets it each time.
|
|
147
146
|
def built_router
|
|
148
|
-
@built_router ||= Router.new(
|
|
147
|
+
@built_router ||= Router.new(
|
|
148
|
+
supergraph: supergraph!,
|
|
149
|
+
subgraphs: @router && @router[:subgraphs],
|
|
150
|
+
fake: (@router && @router[:fake]) || {},
|
|
151
|
+
)
|
|
149
152
|
end
|
|
150
153
|
|
|
151
154
|
# The composed supergraph :router plans against — named, or the
|
|
152
155
|
# conventional dump when that's what it is. A client can't supply
|
|
153
156
|
# one: its schema is the API schema a router serves, with the
|
|
154
157
|
# @join__* routing table stripped out.
|
|
155
|
-
def supergraph!
|
|
158
|
+
private def supergraph!
|
|
156
159
|
return @router[:supergraph] if @router&.key?(:supergraph)
|
|
157
160
|
|
|
158
161
|
path = GraphWeaver::SchemaLoader.locate_path
|
|
@@ -169,7 +172,10 @@ module GraphWeaver
|
|
|
169
172
|
# client already runs in-process. Only a live class has resolvers, so
|
|
170
173
|
# there is nothing else to fall back to: a dump is type information.
|
|
171
174
|
def schema_class!
|
|
172
|
-
|
|
175
|
+
# explicit_schema, not schema: the latter falls back to the committed
|
|
176
|
+
# dump, which loads as an anonymous GraphQL::Schema subclass — runnable
|
|
177
|
+
# by every test that matters, and holding not one resolver.
|
|
178
|
+
runnable(explicit_schema) || GraphWeaver::Internal::Util.live_schema ||
|
|
173
179
|
raise(GraphWeaver::Error, ":in_process runs your resolvers, so it needs the live " \
|
|
174
180
|
"GraphQL::Schema class — and GraphWeaver.client isn't running one in-process to " \
|
|
175
181
|
"borrow. Name it in the example — graphql_in_process(MySchema) — or set " \
|
|
@@ -213,81 +219,32 @@ module GraphWeaver
|
|
|
213
219
|
yield config
|
|
214
220
|
# a typo'd override key pins nothing and the test still passes, so
|
|
215
221
|
# catch it here — while the block that set it is still on the stack
|
|
216
|
-
|
|
222
|
+
Internal::Overrides.validate!(config.explicit_schema, config.overrides) if config.explicit_schema
|
|
217
223
|
config
|
|
218
224
|
end
|
|
219
225
|
|
|
220
|
-
# Override keys name schema coordinates: "Type.field", or a bare field
|
|
221
|
-
# name matching that field on any type. Anything else is a typo that
|
|
222
|
-
# would silently fabricate random data instead of pinning a value.
|
|
223
|
-
def validate_overrides!(schema, overrides)
|
|
224
|
-
overrides.each_key { |key| validate_override_key!(schema, key.to_s) }
|
|
225
|
-
end
|
|
226
|
-
|
|
227
226
|
# back to defaults — between tests, or to undo an experiment
|
|
228
227
|
def reset!
|
|
229
228
|
@config = nil
|
|
230
229
|
end
|
|
231
230
|
|
|
232
231
|
# resolve a cassette name ("github") against cassette_dir; paths
|
|
233
|
-
# with separators or extensions
|
|
232
|
+
# with separators or extensions name the file themselves
|
|
234
233
|
def cassette_path(name)
|
|
235
|
-
return name if name.include?("/") || name.end_with?(".yml", ".yaml")
|
|
234
|
+
return Internal::Util.resolve(name) if name.include?("/") || name.end_with?(".yml", ".yaml")
|
|
236
235
|
|
|
237
236
|
File.join(cassette_dir, "#{name}.yml")
|
|
238
237
|
end
|
|
239
238
|
|
|
240
|
-
# The configured directory
|
|
241
|
-
#
|
|
242
|
-
|
|
243
|
-
# gem doesn't depend on.
|
|
244
|
-
def cassette_dir
|
|
245
|
-
dir = config.cassette_dir
|
|
246
|
-
root = (Object.const_get(:Rails).root if Object.const_defined?(:Rails))
|
|
247
|
-
root ? File.join(root.to_s, dir) : dir
|
|
248
|
-
rescue NoMethodError
|
|
249
|
-
dir # something else named Rails
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
private
|
|
253
|
-
|
|
254
|
-
def validate_override_key!(schema, key)
|
|
255
|
-
type_name, field_name = key.split(".", 2)
|
|
256
|
-
# introspection fields (__typename) are real but absent from #fields
|
|
257
|
-
return if (field_name || type_name).start_with?("__")
|
|
258
|
-
|
|
259
|
-
if field_name.nil?
|
|
260
|
-
known = field_names(schema)
|
|
261
|
-
return if known.include?(type_name)
|
|
262
|
-
|
|
263
|
-
bad_override!(key, "matches no field in this schema", known, type_name)
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
type = schema.get_type(type_name)
|
|
267
|
-
unless type.respond_to?(:fields)
|
|
268
|
-
bad_override!(key, "names no object type in this schema", schema.types.keys, type_name)
|
|
269
|
-
end
|
|
270
|
-
return if type.fields.key?(field_name)
|
|
271
|
-
|
|
272
|
-
bad_override!(key, "is not a field of #{type_name}", type.fields.keys, field_name)
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def bad_override!(key, problem, dictionary, term)
|
|
276
|
-
suggestion = GraphWeaver.did_you_mean(dictionary, term)
|
|
277
|
-
hint = suggestion ? " — did you mean '#{suggestion}'?" : ""
|
|
278
|
-
raise GraphWeaver::Error, "override key #{key.inspect} #{problem}#{hint}"
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
# Every output field name in the schema — walked only when a bare key
|
|
282
|
-
# asks for it.
|
|
283
|
-
def field_names(schema)
|
|
284
|
-
schema.types.each_value.flat_map { |type| type.respond_to?(:fields) ? type.fields.keys : [] }.uniq
|
|
285
|
-
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)
|
|
286
242
|
end
|
|
287
243
|
end
|
|
288
244
|
end
|
|
289
245
|
|
|
290
|
-
require_relative "
|
|
246
|
+
require_relative "internal/overrides"
|
|
247
|
+
require_relative "internal/values"
|
|
291
248
|
require_relative "testing/fake_client"
|
|
292
249
|
require_relative "testing/fake_subgraph"
|
|
293
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