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-readonly ingress 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

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 24, 2026 3:50pm

Request Review

@github-actions github-actions Bot 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 main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants