@opensaas/stack-core 0.35.0 → 0.37.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 +88 -0
- package/CLAUDE.md +44 -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/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/types.d.ts +191 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +51 -95
- package/dist/context/index.js.map +1 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +40 -17
- package/dist/fields/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- 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/index.ts +2 -0
- package/src/config/types.ts +195 -1
- package/src/context/index.ts +96 -115
- package/src/fields/index.ts +47 -14
- package/src/index.ts +10 -0
- package/src/validation/needs-closure.ts +188 -0
- package/tests/field-types.test.ts +18 -2
- package/tests/needs-declared-dependencies.test.ts +500 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { getRelatedListConfig } from '../access/engine.js';
|
|
2
|
+
import { READ_INCLUDE_MAX_DEPTH } from '../access/depth-limits.js';
|
|
3
|
+
function isRelationshipFieldConfig(fieldConfig) {
|
|
4
|
+
return (!!fieldConfig &&
|
|
5
|
+
fieldConfig.type === 'relationship' &&
|
|
6
|
+
'ref' in fieldConfig &&
|
|
7
|
+
!!fieldConfig.ref);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The deepest needs-chain reachable from `listKey`, considering EVERY field
|
|
11
|
+
* on every list along the way that declares `needs` — not only the field
|
|
12
|
+
* that triggered the walk. A computed field runs wherever its list's rows
|
|
13
|
+
* are fetched, so once a list is reached, ALL of its own declared
|
|
14
|
+
* dependencies must be satisfiable too (ADR-0025's "at every level a field
|
|
15
|
+
* is computed").
|
|
16
|
+
*
|
|
17
|
+
* `path` is the list keys already on this DFS branch, root-first, used to
|
|
18
|
+
* detect a cycle (a chain that can never terminate).
|
|
19
|
+
*/
|
|
20
|
+
function listClosureDepth(listKey, config, path) {
|
|
21
|
+
const listConfig = config.lists[listKey];
|
|
22
|
+
if (!listConfig?.fields)
|
|
23
|
+
return { depth: 0, chain: [listKey] };
|
|
24
|
+
let maxDepth = 0;
|
|
25
|
+
let maxChain = [listKey];
|
|
26
|
+
for (const fieldConfig of Object.values(listConfig.fields)) {
|
|
27
|
+
if (!fieldConfig?.hooks?.resolveOutput)
|
|
28
|
+
continue;
|
|
29
|
+
for (const relationName of fieldConfig.needs ?? []) {
|
|
30
|
+
const relatedField = listConfig.fields[relationName];
|
|
31
|
+
if (!isRelationshipFieldConfig(relatedField))
|
|
32
|
+
continue;
|
|
33
|
+
const relatedConfig = getRelatedListConfig(relatedField.ref, config);
|
|
34
|
+
if (!relatedConfig)
|
|
35
|
+
continue;
|
|
36
|
+
const relatedListKey = relatedConfig.listName;
|
|
37
|
+
if (path.includes(relatedListKey)) {
|
|
38
|
+
return { cycle: [...path, relatedListKey] };
|
|
39
|
+
}
|
|
40
|
+
const sub = listClosureDepth(relatedListKey, config, [...path, relatedListKey]);
|
|
41
|
+
if ('cycle' in sub)
|
|
42
|
+
return sub;
|
|
43
|
+
if (1 + sub.depth > maxDepth) {
|
|
44
|
+
maxDepth = 1 + sub.depth;
|
|
45
|
+
maxChain = [listKey, ...sub.chain];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { depth: maxDepth, chain: maxChain };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate that every `needs` entry names an immediate relationship field
|
|
53
|
+
* declared on the SAME list. The generated `Lists.<List>.TypeInfo` already
|
|
54
|
+
* makes a misspelled or non-relation entry a compile error for a config
|
|
55
|
+
* annotated with it (`list<Lists.X.TypeInfo>({...})`, the documented
|
|
56
|
+
* pattern) — this is the runtime backstop for configs that aren't, or that
|
|
57
|
+
* are authored in plain JS.
|
|
58
|
+
*
|
|
59
|
+
* @param config - The fully resolved OpenSaas config.
|
|
60
|
+
* @returns All invalid `needs` entries, flattened across lists and fields.
|
|
61
|
+
*/
|
|
62
|
+
export function validateNeedsDeclarations(config) {
|
|
63
|
+
const errors = [];
|
|
64
|
+
for (const [listKey, listConfig] of Object.entries(config.lists)) {
|
|
65
|
+
if (!listConfig?.fields)
|
|
66
|
+
continue;
|
|
67
|
+
for (const [fieldKey, fieldConfig] of Object.entries(listConfig.fields)) {
|
|
68
|
+
for (const relationName of fieldConfig?.needs ?? []) {
|
|
69
|
+
if (isRelationshipFieldConfig(listConfig.fields[relationName]))
|
|
70
|
+
continue;
|
|
71
|
+
const exists = relationName in listConfig.fields;
|
|
72
|
+
errors.push({
|
|
73
|
+
listKey,
|
|
74
|
+
fieldKey,
|
|
75
|
+
chain: [listKey],
|
|
76
|
+
reason: 'invalid-relation',
|
|
77
|
+
message: exists
|
|
78
|
+
? `"${listKey}.${fieldKey}" declares needs: ['${relationName}'], but "${relationName}" ` +
|
|
79
|
+
`is not a relationship field on "${listKey}". \`needs\` may only name immediate ` +
|
|
80
|
+
`relationship fields declared on the same list.`
|
|
81
|
+
: `"${listKey}.${fieldKey}" declares needs: ['${relationName}'], but "${listKey}" has ` +
|
|
82
|
+
`no field named "${relationName}".`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return errors;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validate that every field's `needs` closure fits within
|
|
91
|
+
* `READ_INCLUDE_MAX_DEPTH` when evaluated starting at the declaring field's
|
|
92
|
+
* own list — the most favourable starting point a caller could ever give it.
|
|
93
|
+
* Intended to run once, before generation, exactly like
|
|
94
|
+
* {@link validateConfigFields} — a config whose closure cannot fit must not
|
|
95
|
+
* generate, per ADR-0025's "Depth" section.
|
|
96
|
+
*
|
|
97
|
+
* @param config - The fully resolved OpenSaas config.
|
|
98
|
+
* @returns All unsatisfiable-closure violations, flattened across lists and fields.
|
|
99
|
+
*/
|
|
100
|
+
export function validateNeedsClosureDepth(config) {
|
|
101
|
+
const errors = [];
|
|
102
|
+
for (const [listKey, listConfig] of Object.entries(config.lists)) {
|
|
103
|
+
if (!listConfig?.fields)
|
|
104
|
+
continue;
|
|
105
|
+
for (const [fieldKey, fieldConfig] of Object.entries(listConfig.fields)) {
|
|
106
|
+
if (!fieldConfig?.hooks?.resolveOutput || !fieldConfig.needs?.length)
|
|
107
|
+
continue;
|
|
108
|
+
const result = listClosureDepth(listKey, config, [listKey]);
|
|
109
|
+
if ('cycle' in result) {
|
|
110
|
+
errors.push({
|
|
111
|
+
listKey,
|
|
112
|
+
fieldKey,
|
|
113
|
+
chain: result.cycle,
|
|
114
|
+
reason: 'cycle',
|
|
115
|
+
message: `"${listKey}.${fieldKey}"'s needs declaration never terminates: ` +
|
|
116
|
+
`${result.cycle.join(' → ')} → … . A chain of \`needs\` across these lists cycles back ` +
|
|
117
|
+
`on itself, so no read could ever satisfy it. Break the cycle by removing one of the ` +
|
|
118
|
+
`\`needs\` entries on this chain, or compute the value without it.`,
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (result.depth >= READ_INCLUDE_MAX_DEPTH) {
|
|
123
|
+
errors.push({
|
|
124
|
+
listKey,
|
|
125
|
+
fieldKey,
|
|
126
|
+
chain: result.chain,
|
|
127
|
+
reason: 'depth',
|
|
128
|
+
message: `"${listKey}.${fieldKey}"'s needs declaration requires a closure ${result.depth} ` +
|
|
129
|
+
`relations deep even starting at "${listKey}" itself: ${result.chain.join(' → ')}. ` +
|
|
130
|
+
`This exceeds the Access Filter's maximum read-include depth ` +
|
|
131
|
+
`(${READ_INCLUDE_MAX_DEPTH}), so no caller — however they start the read — could ever ` +
|
|
132
|
+
`have this closure satisfied. Shorten the \`needs\` chain across these lists.`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return errors;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=needs-closure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"needs-closure.js","sourceRoot":"","sources":["../../src/validation/needs-closure.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { FieldConfig, OpenSaasConfig } from '../config/types.js'
|
|
2
|
+
import { getRelatedListConfig } from './engine.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Declared Dependencies — folding a computed field's `needs` into a read's
|
|
6
|
+
* `include` without widening what the caller receives (ADR-0025).
|
|
7
|
+
*
|
|
8
|
+
* A field's `needs` declares immediate sibling relations its `resolveOutput`
|
|
9
|
+
* hook cannot compute without. This module folds those relations into
|
|
10
|
+
* whatever `include` a read is already building (caller-supplied, fragment-
|
|
11
|
+
* derived, or none at all) BEFORE it reaches the existing access-scoping
|
|
12
|
+
* pipeline (`buildIncludeWithAccessControl` / `mergeIncludeWithAccessControl`
|
|
13
|
+
* in `access-filter.ts`) — a declared relation is scoped exactly like a
|
|
14
|
+
* caller-named one, never a bypass.
|
|
15
|
+
*
|
|
16
|
+
* The fold also tracks provenance: which relation keys, at which nesting
|
|
17
|
+
* level, were added ONLY to satisfy a declaration (as opposed to being named
|
|
18
|
+
* by the caller). `field-visibility.ts` uses that tree to strip those keys
|
|
19
|
+
* from the result after `resolveOutput` hooks have had a chance to read them
|
|
20
|
+
* — a declared dependency is private plumbing, not an implicit `include`.
|
|
21
|
+
*
|
|
22
|
+
* Reach beyond one hop: a relation added here to satisfy a declaration is
|
|
23
|
+
* added BARE (`true`), never with an explicit nested include of its own.
|
|
24
|
+
* `buildIncludeWithAccessControl` already auto-expands a bare relation's own
|
|
25
|
+
* readable-relationship subtree to `READ_INCLUDE_MAX_DEPTH` (pre-ADR-0026),
|
|
26
|
+
* so a chain of declarations rides that existing expansion for free — the
|
|
27
|
+
* related list's own declared needs are already present among what gets
|
|
28
|
+
* auto-included beneath it. This is also why a declaration-driven cycle
|
|
29
|
+
* (e.g. `Order.total` needs `lineItems`, `LineItem.orderRef` needs `order`)
|
|
30
|
+
* can't recurse without bound here: it flows through
|
|
31
|
+
* `buildIncludeWithAccessControl`'s existing `visitedLists` cycle guard
|
|
32
|
+
* rather than through any recursion of this module's own (see ADR-0026's
|
|
33
|
+
* note that this guard's remaining job, after that ADR lands, is defending
|
|
34
|
+
* exactly this fold).
|
|
35
|
+
*
|
|
36
|
+
* This module only recurses into EXPLICIT nested includes the caller wrote
|
|
37
|
+
* (narrowing what's fetched below a relation) — those cut off the free
|
|
38
|
+
* auto-expansion, so a nested list's own declared needs must be folded in
|
|
39
|
+
* explicitly. An explicit caller include is always a finite literal, so this
|
|
40
|
+
* recursion terminates on its own without a separate depth/cycle guard.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** Which relation keys, at which nesting level, exist only to satisfy a `needs` declaration. */
|
|
44
|
+
export type DeclaredOnlyTree = {
|
|
45
|
+
/** Keys at THIS level whose entire branch was added purely by the fold. */
|
|
46
|
+
keys: Set<string>
|
|
47
|
+
/** Per-key trees for relations present in the include for other reasons (caller-named), whose OWN nested include may still contain declaration-only keys. */
|
|
48
|
+
nested: Record<string, DeclaredOnlyTree>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function emptyDeclaredOnlyTree(): DeclaredOnlyTree {
|
|
52
|
+
return { keys: new Set(), nested: {} }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isDeclaredOnlyTreeEmpty(tree: DeclaredOnlyTree): boolean {
|
|
56
|
+
return tree.keys.size === 0 && Object.keys(tree.nested).length === 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isRelationshipFieldConfig(
|
|
60
|
+
fieldConfig: FieldConfig | undefined,
|
|
61
|
+
): fieldConfig is FieldConfig & { type: 'relationship'; ref: string } {
|
|
62
|
+
return (
|
|
63
|
+
!!fieldConfig &&
|
|
64
|
+
fieldConfig.type === 'relationship' &&
|
|
65
|
+
'ref' in fieldConfig &&
|
|
66
|
+
!!fieldConfig.ref
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The deduped set of relation names declared via `needs` by fields on this
|
|
72
|
+
* list that have a `resolveOutput` hook. A `needs` entry on a field without
|
|
73
|
+
* one is inert — there is no hook to feed it to — so it contributes nothing
|
|
74
|
+
* to fetch.
|
|
75
|
+
*/
|
|
76
|
+
export function getDeclaredRelationNames(fieldConfigs: Record<string, FieldConfig>): string[] {
|
|
77
|
+
const names = new Set<string>()
|
|
78
|
+
for (const fieldConfig of Object.values(fieldConfigs)) {
|
|
79
|
+
if (!fieldConfig?.hooks?.resolveOutput) continue
|
|
80
|
+
for (const name of fieldConfig.needs ?? []) {
|
|
81
|
+
names.add(name)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...names]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Fold this list's declared dependencies into `rawInclude`, recursing into
|
|
89
|
+
* any EXPLICIT nested include the caller wrote so a related list's own
|
|
90
|
+
* declared needs are satisfied too (see module doc comment).
|
|
91
|
+
*
|
|
92
|
+
* Returns `rawInclude` itself (same reference) when there is nothing to
|
|
93
|
+
* fold, so a list with no `needs` fields and no caller include stays on the
|
|
94
|
+
* exact bare-read path (ADR-0024) — untouched, not merely equivalent.
|
|
95
|
+
*/
|
|
96
|
+
export function foldDeclaredDependencies(
|
|
97
|
+
rawInclude: Record<string, unknown> | undefined,
|
|
98
|
+
fieldConfigs: Record<string, FieldConfig>,
|
|
99
|
+
config: OpenSaasConfig,
|
|
100
|
+
): { include: Record<string, unknown> | undefined; declaredOnly: DeclaredOnlyTree } {
|
|
101
|
+
const declaredNames = getDeclaredRelationNames(fieldConfigs)
|
|
102
|
+
|
|
103
|
+
if (declaredNames.length === 0 && !rawInclude) {
|
|
104
|
+
return { include: rawInclude, declaredOnly: emptyDeclaredOnlyTree() }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const declaredOnly = emptyDeclaredOnlyTree()
|
|
108
|
+
const merged: Record<string, unknown> = { ...(rawInclude ?? {}) }
|
|
109
|
+
|
|
110
|
+
for (const name of declaredNames) {
|
|
111
|
+
if (name in merged) continue // caller (or fragment) already asked for it — not declaration-only
|
|
112
|
+
if (!isRelationshipFieldConfig(fieldConfigs[name])) continue // invalid `needs` entry; caught by generate-time validation
|
|
113
|
+
merged[name] = true
|
|
114
|
+
declaredOnly.keys.add(name)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
118
|
+
const fieldConfig = fieldConfigs[key]
|
|
119
|
+
if (!isRelationshipFieldConfig(fieldConfig)) continue
|
|
120
|
+
// A whole branch we just added is bare `true` — its own subtree auto-expands
|
|
121
|
+
// (see module doc comment), so there is no explicit nested include to recurse into.
|
|
122
|
+
if (declaredOnly.keys.has(key)) continue
|
|
123
|
+
|
|
124
|
+
const entry = value as { include?: Record<string, unknown> } | boolean
|
|
125
|
+
if (!entry || typeof entry !== 'object' || !entry.include) continue
|
|
126
|
+
|
|
127
|
+
const relatedConfig = getRelatedListConfig(fieldConfig.ref, config)
|
|
128
|
+
if (!relatedConfig) continue
|
|
129
|
+
|
|
130
|
+
const nested = foldDeclaredDependencies(entry.include, relatedConfig.listConfig.fields, config)
|
|
131
|
+
if (nested.include !== entry.include) {
|
|
132
|
+
merged[key] = { ...entry, include: nested.include }
|
|
133
|
+
}
|
|
134
|
+
if (!isDeclaredOnlyTreeEmpty(nested.declaredOnly)) {
|
|
135
|
+
declaredOnly.nested[key] = nested.declaredOnly
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { include: merged, declaredOnly }
|
|
140
|
+
}
|
|
@@ -4,6 +4,8 @@ import { getRelatedListConfig } from './engine.js'
|
|
|
4
4
|
import { checkFieldAccess } from './field-access.js'
|
|
5
5
|
import { RESOLVE_CHAIN_MAX_LENGTH } from './depth-limits.js'
|
|
6
6
|
import { ResolveOutputCycleError } from './errors.js'
|
|
7
|
+
import type { DeclaredOnlyTree } from './declared-dependencies.js'
|
|
8
|
+
import { emptyDeclaredOnlyTree } from './declared-dependencies.js'
|
|
7
9
|
// NOTE: `context/index.ts` imports `filterReadableFields` from this module
|
|
8
10
|
// (via the `access/index.ts` barrel) — this is an intentional cyclic
|
|
9
11
|
// dependency, the same shape and for the same reason as the one documented in
|
|
@@ -181,6 +183,12 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
181
183
|
config?: OpenSaasConfig,
|
|
182
184
|
depth: number = 0,
|
|
183
185
|
listKey?: string,
|
|
186
|
+
// Relation keys added purely to satisfy a field's `needs` (ADR-0025) at
|
|
187
|
+
// THIS level, and the same tree for each nested relation reached via a
|
|
188
|
+
// caller-named branch. Stripped from `filtered` right before it is
|
|
189
|
+
// returned — after resolveOutput has had a chance to read them — so a
|
|
190
|
+
// declared dependency never widens what the caller receives.
|
|
191
|
+
declaredOnly: DeclaredOnlyTree = emptyDeclaredOnlyTree(),
|
|
184
192
|
): Promise<Partial<T>> {
|
|
185
193
|
const filtered: Record<string, unknown> = {}
|
|
186
194
|
|
|
@@ -247,6 +255,11 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
247
255
|
}
|
|
248
256
|
|
|
249
257
|
const relatedConfig = getRelatedListConfig(fieldConfig.ref as string, config)
|
|
258
|
+
// The declared-only tree for whatever THIS relation's own list computes,
|
|
259
|
+
// e.g. a field on the related list that declares its own `needs`. Falls
|
|
260
|
+
// back to an empty tree when this relation isn't declaration-related at
|
|
261
|
+
// all — the common case.
|
|
262
|
+
const nestedDeclaredOnly = declaredOnly.nested[fieldName] ?? emptyDeclaredOnlyTree()
|
|
250
263
|
|
|
251
264
|
if (relatedConfig) {
|
|
252
265
|
// For many relationships (arrays) - recursively filter fields in each item
|
|
@@ -261,6 +274,7 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
261
274
|
config,
|
|
262
275
|
depth + 1,
|
|
263
276
|
relatedConfig.listName,
|
|
277
|
+
nestedDeclaredOnly,
|
|
264
278
|
),
|
|
265
279
|
),
|
|
266
280
|
)
|
|
@@ -275,6 +289,7 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
275
289
|
config,
|
|
276
290
|
depth + 1,
|
|
277
291
|
relatedConfig.listName,
|
|
292
|
+
nestedDeclaredOnly,
|
|
278
293
|
)
|
|
279
294
|
}
|
|
280
295
|
} else {
|
|
@@ -341,5 +356,14 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
341
356
|
}
|
|
342
357
|
}
|
|
343
358
|
|
|
359
|
+
// Strip relations that were fetched ONLY to satisfy a `needs` declaration
|
|
360
|
+
// (ADR-0025), now that every resolveOutput hook at this level — including
|
|
361
|
+
// virtual fields, which read the assembled `filtered` object above — has
|
|
362
|
+
// had the chance to see them. A declared dependency is private plumbing,
|
|
363
|
+
// not an implicit `include`: it never widens what the caller receives.
|
|
364
|
+
for (const key of declaredOnly.keys) {
|
|
365
|
+
delete filtered[key]
|
|
366
|
+
}
|
|
367
|
+
|
|
344
368
|
return filtered as Partial<T>
|
|
345
369
|
}
|
package/src/access/index.ts
CHANGED
|
@@ -31,6 +31,14 @@ export {
|
|
|
31
31
|
export type { AccessIncludeResult } from './access-filter.js'
|
|
32
32
|
// Phase 2 — Field Visibility (post-query field stripping + resolveOutput).
|
|
33
33
|
export { filterReadableFields } from './field-visibility.js'
|
|
34
|
+
// Declared Dependencies — folding `needs` into an include without widening
|
|
35
|
+
// the result (ADR-0025).
|
|
36
|
+
export {
|
|
37
|
+
foldDeclaredDependencies,
|
|
38
|
+
getDeclaredRelationNames,
|
|
39
|
+
emptyDeclaredOnlyTree,
|
|
40
|
+
} from './declared-dependencies.js'
|
|
41
|
+
export type { DeclaredOnlyTree } from './declared-dependencies.js'
|
|
34
42
|
// Thrown when a caller include reaches past the depth the Access Filter can scope.
|
|
35
43
|
export { AccessScopeDepthExceededError } from './errors.js'
|
|
36
44
|
// Thrown when a resolveOutput hook's own resolve chain cycles back into itself.
|
package/src/config/index.ts
CHANGED
package/src/config/types.ts
CHANGED
|
@@ -640,7 +640,8 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
|
|
|
640
640
|
* @param keystoneCompat - Whether Keystone-compat mode is enabled (db.keystoneCompat).
|
|
641
641
|
* When true, non-null text columns without an explicit defaultValue emit
|
|
642
642
|
* `@default("")` to match Keystone 6's implicit empty-string text default.
|
|
643
|
-
* @returns Prisma type string, optional modifiers,
|
|
643
|
+
* @returns Prisma type string, optional modifiers, optional enum values, and
|
|
644
|
+
* an optional block-level index request
|
|
644
645
|
*/
|
|
645
646
|
getPrismaType?: (
|
|
646
647
|
fieldName: string,
|
|
@@ -655,6 +656,26 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
|
|
|
655
656
|
* The enum name is the value of `type`.
|
|
656
657
|
*/
|
|
657
658
|
enumValues?: string[]
|
|
659
|
+
/**
|
|
660
|
+
* If set, this field requires a block-level index on the owning model:
|
|
661
|
+
* `@@index([fieldName])` for `true`, `@@unique([fieldName])` for
|
|
662
|
+
* `'unique'`. `false` and `undefined` both mean "no index".
|
|
663
|
+
*
|
|
664
|
+
* Prisma has no field-level `@index` attribute — a non-unique index can
|
|
665
|
+
* ONLY be expressed as the model-level `@@index([...])` — so a field that
|
|
666
|
+
* wants one has to ask for it out-of-line rather than appending to
|
|
667
|
+
* {@link modifiers}. (A unique index has both forms available; the
|
|
668
|
+
* built-in scalars keep emitting the inline `@unique` modifier for that
|
|
669
|
+
* case, so this channel carries only what cannot be written inline.)
|
|
670
|
+
*
|
|
671
|
+
* Same shape as {@link PrismaRelationResult.foreignKeyIndex}, which is how
|
|
672
|
+
* relationship fields have always emitted their foreign-key indexes. The
|
|
673
|
+
* generator handles both through one emit pass, so the field stays the
|
|
674
|
+
* authority on whether it can be indexed by name at all — a multi-column
|
|
675
|
+
* field (see {@link getPrismaColumns}) has no single column matching its
|
|
676
|
+
* field name and can decline, or name a real column of its own.
|
|
677
|
+
*/
|
|
678
|
+
index?: boolean | 'unique'
|
|
658
679
|
}
|
|
659
680
|
/**
|
|
660
681
|
* Get TypeScript type information for type generation
|
|
@@ -755,6 +776,49 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
|
|
|
755
776
|
* @param value - The resolved logical value (metadata, or `null` to clear)
|
|
756
777
|
*/
|
|
757
778
|
splitColumns?: (fieldName: string, value: unknown) => Record<string, unknown>
|
|
779
|
+
/**
|
|
780
|
+
* Declares the immediate sibling relations this field's `resolveOutput`
|
|
781
|
+
* hook cannot compute without (ADR-0025 — the "Declared dependency" glossary
|
|
782
|
+
* entry in `CONTEXT.md`). The read fetches each declared relation wherever
|
|
783
|
+
* this field is computed — at the root of a read and at every nested level
|
|
784
|
+
* alike — and scopes it through the Access Filter exactly like a
|
|
785
|
+
* caller-named relation: a dependency a session cannot query is not
|
|
786
|
+
* fetched, and the hook sees nothing in its place.
|
|
787
|
+
*
|
|
788
|
+
* A declared dependency is private plumbing, not an implicit `include`: it
|
|
789
|
+
* is stripped from the result unless the caller named it too, so declaring
|
|
790
|
+
* or removing one changes this field's implementation, never the shape of
|
|
791
|
+
* every read of the list.
|
|
792
|
+
*
|
|
793
|
+
* Names immediate relations only — no dotted paths. Reach beyond one hop
|
|
794
|
+
* comes from the recursive fold: a dependency's own list declares its own
|
|
795
|
+
* dependencies.
|
|
796
|
+
*
|
|
797
|
+
* Typed as a plain `string[]`, not narrowed to this list's own relation
|
|
798
|
+
* keys: `BaseFieldConfig` is the contextual type EVERY field builder's
|
|
799
|
+
* return type is checked against, including non-generic third-party ones
|
|
800
|
+
* (`richText(): RichTextField`, with no `TTypeInfo` parameter of its own —
|
|
801
|
+
* the documented third-party field pattern). Narrowing `needs` per-list
|
|
802
|
+
* would make `needs`'s type on a fixed, unparameterized third-party field
|
|
803
|
+
* config disagree with the narrower type this list's own slot expects,
|
|
804
|
+
* breaking assignability for every such field regardless of whether it
|
|
805
|
+
* uses `needs` at all. A misspelled or non-relation entry is instead
|
|
806
|
+
* caught by `pnpm generate` (`validateNeedsDeclarations`), which has no
|
|
807
|
+
* such constraint.
|
|
808
|
+
*
|
|
809
|
+
* @example
|
|
810
|
+
* ```typescript
|
|
811
|
+
* lineItems: relationship({ ref: 'LineItem.order', many: true }),
|
|
812
|
+
* total: virtual({
|
|
813
|
+
* type: 'number',
|
|
814
|
+
* needs: ['lineItems'],
|
|
815
|
+
* hooks: {
|
|
816
|
+
* resolveOutput: ({ item }) => item.lineItems.reduce((sum, li) => sum + li.price, 0),
|
|
817
|
+
* },
|
|
818
|
+
* }),
|
|
819
|
+
* ```
|
|
820
|
+
*/
|
|
821
|
+
needs?: string[]
|
|
758
822
|
}
|
|
759
823
|
|
|
760
824
|
/**
|
|
@@ -1045,6 +1109,28 @@ export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> =
|
|
|
1045
1109
|
*/
|
|
1046
1110
|
isIndexed?: boolean | 'unique'
|
|
1047
1111
|
db?: {
|
|
1112
|
+
/**
|
|
1113
|
+
* Controls DB-level nullability of the foreign key column (and its
|
|
1114
|
+
* relation field) independently of the many side's own shape. Only
|
|
1115
|
+
* meaningful on the FK-owning (single) side of a relationship — the
|
|
1116
|
+
* many side has no column of its own to make non-nullable and rejects
|
|
1117
|
+
* this option.
|
|
1118
|
+
*
|
|
1119
|
+
* @default true (nullable, matching every relationship generated before
|
|
1120
|
+
* this option existed)
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```typescript
|
|
1124
|
+
* // Every session genuinely belongs to a user — make the FK required
|
|
1125
|
+
* user: relationship({
|
|
1126
|
+
* ref: 'User.sessions',
|
|
1127
|
+
* db: { isNullable: false },
|
|
1128
|
+
* })
|
|
1129
|
+
* // Generates: userId String (was String?)
|
|
1130
|
+
* // user User @relation(...) (was User?)
|
|
1131
|
+
* ```
|
|
1132
|
+
*/
|
|
1133
|
+
isNullable?: boolean
|
|
1048
1134
|
/**
|
|
1049
1135
|
* Controls foreign key placement and column name for bidirectional relationships
|
|
1050
1136
|
* Can be a boolean or an object with a map property
|
|
@@ -1182,6 +1268,16 @@ export type PrismaRelationResult = {
|
|
|
1182
1268
|
* for the many side or the non-FK side it is `[relationLine]`.
|
|
1183
1269
|
*/
|
|
1184
1270
|
modelLines: string[]
|
|
1271
|
+
/**
|
|
1272
|
+
* The Prisma-level foreign key field name this side owns (e.g. `authorId`),
|
|
1273
|
+
* regardless of whether it is indexed. `undefined` when this side doesn't
|
|
1274
|
+
* own a foreign key column at all (the many side, or the non-FK side of a
|
|
1275
|
+
* one-to-one). Lets other generator passes resolve a relationship field
|
|
1276
|
+
* name to its physical column — e.g. a model-level composite index
|
|
1277
|
+
* ({@link ListIndex}) naming a relationship field — without duplicating
|
|
1278
|
+
* {@link foreignKeyIndex}'s narrower, indexing-conditional presence.
|
|
1279
|
+
*/
|
|
1280
|
+
foreignKeyField?: string
|
|
1185
1281
|
/**
|
|
1186
1282
|
* Foreign key index to add to the owning model, if this side owns an
|
|
1187
1283
|
* indexed foreign key.
|
|
@@ -1738,6 +1834,43 @@ export type Hooks<
|
|
|
1738
1834
|
validateInput?: (args: ValidateHookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>
|
|
1739
1835
|
}
|
|
1740
1836
|
|
|
1837
|
+
/**
|
|
1838
|
+
* A single field reference within a model-level {@link ListIndex}. Either
|
|
1839
|
+
* just the OpenSaaS field name, or an object naming it alongside a sort
|
|
1840
|
+
* direction.
|
|
1841
|
+
*/
|
|
1842
|
+
export type ListIndexFieldRef =
|
|
1843
|
+
| string
|
|
1844
|
+
| {
|
|
1845
|
+
field: string
|
|
1846
|
+
/** Sort direction for this column within the index/constraint. */
|
|
1847
|
+
sort?: 'asc' | 'desc'
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
/**
|
|
1851
|
+
* A model-level composite `@@unique`/`@@index` constraint spanning two or
|
|
1852
|
+
* more of a list's own fields. See {@link ListConfig.db}'s `indexes` for the
|
|
1853
|
+
* full explanation and examples (#864).
|
|
1854
|
+
*/
|
|
1855
|
+
export type ListIndex = {
|
|
1856
|
+
/**
|
|
1857
|
+
* The fields participating in this index/constraint, in declaration order.
|
|
1858
|
+
* OpenSaaS field names, not raw database column names.
|
|
1859
|
+
*/
|
|
1860
|
+
fields: ListIndexFieldRef[]
|
|
1861
|
+
/**
|
|
1862
|
+
* Emit `@@unique([...])` instead of `@@index([...])`.
|
|
1863
|
+
* @default false
|
|
1864
|
+
*/
|
|
1865
|
+
unique?: boolean
|
|
1866
|
+
/**
|
|
1867
|
+
* Constraint/index name, emitted as Prisma's `map:` argument — lets an
|
|
1868
|
+
* existing live constraint be adopted under its current name rather than
|
|
1869
|
+
* renamed.
|
|
1870
|
+
*/
|
|
1871
|
+
name?: string
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1741
1874
|
// Generic `any` default allows ListConfig to work with any list item type
|
|
1742
1875
|
// This is needed because the item type varies per list and is inferred from Prisma models
|
|
1743
1876
|
/**
|
|
@@ -1815,6 +1948,67 @@ export type ListConfig<TTypeInfo extends TypeInfo> = {
|
|
|
1815
1948
|
* ```
|
|
1816
1949
|
*/
|
|
1817
1950
|
timestamps?: boolean
|
|
1951
|
+
/**
|
|
1952
|
+
* Model-level composite `@@unique`/`@@index` constraints spanning two or
|
|
1953
|
+
* more of this list's own fields (#864).
|
|
1954
|
+
*
|
|
1955
|
+
* Field-level `isIndexed` (on a scalar or relationship field) can only
|
|
1956
|
+
* ever produce a single-column index — it has no way to express a
|
|
1957
|
+
* constraint or index that spans more than one column. `db.indexes` is
|
|
1958
|
+
* that multi-column case: each entry names two or more of the list's own
|
|
1959
|
+
* OpenSaaS field names, not raw database column names. The generator
|
|
1960
|
+
* resolves each to its underlying Prisma column — a scalar field's own
|
|
1961
|
+
* name (its Prisma field name is unaffected by `db.map`), or a
|
|
1962
|
+
* relationship field's foreign key column (`<field>Id`) when this side
|
|
1963
|
+
* owns it.
|
|
1964
|
+
*
|
|
1965
|
+
* A composite **unique** is the load-bearing case: it's the
|
|
1966
|
+
* database-level backstop for a business rule two concurrent writes could
|
|
1967
|
+
* otherwise both slip past (e.g. "one booking per student per
|
|
1968
|
+
* production") — something a hook's existence check cannot close on its
|
|
1969
|
+
* own, and can't be retrofitted once duplicate rows exist. A composite
|
|
1970
|
+
* **index** (non-unique) is the equivalent performance-only case (a hot
|
|
1971
|
+
* multi-column lookup path).
|
|
1972
|
+
*
|
|
1973
|
+
* An entry naming a field the list doesn't have, a virtual field, a
|
|
1974
|
+
* to-many relationship, or the non-FK side of a one-to-one relationship
|
|
1975
|
+
* fails `pnpm generate` with an error naming the list, the entry, and the
|
|
1976
|
+
* bad field — it is never silently dropped or emitted as invalid Prisma.
|
|
1977
|
+
*
|
|
1978
|
+
* @example One audition per student per production (composite unique)
|
|
1979
|
+
* ```typescript
|
|
1980
|
+
* Audition: list({
|
|
1981
|
+
* fields: {
|
|
1982
|
+
* student: relationship({ ref: 'Student.auditions' }),
|
|
1983
|
+
* production: relationship({ ref: 'Production.auditions' }),
|
|
1984
|
+
* },
|
|
1985
|
+
* db: {
|
|
1986
|
+
* indexes: [{ fields: ['student', 'production'], unique: true }],
|
|
1987
|
+
* },
|
|
1988
|
+
* })
|
|
1989
|
+
* // Generates: @@unique([studentId, productionId])
|
|
1990
|
+
* ```
|
|
1991
|
+
*
|
|
1992
|
+
* @example Hot lookup path (composite index) with a sort direction and an adopted constraint name
|
|
1993
|
+
* ```typescript
|
|
1994
|
+
* AuthVerification: list({
|
|
1995
|
+
* fields: {
|
|
1996
|
+
* identifier: text(),
|
|
1997
|
+
* createdAt: timestamp(),
|
|
1998
|
+
* },
|
|
1999
|
+
* db: {
|
|
2000
|
+
* indexes: [
|
|
2001
|
+
* {
|
|
2002
|
+
* fields: ['identifier', { field: 'createdAt', sort: 'desc' }],
|
|
2003
|
+
* name: 'AuthVerification_identifier_createdAt_idx',
|
|
2004
|
+
* },
|
|
2005
|
+
* ],
|
|
2006
|
+
* },
|
|
2007
|
+
* })
|
|
2008
|
+
* // Generates: @@index([identifier, createdAt(sort: Desc)], map: "AuthVerification_identifier_createdAt_idx")
|
|
2009
|
+
* ```
|
|
2010
|
+
*/
|
|
2011
|
+
indexes?: ListIndex[]
|
|
1818
2012
|
}
|
|
1819
2013
|
/**
|
|
1820
2014
|
* MCP server configuration for this list
|