@opensaas/stack-core 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +191 -0
- package/CLAUDE.md +42 -3
- package/dist/access/access-filter.d.ts +70 -113
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +183 -210
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +148 -188
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/declared-dependencies.d.ts +66 -26
- package/dist/access/declared-dependencies.d.ts.map +1 -1
- package/dist/access/declared-dependencies.js +62 -17
- package/dist/access/declared-dependencies.js.map +1 -1
- package/dist/access/declared-dependencies.test.d.ts +2 -0
- package/dist/access/declared-dependencies.test.d.ts.map +1 -0
- package/dist/access/declared-dependencies.test.js +226 -0
- package/dist/access/declared-dependencies.test.js.map +1 -0
- package/dist/access/depth-limits.d.ts +8 -7
- package/dist/access/depth-limits.d.ts.map +1 -1
- package/dist/access/depth-limits.js +8 -7
- package/dist/access/depth-limits.js.map +1 -1
- package/dist/access/engine.d.ts +0 -20
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +0 -25
- package/dist/access/engine.js.map +1 -1
- package/dist/access/errors.d.ts +61 -8
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +91 -12
- package/dist/access/errors.js.map +1 -1
- package/dist/access/field-access.d.ts +39 -1
- package/dist/access/field-access.d.ts.map +1 -1
- package/dist/access/field-access.js +108 -42
- package/dist/access/field-access.js.map +1 -1
- package/dist/access/field-access.test.js +229 -2
- package/dist/access/field-access.test.js.map +1 -1
- package/dist/access/field-transforms.d.ts +0 -27
- package/dist/access/field-transforms.d.ts.map +1 -1
- package/dist/access/field-visibility.d.ts +2 -5
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +89 -41
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +6 -3
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +12 -2
- package/dist/access/index.js.map +1 -1
- package/dist/access/query-validation.d.ts +121 -0
- package/dist/access/query-validation.d.ts.map +1 -0
- package/dist/access/query-validation.js +292 -0
- package/dist/access/query-validation.js.map +1 -0
- package/dist/access/relationship-count.d.ts +1 -1
- package/dist/access/relationship-count.d.ts.map +1 -1
- package/dist/access/relationship-count.js +2 -10
- package/dist/access/relationship-count.js.map +1 -1
- package/dist/access/relationship-label-filter.d.ts +23 -23
- package/dist/access/relationship-label-filter.d.ts.map +1 -1
- package/dist/access/relationship-label-filter.js +5 -76
- package/dist/access/relationship-label-filter.js.map +1 -1
- package/dist/access/relationship-label-filter.test.js +19 -56
- package/dist/access/relationship-label-filter.test.js.map +1 -1
- package/dist/access/transaction-registry.d.ts +45 -0
- package/dist/access/transaction-registry.d.ts.map +1 -0
- package/dist/access/transaction-registry.js +37 -0
- package/dist/access/transaction-registry.js.map +1 -0
- package/dist/access/types.d.ts +55 -59
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/index.d.ts +9 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +10 -18
- package/dist/config/index.js.map +1 -1
- package/dist/config/label.d.ts +6 -13
- package/dist/config/label.d.ts.map +1 -1
- package/dist/config/label.js +6 -13
- package/dist/config/label.js.map +1 -1
- package/dist/config/nav-count.d.ts +11 -19
- package/dist/config/nav-count.d.ts.map +1 -1
- package/dist/config/nav-count.js +11 -19
- package/dist/config/nav-count.js.map +1 -1
- package/dist/config/plugin-engine.d.ts +0 -10
- package/dist/config/plugin-engine.d.ts.map +1 -1
- package/dist/config/plugin-engine.js +1 -37
- package/dist/config/plugin-engine.js.map +1 -1
- package/dist/config/types.d.ts +94 -109
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/apply-defaults.d.ts +8 -31
- package/dist/context/apply-defaults.d.ts.map +1 -1
- package/dist/context/apply-defaults.js +12 -41
- package/dist/context/apply-defaults.js.map +1 -1
- package/dist/context/hook-pipeline.d.ts +4 -29
- package/dist/context/hook-pipeline.d.ts.map +1 -1
- package/dist/context/hook-pipeline.js +8 -41
- package/dist/context/hook-pipeline.js.map +1 -1
- package/dist/context/index.d.ts +4 -15
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +223 -215
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts +7 -26
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +73 -171
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/transaction-boundary.d.ts +36 -47
- package/dist/context/transaction-boundary.d.ts.map +1 -1
- package/dist/context/transaction-boundary.js +92 -79
- package/dist/context/transaction-boundary.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +25 -64
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +81 -111
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/fields/format-prisma-default.d.ts.map +1 -1
- package/dist/fields/format-prisma-default.js +4 -7
- package/dist/fields/format-prisma-default.js.map +1 -1
- package/dist/fields/index.d.ts +17 -2
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +136 -147
- package/dist/fields/index.js.map +1 -1
- package/dist/filter/collect.d.ts +26 -3
- package/dist/filter/collect.d.ts.map +1 -1
- package/dist/filter/collect.js +30 -12
- package/dist/filter/collect.js.map +1 -1
- package/dist/filter/filter.test.js +98 -15
- package/dist/filter/filter.test.js.map +1 -1
- package/dist/filter/index.d.ts +1 -0
- package/dist/filter/index.d.ts.map +1 -1
- package/dist/filter/index.js.map +1 -1
- package/dist/filter/map.d.ts.map +1 -1
- package/dist/filter/map.js +0 -5
- package/dist/filter/map.js.map +1 -1
- package/dist/filter/parse.d.ts.map +1 -1
- package/dist/filter/parse.js +2 -9
- package/dist/filter/parse.js.map +1 -1
- package/dist/filter/serialize.d.ts.map +1 -1
- package/dist/filter/serialize.js +2 -4
- package/dist/filter/serialize.js.map +1 -1
- package/dist/hooks/index.d.ts +4 -42
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +5 -59
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -1
- package/dist/lib/case-utils.d.ts +0 -32
- package/dist/lib/case-utils.d.ts.map +1 -1
- package/dist/lib/case-utils.js +0 -32
- package/dist/lib/case-utils.js.map +1 -1
- package/dist/mcp/handler.d.ts +0 -4
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/mcp/handler.js +12 -69
- package/dist/mcp/handler.js.map +1 -1
- package/dist/query/index.d.ts +55 -227
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +41 -117
- package/dist/query/index.js.map +1 -1
- package/dist/query/relationship-options.d.ts +1 -1
- package/dist/query/relationship-options.js +1 -1
- package/dist/utils/password.d.ts +0 -68
- package/dist/utils/password.d.ts.map +1 -1
- package/dist/utils/password.js +7 -76
- package/dist/utils/password.js.map +1 -1
- package/dist/validation/field-config.d.ts.map +1 -1
- package/dist/validation/field-config.js +0 -6
- package/dist/validation/field-config.js.map +1 -1
- package/dist/validation/schema.d.ts +0 -7
- package/dist/validation/schema.d.ts.map +1 -1
- package/dist/validation/schema.js +0 -10
- package/dist/validation/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +205 -275
- package/src/access/access-filter.ts +236 -272
- package/src/access/declared-dependencies.test.ts +277 -0
- package/src/access/declared-dependencies.ts +117 -37
- package/src/access/depth-limits.ts +8 -7
- package/src/access/engine.ts +0 -25
- package/src/access/errors.ts +97 -12
- package/src/access/field-access.test.ts +312 -2
- package/src/access/field-access.ts +118 -42
- package/src/access/field-transforms.ts +8 -39
- package/src/access/field-visibility.ts +99 -45
- package/src/access/index.ts +17 -5
- package/src/access/query-validation.ts +359 -0
- package/src/access/relationship-count.ts +3 -11
- package/src/access/relationship-label-filter.test.ts +19 -68
- package/src/access/relationship-label-filter.ts +27 -104
- package/src/access/transaction-registry.ts +58 -0
- package/src/access/types.ts +83 -86
- package/src/config/index.ts +10 -22
- package/src/config/label.ts +6 -13
- package/src/config/nav-count.ts +11 -19
- package/src/config/plugin-engine.ts +1 -37
- package/src/config/types.ts +95 -110
- package/src/context/apply-defaults.ts +12 -44
- package/src/context/hook-pipeline.ts +12 -67
- package/src/context/index.ts +311 -276
- package/src/context/nested-operations.ts +94 -226
- package/src/context/transaction-boundary.ts +113 -109
- package/src/context/write-pipeline.ts +99 -155
- package/src/fields/format-prisma-default.ts +4 -7
- package/src/fields/index.ts +155 -147
- package/src/filter/collect.ts +48 -12
- package/src/filter/filter.test.ts +153 -15
- package/src/filter/index.ts +1 -0
- package/src/filter/map.ts +0 -5
- package/src/filter/parse.ts +2 -9
- package/src/filter/serialize.ts +2 -4
- package/src/hooks/index.ts +5 -59
- package/src/index.ts +23 -4
- package/src/internal.ts +5 -0
- package/src/lib/case-utils.ts +0 -32
- package/src/mcp/handler.ts +13 -71
- package/src/query/index.ts +82 -235
- package/src/query/relationship-options.ts +1 -1
- package/src/utils/password.ts +7 -76
- package/src/validation/field-config.ts +0 -6
- package/src/validation/schema.ts +0 -10
- package/tests/access-relationships.test.ts +18 -16
- package/tests/computed-field-selective-evaluation.test.ts +418 -0
- package/tests/context.test.ts +727 -1
- package/tests/field-types.test.ts +222 -0
- package/tests/mcp-handler.test.ts +34 -0
- package/tests/needs-declared-dependencies.test.ts +7 -4
- package/tests/resolve-chain.test.ts +11 -11
- package/tests/transaction-boundary-outermost.test.ts +530 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -9,53 +9,36 @@ import {
|
|
|
9
9
|
type TransactionOutcome,
|
|
10
10
|
} from '../hooks/index.js'
|
|
11
11
|
import type { WriteOperation } from './write-pipeline.js'
|
|
12
|
+
import type {
|
|
13
|
+
TransactionRegistry,
|
|
14
|
+
TransactionSettleOutcome,
|
|
15
|
+
} from '../access/transaction-registry.js'
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
|
-
* Transaction-boundary hooks (#590 / ADR-0010)
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* for non-transactional side effects (e.g. external API calls) that must not
|
|
19
|
-
* hold a DB transaction open and cannot be rolled back. The pair forms a
|
|
20
|
-
* compensation bracket around the atomic write described by ADR-0010.
|
|
21
|
-
*
|
|
22
|
-
* This module:
|
|
23
|
-
* 1. Enumerates the lists involved in a write up front, BY WALKING THE INPUT
|
|
24
|
-
* TREE only (no DB reads), so the bracket can run per involved list before
|
|
25
|
-
* the transaction opens (mirroring how in-transaction before/afterOperation
|
|
26
|
-
* fire per record, but at list granularity).
|
|
27
|
-
* 2. Runs all `beforeTransaction` hooks, tracking exactly which involved lists'
|
|
28
|
-
* `beforeTransaction` ran, then — after the caller settles the transaction —
|
|
29
|
-
* runs `afterTransaction` ONLY for those lists (the symmetric-bracket
|
|
30
|
-
* "always-run" rule), surfacing any hook errors afterward.
|
|
18
|
+
* Transaction-boundary hooks (#590 / ADR-0010): `beforeTransaction`/`afterTransaction`
|
|
19
|
+
* bracket a write's `$transaction` from the outside, for non-transactional side
|
|
20
|
+
* effects. See ADR-0010 for the bracket's design and ADR-0028 for how a joined
|
|
21
|
+
* write's `afterTransaction` defers to the transaction owner.
|
|
31
22
|
*/
|
|
32
23
|
|
|
33
24
|
/**
|
|
34
|
-
* One list involved in a write,
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* lists the per-record persisted row is not reliably recoverable outside the
|
|
40
|
-
* transaction, so they are passed as `undefined` rather than mis-handing the
|
|
41
|
-
* top-level row as if it were the nested row. `originalItem` here is therefore
|
|
42
|
-
* populated only for the top-level update/delete target (the pipeline resolves
|
|
43
|
-
* it before the transaction opens).
|
|
25
|
+
* One list involved in a write, enumerated purely from the input tree (no DB
|
|
26
|
+
* reads). `item`/`originalItem` are populated only for the top-level record
|
|
27
|
+
* (`isTopLevel`) — a nested list's persisted row isn't reliably recoverable
|
|
28
|
+
* outside the transaction, so handing it the top-level row instead would be
|
|
29
|
+
* silently wrong.
|
|
44
30
|
*/
|
|
45
31
|
export interface InvolvedList {
|
|
46
32
|
listKey: string
|
|
47
33
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
48
34
|
listConfig: ListConfig<any>
|
|
49
35
|
operation: WriteOperation
|
|
50
|
-
/** Whether this is the top-level write target (the only list with a reliable persisted row). */
|
|
51
36
|
isTopLevel: boolean
|
|
52
|
-
/**
|
|
37
|
+
/** `undefined` for delete, which has no input payload. */
|
|
53
38
|
inputData: Record<string, unknown> | undefined
|
|
54
|
-
/** The existing row for the TOP-LEVEL update/delete target; `undefined` otherwise. */
|
|
55
39
|
originalItem: Record<string, unknown> | undefined
|
|
56
40
|
}
|
|
57
41
|
|
|
58
|
-
/** Nested-op kinds whose payloads imply an involved list + operation. */
|
|
59
42
|
const NESTED_OP_OPERATIONS: ReadonlyArray<{ kind: string; operation: WriteOperation }> = [
|
|
60
43
|
{ kind: 'create', operation: 'create' },
|
|
61
44
|
{ kind: 'update', operation: 'update' },
|
|
@@ -64,7 +47,6 @@ const NESTED_OP_OPERATIONS: ReadonlyArray<{ kind: string; operation: WriteOperat
|
|
|
64
47
|
{ kind: 'connectOrCreate', operation: 'create' },
|
|
65
48
|
]
|
|
66
49
|
|
|
67
|
-
/** Distinct dedupe-key operations `NESTED_OP_OPERATIONS` can produce (create/update/delete). */
|
|
68
50
|
const DISTINCT_OPERATION_COUNT = new Set(NESTED_OP_OPERATIONS.map((o) => o.operation)).size
|
|
69
51
|
|
|
70
52
|
function isRelationshipField(fieldConfig: FieldConfig | undefined): boolean {
|
|
@@ -72,17 +54,12 @@ function isRelationshipField(fieldConfig: FieldConfig | undefined): boolean {
|
|
|
72
54
|
}
|
|
73
55
|
|
|
74
56
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* pairs, no further pair can be new, so it stops descending. This replaces
|
|
82
|
-
* the old depth cap as the cost bound (#835) — a payload nesting the config's
|
|
83
|
-
* lists more deeply than any previous cap no longer loses their
|
|
84
|
-
* transaction-boundary hooks, while a payload that repeats the same few
|
|
85
|
-
* lists still terminates promptly instead of walking every entry.
|
|
57
|
+
* Distinct (listKey, operation) pairs reachable from `startListName` via the
|
|
58
|
+
* CONFIG's relationship graph (not the payload) — the saturation bound
|
|
59
|
+
* `walkNested` stops at once it has recorded this many. Bounding by reachable
|
|
60
|
+
* pairs rather than a depth cap (#835) avoids losing hooks for payloads
|
|
61
|
+
* nested deeper than any fixed cap, while still terminating promptly on
|
|
62
|
+
* payloads that repeat the same few lists.
|
|
86
63
|
*/
|
|
87
64
|
function countReachableInvolvementPairs(
|
|
88
65
|
startListName: string,
|
|
@@ -114,15 +91,7 @@ function asRecordArray(value: unknown): Array<Record<string, unknown>> {
|
|
|
114
91
|
return []
|
|
115
92
|
}
|
|
116
93
|
|
|
117
|
-
/**
|
|
118
|
-
* Extract the create/update payload from a nested-op entry so nested
|
|
119
|
-
* `beforeTransaction` receives meaningful `inputData`.
|
|
120
|
-
*
|
|
121
|
-
* - `create`: the entry itself is the create data.
|
|
122
|
-
* - `update`: the entry's `data`.
|
|
123
|
-
* - `connectOrCreate`: the entry's `create`.
|
|
124
|
-
* - `delete`: no input payload.
|
|
125
|
-
*/
|
|
94
|
+
/** Extract a nested-op entry's create/update payload so its `beforeTransaction` receives meaningful `inputData`. */
|
|
126
95
|
function nestedInputData(
|
|
127
96
|
kind: string,
|
|
128
97
|
entry: Record<string, unknown>,
|
|
@@ -142,10 +111,10 @@ function nestedInputData(
|
|
|
142
111
|
|
|
143
112
|
/**
|
|
144
113
|
* Recursively walk a write payload's relationship fields, appending one
|
|
145
|
-
* {@link InvolvedList} per nested create/update/delete involvement. De-dups
|
|
146
|
-
* (listKey, operation)
|
|
147
|
-
*
|
|
148
|
-
*
|
|
114
|
+
* {@link InvolvedList} per nested create/update/delete involvement. De-dups
|
|
115
|
+
* by (listKey, operation): these hooks are a per-LIST compensation bracket,
|
|
116
|
+
* not per-record, so a list with many nested records of the same operation
|
|
117
|
+
* fires its bracket once.
|
|
149
118
|
*/
|
|
150
119
|
function walkNested(
|
|
151
120
|
data: Record<string, unknown> | undefined,
|
|
@@ -155,8 +124,7 @@ function walkNested(
|
|
|
155
124
|
seen: Set<string>,
|
|
156
125
|
maxPairs: number,
|
|
157
126
|
): void {
|
|
158
|
-
//
|
|
159
|
-
// anything new, so stop instead of re-walking the rest of the payload.
|
|
127
|
+
// Saturated: no further pair can be new (see countReachableInvolvementPairs).
|
|
160
128
|
if (!data || seen.size >= maxPairs) return
|
|
161
129
|
|
|
162
130
|
for (const [fieldName, value] of Object.entries(data)) {
|
|
@@ -174,7 +142,6 @@ function walkNested(
|
|
|
174
142
|
if (opValue === undefined) continue
|
|
175
143
|
|
|
176
144
|
const entries = asRecordArray(opValue)
|
|
177
|
-
// Record the involvement once per (list, operation).
|
|
178
145
|
const dedupeKey = `${relatedListName}:${operation}`
|
|
179
146
|
if (!seen.has(dedupeKey)) {
|
|
180
147
|
seen.add(dedupeKey)
|
|
@@ -190,7 +157,6 @@ function walkNested(
|
|
|
190
157
|
|
|
191
158
|
if (seen.size >= maxPairs) return
|
|
192
159
|
|
|
193
|
-
// Recurse into each nested entry's own relationship payload.
|
|
194
160
|
for (const entry of entries) {
|
|
195
161
|
const childData = nestedInputData(kind, entry)
|
|
196
162
|
walkNested(childData, relatedListConfig.fields, config, out, seen, maxPairs)
|
|
@@ -201,8 +167,8 @@ function walkNested(
|
|
|
201
167
|
|
|
202
168
|
/**
|
|
203
169
|
* Enumerate the lists involved in a write — the top-level list plus every
|
|
204
|
-
* nested create/update/delete target
|
|
205
|
-
*
|
|
170
|
+
* nested create/update/delete target — without DB reads. The top-level list
|
|
171
|
+
* is always first.
|
|
206
172
|
*/
|
|
207
173
|
export function enumerateInvolvedLists(args: {
|
|
208
174
|
listName: string
|
|
@@ -229,16 +195,12 @@ export function enumerateInvolvedLists(args: {
|
|
|
229
195
|
const seen = new Set<string>([`${listName}:${operation}`])
|
|
230
196
|
const maxPairs = countReachableInvolvementPairs(listName, listConfig, config)
|
|
231
197
|
|
|
232
|
-
// Delete has no nested payload to walk (inputData is undefined).
|
|
233
198
|
walkNested(inputData, listConfig.fields, config, out, seen, maxPairs)
|
|
234
199
|
|
|
235
200
|
return out
|
|
236
201
|
}
|
|
237
202
|
|
|
238
|
-
/**
|
|
239
|
-
* Run the list- and field-level `beforeTransaction` hooks for one involved list.
|
|
240
|
-
* A throw propagates to the caller (which aborts the write).
|
|
241
|
-
*/
|
|
203
|
+
/** Runs one involved list's `beforeTransaction` hooks. A throw propagates to the caller (which aborts the write). */
|
|
242
204
|
async function runBeforeTransactionForList<TPrisma extends PrismaClientLike>(
|
|
243
205
|
involved: InvolvedList,
|
|
244
206
|
context: AccessContext<TPrisma>,
|
|
@@ -280,11 +242,13 @@ async function runBeforeTransactionForList<TPrisma extends PrismaClientLike>(
|
|
|
280
242
|
}
|
|
281
243
|
|
|
282
244
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
245
|
+
* Runs one involved list's `afterTransaction` hooks against the settled
|
|
246
|
+
* {@link TransactionOutcome}, collecting rather than throwing errors so the
|
|
247
|
+
* caller can keep running the remaining lists' compensators. Exported for
|
|
248
|
+
* {@link TransactionRegistry} to reuse when draining a deferred, joined
|
|
249
|
+
* write's bracket (ADR-0028).
|
|
286
250
|
*/
|
|
287
|
-
async function runAfterTransactionForList<TPrisma extends PrismaClientLike>(
|
|
251
|
+
export async function runAfterTransactionForList<TPrisma extends PrismaClientLike>(
|
|
288
252
|
involved: InvolvedList,
|
|
289
253
|
outcome: TransactionOutcome,
|
|
290
254
|
context: AccessContext<TPrisma>,
|
|
@@ -292,15 +256,13 @@ async function runAfterTransactionForList<TPrisma extends PrismaClientLike>(
|
|
|
292
256
|
): Promise<void> {
|
|
293
257
|
const { listKey, listConfig, operation, isTopLevel, inputData, originalItem } = involved
|
|
294
258
|
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
// in-transaction `afterOperation`, which already receives the correct nested row.
|
|
259
|
+
// The persisted row (`outcome.item`) is the TOP-LEVEL row only. Handing it to
|
|
260
|
+
// a nested list's hook would silently mis-type as that list's own item — for
|
|
261
|
+
// nested lists `item`/`originalItem` stay `undefined`; per-record nested
|
|
262
|
+
// compensation belongs in the in-transaction `afterOperation`, which gets the
|
|
263
|
+
// correct row.
|
|
301
264
|
try {
|
|
302
265
|
if (outcome.status === 'committed') {
|
|
303
|
-
// The persisted row is surfaced only for the top-level list (see above).
|
|
304
266
|
const committedItem = isTopLevel ? outcome.item : undefined
|
|
305
267
|
if (operation === 'create') {
|
|
306
268
|
await executeAfterTransaction(listConfig.hooks, {
|
|
@@ -331,7 +293,6 @@ async function runAfterTransactionForList<TPrisma extends PrismaClientLike>(
|
|
|
331
293
|
})
|
|
332
294
|
}
|
|
333
295
|
} else {
|
|
334
|
-
// rolled-back: no persisted item.
|
|
335
296
|
if (operation === 'create') {
|
|
336
297
|
await executeAfterTransaction(listConfig.hooks, {
|
|
337
298
|
listKey,
|
|
@@ -401,34 +362,47 @@ export class AfterTransactionError extends Error {
|
|
|
401
362
|
}
|
|
402
363
|
|
|
403
364
|
/**
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
365
|
+
* Resolves a joined write's final outcome once the owner's settle is known
|
|
366
|
+
* (ADR-0028): the write's own error always wins; otherwise committed iff the
|
|
367
|
+
* owner's transaction also committed.
|
|
368
|
+
*/
|
|
369
|
+
function resolveDeferredOutcome(
|
|
370
|
+
writeOutcome: TransactionOutcome,
|
|
371
|
+
settle: TransactionSettleOutcome,
|
|
372
|
+
): TransactionOutcome {
|
|
373
|
+
if (writeOutcome.status === 'rolled-back') return writeOutcome
|
|
374
|
+
if (settle.status === 'committed') return writeOutcome
|
|
375
|
+
return { status: 'rolled-back', error: settle.error }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Brackets a write's transaction with the transaction-boundary hooks (#590,
|
|
380
|
+
* ADR-0028 / #899): runs every involved list's `beforeTransaction` eagerly —
|
|
381
|
+
* a throw here aborts before `runTransaction` (#569) ever opens the write's
|
|
382
|
+
* transaction — then routes `afterTransaction` by ownership: deferred onto
|
|
383
|
+
* `args.joinedOwner`'s {@link TransactionRegistry} for a joined write, run
|
|
384
|
+
* eagerly (draining `args.ownedRegistry`) for the write that opened the
|
|
385
|
+
* transaction, or run eagerly with neither set. See ADR-0028 for why. A
|
|
386
|
+
* joined write's `afterTransaction` errors therefore surface as an
|
|
387
|
+
* {@link AfterTransactionError} from the OWNER's promise, not this write's.
|
|
418
388
|
*
|
|
419
|
-
* Sudo
|
|
389
|
+
* Sudo bypasses access control only — never these hooks.
|
|
420
390
|
*/
|
|
421
391
|
export async function runWithTransactionBoundary<TPrisma extends PrismaClientLike>(args: {
|
|
422
392
|
involvedLists: InvolvedList[]
|
|
423
393
|
context: AccessContext<TPrisma>
|
|
394
|
+
/** Set when this write is nested in a transaction it did not open (ADR-0028). */
|
|
395
|
+
joinedOwner?: TransactionRegistry
|
|
396
|
+
/** Set when this write just opened the transaction joined writes below it share. */
|
|
397
|
+
ownedRegistry?: TransactionRegistry
|
|
424
398
|
runTransaction: () => Promise<Record<string, unknown> | null>
|
|
425
399
|
}): Promise<Record<string, unknown> | null> {
|
|
426
|
-
const { involvedLists, context, runTransaction } = args
|
|
400
|
+
const { involvedLists, context, joinedOwner, ownedRegistry, runTransaction } = args
|
|
427
401
|
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
402
|
+
// A list counts as "ran" the moment its beforeTransaction BEGINS (pushed
|
|
403
|
+
// before the try below), not on success — so a list whose beforeTransaction
|
|
404
|
+
// itself throws still gets its afterTransaction, in case it took a partial
|
|
405
|
+
// external action that needs compensating.
|
|
432
406
|
const ran: InvolvedList[] = []
|
|
433
407
|
|
|
434
408
|
let beforeError: unknown
|
|
@@ -442,18 +416,29 @@ export async function runWithTransactionBoundary<TPrisma extends PrismaClientLik
|
|
|
442
416
|
}
|
|
443
417
|
}
|
|
444
418
|
|
|
445
|
-
//
|
|
446
|
-
//
|
|
419
|
+
// Abort path: never open the transaction; compensate the lists that ran,
|
|
420
|
+
// then rethrow the original error.
|
|
447
421
|
if (beforeError !== undefined) {
|
|
448
422
|
const outcome: TransactionOutcome = { status: 'rolled-back', error: beforeError }
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
423
|
+
if (joinedOwner) {
|
|
424
|
+
// Discards any afterTransaction errors on this path — only beforeError
|
|
425
|
+
// propagates, matching the eager branch below.
|
|
426
|
+
joinedOwner.enqueue(async (_settle, _errors) => {
|
|
427
|
+
const discarded: unknown[] = []
|
|
428
|
+
for (const involved of ran) {
|
|
429
|
+
await runAfterTransactionForList(involved, outcome, context, discarded)
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
} else {
|
|
433
|
+
const afterErrors: unknown[] = []
|
|
434
|
+
for (const involved of ran) {
|
|
435
|
+
await runAfterTransactionForList(involved, outcome, context, afterErrors)
|
|
436
|
+
}
|
|
452
437
|
}
|
|
453
438
|
throw beforeError
|
|
454
439
|
}
|
|
455
440
|
|
|
456
|
-
// Open the transaction and capture
|
|
441
|
+
// Open (or join) the transaction and capture this write's own settle outcome.
|
|
457
442
|
let outcome: TransactionOutcome
|
|
458
443
|
let result: Record<string, unknown> | null = null
|
|
459
444
|
let txError: unknown
|
|
@@ -465,15 +450,34 @@ export async function runWithTransactionBoundary<TPrisma extends PrismaClientLik
|
|
|
465
450
|
outcome = { status: 'rolled-back', error: err }
|
|
466
451
|
}
|
|
467
452
|
|
|
468
|
-
|
|
469
|
-
|
|
453
|
+
if (joinedOwner) {
|
|
454
|
+
// Deferred (ADR-0028) — only afterTransaction waits; this write's own
|
|
455
|
+
// result/throw is returned/thrown normally below.
|
|
456
|
+
joinedOwner.enqueue(async (settle, errors) => {
|
|
457
|
+
const finalOutcome = resolveDeferredOutcome(outcome, settle)
|
|
458
|
+
for (const involved of ran) {
|
|
459
|
+
await runAfterTransactionForList(involved, finalOutcome, context, errors)
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
if (txError !== undefined) throw txError
|
|
463
|
+
return result
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// All compensators run even if one throws.
|
|
470
467
|
const afterErrors: unknown[] = []
|
|
471
468
|
for (const involved of ran) {
|
|
472
469
|
await runAfterTransactionForList(involved, outcome, context, afterErrors)
|
|
473
470
|
}
|
|
474
471
|
|
|
475
|
-
//
|
|
476
|
-
//
|
|
472
|
+
// Owner: drain joined writes' deferred brackets with this write's own settle
|
|
473
|
+
// outcome (ADR-0028).
|
|
474
|
+
if (ownedRegistry) {
|
|
475
|
+
const settle: TransactionSettleOutcome =
|
|
476
|
+
txError !== undefined ? { status: 'rolled-back', error: txError } : { status: 'committed' }
|
|
477
|
+
await ownedRegistry.drain(settle, afterErrors)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Transaction error takes precedence over afterTransaction errors (ADR-0028).
|
|
477
481
|
if (txError !== undefined) throw txError
|
|
478
482
|
if (afterErrors.length > 0) throw new AfterTransactionError(afterErrors)
|
|
479
483
|
|