Skip to content

feat(core): a computed field declares the relations it needs (ADR-0025) - #857

Merged
borisno2 merged 3 commits into
mainfrom
claude/funny-sagan-cm9iuv
Aug 1, 2026
Merged

feat(core): a computed field declares the relations it needs (ADR-0025)#857
borisno2 merged 3 commits into
mainfrom
claude/funny-sagan-cm9iuv

Conversation

@borisno2

@borisno2 borisno2 commented Aug 1, 2026

Copy link
Copy Markdown
Member

Summary

Implements ADR-0025 / issue #850 — the declared-dependency follow-up to ADR-0024 (#848).

Since ADR-0024, a bare read (no caller include) returns a row's own columns only, so a virtual field's resolveOutput reading a relation off item (item.lineItems, item.posts?.length, …) silently computed over undefined unless a caller happened to include that relation. This adds a needs declaration to the base field config so a computed field can name the immediate relations its hook depends on, and have the read fetch exactly those — without widening what the caller receives.

Order: list({
  fields: {
    lineItems: relationship({ ref: 'LineItem.order', many: true }),
    total: virtual({
      type: 'number',
      needs: ['lineItems'],
      hooks: {
        resolveOutput: ({ item }) =>
          item.lineItems.reduce((sum, li) => sum + li.price * li.quantity, 0),
      },
    }),
  },
})
  • Fold + strip, not a widened include. foldDeclaredDependencies (new access/declared-dependencies.ts) merges declared relations into whatever include a read is already building — bare, caller-supplied, or fragment-derived — before it reaches the existing buildIncludeWithAccessControl / mergeIncludeWithAccessControl pipeline, so a declared relation is scoped exactly like a caller-named one. filterReadableFields (field-visibility.ts) then strips the declaration-only keys from the result after resolveOutput has run, at every nesting level.
  • Session-relative, never withheld. A dependency the session can't query isn't fetched, and the hook sees nothing in its place — the field still computes and is present in the result, never withheld.
  • Cycle safety reuses the existing guard. A two-list declaration cycle (Order.total needs lineItems, LineItem.x needs order) terminates via the visitedLists cycle guard already in buildIncludeWithAccessControl, rather than a new mechanism — matching the note in ADR-0026 that this guard's remaining job is defending exactly this fold.
  • Compile-time constraint. needs is typed as RelationshipFieldKeys<TTypeInfo['fields']>[], so a misspelled or non-relation entry is a compile error — verified against the real generated Lists.<List>.TypeInfo (requires the list to be annotated with it, the documented list<Lists.X.TypeInfo>({...}) pattern every example already uses).
  • Generate-time validation (new validation/needs-closure.ts, wired into opensaas generate) rejects an invalid needs entry and any declaration closure that can't fit within the read-include depth cap from any starting point (or that cycles), naming the offending field/chain rather than silently truncating at runtime.

Test plan

Closes #850


Generated by Claude Code

…DR-0025)

Adds `needs?: string[]` to the base field config so a computed field's
resolveOutput hook can declare the immediate sibling relations it cannot
compute without. The read pipeline folds each declaration into the
include (scoped through the Access Filter exactly like a caller-named
relation) wherever that field is computed — root or nested — and strips
the relation from the result afterward unless the caller named it too,
for both `include` reads and fragment `query` reads.

A field whose declared dependency is partially or fully denied still
computes, on whatever it can see, rather than being withheld. A
declaration cycle across lists terminates via the existing
relationship-graph cycle guard in buildIncludeWithAccessControl. Adds
generate-time validation rejecting an invalid `needs` entry or a
declaration closure that can't fit the read-include depth cap from any
starting point.

Closes #850

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

changeset-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2a32775

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 Minor
@opensaas/stack-cli Minor
@opensaas/stack-auth Minor
@opensaas/stack-rag Minor
@opensaas/stack-storage Minor
@opensaas/stack-tiptap Minor
@opensaas/stack-ui Minor
@opensaas/stack-storage-s3 Minor
@opensaas/stack-storage-vercel Minor

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 Aug 1, 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 Aug 1, 2026 8:05am

…uilds

RelationshipFieldKeys<TTypeInfo['fields']> broke assignability for any
non-generic field builder (e.g. @opensaas/stack-tiptap's richText(),
written per the documented third-party field pattern with no TTypeInfo
parameter of its own) whenever it was used in a list annotated with its
generated TypeInfo: the field's own needs?: string[] no longer matched
the list's narrower per-list expected type, regardless of whether that
field used needs at all. This broke examples/tiptap-demo's build in CI.

needs is now a plain string[] on BaseFieldConfig. An invalid entry
(misspelled or non-relation) is still caught at `pnpm generate` time via
validateNeedsDeclarations, just not at compile time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qmK8PtjUFSEDHbcUn8bWu
Add coverage for validateNeedsDeclarations/validateNeedsClosureDepth's
edge paths: a list with no fields, a needs entry whose ref doesn't
resolve to any list, one naming a non-relationship field, one naming a
field that doesn't exist at all, and two needs entries with equal
closure depth (so the second doesn't overwrite the first's recorded
chain). Brings src/validation/needs-closure.ts to 100%/97.36% (was
90.19%/78.94%), above the src/validation/** threshold (92%/83%) that
failed CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qmK8PtjUFSEDHbcUn8bWu
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

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

Status Category Percentage Covered / Total
🟢 Lines 93.44% (🎯 65%) 1268 / 1357
🟢 Statements 91.91% (🎯 65%) 1365 / 1485
🟢 Functions 98.12% (🎯 62%) 209 / 213
🟢 Branches 83.37% (🎯 50%) 918 / 1101
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/core/src/access/declared-dependencies.ts 95% 91.42% 100% 100% 112, 128
packages/core/src/access/field-visibility.ts 95.94% 92.59% 100% 95.71% 297, 337-338
packages/core/src/validation/needs-closure.ts 100% 97.36% 100% 100%
Generated in workflow #1569 for commit 2a32775 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 1, 2026

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 #1569 for commit 2a32775 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

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

Status Category Percentage Covered / Total
🔵 Lines 78.81% 1499 / 1902
🔵 Statements 78.51% 1560 / 1987
🔵 Functions 85.59% 208 / 243
🔵 Branches 67% 656 / 979
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/cli/src/commands/generate.ts 5.14% 2.5% 80% 4.44% 61-308
Generated in workflow #1569 for commit 2a32775 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 1, 2026

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 #1569 for commit 2a32775 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

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

Status Category Percentage Covered / Total
🔵 Lines 78.57% 220 / 280
🔵 Statements 80.06% 245 / 306
🔵 Functions 86.07% 68 / 79
🔵 Branches 75.88% 214 / 282
File CoverageNo changed files found.
Generated in workflow #1569 for commit 2a32775 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 1, 2026

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 #1569 for commit 2a32775 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 1, 2026

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 #1569 for commit 2a32775 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 1, 2026

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 #1569 for commit 2a32775 by the Vitest Coverage Report Action

@borisno2
borisno2 merged commit cdca174 into main Aug 1, 2026
6 checks passed
@borisno2
borisno2 deleted the claude/funny-sagan-cm9iuv branch August 1, 2026 08:15
@github-actions github-actions Bot mentioned this pull request Aug 1, 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.

Virtual fields should be able to declare the relations they need

2 participants