@opensaas/stack-core 0.32.0 → 0.33.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 +26 -0
- package/dist/access/access-filter.d.ts +76 -20
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +97 -70
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +171 -10
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/depth-limits.d.ts +12 -0
- package/dist/access/depth-limits.d.ts.map +1 -0
- package/dist/access/depth-limits.js +12 -0
- package/dist/access/depth-limits.js.map +1 -0
- package/dist/access/errors.d.ts +19 -0
- package/dist/access/errors.d.ts.map +1 -0
- package/dist/access/errors.js +29 -0
- package/dist/access/errors.js.map +1 -0
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +10 -3
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +3 -1
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +3 -1
- package/dist/access/index.js.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +14 -8
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +1 -5
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/transaction-boundary.d.ts.map +1 -1
- package/dist/context/transaction-boundary.js +43 -6
- package/dist/context/transaction-boundary.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +254 -7
- package/src/access/access-filter.ts +141 -72
- package/src/access/depth-limits.ts +11 -0
- package/src/access/errors.ts +32 -0
- package/src/access/field-visibility.ts +10 -3
- package/src/access/index.ts +4 -0
- package/src/context/index.ts +14 -5
- package/src/context/nested-operations.ts +0 -7
- package/src/context/transaction-boundary.ts +48 -7
- package/src/index.ts +6 -0
- package/tests/access-relationships.test.ts +77 -63
- package/tests/context.test.ts +106 -24
- package/tests/transaction-boundary-hooks.test.ts +246 -1
- package/tsconfig.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @opensaas/stack-core
|
|
2
2
|
|
|
3
|
+
## 0.33.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#838](https://github.com/OpenSaasAU/stack/pull/838) [`0caf680`](https://github.com/OpenSaasAU/stack/commit/0caf68007e41b69f1a5d5f74fb15df2548a559dc) Thanks [@borisno2](https://github.com/borisno2)! - **Behavior change:** reads that previously returned deeply-nested relation data unscoped now throw instead. A caller-supplied `include` nested past the Access Filter's read-include depth cap (`READ_INCLUDE_MAX_DEPTH`, 5) used to fail OPEN — the relation was fetched with no row filter and its fields were never access-checked or `resolveOutput`-processed. It now fails CLOSED: the read throws a new `AccessScopeDepthExceededError` (exported from `@opensaas/stack-core`) naming the list, relation field, and depth reached, instead of returning unscoped data.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { AccessScopeDepthExceededError } from '@opensaas/stack-core'
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await context.db.post.findMany({
|
|
14
|
+
include: { author: { include: {/* … nested past the depth cap */} } },
|
|
15
|
+
})
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (err instanceof AccessScopeDepthExceededError) {
|
|
18
|
+
// err.listKey / err.fieldKey / err.depth — restructure into shallower reads.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
An ordinary read with no caller `include`, or one within the depth limit, is unaffected — the auto-include still stops silently at the cap, matching prior behavior. A read issued from inside a `resolveOutput`/virtual-field hook now also row-scopes its immediate relations (previously it skipped scoping entirely at that point). Field-level read access and `resolveOutput` hooks are now applied at every nesting depth on the returned rows, with no independent cap of their own. See ADR-0022 and issue [#830](https://github.com/OpenSaasAU/stack/issues/830).
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- [#839](https://github.com/OpenSaasAU/stack/pull/839) [`1a3f51d`](https://github.com/OpenSaasAU/stack/commit/1a3f51d5837d6e5244ccf04c3d14c41c264701c3) Thanks [@borisno2](https://github.com/borisno2)! - Fix `beforeTransaction`/`afterTransaction` hooks not firing for lists reachable only past the involved-list enumeration's old fixed depth cap. These hooks now fire for every list a write touches regardless of nesting depth, so compensation logic that previously never ran will start running — that was a bug, not a contract.
|
|
28
|
+
|
|
3
29
|
## 0.32.0
|
|
4
30
|
|
|
5
31
|
## 0.31.1
|
|
@@ -15,17 +15,69 @@ import type { OpenSaasConfig, FieldConfig } from '../config/types.js';
|
|
|
15
15
|
* `docs/adr/0001-access-control-is-a-two-phase-read.md` and the access-control
|
|
16
16
|
* glossary in `CONTEXT.md`.
|
|
17
17
|
*/
|
|
18
|
+
/** A single relation entry in a Prisma `include` object (see below). */
|
|
19
|
+
type IncludeEntry = boolean | {
|
|
20
|
+
where?: PrismaFilter;
|
|
21
|
+
include?: IncludeObject;
|
|
22
|
+
take?: number;
|
|
23
|
+
};
|
|
24
|
+
type IncludeObject = Record<string, IncludeEntry>;
|
|
25
|
+
/**
|
|
26
|
+
* The result of trying to compute an access-controlled include for a list's
|
|
27
|
+
* fields. `buildIncludeWithAccessControl` used to collapse three unrelated
|
|
28
|
+
* outcomes into a single overloaded `undefined`: "inside a resolveOutput
|
|
29
|
+
* context", "hit the depth cap", and "no relationships to scope" all looked
|
|
30
|
+
* identical to callers, which is what let a depth-capped relation pass
|
|
31
|
+
* through unscoped (issue #830). This discriminated result keeps them
|
|
32
|
+
* distinguishable all the way to `mergeIncludeWithAccessControl`, which is the
|
|
33
|
+
* only place that knows whether a caller actually asked for the part that
|
|
34
|
+
* couldn't be scoped.
|
|
35
|
+
*
|
|
36
|
+
* - `scoped`: relationships were found and (to the extent depth allows)
|
|
37
|
+
* access-controlled; `include` is the resulting tree.
|
|
38
|
+
* - `nothing-to-scope`: the list genuinely has no relationships to scope, OR
|
|
39
|
+
* we are inside a resolveOutput/virtual-field context and deliberately did
|
|
40
|
+
* not descend into a relation's own nested relations. Passing the caller's
|
|
41
|
+
* include through unchanged here is correct, not a leak.
|
|
42
|
+
* - `depth-exceeded`: we could not evaluate this level at all because it sits
|
|
43
|
+
* at or past `READ_INCLUDE_MAX_DEPTH`. This is a denial: a caller `include`
|
|
44
|
+
* that reaches here must be rejected, not passed through.
|
|
45
|
+
*/
|
|
46
|
+
export type AccessIncludeResult = {
|
|
47
|
+
kind: 'scoped';
|
|
48
|
+
include: RichIncludeObject;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'nothing-to-scope';
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'depth-exceeded';
|
|
53
|
+
};
|
|
54
|
+
/** A relation entry in the rich, provenance-carrying tree `buildIncludeWithAccessControl` builds internally. */
|
|
55
|
+
type RichIncludeEntry = {
|
|
56
|
+
where?: PrismaFilter;
|
|
57
|
+
nested: AccessIncludeResult;
|
|
58
|
+
};
|
|
59
|
+
type RichIncludeObject = Record<string, RichIncludeEntry>;
|
|
18
60
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
61
|
+
* Collapse an `AccessIncludeResult` to the plain Prisma `include` shape used
|
|
62
|
+
* when there is no caller-supplied include to merge against (the direct
|
|
63
|
+
* auto-include path). `nothing-to-scope` and `depth-exceeded` both become
|
|
64
|
+
* `undefined` here — at this call site nothing was explicitly requested past
|
|
65
|
+
* either boundary, so there is nothing to deny.
|
|
66
|
+
*/
|
|
67
|
+
export declare function toPrismaInclude(result: AccessIncludeResult): IncludeObject | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Build the access-controlled include for a list's fields.
|
|
70
|
+
*
|
|
71
|
+
* This allows us to filter relationships at the database level instead of in
|
|
72
|
+
* memory. Returns an {@link AccessIncludeResult} rather than a plain include
|
|
73
|
+
* object so that `mergeIncludeWithAccessControl` can tell a genuine "nothing
|
|
74
|
+
* to scope" apart from "the engine hit its depth cap" (see that type's doc
|
|
75
|
+
* comment and ADR-0022).
|
|
21
76
|
*/
|
|
22
77
|
export declare function buildIncludeWithAccessControl(fieldConfigs: Record<string, FieldConfig>, args: {
|
|
23
78
|
session: Session | null;
|
|
24
79
|
context: AccessContext;
|
|
25
|
-
}, config: OpenSaasConfig, depth?: number, visitedLists?: readonly string[]): Promise<
|
|
26
|
-
where?: PrismaFilter;
|
|
27
|
-
include?: Record<string, boolean | /*elided*/ any>;
|
|
28
|
-
}> | undefined>;
|
|
80
|
+
}, config: OpenSaasConfig, depth?: number, visitedLists?: readonly string[]): Promise<AccessIncludeResult>;
|
|
29
81
|
/**
|
|
30
82
|
* Merge a caller-supplied `include` with the access-controlled include — phase-1
|
|
31
83
|
* row/relation scoping for explicit caller selections.
|
|
@@ -49,22 +101,25 @@ export declare function buildIncludeWithAccessControl(fieldConfigs: Record<strin
|
|
|
49
101
|
* - If the caller names a key that is NOT a config-declared relationship, it is
|
|
50
102
|
* passed through unchanged (access control does not govern it).
|
|
51
103
|
*
|
|
52
|
-
*
|
|
53
|
-
* `
|
|
54
|
-
*
|
|
55
|
-
*
|
|
104
|
+
* `accessControlledInclude` is the {@link AccessIncludeResult} for THIS level:
|
|
105
|
+
* - `nothing-to-scope` → nothing to merge against (the list has no
|
|
106
|
+
* relationships, or we're inside a resolveOutput context where the caller
|
|
107
|
+
* include is irrelevant to begin with). Pass the caller's include through
|
|
108
|
+
* unchanged — this is a non-denial outcome, not "every relation denied".
|
|
109
|
+
* - `depth-exceeded` → the engine could not compute a scope for THIS level at
|
|
110
|
+
* all because it sits at or past `READ_INCLUDE_MAX_DEPTH`. If the caller
|
|
111
|
+
* named anything here, that is exactly the case that used to pass through
|
|
112
|
+
* unscoped (issue #830): throw `AccessScopeDepthExceededError` instead. An
|
|
113
|
+
* empty caller include at this level (nothing further requested) is not an
|
|
114
|
+
* error — there's simply nothing to do.
|
|
115
|
+
* - `scoped` → the normal per-relation merge below: a declared relationship
|
|
116
|
+
* ABSENT from the access include was denied (drop it); one PRESENT is used
|
|
117
|
+
* as the base, AND-combining `where`s and recursing into nested includes.
|
|
56
118
|
*
|
|
57
|
-
* `
|
|
58
|
-
*
|
|
59
|
-
* that `buildIncludeWithAccessControl` returns when inside a `resolveOutput`/
|
|
60
|
-
* virtual-field context, at `MAX_DEPTH`, or when the list has no relationships.
|
|
61
|
-
* In every one of those cases there is nothing to merge against, so the caller's
|
|
62
|
-
* `include` is passed through unchanged (matching the prior `args.include || …`
|
|
63
|
-
* fallback). This is distinct from an `undefined` ENTRY inside a defined access
|
|
64
|
-
* include, which DOES mean the relation was denied and must be dropped (see the
|
|
65
|
-
* per-relation loop below). Only the whole-object `undefined` is a passthrough.
|
|
119
|
+
* `listKey` and `depth` are carried only to build a useful
|
|
120
|
+
* `AccessScopeDepthExceededError` message; they do not affect merge behaviour.
|
|
66
121
|
*/
|
|
67
|
-
export declare function mergeIncludeWithAccessControl(callerInclude: Record<string, unknown>, accessControlledInclude:
|
|
122
|
+
export declare function mergeIncludeWithAccessControl(callerInclude: Record<string, unknown>, accessControlledInclude: AccessIncludeResult, fieldConfigs: Record<string, FieldConfig>, config: OpenSaasConfig, listKey: string, depth?: number): Record<string, unknown>;
|
|
68
123
|
/**
|
|
69
124
|
* Remove keys that correspond to `virtual` fields from a Prisma `include`
|
|
70
125
|
* object, recursing into nested relationship includes using the related
|
|
@@ -82,4 +137,5 @@ export declare function mergeIncludeWithAccessControl(callerInclude: Record<stri
|
|
|
82
137
|
* no effect on whether the value appears in the result (#628).
|
|
83
138
|
*/
|
|
84
139
|
export declare function stripVirtualFieldsFromInclude(include: Record<string, unknown> | undefined, fieldConfigs: Record<string, FieldConfig>, config: OpenSaasConfig): Record<string, unknown> | undefined;
|
|
140
|
+
export {};
|
|
85
141
|
//# sourceMappingURL=access-filter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-filter.d.ts","sourceRoot":"","sources":["../../src/access/access-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"access-filter.d.ts","sourceRoot":"","sources":["../../src/access/access-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAKrE;;;;;;;;;;;;;;GAcG;AAEH,wEAAwE;AACxE,KAAK,YAAY,GAAG,OAAO,GAAG;IAAE,KAAK,CAAC,EAAE,YAAY,CAAC;IAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAC9F,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAA;AAE9B,gHAAgH;AAChH,KAAK,gBAAgB,GAAG;IAAE,KAAK,CAAC,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAA;AAC7E,KAAK,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;AAqBzD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,mBAAmB,GAAG,aAAa,GAAG,SAAS,CAOtF;AAED;;;;;;;;GAQG;AACH,wBAAsB,6BAA6B,CACjD,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,aAAa,CAAA;CACvB,EACD,MAAM,EAAE,cAAc,EACtB,KAAK,GAAE,MAAU,EAIjB,YAAY,GAAE,SAAS,MAAM,EAAO,GACnC,OAAO,CAAC,mBAAmB,CAAC,CAmE9B;AAiDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,6BAA6B,CAC3C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,uBAAuB,EAAE,mBAAmB,EAC5C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,MAAU,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA2EzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC5C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,MAAM,EAAE,cAAc,GACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAgCrC"}
|
|
@@ -1,39 +1,67 @@
|
|
|
1
1
|
import { checkAccess, getRelatedListConfig } from './engine.js';
|
|
2
|
+
import { READ_INCLUDE_MAX_DEPTH } from './depth-limits.js';
|
|
3
|
+
import { AccessScopeDepthExceededError } from './errors.js';
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* so denied rows and relations never leave the database.
|
|
9
|
-
*
|
|
10
|
-
* Phase 2 (post-query field stripping + `resolveOutput` + virtual computation)
|
|
11
|
-
* lives in `field-visibility.ts`. The two phases cannot be merged: virtual
|
|
12
|
-
* fields are computed in JavaScript and post-query field access can depend on
|
|
13
|
-
* the fetched row, neither of which is expressible in SQL. See
|
|
14
|
-
* `docs/adr/0001-access-control-is-a-two-phase-read.md` and the access-control
|
|
15
|
-
* glossary in `CONTEXT.md`.
|
|
5
|
+
* Collapse a rich, provenance-carrying include entry down to the plain
|
|
6
|
+
* Prisma-shaped form. A `nested` result that is `depth-exceeded` or
|
|
7
|
+
* `nothing-to-scope` contributes no `include` key — this is the AUTO-include
|
|
8
|
+
* silently stopping, which is correct when no caller asked for anything past
|
|
9
|
+
* this point (see `AccessIncludeResult` doc comment).
|
|
16
10
|
*/
|
|
11
|
+
function toPrismaEntry(entry) {
|
|
12
|
+
const result = {};
|
|
13
|
+
if (entry.where)
|
|
14
|
+
result.where = entry.where;
|
|
15
|
+
if (entry.nested.kind === 'scoped') {
|
|
16
|
+
const nestedInclude = toPrismaInclude(entry.nested);
|
|
17
|
+
if (nestedInclude && Object.keys(nestedInclude).length > 0) {
|
|
18
|
+
result.include = nestedInclude;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return Object.keys(result).length > 0 ? result : true;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Collapse an `AccessIncludeResult` to the plain Prisma `include` shape used
|
|
25
|
+
* when there is no caller-supplied include to merge against (the direct
|
|
26
|
+
* auto-include path). `nothing-to-scope` and `depth-exceeded` both become
|
|
27
|
+
* `undefined` here — at this call site nothing was explicitly requested past
|
|
28
|
+
* either boundary, so there is nothing to deny.
|
|
29
|
+
*/
|
|
30
|
+
export function toPrismaInclude(result) {
|
|
31
|
+
if (result.kind !== 'scoped')
|
|
32
|
+
return undefined;
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const [key, entry] of Object.entries(result.include)) {
|
|
35
|
+
out[key] = toPrismaEntry(entry);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
17
39
|
/**
|
|
18
|
-
* Build
|
|
19
|
-
*
|
|
40
|
+
* Build the access-controlled include for a list's fields.
|
|
41
|
+
*
|
|
42
|
+
* This allows us to filter relationships at the database level instead of in
|
|
43
|
+
* memory. Returns an {@link AccessIncludeResult} rather than a plain include
|
|
44
|
+
* object so that `mergeIncludeWithAccessControl` can tell a genuine "nothing
|
|
45
|
+
* to scope" apart from "the engine hit its depth cap" (see that type's doc
|
|
46
|
+
* comment and ADR-0022).
|
|
20
47
|
*/
|
|
21
48
|
export async function buildIncludeWithAccessControl(fieldConfigs, args, config, depth = 0,
|
|
22
49
|
// List names already on the path from the root to here. Used to detect
|
|
23
50
|
// relationship cycles (A → B → … → A) and stop the auto-include from
|
|
24
51
|
// re-descending them. Seed it with the root list name at the call site.
|
|
25
52
|
visitedLists = []) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
// Skip auto-including relationships when inside a resolveOutput hook
|
|
31
|
-
// This prevents infinite loops when hooks make DB queries that include
|
|
32
|
-
// relationships back to the same entity (e.g., User virtual field queries Posts
|
|
33
|
-
// which includes author back to User, triggering the virtual field again)
|
|
34
|
-
if (args.context._resolveOutputCounter.depth > 0) {
|
|
35
|
-
return undefined;
|
|
53
|
+
if (depth >= READ_INCLUDE_MAX_DEPTH) {
|
|
54
|
+
return { kind: 'depth-exceeded' };
|
|
36
55
|
}
|
|
56
|
+
// Inside a resolveOutput/virtual-field context we still scope each immediate
|
|
57
|
+
// relation (its own access `where`), but do not auto-expand INTO its nested
|
|
58
|
+
// relations — no recursive call is made, so a self-referential relation
|
|
59
|
+
// terminates after one level regardless of cycles. This prevents the
|
|
60
|
+
// infinite loops the original passthrough guarded against (hooks making DB
|
|
61
|
+
// queries that include relationships back to the same entity), while still
|
|
62
|
+
// row-scoping the relation itself rather than skipping scoping altogether
|
|
63
|
+
// (issue #830's second trigger).
|
|
64
|
+
const insideResolveOutput = args.context._resolveOutputCounter.depth > 0;
|
|
37
65
|
const include = {};
|
|
38
66
|
let hasRelationships = false;
|
|
39
67
|
for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
|
|
@@ -51,12 +79,7 @@ visitedLists = []) {
|
|
|
51
79
|
if (accessResult === false) {
|
|
52
80
|
continue;
|
|
53
81
|
}
|
|
54
|
-
|
|
55
|
-
const includeEntry = {};
|
|
56
|
-
// If access returns a filter, add it to the where clause
|
|
57
|
-
if (typeof accessResult === 'object') {
|
|
58
|
-
includeEntry.where = accessResult;
|
|
59
|
-
}
|
|
82
|
+
const where = typeof accessResult === 'object' ? accessResult : undefined;
|
|
60
83
|
// Cycle guard: if the related list already appears on the path from the
|
|
61
84
|
// root, DO NOT auto-include its relationships again. On a cyclic
|
|
62
85
|
// readable-relationship graph (A → B → … → A) the depth-first walk would
|
|
@@ -68,20 +91,16 @@ visitedLists = []) {
|
|
|
68
91
|
// genuine single-level fetch, not a re-expansion of the full auto-include.
|
|
69
92
|
// The relation itself is still included (as a FLAT fetch of its own
|
|
70
93
|
// columns); only its onward relationships are pruned at the back-edge.
|
|
94
|
+
let nested = { kind: 'nothing-to-scope' };
|
|
71
95
|
const relatedListName = relatedConfig.listName;
|
|
72
|
-
if (!visitedLists.includes(relatedListName)) {
|
|
73
|
-
|
|
74
|
-
const nestedInclude = await buildIncludeWithAccessControl(relatedConfig.listConfig.fields, args, config, depth + 1, [...visitedLists, relatedListName]);
|
|
75
|
-
if (nestedInclude && Object.keys(nestedInclude).length > 0) {
|
|
76
|
-
includeEntry.include = nestedInclude;
|
|
77
|
-
}
|
|
96
|
+
if (!insideResolveOutput && !visitedLists.includes(relatedListName)) {
|
|
97
|
+
nested = await buildIncludeWithAccessControl(relatedConfig.listConfig.fields, args, config, depth + 1, [...visitedLists, relatedListName]);
|
|
78
98
|
}
|
|
79
|
-
|
|
80
|
-
include[fieldName] = Object.keys(includeEntry).length > 0 ? includeEntry : true;
|
|
99
|
+
include[fieldName] = { where, nested };
|
|
81
100
|
}
|
|
82
101
|
}
|
|
83
102
|
}
|
|
84
|
-
return hasRelationships ? include :
|
|
103
|
+
return hasRelationships ? { kind: 'scoped', include } : { kind: 'nothing-to-scope' };
|
|
85
104
|
}
|
|
86
105
|
/**
|
|
87
106
|
* Narrow an unknown include value to the structured object form (vs bare `true`
|
|
@@ -148,32 +167,37 @@ function andWhere(accessWhere, callerWhere) {
|
|
|
148
167
|
* - If the caller names a key that is NOT a config-declared relationship, it is
|
|
149
168
|
* passed through unchanged (access control does not govern it).
|
|
150
169
|
*
|
|
151
|
-
*
|
|
152
|
-
* `
|
|
153
|
-
*
|
|
154
|
-
*
|
|
170
|
+
* `accessControlledInclude` is the {@link AccessIncludeResult} for THIS level:
|
|
171
|
+
* - `nothing-to-scope` → nothing to merge against (the list has no
|
|
172
|
+
* relationships, or we're inside a resolveOutput context where the caller
|
|
173
|
+
* include is irrelevant to begin with). Pass the caller's include through
|
|
174
|
+
* unchanged — this is a non-denial outcome, not "every relation denied".
|
|
175
|
+
* - `depth-exceeded` → the engine could not compute a scope for THIS level at
|
|
176
|
+
* all because it sits at or past `READ_INCLUDE_MAX_DEPTH`. If the caller
|
|
177
|
+
* named anything here, that is exactly the case that used to pass through
|
|
178
|
+
* unscoped (issue #830): throw `AccessScopeDepthExceededError` instead. An
|
|
179
|
+
* empty caller include at this level (nothing further requested) is not an
|
|
180
|
+
* error — there's simply nothing to do.
|
|
181
|
+
* - `scoped` → the normal per-relation merge below: a declared relationship
|
|
182
|
+
* ABSENT from the access include was denied (drop it); one PRESENT is used
|
|
183
|
+
* as the base, AND-combining `where`s and recursing into nested includes.
|
|
155
184
|
*
|
|
156
|
-
* `
|
|
157
|
-
*
|
|
158
|
-
* that `buildIncludeWithAccessControl` returns when inside a `resolveOutput`/
|
|
159
|
-
* virtual-field context, at `MAX_DEPTH`, or when the list has no relationships.
|
|
160
|
-
* In every one of those cases there is nothing to merge against, so the caller's
|
|
161
|
-
* `include` is passed through unchanged (matching the prior `args.include || …`
|
|
162
|
-
* fallback). This is distinct from an `undefined` ENTRY inside a defined access
|
|
163
|
-
* include, which DOES mean the relation was denied and must be dropped (see the
|
|
164
|
-
* per-relation loop below). Only the whole-object `undefined` is a passthrough.
|
|
185
|
+
* `listKey` and `depth` are carried only to build a useful
|
|
186
|
+
* `AccessScopeDepthExceededError` message; they do not affect merge behaviour.
|
|
165
187
|
*/
|
|
166
|
-
export function mergeIncludeWithAccessControl(callerInclude, accessControlledInclude, fieldConfigs, config) {
|
|
167
|
-
|
|
168
|
-
// MAX_DEPTH, or a list with no relationships) → nothing to scope against, so
|
|
169
|
-
// pass the caller's include through unchanged. Dropping relations here would be
|
|
170
|
-
// fail-closed data loss, not a denial. Denied relations are dropped only when a
|
|
171
|
-
// defined access include OMITS them (handled per-relation below).
|
|
172
|
-
if (accessControlledInclude === undefined) {
|
|
188
|
+
export function mergeIncludeWithAccessControl(callerInclude, accessControlledInclude, fieldConfigs, config, listKey, depth = 0) {
|
|
189
|
+
if (accessControlledInclude.kind === 'nothing-to-scope') {
|
|
173
190
|
return callerInclude;
|
|
174
191
|
}
|
|
192
|
+
if (accessControlledInclude.kind === 'depth-exceeded') {
|
|
193
|
+
const [firstRelationName] = Object.keys(callerInclude);
|
|
194
|
+
if (firstRelationName !== undefined) {
|
|
195
|
+
throw new AccessScopeDepthExceededError(listKey, firstRelationName, depth);
|
|
196
|
+
}
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
175
199
|
const merged = {};
|
|
176
|
-
const accessInclude = accessControlledInclude;
|
|
200
|
+
const accessInclude = accessControlledInclude.include;
|
|
177
201
|
for (const [relationName, callerValue] of Object.entries(callerInclude)) {
|
|
178
202
|
const fieldConfig = fieldConfigs[relationName];
|
|
179
203
|
const isDeclaredRelationship = fieldConfig?.type === 'relationship' && 'ref' in fieldConfig && !!fieldConfig.ref;
|
|
@@ -182,27 +206,30 @@ export function mergeIncludeWithAccessControl(callerInclude, accessControlledInc
|
|
|
182
206
|
merged[relationName] = callerValue;
|
|
183
207
|
continue;
|
|
184
208
|
}
|
|
185
|
-
const
|
|
209
|
+
const accessEntry = accessInclude[relationName];
|
|
186
210
|
// Declared relationship absent from the access include → query access denied → drop it.
|
|
187
|
-
if (
|
|
211
|
+
if (accessEntry === undefined) {
|
|
188
212
|
continue;
|
|
189
213
|
}
|
|
190
|
-
const accessEntry = asEntryObject(accessValue);
|
|
191
214
|
const callerEntry = asEntryObject(callerValue);
|
|
192
215
|
// Resolve the related list's field configs so nested includes merge recursively.
|
|
193
216
|
const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
|
|
194
217
|
const relatedFields = relatedConfig?.listConfig.fields;
|
|
195
|
-
const mergedWhere = andWhere(accessEntry
|
|
218
|
+
const mergedWhere = andWhere(accessEntry.where, callerEntry?.where);
|
|
196
219
|
let mergedNested;
|
|
197
|
-
if (callerEntry?.include && relatedFields) {
|
|
198
|
-
// Recurse: scope the caller's nested selection against the nested access
|
|
199
|
-
|
|
220
|
+
if (callerEntry?.include && relatedFields && relatedConfig) {
|
|
221
|
+
// Recurse: scope the caller's nested selection against the nested access
|
|
222
|
+
// result. If that result is 'depth-exceeded', the recursive call itself
|
|
223
|
+
// throws — the caller named something the engine cannot scope.
|
|
224
|
+
mergedNested = mergeIncludeWithAccessControl(callerEntry.include, accessEntry.nested, relatedFields, config, relatedConfig.listName, depth + 1);
|
|
200
225
|
}
|
|
201
|
-
else if (accessEntry
|
|
226
|
+
else if (accessEntry.nested.kind === 'scoped') {
|
|
202
227
|
// Caller selected the relation bare (no nested include); keep the
|
|
203
228
|
// access-controlled nested include so deeper relations stay filtered.
|
|
204
|
-
mergedNested = accessEntry.
|
|
229
|
+
mergedNested = toPrismaInclude(accessEntry.nested);
|
|
205
230
|
}
|
|
231
|
+
// If the caller's entry has no nested include, a 'depth-exceeded' nested
|
|
232
|
+
// result is silent here too — nothing was asked for past this point.
|
|
206
233
|
const entry = {};
|
|
207
234
|
if (mergedWhere)
|
|
208
235
|
entry.where = mergedWhere;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-filter.js","sourceRoot":"","sources":["../../src/access/access-filter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"access-filter.js","sourceRoot":"","sources":["../../src/access/access-filter.ts"],"names":[],"mappings":"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"}
|