graph_weaver 0.7.0 → 0.7.2
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/Gemfile.lock +4 -4
- data/README.md +40 -88
- data/docs/alternatives.md +1 -7
- data/docs/cassettes.md +54 -59
- data/docs/editors.md +32 -47
- data/docs/errors.md +261 -369
- data/docs/federation.md +650 -837
- data/docs/generated_modules.md +380 -463
- data/docs/getting_started.md +211 -428
- data/docs/i18n.md +114 -177
- data/docs/logging.md +127 -116
- data/docs/real_world.md +26 -39
- data/docs/scalars.md +277 -310
- data/docs/testing.md +343 -486
- data/docs/transports.md +203 -268
- data/docs/upgrading.md +211 -560
- data/examples/README.md +38 -0
- data/examples/countries.rb +39 -0
- data/examples/federation.rb +62 -0
- data/examples/github/generate.rb +20 -0
- data/examples/github/generated/star_mutation.rb +126 -0
- data/examples/github/generated/stargazers_query.rb +232 -0
- data/examples/github/generated/starred_query.rb +151 -0
- data/examples/github/queries/star.graphql +8 -0
- data/examples/github/queries/stargazers.graphql +22 -0
- data/examples/github/queries/starred.graphql +11 -0
- data/examples/github/run.rb +43 -0
- data/examples/github/setup.rb +18 -0
- data/examples/rick_and_morty.rb +57 -0
- data/graph_weaver.gemspec +12 -3
- data/lib/graph_weaver/client.rb +30 -1
- data/lib/graph_weaver/codegen/emit.rb +5 -11
- data/lib/graph_weaver/codegen.rb +23 -55
- data/lib/graph_weaver/context_seam.rb +54 -0
- data/lib/graph_weaver/errors.rb +23 -15
- data/lib/graph_weaver/federation.rb +11 -2
- data/lib/graph_weaver/graph.rb +39 -29
- data/lib/graph_weaver/in_process.rb +15 -9
- data/lib/graph_weaver/internal/endpoint.rb +7 -5
- data/lib/graph_weaver/internal/headers.rb +19 -0
- data/lib/graph_weaver/internal/test_clients.rb +7 -11
- data/lib/graph_weaver/internal.rb +81 -13
- data/lib/graph_weaver/log_subscriber.rb +10 -2
- data/lib/graph_weaver/logging.rb +33 -13
- data/lib/graph_weaver/query_module.rb +44 -23
- data/lib/graph_weaver/retry.rb +12 -8
- data/lib/graph_weaver/rspec.rb +13 -24
- data/lib/graph_weaver/schema_loader.rb +52 -14
- data/lib/graph_weaver/tasks.rb +10 -2
- data/lib/graph_weaver/testing/cassette.rb +28 -5
- data/lib/graph_weaver/testing/endpoint.rb +14 -13
- data/lib/graph_weaver/testing/fake_client.rb +33 -3
- data/lib/graph_weaver/testing/router.rb +7 -3
- data/lib/graph_weaver/testing.rb +12 -4
- data/lib/graph_weaver/transport/http.rb +2 -2
- data/lib/graph_weaver/transport.rb +47 -23
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +32 -10
- metadata +16 -3
- data/CHANGELOG.md +0 -3801
|
@@ -96,7 +96,8 @@ class GraphWeaver::Transport
|
|
|
96
96
|
# a raw query string falls back to the name in the document itself.
|
|
97
97
|
def execute(query, variables: {}, operation_name: nil)
|
|
98
98
|
operation_name ||= GraphWeaver::Internal::Wire.operation_name(query)
|
|
99
|
-
payload = { url: safe_url, operation: operation_name, client: self.class
|
|
99
|
+
payload = { url: safe_url, operation: operation_name, client: self.class,
|
|
100
|
+
kind: GraphWeaver::Internal::Wire.kind(query) }
|
|
100
101
|
|
|
101
102
|
GraphWeaver::Internal::Log.instrument(GraphWeaver::EXECUTE_EVENT, payload) do
|
|
102
103
|
perform(query, variables, operation_name, payload)
|
|
@@ -132,12 +133,24 @@ class GraphWeaver::Transport
|
|
|
132
133
|
post(encoded)
|
|
133
134
|
end
|
|
134
135
|
rescue *GraphWeaver.transport_errors.to_a => e
|
|
135
|
-
# never got a response — DNS, connection refused/reset, TLS, timeout
|
|
136
|
-
|
|
136
|
+
# never got a response — DNS, connection refused/reset, TLS, timeout.
|
|
137
|
+
# The adapter's sentence is its own words, capped like any text we
|
|
138
|
+
# didn't author.
|
|
139
|
+
raise GraphWeaver::TransportError.new(
|
|
140
|
+
"#{e.class}: #{GraphWeaver::Internal::Redact.cap(e.message)}", url: safe_url,
|
|
141
|
+
)
|
|
137
142
|
end
|
|
138
143
|
|
|
139
144
|
payload[:http_status] = status
|
|
140
|
-
|
|
145
|
+
# folded once: a third-party subclass's headers may come back in any
|
|
146
|
+
# casing, and a subclass that returns none says nothing
|
|
147
|
+
fields = GraphWeaver::Internal::Headers.wrap(headers || {})
|
|
148
|
+
# the content type, not the body: it is what tells a proxy's HTML page
|
|
149
|
+
# from a router's JSON without quoting bytes a server chose
|
|
150
|
+
GraphWeaver::Internal::Log.log(:debug) do
|
|
151
|
+
type = GraphWeaver::Internal::Redact.tag(fields["content-type"])
|
|
152
|
+
"HTTP #{status} #{tag} from #{safe_url} (#{body.to_s.bytesize} bytes#{", #{type}" if type})"
|
|
153
|
+
end
|
|
141
154
|
|
|
142
155
|
parsed = parse_body(body)
|
|
143
156
|
|
|
@@ -151,42 +164,54 @@ class GraphWeaver::Transport
|
|
|
151
164
|
# `"errors": null` (or []) isn't a structured error response, so the
|
|
152
165
|
# status stays the signal
|
|
153
166
|
if parsed.is_a?(Hash) && parsed["errors"].is_a?(Array) && parsed["errors"].any?
|
|
154
|
-
return Envelope.new(parsed, status)
|
|
167
|
+
return Envelope.new(parsed, status, fields.retry_after)
|
|
155
168
|
end
|
|
156
169
|
|
|
157
|
-
|
|
170
|
+
refuse!(status, body, headers)
|
|
158
171
|
end
|
|
159
172
|
|
|
160
173
|
unless parsed.is_a?(Hash)
|
|
161
174
|
# a 200 that isn't a GraphQL object — an HTML error page from a proxy, a
|
|
162
175
|
# captive portal, or a bare JSON array/string: the server misbehaved.
|
|
163
|
-
#
|
|
164
|
-
# well-formed @defer stream is named rather than dumped: it isn't
|
|
176
|
+
# A well-formed @defer stream is named rather than lumped in: it isn't
|
|
165
177
|
# non-GraphQL, it's more than one GraphQL document.
|
|
166
|
-
|
|
167
|
-
if incremental?(
|
|
178
|
+
detail =
|
|
179
|
+
if incremental?(fields)
|
|
168
180
|
"this response is incremental delivery (@defer/@stream), which this client doesn't read"
|
|
169
181
|
elsif body.to_s.empty?
|
|
170
182
|
"empty response body"
|
|
171
183
|
else
|
|
172
|
-
"non-GraphQL response
|
|
184
|
+
"non-GraphQL response"
|
|
173
185
|
end
|
|
174
|
-
|
|
186
|
+
refuse!(status, body, headers, detail:)
|
|
175
187
|
end
|
|
176
188
|
|
|
177
|
-
Envelope.new(parsed, status)
|
|
189
|
+
Envelope.new(parsed, status, fields.retry_after)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The response wasn't one we can read. A body is never quoted — not into the
|
|
193
|
+
# message, not into a log line: it is text a server chose, and an error page
|
|
194
|
+
# that echoes the request fills it with the variables and the Authorization
|
|
195
|
+
# header we just sent. `detail` is what WE say went wrong; the bytes are on
|
|
196
|
+
# ServerError#body for whoever rescues it.
|
|
197
|
+
private def refuse!(status, body, headers, detail: nil)
|
|
198
|
+
raise GraphWeaver::ServerError.new(status:, body: body.to_s, headers: headers || {}, url: safe_url, detail:)
|
|
178
199
|
end
|
|
179
200
|
|
|
180
|
-
# The parsed envelope, plus the HTTP
|
|
181
|
-
# everything that reads a GraphQL response, and to the one caller
|
|
182
|
-
# needs
|
|
183
|
-
# an errors body, so the body alone can't say whether to come
|
|
201
|
+
# The parsed envelope, plus what the HTTP response said around it — a Hash
|
|
202
|
+
# to everything that reads a GraphQL response, and more to the one caller
|
|
203
|
+
# that needs it. Retry asks both: a router answers rate limiting with a 503
|
|
204
|
+
# or 429 AND an errors body, so the body alone can't say whether to come
|
|
205
|
+
# back, and Retry-After says when. The seconds, not the headers — that is
|
|
206
|
+
# the whole of what Retry asks, and every other header stays where a
|
|
207
|
+
# ServerError already carries it.
|
|
184
208
|
class Envelope < Hash
|
|
185
|
-
attr_reader :http_status
|
|
209
|
+
attr_reader :http_status, :retry_after
|
|
186
210
|
|
|
187
|
-
def initialize(parsed, http_status)
|
|
211
|
+
def initialize(parsed, http_status, retry_after = nil)
|
|
188
212
|
super()
|
|
189
213
|
@http_status = http_status
|
|
214
|
+
@retry_after = retry_after
|
|
190
215
|
update(parsed)
|
|
191
216
|
end
|
|
192
217
|
end
|
|
@@ -201,10 +226,9 @@ class GraphWeaver::Transport
|
|
|
201
226
|
private_constant :BOM
|
|
202
227
|
|
|
203
228
|
# A multipart/mixed body is one @defer/@stream response arriving in
|
|
204
|
-
# installments.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
GraphWeaver::Internal::Headers.wrap(headers || {})["content-type"].to_s.start_with?("multipart/mixed")
|
|
229
|
+
# installments.
|
|
230
|
+
private def incremental?(fields)
|
|
231
|
+
fields["content-type"].to_s.start_with?("multipart/mixed")
|
|
208
232
|
end
|
|
209
233
|
|
|
210
234
|
# the parsed body, or nil when it isn't JSON (a caller's connection may
|
data/lib/graph_weaver/version.rb
CHANGED
data/lib/graph_weaver.rb
CHANGED
|
@@ -65,8 +65,8 @@ module GraphWeaver
|
|
|
65
65
|
#
|
|
66
66
|
# Anything satisfying the execute contract — a Client, a schema class,
|
|
67
67
|
# a transport, a fake (testing's graphql: tag swaps one in per
|
|
68
|
-
# example). Generated modules resolve per call ->
|
|
69
|
-
#
|
|
68
|
+
# example). Generated modules resolve per call -> a test mode's stand-in
|
|
69
|
+
# -> the client their graph names -> here.
|
|
70
70
|
attr_accessor :client
|
|
71
71
|
|
|
72
72
|
# the default client, when one is required
|
|
@@ -389,7 +389,7 @@ module GraphWeaver
|
|
|
389
389
|
# (see #changed_files). Generated files the plan no longer produces are deleted
|
|
390
390
|
# (see #orphaned), so renaming or dropping a .graphql leaves nothing
|
|
391
391
|
# behind. Pair with a freshness spec (docs/generated_modules.md).
|
|
392
|
-
def generate!(schema: nil, queries: nil, output: nil,
|
|
392
|
+
def generate!(schema: nil, queries: nil, output: nil, types_module: nil)
|
|
393
393
|
@changed_files = []
|
|
394
394
|
@unmatched_registrations = []
|
|
395
395
|
@untyped_scalars_by_graph = {}
|
|
@@ -399,7 +399,7 @@ module GraphWeaver
|
|
|
399
399
|
# tree exactly as it was — the railtie's watch mode regenerates on a
|
|
400
400
|
# request and promises a failed save changes nothing, and that promise
|
|
401
401
|
# was true within a graph and false across them.
|
|
402
|
-
planned = graphs_for(schema:, queries:, output:,
|
|
402
|
+
planned = graphs_for(schema:, queries:, output:, types_module:).map do |graph|
|
|
403
403
|
if Internal::Util.query_files(graph.queries).empty?
|
|
404
404
|
# a brand-new app legitimately has none; a mistyped queries_paths looks
|
|
405
405
|
# exactly the same, and prints nothing either way
|
|
@@ -488,11 +488,11 @@ module GraphWeaver
|
|
|
488
488
|
# it "generated queries are current" do
|
|
489
489
|
# GraphWeaver.verify_generated!
|
|
490
490
|
# end
|
|
491
|
-
def verify_generated!(schema: nil, queries: nil, output: nil,
|
|
491
|
+
def verify_generated!(schema: nil, queries: nil, output: nil, types_module: nil)
|
|
492
492
|
@unmatched_registrations = []
|
|
493
493
|
@untyped_scalars_by_graph = {}
|
|
494
494
|
seen = new_seen
|
|
495
|
-
graphs = graphs_for(schema:, queries:, output:,
|
|
495
|
+
graphs = graphs_for(schema:, queries:, output:, types_module:)
|
|
496
496
|
|
|
497
497
|
# The dump is checked in too, and everything below reads it — so a
|
|
498
498
|
# stale one is answered before staleness downstream of it, because
|
|
@@ -849,7 +849,9 @@ module GraphWeaver
|
|
|
849
849
|
used_unions = []
|
|
850
850
|
shared = Codegen.load_fragments(fragments)
|
|
851
851
|
|
|
852
|
-
|
|
852
|
+
refusals = []
|
|
853
|
+
paths = Internal::Util.query_files(graph.queries)
|
|
854
|
+
plan = paths.filter_map do |path|
|
|
853
855
|
source = File.read(path)
|
|
854
856
|
name, filename = graph.generated_names(path, source)
|
|
855
857
|
refuse_duplicate!(seen, name, filename, graph, path)
|
|
@@ -858,7 +860,6 @@ module GraphWeaver
|
|
|
858
860
|
schema:,
|
|
859
861
|
query: Codegen.inline_fragments(source, shared, path),
|
|
860
862
|
name:,
|
|
861
|
-
client: graph.client,
|
|
862
863
|
graph_name: graph.name,
|
|
863
864
|
types_namespace: graph.types_module,
|
|
864
865
|
hoistable_unions: Codegen.shared_fragment_spreads(source, shared, path),
|
|
@@ -870,7 +871,13 @@ module GraphWeaver
|
|
|
870
871
|
found.concat(codegen.untyped_scalars).uniq!
|
|
871
872
|
used_unions |= codegen.used_union_names
|
|
872
873
|
[filename, out]
|
|
874
|
+
rescue GraphWeaver::Error => e
|
|
875
|
+
# collected, not raised: nothing is written either way, and an adopter
|
|
876
|
+
# aiming generate! at an existing query directory wants the list
|
|
877
|
+
refusals << e
|
|
878
|
+
nil
|
|
873
879
|
end
|
|
880
|
+
refuse_all!(refusals, paths.size)
|
|
874
881
|
|
|
875
882
|
if used_unions.any? || used.values.any?(&:any?)
|
|
876
883
|
refuse_duplicate_types!(seen, graph)
|
|
@@ -890,6 +897,19 @@ module GraphWeaver
|
|
|
890
897
|
end
|
|
891
898
|
private :generation_plan
|
|
892
899
|
|
|
900
|
+
# Every query that refused, in one error. One refusal is re-raised as
|
|
901
|
+
# itself, so a single bad file reads exactly as it always has — class,
|
|
902
|
+
# message and all; several become one list, because clearing them a file
|
|
903
|
+
# per run is the slowest way there is to adopt this.
|
|
904
|
+
def refuse_all!(refusals, considered)
|
|
905
|
+
return if refusals.empty?
|
|
906
|
+
raise refusals.first if refusals.one?
|
|
907
|
+
|
|
908
|
+
raise GraphWeaver::Error, "#{refusals.size} of #{considered} queries refused:\n" +
|
|
909
|
+
refusals.map { |refusal| refusal.message.gsub(/^/, " ") }.join("\n")
|
|
910
|
+
end
|
|
911
|
+
private :refuse_all!
|
|
912
|
+
|
|
893
913
|
# Two query files landing on one constant, or on one output file. Within a
|
|
894
914
|
# graph the fix is a rename, as it has always been; across two graphs it is
|
|
895
915
|
# `namespace:` for the constant and `output:` for the file — so the message
|
|
@@ -1061,8 +1081,10 @@ module GraphWeaver
|
|
|
1061
1081
|
# name derived from the file name and the operation — see #module_name) or
|
|
1062
1082
|
# a raw query string (name derived from the operation name, falling back to
|
|
1063
1083
|
# "Query" for anonymous operations — collisions are impossible since each
|
|
1064
|
-
# parse gets its own container). Pass name: to override, client: to
|
|
1065
|
-
# the module
|
|
1084
|
+
# parse gets its own container). Pass name: to override, client: to say
|
|
1085
|
+
# what the module runs against — a parsed module generates no file, so it
|
|
1086
|
+
# has no graph to read one off, and this is the only time one is bound
|
|
1087
|
+
# (a per-call `client:` still wins).
|
|
1066
1088
|
#
|
|
1067
1089
|
# graph: names the graph this module belongs to, which is what a test mode
|
|
1068
1090
|
# runs it against in an app with more than one — the same thing generation
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graph_weaver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Pepper
|
|
@@ -268,7 +268,6 @@ extensions: []
|
|
|
268
268
|
extra_rdoc_files: []
|
|
269
269
|
files:
|
|
270
270
|
- ".yardopts"
|
|
271
|
-
- CHANGELOG.md
|
|
272
271
|
- Gemfile
|
|
273
272
|
- Gemfile.lock
|
|
274
273
|
- LICENSE.txt
|
|
@@ -287,6 +286,19 @@ files:
|
|
|
287
286
|
- docs/testing.md
|
|
288
287
|
- docs/transports.md
|
|
289
288
|
- docs/upgrading.md
|
|
289
|
+
- examples/README.md
|
|
290
|
+
- examples/countries.rb
|
|
291
|
+
- examples/federation.rb
|
|
292
|
+
- examples/github/generate.rb
|
|
293
|
+
- examples/github/generated/star_mutation.rb
|
|
294
|
+
- examples/github/generated/stargazers_query.rb
|
|
295
|
+
- examples/github/generated/starred_query.rb
|
|
296
|
+
- examples/github/queries/star.graphql
|
|
297
|
+
- examples/github/queries/stargazers.graphql
|
|
298
|
+
- examples/github/queries/starred.graphql
|
|
299
|
+
- examples/github/run.rb
|
|
300
|
+
- examples/github/setup.rb
|
|
301
|
+
- examples/rick_and_morty.rb
|
|
290
302
|
- graph_weaver.gemspec
|
|
291
303
|
- lib/generators/graph_weaver/install_generator.rb
|
|
292
304
|
- lib/graph_weaver.rb
|
|
@@ -300,6 +312,7 @@ files:
|
|
|
300
312
|
- lib/graph_weaver/codegen/scalar_type.rb
|
|
301
313
|
- lib/graph_weaver/codegen/type_helpers.rb
|
|
302
314
|
- lib/graph_weaver/coerce.rb
|
|
315
|
+
- lib/graph_weaver/context_seam.rb
|
|
303
316
|
- lib/graph_weaver/errors.rb
|
|
304
317
|
- lib/graph_weaver/federation.rb
|
|
305
318
|
- lib/graph_weaver/graph.rb
|
|
@@ -350,7 +363,7 @@ licenses:
|
|
|
350
363
|
- MIT
|
|
351
364
|
metadata:
|
|
352
365
|
bug_tracker_uri: https://github.com/dpep/graph_weaver/issues
|
|
353
|
-
changelog_uri: https://github.com/dpep/graph_weaver/blob/
|
|
366
|
+
changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.2/CHANGELOG.md
|
|
354
367
|
documentation_uri: https://github.com/dpep/graph_weaver/tree/main/docs
|
|
355
368
|
rubygems_mfa_required: 'true'
|
|
356
369
|
source_code_uri: https://github.com/dpep/graph_weaver
|