@opensaas/stack-core 0.32.0 → 0.34.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 +61 -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 +102 -70
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +175 -10
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/depth-limits.d.ts +26 -0
- package/dist/access/depth-limits.d.ts.map +1 -0
- package/dist/access/depth-limits.js +26 -0
- package/dist/access/depth-limits.js.map +1 -0
- package/dist/access/errors.d.ts +43 -0
- package/dist/access/errors.d.ts.map +1 -0
- package/dist/access/errors.js +55 -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 +82 -20
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +4 -1
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +5 -1
- package/dist/access/index.js.map +1 -1
- package/dist/access/multi-column-read-write.test.js +1 -1
- package/dist/access/multi-column-read-write.test.js.map +1 -1
- package/dist/access/relationship-count.test.js +1 -1
- package/dist/access/relationship-count.test.js.map +1 -1
- package/dist/access/relationship-label-filter.test.js +1 -1
- package/dist/access/relationship-label-filter.test.js.map +1 -1
- package/dist/access/types.d.ts +15 -7
- package/dist/access/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +15 -9
- 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/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +5 -4
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +258 -7
- package/src/access/access-filter.ts +146 -72
- package/src/access/depth-limits.ts +26 -0
- package/src/access/errors.ts +62 -0
- package/src/access/field-visibility.ts +97 -21
- package/src/access/index.ts +6 -0
- package/src/access/multi-column-read-write.test.ts +1 -1
- package/src/access/relationship-count.test.ts +1 -1
- package/src/access/relationship-label-filter.test.ts +1 -1
- package/src/access/types.ts +12 -5
- package/src/context/index.ts +15 -6
- package/src/context/nested-operations.ts +0 -7
- package/src/context/transaction-boundary.ts +48 -7
- package/src/context/write-pipeline.ts +5 -4
- package/src/index.ts +11 -0
- package/tests/access-relationships.test.ts +78 -64
- package/tests/context.test.ts +106 -24
- package/tests/default-value-create.test.ts +1 -1
- package/tests/hook-pipeline.test.ts +1 -1
- package/tests/nav-count.test.ts +2 -2
- package/tests/resolve-chain.test.ts +394 -0
- package/tests/transaction-boundary-hooks.test.ts +246 -1
- package/tests/write-pipeline.test.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# @opensaas/stack-core
|
|
2
2
|
|
|
3
|
+
## 0.34.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#846](https://github.com/OpenSaasAU/stack/pull/846) [`fedc858`](https://github.com/OpenSaasAU/stack/commit/fedc858f41bf5cacf001f64e7b710112f2fce20b) Thanks [@borisno2](https://github.com/borisno2)! - Fix unbounded recursion when a `resolveOutput` hook issues its own read ([#844](https://github.com/OpenSaasAU/stack/issues/844)): a hook's read could return rows whose own `resolveOutput` hooks issued further reads with no bound, driving the process to the V8 heap limit on a cyclic readable-relationship graph.
|
|
8
|
+
|
|
9
|
+
Reads are now tracked with a resolve chain — the ordered `(list, field)` pairs a read has entered via `resolveOutput` hooks. A hook that would re-enter a pair already on its own chain throws the new `ResolveOutputCycleError` naming the full chain, instead of recursing forever:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { ResolveOutputCycleError } from '@opensaas/stack-core'
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await context.db.user.findMany({})
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (err instanceof ResolveOutputCycleError) {
|
|
18
|
+
// err.chain: readonly { listKey: string; fieldKey: string }[]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
An acyclic chain that runs deeper than `RESOLVE_CHAIN_MAX_LENGTH` (a cost limit, not a correctness guard) omits the field and logs a single `console.warn` instead of throwing — a legitimately terminating hook chain (e.g. a virtual field reading another virtual field several hops deep) is never denied.
|
|
24
|
+
|
|
25
|
+
This also fixes a related bug where a plain top-level read running concurrently with an unrelated in-flight `resolveOutput` hook could have its own nested auto-include silently collapsed, because the previous implementation tracked "am I inside a hook?" on one mutable counter shared by the whole request.
|
|
26
|
+
|
|
27
|
+
**Breaking (internal plumbing only):** `AccessContext`'s underscore-prefixed `_resolveOutputCounter: { depth: number }` is replaced by `_resolveOutputChain: readonly { listKey: string; fieldKey: string }[]`. Application code never reads this field. Hand-built `AccessContext` mocks in tests need a one-line update:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// Before
|
|
31
|
+
_resolveOutputCounter: {
|
|
32
|
+
depth: 0
|
|
33
|
+
}
|
|
34
|
+
// After
|
|
35
|
+
_resolveOutputChain: []
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 0.33.0
|
|
39
|
+
|
|
40
|
+
### Minor Changes
|
|
41
|
+
|
|
42
|
+
- [#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.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { AccessScopeDepthExceededError } from '@opensaas/stack-core'
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await context.db.post.findMany({
|
|
49
|
+
include: { author: { include: {/* … nested past the depth cap */} } },
|
|
50
|
+
})
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err instanceof AccessScopeDepthExceededError) {
|
|
53
|
+
// err.listKey / err.fieldKey / err.depth — restructure into shallower reads.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
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).
|
|
59
|
+
|
|
60
|
+
### Patch Changes
|
|
61
|
+
|
|
62
|
+
- [#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.
|
|
63
|
+
|
|
3
64
|
## 0.32.0
|
|
4
65
|
|
|
5
66
|
## 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,CAwE9B;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,72 @@
|
|
|
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 — this keeps a single hook-issued read's include from growing
|
|
59
|
+
// depth-first. Correction (ADR-0023): this does NOT by itself terminate a
|
|
60
|
+
// cycle, despite what an earlier version of this comment claimed. A
|
|
61
|
+
// self-referential relation read from inside a hook is a NEW top-level
|
|
62
|
+
// read, invisible to this function's own recursion — the loop runs through
|
|
63
|
+
// the hook↔read boundary, not through repeated calls to
|
|
64
|
+
// `buildIncludeWithAccessControl`. Loop termination is the resolve chain's
|
|
65
|
+
// job: `field-visibility.ts`'s `resolveReadableFieldValue` refuses to
|
|
66
|
+
// re-enter a `(list, field)` pair already on the chain. What this check
|
|
67
|
+
// still does correctly is row-scope the immediate relation rather than
|
|
68
|
+
// skipping scoping altogether (issue #830's second trigger).
|
|
69
|
+
const insideResolveOutput = args.context._resolveOutputChain.length > 0;
|
|
37
70
|
const include = {};
|
|
38
71
|
let hasRelationships = false;
|
|
39
72
|
for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
|
|
@@ -51,12 +84,7 @@ visitedLists = []) {
|
|
|
51
84
|
if (accessResult === false) {
|
|
52
85
|
continue;
|
|
53
86
|
}
|
|
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
|
-
}
|
|
87
|
+
const where = typeof accessResult === 'object' ? accessResult : undefined;
|
|
60
88
|
// Cycle guard: if the related list already appears on the path from the
|
|
61
89
|
// root, DO NOT auto-include its relationships again. On a cyclic
|
|
62
90
|
// readable-relationship graph (A → B → … → A) the depth-first walk would
|
|
@@ -68,20 +96,16 @@ visitedLists = []) {
|
|
|
68
96
|
// genuine single-level fetch, not a re-expansion of the full auto-include.
|
|
69
97
|
// The relation itself is still included (as a FLAT fetch of its own
|
|
70
98
|
// columns); only its onward relationships are pruned at the back-edge.
|
|
99
|
+
let nested = { kind: 'nothing-to-scope' };
|
|
71
100
|
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
|
-
}
|
|
101
|
+
if (!insideResolveOutput && !visitedLists.includes(relatedListName)) {
|
|
102
|
+
nested = await buildIncludeWithAccessControl(relatedConfig.listConfig.fields, args, config, depth + 1, [...visitedLists, relatedListName]);
|
|
78
103
|
}
|
|
79
|
-
|
|
80
|
-
include[fieldName] = Object.keys(includeEntry).length > 0 ? includeEntry : true;
|
|
104
|
+
include[fieldName] = { where, nested };
|
|
81
105
|
}
|
|
82
106
|
}
|
|
83
107
|
}
|
|
84
|
-
return hasRelationships ? include :
|
|
108
|
+
return hasRelationships ? { kind: 'scoped', include } : { kind: 'nothing-to-scope' };
|
|
85
109
|
}
|
|
86
110
|
/**
|
|
87
111
|
* Narrow an unknown include value to the structured object form (vs bare `true`
|
|
@@ -148,32 +172,37 @@ function andWhere(accessWhere, callerWhere) {
|
|
|
148
172
|
* - If the caller names a key that is NOT a config-declared relationship, it is
|
|
149
173
|
* passed through unchanged (access control does not govern it).
|
|
150
174
|
*
|
|
151
|
-
*
|
|
152
|
-
* `
|
|
153
|
-
*
|
|
154
|
-
*
|
|
175
|
+
* `accessControlledInclude` is the {@link AccessIncludeResult} for THIS level:
|
|
176
|
+
* - `nothing-to-scope` → nothing to merge against (the list has no
|
|
177
|
+
* relationships, or we're inside a resolveOutput context where the caller
|
|
178
|
+
* include is irrelevant to begin with). Pass the caller's include through
|
|
179
|
+
* unchanged — this is a non-denial outcome, not "every relation denied".
|
|
180
|
+
* - `depth-exceeded` → the engine could not compute a scope for THIS level at
|
|
181
|
+
* all because it sits at or past `READ_INCLUDE_MAX_DEPTH`. If the caller
|
|
182
|
+
* named anything here, that is exactly the case that used to pass through
|
|
183
|
+
* unscoped (issue #830): throw `AccessScopeDepthExceededError` instead. An
|
|
184
|
+
* empty caller include at this level (nothing further requested) is not an
|
|
185
|
+
* error — there's simply nothing to do.
|
|
186
|
+
* - `scoped` → the normal per-relation merge below: a declared relationship
|
|
187
|
+
* ABSENT from the access include was denied (drop it); one PRESENT is used
|
|
188
|
+
* as the base, AND-combining `where`s and recursing into nested includes.
|
|
155
189
|
*
|
|
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.
|
|
190
|
+
* `listKey` and `depth` are carried only to build a useful
|
|
191
|
+
* `AccessScopeDepthExceededError` message; they do not affect merge behaviour.
|
|
165
192
|
*/
|
|
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) {
|
|
193
|
+
export function mergeIncludeWithAccessControl(callerInclude, accessControlledInclude, fieldConfigs, config, listKey, depth = 0) {
|
|
194
|
+
if (accessControlledInclude.kind === 'nothing-to-scope') {
|
|
173
195
|
return callerInclude;
|
|
174
196
|
}
|
|
197
|
+
if (accessControlledInclude.kind === 'depth-exceeded') {
|
|
198
|
+
const [firstRelationName] = Object.keys(callerInclude);
|
|
199
|
+
if (firstRelationName !== undefined) {
|
|
200
|
+
throw new AccessScopeDepthExceededError(listKey, firstRelationName, depth);
|
|
201
|
+
}
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
175
204
|
const merged = {};
|
|
176
|
-
const accessInclude = accessControlledInclude;
|
|
205
|
+
const accessInclude = accessControlledInclude.include;
|
|
177
206
|
for (const [relationName, callerValue] of Object.entries(callerInclude)) {
|
|
178
207
|
const fieldConfig = fieldConfigs[relationName];
|
|
179
208
|
const isDeclaredRelationship = fieldConfig?.type === 'relationship' && 'ref' in fieldConfig && !!fieldConfig.ref;
|
|
@@ -182,27 +211,30 @@ export function mergeIncludeWithAccessControl(callerInclude, accessControlledInc
|
|
|
182
211
|
merged[relationName] = callerValue;
|
|
183
212
|
continue;
|
|
184
213
|
}
|
|
185
|
-
const
|
|
214
|
+
const accessEntry = accessInclude[relationName];
|
|
186
215
|
// Declared relationship absent from the access include → query access denied → drop it.
|
|
187
|
-
if (
|
|
216
|
+
if (accessEntry === undefined) {
|
|
188
217
|
continue;
|
|
189
218
|
}
|
|
190
|
-
const accessEntry = asEntryObject(accessValue);
|
|
191
219
|
const callerEntry = asEntryObject(callerValue);
|
|
192
220
|
// Resolve the related list's field configs so nested includes merge recursively.
|
|
193
221
|
const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
|
|
194
222
|
const relatedFields = relatedConfig?.listConfig.fields;
|
|
195
|
-
const mergedWhere = andWhere(accessEntry
|
|
223
|
+
const mergedWhere = andWhere(accessEntry.where, callerEntry?.where);
|
|
196
224
|
let mergedNested;
|
|
197
|
-
if (callerEntry?.include && relatedFields) {
|
|
198
|
-
// Recurse: scope the caller's nested selection against the nested access
|
|
199
|
-
|
|
225
|
+
if (callerEntry?.include && relatedFields && relatedConfig) {
|
|
226
|
+
// Recurse: scope the caller's nested selection against the nested access
|
|
227
|
+
// result. If that result is 'depth-exceeded', the recursive call itself
|
|
228
|
+
// throws — the caller named something the engine cannot scope.
|
|
229
|
+
mergedNested = mergeIncludeWithAccessControl(callerEntry.include, accessEntry.nested, relatedFields, config, relatedConfig.listName, depth + 1);
|
|
200
230
|
}
|
|
201
|
-
else if (accessEntry
|
|
231
|
+
else if (accessEntry.nested.kind === 'scoped') {
|
|
202
232
|
// Caller selected the relation bare (no nested include); keep the
|
|
203
233
|
// access-controlled nested include so deeper relations stay filtered.
|
|
204
|
-
mergedNested = accessEntry.
|
|
234
|
+
mergedNested = toPrismaInclude(accessEntry.nested);
|
|
205
235
|
}
|
|
236
|
+
// If the caller's entry has no nested include, a 'depth-exceeded' nested
|
|
237
|
+
// result is silent here too — nothing was asked for past this point.
|
|
206
238
|
const entry = {};
|
|
207
239
|
if (mergedWhere)
|
|
208
240
|
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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"}
|