graph_weaver 0.4.6 → 0.5.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 +1314 -0
- data/CLAUDE.md +100 -8
- data/DECISIONS.md +309 -0
- data/Gemfile.lock +23 -23
- data/NOTES.md +5 -5
- data/PLAN.md +106 -135
- data/README.md +115 -96
- data/REVIEW.md +946 -0
- data/docs/cassettes.md +75 -48
- data/docs/editors.md +82 -0
- data/docs/errors.md +32 -30
- data/docs/federation.md +520 -48
- data/docs/generated_modules.md +352 -137
- data/docs/getting_started.md +237 -67
- data/docs/logging.md +35 -6
- data/docs/real_world.md +21 -15
- data/docs/scalars.md +49 -154
- data/docs/testing.md +299 -52
- data/docs/transports.md +129 -30
- data/docs/upgrading.md +112 -0
- data/graph_weaver.gemspec +3 -1
- data/lib/generators/graph_weaver/install_generator.rb +259 -0
- data/lib/graph_weaver/client.rb +114 -111
- data/lib/graph_weaver/codegen/aliases.rb +217 -0
- data/lib/graph_weaver/codegen/emit.rb +272 -258
- data/lib/graph_weaver/codegen/enum_type.rb +27 -124
- data/lib/graph_weaver/codegen/nodes.rb +72 -13
- data/lib/graph_weaver/codegen/scalar_type.rb +68 -66
- data/lib/graph_weaver/codegen/type_helpers.rb +142 -0
- data/lib/graph_weaver/codegen.rb +593 -334
- data/lib/graph_weaver/errors.rb +127 -10
- data/lib/graph_weaver/federation.rb +272 -0
- data/lib/graph_weaver/hints.rb +9 -1
- data/lib/graph_weaver/in_process.rb +90 -0
- data/lib/graph_weaver/input_struct.rb +14 -2
- data/lib/graph_weaver/logging.rb +29 -0
- data/lib/graph_weaver/parsing.rb +67 -0
- data/lib/graph_weaver/query_module.rb +55 -0
- data/lib/graph_weaver/railtie.rb +23 -1
- data/lib/graph_weaver/representation.rb +74 -0
- data/lib/graph_weaver/response.rb +7 -0
- data/lib/graph_weaver/retry.rb +29 -8
- data/lib/graph_weaver/rspec.rb +214 -16
- data/lib/graph_weaver/schema_loader.rb +794 -59
- data/lib/graph_weaver/schemas.rb +46 -0
- data/lib/graph_weaver/selection.rb +43 -8
- data/lib/graph_weaver/tasks.rb +216 -21
- data/lib/graph_weaver/testing/cassette.rb +160 -61
- data/lib/graph_weaver/testing/coverage.rb +165 -0
- data/lib/graph_weaver/testing/failure.rb +10 -23
- data/lib/graph_weaver/testing/fake_client.rb +181 -21
- data/lib/graph_weaver/testing/fake_subgraph.rb +85 -0
- data/lib/graph_weaver/testing/router.rb +1431 -0
- data/lib/graph_weaver/testing/subgraphs.rb +130 -0
- data/lib/graph_weaver/testing.rb +204 -14
- data/lib/graph_weaver/transport/faraday.rb +28 -10
- data/lib/graph_weaver/transport/http.rb +99 -36
- data/lib/graph_weaver/transport.rb +67 -14
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +389 -170
- metadata +20 -3
|
@@ -5,11 +5,12 @@ require "json"
|
|
|
5
5
|
require "sorbet-runtime"
|
|
6
6
|
|
|
7
7
|
require_relative "errors"
|
|
8
|
+
require_relative "version"
|
|
8
9
|
|
|
9
10
|
# Base class for the bundled network transports — Transport::HTTP
|
|
10
11
|
# (zero-dependency net/http, loaded by default) and Transport::Faraday
|
|
11
|
-
# (opt-in). A transport speaks GraphQL-over-HTTP and satisfies the
|
|
12
|
-
#
|
|
12
|
+
# (opt-in). A transport speaks GraphQL-over-HTTP and satisfies the same
|
|
13
|
+
# execute(query, variables:, operation_name:) => {"data" => ..., "errors" => ...}
|
|
13
14
|
# contract as a schema class or a fake — anything in a client slot.
|
|
14
15
|
#
|
|
15
16
|
# The base class owns the shared flow — encode the request, reclassify
|
|
@@ -21,29 +22,68 @@ class GraphWeaver::Transport
|
|
|
21
22
|
extend T::Helpers
|
|
22
23
|
abstract!
|
|
23
24
|
|
|
25
|
+
# What every request sends unless the caller says otherwise.
|
|
26
|
+
# graphql-over-http requires a conforming client to accept
|
|
27
|
+
# application/graphql-response+json; the q=0.9 fallback keeps servers
|
|
28
|
+
# that only speak the legacy media type working. The User-Agent is what
|
|
29
|
+
# lets a server operator attribute the traffic.
|
|
30
|
+
DEFAULT_HEADERS = {
|
|
31
|
+
"Content-Type" => "application/json",
|
|
32
|
+
"Accept" => "application/graphql-response+json, application/json;q=0.9",
|
|
33
|
+
"User-Agent" => "graph_weaver/#{GraphWeaver::VERSION}",
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# Timeouts, in seconds, shared by the bundled transports — a missing
|
|
37
|
+
# timeout is an outage, and net/http's own 60s/60s is far too patient
|
|
38
|
+
# for an API call.
|
|
39
|
+
DEFAULT_OPEN_TIMEOUT = 10
|
|
40
|
+
DEFAULT_READ_TIMEOUT = 30
|
|
41
|
+
|
|
24
42
|
# the endpoint this transport talks to — recorded into cached schema
|
|
25
43
|
# dumps as provenance (see SchemaLoader.introspect)
|
|
26
44
|
attr_reader :url
|
|
27
45
|
|
|
28
|
-
|
|
46
|
+
# operation_name: names the operation to run — sent on the wire as
|
|
47
|
+
# `operationName`, which is what an APM keys its traces, rate limits and
|
|
48
|
+
# slow-query reports on. Generated modules pass their OPERATION_NAME;
|
|
49
|
+
# a raw query string falls back to the name in the document itself.
|
|
50
|
+
def execute(query, variables: {}, operation_name: nil)
|
|
51
|
+
operation_name ||= GraphWeaver::Transport.operation_name(query)
|
|
52
|
+
payload = { url:, operation: operation_name }
|
|
53
|
+
|
|
54
|
+
GraphWeaver.instrument(GraphWeaver::EXECUTE_EVENT, payload) do
|
|
55
|
+
perform(query, variables, operation_name, payload)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The request itself. Separate from execute so the instrumenter wraps
|
|
60
|
+
# a call rather than a block this method returns out of.
|
|
61
|
+
private def perform(query, variables, operation_name, payload)
|
|
29
62
|
# tag pairs this request's log lines (threads interleave), and names
|
|
30
63
|
# the operation so the log says WHICH query, not just the url
|
|
31
|
-
tag = GraphWeaver.logger && GraphWeaver::Transport.log_tag(
|
|
64
|
+
tag = GraphWeaver.logger && GraphWeaver::Transport.log_tag(operation_name)
|
|
32
65
|
|
|
33
66
|
# full query + variables at debug only — they can carry PII
|
|
34
67
|
GraphWeaver.log(:debug) do
|
|
35
68
|
"POST #{url} #{tag} variables=#{JSON.generate(variables)}\n#{GraphWeaver::Transport.truncate_for_log(query)}"
|
|
36
69
|
end
|
|
37
70
|
|
|
71
|
+
# camelCase because it's the graphql-over-http request field, not a
|
|
72
|
+
# Ruby name; omitted rather than null when the operation is anonymous
|
|
73
|
+
request = { query:, variables: }
|
|
74
|
+
request[:operationName] = operation_name if operation_name
|
|
75
|
+
|
|
38
76
|
encoded = begin
|
|
39
|
-
JSON.generate(
|
|
77
|
+
JSON.generate(request)
|
|
40
78
|
rescue JSON::GeneratorError => e
|
|
41
79
|
# a value with no JSON form (NaN, Infinity, binary) — the caller's
|
|
42
80
|
# bug, surfaced under the umbrella instead of a raw JSON:: error
|
|
43
81
|
raise GraphWeaver::Error, "variables are not JSON-serializable: #{e.message}"
|
|
44
82
|
end
|
|
45
83
|
|
|
46
|
-
|
|
84
|
+
# headers is optional: a third-party subclass returning the
|
|
85
|
+
# documented [status, body] pair simply has none
|
|
86
|
+
status, body, headers = begin
|
|
47
87
|
GraphWeaver.log_timed(:debug, "POST #{url} #{tag} completed") do
|
|
48
88
|
post(encoded)
|
|
49
89
|
end
|
|
@@ -52,6 +92,7 @@ class GraphWeaver::Transport
|
|
|
52
92
|
raise GraphWeaver::TransportError, "#{e.class}: #{e.message}"
|
|
53
93
|
end
|
|
54
94
|
|
|
95
|
+
payload[:status] = status
|
|
55
96
|
GraphWeaver.log(:debug) { "HTTP #{status} #{tag} from #{url} (#{body.to_s.bytesize} bytes)" }
|
|
56
97
|
|
|
57
98
|
parsed = parse_body(body)
|
|
@@ -67,13 +108,15 @@ class GraphWeaver::Transport
|
|
|
67
108
|
# status stays the signal
|
|
68
109
|
return parsed if parsed.is_a?(Hash) && parsed["errors"].is_a?(Array) && parsed["errors"].any?
|
|
69
110
|
|
|
70
|
-
raise GraphWeaver::ServerError.new(status:, body: body.to_s)
|
|
111
|
+
raise GraphWeaver::ServerError.new(status:, body: body.to_s, headers: headers || {})
|
|
71
112
|
end
|
|
72
113
|
|
|
73
114
|
unless parsed.is_a?(Hash)
|
|
74
115
|
# a 200 that isn't a GraphQL object — an HTML error page from a proxy, a
|
|
75
116
|
# captive portal, or a bare JSON array/string: the server misbehaved
|
|
76
|
-
raise GraphWeaver::ServerError.new(
|
|
117
|
+
raise GraphWeaver::ServerError.new(
|
|
118
|
+
status:, body: "non-GraphQL response: #{body.to_s[0, 500]}", headers: headers || {}
|
|
119
|
+
)
|
|
77
120
|
end
|
|
78
121
|
|
|
79
122
|
parsed
|
|
@@ -96,14 +139,21 @@ class GraphWeaver::Transport
|
|
|
96
139
|
end
|
|
97
140
|
alias to_s inspect
|
|
98
141
|
|
|
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
|
+
|
|
99
150
|
# "[req 3 FilteredPokemon]" — a per-process request id plus the
|
|
100
|
-
# operation name
|
|
151
|
+
# operation name, when there is one
|
|
101
152
|
REQUEST_MUTEX = Mutex.new
|
|
102
153
|
|
|
103
|
-
def self.log_tag(
|
|
154
|
+
def self.log_tag(operation_name = nil)
|
|
104
155
|
id = REQUEST_MUTEX.synchronize { @request_count = (@request_count || 0) + 1 }
|
|
105
|
-
|
|
106
|
-
"[req #{id}#{" #{name}" if name}]"
|
|
156
|
+
"[req #{id}#{" #{operation_name}" if operation_name}]"
|
|
107
157
|
end
|
|
108
158
|
|
|
109
159
|
# keep debug readable: a 100-line introspection query would drown the
|
|
@@ -117,7 +167,10 @@ class GraphWeaver::Transport
|
|
|
117
167
|
|
|
118
168
|
private
|
|
119
169
|
|
|
120
|
-
# POST the JSON body to the endpoint; return [status code, raw body]
|
|
121
|
-
|
|
170
|
+
# POST the JSON body to the endpoint; return [status code, raw body]
|
|
171
|
+
# — optionally with a third element, the response headers as a Hash
|
|
172
|
+
# with downcased names, which ServerError then carries (Retry-After,
|
|
173
|
+
# x-ratelimit-*). Two elements remains a complete answer.
|
|
174
|
+
sig { abstract.params(body: String).returns(T::Array[T.untyped]) }
|
|
122
175
|
def post(body); end
|
|
123
176
|
end
|
data/lib/graph_weaver/version.rb
CHANGED