Skip to content

fix(objectql): enforce array shape for multi-value fields in the write pipeline#2553

Merged
os-zhuang merged 1 commit into
mainfrom
fix/objectql-multivalue-shape-2552
Jul 4, 2026
Merged

fix(objectql): enforce array shape for multi-value fields in the write pipeline#2553
os-zhuang merged 1 commit into
mainfrom
fix/objectql-multivalue-shape-2552

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Closes #2552

Problem (scanned from source, verified via REST e2e)

The write pipeline stored a lone scalar sent at a multi-value field verbatimPATCH { labels: "frontend" } at a multiselect returned 200 and the column read back as a string, silently corrupting the array shape for every consumer. Found via the console bulk-edit dialog (pre-#2186 objectui sent scalars for multi params), but the server-side hole accepts it from ANY client.

Worse, select+multiple mis-rejected legal arrays: the single-select validation branch (record-validator.ts) stringified ["a","b"] to "a,b" and threw invalid_option.

Scan also confirmed the runtime expands Field.user to type: 'user' (NOT 'lookup') — e2e against the showcase team_members field caught that a lookup-only type set missed it. Per packages/spec/src/data/field.zod.ts:377, multiple applies to select/lookup/file/image, and user is stored identically to lookup.

Fix (@objectstack/objectql)

  • normalizeMultiValueFields (new, called at all 4 validateRecord call-sites in engine.ts): coerces unambiguous scalars (string/number/boolean) into single-element arrays IN PLACE before validation, for multiselect/checkboxes/tags and select/radio/lookup/user/file/image with multiple: true.
  • validateRecord: multi-value fields must now be arrays — anything un-wrappable (plain objects, junk) is rejected with a new invalid_type code instead of hitting storage. Array elements are still validated against options; reference/attachment types skip options (integrity handled elsewhere).
  • Single select/radio branch now guarded with multiple !== true, fixing the legal-array mis-rejection.

Tests

  • Unit: record-validator.test.ts — normalization, shape rejection, element option checks, single-select non-regression (incl. user/file types).
  • Engine-level: engine-multivalue-normalize.test.ts (new) — asserts on what the driver receives across insert/update for all covered types.
  • Full @objectstack/objectql suite: 59 files / 737 tests pass.
  • REST e2e on a live showcase dev server (SQLite inspected directly):
    • scalar PATCH {"team_members":"<uid>"} at a user+multiple field → stored as ["<uid>"]
    • junk object → 400 invalid_type
    • illegal option scalar → 400 invalid_option
    • legal arrays pass through unchanged ✅

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

@vercel

vercel Bot commented Jul 4, 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 4, 2026 1:42am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 4, 2026
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

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

  • content/docs/concepts/core/services.mdx (via @objectstack/objectql)
  • content/docs/concepts/implementation-status.mdx (via @objectstack/objectql)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/concepts/packages.mdx (via @objectstack/objectql)
  • content/docs/guides/authentication.mdx (via @objectstack/objectql)
  • content/docs/guides/deployment-vercel.mdx (via @objectstack/objectql)
  • content/docs/guides/formula.mdx (via packages/objectql)
  • content/docs/guides/kernel-services.mdx (via @objectstack/objectql)
  • content/docs/guides/objectql-migration.mdx (via @objectstack/objectql)
  • content/docs/guides/packages.mdx (via @objectstack/objectql)
  • content/docs/guides/plugins.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)

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.

@os-zhuang
os-zhuang merged commit 24b62ee into main Jul 4, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the fix/objectql-multivalue-shape-2552 branch July 4, 2026 02:52
…e pipeline (#2552)

A lone scalar sent at a multiselect / checkboxes / tags / select+multiple /
lookup+multiple field used to pass validation and reach the driver verbatim
(PATCH {"labels":"frontend"} → 200, stored as a string), silently corrupting
the column shape for every array-consumer. select+multiple was worse: a legal
ARRAY was stringified to "a,b" and mis-rejected as invalid_option, while
lookup+multiple had no shape check at all.

- normalizeMultiValueFields(): wrap unambiguous scalars (string/number/boolean)
  into single-element arrays, in place, before validation — called at all four
  validateRecord sites in engine insert/update (covers REST, engine callers,
  and SeedLoader, which all funnel through them)
- validateOne(): multi-value fields must be arrays; anything un-wrappable
  (plain objects, nested junk) is rejected with a new `invalid_type` code
  instead of hitting storage
- select/radio + multiple:true now routes through the multi-value branch with
  per-element option validation

Closes #2552

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

objectql: write pipeline accepts scalar values for multi-value fields (multiselect/tags/select+multiple/lookup+multiple), corrupting stored shape

2 participants