Repository navigation
fix(rest): every 2xx write answer whose body reports droppedFields also sends the X-ObjectStack-Dropped-Fields header - #29034
Conversation
…ropped-fields header Clone, createMany, the per-object batch, updateMany, an import commit and the cross-object batch reported a legal write strip in the body only. They now call applyDroppedFieldsHeader as single create and update do. The seam reads a door's per-row reports when passed and names each token once, so a bulk header is the union of the rows' strips; the body keeps the per-row detail. An import dry run writes nothing and sends no header. The spec describe text, the client CloneDataResult docblock and the data API docs stop saying these answers carry no header; the reference docs are regenerated. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5nE2GswUTcEX8HbTmt
📓 Docs Drift CheckThis PR changes 3 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 175 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 8a51c90efc8d05c204fe889617ca3dd8e5bf2e46 && git checkout 8a51c90efc8d05c204fe889617ca3dd8e5bf2e46
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab085a048241b0bf1fde06696fc6cbf659a6707f a6b0d839d45a4e08037beab8bd4efb60b2fcb369 && git checkout -B drift-repro ab085a048241b0bf1fde06696fc6cbf659a6707f && git merge --no-ff a6b0d839d45a4e08037beab8bd4efb60b2fcb369
node scripts/docs-audit/affected-docs.mjs --json ab085a048241b0bf1fde06696fc6cbf659a6707f
|
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo Contract reviewServed-tier: Face: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo Landing record · PR #29034
Release: ships Generated by Claude Code |
Requested by Leehom · project thread
Fixes #28933
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
droppedFields,现在也会发X-ObjectStack-Dropped-Fields响应头,和单条创建、更新一样,走同一个applyDroppedFieldsHeader。packages/rest/src/error-response.ts(头的格式去重、读逐行报告);packages/rest/src/batch-routes.ts(createMany、batch、updateMany、跨对象 batch 四处调用);packages/rest/src/data-action-routes.ts(克隆与导入提交;导入预演不发头)。pnpm --filter @objectstack/rest exec vitest run src/rest-dropped-fields-every-write-door.test.ts,预期 15 个用例全过。What changes
Single
POST /data/:objectandPATCH /data/:object/:idadvertised a legal write strip (readonly, a truereadonlyWhen) in the body and in theX-ObjectStack-Dropped-Fieldsheader (#3431). Every other write door that reports the same strip in its body now sends the header too, through the existingapplyDroppedFieldsHeaderand in the samefield;reason=REASONformat. There is one seam and no second formatter.POST /data/:object/:id/clonedroppedFieldsPOST /data/:object/createManydroppedFields, already the rows' unionPOST /data/:object/batchresults[i].droppedFieldsPOST /data/:object/updateManyresults[i].droppedFieldsPOST /data/:object/import, commitresults[i].droppedFieldsPOST /batch, cross-objectdroppedFields, one event per op with itsindexapplyDroppedFieldsHeader(res, result, rows?)readsresult.droppedFieldsand, when a door passes them, each row'sdroppedFields. The formatter now emits eachfield;reasontoken once. A single-record answer never repeats a token, so its header value is unchanged (the existing rest: PATCH /data 对被静默剥离的写入字段无任何回传 — onFieldsDropped 通道未接线(follow-up #3407/#3413) #3431 cases pass unedited).201over a write that landed under a failing roll-up (data writes: form submit, batch / *ManyData, cloneData, the MCP create_record bridge and callData's fallback still answer 500 after a landed write when the summary recompute fails #28782) now goes throughearnedWriteDespiteStaleSummary, which sets the header as single create's earned answer already did..describe()text on clone's and createMany'sdroppedFieldsand on the batch row (BatchOperationResultSchema) said these answers carry no header. So did theCloneDataResultdocblock in@objectstack/clientand the clone section ofcontent/docs/api/data-api.mdx. They now say the header is sent;content/docs/references/api/{batch,protocol}.mdxare regenerated (gen:schema && gen:docs). No key, type or status changes.@objectstack/rest,@objectstack/specand@objectstack/clientpatch. It states that a bulk header is the union and the body keeps the per-row detail.Tests
packages/rest/src/rest-dropped-fields-every-write-door.test.ts(new, 15 cases):RestServerover the realObjectStackProtocolImplementationandObjectQLon better-sqlite3. It covers every door in the table: header value, body unchanged, a mixed createMany naming the field once, rows stripping different fields, and a no-strip control per door. It also covers the import dry run sending no header, and the stale roll-up answer of clone and createMany through a protocol double.packages/qa/dogfood/test/showcase-static-readonly.dogfood.test.ts(four cases added) on the stock showcase as admin: clone withoverrides.lead_scoreand a two-row createMany carryinglead_scoresendlead_score;reason=readonly; a mixed createMany names it once; a createMany that strips nothing sends no header.Acceptance notes
POST /data/:object. That route refuses an array body (400 VALIDATION_FAILED, 请求体从不与声明它的 schema 对照(#3877 的请求侧对偶):7 个 schema 定义了从未启用,而 API 目录已宣称生效 #3899). The bare-array door isPOST /data/:object/createMany, which is the door fixed here.cloneDatacopies every column it does not reset, null ones included, and the engine's strip reports a present key whatever its value. So a plain clone of a row whose readonly column is empty answersdroppedFieldsnaming that column (measured on main: a clone of a row with nullro2, and a clone of a showcase contact with no override, both name the column). The header now repeats what the body says. This PR does not change the producer. That is why the clone cases use objects of their own and the dogfood clone case asserts withtoContain. It is handed to the seat as a finding..describe()text is updated to say so.droppedFields. The async import job: its202carries none, and its results route is a read. The runtime dispatcher'sPOST/PATCH /dataarms (the@objectstack/honocatch-all twin) also set no header, for single create and update too. That is outside this card's REST doors; observed, not measured live.crud-routes.ts,batch-routes.ts,data-action-routes.ts,error-response.tsand the two spec files. fix(metadata-protocol): the by-id OCC comparison answers only for a row the caller reads #28731 is the only nearby one, and its files are inmetadata-protocol,runtimeand dogfood. No shared file.Verification
Everything below was measured at
a6b0d839.Red first on main (135054e + the new tests only)
Green
Ablation (scripts/ablation-replace.mjs, anchor hits 1, restore proven blob == HEAD each time)
Dist-resolved dogfood leg: the clone and createMany calls were replaced by a marker, rest was rebuilt, and
ablation-dist-preflightfound the marker in dist. Three dogfood cases went red (clone, two-row createMany, mixed createMany), with 5 passed. After the restore, rest was rebuilt, the marker was absent from dist and the tree was clean, and all 8 passed.Derived gates (dispatch-gates --commands, local subset)
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5nE2GswUTcEX8HbTmt
Generated by Claude Code