shopify_draft_proxy/proxy/mutation_helpers

Helpers shared across mutation handlers.

Pass 13 introduced AST-level argument validation in proxy/webhooks.gleam to mirror the structured top-level GraphQL error envelope TS emits (extensions.code = missingRequiredArguments / argumentLiteralsIncompatible / INVALID_VARIABLE). Pass 14 lifts the validator + its three error builders + the resolved-value readers here so future domain handlers don’t have to copy them.

What’s here:

Types

Per-domain description of a mutation log entry that needs to be recorded. The dispatcher (draft_proxy.route_mutation) is the only site that actually mints the synthetic id/timestamp and calls store.record_mutation_log_entry. Domain process_mutation implementations return one LogDraft per logical entry they want in the buffer (typically one per root mutation field).

Centralising the record call here means a domain that forgets to build a draft has its mutation invisible — but the four-domain regression that prompted this design (gift_cards / localization / metafield_definitions / segments shipping no log entries) cannot recur without a structurally-empty MutationOutcome.log_drafts, which is much easier to spot in code review and in domain unit tests than a missing per-handler record_mutation_log_entry call.

pub type LogDraft {
  LogDraft(
    operation_name: option.Option(String),
    root_fields: List(String),
    primary_root_field: option.Option(String),
    domain: String,
    execution: String,
    query: option.Option(String),
    variables: option.Option(
      dict.Dict(String, root_field.ResolvedValue),
    ),
    staged_resource_ids: List(String),
    status: store.EntryStatus,
    notes: option.Option(String),
  )
}

Constructors

One required top-level argument on a mutation field. name is the argument name as it appears in the schema; expected_type is the type string used in error messages (e.g. "WebhookSubscriptionTopic!").

pub type RequiredArgument {
  RequiredArgument(name: String, expected_type: String)
}

Constructors

  • RequiredArgument(name: String, expected_type: String)

Values

pub fn build_missing_required_argument_error(
  operation_name: String,
  argument_names_joined: String,
  operation_path: String,
  field_loc: option.Option(ast.Location),
  source_body: String,
) -> json.Json

Build the structured error for one or more missing required arguments. argument_names_joined is comma-separated per TS.

pub fn build_missing_variable_error(
  variable_name: String,
  variable_type: String,
) -> json.Json

Build the structured error for an argument bound to a variable that resolved to null or wasn’t supplied.

pub fn build_null_argument_error(
  operation_name: String,
  argument_name: String,
  expected_type: String,
  operation_path: String,
  field_loc: option.Option(ast.Location),
  source_body: String,
) -> json.Json

Build the structured error for an argument bound to a literal null AST node.

pub fn find_argument(
  arguments: List(ast.Argument),
  name: String,
) -> option.Option(ast.Argument)

Look up a named argument in a list. Public so domain handlers can inspect specific arguments after validation has passed.

pub fn read_optional_string(
  input: dict.Dict(String, root_field.ResolvedValue),
  key: String,
) -> option.Option(String)

Read an optional string from a resolved-arg dict. Returns None if the key is absent or bound to a non-string value.

pub fn read_optional_string_array(
  input: dict.Dict(String, root_field.ResolvedValue),
  key: String,
) -> option.Option(List(String))

Read an optional [String] array from a resolved-arg dict. Non-string list elements are dropped silently to mirror the TS filter-then-map pattern. Returns None for absent or non-list values.

pub fn record_log_drafts(
  store: store.Store,
  identity: synthetic_identity.SyntheticIdentityRegistry,
  request_path: String,
  document: String,
  variables: dict.Dict(String, root_field.ResolvedValue),
  drafts: List(LogDraft),
) -> #(store.Store, synthetic_identity.SyntheticIdentityRegistry)

Record each draft into the store, threading the synthetic-identity registry through make_synthetic_gid + make_synthetic_timestamp for the entry id and received_at. Returns the updated store and identity registry.

pub fn single_root_log_draft(
  root_field: String,
  staged_resource_ids: List(String),
  status: store.EntryStatus,
  domain: String,
  execution: String,
  notes: option.Option(String),
) -> LogDraft

Build a LogDraft for a single-root-field mutation. Mirrors the shape that webhooks/apps/saved_searches/functions all historically produced inline. domain and execution are the Capability fields the entry should record; they’re domain constants like "webhooks" / "stage-locally".

pub fn validate_required_field_arguments(
  field: ast.Selection,
  variables: dict.Dict(String, root_field.ResolvedValue),
  operation_name: String,
  required_arguments: List(RequiredArgument),
  operation_path: String,
  source_body: String,
) -> List(json.Json)

Validate the AST-level arguments on a mutation field. Returns one JSON error object per problem; an empty list means “all good”.

Mirrors TS validateRequiredFieldArguments. The split between “validate against AST” and “execute against resolved arg dict” is intentional and necessary: only the AST distinguishes omitted / literal null / unbound variable, each of which maps to a distinct GraphQL error code.

operation_path is the operation path label (e.g. "mutation" / "mutation Foo") — formed from the parsed operation upstream and threaded down here.

pub fn validate_required_id_argument(
  field: ast.Selection,
  variables: dict.Dict(String, root_field.ResolvedValue),
  operation_name: String,
  operation_path: String,
  source_body: String,
) -> #(option.Option(String), List(json.Json))

Validate the single id argument on a *Delete mutation field and return the resolved id alongside any top-level errors. Mirrors the per-mutation pattern in *Delete TS handlers, where the validator both surfaces structured errors and hands the caller the resolved id when validation passed.

On a literal string id with no errors, returns #(Some(id), []). On any validation failure or unresolvable variable, returns #(None, [errors...]).

Search Document