@opensaas/stack-core 0.37.0 → 0.39.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 +191 -0
- package/CLAUDE.md +42 -3
- package/dist/access/access-filter.d.ts +70 -113
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +183 -210
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +148 -188
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/declared-dependencies.d.ts +66 -26
- package/dist/access/declared-dependencies.d.ts.map +1 -1
- package/dist/access/declared-dependencies.js +62 -17
- package/dist/access/declared-dependencies.js.map +1 -1
- package/dist/access/declared-dependencies.test.d.ts +2 -0
- package/dist/access/declared-dependencies.test.d.ts.map +1 -0
- package/dist/access/declared-dependencies.test.js +226 -0
- package/dist/access/declared-dependencies.test.js.map +1 -0
- package/dist/access/depth-limits.d.ts +8 -7
- package/dist/access/depth-limits.d.ts.map +1 -1
- package/dist/access/depth-limits.js +8 -7
- package/dist/access/depth-limits.js.map +1 -1
- package/dist/access/engine.d.ts +0 -20
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +0 -25
- package/dist/access/engine.js.map +1 -1
- package/dist/access/errors.d.ts +61 -8
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +91 -12
- package/dist/access/errors.js.map +1 -1
- package/dist/access/field-access.d.ts +39 -1
- package/dist/access/field-access.d.ts.map +1 -1
- package/dist/access/field-access.js +108 -42
- package/dist/access/field-access.js.map +1 -1
- package/dist/access/field-access.test.js +229 -2
- package/dist/access/field-access.test.js.map +1 -1
- package/dist/access/field-transforms.d.ts +0 -27
- package/dist/access/field-transforms.d.ts.map +1 -1
- package/dist/access/field-visibility.d.ts +2 -5
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +89 -41
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +6 -3
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +12 -2
- package/dist/access/index.js.map +1 -1
- package/dist/access/query-validation.d.ts +121 -0
- package/dist/access/query-validation.d.ts.map +1 -0
- package/dist/access/query-validation.js +292 -0
- package/dist/access/query-validation.js.map +1 -0
- package/dist/access/relationship-count.d.ts +1 -1
- package/dist/access/relationship-count.d.ts.map +1 -1
- package/dist/access/relationship-count.js +2 -10
- package/dist/access/relationship-count.js.map +1 -1
- package/dist/access/relationship-label-filter.d.ts +23 -23
- package/dist/access/relationship-label-filter.d.ts.map +1 -1
- package/dist/access/relationship-label-filter.js +5 -76
- package/dist/access/relationship-label-filter.js.map +1 -1
- package/dist/access/relationship-label-filter.test.js +19 -56
- package/dist/access/relationship-label-filter.test.js.map +1 -1
- package/dist/access/transaction-registry.d.ts +45 -0
- package/dist/access/transaction-registry.d.ts.map +1 -0
- package/dist/access/transaction-registry.js +37 -0
- package/dist/access/transaction-registry.js.map +1 -0
- package/dist/access/types.d.ts +55 -59
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/index.d.ts +9 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +10 -18
- package/dist/config/index.js.map +1 -1
- package/dist/config/label.d.ts +6 -13
- package/dist/config/label.d.ts.map +1 -1
- package/dist/config/label.js +6 -13
- package/dist/config/label.js.map +1 -1
- package/dist/config/nav-count.d.ts +11 -19
- package/dist/config/nav-count.d.ts.map +1 -1
- package/dist/config/nav-count.js +11 -19
- package/dist/config/nav-count.js.map +1 -1
- package/dist/config/plugin-engine.d.ts +0 -10
- package/dist/config/plugin-engine.d.ts.map +1 -1
- package/dist/config/plugin-engine.js +1 -37
- package/dist/config/plugin-engine.js.map +1 -1
- package/dist/config/types.d.ts +94 -109
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/apply-defaults.d.ts +8 -31
- package/dist/context/apply-defaults.d.ts.map +1 -1
- package/dist/context/apply-defaults.js +12 -41
- package/dist/context/apply-defaults.js.map +1 -1
- package/dist/context/hook-pipeline.d.ts +4 -29
- package/dist/context/hook-pipeline.d.ts.map +1 -1
- package/dist/context/hook-pipeline.js +8 -41
- package/dist/context/hook-pipeline.js.map +1 -1
- package/dist/context/index.d.ts +4 -15
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +223 -215
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts +7 -26
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +73 -171
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/transaction-boundary.d.ts +36 -47
- package/dist/context/transaction-boundary.d.ts.map +1 -1
- package/dist/context/transaction-boundary.js +92 -79
- package/dist/context/transaction-boundary.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +25 -64
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +81 -111
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/fields/format-prisma-default.d.ts.map +1 -1
- package/dist/fields/format-prisma-default.js +4 -7
- package/dist/fields/format-prisma-default.js.map +1 -1
- package/dist/fields/index.d.ts +17 -2
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +136 -147
- package/dist/fields/index.js.map +1 -1
- package/dist/filter/collect.d.ts +26 -3
- package/dist/filter/collect.d.ts.map +1 -1
- package/dist/filter/collect.js +30 -12
- package/dist/filter/collect.js.map +1 -1
- package/dist/filter/filter.test.js +98 -15
- package/dist/filter/filter.test.js.map +1 -1
- package/dist/filter/index.d.ts +1 -0
- package/dist/filter/index.d.ts.map +1 -1
- package/dist/filter/index.js.map +1 -1
- package/dist/filter/map.d.ts.map +1 -1
- package/dist/filter/map.js +0 -5
- package/dist/filter/map.js.map +1 -1
- package/dist/filter/parse.d.ts.map +1 -1
- package/dist/filter/parse.js +2 -9
- package/dist/filter/parse.js.map +1 -1
- package/dist/filter/serialize.d.ts.map +1 -1
- package/dist/filter/serialize.js +2 -4
- package/dist/filter/serialize.js.map +1 -1
- package/dist/hooks/index.d.ts +4 -42
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +5 -59
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -1
- package/dist/lib/case-utils.d.ts +0 -32
- package/dist/lib/case-utils.d.ts.map +1 -1
- package/dist/lib/case-utils.js +0 -32
- package/dist/lib/case-utils.js.map +1 -1
- package/dist/mcp/handler.d.ts +0 -4
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/mcp/handler.js +12 -69
- package/dist/mcp/handler.js.map +1 -1
- package/dist/query/index.d.ts +55 -227
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +41 -117
- package/dist/query/index.js.map +1 -1
- package/dist/query/relationship-options.d.ts +1 -1
- package/dist/query/relationship-options.js +1 -1
- package/dist/utils/password.d.ts +0 -68
- package/dist/utils/password.d.ts.map +1 -1
- package/dist/utils/password.js +7 -76
- package/dist/utils/password.js.map +1 -1
- package/dist/validation/field-config.d.ts.map +1 -1
- package/dist/validation/field-config.js +0 -6
- package/dist/validation/field-config.js.map +1 -1
- package/dist/validation/schema.d.ts +0 -7
- package/dist/validation/schema.d.ts.map +1 -1
- package/dist/validation/schema.js +0 -10
- package/dist/validation/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +205 -275
- package/src/access/access-filter.ts +236 -272
- package/src/access/declared-dependencies.test.ts +277 -0
- package/src/access/declared-dependencies.ts +117 -37
- package/src/access/depth-limits.ts +8 -7
- package/src/access/engine.ts +0 -25
- package/src/access/errors.ts +97 -12
- package/src/access/field-access.test.ts +312 -2
- package/src/access/field-access.ts +118 -42
- package/src/access/field-transforms.ts +8 -39
- package/src/access/field-visibility.ts +99 -45
- package/src/access/index.ts +17 -5
- package/src/access/query-validation.ts +359 -0
- package/src/access/relationship-count.ts +3 -11
- package/src/access/relationship-label-filter.test.ts +19 -68
- package/src/access/relationship-label-filter.ts +27 -104
- package/src/access/transaction-registry.ts +58 -0
- package/src/access/types.ts +83 -86
- package/src/config/index.ts +10 -22
- package/src/config/label.ts +6 -13
- package/src/config/nav-count.ts +11 -19
- package/src/config/plugin-engine.ts +1 -37
- package/src/config/types.ts +95 -110
- package/src/context/apply-defaults.ts +12 -44
- package/src/context/hook-pipeline.ts +12 -67
- package/src/context/index.ts +311 -276
- package/src/context/nested-operations.ts +94 -226
- package/src/context/transaction-boundary.ts +113 -109
- package/src/context/write-pipeline.ts +99 -155
- package/src/fields/format-prisma-default.ts +4 -7
- package/src/fields/index.ts +155 -147
- package/src/filter/collect.ts +48 -12
- package/src/filter/filter.test.ts +153 -15
- package/src/filter/index.ts +1 -0
- package/src/filter/map.ts +0 -5
- package/src/filter/parse.ts +2 -9
- package/src/filter/serialize.ts +2 -4
- package/src/hooks/index.ts +5 -59
- package/src/index.ts +23 -4
- package/src/internal.ts +5 -0
- package/src/lib/case-utils.ts +0 -32
- package/src/mcp/handler.ts +13 -71
- package/src/query/index.ts +82 -235
- package/src/query/relationship-options.ts +1 -1
- package/src/utils/password.ts +7 -76
- package/src/validation/field-config.ts +0 -6
- package/src/validation/schema.ts +0 -10
- package/tests/access-relationships.test.ts +18 -16
- package/tests/computed-field-selective-evaluation.test.ts +418 -0
- package/tests/context.test.ts +727 -1
- package/tests/field-types.test.ts +222 -0
- package/tests/mcp-handler.test.ts +34 -0
- package/tests/needs-declared-dependencies.test.ts +7 -4
- package/tests/resolve-chain.test.ts +11 -11
- package/tests/transaction-boundary-outermost.test.ts +530 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
import type { Session, AccessContext } from './types.js'
|
|
2
|
-
import type { FieldAccess } from './types.js'
|
|
2
|
+
import type { FieldAccess, FieldAccessControl } from './types.js'
|
|
3
3
|
// `ValidationError` is referenced only inside function bodies (call-time), never
|
|
4
4
|
// at module-evaluation time, so the field-access ⇄ hooks import cycle is safe
|
|
5
5
|
// under ESM live bindings.
|
|
6
6
|
import { ValidationError } from '../hooks/index.js'
|
|
7
|
+
import { InvalidFieldAccessResultError } from './errors.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Marks a throw caused by touching {@link createPoisonedItem}'s `item`, as
|
|
11
|
+
* opposed to some other error a field rule legitimately raises. Not exported
|
|
12
|
+
* — callers only ever see its effect (a `false` from
|
|
13
|
+
* `isFieldReadableForPredicate`), never the class itself.
|
|
14
|
+
*/
|
|
15
|
+
class PredicateTimeItemAccessError extends Error {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An `item` that throws {@link PredicateTimeItemAccessError} on ANY attempt to
|
|
19
|
+
* read a property off it — including via optional chaining (`item?.x`),
|
|
20
|
+
* since the Proxy itself is a truthy object and optional chaining still
|
|
21
|
+
* performs the property read once its base is non-nullish. Used by
|
|
22
|
+
* `isFieldReadableForPredicate` to detect a read rule that depends on the
|
|
23
|
+
* fetched row: there is no row yet at predicate-evaluation time (see that
|
|
24
|
+
* function's doc), so any rule that reaches into `item` at all cannot be
|
|
25
|
+
* answered here.
|
|
26
|
+
*/
|
|
27
|
+
function createPoisonedItem(): Record<string, unknown> {
|
|
28
|
+
return new Proxy(
|
|
29
|
+
{},
|
|
30
|
+
{
|
|
31
|
+
get(_target, prop) {
|
|
32
|
+
throw new PredicateTimeItemAccessError(String(prop))
|
|
33
|
+
},
|
|
34
|
+
has(_target, prop) {
|
|
35
|
+
throw new PredicateTimeItemAccessError(String(prop))
|
|
36
|
+
},
|
|
37
|
+
ownKeys() {
|
|
38
|
+
throw new PredicateTimeItemAccessError('ownKeys')
|
|
39
|
+
},
|
|
40
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
41
|
+
throw new PredicateTimeItemAccessError(String(prop))
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
) as Record<string, unknown>
|
|
45
|
+
}
|
|
7
46
|
|
|
8
47
|
/**
|
|
9
48
|
* Shared field-level access evaluation.
|
|
@@ -24,6 +63,10 @@ import { ValidationError } from '../hooks/index.js'
|
|
|
24
63
|
* (read/update/delete), and on the `inputData` being written (create/update),
|
|
25
64
|
* so all of those are accepted. Do not introduce a parallel evaluator with a
|
|
26
65
|
* narrower signature.
|
|
66
|
+
*
|
|
67
|
+
* Returns a strict boolean, never a filter: a rule that returns anything else
|
|
68
|
+
* throws `InvalidFieldAccessResultError` rather than defaulting to allow (see
|
|
69
|
+
* that error's doc, ADR-0001, and ADR-0030).
|
|
27
70
|
*/
|
|
28
71
|
export async function checkFieldAccess(
|
|
29
72
|
fieldAccess: FieldAccess | undefined,
|
|
@@ -35,18 +78,25 @@ export async function checkFieldAccess(
|
|
|
35
78
|
inputData?: Record<string, unknown>
|
|
36
79
|
},
|
|
37
80
|
): Promise<boolean> {
|
|
38
|
-
// Skip access check in sudo mode
|
|
39
81
|
if (args.context._isSudo) {
|
|
40
82
|
return true
|
|
41
83
|
}
|
|
42
84
|
|
|
43
85
|
if (!fieldAccess) {
|
|
44
|
-
return true
|
|
86
|
+
return true
|
|
45
87
|
}
|
|
46
88
|
|
|
47
|
-
|
|
89
|
+
// `FieldAccess['read']` is narrower than `FieldAccess['create'/'update']` (it
|
|
90
|
+
// only accepts the single `operation: 'read'` call shape, see `types.ts`),
|
|
91
|
+
// so indexing by a not-yet-narrowed `operation` union produces a callable
|
|
92
|
+
// whose effective parameter type collapses to an intersection TypeScript
|
|
93
|
+
// can't satisfy generically here. Widen back to the general
|
|
94
|
+
// `FieldAccessControl` — the same shape this function has always built and
|
|
95
|
+
// passed below — since by construction the args object always matches
|
|
96
|
+
// whichever operation is actually requested.
|
|
97
|
+
const accessControl = fieldAccess[operation] as FieldAccessControl | undefined
|
|
48
98
|
if (!accessControl) {
|
|
49
|
-
return true
|
|
99
|
+
return true
|
|
50
100
|
}
|
|
51
101
|
|
|
52
102
|
const result = await accessControl({
|
|
@@ -57,51 +107,79 @@ export async function checkFieldAccess(
|
|
|
57
107
|
operation,
|
|
58
108
|
} as Parameters<typeof accessControl>[0])
|
|
59
109
|
|
|
60
|
-
// If result is false, deny access
|
|
61
110
|
if (result === false) {
|
|
62
111
|
return false
|
|
63
112
|
}
|
|
64
113
|
|
|
65
|
-
// If result is true, allow access
|
|
66
114
|
if (result === true) {
|
|
67
115
|
return true
|
|
68
116
|
}
|
|
69
117
|
|
|
70
|
-
//
|
|
71
|
-
|
|
118
|
+
// `FieldAccessControl` is typed to return `boolean` only — field access is a
|
|
119
|
+
// per-field visibility decision, not a row filter (ADR-0001, ADR-0030). A
|
|
120
|
+
// well-typed rule can never reach this line; getting here means a caller
|
|
121
|
+
// bypassed the type (most notably by returning a Prisma filter, the shape
|
|
122
|
+
// operation-level `AccessControl` accepts but this does not). Fail loudly
|
|
123
|
+
// and closed — this operation applies identically whether or not `item` is
|
|
124
|
+
// available, so `create` (which has no `item` to evaluate a filter against)
|
|
125
|
+
// needs no separate answer: neither operation ever honours a non-boolean.
|
|
126
|
+
throw new InvalidFieldAccessResultError(operation, result)
|
|
72
127
|
}
|
|
73
128
|
|
|
74
129
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
130
|
+
* Whether a field's `read` access allows it to be NAMED in a `where`/`orderBy`
|
|
131
|
+
* predicate, evaluated BEFORE the query runs (#915).
|
|
132
|
+
*
|
|
133
|
+
* Field-level `read` access has always been a post-query check (Field
|
|
134
|
+
* Visibility, `field-visibility.ts`): it strips a denied key from a row that
|
|
135
|
+
* has already been fetched. That leaves the predicate itself unconstrained —
|
|
136
|
+
* a caller can still filter or sort by a field whose value they could never
|
|
137
|
+
* read, and recover it (or its relative order) by probing. This function is
|
|
138
|
+
* the pre-query counterpart, called from `query-validation.ts`'s `where`/
|
|
139
|
+
* `orderBy` walk for every key that resolves to a declared field.
|
|
140
|
+
*
|
|
141
|
+
* It delegates to the same evaluator Field Visibility uses (`checkFieldAccess`),
|
|
142
|
+
* per this module's canonical-evaluator rule above. The one difference is the
|
|
143
|
+
* `item` it hands the rule: there is no fetched row
|
|
144
|
+
* yet, so a rule that depends on one (the shape `FieldAccess['read']`
|
|
145
|
+
* documents as the norm, e.g. `item?.ownerId === session?.userId`) cannot be
|
|
146
|
+
* answered here. Rather than skip the check for such a rule — which would
|
|
147
|
+
* reopen exactly the hole this closes for the fields most likely to be
|
|
148
|
+
* sensitive — it is handed a poisoned `item` that throws on any property
|
|
149
|
+
* read, and that throw is caught and resolved to `false`: a row-dependent
|
|
150
|
+
* `read` rule always denies at predicate time, documented and deliberate
|
|
151
|
+
* (see docs/adr/0031). A rule that never touches `item` (checking only
|
|
152
|
+
* `session`, the common case for a field meant to be filterable/sortable at
|
|
153
|
+
* all) evaluates normally and returns its real answer.
|
|
154
|
+
*
|
|
155
|
+
* A rule that returns a non-boolean is a distinct, louder failure (#913,
|
|
156
|
+
* ADR-0030) — `InvalidFieldAccessResultError` — and is deliberately NOT
|
|
157
|
+
* folded into the `false` here; it propagates so the config bug it signals is
|
|
158
|
+
* never mistaken for an ordinary field-level denial.
|
|
77
159
|
*/
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
160
|
+
export async function isFieldReadableForPredicate(
|
|
161
|
+
fieldAccess: FieldAccess | undefined,
|
|
162
|
+
args: {
|
|
163
|
+
session: Session | null
|
|
164
|
+
context: AccessContext & { _isSudo?: boolean }
|
|
165
|
+
},
|
|
166
|
+
): Promise<boolean> {
|
|
167
|
+
try {
|
|
168
|
+
return await checkFieldAccess(fieldAccess, 'read', {
|
|
169
|
+
session: args.session,
|
|
170
|
+
context: args.context,
|
|
171
|
+
item: createPoisonedItem(),
|
|
172
|
+
})
|
|
173
|
+
} catch (err) {
|
|
174
|
+
// Only the poisoned-item signal means "row-dependent, deny". Anything
|
|
175
|
+
// else — including `InvalidFieldAccessResultError` and a genuine bug in
|
|
176
|
+
// the rule itself — propagates unchanged rather than being silently
|
|
177
|
+
// folded into an ordinary denial (found in review of #925).
|
|
178
|
+
if (err instanceof PredicateTimeItemAccessError) return false
|
|
179
|
+
throw err
|
|
98
180
|
}
|
|
99
|
-
return true
|
|
100
181
|
}
|
|
101
182
|
|
|
102
|
-
/**
|
|
103
|
-
* Filter fields from input data based on write access (create/update)
|
|
104
|
-
*/
|
|
105
183
|
export async function filterWritableFields<T extends Record<string, unknown>>(
|
|
106
184
|
data: T,
|
|
107
185
|
fieldConfigs: Record<
|
|
@@ -122,8 +200,7 @@ export async function filterWritableFields<T extends Record<string, unknown>>(
|
|
|
122
200
|
): Promise<Partial<T>> {
|
|
123
201
|
const filtered: Record<string, unknown> = {}
|
|
124
202
|
|
|
125
|
-
//
|
|
126
|
-
// Foreign keys should not be in the data when using Prisma's relation syntax
|
|
203
|
+
// Foreign keys must not appear in `data` when using Prisma's relation syntax.
|
|
127
204
|
const foreignKeyFields = new Set<string>()
|
|
128
205
|
// Map each raw per-part column name contributed by a multi-column field
|
|
129
206
|
// (e.g. storage image()/file() in Keystone-parity mode) back to its OWNING
|
|
@@ -162,19 +239,18 @@ export async function filterWritableFields<T extends Record<string, unknown>>(
|
|
|
162
239
|
for (const [fieldName, value] of Object.entries(data)) {
|
|
163
240
|
const fieldConfig = fieldConfigs[fieldName]
|
|
164
241
|
|
|
165
|
-
// Skip system fields
|
|
166
242
|
if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) {
|
|
167
243
|
continue
|
|
168
244
|
}
|
|
169
245
|
|
|
170
|
-
//
|
|
171
|
-
//
|
|
246
|
+
// Virtual fields don't store in the database — skipped here, but their
|
|
247
|
+
// resolveInput hooks still run as a separate side-effect step.
|
|
172
248
|
if (fieldConfig && 'virtual' in fieldConfig && fieldConfig.virtual) {
|
|
173
249
|
continue
|
|
174
250
|
}
|
|
175
251
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
252
|
+
// Prevents conflicts with Prisma's relation syntax (e.g.,
|
|
253
|
+
// `author: { connect: { id } }`).
|
|
178
254
|
if (foreignKeyFields.has(fieldName)) {
|
|
179
255
|
continue
|
|
180
256
|
}
|
|
@@ -1,29 +1,20 @@
|
|
|
1
1
|
import type { OpenSaasConfig, FieldConfig } from '../config/types.js'
|
|
2
2
|
import type { HashedPassword } from '../utils/password.js'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Extract the return type of a field's afterOperation hook
|
|
6
|
-
* If the field has an afterOperation hook, infer its return type
|
|
7
|
-
* Otherwise, use the original type
|
|
8
|
-
*/
|
|
9
4
|
export type InferFieldReadType<TField extends FieldConfig, TOriginal> = TField extends {
|
|
10
|
-
//
|
|
11
|
-
//
|
|
5
|
+
// Uses `any` here so TypeScript's conditional type inference can pick up the
|
|
6
|
+
// exact return type `R` from a hook of any signature.
|
|
12
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
8
|
hooks?: { afterOperation?: (...args: any[]) => infer R }
|
|
14
9
|
}
|
|
15
10
|
? R extends never
|
|
16
|
-
? TOriginal
|
|
17
|
-
: R
|
|
18
|
-
: TOriginal
|
|
11
|
+
? TOriginal
|
|
12
|
+
: R
|
|
13
|
+
: TOriginal
|
|
19
14
|
|
|
20
|
-
/**
|
|
21
|
-
* Transform a Prisma model's field types based on OpenSaas field configs
|
|
22
|
-
* This applies afterOperation hook transformations to field types
|
|
23
|
-
*/
|
|
24
15
|
export type TransformModelFields<
|
|
25
|
-
//
|
|
26
|
-
//
|
|
16
|
+
// Uses `any` so this constraint accepts Prisma's generated model types, which are
|
|
17
|
+
// indexed by string keys.
|
|
27
18
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
19
|
TModel extends Record<string, any>,
|
|
29
20
|
TFields extends Record<string, FieldConfig>,
|
|
@@ -33,34 +24,22 @@ export type TransformModelFields<
|
|
|
33
24
|
: TModel[K]
|
|
34
25
|
}
|
|
35
26
|
|
|
36
|
-
/**
|
|
37
|
-
* Get the field configs for a specific list from the OpenSaas config
|
|
38
|
-
*/
|
|
39
27
|
export type GetListFields<
|
|
40
28
|
TConfig extends OpenSaasConfig,
|
|
41
29
|
TListKey extends keyof TConfig['lists'],
|
|
42
30
|
> = TConfig['lists'][TListKey]['fields']
|
|
43
31
|
|
|
44
|
-
/**
|
|
45
|
-
* Transform a Prisma model result based on OpenSaas config
|
|
46
|
-
* Applies field hooks transformations
|
|
47
|
-
*/
|
|
48
32
|
export type TransformResult<
|
|
49
33
|
TConfig extends OpenSaasConfig,
|
|
50
34
|
TListKey extends keyof TConfig['lists'],
|
|
51
35
|
TResult,
|
|
52
36
|
> =
|
|
53
|
-
//
|
|
54
|
-
// This pattern is standard in TypeScript for conditional types on object shapes
|
|
37
|
+
// Uses `any` to check whether TResult is an object type that can be transformed.
|
|
55
38
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
39
|
TResult extends Record<string, any>
|
|
57
40
|
? TransformModelFields<TResult, GetListFields<TConfig, TListKey>>
|
|
58
41
|
: TResult
|
|
59
42
|
|
|
60
|
-
/**
|
|
61
|
-
* Transform a Prisma operation's return type
|
|
62
|
-
* Handles single results, arrays, and null cases
|
|
63
|
-
*/
|
|
64
43
|
export type TransformOperationResult<
|
|
65
44
|
TConfig extends OpenSaasConfig,
|
|
66
45
|
TListKey extends keyof TConfig['lists'],
|
|
@@ -76,20 +55,10 @@ export type TransformOperationResult<
|
|
|
76
55
|
>
|
|
77
56
|
: never
|
|
78
57
|
|
|
79
|
-
/**
|
|
80
|
-
* Known field type mappings for afterOperation hooks
|
|
81
|
-
* These provide concrete type hints for common field transformations
|
|
82
|
-
*/
|
|
83
58
|
export interface FieldTypeTransforms {
|
|
84
59
|
password: HashedPassword
|
|
85
|
-
// Future field types can be added here
|
|
86
|
-
// richText: TiptapContent
|
|
87
|
-
// json: JSONValue
|
|
88
60
|
}
|
|
89
61
|
|
|
90
|
-
/**
|
|
91
|
-
* Helper to infer field type based on field config type
|
|
92
|
-
*/
|
|
93
62
|
export type InferFieldTypeTransform<TField extends FieldConfig> = TField extends {
|
|
94
63
|
type: infer TType
|
|
95
64
|
}
|
|
@@ -6,6 +6,7 @@ import { RESOLVE_CHAIN_MAX_LENGTH } from './depth-limits.js'
|
|
|
6
6
|
import { ResolveOutputCycleError } from './errors.js'
|
|
7
7
|
import type { DeclaredOnlyTree } from './declared-dependencies.js'
|
|
8
8
|
import { emptyDeclaredOnlyTree } from './declared-dependencies.js'
|
|
9
|
+
import type { FieldSelectionScope } from '../query/index.js'
|
|
9
10
|
// NOTE: `context/index.ts` imports `filterReadableFields` from this module
|
|
10
11
|
// (via the `access/index.ts` barrel) — this is an intentional cyclic
|
|
11
12
|
// dependency, the same shape and for the same reason as the one documented in
|
|
@@ -23,16 +24,20 @@ import { buildDbDelegate } from '../context/index.js'
|
|
|
23
24
|
* virtual fields. None of this can move into phase 1: virtual fields are
|
|
24
25
|
* computed in JavaScript and field access can depend on the fetched row.
|
|
25
26
|
*
|
|
27
|
+
* A computed field — any field carrying a `resolveOutput` hook, virtual or
|
|
28
|
+
* not — is produced only where the read is going to return it (ADR-0027). A
|
|
29
|
+
* fragment `query`'s own field selection is the only thing that restricts a
|
|
30
|
+
* level this way; a bare or `include`-based read, and any relation reached
|
|
31
|
+
* purely to satisfy a `needs` declaration, still compute every field, as
|
|
32
|
+
* before. A field the read is not going to return does no work at all —
|
|
33
|
+
* neither its read-access evaluation nor its hook. The projection-aware skip
|
|
34
|
+
* below shows this rule at each of the two places it applies.
|
|
35
|
+
*
|
|
26
36
|
* Phase 1 (pre-query row/relation scoping) lives in `access-filter.ts`. See
|
|
27
37
|
* `docs/adr/0001-access-control-is-a-two-phase-read.md` and the access-control
|
|
28
38
|
* glossary in `CONTEXT.md`.
|
|
29
39
|
*/
|
|
30
40
|
|
|
31
|
-
/**
|
|
32
|
-
* Runtime type for resolveOutput hooks
|
|
33
|
-
* Used when we need to call hooks generically without knowing the specific field type
|
|
34
|
-
* Supports both sync and async implementations
|
|
35
|
-
*/
|
|
36
41
|
type ResolveOutputHookRuntime = (args: {
|
|
37
42
|
operation: 'query'
|
|
38
43
|
value: unknown
|
|
@@ -96,8 +101,11 @@ function deriveResolveOutputContext(
|
|
|
96
101
|
* from the result.
|
|
97
102
|
*
|
|
98
103
|
* `accessItem` is the row used to evaluate field access; `hookItem` is the
|
|
99
|
-
* object passed to the hook as `item
|
|
100
|
-
*
|
|
104
|
+
* object passed to the hook as `item`. For a stored field, both are
|
|
105
|
+
* `workingItem` (the row's own stored/fetched columns). For a virtual field,
|
|
106
|
+
* `hookItem` is `computedFieldItem` instead — the same stored columns with
|
|
107
|
+
* every skipped-or-denied key removed — so it never sees another computed
|
|
108
|
+
* field's resolved value (ADR-0027).
|
|
101
109
|
*/
|
|
102
110
|
async function resolveReadableFieldValue(params: {
|
|
103
111
|
fieldConfig: FieldConfig | undefined
|
|
@@ -111,7 +119,6 @@ async function resolveReadableFieldValue(params: {
|
|
|
111
119
|
}): Promise<{ readable: false } | { readable: true; value: unknown }> {
|
|
112
120
|
const { fieldConfig, fieldName, value, accessItem, hookItem, listKey, args, config } = params
|
|
113
121
|
|
|
114
|
-
// Check field access (checkFieldAccess already handles sudo mode)
|
|
115
122
|
const canRead = await checkFieldAccess(fieldConfig?.access, 'read', {
|
|
116
123
|
...args,
|
|
117
124
|
item: accessItem,
|
|
@@ -121,10 +128,9 @@ async function resolveReadableFieldValue(params: {
|
|
|
121
128
|
return { readable: false }
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
// Apply resolveOutput hook if present
|
|
125
131
|
if (fieldConfig?.hooks?.resolveOutput && listKey) {
|
|
126
|
-
//
|
|
127
|
-
//
|
|
132
|
+
// The hook is erased to this runtime shape here; at the actual call it
|
|
133
|
+
// receives the value typed for its own field.
|
|
128
134
|
const hook = fieldConfig.hooks.resolveOutput as unknown as ResolveOutputHookRuntime
|
|
129
135
|
const link = { listKey, fieldKey: fieldName }
|
|
130
136
|
const chain = args.context._resolveOutputChain
|
|
@@ -152,7 +158,6 @@ async function resolveReadableFieldValue(params: {
|
|
|
152
158
|
return { readable: false }
|
|
153
159
|
}
|
|
154
160
|
|
|
155
|
-
// Use Promise.resolve() to handle both sync and async hooks
|
|
156
161
|
const resolved = await Promise.resolve(
|
|
157
162
|
hook({
|
|
158
163
|
value,
|
|
@@ -169,10 +174,6 @@ async function resolveReadableFieldValue(params: {
|
|
|
169
174
|
return { readable: true, value }
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
/**
|
|
173
|
-
* Filter fields from an object based on read access
|
|
174
|
-
* Recursively applies access control to nested relationships
|
|
175
|
-
*/
|
|
176
177
|
export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
177
178
|
item: T,
|
|
178
179
|
fieldConfigs: Record<string, FieldConfig>,
|
|
@@ -189,6 +190,9 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
189
190
|
// returned — after resolveOutput has had a chance to read them — so a
|
|
190
191
|
// declared dependency never widens what the caller receives.
|
|
191
192
|
declaredOnly: DeclaredOnlyTree = emptyDeclaredOnlyTree(),
|
|
193
|
+
// The fragment scope this level was reached under (ADR-0027, see module doc
|
|
194
|
+
// above), and the same tree one level down for each nested relation.
|
|
195
|
+
selection?: FieldSelectionScope,
|
|
192
196
|
): Promise<Partial<T>> {
|
|
193
197
|
const filtered: Record<string, unknown> = {}
|
|
194
198
|
|
|
@@ -214,19 +218,35 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
214
218
|
workingItem[fieldName] = assembled
|
|
215
219
|
}
|
|
216
220
|
|
|
221
|
+
// Keys denied by field-level read access during the pass below — as opposed
|
|
222
|
+
// to a key merely skipped by `selection` or held back only for
|
|
223
|
+
// `declaredOnly` stripping. Tracked separately because a denied key must
|
|
224
|
+
// stay invisible to a computed field's hook (below), while a declared-only
|
|
225
|
+
// key must stay VISIBLE to one — that is the entire point of declaring it
|
|
226
|
+
// (ADR-0025) — even though `selection` above skipped adding it to
|
|
227
|
+
// `filtered` because the caller's fragment never asked for it.
|
|
228
|
+
const accessDeniedKeys = new Set<string>()
|
|
229
|
+
|
|
217
230
|
// Process existing fields from the database result
|
|
218
231
|
for (const [fieldName, value] of Object.entries(workingItem)) {
|
|
219
232
|
const fieldConfig = fieldConfigs[fieldName]
|
|
220
233
|
|
|
221
|
-
// Always include id, createdAt, updatedAt
|
|
222
234
|
if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) {
|
|
223
235
|
filtered[fieldName] = value
|
|
224
236
|
continue
|
|
225
237
|
}
|
|
226
238
|
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
239
|
+
// Projection-aware skip (ADR-0027, see module doc above): a field this
|
|
240
|
+
// level's fragment did not select gets no read-access check, no
|
|
241
|
+
// resolveOutput, and no recursion into a relation — none of that work
|
|
242
|
+
// happens for a value the read isn't going to return.
|
|
243
|
+
if (selection?.fields && !selection.fields.has(fieldName)) {
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Row/relation-level access is already scoped at the DB level via
|
|
248
|
+
// buildAccessScopedInclude; this only handles field-level access (hiding
|
|
249
|
+
// sensitive fields).
|
|
230
250
|
//
|
|
231
251
|
// Deliberately uncapped: the row/relation scoping in access-filter.ts bounds
|
|
232
252
|
// what gets FETCHED (a caller include past its depth cap is now a denial,
|
|
@@ -244,13 +264,13 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
244
264
|
value !== null &&
|
|
245
265
|
value !== undefined
|
|
246
266
|
) {
|
|
247
|
-
// Gate the relationship on read access before recursing.
|
|
248
267
|
const canRead = await checkFieldAccess(fieldConfig?.access, 'read', {
|
|
249
268
|
...args,
|
|
250
269
|
item: workingItem,
|
|
251
270
|
})
|
|
252
271
|
|
|
253
272
|
if (!canRead) {
|
|
273
|
+
accessDeniedKeys.add(fieldName)
|
|
254
274
|
continue
|
|
255
275
|
}
|
|
256
276
|
|
|
@@ -260,10 +280,14 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
260
280
|
// back to an empty tree when this relation isn't declaration-related at
|
|
261
281
|
// all — the common case.
|
|
262
282
|
const nestedDeclaredOnly = declaredOnly.nested[fieldName] ?? emptyDeclaredOnlyTree()
|
|
283
|
+
// This relation's own fragment scope, if the caller's fragment named it
|
|
284
|
+
// with a nested Fragment/RelationSelector. `undefined` (a bare `true`
|
|
285
|
+
// selector, or no `selection` at all) means the nested list computes
|
|
286
|
+
// unrestricted — matching what naming a relation without narrowing it
|
|
287
|
+
// further has always meant.
|
|
288
|
+
const nestedSelection = selection?.nested[fieldName]
|
|
263
289
|
|
|
264
290
|
if (relatedConfig) {
|
|
265
|
-
// For many relationships (arrays) - recursively filter fields in each item
|
|
266
|
-
// The recursive call already handles applying resolveOutput hooks
|
|
267
291
|
if (Array.isArray(value)) {
|
|
268
292
|
filtered[fieldName] = await Promise.all(
|
|
269
293
|
value.map((relatedItem) =>
|
|
@@ -275,13 +299,11 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
275
299
|
depth + 1,
|
|
276
300
|
relatedConfig.listName,
|
|
277
301
|
nestedDeclaredOnly,
|
|
302
|
+
nestedSelection,
|
|
278
303
|
),
|
|
279
304
|
),
|
|
280
305
|
)
|
|
281
|
-
}
|
|
282
|
-
// For single relationships (objects) - recursively filter fields
|
|
283
|
-
// The recursive call already handles applying resolveOutput hooks
|
|
284
|
-
else if (typeof value === 'object') {
|
|
306
|
+
} else if (typeof value === 'object') {
|
|
285
307
|
filtered[fieldName] = await filterReadableFields(
|
|
286
308
|
value as Record<string, unknown>,
|
|
287
309
|
relatedConfig.listConfig.fields,
|
|
@@ -290,17 +312,17 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
290
312
|
depth + 1,
|
|
291
313
|
relatedConfig.listName,
|
|
292
314
|
nestedDeclaredOnly,
|
|
315
|
+
nestedSelection,
|
|
293
316
|
)
|
|
294
317
|
}
|
|
295
318
|
} else {
|
|
296
|
-
// Related config not found, include the value as-is
|
|
297
319
|
filtered[fieldName] = value
|
|
298
320
|
}
|
|
299
321
|
continue
|
|
300
322
|
}
|
|
301
323
|
|
|
302
|
-
// Non-relationship field
|
|
303
|
-
//
|
|
324
|
+
// Non-relationship field, or a relationship field whose value is not
|
|
325
|
+
// includable (null/undefined) — falls through to the shared helper.
|
|
304
326
|
const result = await resolveReadableFieldValue({
|
|
305
327
|
fieldConfig,
|
|
306
328
|
fieldName,
|
|
@@ -314,38 +336,69 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
314
336
|
|
|
315
337
|
if (result.readable) {
|
|
316
338
|
filtered[fieldName] = result.value
|
|
339
|
+
} else {
|
|
340
|
+
accessDeniedKeys.add(fieldName)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// The item a virtual field's hook sees: stored columns and fetched
|
|
345
|
+
// relations (from `workingItem`, never a resolved value — no hook's output
|
|
346
|
+
// is ever written back into `workingItem`). A key is visible here if it
|
|
347
|
+
// either survived into `filtered` (selected and allowed) OR exists only to
|
|
348
|
+
// satisfy a `needs` declaration (`declaredOnly` — that IS the point of
|
|
349
|
+
// declaring it: fetched for a hook, never for the caller, ADR-0025).
|
|
350
|
+
// Everything else — field-level denied, or skipped by `selection` and
|
|
351
|
+
// declared by no one — is deleted. A computed field reaches for exactly its
|
|
352
|
+
// own declared dependencies and nothing another field's hook produced
|
|
353
|
+
// (ADR-0027): reaching for a sibling that was denied or skipped-and-
|
|
354
|
+
// undeclared finds nothing there, the same as reaching for one never
|
|
355
|
+
// declared at all, and reaching for a sibling that DID survive finds its
|
|
356
|
+
// raw stored form, never another hook's resolved value — a virtual field
|
|
357
|
+
// computed earlier in declaration order is exactly as invisible as one
|
|
358
|
+
// computed later.
|
|
359
|
+
const computedFieldItem: Record<string, unknown> = { ...workingItem }
|
|
360
|
+
for (const key of Object.keys(workingItem)) {
|
|
361
|
+
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue
|
|
362
|
+
if (accessDeniedKeys.has(key)) {
|
|
363
|
+
delete computedFieldItem[key]
|
|
364
|
+
continue
|
|
317
365
|
}
|
|
366
|
+
if (key in filtered) continue
|
|
367
|
+
if (declaredOnly.keys.has(key)) continue
|
|
368
|
+
delete computedFieldItem[key]
|
|
318
369
|
}
|
|
319
370
|
|
|
320
|
-
// Process virtual fields - compute values from other fields
|
|
321
|
-
// Virtual fields don't exist in the database result, so we need to compute them separately
|
|
322
371
|
for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
|
|
323
|
-
// Skip if already processed (from database result)
|
|
324
372
|
if (fieldName in filtered) {
|
|
325
373
|
continue
|
|
326
374
|
}
|
|
327
375
|
|
|
328
|
-
// Only process virtual fields
|
|
329
376
|
if (!fieldConfig.virtual) {
|
|
330
377
|
continue
|
|
331
378
|
}
|
|
332
379
|
|
|
333
|
-
//
|
|
334
|
-
//
|
|
380
|
+
// Projection-aware skip (ADR-0027): same rule as the stored-field pass
|
|
381
|
+
// above — a fragment that does not select this virtual field does no
|
|
382
|
+
// work for it at all.
|
|
383
|
+
if (selection?.fields && !selection.fields.has(fieldName)) {
|
|
384
|
+
continue
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// A virtual field with no resolveOutput hook can never produce a value
|
|
388
|
+
// on ANY read — there is nothing to compute, so there is nothing to do,
|
|
389
|
+
// including evaluating its read access (ADR-0027 reconciles the
|
|
390
|
+
// access-only evaluation this branch used to preserve: a field that
|
|
391
|
+
// never has output has no side effect worth preserving access for).
|
|
335
392
|
if (!(fieldConfig.hooks?.resolveOutput && listKey)) {
|
|
336
|
-
// Still evaluate read access to preserve any access-fn side effects.
|
|
337
|
-
await checkFieldAccess(fieldConfig.access, 'read', { ...args, item: workingItem })
|
|
338
393
|
continue
|
|
339
394
|
}
|
|
340
395
|
|
|
341
|
-
// Check read access and compute the value via the shared helper. Virtual
|
|
342
|
-
// fields see the already-filtered item so they can read sibling fields.
|
|
343
396
|
const result = await resolveReadableFieldValue({
|
|
344
397
|
fieldConfig,
|
|
345
398
|
fieldName,
|
|
346
399
|
value: undefined, // Virtual fields don't have a database value
|
|
347
400
|
accessItem: workingItem,
|
|
348
|
-
hookItem:
|
|
401
|
+
hookItem: computedFieldItem,
|
|
349
402
|
listKey,
|
|
350
403
|
args,
|
|
351
404
|
config,
|
|
@@ -357,10 +410,11 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
357
410
|
}
|
|
358
411
|
|
|
359
412
|
// Strip relations that were fetched ONLY to satisfy a `needs` declaration
|
|
360
|
-
// (ADR-0025), now that every resolveOutput hook at this level
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
413
|
+
// (ADR-0025), now that every resolveOutput hook at this level has had the
|
|
414
|
+
// chance to see them (via `computedFieldItem`, never `filtered` itself — a
|
|
415
|
+
// declared dependency is read from stored columns, not from another
|
|
416
|
+
// field's resolved output). A declared dependency is private plumbing, not
|
|
417
|
+
// an implicit `include`: it never widens what the caller receives.
|
|
364
418
|
for (const key of declaredOnly.keys) {
|
|
365
419
|
delete filtered[key]
|
|
366
420
|
}
|
package/src/access/index.ts
CHANGED
|
@@ -20,15 +20,23 @@ export {
|
|
|
20
20
|
getRelatedListConfig,
|
|
21
21
|
} from './engine.js'
|
|
22
22
|
// Canonical field-level access evaluation (shared by read and write paths).
|
|
23
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
checkFieldAccess,
|
|
25
|
+
filterWritableFields,
|
|
26
|
+
isFieldReadableForPredicate,
|
|
27
|
+
} from './field-access.js'
|
|
28
|
+
// Read-path key validation — the `findMany`/`count` counterpart to the write
|
|
29
|
+
// path's #564 undeclared-key reject.
|
|
30
|
+
export { validateQueryKeys } from './query-validation.js'
|
|
31
|
+
// Read-path field-level access on `where`/`orderBy` keys — a field the
|
|
32
|
+
// session cannot read cannot be named in a predicate either (#915).
|
|
33
|
+
export { validateQueryFieldReadAccess } from './query-validation.js'
|
|
24
34
|
// Phase 1 — Access Filter (pre-query row/relation scoping).
|
|
25
35
|
export {
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
buildAccessScopedInclude,
|
|
37
|
+
buildAccessScopedWhere,
|
|
28
38
|
stripVirtualFieldsFromInclude,
|
|
29
|
-
toPrismaInclude,
|
|
30
39
|
} from './access-filter.js'
|
|
31
|
-
export type { AccessIncludeResult } from './access-filter.js'
|
|
32
40
|
// Phase 2 — Field Visibility (post-query field stripping + resolveOutput).
|
|
33
41
|
export { filterReadableFields } from './field-visibility.js'
|
|
34
42
|
// Declared Dependencies — folding `needs` into an include without widening
|
|
@@ -43,3 +51,7 @@ export type { DeclaredOnlyTree } from './declared-dependencies.js'
|
|
|
43
51
|
export { AccessScopeDepthExceededError } from './errors.js'
|
|
44
52
|
// Thrown when a resolveOutput hook's own resolve chain cycles back into itself.
|
|
45
53
|
export { ResolveOutputCycleError } from './errors.js'
|
|
54
|
+
// Thrown when a field-level access control function returns a non-boolean result.
|
|
55
|
+
export { InvalidFieldAccessResultError } from './errors.js'
|
|
56
|
+
// Thrown when a relation filter's related list denies query access outright (#916).
|
|
57
|
+
export { RelationFilterAccessDeniedError } from './errors.js'
|