Skip to content

fix(core): validate multi-column fields before splitting into physical columns#794

Merged
borisno2 merged 1 commit into
mainfrom
claude/funny-sagan-ryzxcn
Jul 21, 2026
Merged

fix(core): validate multi-column fields before splitting into physical columns#794
borisno2 merged 1 commit into
mainfrom
claude/funny-sagan-ryzxcn

Conversation

@borisno2

Copy link
Copy Markdown
Member

Summary

  • Moves the splitColumns step for multi-column fields (currently only @opensaas/stack-storage's image()/file() in db.columns: 'keystone' mode) so it runs after validateFieldRules instead of inline inside executeFieldResolveInputHooks (Hook Pipeline Phase 1.5, before validation).
  • Previously, an unrecognised/non-nullish value returned by a field's resolveInput "let validation catch it" fallback was split into null/undefined physical columns and written silently — validateFieldRules never saw the logical field because it had already been deleted from resolvedData.
  • executeFieldResolveInputHooks (packages/core/src/hooks/index.ts) now only resolves each field's value under its logical key — no split. A new splitMultiColumnFields function performs the split, gated by the exact same field-level write-access check (checkFieldAccess) that previously ran inline, so a denied field still drops its logical key and contributes none of its per-part columns (sudo still bypasses).
  • Updated the three call sites in lockstep, since each independently runs the same resolveInput → validate → validateFieldRules → filterWritableFields sequence:
    • packages/core/src/context/hook-pipeline.ts (top-level Write Pipeline) — added as Phase 4, after Phase 3 (validateFieldRules).
    • packages/core/src/context/nested-operations.tsprocessNestedCreate and processNestedUpdate (nested relationship writes).
  • Updated CONTEXT.md's Hook Pipeline glossary entry and comments in field-access.ts that referenced the old gate location.

Test plan

  • packages/core/src/access/multi-column-read-write.test.ts retargeted: the split-behavior tests now drive the new splitMultiColumnFields function directly (access-gate and null-split coverage preserved unweakened); added a new describe block proving executeFieldResolveInputHooks no longer splits and passes unrecognised values through unsplit under the logical key.
  • Added packages/core/tests/multi-column-validation-ordering.test.ts — full context.db integration tests proving:
    • top-level create/update with an unrecognised multi-column value throws ValidationError and never reaches the DB;
    • the same holds through a nested relationship create and update;
    • the legitimate shapes (null/undefined, a valid shaped object) continue to split and persist identically to before.
  • pnpm test passes in packages/core (44 files / 898 tests) and packages/storage (7 files / 148 tests, unaffected).
  • pnpm build (core), pnpm lint, pnpm manypkg fix, pnpm format all clean.
  • Changeset added (patch — bug fix, no public API change).

Closes #789


Generated by Claude Code

…l columns

Move the splitColumns step (storage image()/file() in Keystone-parity
mode) to run AFTER validateFieldRules instead of inline inside
executeFieldResolveInputHooks. Previously an unrecognised value from a
field's resolveInput fallback bypassed validation entirely, since it
was split into null/undefined physical columns before the field's Zod
schema ever saw it. Updated in lockstep across the top-level Hook
Pipeline and both nested create/update write paths.

Closes #789

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPX69aoqSuBMAwD3bDCSWk
@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0cd584b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@opensaas/stack-core Patch
@opensaas/stack-auth Patch
@opensaas/stack-cli Patch
@opensaas/stack-rag Patch
@opensaas/stack-storage Patch
@opensaas/stack-tiptap Patch
@opensaas/stack-ui Patch
@opensaas/stack-storage-s3 Patch
@opensaas/stack-storage-vercel Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
stack-docs Ready Ready Preview, Comment Jul 21, 2026 11:30am

Copy link
Copy Markdown
Member Author

Self-review

Overview

Moves the splitColumns step for multi-column fields (storage image()/file() in db.columns: 'keystone' mode) so it runs after validateFieldRules instead of inline inside executeFieldResolveInputHooks (which ran before validation). executeFieldResolveInputHooks now only resolves each field's value under its logical key; a new splitMultiColumnFields (in packages/core/src/hooks/index.ts) performs the split + field-access gate, called from all three write paths that independently run resolveInput → validate → validateFieldRules → filterWritableFields: the top-level Hook Pipeline (hook-pipeline.ts, new Phase 4) and nested create/update (nested-operations.ts).

Correctness

  • Verified exactly 3 call sites for the new split step (hook-pipeline.ts ×1, nested-operations.ts ×2) — matches the issue's "three call sites... in lockstep" requirement, confirmed by grep.
  • Field-level write-access gate is preserved byte-for-byte: same checkFieldAccess call, same args, same "drop logical key on deny" behavior, sudo bypass intact.
  • connectOrCreate's create branch reuses processNestedCreate, so it inherits the fix without a separate call site.
  • Confirmed via tsc --noEmit that the resolvedData type threading through let resolvedData = ... reassignments across executeResolveInputexecuteFieldResolveInputHookssplitMultiColumnFields stays consistent.

Minor (non-blocking) observation

applyCreateDefaults (Phase 1.75) now runs before the split instead of after (previously split was at old Phase 1.5, before defaults). This means a multi-column field with a defaultValue that's omitted from input will now actually get its default value split into physical columns, whereas previously it wouldn't have been split at all (and would likely have broken the write, since the raw JSON-shaped logical key isn't a real Prisma column in multi-column mode). This looks like an incidental improvement rather than a regression, but it's not explicitly covered by a test — worth keeping in mind if image()/file() ever gain defaultValue support in multi-column mode.

Test coverage

  • multi-column-read-write.test.ts retargeted: split-behavior tests now drive splitMultiColumnFields directly; access-gate and null-split coverage preserved unweakened; added coverage proving executeFieldResolveInputHooks no longer splits and passes unrecognised values through under the logical key.
  • New multi-column-validation-ordering.test.ts: full context.db integration tests proving an unrecognised value throws ValidationError before touching the DB, for top-level create/update AND nested create/update, plus happy-path (null + valid shape) parity.
  • pnpm test in packages/core: 44 files / 898 tests pass. packages/storage: 7 files / 148 tests pass (unaffected — its tests exercise resolveInput directly, not the removed split logic).
  • pnpm build, pnpm lint, pnpm manypkg fix, pnpm format all clean.

Security

No change to the access-control surface — the field-level write-access gate around the split is identical to before, just relocated. No new undeclared-key or bypass paths introduced (filterWritableFields's existing splitColumnOwners gate for direct raw-column writes is untouched).

No blocking issues found.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Core Package Coverage (./packages/core)

Status Category Percentage Covered / Total
🟢 Lines 92.63% (🎯 65%) 1145 / 1236
🟢 Statements 91.11% (🎯 65%) 1221 / 1340
🟢 Functions 97.93% (🎯 62%) 190 / 194
🟢 Branches 81.91% (🎯 50%) 820 / 1001
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/core/src/access/field-access.ts 78.68% 65.3% 66.66% 78.68% 71-99, 173
packages/core/src/context/hook-pipeline.ts 100% 100% 100% 100%
packages/core/src/context/nested-operations.ts 91.74% 78.15% 97.29% 92.45% 85-90, 121, 210, 479, 499, 589, 783, 798, 811, 944, 955, 1065, 1323, 1341-1342
Generated in workflow #1493 for commit 0cd584b by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for UI Package Coverage (./packages/ui)

Status Category Percentage Covered / Total
🔵 Lines 76.72% 244 / 318
🔵 Statements 76.29% 251 / 329
🔵 Functions 69.15% 74 / 107
🔵 Branches 64.25% 160 / 249
File CoverageNo changed files found.
Generated in workflow #1493 for commit 0cd584b by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for CLI Package Coverage (./packages/cli)

Status Category Percentage Covered / Total
🔵 Lines 79.4% 1496 / 1884
🔵 Statements 79.1% 1556 / 1967
🔵 Functions 86.19% 206 / 239
🔵 Branches 67.31% 655 / 973
File CoverageNo changed files found.
Generated in workflow #1493 for commit 0cd584b by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Auth Package Coverage (./packages/auth)

Status Category Percentage Covered / Total
🔵 Lines 97.45% 115 / 118
🔵 Statements 97.52% 118 / 121
🔵 Functions 100% 38 / 38
🔵 Branches 92.85% 78 / 84
File CoverageNo changed files found.
Generated in workflow #1493 for commit 0cd584b by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Storage Package Coverage (./packages/storage)

Status Category Percentage Covered / Total
🔵 Lines 75.09% 193 / 257
🔵 Statements 76.7% 214 / 279
🔵 Functions 85.89% 67 / 78
🔵 Branches 70.96% 176 / 248
File CoverageNo changed files found.
Generated in workflow #1493 for commit 0cd584b by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for RAG Package Coverage (./packages/rag)

Status Category Percentage Covered / Total
🔵 Lines 47.97% 355 / 740
🔵 Statements 48.14% 377 / 783
🔵 Functions 54.26% 70 / 129
🔵 Branches 42.55% 180 / 423
File CoverageNo changed files found.
Generated in workflow #1493 for commit 0cd584b by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)

Status Category Percentage Covered / Total
🔵 Lines 100% 40 / 40
🔵 Statements 100% 40 / 40
🔵 Functions 100% 9 / 9
🔵 Branches 100% 19 / 19
File CoverageNo changed files found.
Generated in workflow #1493 for commit 0cd584b by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)

Status Category Percentage Covered / Total
🔵 Lines 100% 68 / 68
🔵 Statements 100% 71 / 71
🔵 Functions 100% 15 / 15
🔵 Branches 97.87% 46 / 47
File CoverageNo changed files found.
Generated in workflow #1493 for commit 0cd584b by the Vitest Coverage Report Action

@borisno2
borisno2 merged commit 96e1067 into main Jul 21, 2026
6 checks passed
@borisno2
borisno2 deleted the claude/funny-sagan-ryzxcn branch July 21, 2026 11:40
@github-actions github-actions Bot mentioned this pull request Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hook Pipeline: validate multi-column fields before splitting into physical columns

2 participants