@opensaas/stack-core 0.34.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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +82 -0
- package/CLAUDE.md +48 -0
- package/dist/access/declared-dependencies.d.ts +68 -0
- package/dist/access/declared-dependencies.d.ts.map +1 -0
- package/dist/access/declared-dependencies.js +79 -0
- package/dist/access/declared-dependencies.js.map +1 -0
- package/dist/access/field-visibility.d.ts +2 -1
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +23 -3
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +2 -0
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +3 -0
- package/dist/access/index.js.map +1 -1
- package/dist/config/types.d.ts +65 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +65 -72
- package/dist/context/index.js.map +1 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +12 -12
- package/dist/fields/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/validation/needs-closure.d.ts +49 -0
- package/dist/validation/needs-closure.d.ts.map +1 -0
- package/dist/validation/needs-closure.js +139 -0
- package/dist/validation/needs-closure.js.map +1 -0
- package/package.json +1 -1
- package/src/access/declared-dependencies.ts +140 -0
- package/src/access/field-visibility.ts +24 -0
- package/src/access/index.ts +8 -0
- package/src/config/types.ts +65 -1
- package/src/context/index.ts +117 -90
- package/src/fields/index.ts +12 -9
- package/src/index.ts +8 -0
- package/src/validation/needs-closure.ts +188 -0
- package/tests/bare-read-scalars.test.ts +147 -0
- package/tests/field-types.test.ts +6 -2
- package/tests/needs-declared-dependencies.test.ts +500 -0
- package/tests/nested-access-and-hooks.test.ts +10 -0
- package/tests/resolve-chain.test.ts +40 -23
- package/tests/singleton.test.ts +83 -0
- package/tsconfig.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
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
|
+
|
|
38
|
+
## 0.35.0
|
|
39
|
+
|
|
40
|
+
### Minor Changes
|
|
41
|
+
|
|
42
|
+
- [#853](https://github.com/OpenSaasAU/stack/pull/853) [`d0c94a9`](https://github.com/OpenSaasAU/stack/commit/d0c94a994e8be67742c97b6757ca4dd4e454f682) Thanks [@borisno2](https://github.com/borisno2)! - **This break is silent.** A `context.db` read with no `include` (and no fragment `query`) used to auto-include every readable relationship of the list, recursing up to 5 levels deep. It now returns the row's own columns plus its virtual fields only — matching Prisma's own semantics for a bare read — and relations arrive only when you name them. A read that used to return `post.author` now returns no `author` key at all: no error, no warning, just less data (ADR-0024). This applies uniformly to `findUnique`, `findMany`, and a singleton's `get()`, under sudo and under a session alike. Foreign-key columns (e.g. `authorId`) are unaffected and always returned, so a relation stays reachable by id without an `include`.
|
|
43
|
+
|
|
44
|
+
**Detect call sites that need updating:**
|
|
45
|
+
|
|
46
|
+
- Grep for bare reads: `context.db.*.find*` calls (or a singleton's `.get()`) with no `include` and no `query` argument, whose result is later used to access a relationship field.
|
|
47
|
+
- Grep for `resolveOutput` hooks on `virtual` fields that read a relation off `item` (e.g. `item.author`, `item.posts`) — these silently degrade the same way, since a hook's own `context.db` read is subject to the same rule.
|
|
48
|
+
|
|
49
|
+
**Migrate** by naming the relation explicitly, either via `include`:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Before — relied on the auto-include
|
|
53
|
+
const post = await context.db.post.findUnique({ where: { id } })
|
|
54
|
+
post.author // used to be populated
|
|
55
|
+
|
|
56
|
+
// After — name it
|
|
57
|
+
const post = await context.db.post.findUnique({
|
|
58
|
+
where: { id },
|
|
59
|
+
include: { author: true },
|
|
60
|
+
})
|
|
61
|
+
post.author // populated
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
or via a fragment `query`:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const post = await context.db.post.findUnique({
|
|
68
|
+
where: { id },
|
|
69
|
+
query: postWithAuthorFragment,
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A `resolveOutput` hook that read `item.<relation>` should instead read through `context.db` with an explicit `include`, or its caller should pass one.
|
|
74
|
+
|
|
75
|
+
**Singleton `get()` gains caller-`include` support** it never had — it can now be narrowed and widened like any other read:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const settings = await context.db.settings.get({ include: { homepage: true } })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Bare reads also stop evaluating operation-level `query` access on related lists (that walk previously ran for every relation at every level before fetching anything), so an access function relied on for a side effect will no longer fire on a bare read.
|
|
82
|
+
|
|
83
|
+
See `docs/adr/0024-a-read-with-no-include-fetches-scalars-not-relations.md` for the full rationale.
|
|
84
|
+
|
|
3
85
|
## 0.34.0
|
|
4
86
|
|
|
5
87
|
### Minor Changes
|
package/CLAUDE.md
CHANGED
|
@@ -247,6 +247,10 @@ Reads run no `afterOperation` (list or field):
|
|
|
247
247
|
|
|
248
248
|
`context.db` reads (`findUnique`, `findMany`) do **not** apply Prisma's `select` semantics. Narrow a read with `include` (for relationships) or a fragment `query` instead. Passing `select` is a visible no-op: the op logs a one-time `console.warn` and still returns the full, access-filtered record. Field-level visibility is always enforced by access control regardless of `select`, so there is no leak — only a correctness/perf footgun the warning surfaces.
|
|
249
249
|
|
|
250
|
+
### A Bare Read Fetches Scalars, Not Relations (ADR-0024)
|
|
251
|
+
|
|
252
|
+
A read with no `include` and no fragment `query` returns the row's own columns plus its virtual fields — **never relations** — matching Prisma's own default. `findUnique`, `findMany`, and a singleton's `get()` all follow this rule uniformly, under sudo and under a session alike. Relations are fetched only when a caller names them via `include` or a fragment `query`, at which point the existing merge-with-access-control path (`mergeIncludeWithAccessControl`) applies exactly as before (#566/#830 unaffected). Foreign-key columns (e.g. `authorId`) are unaffected and always returned, so a relation stays reachable by id without an `include`. A `resolveOutput` hook that issues its own bare `context.db` read is subject to the same rule — reading `item.<relation>` inside such a hook silently returns `undefined` unless the hook's own read names that relation. See `docs/adr/0024-a-read-with-no-include-fetches-scalars-not-relations.md`.
|
|
253
|
+
|
|
250
254
|
### Context Type Safety
|
|
251
255
|
|
|
252
256
|
Context uses generic typing to preserve Prisma types:
|
|
@@ -409,6 +413,50 @@ console.log(user.fullName) // "John Doe" — computed via resolveOutput on every
|
|
|
409
413
|
- Can optionally provide `resolveInput` for write side effects
|
|
410
414
|
- Useful for derived values, computed properties, and external API sync
|
|
411
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
|
+
|
|
412
460
|
## Type Safety
|
|
413
461
|
|
|
414
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":"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"}
|
|
@@ -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;
|
|
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":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"field-visibility.js","sourceRoot":"","sources":["../../src/access/field-visibility.ts"],"names":[],"mappings":"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"}
|
package/dist/access/index.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/access/index.js
CHANGED
|
@@ -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.
|
package/dist/access/index.js.map
CHANGED
|
@@ -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"}
|
package/dist/config/types.d.ts
CHANGED
|
@@ -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,
|
|
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
|