feat(core): a computed field declares the relations it needs (ADR-0025) - #857
Merged
Conversation
…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 detectedLatest commit: 2a32775 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…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
Contributor
Coverage Report for Core Package Coverage (./packages/core)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||
Contributor
Coverage Report for UI Package Coverage (./packages/ui)
File CoverageNo changed files found. |
Contributor
Coverage Report for CLI Package Coverage (./packages/cli)
File Coverage
|
||||||||||||||||||||||||||||||||||||||
Contributor
Coverage Report for Auth Package Coverage (./packages/auth)
File CoverageNo changed files found. |
Contributor
Coverage Report for Storage Package Coverage (./packages/storage)
File CoverageNo changed files found. |
Contributor
Coverage Report for RAG Package Coverage (./packages/rag)
File CoverageNo changed files found. |
Contributor
Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)
File CoverageNo changed files found. |
Contributor
Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)
File CoverageNo changed files found. |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'sresolveOutputreading a relation offitem(item.lineItems,item.posts?.length, …) silently computed overundefinedunless a caller happened to include that relation. This adds aneedsdeclaration 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.foldDeclaredDependencies(newaccess/declared-dependencies.ts) merges declared relations into whatever include a read is already building — bare, caller-supplied, or fragment-derived — before it reaches the existingbuildIncludeWithAccessControl/mergeIncludeWithAccessControlpipeline, 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 afterresolveOutputhas run, at every nesting level.Order.totalneedslineItems,LineItem.xneedsorder) terminates via thevisitedListscycle guard already inbuildIncludeWithAccessControl, rather than a new mechanism — matching the note in ADR-0026 that this guard's remaining job is defending exactly this fold.needsis typed asRelationshipFieldKeys<TTypeInfo['fields']>[], so a misspelled or non-relation entry is a compile error — verified against the real generatedLists.<List>.TypeInfo(requires the list to be annotated with it, the documentedlist<Lists.X.TypeInfo>({...})pattern every example already uses).validation/needs-closure.ts, wired intoopensaas generate) rejects an invalidneedsentry 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
packages/core/tests/needs-declared-dependencies.test.ts(12 tests): bare-read fetch + strip, caller-include unchanged, nested explicit-include fold + strip, Access Filter scoping (session-relative value), fully-denied and field-denied dependencies still compute, fragmentqueryreads, two-list declaration cycle termination, generate-time validation (invalid relation, cyclic closure, too-deep closure, valid config).packages/cli/src/commands/generate.test.tsforvalidateNeedsDeclarations/validateNeedsClosureDepth/formatNeedsClosureErrors.resolveOutputrecurse without bound — server OOMs on any access-scoped read #844/Reads with noincludeauto-include every relation to depth 5 — Prisma returns scalars only, and this default is the root cause behind #566 / #830 / #844 #848 pass unmodified (935/935 core tests, 326/326 CLI tests).pnpm lint,pnpm manypkg fix,pnpm formatclean.opensaas generatere-verified end-to-end againstexamples/blog.@opensaas/stack-core+@opensaas/stack-cli, minor).Closes #850
Generated by Claude Code