Skip to content

fix(rest): route batch creates through the create ingress so readonly is enforced (#3835) - #3844

Closed
baozhoutao wants to merge 3 commits into
mainfrom
fix/issue-3835-batch-create-readonly
Closed

fix(rest): route batch creates through the create ingress so readonly is enforced (#3835)#3844
baozhoutao wants to merge 3 commits into
mainfrom
fix/issue-3835-batch-create-readonly

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Closes #3835.

Stacked on #3834 — it touches the same /batch handler block and this branch is cut from it, so the diff shows both until #3834 lands. Merge #3834 first; I'll rebase this to a single commit.

readonly meant two different things depending on which create endpoint you used

POST /data/:object runs the #3043 ingress strip, so a non-system caller cannot seed a read-only column — the field is dropped and reported. The cross-object transactional batch called ql.insert directly and skipped that ingress, and the engine's INSERT path is static-readonly-exempt by design (#3413 — the strip deliberately lives one layer up). So nothing enforced it on that route.

Measured on the showcase, showcase_contact.lead_score (readonly: true), same signed-in non-system user, identical payload:

before after
POST /data/showcase_contact lead_score = null, droppedFields reported unchanged
POST /batch (action: 'create') lead_score = 999 written, nothing said null, droppedFields reported

Why route to the ingress instead of re-applying the strip here

Reading stripReadonlyForInsert turned up a deliberate scope carve-out that a second copy would have lost: platform objects (sys_ / managedBy) are exempt on purpose, because their own guards must reject a forged value with 403 (ADR-0086's managed_by/package_id, #3004's owner_id anchor) — silently swallowing it would eat the very payload the guard exists to refuse. isSystem is exempt for the same class of reason.

That rules out the "sink the strip into the engine" option I floated in the issue: the engine has no notion of that boundary. So the batch's create ops now go through p.createData — the ingress itself. One create ingress, and a future change to its policy covers the batch for free (AGENTS.md PD #12: fix the producer, don't grow a second de-facto contract).

Mechanics that had to keep working, and do:

  • TransactiontrxCtx is passed as the context, so the insert joins this batch's transaction and rolls back with it (asserted in a test: the ingress receives the trx context, not a bare one).
  • { $ref: <opIndex> }results now holds the ingress's echoed record instead of the raw engine return; $ref resolves off .id either way. Verified live: an invoice + line batch produced a line whose invoice points at the invoice created in the same request.
  • droppedFields — the ingress's events fold into the batch's per-op list (审批场景下记录可写性的反馈全线失真:能改的显示「已锁定」,改不了的提示「更新成功」 #3794), so a batch create reports strips the way an update does.

Update ops are untouched — the engine enforces readonly (#2948) and readonlyWhen (#3042) on its own update path.

Tests

3 new in rest-batch-endpoint.test.ts: create ops go through the ingress with the transaction context; a forged readonly column never reaches the engine and comes back in droppedFields tagged with its op index; $ref still resolves through the ingress. The harness's protocol mock gained a createData that delegates to the same ql.insert, so existing assertions still read off ql.insert.

rest: 413 passing.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 9:22am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/plugin-approvals, @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 @objectstack/plugin-approvals, 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 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/plugin-approvals, @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/plugin-approvals, @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/plugin-approvals, @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.

@baozhoutao

Copy link
Copy Markdown
Contributor Author

折叠进 #3834 了——两个改动落在同一段 /batch 处理器上,拆成叠加 PR 只会让审核人多背一次合并顺序。这条提交已 cherry-pick 到 #3834 分支,#3835#3834 一并关闭。

@baozhoutao baozhoutao closed this Jul 28, 2026
@baozhoutao
baozhoutao deleted the fix/issue-3835-batch-create-readonly branch July 28, 2026 09:27
baozhoutao and others added 3 commits July 28, 2026 04:53
…ropped fields (#3794)

An approval flow reported record writability wrong in both directions: what the
user could change said "locked", and what they couldn't said "updated
successfully". Both halves were missing signal, not wrong behaviour — the server
did the right thing and told nobody.

`rowFromRequest` now emits `locks_record`, read from the same `node_config_json`
snapshot the `beforeUpdate` lock hook reads, with the same default-true. A client
could previously only see "a request is pending" and had to guess whether that
locked the record; the Console guessed "locked" every time, which hides the whole
point of a `lockRecord: false` node.

`POST /batch` (cross-object transactional batch) never wired `onFieldsDropped`,
so the one write path the Console's master-detail record form takes was also the
one path with no write-observability — a `readonlyWhen` strip there was
completely silent. It now collects per-op events and returns them as a top-level
`droppedFields` list tagged with each operation's index; omitted when empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dFields (#3794)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y` is enforced (#3835)

`POST /batch` called `ql.insert` directly, and the engine's INSERT path is
static-`readonly`-exempt by design (#3413) — the strip that stops a non-system
caller from seeding a read-only column lives at the protocol's create ingress
(#3043). So the same forged value was dropped on `POST /data/:object` and
written through on `/batch`: one rule, two answers.

Create ops now go through `p.createData`, the ingress itself, rather than a
second copy of the strip at the REST layer. One ingress means a future change to
its policy covers the batch for free, and the carve-outs it already encodes stay
intact — the platform-object exemption (a `sys_`/`managedBy` object's own guard
must REJECT a forged value, not silently swallow it) and the `isSystem`
exemption. `trxCtx` is passed as the context, so the insert still joins the batch
transaction and `$ref` resolution is unaffected; the ingress's `droppedFields`
fold into the batch's per-op list.

Update ops are untouched — the engine enforces both strips on its update path.

Co-Authored-By: Claude Opus 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.

跨对象事务批 POST /batch 绕过 create 侧的 readonly 入口剥离(#3043)

1 participant