Skip to content

feat(rest): surface silently-dropped write fields on PATCH/POST /data (#3431) - #3448

Merged
os-zhuang merged 2 commits into
mainfrom
claude/patch-data-fields-dropped-4d0qpn
Jul 24, 2026
Merged

feat(rest): surface silently-dropped write fields on PATCH/POST /data (#3431)#3448
os-zhuang merged 2 commits into
mainfrom
claude/patch-data-fields-dropped-4d0qpn

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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_record emit a step warning + droppedFields). The
REST write path was never wired, so an external API caller writing N fields
still got a bare 200 + record when readonly (#2948) / readonlyWhen (#3042)
stripping meant < N actually landed — the only way to notice was a per-field
diff 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:

  • Passthrough (D3): the DataProtocol result carries an optional
    droppedFields event 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.
  • REST header (D1): PATCH/POST additionally echo an
    X-ObjectStack-Dropped-Fields response header (zero body-contract intrusion,
    matching the existing X-Export-Styles: dropped precedent).

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-protocol

  • updateData registers an onFieldsDropped collector on engine.update and
    returns the events as droppedFields.
  • createData surfaces the 安全/设计:静态 readonly 的 INSERT 豁免让审批/状态字段可在创建时被直接播种(比 #3003 少一步) #3043 static-readonlyingress strip too — that
    strip runs at the protocol ingress (stripReadonlyForInsert), before the
    engine (which is INSERT-readonly-exempt), so it is recovered by diffing the
    supplied payload against the stripped one (diffDroppedFields). The engine's
    onFieldsDropped is also wired for a future insert-side engine strip. A faulty
    listener never breaks the write (the engine catches + logs).

@objectstack/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.

@objectstack/rest

  • PATCH /data/:object/:id and POST /data/:object echo drops as the
    X-ObjectStack-Dropped-Fields header (field;reason=<reason> tokens,
    comma-joined) and keep the structured list on the body. Tolerates both the
    Hono-style res.header and node-style res.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 body
    droppedFields, multi-field/reason join, no-header when nothing dropped,
    create stays 201.
  • Full suites green: spec 6849, metadata-protocol 58, rest 335. tsc --noEmit
    clean for the touched files.

Out of scope (issue #3431 D2 open questions, deferred)

  • Bulk wiring: updateManyData / createManyData / batchData and GraphQL
    mutation.
  • Typed @objectstack/client warnings (the body droppedFields is already
    readable; typing it is a follow-up).
  • Adding the header to the Hono CORS exposeHeaders allow-list for cross-origin
    browser reads (three-site lockstep) — the body droppedFields is the
    cross-origin-safe channel meanwhile.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NNqeAmxCCq9jgLFwa9subq


Generated by Claude Code

…#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
@vercel

vercelBot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 24, 2026 3:50pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/rest, @objectstack/spec.

104 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-protocol, packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…#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
os-zhuang marked this pull request as ready for review July 24, 2026 15:10
@os-zhuang
os-zhuang merged commit 5ac93d4 into mainJul 24, 2026
16 of 17 checks passed
@os-zhuang
os-zhuang deleted the claude/patch-data-fields-dropped-4d0qpn branch July 24, 2026 15:10
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rest: PATCH /data 对被静默剥离的写入字段无任何回传 — onFieldsDropped 通道未接线(follow-up #3407/#3413)

2 participants

@os-zhuang@claude