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:
RequiredArgumentandvalidate_required_field_arguments— the generic AST validator. MirrorsvalidateRequiredFieldArgumentsinsrc/proxy/webhooks.ts.validate_required_id_argument— single-id-arg variant used by*Deletemutations whose only top-level requirement isid.build_missing_required_argument_error/build_null_argument_error/build_missing_variable_error— the three error builders, directly reusable.read_optional_string/read_optional_string_array— resolved-arg readers that ignore non-matching variants. Bothwebhooksandsaved_searchesuse these.
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
-
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), )
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...]).