Skip to main content

TypeScript Advanced Order Mutations

Remove one line without losing the total, enforce the allowed lifecycle of an Order, and handle a rejected stale save without overwriting the winner. Start with the TypeScript Order tutorial for workspace setup, Q/E result handling and the first audited edit.

The selected retained TypeScript core/advanced workspace remains WORKSPACE PASS — RELEASE PENDING; the nullable generator repair is pending generator deployment. Keep the Node SQL package/alias resolution from the receipt; browser/TFP integration is a separate scope.

Prepare three independent recipes​

Use the v1.0 model and original three-order fixture. The target is ORD-2026-1001: DRAFT (1001), total 3850 USD cents, Notebook quantity 2 at 1250, Pen quantity 3 at 450; Order and items start at version 1. Do not first run the introductory quantity edit: that changes the total to 5100 and invalidates these recipe starting values. The export, isolation and stock fixtures are different.

The complete native application contains startup, helpers and phase handlers. Code below is quoted from that application and assumes its trusted context, imports and validation helpers. These are function-body excerpts or complete functions, not replacement standalone programs. removeLine, transition, worker, status, report and their casing variants are application helpers where present; they are not extra generated API methods.

Each recipe needs a fresh disposable database initialized with bootstrap and seeded once. The retained built workspace/toolchain is required; this is not a clean-machine bootstrap. The original receipts keep each control on a separate seed copy, so an omission probe, removal and stale-removal probe do not silently alter each other's starting state.

1. Explicitly remove Pen and recompute the total​

  1. Load all Order scalars and its complete bounded item list. The selected list has a 101-item overflow sentinel; reject more than 100 and require a DRAFT Order, at least two items, and exactly one SKU-PEN.
  2. Validate each price, quantity and stored line total. Sum only the retained Notebook line: 2 × 1250 = 2500 cents. Do not calculate from a one-item preview.
  3. Mark Pen for deletion with the generated deletion API, update the Order total, and save both changes in one provider transaction.
  4. Read again through a normal query and a deleted-only query in a new process. Preserve tombstone identity; this is soft deletion, not physical row removal.
target.markForDeletion();order.updateTotalMinor(total);console.log("ORDER_REMOVAL_ATTEMPT");
await order.auditAs("Remove Order line and recompute total atomically").save(context);

Keep the marked target in the fully loaded Order graph, update the complete remaining total, then await one audited Order graph save. The example validates safe integer prices, quantities and totals before mutation. Marking an item or changing the total without the save has no durable effect.

The snippet assumes the full function has already selected the unique target and validated total. A generated delete marker does not validate prices, quantities, Order state or authorization. The fixture's business-number query is not a production buyer/tenant policy.

Read the tombstone deliberately​

The existing complete-read handler uses this explicit deleted-only request:

const deleted=await Q.orderItems().deletedRowsOnly().withSkuCodeIs("SKU-PEN").limit(10)
.comment("Read retained deleted Order lines").purpose("Verify explicit soft deletion after restart").executeForList(context);

This is a diagnostic query across the synthetic fixture, not an authorized cross-tenant endpoint. Use the separate isolation model and trusted scope for protected access. Ordinary relation loading excludes the tombstone.

Seed-based controlExpected durable resultFailure/recovery decision
remove-lineOrder total 2500, version 2; Notebook remains quantity 2/version 1; Pen remains in storage at version -2 and is absent from normal item reads. Other Orders stay unchanged.Treat the returned audited save/readback as the accepted removal; keep the tombstone ID for diagnosis.
omission-probe, on a separate fresh seedSaving the Notebook-only preview changes only the Order note/version. Total remains 3850 and both items stay live at version 1.Omission is not a delete command; explicitly mark the intended line.
remove-conflict, on a separate fresh seedA competing Pen label save advances Pen to version 2 with Pen (concurrent winner). The attempted total/deletion fails: Order stays total 3850, version 1, with original timestamp; both items remain live.Preserve the competing item. Reload the full Order/items and ask whether removal is still intended; do not publish a 2500 total after a failed transaction.

For the stale-removal control, the native log contains the Order UPDATE before the failing item UPDATE. The independent snapshot proves the earlier Order write rolled back. The winner's Pen update is intentionally retained; “rollback” does not mean erasing another transaction's commit. Do not catch a child failure inside the transaction and return a successful removal.

Walk through the successful removal​

From the site root, using the retained built workspace. Stop if a native command fails; expected business rejections are reported as normal JSON, not process failures.

export ORDER_WORKSPACE="$PWD/artifacts/order/2026-10-27/typescript/typescript-app-console"
export ORDER_FIXTURE="$PWD/static/examples/order/fixture.json"
export ORDER_DATABASE="$(mktemp -d)/orders.sqlite"
cd "$ORDER_WORKSPACE"
for phase in bootstrap seed remove-line complete-read; do
node -r ts-node/register src/main.ts "$phase"
done

Inspect ORDER_CASE for remove-line and complete-read, not only the exit status. To inspect omission-probe or remove-conflict, start another fresh seed and substitute that phase for remove-line; never chain all three on the same edited database. Version -2 is the result for this version-1 fixture, not a constant to assign to arbitrary deleted entities.

2. Apply an allowed state transition​

The model declares DRAFT (1001), CONFIRMED (1002) and CANCELLED (1003). The application implements this policy before calling a generated setter:

Current statusTo DRAFTTo CONFIRMEDTo CANCELLED
DRAFTRejectCommitCommit
CONFIRMEDRejectRejectCommit
CANCELLEDRejectRejectReject

Read the complete current Order with its version; require one target. Reject unknown target 9999, repeated states and prohibited reversals without saving. A generated status setter expresses a value change; it does not itself enforce this transition matrix or your service's authorization.

async function transition(context:UserContext) {
const target=required(process.env.ORDER_TARGET),next=Number(required(process.env.ORDER_NEXT_STATUS));
const rows=await Q.customerOrders().withOrderNumberIs(target).limit(2)
.comment("Load full Order before state transition").purpose("Apply the Order lifecycle policy with original version").executeForList(context);
assert.equal(rows.length,1);const order=rows[0],current=Number(E.customerOrder(order).orderStatusId().eval());
const result={target,from:current,to:next,observedVersion:E.customerOrder(order).version().eval()};
if(!((current===1001&&(next===1002||next===1003))||(current===1002&&next===1003)))return {...result,outcome:"rejected",reason:"Transition not allowed"};
if(next===1002)order.updateOrderStatusToConfirmed();else order.updateOrderStatusToCancelled();
await order.auditAs("Apply an allowed Order status transition").save(context);return {...result,outcome:"committed",reason:""};
}

ORDER_TARGET and ORDER_NEXT_STATUS are console inputs used to demonstrate the policy. A service should validate its request before passing the values to this operation. The returned observedVersion is the version read before save, not the version of the committed result; complete-read obtains current state.

On a separate fresh database, run the allowed path and rejection guards from the prepared application directory (the previous section established the same workspace and absolute ORDER_FIXTURE):

export ORDER_DATABASE="$(mktemp -d)/orders.sqlite"
run_order_phase() { node -r ts-node/register src/main.ts "$1"; }
run_order_phase bootstrap
run_order_phase seed
export ORDER_TARGET=ORD-2026-1001
export ORDER_NEXT_STATUS=1002
run_order_phase transition
run_order_phase guard-edit
export ORDER_NEXT_STATUS=1003
run_order_phase transition
run_order_phase transition
run_order_phase complete-read
Operation in that sequenceExpected result
DRAFT → CONFIRMEDoutcome=committed, observed version 1; subsequent read shows state 1002/version 2.
guard-edit while CONFIRMEDrejected=true, reason Only DRAFT orders can be edited; no quantity, total or version write.
CONFIRMED → CANCELLEDCommit with observed version 2; subsequent read shows state 1003/version 3.
CANCELLED → CANCELLED againoutcome=rejected, Transition not allowed; state/version remain 1003/3.

These transitions preserve the 3850 total and all item values/versions. Setting CANCELLED does not delete items, release stock or deliver an external event: the v1.0 recipe has no inventory reservation workflow. The retained state controls also cover DRAFT → CANCELLED, all prohibited pairs, unknown state and item edits on a CANCELLED Order. A rejected policy decision is a normal business result, not proof of a provider failure. If save itself fails, preserve that native error and use the conflict/provider decision below; do not return committed in a finally block.

3. Handle a stale Order version​

The existing stale-order control isolates the version decision from business math: two native processes change only the Order note. Both fully load the same existing version-1 Order before either is allowed to write.

StepWriter AWriter BAuthoritative database
LoadRetain original Order/version 1.Retain original Order/version 1.Still seed state; no writes before the readiness barrier.
Release BKeep waiting with version 1.Save Delivery note from writer B.B's note at version 2, independently checked before A is released.
Release AAttempt audited save with the original version 1; native rejection.Already finished.B's note, version and timestamp remain unchanged; amounts/items stay at seed values.
Read againReload authoritative state before any new business decision.—Fresh process sees B's committed version 2.

After its readiness/release barrier, this language's worker executes:

order.updateOrderNote(note); let outcome="committed",conflictReason="";
try { await order.auditAs("Revise Order note with optimistic version").save(context); }
catch(error) { outcome="conflict";conflictReason=String(error); }
console.log("ORDER_WORKER "+JSON.stringify({...result,outcome,conflictReason}));

The test worker catches every save error and reports its text. The controller, not that broad catch, proves this controlled failure is an optimistic conflict. Do not copy the catch as a generic production classifier.

Classify the error, then decide​

The native stale-run diagnostic in the existing typescript receipt is:

Error: Optimistic lock failed or CustomerOrder(1) does not exist

The recorded message can also describe a missing entity. In this controlled case both writers loaded an existing version-1 Order and the independent snapshot retains the winner. Reload within the same trusted scope before deciding between a stale revision and a missing Order.

The numeric entity ID in the recorded diagnostic belongs to that database; use the scoped business identity rather than copying it as a portable Order key.

OutcomeApplication action
Confirmed optimistic version rejectionReload the complete authoritative Order and needed items in the same trusted scope. Revalidate current state and amounts. Reject the stale command or reconcile the user's intent before creating a new audited attempt.
Policy says Transition not allowed / non-DRAFT editReturn that business rejection without a write. Do not retry it as a transient database problem.
Busy/locked, timeout, provider statement or COMMIT failurePreserve the native cause and diagnose transaction/provider cleanup. These errors do not count as version conflicts.
Missing Order after reloadHandle not-found in the same scope. Do not fabricate a successful edit or borrow an ID from another fixture.

Do not overwrite a losing object's version with the winner's version and resubmit its old fields. Do not put every failure into an unbounded retry loop. A reloaded Order may now be CONFIRMED/CANCELLED, or the Pen line may have changed; repeat the business checks before deciding whether to retry. A newly accepted edit needs its own audit reason and one complete transaction.

The worker files use a 45-second readiness/release protocol and are coordinated by the existing P1 runner. Running the bare worker phase without its controller will wait for release; use the stored round trace to understand the timeline. The same-Order control releases both writers together and permits one winner; the distinct-Order control expects two commits. Use the recorded ready/result snapshots to distinguish the one-target and two-target outcomes.

The retained stale race changes notes. It does not supply a complete service-level retry/merge policy for two concurrent state-transition commands. Keep the save failure boundary explicit and design that business decision at the service layer.

Existing evidence and next steps​

TopicWhere to inspect the retained result
Exact snippets and runtime/package qualificationCurrent core receipt: runtimes.typescript.verification.cases and the source-bound application above.
Explicit removal, omission, stale child and state matrixP1 completion receipt, with its original sources; the current core receipt reruns those cases with the quoted source.
Ordered stale writer and same/different Order racesP1 concurrency receipt, plus cases.stale-order in the current core receipt.
Failures and database invariantsNative errors, provider UPDATE/audit logs, before/after/fresh-read snapshots, and the removal/state expectations.

Use the existing receipt's controls for the three removal branches, steps for state decisions and rounds for writer ordering. A positive process exit from a negative-test reporter is not a successful business mutation. Do not relabel observedVersion as a returned committed version.

Continue with the TypeScript troubleshooting table, API usage guide and documentation coverage matrix. The first-edit tutorial and School regression baseline remain separate paths.

Continue with Order queries​

Use TypeScript Order Queries for continuous export, retained-ID page jumps and provider summaries. The query recipes use separate nine-/ten-order datasets; keep the fixture and context/store lifetime with the operation you are learning.

Continue with buyer and tenant isolation​

Use TypeScript Order Isolation to upgrade the model to v1.1 and build root/buyer scope from an application-owned trusted session.