feat(rest): surface silently-dropped write fields on PATCH/POST /data (#3431)#3448
Merged
Merged
Conversation
…#3431) Wire the engine's `onFieldsDropped` strip-observability channel (#3413) through the DataProtocol and REST write path so an external API caller is no longer silently stripped when `readonly` (#2948) / `readonlyWhen` (#3042) drops caller-supplied fields. The write still succeeds — this only makes the strip observable (the same silent-success class #3407 fixed flow-side). - metadata-protocol: `updateData` collects the engine's `onFieldsDropped` events; `createData` surfaces the #3043 static-`readonly` INGRESS strip via a payload diff (that strip runs BEFORE the engine, which is INSERT-readonly- exempt, so the engine listener never sees it). Both attach an optional `droppedFields` list to the response when ≥1 field was dropped. - spec: `UpdateDataResponseSchema` / `CreateDataResponseSchema` gain an optional `droppedFields: DroppedFieldsEvent[]` — present only on a drop, so the shape stays backward-compatible for clients that only read `record`. - rest: PATCH `/data/:object/:id` and POST `/data/:object` echo drops as the `X-ObjectStack-Dropped-Fields` response header and keep the structured list on the body. Status/success semantics unchanged (200 update / 201 create). Tests: protocol passthrough (update forwards engine events; create surfaces the ingress strip; no-drop omits the key) and REST header/body (single + multi field, no-drop, create 201). Deferred (issue #3431 D2 open questions): bulk (`updateManyData` / `createManyData` / `batchData`) and GraphQL mutation wiring, typed `@objectstack/client` warnings, and adding the header to the Hono CORS `exposeHeaders` allow-list for cross-origin browser reads. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNqeAmxCCq9jgLFwa9subq
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 104 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…#3431) `content/docs/references/api/protocol.mdx` is generated from the spec Zod schemas (build-docs.ts) and is checked in; the new optional `droppedFields` on Create/Update DataResponse must be regenerated so `check:docs` (run inside the TypeScript Type Check job) stays green. Generated output only — no hand-edits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNqeAmxCCq9jgLFwa9subq
os-zhuang
marked this pull request as ready for review
July 24, 2026 15:10
Closed
5 tasks
os-zhuang
added a commit
that referenced
this pull request
Jul 25, 2026
… paths + client SDK (#3455) (#3468) * feat(rest/protocol): extend droppedFields write-observability to bulk paths + client SDK (#3455) Follow-up to #3448 (#3431 D2). Single-write PATCH/POST /data already surfaces LEGALLY-stripped write fields (readonly #2948 / readonlyWhen #3042 / #3043 create ingress) as `droppedFields`; the bulk paths dropped them silently. This closes out the deferred波及面. - metadata-protocol: updateManyData + batchData collect per-row onFieldsDropped and attach to each result row; insertManyData attaches per-row via ingress diff; createManyData returns an aggregated top-level droppedFields (no per-row slot; static-readonly strip is schema-uniform). Correctness fix: updateManyData and batchData never threaded the caller `context` — bulk writes ran context-less (RLS/FLS/readonlyWhen without the principal; batch create strip forced non-system). All engine calls now run under the resolved context. - spec: BatchOperationResultSchema gains optional per-row droppedFields (covers updateMany + batch); CreateManyDataResponseSchema gains the aggregated one. Both omit-when-empty. No X-ObjectStack-Dropped-Fields header for batches by design — one header cannot express per-row drops, so the body is the canonical channel. - client: CreateDataResult / UpdateDataResult gain droppedFields?: DroppedFieldsEvent[]. - hono adapter + plugin-hono-server: x-objectstack-dropped-fields added to the default Access-Control-Expose-Headers (lockstep across both CORS sites). - Design decision: kept precise droppedFields (reused DroppedFieldsEventSchema) over a generic warnings envelope, for symmetry with the shipped single-write path. - GraphQL item is a no-op: GraphQL has no runtime (kernel.graphql unassigned, handleGraphQL 501s, discovery never advertises it) — nothing to wire until an engine lands, at which point the protocol-layer droppedFields is already present. Tests: 10 bulk protocol cases (per-row + aggregated + context threading + returnRecords=false) and a CORS default-expose assertion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(spec): regenerate api reference for bulk droppedFields fields (#3455) Generated from the batch/protocol Zod schemas — BatchOperationResult (per-row) and CreateManyDataResponse (aggregated) gained droppedFields. Regenerated via gen:schema && gen:docs; these files are generated, not hand-edited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(spec): regenerate objectstack-api skill references for droppedFields transitive deps (#3455) batch.zod.ts now imports DroppedFieldsEventSchema from data-engine.zod, whose transitive imports (kernel/execution-context, security/explain) pull those into the objectstack-api skill's referenced-schema set. Regenerated via gen:skill-refs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3431. Follow-up to #3407 / #3413.
Problem
#3413 built the engine-level strip-observability channel
(
WriteObservabilityOptions.onFieldsDropped) and wired the flow side(
update_record/create_recordemit a step warning +droppedFields). TheREST write path was never wired, so an external API caller writing N fields
still got a bare
200 + recordwhenreadonly(#2948) /readonlyWhen(#3042)stripping meant
< Nactually landed — the only way to notice was a per-fielddiff of the returned row (which need not echo every field). Same silent-success
class #3407 fixed flow-side, just on HTTP.
Design decisions
The issue left the feedback shape "待拍板" (to be decided). This PR ships both
robust channels, resolving the D1/D3 open points:
droppedFieldsevent list — the issue's own sanctioned option ("protocol 结果附带事件列表"). This is the reliable, structured, cross-origin-safe surface and
feeds every protocol consumer, not just REST.
X-ObjectStack-Dropped-Fieldsresponse header (zero body-contract intrusion,matching the existing
X-Export-Styles: droppedprecedent).Status/success semantics are unchanged (200 update / 201 create) — a strip
is legitimate semantics, not a failure (same principle as #3413). The FLS write
gate is untouched (already fails closed with 403).
Changes
@objectstack/metadata-protocolupdateDataregisters anonFieldsDroppedcollector onengine.updateandreturns the events as
droppedFields.createDatasurfaces the 安全/设计:静态 readonly 的 INSERT 豁免让审批/状态字段可在创建时被直接播种(比 #3003 少一步) #3043 static-readonlyingress strip too — thatstrip runs at the protocol ingress (
stripReadonlyForInsert), before theengine (which is INSERT-
readonly-exempt), so it is recovered by diffing thesupplied payload against the stripped one (
diffDroppedFields). The engine'sonFieldsDroppedis also wired for a future insert-side engine strip. A faultylistener never breaks the write (the engine catches + logs).
@objectstack/specUpdateDataResponseSchema/CreateDataResponseSchemagain an optionaldroppedFields: DroppedFieldsEvent[]— present only on a drop, so the shapestays backward-compatible for clients that only read
record.@objectstack/rest/data/:object/:idand POST/data/:objectecho drops as theX-ObjectStack-Dropped-Fieldsheader (field;reason=<reason>tokens,comma-joined) and keep the structured list on the body. Tolerates both the
Hono-style
res.headerand node-styleres.setHeader.Testing
protocol.dropped-fields.test.ts— update forwards engine events (single +multi-pass); create surfaces the ingress strip; no-drop omits the key; system
create keeps the field.
rest-dropped-fields.test.ts— PATCH/POST set the header + keep bodydroppedFields, multi-field/reason join, no-header when nothing dropped,create stays 201.
tsc --noEmitclean for the touched files.
Out of scope (issue #3431 D2 open questions, deferred)
updateManyData/createManyData/batchDataand GraphQLmutation.
@objectstack/clientwarnings (the bodydroppedFieldsis alreadyreadable; typing it is a follow-up).
exposeHeadersallow-list for cross-originbrowser reads (three-site lockstep) — the body
droppedFieldsis thecross-origin-safe channel meanwhile.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NNqeAmxCCq9jgLFwa9subq
Generated by Claude Code