@opensaas/stack-core 0.40.0 → 0.41.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 +39 -0
- package/CLAUDE.md +52 -0
- package/dist/access/access-filter.d.ts +88 -18
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +343 -36
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +759 -6
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/denied-relation-visibility.test.d.ts +2 -0
- package/dist/access/denied-relation-visibility.test.d.ts.map +1 -0
- package/dist/access/denied-relation-visibility.test.js +114 -0
- package/dist/access/denied-relation-visibility.test.js.map +1 -0
- package/dist/access/engine.d.ts +11 -0
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +26 -0
- package/dist/access/engine.js.map +1 -1
- package/dist/access/errors.d.ts +34 -0
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +47 -0
- package/dist/access/errors.js.map +1 -1
- package/dist/access/field-visibility.d.ts +2 -2
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +73 -23
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +7 -3
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +9 -2
- package/dist/access/index.js.map +1 -1
- package/dist/access/multi-column-read-write.test.js.map +1 -1
- package/dist/access/query-validation.d.ts +31 -0
- package/dist/access/query-validation.d.ts.map +1 -1
- package/dist/access/query-validation.js +75 -16
- package/dist/access/query-validation.js.map +1 -1
- package/dist/access/relationship-count.d.ts +41 -3
- package/dist/access/relationship-count.d.ts.map +1 -1
- package/dist/access/relationship-count.js +44 -7
- package/dist/access/relationship-count.js.map +1 -1
- package/dist/access/relationship-count.test.js +62 -0
- package/dist/access/relationship-count.test.js.map +1 -1
- package/dist/access/synthetic-include-read.test.d.ts +2 -0
- package/dist/access/synthetic-include-read.test.d.ts.map +1 -0
- package/dist/access/synthetic-include-read.test.js +121 -0
- package/dist/access/synthetic-include-read.test.js.map +1 -0
- package/dist/config/types.d.ts +22 -22
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/hook-pipeline.d.ts +2 -2
- package/dist/context/hook-pipeline.d.ts.map +1 -1
- package/dist/context/hook-pipeline.js.map +1 -1
- package/dist/context/index.d.ts +16 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +76 -26
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts +2 -2
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +21 -3
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +23 -29
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/hooks/index.d.ts +16 -15
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/package.json +2 -2
- package/src/access/access-filter.test.ts +1403 -159
- package/src/access/access-filter.ts +525 -47
- package/src/access/denied-relation-visibility.test.ts +173 -0
- package/src/access/engine.ts +28 -0
- package/src/access/errors.ts +55 -0
- package/src/access/field-visibility.ts +105 -24
- package/src/access/index.ts +11 -0
- package/src/access/multi-column-read-write.test.ts +3 -2
- package/src/access/query-validation.ts +136 -9
- package/src/access/relationship-count.test.ts +75 -0
- package/src/access/relationship-count.ts +53 -10
- package/src/access/synthetic-include-read.test.ts +179 -0
- package/src/config/types.ts +22 -22
- package/src/context/hook-pipeline.ts +4 -2
- package/src/context/index.ts +120 -39
- package/src/context/nested-operations.ts +35 -11
- package/src/context/write-pipeline.ts +52 -32
- package/src/hooks/index.ts +22 -15
- package/tests/context.test.ts +193 -5
- package/tests/hook-context-secured.test.ts +424 -0
- package/tests/nested-access-and-hooks.test.ts +324 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/config/types.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type FieldResolveInputHookArgs<
|
|
|
21
21
|
inputData: TTypeInfo['inputs']['create']
|
|
22
22
|
item: undefined
|
|
23
23
|
resolvedData: TTypeInfo['inputs']['create']
|
|
24
|
-
context: import('../
|
|
24
|
+
context: import('../context/index.js').StackContext
|
|
25
25
|
}
|
|
26
26
|
| {
|
|
27
27
|
listKey: string
|
|
@@ -30,7 +30,7 @@ export type FieldResolveInputHookArgs<
|
|
|
30
30
|
inputData: TTypeInfo['inputs']['update']
|
|
31
31
|
item: TTypeInfo['item']
|
|
32
32
|
resolvedData: TTypeInfo['inputs']['update']
|
|
33
|
-
context: import('../
|
|
33
|
+
context: import('../context/index.js').StackContext
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** Arguments for {@link FieldHooks.validate} (and its deprecated `validateInput` alias). */
|
|
@@ -45,7 +45,7 @@ export type FieldValidateHookArgs<
|
|
|
45
45
|
inputData: TTypeInfo['inputs']['create']
|
|
46
46
|
item: undefined
|
|
47
47
|
resolvedData: TTypeInfo['inputs']['create']
|
|
48
|
-
context: import('../
|
|
48
|
+
context: import('../context/index.js').StackContext
|
|
49
49
|
addValidationError: (msg: string) => void
|
|
50
50
|
}
|
|
51
51
|
| {
|
|
@@ -55,7 +55,7 @@ export type FieldValidateHookArgs<
|
|
|
55
55
|
inputData: TTypeInfo['inputs']['update']
|
|
56
56
|
item: TTypeInfo['item']
|
|
57
57
|
resolvedData: TTypeInfo['inputs']['update']
|
|
58
|
-
context: import('../
|
|
58
|
+
context: import('../context/index.js').StackContext
|
|
59
59
|
addValidationError: (msg: string) => void
|
|
60
60
|
}
|
|
61
61
|
| {
|
|
@@ -63,7 +63,7 @@ export type FieldValidateHookArgs<
|
|
|
63
63
|
fieldKey: TFieldKey
|
|
64
64
|
operation: 'delete'
|
|
65
65
|
item: TTypeInfo['item']
|
|
66
|
-
context: import('../
|
|
66
|
+
context: import('../context/index.js').StackContext
|
|
67
67
|
addValidationError: (msg: string) => void
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -78,7 +78,7 @@ export type FieldBeforeOperationHookArgs<
|
|
|
78
78
|
operation: 'create'
|
|
79
79
|
inputData: TTypeInfo['inputs']['create']
|
|
80
80
|
resolvedData: TTypeInfo['inputs']['create']
|
|
81
|
-
context: import('../
|
|
81
|
+
context: import('../context/index.js').StackContext
|
|
82
82
|
}
|
|
83
83
|
| {
|
|
84
84
|
listKey: string
|
|
@@ -87,14 +87,14 @@ export type FieldBeforeOperationHookArgs<
|
|
|
87
87
|
inputData: TTypeInfo['inputs']['update']
|
|
88
88
|
item: TTypeInfo['item']
|
|
89
89
|
resolvedData: TTypeInfo['inputs']['update']
|
|
90
|
-
context: import('../
|
|
90
|
+
context: import('../context/index.js').StackContext
|
|
91
91
|
}
|
|
92
92
|
| {
|
|
93
93
|
listKey: string
|
|
94
94
|
fieldKey: TFieldKey
|
|
95
95
|
operation: 'delete'
|
|
96
96
|
item: TTypeInfo['item']
|
|
97
|
-
context: import('../
|
|
97
|
+
context: import('../context/index.js').StackContext
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/** Arguments for {@link FieldHooks.afterOperation}. */
|
|
@@ -109,7 +109,7 @@ export type FieldAfterOperationHookArgs<
|
|
|
109
109
|
inputData: TTypeInfo['inputs']['create']
|
|
110
110
|
item: TTypeInfo['item']
|
|
111
111
|
resolvedData: TTypeInfo['inputs']['create']
|
|
112
|
-
context: import('../
|
|
112
|
+
context: import('../context/index.js').StackContext
|
|
113
113
|
}
|
|
114
114
|
| {
|
|
115
115
|
listKey: string
|
|
@@ -119,14 +119,14 @@ export type FieldAfterOperationHookArgs<
|
|
|
119
119
|
originalItem: TTypeInfo['item']
|
|
120
120
|
item: TTypeInfo['item']
|
|
121
121
|
resolvedData: TTypeInfo['inputs']['update']
|
|
122
|
-
context: import('../
|
|
122
|
+
context: import('../context/index.js').StackContext
|
|
123
123
|
}
|
|
124
124
|
| {
|
|
125
125
|
listKey: string
|
|
126
126
|
fieldKey: TFieldKey
|
|
127
127
|
operation: 'delete'
|
|
128
128
|
originalItem: TTypeInfo['item']
|
|
129
|
-
context: import('../
|
|
129
|
+
context: import('../context/index.js').StackContext
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
@@ -1609,7 +1609,7 @@ export type ResolveInputHookArgs<
|
|
|
1609
1609
|
inputData: TCreateInput
|
|
1610
1610
|
resolvedData: TCreateInput
|
|
1611
1611
|
item: undefined
|
|
1612
|
-
context: import('../
|
|
1612
|
+
context: import('../context/index.js').StackContext
|
|
1613
1613
|
}
|
|
1614
1614
|
| {
|
|
1615
1615
|
listKey: string
|
|
@@ -1617,7 +1617,7 @@ export type ResolveInputHookArgs<
|
|
|
1617
1617
|
inputData: TUpdateInput
|
|
1618
1618
|
resolvedData: TUpdateInput
|
|
1619
1619
|
item: TOutput
|
|
1620
|
-
context: import('../
|
|
1620
|
+
context: import('../context/index.js').StackContext
|
|
1621
1621
|
}
|
|
1622
1622
|
|
|
1623
1623
|
/**
|
|
@@ -1637,7 +1637,7 @@ export type ValidateHookArgs<
|
|
|
1637
1637
|
inputData: TCreateInput
|
|
1638
1638
|
resolvedData: TCreateInput
|
|
1639
1639
|
item: undefined
|
|
1640
|
-
context: import('../
|
|
1640
|
+
context: import('../context/index.js').StackContext
|
|
1641
1641
|
addValidationError: (msg: string) => void
|
|
1642
1642
|
}
|
|
1643
1643
|
| {
|
|
@@ -1646,14 +1646,14 @@ export type ValidateHookArgs<
|
|
|
1646
1646
|
inputData: TUpdateInput
|
|
1647
1647
|
resolvedData: TUpdateInput
|
|
1648
1648
|
item: TOutput
|
|
1649
|
-
context: import('../
|
|
1649
|
+
context: import('../context/index.js').StackContext
|
|
1650
1650
|
addValidationError: (msg: string) => void
|
|
1651
1651
|
}
|
|
1652
1652
|
| {
|
|
1653
1653
|
listKey: string
|
|
1654
1654
|
operation: 'delete'
|
|
1655
1655
|
item: TOutput
|
|
1656
|
-
context: import('../
|
|
1656
|
+
context: import('../context/index.js').StackContext
|
|
1657
1657
|
addValidationError: (msg: string) => void
|
|
1658
1658
|
}
|
|
1659
1659
|
|
|
@@ -1673,7 +1673,7 @@ export type BeforeOperationHookArgs<
|
|
|
1673
1673
|
operation: 'create'
|
|
1674
1674
|
inputData: TCreateInput
|
|
1675
1675
|
resolvedData: TCreateInput
|
|
1676
|
-
context: import('../
|
|
1676
|
+
context: import('../context/index.js').StackContext
|
|
1677
1677
|
}
|
|
1678
1678
|
| {
|
|
1679
1679
|
listKey: string
|
|
@@ -1681,13 +1681,13 @@ export type BeforeOperationHookArgs<
|
|
|
1681
1681
|
inputData: TUpdateInput
|
|
1682
1682
|
item: TOutput
|
|
1683
1683
|
resolvedData: TUpdateInput
|
|
1684
|
-
context: import('../
|
|
1684
|
+
context: import('../context/index.js').StackContext
|
|
1685
1685
|
}
|
|
1686
1686
|
| {
|
|
1687
1687
|
listKey: string
|
|
1688
1688
|
operation: 'delete'
|
|
1689
1689
|
item: TOutput
|
|
1690
|
-
context: import('../
|
|
1690
|
+
context: import('../context/index.js').StackContext
|
|
1691
1691
|
}
|
|
1692
1692
|
|
|
1693
1693
|
/**
|
|
@@ -1707,7 +1707,7 @@ export type AfterOperationHookArgs<
|
|
|
1707
1707
|
inputData: TCreateInput
|
|
1708
1708
|
item: TOutput
|
|
1709
1709
|
resolvedData: TCreateInput
|
|
1710
|
-
context: import('../
|
|
1710
|
+
context: import('../context/index.js').StackContext
|
|
1711
1711
|
}
|
|
1712
1712
|
| {
|
|
1713
1713
|
listKey: string
|
|
@@ -1716,13 +1716,13 @@ export type AfterOperationHookArgs<
|
|
|
1716
1716
|
originalItem: TOutput
|
|
1717
1717
|
item: TOutput
|
|
1718
1718
|
resolvedData: TUpdateInput
|
|
1719
|
-
context: import('../
|
|
1719
|
+
context: import('../context/index.js').StackContext
|
|
1720
1720
|
}
|
|
1721
1721
|
| {
|
|
1722
1722
|
listKey: string
|
|
1723
1723
|
operation: 'delete'
|
|
1724
1724
|
originalItem: TOutput
|
|
1725
|
-
context: import('../
|
|
1725
|
+
context: import('../context/index.js').StackContext
|
|
1726
1726
|
}
|
|
1727
1727
|
|
|
1728
1728
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ListConfig } from '../config/types.js'
|
|
2
|
-
import type {
|
|
2
|
+
import type { StackContext } from './index.js'
|
|
3
3
|
import {
|
|
4
4
|
executeResolveInput,
|
|
5
5
|
executeValidate,
|
|
@@ -25,7 +25,9 @@ export interface HookPipelineArgs {
|
|
|
25
25
|
listConfig: ListConfig<any>
|
|
26
26
|
inputData: Record<string, unknown>
|
|
27
27
|
item: Record<string, unknown> | undefined
|
|
28
|
-
|
|
28
|
+
// #1176: the full secured context (sudo/withSession/transaction), bound to
|
|
29
|
+
// the write's transaction client — see `bindContextToTransaction`.
|
|
30
|
+
context: StackContext
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface HookPipelineResult {
|
package/src/context/index.ts
CHANGED
|
@@ -12,8 +12,13 @@ import {
|
|
|
12
12
|
validateQueryFieldReadAccess,
|
|
13
13
|
resolveToOneAccessVisibility,
|
|
14
14
|
emptyToOneAccessFilterTree,
|
|
15
|
+
emptyCountAccessDenialTree,
|
|
16
|
+
} from '../access/index.js'
|
|
17
|
+
import type {
|
|
18
|
+
DeclaredOnlyTree,
|
|
19
|
+
ToOneAccessFilterTree,
|
|
20
|
+
CountAccessDenialTree,
|
|
15
21
|
} from '../access/index.js'
|
|
16
|
-
import type { DeclaredOnlyTree, ToOneAccessFilterTree } from '../access/index.js'
|
|
17
22
|
import { ValidationError, DatabaseError } from '../hooks/index.js'
|
|
18
23
|
import { getDbKey } from '../lib/case-utils.js'
|
|
19
24
|
import { uniqueConstraintOf } from '../lib/prisma-errors.js'
|
|
@@ -359,6 +364,15 @@ export interface StackContext<TPrisma extends PrismaClientLike = PrismaClientLik
|
|
|
359
364
|
*/
|
|
360
365
|
withSession: (session: Session | null) => StackContext<TPrisma>
|
|
361
366
|
_isSudo: boolean
|
|
367
|
+
/**
|
|
368
|
+
* @internal Present so a hook-bound `StackContext` (issue #1176) satisfies
|
|
369
|
+
* {@link AccessContext} and can be threaded through the same internal
|
|
370
|
+
* write-pipeline/access plumbing that a plain `AccessContext` is — see
|
|
371
|
+
* `_resolveOutputChain` on `AccessContext` for what this tracks.
|
|
372
|
+
*/
|
|
373
|
+
_resolveOutputChain: readonly { listKey: string; fieldKey: string }[]
|
|
374
|
+
/** @internal See `_transactionOwner` on `AccessContext`. */
|
|
375
|
+
_transactionOwner?: TransactionRegistry
|
|
362
376
|
}
|
|
363
377
|
|
|
364
378
|
/**
|
|
@@ -406,6 +420,11 @@ export function getContext<
|
|
|
406
420
|
// owner's callback body, carry the deferral registry so writes reached
|
|
407
421
|
// through this context join it instead of firing afterTransaction eagerly.
|
|
408
422
|
_transactionOwner?: TransactionRegistry,
|
|
423
|
+
// Internal (ADR-0023, issue #1176): when rebuilding the context for a
|
|
424
|
+
// hook-bound write (or a derived `sudo()`/`withSession()` of one), carry the
|
|
425
|
+
// resolve chain forward so a write issued from inside a `resolveOutput` hook
|
|
426
|
+
// keeps that hook's cycle-guard chain instead of resetting to empty.
|
|
427
|
+
_resolveOutputChain?: readonly { listKey: string; fieldKey: string }[],
|
|
409
428
|
): StackContext<TPrisma> {
|
|
410
429
|
// Broad type to allow dynamic model access; populated by populateDbDelegate below.
|
|
411
430
|
const db: Record<string, unknown> = {}
|
|
@@ -440,7 +459,7 @@ export function getContext<
|
|
|
440
459
|
// client, otherwise start empty and populate via plugin runtimes below.
|
|
441
460
|
plugins: _sharedPlugins ?? {},
|
|
442
461
|
_isSudo,
|
|
443
|
-
_resolveOutputChain: [],
|
|
462
|
+
_resolveOutputChain: _resolveOutputChain ?? [],
|
|
444
463
|
_transactionOwner,
|
|
445
464
|
}
|
|
446
465
|
|
|
@@ -791,6 +810,10 @@ export function getContext<
|
|
|
791
810
|
// ADR-0028: a sudo write issued from inside an owned transaction (e.g.
|
|
792
811
|
// `tx.sudo().db.x.create()`) must still defer to that owner.
|
|
793
812
|
context._transactionOwner,
|
|
813
|
+
// #1176: carry the resolve chain forward so a `context.sudo()` called
|
|
814
|
+
// from inside a `resolveOutput` hook keeps that hook's cycle-guard
|
|
815
|
+
// chain (ADR-0023) rather than resetting to empty.
|
|
816
|
+
context._resolveOutputChain,
|
|
794
817
|
)
|
|
795
818
|
}
|
|
796
819
|
|
|
@@ -807,6 +830,8 @@ export function getContext<
|
|
|
807
830
|
// ADR-0028: a write issued from inside an owned transaction (e.g.
|
|
808
831
|
// `tx.withSession(s).db.x.create()`) must still defer to that owner.
|
|
809
832
|
context._transactionOwner,
|
|
833
|
+
// #1176: see the identical comment in `sudo()` above.
|
|
834
|
+
context._resolveOutputChain,
|
|
810
835
|
)
|
|
811
836
|
}
|
|
812
837
|
|
|
@@ -847,6 +872,7 @@ export function getContext<
|
|
|
847
872
|
_isSudo,
|
|
848
873
|
context.plugins,
|
|
849
874
|
registry,
|
|
875
|
+
context._resolveOutputChain,
|
|
850
876
|
),
|
|
851
877
|
)
|
|
852
878
|
: (client.$transaction(
|
|
@@ -860,6 +886,7 @@ export function getContext<
|
|
|
860
886
|
_isSudo,
|
|
861
887
|
context.plugins,
|
|
862
888
|
registry,
|
|
889
|
+
context._resolveOutputChain,
|
|
863
890
|
),
|
|
864
891
|
),
|
|
865
892
|
options,
|
|
@@ -879,6 +906,12 @@ export function getContext<
|
|
|
879
906
|
withSession,
|
|
880
907
|
transaction,
|
|
881
908
|
_isSudo,
|
|
909
|
+
// #1176: carried so a `StackContext` structurally satisfies `AccessContext`
|
|
910
|
+
// and can be handed, unchanged, to the internal write-pipeline/hook
|
|
911
|
+
// plumbing that a plain `AccessContext` was built for — see
|
|
912
|
+
// `bindContextToTransaction` in `write-pipeline.ts`.
|
|
913
|
+
_resolveOutputChain: context._resolveOutputChain,
|
|
914
|
+
_transactionOwner: context._transactionOwner,
|
|
882
915
|
}
|
|
883
916
|
return returned
|
|
884
917
|
}
|
|
@@ -955,11 +988,12 @@ export function buildDbDelegate<TPrisma extends PrismaClientLike>(
|
|
|
955
988
|
* caller include / bare (ADR-0024) — while folding declared dependencies
|
|
956
989
|
* (`needs`, ADR-0025) into whichever of those the read is already using.
|
|
957
990
|
*
|
|
958
|
-
* A fragment's own `include` and a sudo caller's `include` are
|
|
959
|
-
*
|
|
960
|
-
* caller
|
|
961
|
-
*
|
|
962
|
-
*
|
|
991
|
+
* A non-sudo fragment's own `include` and a non-sudo caller's `include` are
|
|
992
|
+
* both folded and then scoped by `buildAccessScopedInclude` (ADR-0026) —
|
|
993
|
+
* caller-directed, so a relation named nowhere in the folded tree never has
|
|
994
|
+
* its list's `query` access evaluated at all (issue #1088: a fragment read
|
|
995
|
+
* used to skip this walk entirely). A sudo caller's `include` (fragment or
|
|
996
|
+
* not) is folded and used as-is, unscoped — sudo is unaffected. A bare read
|
|
963
997
|
* stays on the exact ADR-0024 path — `include: undefined`, no related
|
|
964
998
|
* `query` access evaluated — unless folding actually added something, which
|
|
965
999
|
* only happens when a field on this list declares `needs`.
|
|
@@ -973,9 +1007,10 @@ export function buildDbDelegate<TPrisma extends PrismaClientLike>(
|
|
|
973
1007
|
*
|
|
974
1008
|
* Also returns `toOneAccessFilters` — the to-one relations `buildAccessScopedInclude`
|
|
975
1009
|
* flagged as needing a post-query existence check rather than a Prisma-side
|
|
976
|
-
* `where` (issue #974). Only
|
|
977
|
-
* non-empty tree:
|
|
978
|
-
* access at all.
|
|
1010
|
+
* `where` (issue #974). Only a non-sudo fragment or caller-include read can
|
|
1011
|
+
* produce a non-empty tree: those are the only paths that evaluate a related
|
|
1012
|
+
* list's `query` access at all. A sudo read and a bare read always return an
|
|
1013
|
+
* empty tree.
|
|
979
1014
|
*/
|
|
980
1015
|
async function resolveReadInclude(
|
|
981
1016
|
callerInclude: Record<string, unknown> | undefined,
|
|
@@ -990,6 +1025,7 @@ async function resolveReadInclude(
|
|
|
990
1025
|
declaredOnly: DeclaredOnlyTree
|
|
991
1026
|
selection: FieldSelectionScope | undefined
|
|
992
1027
|
toOneAccessFilters: ToOneAccessFilterTree
|
|
1028
|
+
countDenials: CountAccessDenialTree
|
|
993
1029
|
}> {
|
|
994
1030
|
if (fragmentFields !== undefined) {
|
|
995
1031
|
const fragmentInclude = buildInclude(fragmentFields) ?? undefined
|
|
@@ -1002,27 +1038,66 @@ async function resolveReadInclude(
|
|
|
1002
1038
|
[listName],
|
|
1003
1039
|
selection,
|
|
1004
1040
|
)
|
|
1005
|
-
|
|
1041
|
+
|
|
1042
|
+
if (context._isSudo || !folded.include) {
|
|
1043
|
+
return {
|
|
1044
|
+
...folded,
|
|
1045
|
+
selection,
|
|
1046
|
+
toOneAccessFilters: emptyToOneAccessFilterTree(),
|
|
1047
|
+
countDenials: emptyCountAccessDenialTree(),
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const { include, toOneAccessFilters, countDenials } = await buildAccessScopedInclude(
|
|
1052
|
+
folded.include,
|
|
1053
|
+
listConfig.fields,
|
|
1054
|
+
{ session: context.session, context },
|
|
1055
|
+
config,
|
|
1056
|
+
listName,
|
|
1057
|
+
)
|
|
1058
|
+
return {
|
|
1059
|
+
include,
|
|
1060
|
+
declaredOnly: folded.declaredOnly,
|
|
1061
|
+
selection,
|
|
1062
|
+
toOneAccessFilters,
|
|
1063
|
+
countDenials,
|
|
1064
|
+
}
|
|
1006
1065
|
}
|
|
1007
1066
|
|
|
1008
1067
|
if (context._isSudo) {
|
|
1009
1068
|
const folded = foldDeclaredDependencies(callerInclude, listConfig.fields, config, listName)
|
|
1010
|
-
return {
|
|
1069
|
+
return {
|
|
1070
|
+
...folded,
|
|
1071
|
+
selection: undefined,
|
|
1072
|
+
toOneAccessFilters: emptyToOneAccessFilterTree(),
|
|
1073
|
+
countDenials: emptyCountAccessDenialTree(),
|
|
1074
|
+
}
|
|
1011
1075
|
}
|
|
1012
1076
|
|
|
1013
1077
|
const folded = foldDeclaredDependencies(callerInclude, listConfig.fields, config, listName)
|
|
1014
1078
|
if (!folded.include) {
|
|
1015
|
-
return {
|
|
1079
|
+
return {
|
|
1080
|
+
...folded,
|
|
1081
|
+
selection: undefined,
|
|
1082
|
+
toOneAccessFilters: emptyToOneAccessFilterTree(),
|
|
1083
|
+
countDenials: emptyCountAccessDenialTree(),
|
|
1084
|
+
}
|
|
1016
1085
|
}
|
|
1017
1086
|
|
|
1018
|
-
const { include, toOneAccessFilters } = await buildAccessScopedInclude(
|
|
1087
|
+
const { include, toOneAccessFilters, countDenials } = await buildAccessScopedInclude(
|
|
1019
1088
|
folded.include,
|
|
1020
1089
|
listConfig.fields,
|
|
1021
1090
|
{ session: context.session, context },
|
|
1022
1091
|
config,
|
|
1023
1092
|
listName,
|
|
1024
1093
|
)
|
|
1025
|
-
return {
|
|
1094
|
+
return {
|
|
1095
|
+
include,
|
|
1096
|
+
declaredOnly: folded.declaredOnly,
|
|
1097
|
+
selection: undefined,
|
|
1098
|
+
toOneAccessFilters,
|
|
1099
|
+
countDenials,
|
|
1100
|
+
}
|
|
1026
1101
|
}
|
|
1027
1102
|
|
|
1028
1103
|
function createFindUnique<TPrisma extends PrismaClientLike>(
|
|
@@ -1079,14 +1154,15 @@ function createFindUnique<TPrisma extends PrismaClientLike>(
|
|
|
1079
1154
|
// Resolve `include`, folding any declared dependencies (`needs`,
|
|
1080
1155
|
// ADR-0025) in alongside whatever the fragment/caller/sudo/bare path
|
|
1081
1156
|
// already produces — see `resolveReadInclude`'s doc comment.
|
|
1082
|
-
let { include, declaredOnly, selection, toOneAccessFilters } =
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1157
|
+
let { include, declaredOnly, selection, toOneAccessFilters, countDenials } =
|
|
1158
|
+
await resolveReadInclude(
|
|
1159
|
+
args.include,
|
|
1160
|
+
fragment ? fragment._fields : undefined,
|
|
1161
|
+
listName,
|
|
1162
|
+
listConfig,
|
|
1163
|
+
context,
|
|
1164
|
+
config,
|
|
1165
|
+
)
|
|
1090
1166
|
|
|
1091
1167
|
// Virtual fields have no database column. Whichever path produced
|
|
1092
1168
|
// `include` (fragment, access-controlled merge, or sudo passthrough), a
|
|
@@ -1131,6 +1207,7 @@ function createFindUnique<TPrisma extends PrismaClientLike>(
|
|
|
1131
1207
|
declaredOnly,
|
|
1132
1208
|
selection,
|
|
1133
1209
|
toOneVisibility,
|
|
1210
|
+
countDenials,
|
|
1134
1211
|
)
|
|
1135
1212
|
|
|
1136
1213
|
if (fragment) {
|
|
@@ -1240,14 +1317,15 @@ function createFindMany<TPrisma extends PrismaClientLike>(
|
|
|
1240
1317
|
// Resolve `include`, folding any declared dependencies (`needs`,
|
|
1241
1318
|
// ADR-0025) in alongside whatever the fragment/caller/sudo/bare path
|
|
1242
1319
|
// already produces — see `resolveReadInclude`'s doc comment.
|
|
1243
|
-
let { include, declaredOnly, selection, toOneAccessFilters } =
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1320
|
+
let { include, declaredOnly, selection, toOneAccessFilters, countDenials } =
|
|
1321
|
+
await resolveReadInclude(
|
|
1322
|
+
args?.include,
|
|
1323
|
+
fragment ? fragment._fields : undefined,
|
|
1324
|
+
listName,
|
|
1325
|
+
listConfig,
|
|
1326
|
+
context,
|
|
1327
|
+
config,
|
|
1328
|
+
)
|
|
1251
1329
|
|
|
1252
1330
|
// Strips virtual keys from `include` before the Prisma call — see the
|
|
1253
1331
|
// `createFindUnique` comment above for why (#628, ADR-0027).
|
|
@@ -1289,6 +1367,7 @@ function createFindMany<TPrisma extends PrismaClientLike>(
|
|
|
1289
1367
|
declaredOnly,
|
|
1290
1368
|
selection,
|
|
1291
1369
|
toOneVisibility,
|
|
1370
|
+
countDenials,
|
|
1292
1371
|
),
|
|
1293
1372
|
),
|
|
1294
1373
|
)
|
|
@@ -1573,14 +1652,15 @@ function createGet<TPrisma extends PrismaClientLike>(
|
|
|
1573
1652
|
// Resolve `include`, folding any declared dependencies (`needs`,
|
|
1574
1653
|
// ADR-0025) in alongside whatever the fragment/caller/sudo/bare path
|
|
1575
1654
|
// already produces — see `resolveReadInclude`'s doc comment.
|
|
1576
|
-
let { include, declaredOnly, selection, toOneAccessFilters } =
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1655
|
+
let { include, declaredOnly, selection, toOneAccessFilters, countDenials } =
|
|
1656
|
+
await resolveReadInclude(
|
|
1657
|
+
args?.include,
|
|
1658
|
+
fragment ? fragment._fields : undefined,
|
|
1659
|
+
listName,
|
|
1660
|
+
listConfig,
|
|
1661
|
+
context,
|
|
1662
|
+
config,
|
|
1663
|
+
)
|
|
1584
1664
|
|
|
1585
1665
|
// Virtual fields have no database column and must never reach Prisma (#628).
|
|
1586
1666
|
include = stripVirtualFieldsFromInclude(include, listConfig.fields, config)
|
|
@@ -1611,6 +1691,7 @@ function createGet<TPrisma extends PrismaClientLike>(
|
|
|
1611
1691
|
declaredOnly,
|
|
1612
1692
|
selection,
|
|
1613
1693
|
toOneVisibility,
|
|
1694
|
+
countDenials,
|
|
1614
1695
|
)
|
|
1615
1696
|
if (fragment) {
|
|
1616
1697
|
return pickFields(filtered, fragment._fields)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { OpenSaasConfig, ListConfig, FieldConfig } from '../config/types.js'
|
|
2
|
-
import type {
|
|
2
|
+
import type { FieldAccess, PrismaFilter } from '../access/types.js'
|
|
3
|
+
import type { StackContext } from './index.js'
|
|
3
4
|
import {
|
|
4
5
|
checkAccess,
|
|
5
6
|
checkCreateAccess,
|
|
6
7
|
filterWritableFields,
|
|
7
8
|
getRelatedListConfig,
|
|
9
|
+
mergeFilters,
|
|
8
10
|
resolveSyntheticReverseRelation,
|
|
9
11
|
} from '../access/index.js'
|
|
10
12
|
import { checkFieldAccess } from '../access/field-access.js'
|
|
@@ -199,7 +201,7 @@ async function processNestedCreate(
|
|
|
199
201
|
relatedListName: string,
|
|
200
202
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
201
203
|
relatedListConfig: ListConfig<any>,
|
|
202
|
-
context:
|
|
204
|
+
context: StackContext,
|
|
203
205
|
config: OpenSaasConfig,
|
|
204
206
|
prisma: unknown,
|
|
205
207
|
afterTasks: AfterTask[],
|
|
@@ -391,7 +393,7 @@ async function verifyConnectReachable(
|
|
|
391
393
|
relatedListName: string,
|
|
392
394
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
393
395
|
relatedListConfig: ListConfig<any>,
|
|
394
|
-
context:
|
|
396
|
+
context: StackContext,
|
|
395
397
|
prisma: unknown,
|
|
396
398
|
owningFieldAccess: FieldAccess | undefined,
|
|
397
399
|
enclosingOperation: 'create' | 'update',
|
|
@@ -456,7 +458,7 @@ async function processNestedConnect(
|
|
|
456
458
|
relatedListName: string,
|
|
457
459
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
458
460
|
relatedListConfig: ListConfig<any>,
|
|
459
|
-
context:
|
|
461
|
+
context: StackContext,
|
|
460
462
|
prisma: unknown,
|
|
461
463
|
owningFieldAccess: FieldAccess | undefined,
|
|
462
464
|
enclosingOperation: 'create' | 'update',
|
|
@@ -484,6 +486,28 @@ async function processNestedConnect(
|
|
|
484
486
|
return connections
|
|
485
487
|
}
|
|
486
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Re-check a nested update/delete access result against the target row,
|
|
491
|
+
* mirroring the Write Pipeline's `resolveExistingTarget` (#1081): `false`
|
|
492
|
+
* denies, `true` allows outright (the row's existence is already established
|
|
493
|
+
* by the caller's own `findUnique`), and a returned PrismaFilter must
|
|
494
|
+
* additionally match the row, re-checked in the DATABASE via
|
|
495
|
+
* `findFirst(mergeFilters(where, accessResult))` rather than in memory — the
|
|
496
|
+
* same requirement `connect`'s reachability check already applies (#578), so
|
|
497
|
+
* nested-relation predicates and boolean combinators are honoured correctly.
|
|
498
|
+
*/
|
|
499
|
+
async function isExistingTargetAccessible(
|
|
500
|
+
model: { findFirst: (args: { where: Record<string, unknown> }) => Promise<unknown> },
|
|
501
|
+
where: Record<string, unknown>,
|
|
502
|
+
accessResult: boolean | PrismaFilter,
|
|
503
|
+
): Promise<boolean> {
|
|
504
|
+
if (accessResult === false) return false
|
|
505
|
+
if (accessResult === true) return true
|
|
506
|
+
|
|
507
|
+
const matchesFilter = await model.findFirst({ where: mergeFilters(where, accessResult) ?? {} })
|
|
508
|
+
return matchesFilter !== null
|
|
509
|
+
}
|
|
510
|
+
|
|
487
511
|
/**
|
|
488
512
|
* Process nested update operations.
|
|
489
513
|
*
|
|
@@ -497,7 +521,7 @@ async function processNestedUpdate(
|
|
|
497
521
|
relatedListName: string,
|
|
498
522
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
499
523
|
relatedListConfig: ListConfig<any>,
|
|
500
|
-
context:
|
|
524
|
+
context: StackContext,
|
|
501
525
|
config: OpenSaasConfig,
|
|
502
526
|
prisma: unknown,
|
|
503
527
|
afterTasks: AfterTask[],
|
|
@@ -529,7 +553,7 @@ async function processNestedUpdate(
|
|
|
529
553
|
context,
|
|
530
554
|
})
|
|
531
555
|
|
|
532
|
-
if (
|
|
556
|
+
if (!(await isExistingTargetAccessible(model, where, accessResult))) {
|
|
533
557
|
throw new Error('Access denied: Cannot update related item')
|
|
534
558
|
}
|
|
535
559
|
}
|
|
@@ -693,7 +717,7 @@ async function processNestedDelete(
|
|
|
693
717
|
relatedListName: string,
|
|
694
718
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
695
719
|
relatedListConfig: ListConfig<any>,
|
|
696
|
-
context:
|
|
720
|
+
context: StackContext,
|
|
697
721
|
prisma: unknown,
|
|
698
722
|
afterTasks: AfterTask[],
|
|
699
723
|
): Promise<Record<string, unknown> | Array<Record<string, unknown>> | boolean> {
|
|
@@ -726,7 +750,7 @@ async function processNestedDelete(
|
|
|
726
750
|
context,
|
|
727
751
|
})
|
|
728
752
|
|
|
729
|
-
if (
|
|
753
|
+
if (!(await isExistingTargetAccessible(model, where, accessResult))) {
|
|
730
754
|
throw new Error('Access denied: Cannot delete related item')
|
|
731
755
|
}
|
|
732
756
|
}
|
|
@@ -801,7 +825,7 @@ async function processNestedConnectOrCreate(
|
|
|
801
825
|
relatedListName: string,
|
|
802
826
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
803
827
|
relatedListConfig: ListConfig<any>,
|
|
804
|
-
context:
|
|
828
|
+
context: StackContext,
|
|
805
829
|
config: OpenSaasConfig,
|
|
806
830
|
prisma: unknown,
|
|
807
831
|
afterTasks: AfterTask[],
|
|
@@ -922,7 +946,7 @@ interface NestedOpHandlerArgs {
|
|
|
922
946
|
enclosingItem: Record<string, unknown> | undefined
|
|
923
947
|
/** The enclosing write's input data, passed to the owning-field gate for the same reason as `enclosingItem`. */
|
|
924
948
|
enclosingInputData: Record<string, unknown> | undefined
|
|
925
|
-
context:
|
|
949
|
+
context: StackContext
|
|
926
950
|
config: OpenSaasConfig
|
|
927
951
|
/** Prisma client used for dynamic model access during access checks. */
|
|
928
952
|
prisma: unknown
|
|
@@ -1190,7 +1214,7 @@ export async function processNestedOperations(
|
|
|
1190
1214
|
data: Record<string, unknown>,
|
|
1191
1215
|
fieldConfigs: Record<string, FieldConfig>,
|
|
1192
1216
|
config: OpenSaasConfig,
|
|
1193
|
-
context:
|
|
1217
|
+
context: StackContext & { prisma: unknown },
|
|
1194
1218
|
operation: 'create' | 'update',
|
|
1195
1219
|
parentListName: string,
|
|
1196
1220
|
parentOriginalItem: Record<string, unknown> | undefined,
|