@opensaas/stack-core 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +35 -0
  3. package/CLAUDE.md +44 -0
  4. package/dist/access/declared-dependencies.d.ts +68 -0
  5. package/dist/access/declared-dependencies.d.ts.map +1 -0
  6. package/dist/access/declared-dependencies.js +79 -0
  7. package/dist/access/declared-dependencies.js.map +1 -0
  8. package/dist/access/field-visibility.d.ts +2 -1
  9. package/dist/access/field-visibility.d.ts.map +1 -1
  10. package/dist/access/field-visibility.js +23 -3
  11. package/dist/access/field-visibility.js.map +1 -1
  12. package/dist/access/index.d.ts +2 -0
  13. package/dist/access/index.d.ts.map +1 -1
  14. package/dist/access/index.js +3 -0
  15. package/dist/access/index.js.map +1 -1
  16. package/dist/config/types.d.ts +65 -1
  17. package/dist/config/types.d.ts.map +1 -1
  18. package/dist/context/index.d.ts.map +1 -1
  19. package/dist/context/index.js +51 -95
  20. package/dist/context/index.js.map +1 -1
  21. package/dist/fields/index.d.ts.map +1 -1
  22. package/dist/fields/index.js +12 -12
  23. package/dist/fields/index.js.map +1 -1
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +6 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/validation/needs-closure.d.ts +49 -0
  29. package/dist/validation/needs-closure.d.ts.map +1 -0
  30. package/dist/validation/needs-closure.js +139 -0
  31. package/dist/validation/needs-closure.js.map +1 -0
  32. package/package.json +1 -1
  33. package/src/access/declared-dependencies.ts +140 -0
  34. package/src/access/field-visibility.ts +24 -0
  35. package/src/access/index.ts +8 -0
  36. package/src/config/types.ts +65 -1
  37. package/src/context/index.ts +96 -115
  38. package/src/fields/index.ts +12 -9
  39. package/src/index.ts +8 -0
  40. package/src/validation/needs-closure.ts +188 -0
  41. package/tests/field-types.test.ts +6 -2
  42. package/tests/needs-declared-dependencies.test.ts +500 -0
  43. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
1
 
2
- > @opensaas/stack-core@0.35.0 build /home/runner/work/stack/stack/packages/core
2
+ > @opensaas/stack-core@0.36.0 build /home/runner/work/stack/stack/packages/core
3
3
  > tsc
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @opensaas/stack-core
2
2
 
3
+ ## 0.36.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#857](https://github.com/OpenSaasAU/stack/pull/857) [`cdca174`](https://github.com/OpenSaasAU/stack/commit/cdca17444a5259cd0d3d8604a90a2cea4566cda2) Thanks [@borisno2](https://github.com/borisno2)! - Add `needs` to the base field config: a computed field can declare the immediate relations its `resolveOutput` hook depends on, so the read fetches exactly those — without widening what the caller receives (ADR-0025).
8
+
9
+ Since ADR-0024, a bare read (no caller `include`) returns a row's own columns only, so a virtual field reading `item.someRelation` silently computed over `undefined` unless a caller happened to include it. `needs` fixes that:
10
+
11
+ ```typescript
12
+ Order: list({
13
+ fields: {
14
+ lineItems: relationship({ ref: 'LineItem.order', many: true }),
15
+ total: virtual({
16
+ type: 'number',
17
+ needs: ['lineItems'],
18
+ hooks: {
19
+ resolveOutput: ({ item }) =>
20
+ item.lineItems.reduce((sum, li) => sum + li.price * li.quantity, 0),
21
+ },
22
+ }),
23
+ },
24
+ })
25
+ ```
26
+
27
+ The declared relation is fetched wherever the field is computed — at the root of a read and at every nested level — and is scoped through the Access Filter exactly like a caller-named relation: a dependency the session can't query is not fetched, and the hook sees nothing in its place. A field always computes on whatever it can see, so a partially-denied dependency still produces a value rather than being withheld. The relation itself is stripped from the result unless the caller named it too, for both `include` reads and fragment `query` reads.
28
+
29
+ `needs` is available on every field type, not only `virtual()`. `opensaas generate` now also validates every `needs` declaration: an entry naming a non-relationship or non-existent field, or a declaration closure that can't fit within the read-include depth cap from any starting point, fails generation with a message naming the offending field/chain rather than silently truncating at runtime.
30
+
31
+ See `docs/adr/0025-a-computed-field-declares-the-relations-it-needs.md`.
32
+
33
+ ### Patch Changes
34
+
35
+ - [#859](https://github.com/OpenSaasAU/stack/pull/859) [`ebb4cd3`](https://github.com/OpenSaasAU/stack/commit/ebb4cd3515ff40f960f888e7b4147d1d089a0966) Thanks [@borisno2](https://github.com/borisno2)! - Fix `isIndexed: true` on `text`, `decimal` and `calendarDay` emitting an invalid inline `@index` attribute, producing a schema Prisma rejects with "Attribute not known: @index".
36
+ Non-unique indexes are now emitted as block-level `@@index([field])`; `isIndexed: 'unique'` is unchanged.
37
+
3
38
  ## 0.35.0
4
39
 
5
40
  ### Minor Changes
package/CLAUDE.md CHANGED
@@ -413,6 +413,50 @@ console.log(user.fullName) // "John Doe" — computed via resolveOutput on every
413
413
  - Can optionally provide `resolveInput` for write side effects
414
414
  - Useful for derived values, computed properties, and external API sync
415
415
 
416
+ ### Declaring Relation Dependencies (`needs`, ADR-0025)
417
+
418
+ Since ADR-0024, a bare read (no caller `include`) returns a row's own columns
419
+ only — never its relations. A virtual field whose `resolveOutput` reads a
420
+ relation off `item` (`item.lineItems`, `item.posts.length`, …) would
421
+ otherwise silently compute over `undefined` whenever the caller didn't happen
422
+ to include that relation. `needs` is the fix: declare the immediate
423
+ relations a field's hook cannot compute without, and the read fetches
424
+ exactly those — wherever that field is computed, at the root of a read and
425
+ at every nested level alike.
426
+
427
+ ```typescript
428
+ Order: list({
429
+ fields: {
430
+ lineItems: relationship({ ref: 'LineItem.order', many: true }),
431
+ total: virtual({
432
+ type: 'number',
433
+ needs: ['lineItems'], // fetched for this hook, even on a bare read
434
+ hooks: {
435
+ resolveOutput: ({ item }) =>
436
+ item.lineItems.reduce((sum, li) => sum + li.price * li.quantity, 0),
437
+ },
438
+ }),
439
+ },
440
+ })
441
+
442
+ // The caller never asked for lineItems, and never receives it:
443
+ const order = await context.db.order.findMany()
444
+ order[0].total // computed correctly
445
+ order[0].lineItems // undefined — `needs` is private plumbing, not an implicit `include`
446
+ ```
447
+
448
+ **Key characteristics:**
449
+
450
+ - Available on every field type (via `BaseFieldConfig`), not only `virtual()` — any field whose `resolveOutput` reads a relation can declare it.
451
+ - Names **immediate relations only** — no dotted paths. A dependency of a dependency is pulled in by the next list's own `needs` declaration, not by a path grammar.
452
+ - **Never widens what the caller receives.** A declared relation is stripped from the result unless the caller named it too, for both `include` reads and fragment `query` reads alike.
453
+ - **Scoped by the Access Filter like any other relation a read asks for — never a bypass.** A dependency the session cannot query (operation-level `query` access) is not fetched; a dependency a relationship field denies field-level `read` on does not reach the hook; a dependency the Access Filter scopes with a filter yields only the visible rows.
454
+ - **Session-relative, with no escape.** The field always computes, on whatever its session can see — a field declaring two dependencies and granted access to one still produces a value from that one. A total over a scoped relation is a projection of the visible rows, not a fact about the underlying row. Computing the "true" figure is not something a declared dependency can do, because that would leak the values of rows the session was denied. **A field that genuinely needs the unscoped view must issue a privileged read inside its own hook (`context.sudo()`) and explicitly own that decision** — `needs` does not provide an escape hatch from access control.
455
+ - **A declaration closure that can't fit is refused, not silently truncated.** Declarations fold in recursively (a related list's own `needs` are satisfied too, wherever it's reached), so a long chain can exceed the read-include depth cap. A chain that cannot fit from any starting point fails `pnpm generate`; a caller-triggered overflow hits the ordinary runtime depth denial.
456
+ - **Typed as a plain `string[]`, not compile-checked against the list's relation names.** `BaseFieldConfig` is the contextual type every field builder's return value is checked against — including non-generic third-party fields (`richText(): RichTextField`, with no `TTypeInfo` parameter of its own, the documented third-party field pattern). Narrowing `needs`'s type per-list would make it disagree with the type a fixed, unparameterized field config presents, breaking assignability for every such field regardless of whether it uses `needs` at all. Instead, `pnpm generate` (`validateNeedsDeclarations`) rejects a `needs` entry that isn't an immediate relationship field on the same list, naming the field and the bad entry.
457
+
458
+ See `docs/adr/0025-a-computed-field-declares-the-relations-it-needs.md` and the "Declared dependency" / "Session-relative value" glossary entries in `CONTEXT.md`.
459
+
416
460
  ## Type Safety
417
461
 
418
462
  All types are strongly typed with TypeScript:
@@ -0,0 +1,68 @@
1
+ import type { FieldConfig, OpenSaasConfig } from '../config/types.js';
2
+ /**
3
+ * Declared Dependencies — folding a computed field's `needs` into a read's
4
+ * `include` without widening what the caller receives (ADR-0025).
5
+ *
6
+ * A field's `needs` declares immediate sibling relations its `resolveOutput`
7
+ * hook cannot compute without. This module folds those relations into
8
+ * whatever `include` a read is already building (caller-supplied, fragment-
9
+ * derived, or none at all) BEFORE it reaches the existing access-scoping
10
+ * pipeline (`buildIncludeWithAccessControl` / `mergeIncludeWithAccessControl`
11
+ * in `access-filter.ts`) — a declared relation is scoped exactly like a
12
+ * caller-named one, never a bypass.
13
+ *
14
+ * The fold also tracks provenance: which relation keys, at which nesting
15
+ * level, were added ONLY to satisfy a declaration (as opposed to being named
16
+ * by the caller). `field-visibility.ts` uses that tree to strip those keys
17
+ * from the result after `resolveOutput` hooks have had a chance to read them
18
+ * — a declared dependency is private plumbing, not an implicit `include`.
19
+ *
20
+ * Reach beyond one hop: a relation added here to satisfy a declaration is
21
+ * added BARE (`true`), never with an explicit nested include of its own.
22
+ * `buildIncludeWithAccessControl` already auto-expands a bare relation's own
23
+ * readable-relationship subtree to `READ_INCLUDE_MAX_DEPTH` (pre-ADR-0026),
24
+ * so a chain of declarations rides that existing expansion for free — the
25
+ * related list's own declared needs are already present among what gets
26
+ * auto-included beneath it. This is also why a declaration-driven cycle
27
+ * (e.g. `Order.total` needs `lineItems`, `LineItem.orderRef` needs `order`)
28
+ * can't recurse without bound here: it flows through
29
+ * `buildIncludeWithAccessControl`'s existing `visitedLists` cycle guard
30
+ * rather than through any recursion of this module's own (see ADR-0026's
31
+ * note that this guard's remaining job, after that ADR lands, is defending
32
+ * exactly this fold).
33
+ *
34
+ * This module only recurses into EXPLICIT nested includes the caller wrote
35
+ * (narrowing what's fetched below a relation) — those cut off the free
36
+ * auto-expansion, so a nested list's own declared needs must be folded in
37
+ * explicitly. An explicit caller include is always a finite literal, so this
38
+ * recursion terminates on its own without a separate depth/cycle guard.
39
+ */
40
+ /** Which relation keys, at which nesting level, exist only to satisfy a `needs` declaration. */
41
+ export type DeclaredOnlyTree = {
42
+ /** Keys at THIS level whose entire branch was added purely by the fold. */
43
+ keys: Set<string>;
44
+ /** Per-key trees for relations present in the include for other reasons (caller-named), whose OWN nested include may still contain declaration-only keys. */
45
+ nested: Record<string, DeclaredOnlyTree>;
46
+ };
47
+ export declare function emptyDeclaredOnlyTree(): DeclaredOnlyTree;
48
+ /**
49
+ * The deduped set of relation names declared via `needs` by fields on this
50
+ * list that have a `resolveOutput` hook. A `needs` entry on a field without
51
+ * one is inert — there is no hook to feed it to — so it contributes nothing
52
+ * to fetch.
53
+ */
54
+ export declare function getDeclaredRelationNames(fieldConfigs: Record<string, FieldConfig>): string[];
55
+ /**
56
+ * Fold this list's declared dependencies into `rawInclude`, recursing into
57
+ * any EXPLICIT nested include the caller wrote so a related list's own
58
+ * declared needs are satisfied too (see module doc comment).
59
+ *
60
+ * Returns `rawInclude` itself (same reference) when there is nothing to
61
+ * fold, so a list with no `needs` fields and no caller include stays on the
62
+ * exact bare-read path (ADR-0024) — untouched, not merely equivalent.
63
+ */
64
+ export declare function foldDeclaredDependencies(rawInclude: Record<string, unknown> | undefined, fieldConfigs: Record<string, FieldConfig>, config: OpenSaasConfig): {
65
+ include: Record<string, unknown> | undefined;
66
+ declaredOnly: DeclaredOnlyTree;
67
+ };
68
+ //# sourceMappingURL=declared-dependencies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declared-dependencies.d.ts","sourceRoot":"","sources":["../../src/access/declared-dependencies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAGrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,gGAAgG;AAChG,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2EAA2E;IAC3E,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACjB,6JAA6J;IAC7J,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;CACzC,CAAA;AAED,wBAAgB,qBAAqB,IAAI,gBAAgB,CAExD;AAiBD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,EAAE,CAS5F;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC/C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,MAAM,EAAE,cAAc,GACrB;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAAC,YAAY,EAAE,gBAAgB,CAAA;CAAE,CAwClF"}
@@ -0,0 +1,79 @@
1
+ import { getRelatedListConfig } from './engine.js';
2
+ export function emptyDeclaredOnlyTree() {
3
+ return { keys: new Set(), nested: {} };
4
+ }
5
+ function isDeclaredOnlyTreeEmpty(tree) {
6
+ return tree.keys.size === 0 && Object.keys(tree.nested).length === 0;
7
+ }
8
+ function isRelationshipFieldConfig(fieldConfig) {
9
+ return (!!fieldConfig &&
10
+ fieldConfig.type === 'relationship' &&
11
+ 'ref' in fieldConfig &&
12
+ !!fieldConfig.ref);
13
+ }
14
+ /**
15
+ * The deduped set of relation names declared via `needs` by fields on this
16
+ * list that have a `resolveOutput` hook. A `needs` entry on a field without
17
+ * one is inert — there is no hook to feed it to — so it contributes nothing
18
+ * to fetch.
19
+ */
20
+ export function getDeclaredRelationNames(fieldConfigs) {
21
+ const names = new Set();
22
+ for (const fieldConfig of Object.values(fieldConfigs)) {
23
+ if (!fieldConfig?.hooks?.resolveOutput)
24
+ continue;
25
+ for (const name of fieldConfig.needs ?? []) {
26
+ names.add(name);
27
+ }
28
+ }
29
+ return [...names];
30
+ }
31
+ /**
32
+ * Fold this list's declared dependencies into `rawInclude`, recursing into
33
+ * any EXPLICIT nested include the caller wrote so a related list's own
34
+ * declared needs are satisfied too (see module doc comment).
35
+ *
36
+ * Returns `rawInclude` itself (same reference) when there is nothing to
37
+ * fold, so a list with no `needs` fields and no caller include stays on the
38
+ * exact bare-read path (ADR-0024) — untouched, not merely equivalent.
39
+ */
40
+ export function foldDeclaredDependencies(rawInclude, fieldConfigs, config) {
41
+ const declaredNames = getDeclaredRelationNames(fieldConfigs);
42
+ if (declaredNames.length === 0 && !rawInclude) {
43
+ return { include: rawInclude, declaredOnly: emptyDeclaredOnlyTree() };
44
+ }
45
+ const declaredOnly = emptyDeclaredOnlyTree();
46
+ const merged = { ...(rawInclude ?? {}) };
47
+ for (const name of declaredNames) {
48
+ if (name in merged)
49
+ continue; // caller (or fragment) already asked for it — not declaration-only
50
+ if (!isRelationshipFieldConfig(fieldConfigs[name]))
51
+ continue; // invalid `needs` entry; caught by generate-time validation
52
+ merged[name] = true;
53
+ declaredOnly.keys.add(name);
54
+ }
55
+ for (const [key, value] of Object.entries(merged)) {
56
+ const fieldConfig = fieldConfigs[key];
57
+ if (!isRelationshipFieldConfig(fieldConfig))
58
+ continue;
59
+ // A whole branch we just added is bare `true` — its own subtree auto-expands
60
+ // (see module doc comment), so there is no explicit nested include to recurse into.
61
+ if (declaredOnly.keys.has(key))
62
+ continue;
63
+ const entry = value;
64
+ if (!entry || typeof entry !== 'object' || !entry.include)
65
+ continue;
66
+ const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
67
+ if (!relatedConfig)
68
+ continue;
69
+ const nested = foldDeclaredDependencies(entry.include, relatedConfig.listConfig.fields, config);
70
+ if (nested.include !== entry.include) {
71
+ merged[key] = { ...entry, include: nested.include };
72
+ }
73
+ if (!isDeclaredOnlyTreeEmpty(nested.declaredOnly)) {
74
+ declaredOnly.nested[key] = nested.declaredOnly;
75
+ }
76
+ }
77
+ return { include: merged, declaredOnly };
78
+ }
79
+ //# sourceMappingURL=declared-dependencies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declared-dependencies.js","sourceRoot":"","sources":["../../src/access/declared-dependencies.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAiDlD,MAAM,UAAU,qBAAqB;IACnC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;AACxC,CAAC;AAED,SAAS,uBAAuB,CAAC,IAAsB;IACrD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,CAAA;AACtE,CAAC;AAED,SAAS,yBAAyB,CAChC,WAAoC;IAEpC,OAAO,CACL,CAAC,CAAC,WAAW;QACb,WAAW,CAAC,IAAI,KAAK,cAAc;QACnC,KAAK,IAAI,WAAW;QACpB,CAAC,CAAC,WAAW,CAAC,GAAG,CAClB,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,YAAyC;IAChF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;QACtD,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,aAAa;YAAE,SAAQ;QAChD,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;YAC3C,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACjB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAA;AACnB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB,CACtC,UAA+C,EAC/C,YAAyC,EACzC,MAAsB;IAEtB,MAAM,aAAa,GAAG,wBAAwB,CAAC,YAAY,CAAC,CAAA;IAE5D,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;QAC9C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,qBAAqB,EAAE,EAAE,CAAA;IACvE,CAAC;IAED,MAAM,YAAY,GAAG,qBAAqB,EAAE,CAAA;IAC5C,MAAM,MAAM,GAA4B,EAAE,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAA;IAEjE,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,IAAI,IAAI,IAAI,MAAM;YAAE,SAAQ,CAAC,mEAAmE;QAChG,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;YAAE,SAAQ,CAAC,4DAA4D;QACzH,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;QACnB,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC7B,CAAC;IAED,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,MAAM,WAAW,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;QACrC,IAAI,CAAC,yBAAyB,CAAC,WAAW,CAAC;YAAE,SAAQ;QACrD,6EAA6E;QAC7E,oFAAoF;QACpF,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAQ;QAExC,MAAM,KAAK,GAAG,KAAwD,CAAA;QACtE,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,SAAQ;QAEnE,MAAM,aAAa,GAAG,oBAAoB,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;QACnE,IAAI,CAAC,aAAa;YAAE,SAAQ;QAE5B,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,CAAC,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC/F,IAAI,MAAM,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;YACrC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAA;QACrD,CAAC;QACD,IAAI,CAAC,uBAAuB,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;YAClD,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,YAAY,CAAA;QAChD,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAA;AAC1C,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import type { Session, AccessContext } from './types.js';
2
2
  import type { OpenSaasConfig, FieldConfig } from '../config/types.js';
3
+ import type { DeclaredOnlyTree } from './declared-dependencies.js';
3
4
  /**
4
5
  * Filter fields from an object based on read access
5
6
  * Recursively applies access control to nested relationships
@@ -9,5 +10,5 @@ export declare function filterReadableFields<T extends Record<string, unknown>>(
9
10
  context: AccessContext & {
10
11
  _isSudo?: boolean;
11
12
  };
12
- }, config?: OpenSaasConfig, depth?: number, listKey?: string): Promise<Partial<T>>;
13
+ }, config?: OpenSaasConfig, depth?: number, listKey?: string, declaredOnly?: DeclaredOnlyTree): Promise<Partial<T>>;
13
14
  //# sourceMappingURL=field-visibility.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"field-visibility.d.ts","sourceRoot":"","sources":["../../src/access/field-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAwKrE;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,IAAI,EAAE,CAAC,EACP,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAC/C,EACD,MAAM,CAAC,EAAE,cAAc,EACvB,KAAK,GAAE,MAAU,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAiKrB"}
1
+ {"version":3,"file":"field-visibility.d.ts","sourceRoot":"","sources":["../../src/access/field-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAKrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAqKlE;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,IAAI,EAAE,CAAC,EACP,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAC/C,EACD,MAAM,CAAC,EAAE,cAAc,EACvB,KAAK,GAAE,MAAU,EACjB,OAAO,CAAC,EAAE,MAAM,EAMhB,YAAY,GAAE,gBAA0C,GACvD,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAiLrB"}
@@ -2,6 +2,7 @@ import { getRelatedListConfig } from './engine.js';
2
2
  import { checkFieldAccess } from './field-access.js';
3
3
  import { RESOLVE_CHAIN_MAX_LENGTH } from './depth-limits.js';
4
4
  import { ResolveOutputCycleError } from './errors.js';
5
+ import { emptyDeclaredOnlyTree } from './declared-dependencies.js';
5
6
  // NOTE: `context/index.ts` imports `filterReadableFields` from this module
6
7
  // (via the `access/index.ts` barrel) — this is an intentional cyclic
7
8
  // dependency, the same shape and for the same reason as the one documented in
@@ -107,7 +108,13 @@ async function resolveReadableFieldValue(params) {
107
108
  * Filter fields from an object based on read access
108
109
  * Recursively applies access control to nested relationships
109
110
  */
110
- export async function filterReadableFields(item, fieldConfigs, args, config, depth = 0, listKey) {
111
+ export async function filterReadableFields(item, fieldConfigs, args, config, depth = 0, listKey,
112
+ // Relation keys added purely to satisfy a field's `needs` (ADR-0025) at
113
+ // THIS level, and the same tree for each nested relation reached via a
114
+ // caller-named branch. Stripped from `filtered` right before it is
115
+ // returned — after resolveOutput has had a chance to read them — so a
116
+ // declared dependency never widens what the caller receives.
117
+ declaredOnly = emptyDeclaredOnlyTree()) {
111
118
  const filtered = {};
112
119
  // Multi-column fields (e.g. storage image()/file() in Keystone-parity mode)
113
120
  // back several physical columns rather than one. Before the per-field pass,
@@ -167,16 +174,21 @@ export async function filterReadableFields(item, fieldConfigs, args, config, dep
167
174
  continue;
168
175
  }
169
176
  const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
177
+ // The declared-only tree for whatever THIS relation's own list computes,
178
+ // e.g. a field on the related list that declares its own `needs`. Falls
179
+ // back to an empty tree when this relation isn't declaration-related at
180
+ // all — the common case.
181
+ const nestedDeclaredOnly = declaredOnly.nested[fieldName] ?? emptyDeclaredOnlyTree();
170
182
  if (relatedConfig) {
171
183
  // For many relationships (arrays) - recursively filter fields in each item
172
184
  // The recursive call already handles applying resolveOutput hooks
173
185
  if (Array.isArray(value)) {
174
- filtered[fieldName] = await Promise.all(value.map((relatedItem) => filterReadableFields(relatedItem, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName)));
186
+ filtered[fieldName] = await Promise.all(value.map((relatedItem) => filterReadableFields(relatedItem, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName, nestedDeclaredOnly)));
175
187
  }
176
188
  // For single relationships (objects) - recursively filter fields
177
189
  // The recursive call already handles applying resolveOutput hooks
178
190
  else if (typeof value === 'object') {
179
- filtered[fieldName] = await filterReadableFields(value, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName);
191
+ filtered[fieldName] = await filterReadableFields(value, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName, nestedDeclaredOnly);
180
192
  }
181
193
  }
182
194
  else {
@@ -235,6 +247,14 @@ export async function filterReadableFields(item, fieldConfigs, args, config, dep
235
247
  filtered[fieldName] = result.value;
236
248
  }
237
249
  }
250
+ // Strip relations that were fetched ONLY to satisfy a `needs` declaration
251
+ // (ADR-0025), now that every resolveOutput hook at this level — including
252
+ // virtual fields, which read the assembled `filtered` object above — has
253
+ // had the chance to see them. A declared dependency is private plumbing,
254
+ // not an implicit `include`: it never widens what the caller receives.
255
+ for (const key of declaredOnly.keys) {
256
+ delete filtered[key];
257
+ }
238
258
  return filtered;
239
259
  }
240
260
  //# sourceMappingURL=field-visibility.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"field-visibility.js","sourceRoot":"","sources":["../../src/access/field-visibility.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"field-visibility.js","sourceRoot":"","sources":["../../src/access/field-visibility.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@ export { checkFieldAccess, filterWritableFields } from './field-access.js';
4
4
  export { buildIncludeWithAccessControl, mergeIncludeWithAccessControl, stripVirtualFieldsFromInclude, toPrismaInclude, } from './access-filter.js';
5
5
  export type { AccessIncludeResult } from './access-filter.js';
6
6
  export { filterReadableFields } from './field-visibility.js';
7
+ export { foldDeclaredDependencies, getDeclaredRelationNames, emptyDeclaredOnlyTree, } from './declared-dependencies.js';
8
+ export type { DeclaredOnlyTree } from './declared-dependencies.js';
7
9
  export { AccessScopeDepthExceededError } from './errors.js';
8
10
  export { ResolveOutputCycleError } from './errors.js';
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/access/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,WAAW,EACX,OAAO,EACP,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,WAAW,EACX,YAAY,EACZ,SAAS,EACT,cAAc,EACd,oBAAoB,GACrB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAE1E,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,EAC7B,6BAA6B,EAC7B,eAAe,GAChB,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAE7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAE5D,OAAO,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAA;AAE3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/access/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,WAAW,EACX,OAAO,EACP,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,WAAW,EACX,YAAY,EACZ,SAAS,EACT,cAAc,EACd,oBAAoB,GACrB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAE1E,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,EAC7B,6BAA6B,EAC7B,eAAe,GAChB,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAE7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAG5D,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,4BAA4B,CAAA;AACnC,YAAY,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAElE,OAAO,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAA;AAE3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA"}
@@ -6,6 +6,9 @@ export { checkFieldAccess, filterWritableFields } from './field-access.js';
6
6
  export { buildIncludeWithAccessControl, mergeIncludeWithAccessControl, stripVirtualFieldsFromInclude, toPrismaInclude, } from './access-filter.js';
7
7
  // Phase 2 — Field Visibility (post-query field stripping + resolveOutput).
8
8
  export { filterReadableFields } from './field-visibility.js';
9
+ // Declared Dependencies — folding `needs` into an include without widening
10
+ // the result (ADR-0025).
11
+ export { foldDeclaredDependencies, getDeclaredRelationNames, emptyDeclaredOnlyTree, } from './declared-dependencies.js';
9
12
  // Thrown when a caller include reaches past the depth the Access Filter can scope.
10
13
  export { AccessScopeDepthExceededError } from './errors.js';
11
14
  // Thrown when a resolveOutput hook's own resolve chain cycles back into itself.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/access/index.ts"],"names":[],"mappings":"AAaA,mEAAmE;AACnE,OAAO,EACL,WAAW,EACX,YAAY,EACZ,SAAS,EACT,cAAc,EACd,oBAAoB,GACrB,MAAM,aAAa,CAAA;AACpB,4EAA4E;AAC5E,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC1E,4DAA4D;AAC5D,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,EAC7B,6BAA6B,EAC7B,eAAe,GAChB,MAAM,oBAAoB,CAAA;AAE3B,2EAA2E;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,mFAAmF;AACnF,OAAO,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAA;AAC3D,gFAAgF;AAChF,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/access/index.ts"],"names":[],"mappings":"AAaA,mEAAmE;AACnE,OAAO,EACL,WAAW,EACX,YAAY,EACZ,SAAS,EACT,cAAc,EACd,oBAAoB,GACrB,MAAM,aAAa,CAAA;AACpB,4EAA4E;AAC5E,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC1E,4DAA4D;AAC5D,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,EAC7B,6BAA6B,EAC7B,eAAe,GAChB,MAAM,oBAAoB,CAAA;AAE3B,2EAA2E;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,2EAA2E;AAC3E,yBAAyB;AACzB,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,4BAA4B,CAAA;AAEnC,mFAAmF;AACnF,OAAO,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAA;AAC3D,gFAAgF;AAChF,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA"}
@@ -557,7 +557,8 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
557
557
  * @param keystoneCompat - Whether Keystone-compat mode is enabled (db.keystoneCompat).
558
558
  * When true, non-null text columns without an explicit defaultValue emit
559
559
  * `@default("")` to match Keystone 6's implicit empty-string text default.
560
- * @returns Prisma type string, optional modifiers, and optional enum values
560
+ * @returns Prisma type string, optional modifiers, optional enum values, and
561
+ * an optional block-level index request
561
562
  */
562
563
  getPrismaType?: (fieldName: string, provider?: string, listName?: string, keystoneCompat?: boolean) => {
563
564
  type: string;
@@ -567,6 +568,26 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
567
568
  * The enum name is the value of `type`.
568
569
  */
569
570
  enumValues?: string[];
571
+ /**
572
+ * If set, this field requires a block-level index on the owning model:
573
+ * `@@index([fieldName])` for `true`, `@@unique([fieldName])` for
574
+ * `'unique'`. `false` and `undefined` both mean "no index".
575
+ *
576
+ * Prisma has no field-level `@index` attribute — a non-unique index can
577
+ * ONLY be expressed as the model-level `@@index([...])` — so a field that
578
+ * wants one has to ask for it out-of-line rather than appending to
579
+ * {@link modifiers}. (A unique index has both forms available; the
580
+ * built-in scalars keep emitting the inline `@unique` modifier for that
581
+ * case, so this channel carries only what cannot be written inline.)
582
+ *
583
+ * Same shape as {@link PrismaRelationResult.foreignKeyIndex}, which is how
584
+ * relationship fields have always emitted their foreign-key indexes. The
585
+ * generator handles both through one emit pass, so the field stays the
586
+ * authority on whether it can be indexed by name at all — a multi-column
587
+ * field (see {@link getPrismaColumns}) has no single column matching its
588
+ * field name and can decline, or name a real column of its own.
589
+ */
590
+ index?: boolean | 'unique';
570
591
  };
571
592
  /**
572
593
  * Get TypeScript type information for type generation
@@ -663,6 +684,49 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
663
684
  * @param value - The resolved logical value (metadata, or `null` to clear)
664
685
  */
665
686
  splitColumns?: (fieldName: string, value: unknown) => Record<string, unknown>;
687
+ /**
688
+ * Declares the immediate sibling relations this field's `resolveOutput`
689
+ * hook cannot compute without (ADR-0025 — the "Declared dependency" glossary
690
+ * entry in `CONTEXT.md`). The read fetches each declared relation wherever
691
+ * this field is computed — at the root of a read and at every nested level
692
+ * alike — and scopes it through the Access Filter exactly like a
693
+ * caller-named relation: a dependency a session cannot query is not
694
+ * fetched, and the hook sees nothing in its place.
695
+ *
696
+ * A declared dependency is private plumbing, not an implicit `include`: it
697
+ * is stripped from the result unless the caller named it too, so declaring
698
+ * or removing one changes this field's implementation, never the shape of
699
+ * every read of the list.
700
+ *
701
+ * Names immediate relations only — no dotted paths. Reach beyond one hop
702
+ * comes from the recursive fold: a dependency's own list declares its own
703
+ * dependencies.
704
+ *
705
+ * Typed as a plain `string[]`, not narrowed to this list's own relation
706
+ * keys: `BaseFieldConfig` is the contextual type EVERY field builder's
707
+ * return type is checked against, including non-generic third-party ones
708
+ * (`richText(): RichTextField`, with no `TTypeInfo` parameter of its own —
709
+ * the documented third-party field pattern). Narrowing `needs` per-list
710
+ * would make `needs`'s type on a fixed, unparameterized third-party field
711
+ * config disagree with the narrower type this list's own slot expects,
712
+ * breaking assignability for every such field regardless of whether it
713
+ * uses `needs` at all. A misspelled or non-relation entry is instead
714
+ * caught by `pnpm generate` (`validateNeedsDeclarations`), which has no
715
+ * such constraint.
716
+ *
717
+ * @example
718
+ * ```typescript
719
+ * lineItems: relationship({ ref: 'LineItem.order', many: true }),
720
+ * total: virtual({
721
+ * type: 'number',
722
+ * needs: ['lineItems'],
723
+ * hooks: {
724
+ * resolveOutput: ({ item }) => item.lineItems.reduce((sum, li) => sum + li.price, 0),
725
+ * },
726
+ * }),
727
+ * ```
728
+ */
729
+ needs?: string[];
666
730
  };
667
731
  /**
668
732
  * A single physical column contributed by a multi-column field