@opensaas/stack-core 0.37.0 → 0.38.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 +68 -0
- package/CLAUDE.md +21 -3
- package/dist/access/access-filter.d.ts +30 -118
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +70 -206
- 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 +67 -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/errors.d.ts +12 -8
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +16 -12
- package/dist/access/errors.js.map +1 -1
- 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 +91 -17
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +1 -2
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +1 -1
- package/dist/access/index.js.map +1 -1
- package/dist/access/relationship-count.d.ts +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +38 -27
- package/dist/context/index.js.map +1 -1
- package/dist/query/index.d.ts +29 -0
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +27 -0
- 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/package.json +1 -1
- package/src/access/access-filter.test.ts +205 -275
- package/src/access/access-filter.ts +84 -267
- package/src/access/declared-dependencies.test.ts +277 -0
- package/src/access/declared-dependencies.ts +122 -37
- package/src/access/depth-limits.ts +8 -7
- package/src/access/errors.ts +16 -12
- package/src/access/field-visibility.ts +99 -14
- package/src/access/index.ts +1 -7
- package/src/access/relationship-count.ts +1 -1
- package/src/context/index.ts +52 -33
- package/src/query/index.ts +53 -0
- package/src/query/relationship-options.ts +1 -1
- package/tests/access-relationships.test.ts +18 -16
- package/tests/computed-field-selective-evaluation.test.ts +418 -0
- package/tests/context.test.ts +27 -0
- package/tests/needs-declared-dependencies.test.ts +7 -4
- package/tests/resolve-chain.test.ts +11 -11
- package/tsconfig.tsbuildinfo +1 -1
package/dist/access/errors.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { READ_INCLUDE_MAX_DEPTH } from './depth-limits.js';
|
|
2
2
|
/**
|
|
3
3
|
* Thrown when a caller-supplied `include` names a relation nested deeper than
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* swallow this.
|
|
4
|
+
* `READ_INCLUDE_MAX_DEPTH`. Deliberately distinct from `ValidationError`: this
|
|
5
|
+
* is not bad user input, it is the engine declining to serve a tree this
|
|
6
|
+
* expensive. Code that catches `ValidationError` to report form errors must
|
|
7
|
+
* not silently swallow this.
|
|
9
8
|
*
|
|
10
|
-
* Only an explicit
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Only an explicit request naming something at or past the cap triggers this
|
|
10
|
+
* — a read that simply doesn't reach this deep never throws. Before
|
|
11
|
+
* ADR-0026 made the read pipeline caller-directed, this cap was the engine's
|
|
12
|
+
* last line of defense against returning a relation it could not prove was
|
|
13
|
+
* row/field scoped (ADR-0022, issue #830); a request naming anything at this
|
|
14
|
+
* depth is now scoped exactly like every other named relation; the cap
|
|
15
|
+
* exists solely to bound how deep a request may cost the engine to serve. See
|
|
16
|
+
* ADR-0026 and `docs/adr/0022-access-control-fails-closed-when-it-cannot-scope.md`.
|
|
13
17
|
*/
|
|
14
18
|
export class AccessScopeDepthExceededError extends Error {
|
|
15
19
|
listKey;
|
|
16
20
|
fieldKey;
|
|
17
21
|
depth;
|
|
18
22
|
constructor(listKey, fieldKey, depth) {
|
|
19
|
-
super(`Cannot
|
|
20
|
-
`
|
|
21
|
-
`
|
|
22
|
-
`
|
|
23
|
+
super(`Cannot include "${listKey}.${fieldKey}" at include depth ${depth}: this exceeds the read ` +
|
|
24
|
+
`pipeline's maximum include depth (${READ_INCLUDE_MAX_DEPTH}). This is a cost limit, not an ` +
|
|
25
|
+
`inability to scope — the engine declines to serve a tree this deep rather than returning ` +
|
|
26
|
+
`it. Restructure the query to fetch this relation separately.`);
|
|
23
27
|
this.name = 'AccessScopeDepthExceededError';
|
|
24
28
|
this.listKey = listKey;
|
|
25
29
|
this.fieldKey = fieldKey;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/access/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAE1D
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/access/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAE1D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,6BAA8B,SAAQ,KAAK;IAC/C,OAAO,CAAQ;IACf,QAAQ,CAAQ;IAChB,KAAK,CAAQ;IAEpB,YAAY,OAAe,EAAE,QAAgB,EAAE,KAAa;QAC1D,KAAK,CACH,mBAAmB,OAAO,IAAI,QAAQ,sBAAsB,KAAK,0BAA0B;YACzF,qCAAqC,sBAAsB,kCAAkC;YAC7F,2FAA2F;YAC3F,8DAA8D,CACjE,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAA;QAC3C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IACzC,KAAK,CAAkD;IAE9D,YAAY,KAAuD;QACjE,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAChF,KAAK,CACH,iCAAiC,IAAI,+CAA+C;YAClF,wFAAwF;YACxF,0FAA0F;YAC1F,iCAAiC,CACpC,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAA;QACrC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Session, AccessContext } from './types.js';
|
|
2
2
|
import type { OpenSaasConfig, FieldConfig } from '../config/types.js';
|
|
3
3
|
import type { DeclaredOnlyTree } from './declared-dependencies.js';
|
|
4
|
+
import type { FieldSelectionScope } from '../query/index.js';
|
|
4
5
|
/**
|
|
5
6
|
* Filter fields from an object based on read access
|
|
6
7
|
* Recursively applies access control to nested relationships
|
|
@@ -10,5 +11,5 @@ export declare function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
10
11
|
context: AccessContext & {
|
|
11
12
|
_isSudo?: boolean;
|
|
12
13
|
};
|
|
13
|
-
}, config?: OpenSaasConfig, depth?: number, listKey?: string, declaredOnly?: DeclaredOnlyTree): Promise<Partial<T>>;
|
|
14
|
+
}, config?: OpenSaasConfig, depth?: number, listKey?: string, declaredOnly?: DeclaredOnlyTree, selection?: FieldSelectionScope): Promise<Partial<T>>;
|
|
14
15
|
//# 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;AAKrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,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;AAElE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AA+K5D;;;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,EAOxD,SAAS,CAAC,EAAE,mBAAmB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAmPrB"}
|
|
@@ -54,8 +54,11 @@ function deriveResolveOutputContext(context, link, config) {
|
|
|
54
54
|
* from the result.
|
|
55
55
|
*
|
|
56
56
|
* `accessItem` is the row used to evaluate field access; `hookItem` is the
|
|
57
|
-
* object passed to the hook as `item
|
|
58
|
-
*
|
|
57
|
+
* object passed to the hook as `item`. For a stored field, both are
|
|
58
|
+
* `workingItem` (the row's own stored/fetched columns). For a virtual field,
|
|
59
|
+
* `hookItem` is `computedFieldItem` instead — the same stored columns with
|
|
60
|
+
* every skipped-or-denied key removed — so it never sees another computed
|
|
61
|
+
* field's resolved value (ADR-0027).
|
|
59
62
|
*/
|
|
60
63
|
async function resolveReadableFieldValue(params) {
|
|
61
64
|
const { fieldConfig, fieldName, value, accessItem, hookItem, listKey, args, config } = params;
|
|
@@ -114,7 +117,14 @@ export async function filterReadableFields(item, fieldConfigs, args, config, dep
|
|
|
114
117
|
// caller-named branch. Stripped from `filtered` right before it is
|
|
115
118
|
// returned — after resolveOutput has had a chance to read them — so a
|
|
116
119
|
// declared dependency never widens what the caller receives.
|
|
117
|
-
declaredOnly = emptyDeclaredOnlyTree()
|
|
120
|
+
declaredOnly = emptyDeclaredOnlyTree(),
|
|
121
|
+
// The fragment scope this level was reached under (ADR-0027), and the same
|
|
122
|
+
// tree one level down for each nested relation. `undefined` — the default,
|
|
123
|
+
// and what a bare/`include`-based read passes at every level — means
|
|
124
|
+
// unrestricted: every field on the list is computed, unchanged from
|
|
125
|
+
// before ADR-0027. Only a `query` fragment's own field selection ever
|
|
126
|
+
// restricts a level.
|
|
127
|
+
selection) {
|
|
118
128
|
const filtered = {};
|
|
119
129
|
// Multi-column fields (e.g. storage image()/file() in Keystone-parity mode)
|
|
120
130
|
// back several physical columns rather than one. Before the per-field pass,
|
|
@@ -139,6 +149,14 @@ declaredOnly = emptyDeclaredOnlyTree()) {
|
|
|
139
149
|
}
|
|
140
150
|
workingItem[fieldName] = assembled;
|
|
141
151
|
}
|
|
152
|
+
// Keys denied by field-level read access during the pass below — as opposed
|
|
153
|
+
// to a key merely skipped by `selection` or held back only for
|
|
154
|
+
// `declaredOnly` stripping. Tracked separately because a denied key must
|
|
155
|
+
// stay invisible to a computed field's hook (below), while a declared-only
|
|
156
|
+
// key must stay VISIBLE to one — that is the entire point of declaring it
|
|
157
|
+
// (ADR-0025) — even though `selection` above skipped adding it to
|
|
158
|
+
// `filtered` because the caller's fragment never asked for it.
|
|
159
|
+
const accessDeniedKeys = new Set();
|
|
142
160
|
// Process existing fields from the database result
|
|
143
161
|
for (const [fieldName, value] of Object.entries(workingItem)) {
|
|
144
162
|
const fieldConfig = fieldConfigs[fieldName];
|
|
@@ -147,8 +165,18 @@ declaredOnly = emptyDeclaredOnlyTree()) {
|
|
|
147
165
|
filtered[fieldName] = value;
|
|
148
166
|
continue;
|
|
149
167
|
}
|
|
168
|
+
// Projection-aware skip (ADR-0027): a fragment read that does not select
|
|
169
|
+
// this field does no work for it at all — no field-level read-access
|
|
170
|
+
// check, no resolveOutput, no recursion into a relation — because the
|
|
171
|
+
// read is never going to return it. `selection` is only ever restricted
|
|
172
|
+
// by a fragment's own field selection; a bare/`include`-based read, and a
|
|
173
|
+
// relation reached only to satisfy another field's `needs`, pass no
|
|
174
|
+
// selection at all and compute every field here, unchanged.
|
|
175
|
+
if (selection?.fields && !selection.fields.has(fieldName)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
150
178
|
// Handle relationship fields - recursively filter fields within related items
|
|
151
|
-
// Note: Access control filtering is now done at database level via
|
|
179
|
+
// Note: Access control filtering is now done at database level via buildAccessScopedInclude
|
|
152
180
|
// This only handles field-level access (hiding sensitive fields)
|
|
153
181
|
//
|
|
154
182
|
// Deliberately uncapped: the row/relation scoping in access-filter.ts bounds
|
|
@@ -171,6 +199,7 @@ declaredOnly = emptyDeclaredOnlyTree()) {
|
|
|
171
199
|
item: workingItem,
|
|
172
200
|
});
|
|
173
201
|
if (!canRead) {
|
|
202
|
+
accessDeniedKeys.add(fieldName);
|
|
174
203
|
continue;
|
|
175
204
|
}
|
|
176
205
|
const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
|
|
@@ -179,16 +208,22 @@ declaredOnly = emptyDeclaredOnlyTree()) {
|
|
|
179
208
|
// back to an empty tree when this relation isn't declaration-related at
|
|
180
209
|
// all — the common case.
|
|
181
210
|
const nestedDeclaredOnly = declaredOnly.nested[fieldName] ?? emptyDeclaredOnlyTree();
|
|
211
|
+
// This relation's own fragment scope, if the caller's fragment named it
|
|
212
|
+
// with a nested Fragment/RelationSelector. `undefined` (a bare `true`
|
|
213
|
+
// selector, or no `selection` at all) means the nested list computes
|
|
214
|
+
// unrestricted — matching what naming a relation without narrowing it
|
|
215
|
+
// further has always meant.
|
|
216
|
+
const nestedSelection = selection?.nested[fieldName];
|
|
182
217
|
if (relatedConfig) {
|
|
183
218
|
// For many relationships (arrays) - recursively filter fields in each item
|
|
184
219
|
// The recursive call already handles applying resolveOutput hooks
|
|
185
220
|
if (Array.isArray(value)) {
|
|
186
|
-
filtered[fieldName] = await Promise.all(value.map((relatedItem) => filterReadableFields(relatedItem, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName, nestedDeclaredOnly)));
|
|
221
|
+
filtered[fieldName] = await Promise.all(value.map((relatedItem) => filterReadableFields(relatedItem, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName, nestedDeclaredOnly, nestedSelection)));
|
|
187
222
|
}
|
|
188
223
|
// For single relationships (objects) - recursively filter fields
|
|
189
224
|
// The recursive call already handles applying resolveOutput hooks
|
|
190
225
|
else if (typeof value === 'object') {
|
|
191
|
-
filtered[fieldName] = await filterReadableFields(value, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName, nestedDeclaredOnly);
|
|
226
|
+
filtered[fieldName] = await filterReadableFields(value, relatedConfig.listConfig.fields, args, config, depth + 1, relatedConfig.listName, nestedDeclaredOnly, nestedSelection);
|
|
192
227
|
}
|
|
193
228
|
}
|
|
194
229
|
else {
|
|
@@ -212,6 +247,38 @@ declaredOnly = emptyDeclaredOnlyTree()) {
|
|
|
212
247
|
if (result.readable) {
|
|
213
248
|
filtered[fieldName] = result.value;
|
|
214
249
|
}
|
|
250
|
+
else {
|
|
251
|
+
accessDeniedKeys.add(fieldName);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// The item a virtual field's hook sees: stored columns and fetched
|
|
255
|
+
// relations (from `workingItem`, never a resolved value — no hook's output
|
|
256
|
+
// is ever written back into `workingItem`). A key is visible here if it
|
|
257
|
+
// either survived into `filtered` (selected and allowed) OR exists only to
|
|
258
|
+
// satisfy a `needs` declaration (`declaredOnly` — that IS the point of
|
|
259
|
+
// declaring it: fetched for a hook, never for the caller, ADR-0025).
|
|
260
|
+
// Everything else — field-level denied, or skipped by `selection` and
|
|
261
|
+
// declared by no one — is deleted. A computed field reaches for exactly its
|
|
262
|
+
// own declared dependencies and nothing another field's hook produced
|
|
263
|
+
// (ADR-0027): reaching for a sibling that was denied or skipped-and-
|
|
264
|
+
// undeclared finds nothing there, the same as reaching for one never
|
|
265
|
+
// declared at all, and reaching for a sibling that DID survive finds its
|
|
266
|
+
// raw stored form, never another hook's resolved value — a virtual field
|
|
267
|
+
// computed earlier in declaration order is exactly as invisible as one
|
|
268
|
+
// computed later.
|
|
269
|
+
const computedFieldItem = { ...workingItem };
|
|
270
|
+
for (const key of Object.keys(workingItem)) {
|
|
271
|
+
if (['id', 'createdAt', 'updatedAt'].includes(key))
|
|
272
|
+
continue;
|
|
273
|
+
if (accessDeniedKeys.has(key)) {
|
|
274
|
+
delete computedFieldItem[key];
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (key in filtered)
|
|
278
|
+
continue;
|
|
279
|
+
if (declaredOnly.keys.has(key))
|
|
280
|
+
continue;
|
|
281
|
+
delete computedFieldItem[key];
|
|
215
282
|
}
|
|
216
283
|
// Process virtual fields - compute values from other fields
|
|
217
284
|
// Virtual fields don't exist in the database result, so we need to compute them separately
|
|
@@ -224,21 +291,27 @@ declaredOnly = emptyDeclaredOnlyTree()) {
|
|
|
224
291
|
if (!fieldConfig.virtual) {
|
|
225
292
|
continue;
|
|
226
293
|
}
|
|
227
|
-
//
|
|
228
|
-
//
|
|
294
|
+
// Projection-aware skip (ADR-0027): same rule as the stored-field pass
|
|
295
|
+
// above — a fragment that does not select this virtual field does no
|
|
296
|
+
// work for it at all.
|
|
297
|
+
if (selection?.fields && !selection.fields.has(fieldName)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
// A virtual field with no resolveOutput hook can never produce a value
|
|
301
|
+
// on ANY read — there is nothing to compute, so there is nothing to do,
|
|
302
|
+
// including evaluating its read access (ADR-0027 reconciles the
|
|
303
|
+
// access-only evaluation this branch used to preserve: a field that
|
|
304
|
+
// never has output has no side effect worth preserving access for).
|
|
229
305
|
if (!(fieldConfig.hooks?.resolveOutput && listKey)) {
|
|
230
|
-
// Still evaluate read access to preserve any access-fn side effects.
|
|
231
|
-
await checkFieldAccess(fieldConfig.access, 'read', { ...args, item: workingItem });
|
|
232
306
|
continue;
|
|
233
307
|
}
|
|
234
|
-
// Check read access and compute the value via the shared helper.
|
|
235
|
-
// fields see the already-filtered item so they can read sibling fields.
|
|
308
|
+
// Check read access and compute the value via the shared helper.
|
|
236
309
|
const result = await resolveReadableFieldValue({
|
|
237
310
|
fieldConfig,
|
|
238
311
|
fieldName,
|
|
239
312
|
value: undefined, // Virtual fields don't have a database value
|
|
240
313
|
accessItem: workingItem,
|
|
241
|
-
hookItem:
|
|
314
|
+
hookItem: computedFieldItem,
|
|
242
315
|
listKey,
|
|
243
316
|
args,
|
|
244
317
|
config,
|
|
@@ -248,10 +321,11 @@ declaredOnly = emptyDeclaredOnlyTree()) {
|
|
|
248
321
|
}
|
|
249
322
|
}
|
|
250
323
|
// Strip relations that were fetched ONLY to satisfy a `needs` declaration
|
|
251
|
-
// (ADR-0025), now that every resolveOutput hook at this level
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
324
|
+
// (ADR-0025), now that every resolveOutput hook at this level has had the
|
|
325
|
+
// chance to see them (via `computedFieldItem`, never `filtered` itself — a
|
|
326
|
+
// declared dependency is read from stored columns, not from another
|
|
327
|
+
// field's resolved output). A declared dependency is private plumbing, not
|
|
328
|
+
// an implicit `include`: it never widens what the caller receives.
|
|
255
329
|
for (const key of declaredOnly.keys) {
|
|
256
330
|
delete filtered[key];
|
|
257
331
|
}
|
|
@@ -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;AAErD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,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
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export type { AccessControl, FieldAccess, Session, AccessContext, PrismaFilter, AccessControlledDB, PrismaClientLike, StorageUtils, AugmentedFindMany, AugmentedFindUnique, FindManyQueryArgs, } from './types.js';
|
|
2
2
|
export { checkAccess, mergeFilters, isBoolean, isPrismaFilter, getRelatedListConfig, } from './engine.js';
|
|
3
3
|
export { checkFieldAccess, filterWritableFields } from './field-access.js';
|
|
4
|
-
export {
|
|
5
|
-
export type { AccessIncludeResult } from './access-filter.js';
|
|
4
|
+
export { buildAccessScopedInclude, stripVirtualFieldsFromInclude } from './access-filter.js';
|
|
6
5
|
export { filterReadableFields } from './field-visibility.js';
|
|
7
6
|
export { foldDeclaredDependencies, getDeclaredRelationNames, emptyDeclaredOnlyTree, } from './declared-dependencies.js';
|
|
8
7
|
export type { DeclaredOnlyTree } from './declared-dependencies.js';
|
|
@@ -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,
|
|
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,EAAE,wBAAwB,EAAE,6BAA6B,EAAE,MAAM,oBAAoB,CAAA;AAE5F,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
|
@@ -3,7 +3,7 @@ export { checkAccess, mergeFilters, isBoolean, isPrismaFilter, getRelatedListCon
|
|
|
3
3
|
// Canonical field-level access evaluation (shared by read and write paths).
|
|
4
4
|
export { checkFieldAccess, filterWritableFields } from './field-access.js';
|
|
5
5
|
// Phase 1 — Access Filter (pre-query row/relation scoping).
|
|
6
|
-
export {
|
|
6
|
+
export { buildAccessScopedInclude, stripVirtualFieldsFromInclude } from './access-filter.js';
|
|
7
7
|
// Phase 2 — Field Visibility (post-query field stripping + resolveOutput).
|
|
8
8
|
export { filterReadableFields } from './field-visibility.js';
|
|
9
9
|
// Declared Dependencies — folding `needs` into an include without widening
|
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,
|
|
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,EAAE,wBAAwB,EAAE,6BAA6B,EAAE,MAAM,oBAAoB,CAAA;AAC5F,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"}
|
|
@@ -10,7 +10,7 @@ import type { OpenSaasConfig, ListConfig, FieldConfig } from '../config/types.js
|
|
|
10
10
|
* per-row query is issued and the count can never include rows the session
|
|
11
11
|
* cannot read. This module is the single place the related list's
|
|
12
12
|
* operation-level `query` access is folded into that `_count`, mirroring how
|
|
13
|
-
* `
|
|
13
|
+
* `buildAccessScopedInclude` folds it into relation includes.
|
|
14
14
|
*
|
|
15
15
|
* It also resolves the count Filter spec's markers: Prisma cannot compare a
|
|
16
16
|
* relation count in a `where`, so a to-many relationship's Filter spec emits a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/context/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,oBAAoB,CAAA;AACpE,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/context/index.ts"],"names":[],"mappings":"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"}
|
package/dist/context/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { checkAccess, mergeFilters, filterReadableFields,
|
|
1
|
+
import { checkAccess, mergeFilters, filterReadableFields, buildAccessScopedInclude, stripVirtualFieldsFromInclude, foldDeclaredDependencies, } from '../access/index.js';
|
|
2
2
|
import { ValidationError, DatabaseError } from '../hooks/index.js';
|
|
3
3
|
import { getDbKey } from '../lib/case-utils.js';
|
|
4
|
-
import { buildInclude, pickFields, isFragment } from '../query/index.js';
|
|
4
|
+
import { buildInclude, pickFields, isFragment, buildFieldSelectionScope } from '../query/index.js';
|
|
5
5
|
import { getRelationshipOptions } from '../query/relationship-options.js';
|
|
6
6
|
import { runWritePipeline, createWriteStrategy, updateWriteStrategy, deleteWriteStrategy, } from './write-pipeline.js';
|
|
7
7
|
/**
|
|
@@ -619,32 +619,39 @@ export function buildDbDelegate(config, prisma, context) {
|
|
|
619
619
|
*
|
|
620
620
|
* A fragment's own `include` and a sudo caller's `include` are folded and
|
|
621
621
|
* used as-is, matching their existing (unmerged) treatment. A non-sudo
|
|
622
|
-
* caller include is folded and then
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
622
|
+
* caller include is folded and then scoped by `buildAccessScopedInclude`
|
|
623
|
+
* (ADR-0026) — caller-directed, so a relation named nowhere in the folded
|
|
624
|
+
* tree never has its list's `query` access evaluated at all. A bare read
|
|
625
|
+
* stays on the exact ADR-0024 path — `include: undefined`, no related
|
|
626
|
+
* `query` access evaluated — unless folding actually added something, which
|
|
627
|
+
* only happens when a field on this list declares `needs`.
|
|
628
|
+
*
|
|
629
|
+
* Also returns the `FieldSelectionScope` a fragment's own field selection
|
|
630
|
+
* produces (ADR-0027), so the caller can pass it to `filterReadableFields`
|
|
631
|
+
* and make computation itself projection-aware, not only the fold above.
|
|
632
|
+
* `undefined` for every non-fragment path: a caller `include` (sudo or not)
|
|
633
|
+
* and a bare read both mean "compute every field," matching what they
|
|
634
|
+
* already fetch.
|
|
627
635
|
*/
|
|
628
636
|
async function resolveReadInclude(callerInclude, fragmentFields, listName,
|
|
629
637
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
630
638
|
listConfig, context, config) {
|
|
631
639
|
if (fragmentFields !== undefined) {
|
|
632
640
|
const fragmentInclude = buildInclude(fragmentFields) ?? undefined;
|
|
633
|
-
|
|
641
|
+
const selection = buildFieldSelectionScope(fragmentFields);
|
|
642
|
+
const folded = foldDeclaredDependencies(fragmentInclude, listConfig.fields, config, listName, [listName], selection);
|
|
643
|
+
return { ...folded, selection };
|
|
634
644
|
}
|
|
635
645
|
if (context._isSudo) {
|
|
636
|
-
|
|
646
|
+
const folded = foldDeclaredDependencies(callerInclude, listConfig.fields, config, listName);
|
|
647
|
+
return { ...folded, selection: undefined };
|
|
637
648
|
}
|
|
638
|
-
const folded = foldDeclaredDependencies(callerInclude, listConfig.fields, config);
|
|
649
|
+
const folded = foldDeclaredDependencies(callerInclude, listConfig.fields, config, listName);
|
|
639
650
|
if (!folded.include) {
|
|
640
|
-
return folded;
|
|
651
|
+
return { ...folded, selection: undefined };
|
|
641
652
|
}
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
// to it (self-referential or longer) stops re-descending.
|
|
645
|
-
[listName]);
|
|
646
|
-
const include = mergeIncludeWithAccessControl(folded.include, accessControlledInclude, listConfig.fields, config, listName);
|
|
647
|
-
return { include, declaredOnly: folded.declaredOnly };
|
|
653
|
+
const include = await buildAccessScopedInclude(folded.include, listConfig.fields, { session: context.session, context }, config, listName);
|
|
654
|
+
return { include, declaredOnly: folded.declaredOnly, selection: undefined };
|
|
648
655
|
}
|
|
649
656
|
/**
|
|
650
657
|
* Create findUnique operation with access control
|
|
@@ -688,12 +695,14 @@ listConfig, prisma, context, config) {
|
|
|
688
695
|
// Resolve `include`, folding any declared dependencies (`needs`,
|
|
689
696
|
// ADR-0025) in alongside whatever the fragment/caller/sudo/bare path
|
|
690
697
|
// already produces — see `resolveReadInclude`'s doc comment.
|
|
691
|
-
let { include, declaredOnly } = await resolveReadInclude(args.include, fragment ? fragment._fields : undefined, listName, listConfig, context, config);
|
|
698
|
+
let { include, declaredOnly, selection } = await resolveReadInclude(args.include, fragment ? fragment._fields : undefined, listName, listConfig, context, config);
|
|
692
699
|
// Virtual fields have no database column. Whichever path produced
|
|
693
700
|
// `include` (fragment, access-controlled merge, or sudo passthrough), a
|
|
694
701
|
// virtual key must never reach Prisma — it would throw "Unknown field"
|
|
695
|
-
// (#628).
|
|
696
|
-
// `
|
|
702
|
+
// (#628). Below, `filterReadableFields` computes a virtual field's value
|
|
703
|
+
// exactly when `selection` says the read is going to return it (ADR-0027)
|
|
704
|
+
// — every one of them for a bare/`include`-based read (`selection` is
|
|
705
|
+
// `undefined`), only the ones a fragment named otherwise.
|
|
697
706
|
include = stripVirtualFieldsFromInclude(include, listConfig.fields, config);
|
|
698
707
|
// Execute query with optimized includes
|
|
699
708
|
// Access Prisma model dynamically - required because model names are generated at runtime
|
|
@@ -711,7 +720,7 @@ listConfig, prisma, context, config) {
|
|
|
711
720
|
const filtered = await filterReadableFields(item, listConfig.fields, {
|
|
712
721
|
session: context.session,
|
|
713
722
|
context: { ...context, _isSudo: context._isSudo },
|
|
714
|
-
}, config, 0, listName, declaredOnly);
|
|
723
|
+
}, config, 0, listName, declaredOnly, selection);
|
|
715
724
|
// When a fragment is provided, pick only the requested fields from the result
|
|
716
725
|
if (fragment) {
|
|
717
726
|
return pickFields(filtered, fragment._fields);
|
|
@@ -755,12 +764,14 @@ listConfig, prisma, context, config) {
|
|
|
755
764
|
// Resolve `include`, folding any declared dependencies (`needs`,
|
|
756
765
|
// ADR-0025) in alongside whatever the fragment/caller/sudo/bare path
|
|
757
766
|
// already produces — see `resolveReadInclude`'s doc comment.
|
|
758
|
-
let { include, declaredOnly } = await resolveReadInclude(args?.include, fragment ? fragment._fields : undefined, listName, listConfig, context, config);
|
|
767
|
+
let { include, declaredOnly, selection } = await resolveReadInclude(args?.include, fragment ? fragment._fields : undefined, listName, listConfig, context, config);
|
|
759
768
|
// Virtual fields have no database column. Whichever path produced
|
|
760
769
|
// `include` (fragment, access-controlled merge, or sudo passthrough), a
|
|
761
770
|
// virtual key must never reach Prisma — it would throw "Unknown field"
|
|
762
|
-
// (#628).
|
|
763
|
-
// `
|
|
771
|
+
// (#628). Below, `filterReadableFields` computes a virtual field's value
|
|
772
|
+
// exactly when `selection` says the read is going to return it (ADR-0027)
|
|
773
|
+
// — every one of them for a bare/`include`-based read (`selection` is
|
|
774
|
+
// `undefined`), only the ones a fragment named otherwise.
|
|
764
775
|
include = stripVirtualFieldsFromInclude(include, listConfig.fields, config);
|
|
765
776
|
// Execute query with optimized includes
|
|
766
777
|
// Access Prisma model dynamically - required because model names are generated at runtime
|
|
@@ -778,7 +789,7 @@ listConfig, prisma, context, config) {
|
|
|
778
789
|
const filtered = await Promise.all(items.map((item) => filterReadableFields(item, listConfig.fields, {
|
|
779
790
|
session: context.session,
|
|
780
791
|
context: { ...context, _isSudo: context._isSudo },
|
|
781
|
-
}, config, 0, listName, declaredOnly)));
|
|
792
|
+
}, config, 0, listName, declaredOnly, selection)));
|
|
782
793
|
// When a fragment is provided, pick only the requested fields from each result
|
|
783
794
|
if (fragment) {
|
|
784
795
|
return filtered.map((item) => pickFields(item, fragment._fields));
|
|
@@ -976,7 +987,7 @@ createFn) {
|
|
|
976
987
|
// Resolve `include`, folding any declared dependencies (`needs`,
|
|
977
988
|
// ADR-0025) in alongside whatever the fragment/caller/sudo/bare path
|
|
978
989
|
// already produces — see `resolveReadInclude`'s doc comment.
|
|
979
|
-
let { include, declaredOnly } = await resolveReadInclude(args?.include, fragment ? fragment._fields : undefined, listName, listConfig, context, config);
|
|
990
|
+
let { include, declaredOnly, selection } = await resolveReadInclude(args?.include, fragment ? fragment._fields : undefined, listName, listConfig, context, config);
|
|
980
991
|
// Virtual fields have no database column and must never reach Prisma (#628).
|
|
981
992
|
include = stripVirtualFieldsFromInclude(include, listConfig.fields, config);
|
|
982
993
|
// Try to find the record
|
|
@@ -990,7 +1001,7 @@ createFn) {
|
|
|
990
1001
|
const filtered = await filterReadableFields(item, listConfig.fields, {
|
|
991
1002
|
session: context.session,
|
|
992
1003
|
context: { ...context, _isSudo: context._isSudo },
|
|
993
|
-
}, config, 0, listName, declaredOnly);
|
|
1004
|
+
}, config, 0, listName, declaredOnly, selection);
|
|
994
1005
|
// When a fragment is provided, pick only the requested fields from the result
|
|
995
1006
|
if (fragment) {
|
|
996
1007
|
return pickFields(filtered, fragment._fields);
|