Skip to content

Commit 0fbbbf7

Browse files
committed
chore(changeset): name the sandboxed-hook data-door move, the find hooks, and the declared-5xx prose withhold
Contract-review round on #29519 (record PASS, should-fix 1 and notes 3-4): the byte-identical sentence is scoped to the direct refusal; a converted refusal relayed out of a sandboxed hook body now answers 500 INTERNAL_ERROR at the REST data door (was 400 with the inner sentence); the seam covers beforeFind/afterFind; the dispatcher data door and MCP tools withhold a declared 5xx's prose with status unchanged. Claude-Session: https://claude.ai/code/session_01ADHkmPCSqt8uDw17RmX9hu Co-authored-by: Claude <noreply@anthropic.com>
1 parent b81dcfa commit 0fbbbf7

1 file changed

Lines changed: 2 additions & 2 deletions

File tree

‎.changeset/29422-hook-write-stale-summary-refusal.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ Narrowing note: a write whose hook let its own write's stale-roll-up refusal esc
1313

1414
**BREAKING** for these answers only. `ERR_SUMMARY_RECOMPUTE` means "this write is stored, only a parent roll-up recompute failed". The data doors (REST, MCP) and the action, sandbox and flow writers answer it as the write's own success, using the refusal's `written` as the result.
1515

16-
- **A hook's own write.** Suppose a hook writes through `ctx.api` and lets that write's refusal escape. The refusal reached the caller of the write the hook is bound to, carrying the HOOK's record. So `POST /api/v1/data/:object` answered `201` with the id and record of the row the hook wrote. Under an aborting `after*` hook the write's own unit then committed both rows. Now the refusal is converted where it leaves the handler, for `before*` and `after*` hooks alike. The write refuses with an internal failure that names it, the hook and the stale summary, and keeps the original refusal as `cause`. An aborting `after*` hook's unit rolls both rows back, as `HookSchema.onError` `abort` says. With no unit (a code-registered hook, a driver without transactions, a caller's own unit), the message says that what was written before that point stays written. Every other error a hook throws passes through unchanged, and a hook declared `onError: 'log'` still does not fail the write.
16+
- **A hook's own write.** Suppose a hook writes through `ctx.api` and lets that write's refusal escape. The refusal reached the caller of the write the hook is bound to, carrying the HOOK's record. So `POST /api/v1/data/:object` answered `201` with the id and record of the row the hook wrote. Under an aborting `after*` hook the write's own unit then committed both rows. Now the refusal is converted where it leaves the handler, for `before*` and `after*` hooks alike, and for `beforeFind` / `afterFind` too: a find hook whose own write refuses now fails the read with the same declared `500` (the read failed before as well, only its sentence and status were inferred). The write refuses with an internal failure that names it, the hook and the stale summary, and keeps the original refusal as `cause`. An aborting `after*` hook's unit rolls both rows back, as `HookSchema.onError` `abort` says. With no unit (a code-registered hook, a driver without transactions, a caller's own unit), the message says that what was written before that point stays written. Every other error a hook throws passes through unchanged, and a hook declared `onError: 'log'` still does not fail the write.
1717
- **The refusal's lead on a nested cascade.** When a grandchild's delete refused, the parent's caller read `The delete of "<child>" did not complete`. Each outer cascade walk and each hook now re-leads the refusal, so the caller reads the write it asked for. `cause` stays the original refusal at every depth.
18-
- **A declared status.** The refusal a write answers when a nested write's refusal stopped it (a cascade delete, a rolled-back transaction, and now a hook) declares `status: 500` and `code: 'INTERNAL_ERROR'`. It no longer carries no `code`, which supersedes the sentence of the pending `@objectstack/objectql` changeset that said it did. The data door's answer is byte-identical: `500 { error: "Internal server error", code: "INTERNAL_ERROR" }`, now declared instead of reached by falling through every message heuristic. **The action door now answers these refusals `500 INTERNAL_ERROR`, not `400`**, with the same sentence.
18+
- **A declared status.** The refusal a write answers when a nested write's refusal stopped it (a cascade delete, a rolled-back transaction, and now a hook) declares `status: 500` and `code: 'INTERNAL_ERROR'`. It no longer carries no `code`, which supersedes the sentence of the pending `@objectstack/objectql` changeset that said it did. For the refusal itself, the REST data door's answer is byte-identical: `500 { error: "Internal server error", code: "INTERNAL_ERROR" }`, now declared instead of reached by falling through every message heuristic. **The action door now answers these refusals `500 INTERNAL_ERROR`, not `400`**, with the same sentence. **A converted refusal relayed out of a sandboxed hook body** (its `ctx.api` cascade delete or transaction was stopped, and the body let the error escape) **now answers `500 INTERNAL_ERROR` at the REST data door, where it answered `400` with the inner sentence.** The dispatcher data door and the MCP tools keep status `500` and code `INTERNAL_ERROR` for these refusals, but now withhold the prose, as they do for any declared 5xx.
1919
- **A failed rollback.** When `ObjectQL.transaction`'s rollback itself threw, the refusal still said "The transaction rolled back, so nothing written inside it was stored". It now says that the rollback failed and that what was written inside it may or may not be stored.
2020

2121
**The remedy.** Fix the summary field's `summaryOperations` (the engine's warning names why the recompute was refused), then retry. A hook that can tolerate a stale summary on its own write catches `ERR_SUMMARY_RECOMPUTE` itself.

0 commit comments

Comments
 (0)