Skip to content

fix(objectql): enforce validation rules, requiredWhen and option visibleWhen on multi-row updates - #3160

Merged
os-zhuang merged 2 commits into
mainfrom
claude/validation-multi-row-updates-kqxgg4
Jul 18, 2026
Merged

fix(objectql): enforce validation rules, requiredWhen and option visibleWhen on multi-row updates#3160
os-zhuang merged 2 commits into
mainfrom
claude/validation-multi-row-updates-kqxgg4

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Summary

Closes#3106.

evaluateValidationRules() is the single gate for object-level validation rules (script, state_machine, format, cross_field, json_schema, conditional), field-level requiredWhen, and per-option visibleWhen authorization. It ran on insert and single-id update but was never called in the bulk branch of engine.update (options.multidriver.updateMany) — every one of those checks was a silent no-op there, with only a narrow warning that skipped format/json_schema-only schemas entirely.

Per the decision on the issue, this is the full enforcement option:

  • The multi branch now reads the row-scoped match set once — the same middleware-composed AST the write binds, with the fetch shared with the 安全:readonlyWhen 服务端剥离仅覆盖单条 update 路径,多行 updateMany 不强制(条件只读可被批量绕过) #3042readonlyWhen bulk strip (needsPriorRecord subsumes hasReadonlyWhenInPayload, so there is never a second query).
  • The stripped payload is evaluated against each matched row's prior state, mirroring the single-id branch's ordering (strips first, then rules). Any error-severity violation throws ValidationError — annotated with the failing record id — before updateMany writes anything (all-or-nothing, like the strip's locked-in-any-row rule).
  • Schemas with no prior-dependent rules (format / json_schema / non-prior conditional only) are evaluated once against the payload with no fetch.
  • Rule-free schemas hit the evaluator's early return: the internal multi: true callers (messaging outboxes, settings rows) see zero behavior change and no extra query — covered by a regression test.

Deliberate behavior change: bulk writes that previously slipped past declared rules now throw. Neither REST nor GraphQL surfaces multi, so this is unreachable from untrusted external callers; in-process callers with rule-bearing schemas are the affected surface.

Also fixes the two doc comments that overstated coverage (rule-validator.ts "no silent no-ops", validation.zod.ts "nothing is a silent no-op") — both now describe the per-row bulk enforcement and name the remaining events: ['delete'] gap, which stays split out per the issue decision — and refreshes the AGENTS.md PD #10 example that described the pre-fix state.

Changes

  • packages/objectql/src/engine.ts — per-row rule evaluation in the multi branch; single shared prior-row fetch; obsolete warning removed; ValidationError imported for the row-annotated rethrow.
  • packages/objectql/src/validation/rule-validator.ts, packages/spec/src/data/validation.zod.ts — doc comments corrected (no logic change).
  • AGENTS.md — PD chore: version packages #10 example updated to past tense with the same "check the call site" moral.
  • .changeset/bulk-update-validation-rules.md — patch changeset for @objectstack/objectql + @objectstack/spec.

Tests

Verified: objectql suite 67 files / 917 tests green, spec suite 255 files / 6895 tests green, objectql build green, eslint clean on changed files.

Known gaps (out of scope, tracked separately)

🤖 Generated with Claude Code

https://claude.ai/code/session_01VCUSMJBsX14C3RQdWFw7N7


Generated by Claude Code

…bleWhen on multi-row updates (#3106)
The bulk branch of engine.update (options.multi → driver.updateMany) never
called evaluateValidationRules, so every object-level rule (script,
state_machine, format, cross_field, json_schema, conditional), field-level
requiredWhen, and per-option visibleWhen check was a silent no-op there —
the only signal was a warning that skipped format/json_schema-only schemas
entirely.
The engine now reads the row-scoped match set once (the same middleware-
composed AST the write binds, shared with the #3042 readonlyWhen strip) and
evaluates the stripped payload against each matched row's prior state; any
error-severity violation throws ValidationError (annotated with the failing
record id) before updateMany writes anything. Schemas needing no prior state
are evaluated once against the payload with no fetch; rule-free schemas
(messaging outboxes, settings rows) hit the evaluator's early return and are
unaffected.
Deliberate behavior change: bulk writes that previously slipped past
declared rules now throw.
Also corrects the two doc comments that overstated coverage
(rule-validator.ts, validation.zod.ts) — both now name the remaining
events:['delete'] gap, which is tracked separately — and refreshes the
AGENTS.md PD #10 example that described the pre-fix state.
Closes#3106
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCUSMJBsX14C3RQdWFw7N7
@vercel

vercelBot commented Jul 18, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 18, 2026 3:17am

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec.

107 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/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @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/objectql, 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 packages/objectql, @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/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.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/validating-metadata.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/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • 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/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql, @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 @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql)
  • 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 packages/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/objectql, @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/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/objectql, @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.

…c comment
The validation.zod.ts module doc comment feeds the generated
content/docs/references/data/validation.mdx; regenerate so `check:docs`
passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCUSMJBsX14C3RQdWFw7N7
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Validation rules, requiredWhen and option visibleWhen are silently skipped on multi-row updates (options.multi)

2 participants

@os-zhuang@claude