shopify_draft_proxy/proxy/commit
Mutation-log replay against the upstream Shopify Admin GraphQL endpoint.
Mirrors commitMetaState in src/meta/routes.ts:606.
All pure logic (id-map building, GID rewriting, response interpretation)
lives here and is target-agnostic. The two drivers (run_commit_sync for
Erlang, run_commit_async for JavaScript) are also exposed here, both
taking an injected send function so tests can drive the engine without
real HTTP. draft_proxy.gleam wires the production gleam_httpc /
gleam_fetch clients into the corresponding driver via target-specific
thin shims.
Types
Outcome of a single replay attempt.
pub type CommitAttempt {
CommitAttempt(
log_entry_id: String,
operation_name: option.Option(String),
path: String,
success: Bool,
status: store.EntryStatus,
upstream_status: option.Option(Int),
upstream_body: option.Option(JsonValue),
upstream_error: option.Option(String),
response_body: JsonValue,
)
}
Constructors
-
CommitAttempt( log_entry_id: String, operation_name: option.Option(String), path: String, success: Bool, status: store.EntryStatus, upstream_status: option.Option(Int), upstream_body: option.Option(JsonValue), upstream_error: option.Option(String), response_body: JsonValue, )
Normalised HTTP-transport error surfaced by the injected send. The
Erlang and JS production shims map their respective library errors into
this single message-bearing type.
pub type CommitTransportError {
CommitTransportError(message: String)
}
Constructors
-
CommitTransportError(message: String)
Normalised successful HTTP outcome. Both targets convert their library’s response into this so the driver code can remain target-agnostic.
pub type HttpOutcome {
HttpOutcome(status: Int, body: String)
}
Constructors
-
HttpOutcome(status: Int, body: String)
The body shape returned by the __meta/commit HTTP route.
pub type MetaCommitResponse {
MetaCommitResponse(
ok: Bool,
stop_index: option.Option(Int),
attempts: List(CommitAttempt),
)
}
Constructors
-
MetaCommitResponse( ok: Bool, stop_index: option.Option(Int), attempts: List(CommitAttempt), )
Values
pub fn apply_id_map_to_body_string(
body: String,
id_map: dict.Dict(String, String),
) -> String
Replace every entry of id_map (synthetic gid → authoritative gid) in
value. Mirrors the chained replaceAll walk in the TS helper, but
applied to the wire-form request body so we don’t have to re-parse and
re-serialise every replay. Synthetic GIDs only ever appear in JSON
string values (the gid://… form is never a JSON key), so substring
substitution is equivalent to the AST walk.
pub fn build_replay_body(entry: store.MutationLogEntry) -> String
pub fn build_replay_request(
origin: String,
entry: store.MutationLogEntry,
id_map: dict.Dict(String, String),
inbound_headers: dict.Dict(String, String),
) -> Result(request.Request(String), Nil)
Build the gleam_http request to send upstream for a single entry.
headers is the inbound proxy request’s headers (forwarded with the
usual stripping/overrides applied) and origin is the configured
shopifyAdminOrigin. Returns Error(Nil) only when origin+path don’t
form a parseable URL — which would be a config bug.
pub fn collect_authoritative_gids_by_type(
value: JsonValue,
) -> dict.Dict(String, List(String))
Collect every non-synthetic gid://shopify/Type/… value found anywhere
in value, grouped by resource type and de-duplicated in encounter
order. Mirrors collectAuthoritativeGidsByType.
pub fn entry_requires_commit(
entry: store.MutationLogEntry,
) -> Bool
pub fn forward_headers(
incoming: dict.Dict(String, String),
) -> List(#(String, String))
Lower-cased name + trimmed value, with the omitted set stripped. Forces
content-type: application/json and stamps user-agent with our marker
(wrapping the inbound UA when present).
pub fn gid_resource_type(value: String) -> option.Option(String)
Extract the Type segment from gid://shopify/Type/123(?…). Returns
None if the input isn’t a Shopify gid.
pub fn json_value_decoder() -> decode.Decoder(JsonValue)
Recursive decoder for arbitrary JSON. Order-preserving for objects so the round-trip matches the input byte ordering when feasible.
pub fn json_value_to_json(value: JsonValue) -> json.Json
Convert a parsed JsonValue back into a gleam/json.Json tree for
re-serialisation in the response envelope.
pub fn parse_json_value(body: String) -> JsonValue
Parse a JSON string into the AST. Falls back to JsonString(<raw>) so
upstream responses that aren’t JSON don’t crash the commit loop —
they’re surfaced verbatim as a string in upstream_body.
pub fn proxy_user_agent(
incoming: option.Option(String),
) -> String
Build the User-Agent string the proxy stamps on outbound replays.
Mirrors buildShopifyDraftProxyUserAgent.
pub fn record_commit_id_mappings(
entry: store.MutationLogEntry,
response_body: JsonValue,
id_map: dict.Dict(String, String),
) -> dict.Dict(String, String)
Update id_map with newly-mintable synthetic → authoritative pairs
inferred from entry.staged_resource_ids (the synthetic GIDs the entry
produced when it was staged) and response_body (the upstream’s actual
reply, which carries the real GIDs). Mirrors recordCommitIdMappings.
pub fn response_body_has_graphql_errors(body: JsonValue) -> Bool
True when body contains a non-empty top-level errors array, the
GraphQL convention for a failed operation. Mirrors the TS helper.
pub fn run_commit_async(
proxy_store: store.Store,
origin: String,
inbound_headers: dict.Dict(String, String),
send: fn(request.Request(String)) -> promise.Promise(
Result(HttpOutcome, CommitTransportError),
),
) -> promise.Promise(#(store.Store, MetaCommitResponse))
pub fn run_commit_sync(
proxy_store: store.Store,
origin: String,
inbound_headers: dict.Dict(String, String),
send: fn(request.Request(String)) -> Result(
HttpOutcome,
CommitTransportError,
),
) -> #(store.Store, MetaCommitResponse)
pub fn serialize_meta_response(
meta: MetaCommitResponse,
) -> json.Json
pub fn step(
proxy_store: store.Store,
entry: store.MutationLogEntry,
id_map: dict.Dict(String, String),
send_outcome: Result(HttpOutcome, CommitTransportError),
) -> #(
store.Store,
dict.Dict(String, String),
CommitAttempt,
Bool,
)