Repository navigation
fix(metadata-protocol): deleteMany names the id entry it refuses - #29198
Conversation
A deleteMany whose ids array holds an empty string, null or an operator object is still refused with 400 VALIDATION_FAILED, but the answer now names each offending entry in fields[] as ids.N (required for an empty string, invalid_type otherwise), the shape the REST ingress already gives [null]. A non-array ids keeps the old sentence. Claude-Session: https://claude.ai/code/session_01ADHkmQJyGvWVm7dbChPMZ7 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dc385fa4996e997ee23f793743aa6faad200b48e && git checkout dc385fa4996e997ee23f793743aa6faad200b48e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 692e0dc8b936ad37311352868083d78745c76a21 9e074bf5c993869678459b60406304591572c1a0 && git checkout -B drift-repro 692e0dc8b936ad37311352868083d78745c76a21 && git merge --no-ff 9e074bf5c993869678459b60406304591572c1a0
node scripts/docs-audit/affected-docs.mjs --json 692e0dc8b936ad37311352868083d78745c76a21
|
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY 席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #29137. 4 files (+145 / −1): deleteManyData and a private deleteManyIdEntryField in metadata-protocol/src/protocol-batch-data.ts, pins in protocol.delete-many.test.ts and rest/src/rest-delete-many-ingress.test.ts (declared deviation: the REST door pin), a @objectstack/metadata-protocol patch changeset. Metadata-protocol: none. No packages/spec/**, no new error code, no gate. Not governed. |
| Rule | The one check is split: a non-array ids keeps its sentence and empty fields; an array with a non-id entry is still 400 VALIDATION_FAILED, now with one fields[] row per entry {field: 'ids.N', code, message} (required for "", invalid_type for null / object / boolean), the shape the REST ingress gives [null]. isScalarId is unchanged, so what is refused is unchanged (fail-closed for {"$ne":null} kept); [" "] still reaches the row loop. |
| Premise | Measured on base by the dev: [""] and ["a",""] answered the array sentence with fields: []. |
| Evidence | metadata-protocol 22069 pass / 26 skipped, typecheck 0; 17 REST deleteMany files 1049 pass; ablation red on both legs (protocol and REST through dist), restore clean. Gates 60 derived; check:dual-build-cjs-loads NOT MEASURED locally (needs a full build), left to CI. CI on this head is running; auto-merge waits for every check green or an expected skip. |
| Open question | required vs min_length for "": the seat takes required (the dev's recommendation; it matches the batch door's "Record id is required" and is in the ADR-0114 field catalog). |
| Out of scope | none. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay. |
Requested by Steve · project thread
Fixes #29137
Clause-②: no
Metadata-protocol: none
维护者速读
What changes
deleteManyData(the bulk delete door of@objectstack/metadata-protocol) used one check, with one sentence and nofields, for two different faults:idsis not an array, andidsis an array with an entry that is not a record id. It now splits them:idsnot an array: unchanged, the sentence "requires 'ids' to be an array of record ids" and nofields.idsan array with bad entries: still400 VALIDATION_FAILED, now with onefields[]row per offending entry,{ field: 'ids.N', code, message }, the shape the REST ingress already gives[null](zodIssuesToFields). An empty string answersrequired(the same fault the batch door'srowRequiredIdErrorcalls "Record id is required"), anything else that is not a string or number (null, an operator object, a boolean) answersinvalid_type. The sentence names the first offending entry, and says how many were refused when more than one.What is refused does not move: the fail-closed rule for non-scalar entries is unchanged, and a whitespace-only id still reaches the row loop and reports
RECORD_NOT_FOUNDfor its row. No new error code (the codes are existing members of the ADR-0114 field-level catalog), nopackages/specedit.Tests
packages/metadata-protocol/src/protocol.delete-many.test.ts: new cases for[''],['a', ''],[{"$ne":null}](still refused, now named),[null](same pair as the ingress), several bad entries named at once, a non-array (old sentence, nofields), and[' '](still reaches the row loop).packages/rest/src/rest-delete-many-ingress.test.ts: a new block through the real protocol at the REST door:{"ids":["a",""]}answers 400 withfieldsnamingids.1asrequired, and[null]keeps the ingress answerids.0/invalid_type.Verification
Base
fea2dbb7, head9e074bf5.Base reading (REST door in-process, real protocol, fake engine)
In-process (protocol called directly) every one of the first five got the array sentence and no
fields. Premise holds.Suites
pnpm --filter @objectstack/metadata-protocol test: Test Files 297 passed, 4 skipped; Tests 22069 passed, 26 skipped.typecheck: exit 0.packages/rest/src/*.test.tsfiles that mention deleteMany): Test Files 17 passed; Tests 1049 passed.pnpm exec eslint --no-inline-configon the three touched source/test files: exit 0.Ablation (two legs, each rebuilt, restored, git diff HEAD empty)
err.fields = idFields: 5 protocol cases red (expected undefined to deeply equal [ { field: 'ids.0', ... } ]) and the REST case red (expected [] to deeply equal [ [ 'ids.1', 'required' ] ]).expected 200 to be 400).ablation-replace.mjs --restore(blob equals HEAD), rebuilt,ablation-dist-preflight --absentpassed.Gates
dispatch-gates --commandsderived 60 commands; 58 exited 0.check-plugin-teardown-shape --self-testexited 3 on a shallow clone, then 0 after fetching its pinned fixture commit.check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: needs the whole workspace built): NOT MEASURED locally, CI runs it.--ranreconciliation: 60 derived, 60 run, 0 UNRUN.Acceptance notes
requiredfor an empty string (rather thanmin_length, which az.string().min(1)ingress would spell) follows the batch door's "Record id is required" for the same fault. Flagged to the seat as an open question.Generated by Claude Code