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

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)

A walkable JSON value parsed from upstream response bodies.

pub type JsonValue {
  JsonNull
  JsonBool(Bool)
  JsonInt(Int)
  JsonFloat(Float)
  JsonString(String)
  JsonArray(List(JsonValue))
  JsonObject(List(#(String, JsonValue)))
}

Constructors

  • JsonNull
  • JsonBool(Bool)
  • JsonInt(Int)
  • JsonFloat(Float)
  • JsonString(String)
  • JsonArray(List(JsonValue))
  • JsonObject(List(#(String, JsonValue)))

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

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,
)
Search Document