shopify_draft_proxy/proxy/upstream_query
Single chokepoint that operation handlers use to ask Shopify a question.
The substrate exists so the parity runner can install a recorded
cassette as the proxy’s upstream transport (with_upstream_transport)
and have every per-operation upstream call deterministically replay
from that cassette. Production callers leave the transport unset and
fall through to the real HTTP shims (upstream_client.send_sync on
Erlang; the JS-async path is not yet wired here — JS production
handlers needing upstream must wait for a Promise-flavoured fetch
helper, which lands when the first JS-only domain needs it).
There is intentionally no domain-wide hydration helper. Each
operation calls fetch_sync itself and decides what to do with the
response: persist into base state, use it once and discard, or
transform it into the caller-facing reply. The choice is documented
per-handler — see the per-domain migration playbook in
docs/parity-runner.md.
Types
What can go wrong when asking upstream a question. TransportFailed
reports the underlying network shim’s error message; HttpStatusError
surfaces non-2xx responses (the caller decides whether to swallow or
propagate); MalformedResponse covers JSON the proxy can’t parse;
NoTransportInstalled is what JS production handlers see today (they
can’t issue sync upstream calls because production fetch is async,
and no async helper exists yet on this seam).
pub type FetchError {
TransportFailed(message: String)
HttpStatusError(status: Int, body: String)
MalformedResponse(message: String)
NoTransportInstalled
}
Constructors
-
TransportFailed(message: String) -
HttpStatusError(status: Int, body: String) -
MalformedResponse(message: String) -
NoTransportInstalled
Shared upstream-call context. Bundles the three pieces every
handler needs to issue an upstream GraphQL call: the optional
SyncTransport (set by parity tests, unset in production), the
origin to address, and the inbound request’s headers (so the proxy
can forward auth tokens etc.). process_request builds one of these
per inbound request and threads it into any handler that wants to
reach upstream.
pub type UpstreamContext {
UpstreamContext(
transport: option.Option(upstream_client.SyncTransport),
origin: String,
headers: dict.Dict(String, String),
)
}
Constructors
-
UpstreamContext( transport: option.Option(upstream_client.SyncTransport), origin: String, headers: dict.Dict(String, String), )
Values
pub fn empty_upstream_context() -> UpstreamContext
Context whose fetch_sync calls fall through to the live HTTP shim
on Erlang and fail with NoTransportInstalled on JS. Useful for
tests and callers that don’t have headers or origin in scope.
pub fn fetch_sync(
origin: String,
transport: option.Option(upstream_client.SyncTransport),
inbound_headers: dict.Dict(String, String),
operation_name: String,
query: String,
variables: json.Json,
) -> Result(commit.JsonValue, FetchError)
Synchronous upstream call. Returns the parsed response body as a
commit.JsonValue AST so callers can walk it cheaply.
variables is a Json tree (the write-only kind produced by
json.object, json.string, etc.). The body sent upstream is the
canonical {"operationName":..,"query":..,"variables":..} envelope.
On Erlang the real HTTP shim is used as the fallback when no
transport is installed. On JS, no fallback exists yet — call sites
that need upstream from JS must install a SyncTransport (cassette)
or use a future async helper.