@fragno-dev/db 0.2.0 → 0.2.2
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 +34 -30
- package/CHANGELOG.md +49 -0
- package/dist/adapters/generic-sql/query/where-builder.js +1 -1
- package/dist/db-fragment-definition-builder.d.ts +31 -39
- package/dist/db-fragment-definition-builder.d.ts.map +1 -1
- package/dist/db-fragment-definition-builder.js +20 -16
- package/dist/db-fragment-definition-builder.js.map +1 -1
- package/dist/fragments/internal-fragment.d.ts +94 -8
- package/dist/fragments/internal-fragment.d.ts.map +1 -1
- package/dist/fragments/internal-fragment.js +56 -55
- package/dist/fragments/internal-fragment.js.map +1 -1
- package/dist/hooks/hooks.d.ts +5 -3
- package/dist/hooks/hooks.d.ts.map +1 -1
- package/dist/hooks/hooks.js +38 -37
- package/dist/hooks/hooks.js.map +1 -1
- package/dist/mod.d.ts +3 -3
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +4 -4
- package/dist/mod.js.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.d.ts +367 -80
- package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.js +448 -148
- package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.d.ts +35 -11
- package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.js +49 -19
- package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
- package/dist/query/value-decoding.js +1 -1
- package/dist/schema/create.d.ts +2 -3
- package/dist/schema/create.d.ts.map +1 -1
- package/dist/schema/create.js +2 -5
- package/dist/schema/create.js.map +1 -1
- package/dist/schema/generate-id.d.ts +20 -0
- package/dist/schema/generate-id.d.ts.map +1 -0
- package/dist/schema/generate-id.js +28 -0
- package/dist/schema/generate-id.js.map +1 -0
- package/dist/sql-driver/dialects/durable-object-dialect.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +1 -0
- package/src/adapters/drizzle/drizzle-adapter-sqlite3.test.ts +41 -25
- package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +39 -25
- package/src/db-fragment-definition-builder.test.ts +58 -42
- package/src/db-fragment-definition-builder.ts +78 -88
- package/src/db-fragment-instantiator.test.ts +64 -88
- package/src/db-fragment-integration.test.ts +292 -142
- package/src/fragments/internal-fragment.test.ts +272 -266
- package/src/fragments/internal-fragment.ts +155 -122
- package/src/hooks/hooks.test.ts +268 -264
- package/src/hooks/hooks.ts +74 -63
- package/src/mod.ts +14 -4
- package/src/query/unit-of-work/execute-unit-of-work.test.ts +1582 -998
- package/src/query/unit-of-work/execute-unit-of-work.ts +1746 -343
- package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
- package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +269 -21
- package/src/query/unit-of-work/unit-of-work.test.ts +64 -0
- package/src/query/unit-of-work/unit-of-work.ts +65 -30
- package/src/schema/create.ts +2 -5
- package/src/schema/generate-id.test.ts +57 -0
- package/src/schema/generate-id.ts +38 -0
- package/src/shared/config.ts +0 -10
- package/src/shared/connection-pool.ts +0 -24
- package/src/shared/prisma.ts +0 -45
|
@@ -1,333 +1,481 @@
|
|
|
1
1
|
import type { AnySchema } from "../../schema/create";
|
|
2
2
|
import type { TypedUnitOfWork, IUnitOfWork } from "./unit-of-work";
|
|
3
3
|
import type { HooksMap } from "../../hooks/hooks";
|
|
4
|
-
import {
|
|
5
|
-
import type { FragnoId } from "../../schema/create";
|
|
4
|
+
import { ExponentialBackoffRetryPolicy, type RetryPolicy } from "./retry-policy";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
9
|
-
* This error triggers automatic retry behavior in executeRestrictedUnitOfWork.
|
|
7
|
+
* Symbol to identify TxResult objects
|
|
10
8
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const TX_RESULT_BRAND = Symbol("TxResult");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if a value is a TxResult
|
|
13
|
+
*/
|
|
14
|
+
export function isTxResult(value: unknown): value is TxResult<unknown> {
|
|
15
|
+
return (
|
|
16
|
+
value !== null &&
|
|
17
|
+
typeof value === "object" &&
|
|
18
|
+
TX_RESULT_BRAND in value &&
|
|
19
|
+
(value as Record<symbol, boolean>)[TX_RESULT_BRAND] === true
|
|
20
|
+
);
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
24
|
+
* Extract the retrieve success result type from a TxResult.
|
|
25
|
+
* If the TxResult has retrieveSuccess, returns its return type.
|
|
26
|
+
* Otherwise returns the raw retrieve results type.
|
|
27
|
+
* Handles undefined (for optional service patterns like optionalService?.method()).
|
|
21
28
|
*/
|
|
22
|
-
export type
|
|
23
|
-
|
|
24
|
-
T extends
|
|
25
|
-
?
|
|
26
|
-
:
|
|
27
|
-
T extends readonly [unknown, ...unknown[]]
|
|
28
|
-
? { [K in keyof T]: AwaitedPromisesInObject<T[K]> }
|
|
29
|
-
: T extends [unknown, ...unknown[]]
|
|
30
|
-
? { [K in keyof T]: AwaitedPromisesInObject<T[K]> }
|
|
31
|
-
: // Check for regular arrays (unknown length)
|
|
32
|
-
T extends (infer U)[]
|
|
33
|
-
? Awaited<U>[]
|
|
34
|
-
: T extends readonly (infer U)[]
|
|
35
|
-
? readonly Awaited<U>[]
|
|
36
|
-
: // Check for objects
|
|
37
|
-
T extends Record<string, unknown>
|
|
38
|
-
? {
|
|
39
|
-
[K in keyof T]: T[K] extends Promise<infer U> ? Awaited<U> : T[K];
|
|
40
|
-
}
|
|
41
|
-
: // Otherwise return as-is
|
|
42
|
-
T;
|
|
29
|
+
export type ExtractTxRetrieveSuccessResult<T> = T extends undefined
|
|
30
|
+
? undefined
|
|
31
|
+
: T extends TxResult<unknown, infer R>
|
|
32
|
+
? R
|
|
33
|
+
: never;
|
|
43
34
|
|
|
44
35
|
/**
|
|
45
|
-
*
|
|
36
|
+
* Extract the final result type from a TxResult.
|
|
37
|
+
* Handles undefined (for optional service patterns like optionalService?.method()).
|
|
46
38
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
export type ExtractTxFinalResult<T> = T extends undefined
|
|
40
|
+
? undefined
|
|
41
|
+
: T extends TxResult<infer R, infer _>
|
|
42
|
+
? R
|
|
43
|
+
: Awaited<T>;
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Map over service calls array to extract retrieve success results from each service call.
|
|
47
|
+
* Preserves tuple structure while extracting the retrieve success result type from each element.
|
|
48
|
+
*/
|
|
49
|
+
export type ExtractServiceRetrieveResults<T extends readonly unknown[]> = {
|
|
50
|
+
[K in keyof T]: ExtractTxRetrieveSuccessResult<T[K]>;
|
|
51
|
+
};
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Map over service calls array to extract final results from each service call.
|
|
55
|
+
* Preserves tuple structure while extracting the final result type from each element.
|
|
56
|
+
*/
|
|
57
|
+
export type ExtractServiceFinalResults<T extends readonly unknown[]> = {
|
|
58
|
+
[K in keyof T]: ExtractTxFinalResult<T[K]>;
|
|
59
|
+
};
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Context passed to mutate callback for service methods
|
|
63
|
+
*/
|
|
64
|
+
export interface ServiceTxMutateContext<
|
|
65
|
+
TSchema extends AnySchema,
|
|
66
|
+
TRetrieveSuccessResult,
|
|
67
|
+
TServiceRetrieveResults extends readonly unknown[],
|
|
68
|
+
THooks extends HooksMap,
|
|
69
|
+
> {
|
|
70
|
+
/** Unit of work for scheduling mutations */
|
|
71
|
+
uow: TypedUnitOfWork<TSchema, [], unknown, THooks>;
|
|
72
|
+
/** Result from retrieveSuccess callback (or raw retrieve results if no retrieveSuccess) */
|
|
73
|
+
retrieveResult: TRetrieveSuccessResult;
|
|
74
|
+
/** Array of retrieve success results from service calls (intermediate results, not final) */
|
|
75
|
+
serviceIntermediateResult: TServiceRetrieveResults;
|
|
76
|
+
}
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
+
/**
|
|
79
|
+
* Context passed to handler-level callbacks
|
|
80
|
+
*/
|
|
81
|
+
export interface HandlerTxContext<THooks extends HooksMap> {
|
|
82
|
+
/** Get a typed Unit of Work for the given schema */
|
|
83
|
+
forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
|
|
84
|
+
schema: S,
|
|
85
|
+
hooks?: H,
|
|
86
|
+
) => TypedUnitOfWork<S, [], unknown, H>;
|
|
87
|
+
/** Unique key for this transaction attempt (for idempotency/deduplication) */
|
|
88
|
+
idempotencyKey: string;
|
|
89
|
+
/** Current attempt number (0-based) */
|
|
90
|
+
currentAttempt: number;
|
|
91
|
+
}
|
|
78
92
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Context passed to handler mutate callback
|
|
95
|
+
*/
|
|
96
|
+
export interface HandlerTxMutateContext<
|
|
97
|
+
TRetrieveSuccessResult,
|
|
98
|
+
TServiceRetrieveResults extends readonly unknown[],
|
|
99
|
+
THooks extends HooksMap,
|
|
100
|
+
> extends HandlerTxContext<THooks> {
|
|
101
|
+
/** Result from retrieveSuccess callback (or raw retrieve results if no retrieveSuccess) */
|
|
102
|
+
retrieveResult: TRetrieveSuccessResult;
|
|
103
|
+
/** Array of retrieve success results from service calls (intermediate results, not final) */
|
|
104
|
+
serviceIntermediateResult: TServiceRetrieveResults;
|
|
105
|
+
}
|
|
82
106
|
|
|
83
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Context passed to success callback when mutate IS provided
|
|
109
|
+
*/
|
|
110
|
+
export interface TxSuccessContextWithMutate<
|
|
111
|
+
TRetrieveSuccessResult,
|
|
112
|
+
TMutateResult,
|
|
113
|
+
TServiceFinalResults extends readonly unknown[],
|
|
114
|
+
TServiceRetrieveResults extends readonly unknown[],
|
|
115
|
+
> {
|
|
116
|
+
/** Result from retrieveSuccess callback (or raw retrieve results if no retrieveSuccess) */
|
|
117
|
+
retrieveResult: TRetrieveSuccessResult;
|
|
118
|
+
/** Result from mutate callback */
|
|
119
|
+
mutateResult: TMutateResult;
|
|
120
|
+
/** Array of final results from service calls */
|
|
121
|
+
serviceResult: TServiceFinalResults;
|
|
122
|
+
/** Array of retrieve success results from service calls (same as what mutate receives) */
|
|
123
|
+
serviceIntermediateResult: TServiceRetrieveResults;
|
|
84
124
|
}
|
|
85
125
|
|
|
86
126
|
/**
|
|
87
|
-
*
|
|
88
|
-
* Promises in mutationResult are unwrapped 1 level deep
|
|
127
|
+
* Context passed to success callback when mutate is NOT provided
|
|
89
128
|
*/
|
|
90
|
-
export
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
129
|
+
export interface TxSuccessContextWithoutMutate<
|
|
130
|
+
TRetrieveSuccessResult,
|
|
131
|
+
TServiceFinalResults extends readonly unknown[],
|
|
132
|
+
TServiceRetrieveResults extends readonly unknown[],
|
|
133
|
+
> {
|
|
134
|
+
/** Result from retrieveSuccess callback (or raw retrieve results if no retrieveSuccess) */
|
|
135
|
+
retrieveResult: TRetrieveSuccessResult;
|
|
136
|
+
/** No mutate callback was provided */
|
|
137
|
+
mutateResult: undefined;
|
|
138
|
+
/** Array of final results from service calls */
|
|
139
|
+
serviceResult: TServiceFinalResults;
|
|
140
|
+
/** Array of retrieve success results from service calls (same as what mutate receives) */
|
|
141
|
+
serviceIntermediateResult: TServiceRetrieveResults;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Context passed to success callback.
|
|
146
|
+
* Union of TxSuccessContextWithMutate and TxSuccessContextWithoutMutate to handle
|
|
147
|
+
* both cases in a single callback signature.
|
|
148
|
+
*/
|
|
149
|
+
export type TxSuccessContext<
|
|
150
|
+
TRetrieveSuccessResult,
|
|
151
|
+
TMutateResult,
|
|
152
|
+
TServiceFinalResults extends readonly unknown[],
|
|
153
|
+
TServiceRetrieveResults extends readonly unknown[] = readonly unknown[],
|
|
154
|
+
> =
|
|
155
|
+
| TxSuccessContextWithMutate<
|
|
156
|
+
TRetrieveSuccessResult,
|
|
157
|
+
TMutateResult,
|
|
158
|
+
TServiceFinalResults,
|
|
159
|
+
TServiceRetrieveResults
|
|
160
|
+
>
|
|
161
|
+
| TxSuccessContextWithoutMutate<
|
|
162
|
+
TRetrieveSuccessResult,
|
|
163
|
+
TServiceFinalResults,
|
|
164
|
+
TServiceRetrieveResults
|
|
165
|
+
>;
|
|
111
166
|
|
|
112
167
|
/**
|
|
113
|
-
* Callbacks for
|
|
168
|
+
* Callbacks for service-level TxResult.
|
|
169
|
+
*
|
|
170
|
+
* Return type priority:
|
|
171
|
+
* 1. If success exists: ReturnType<success>
|
|
172
|
+
* 2. Else if mutate exists: ReturnType<mutate>
|
|
173
|
+
* 3. Else if retrieveSuccess exists: ReturnType<retrieveSuccess>
|
|
174
|
+
* 4. Else if retrieve exists: TRetrieveResults
|
|
175
|
+
* 5. Else: serviceResult array type
|
|
114
176
|
*/
|
|
115
|
-
export interface
|
|
177
|
+
export interface ServiceTxCallbacks<
|
|
116
178
|
TSchema extends AnySchema,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
179
|
+
TRetrieveResults extends unknown[],
|
|
180
|
+
TRetrieveSuccessResult,
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
182
|
+
TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
|
|
183
|
+
TMutateResult,
|
|
184
|
+
TSuccessResult,
|
|
185
|
+
THooks extends HooksMap,
|
|
120
186
|
> {
|
|
121
187
|
/**
|
|
122
|
-
*
|
|
188
|
+
* Service calls - other TxResults to execute first.
|
|
189
|
+
*/
|
|
190
|
+
serviceCalls?: () => TServiceCalls;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Retrieval phase callback - schedules retrieval operations.
|
|
123
194
|
*/
|
|
124
195
|
retrieve?: (
|
|
125
|
-
uow: TypedUnitOfWork<TSchema, [],
|
|
126
|
-
) => TypedUnitOfWork<TSchema,
|
|
196
|
+
uow: TypedUnitOfWork<TSchema, [], unknown, THooks>,
|
|
197
|
+
) => TypedUnitOfWork<TSchema, TRetrieveResults, unknown, THooks>;
|
|
127
198
|
|
|
128
199
|
/**
|
|
129
|
-
*
|
|
200
|
+
* Transform retrieve results before passing to mutate.
|
|
201
|
+
*/
|
|
202
|
+
retrieveSuccess?: (
|
|
203
|
+
retrieveResult: TRetrieveResults,
|
|
204
|
+
serviceResult: ExtractServiceRetrieveResults<TServiceCalls>,
|
|
205
|
+
) => TRetrieveSuccessResult;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Mutation phase callback - schedules mutations based on retrieve results.
|
|
130
209
|
*/
|
|
131
210
|
mutate?: (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
211
|
+
ctx: ServiceTxMutateContext<
|
|
212
|
+
TSchema,
|
|
213
|
+
TRetrieveSuccessResult,
|
|
214
|
+
ExtractServiceRetrieveResults<TServiceCalls>,
|
|
215
|
+
THooks
|
|
216
|
+
>,
|
|
217
|
+
) => TMutateResult;
|
|
135
218
|
|
|
136
219
|
/**
|
|
137
|
-
* Success callback -
|
|
138
|
-
* Promises in mutationResult are already unwrapped 1 level deep
|
|
220
|
+
* Success callback - final transformation after mutations complete.
|
|
139
221
|
*/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
222
|
+
success?: (
|
|
223
|
+
ctx: TxSuccessContext<
|
|
224
|
+
TRetrieveSuccessResult,
|
|
225
|
+
TMutateResult,
|
|
226
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
227
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
228
|
+
>,
|
|
229
|
+
) => TSuccessResult;
|
|
146
230
|
}
|
|
147
231
|
|
|
148
232
|
/**
|
|
149
|
-
*
|
|
233
|
+
* Callbacks for handler-level executeTx.
|
|
234
|
+
* Uses context-based callbacks that provide forSchema() method.
|
|
150
235
|
*/
|
|
151
|
-
export interface
|
|
236
|
+
export interface HandlerTxCallbacks<
|
|
237
|
+
TRetrieveResults extends unknown[],
|
|
238
|
+
TRetrieveSuccessResult,
|
|
239
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
240
|
+
TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
|
|
241
|
+
TMutateResult,
|
|
242
|
+
TSuccessResult,
|
|
243
|
+
THooks extends HooksMap,
|
|
244
|
+
> {
|
|
152
245
|
/**
|
|
153
|
-
*
|
|
246
|
+
* Service calls - other TxResults to execute first.
|
|
154
247
|
*/
|
|
155
|
-
|
|
248
|
+
serviceCalls?: () => TServiceCalls;
|
|
156
249
|
|
|
157
250
|
/**
|
|
158
|
-
*
|
|
251
|
+
* Retrieval phase callback - schedules retrieval operations using context.forSchema().
|
|
252
|
+
* Return a TypedUnitOfWork to get typed results, or void for no retrieval.
|
|
159
253
|
*/
|
|
160
|
-
|
|
254
|
+
retrieve?: (
|
|
255
|
+
context: HandlerTxContext<THooks>,
|
|
256
|
+
) => TypedUnitOfWork<AnySchema, TRetrieveResults, unknown, HooksMap> | void;
|
|
161
257
|
|
|
162
258
|
/**
|
|
163
|
-
*
|
|
259
|
+
* Transform retrieve results before passing to mutate.
|
|
164
260
|
*/
|
|
165
|
-
|
|
261
|
+
retrieveSuccess?: (
|
|
262
|
+
retrieveResult: TRetrieveResults,
|
|
263
|
+
serviceResult: ExtractServiceRetrieveResults<TServiceCalls>,
|
|
264
|
+
) => TRetrieveSuccessResult;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Mutation phase callback - schedules mutations based on retrieve results.
|
|
268
|
+
*/
|
|
269
|
+
mutate?: (
|
|
270
|
+
ctx: HandlerTxMutateContext<
|
|
271
|
+
TRetrieveSuccessResult,
|
|
272
|
+
ExtractServiceRetrieveResults<TServiceCalls>,
|
|
273
|
+
THooks
|
|
274
|
+
>,
|
|
275
|
+
) => TMutateResult;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Success callback - final transformation after mutations complete.
|
|
279
|
+
*/
|
|
280
|
+
success?: (
|
|
281
|
+
ctx: TxSuccessContext<
|
|
282
|
+
TRetrieveSuccessResult,
|
|
283
|
+
TMutateResult,
|
|
284
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
285
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
286
|
+
>,
|
|
287
|
+
) => TSuccessResult;
|
|
166
288
|
}
|
|
167
289
|
|
|
168
290
|
/**
|
|
169
|
-
*
|
|
170
|
-
* This is useful for handler contexts where the factory is already known.
|
|
171
|
-
*
|
|
172
|
-
* @param createUnitOfWork - Factory function that creates a fresh UOW instance
|
|
173
|
-
* @returns A bound executeUnitOfWork function that doesn't require the factory parameter
|
|
174
|
-
*
|
|
175
|
-
* @example
|
|
176
|
-
* ```ts
|
|
177
|
-
* const boundExecute = createExecuteUnitOfWork(() => db.createUnitOfWork());
|
|
178
|
-
* const result = await boundExecute({
|
|
179
|
-
* retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
180
|
-
* mutate: (uow, [users]) => {
|
|
181
|
-
* uow.update("users", users[0].id, (b) => b.set({ balance: newBalance }));
|
|
182
|
-
* }
|
|
183
|
-
* });
|
|
184
|
-
* ```
|
|
291
|
+
* Internal structure storing TxResult callbacks and state.
|
|
185
292
|
*/
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
293
|
+
interface TxResultInternal<
|
|
294
|
+
TSchema extends AnySchema,
|
|
295
|
+
TRetrieveResults extends unknown[],
|
|
296
|
+
TRetrieveSuccessResult,
|
|
297
|
+
TServiceCalls extends readonly (TxResult<unknown> | undefined)[],
|
|
298
|
+
TMutateResult,
|
|
299
|
+
TSuccessResult,
|
|
300
|
+
THooks extends HooksMap,
|
|
301
|
+
> {
|
|
302
|
+
schema: TSchema | undefined;
|
|
303
|
+
callbacks: ServiceTxCallbacks<
|
|
304
|
+
TSchema,
|
|
305
|
+
TRetrieveResults,
|
|
306
|
+
TRetrieveSuccessResult,
|
|
307
|
+
TServiceCalls,
|
|
308
|
+
TMutateResult,
|
|
309
|
+
TSuccessResult,
|
|
310
|
+
THooks
|
|
311
|
+
>;
|
|
312
|
+
/** The typed UOW created during retrieve callback */
|
|
313
|
+
typedUow: TypedUnitOfWork<TSchema, TRetrieveResults, unknown, THooks> | undefined;
|
|
314
|
+
/** The restricted UOW for signaling (used when typedUow is undefined) */
|
|
315
|
+
restrictedUow: IUnitOfWork;
|
|
316
|
+
/** Promise that resolves when retrieve phase is complete */
|
|
317
|
+
retrievePhase: Promise<TRetrieveResults>;
|
|
318
|
+
/** Resolve function for retrievePhase */
|
|
319
|
+
resolveRetrievePhase: (results: TRetrieveResults) => void;
|
|
320
|
+
/** Reject function for retrievePhase */
|
|
321
|
+
rejectRetrievePhase: (error: unknown) => void;
|
|
322
|
+
/** Computed retrieve success result (set after retrieveSuccess runs) */
|
|
323
|
+
retrieveSuccessResult: TRetrieveSuccessResult | undefined;
|
|
324
|
+
/** Computed mutate result (set after mutate runs) */
|
|
325
|
+
mutateResult: TMutateResult | undefined;
|
|
326
|
+
/** Computed final result (set after success runs or defaults) */
|
|
327
|
+
finalResult: TSuccessResult | undefined;
|
|
328
|
+
/** Service calls resolved */
|
|
329
|
+
serviceCalls: TServiceCalls | undefined;
|
|
195
330
|
}
|
|
196
331
|
|
|
197
332
|
/**
|
|
198
|
-
*
|
|
333
|
+
* TxResult represents a transaction definition (not yet executed).
|
|
334
|
+
* It describes the work to be done: retrieve operations, transformations, and mutations.
|
|
199
335
|
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
336
|
+
* Service methods return TxResult objects, and the handler's executeTx function
|
|
337
|
+
* orchestrates their execution with retry support.
|
|
202
338
|
*
|
|
203
|
-
* @
|
|
204
|
-
* @
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* @example
|
|
208
|
-
* ```ts
|
|
209
|
-
* const result = await executeUnitOfWork(
|
|
210
|
-
* {
|
|
211
|
-
* retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
212
|
-
* mutate: (uow, [users]) => {
|
|
213
|
-
* const user = users[0];
|
|
214
|
-
* uow.update("users", user.id, (b) => b.set({ balance: newBalance }));
|
|
215
|
-
* },
|
|
216
|
-
* onSuccess: async ({ results, mutationResult }) => {
|
|
217
|
-
* console.log("Update successful!");
|
|
218
|
-
* }
|
|
219
|
-
* },
|
|
220
|
-
* {
|
|
221
|
-
* createUnitOfWork: () => queryEngine.createUnitOfWork(),
|
|
222
|
-
* retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3 })
|
|
223
|
-
* }
|
|
224
|
-
* );
|
|
225
|
-
* ```
|
|
339
|
+
* @template TResult - The final result type (determined by return type priority)
|
|
340
|
+
* @template TRetrieveSuccessResult - The retrieve success result type (what serviceCalls receive).
|
|
341
|
+
* Defaults to TResult, meaning serviceCalls receive the same type as the final result.
|
|
226
342
|
*/
|
|
227
|
-
export
|
|
343
|
+
export interface TxResult<TResult, TRetrieveSuccessResult = TResult> {
|
|
344
|
+
/** Brand to identify TxResult objects */
|
|
345
|
+
readonly [TX_RESULT_BRAND]: true;
|
|
346
|
+
|
|
347
|
+
/** Internal structure - do not access directly */
|
|
348
|
+
readonly _internal: TxResultInternal<
|
|
349
|
+
AnySchema,
|
|
350
|
+
unknown[],
|
|
351
|
+
TRetrieveSuccessResult,
|
|
352
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
353
|
+
readonly TxResult<any, any>[],
|
|
354
|
+
unknown,
|
|
355
|
+
TResult,
|
|
356
|
+
HooksMap
|
|
357
|
+
>;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Create a TxResult for service context.
|
|
362
|
+
* Schedules retrieve operations on the baseUow and returns a TxResult with callbacks stored.
|
|
363
|
+
* @internal Used by ServiceTxBuilder.build()
|
|
364
|
+
*/
|
|
365
|
+
function createServiceTx<
|
|
228
366
|
TSchema extends AnySchema,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
367
|
+
TRetrieveResults extends unknown[],
|
|
368
|
+
TRetrieveSuccessResult,
|
|
369
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
370
|
+
TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
|
|
371
|
+
TMutateResult,
|
|
372
|
+
TSuccessResult,
|
|
373
|
+
THooks extends HooksMap = {},
|
|
232
374
|
>(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
375
|
+
schema: TSchema | undefined,
|
|
376
|
+
callbacks: ServiceTxCallbacks<
|
|
377
|
+
TSchema,
|
|
378
|
+
TRetrieveResults,
|
|
379
|
+
TRetrieveSuccessResult,
|
|
380
|
+
TServiceCalls,
|
|
381
|
+
TMutateResult,
|
|
382
|
+
TSuccessResult,
|
|
383
|
+
THooks
|
|
384
|
+
>,
|
|
385
|
+
baseUow: IUnitOfWork,
|
|
386
|
+
): TxResult<unknown, unknown> {
|
|
387
|
+
// Create deferred promise for retrieve phase
|
|
388
|
+
const {
|
|
389
|
+
promise: retrievePhase,
|
|
390
|
+
resolve: resolveRetrievePhase,
|
|
391
|
+
reject: rejectRetrievePhase,
|
|
392
|
+
} = Promise.withResolvers<TRetrieveResults>();
|
|
240
393
|
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
let attempt = 0;
|
|
394
|
+
// Get a restricted view that signals readiness
|
|
395
|
+
const restrictedUow = baseUow.restrict({ readyFor: "none" });
|
|
244
396
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
397
|
+
// Call serviceCalls factory if provided - this invokes other services which schedule their operations
|
|
398
|
+
let serviceCalls: TServiceCalls | undefined;
|
|
399
|
+
try {
|
|
400
|
+
if (callbacks.serviceCalls) {
|
|
401
|
+
serviceCalls = callbacks.serviceCalls();
|
|
249
402
|
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
restrictedUow.signalReadyForRetrieval();
|
|
405
|
+
restrictedUow.signalReadyForMutation();
|
|
406
|
+
retrievePhase.catch(() => {});
|
|
407
|
+
rejectRetrievePhase(error);
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
let typedUow: TypedUnitOfWork<TSchema, TRetrieveResults, unknown, THooks> | undefined;
|
|
411
|
+
try {
|
|
412
|
+
if (schema && callbacks.retrieve) {
|
|
413
|
+
const emptyUow = restrictedUow.forSchema<TSchema, THooks>(schema);
|
|
414
|
+
typedUow = callbacks.retrieve(emptyUow);
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
restrictedUow.signalReadyForRetrieval();
|
|
418
|
+
restrictedUow.signalReadyForMutation();
|
|
419
|
+
retrievePhase.catch(() => {});
|
|
420
|
+
rejectRetrievePhase(error);
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
restrictedUow.signalReadyForRetrieval();
|
|
250
424
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// This is safe because when there's no retrieve, TRetrievalResults should be []
|
|
262
|
-
retrievalUow = uow as unknown as TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Execute retrieval phase
|
|
266
|
-
const results = (await retrievalUow.executeRetrieve()) as TRetrievalResults;
|
|
267
|
-
|
|
268
|
-
// Invoke mutation phase callback if provided
|
|
269
|
-
let mutationResult: TMutationResult;
|
|
270
|
-
if (callbacks.mutate) {
|
|
271
|
-
mutationResult = await callbacks.mutate(retrievalUow, results);
|
|
272
|
-
} else {
|
|
273
|
-
mutationResult = undefined as TMutationResult;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Execute mutation phase
|
|
277
|
-
const { success } = await retrievalUow.executeMutations();
|
|
278
|
-
|
|
279
|
-
if (success) {
|
|
280
|
-
// Success! Get created IDs and nonce, then invoke onSuccess if provided
|
|
281
|
-
const createdIds = retrievalUow.getCreatedIds();
|
|
282
|
-
const nonce = retrievalUow.nonce;
|
|
283
|
-
|
|
284
|
-
// Await promises in mutationResult (1 level deep)
|
|
285
|
-
const awaitedMutationResult = await awaitPromisesInObject(mutationResult);
|
|
286
|
-
|
|
287
|
-
if (callbacks.onSuccess) {
|
|
288
|
-
await callbacks.onSuccess({
|
|
289
|
-
results,
|
|
290
|
-
mutationResult: awaitedMutationResult,
|
|
291
|
-
createdIds,
|
|
292
|
-
nonce,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
success: true,
|
|
298
|
-
results,
|
|
299
|
-
mutationResult: awaitedMutationResult,
|
|
300
|
-
createdIds,
|
|
301
|
-
nonce,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Failed - check if we should retry
|
|
306
|
-
// attempt represents the number of attempts completed so far
|
|
307
|
-
if (!retryPolicy.shouldRetry(attempt, undefined, signal)) {
|
|
308
|
-
// No more retries
|
|
309
|
-
return { success: false, reason: "conflict" };
|
|
310
|
-
}
|
|
425
|
+
// Set up the retrieve phase promise to resolve when the handler executes retrieve
|
|
426
|
+
if (typedUow) {
|
|
427
|
+
typedUow.retrievalPhase.then(
|
|
428
|
+
(results) => resolveRetrievePhase(results as TRetrieveResults),
|
|
429
|
+
(error) => rejectRetrievePhase(error),
|
|
430
|
+
);
|
|
431
|
+
} else if (!callbacks.retrieve) {
|
|
432
|
+
// No retrieve callback - resolve immediately with empty array
|
|
433
|
+
resolveRetrievePhase([] as unknown as TRetrieveResults);
|
|
434
|
+
}
|
|
311
435
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
436
|
+
const internal: TxResultInternal<
|
|
437
|
+
TSchema,
|
|
438
|
+
TRetrieveResults,
|
|
439
|
+
TRetrieveSuccessResult,
|
|
440
|
+
TServiceCalls,
|
|
441
|
+
TMutateResult,
|
|
442
|
+
TSuccessResult,
|
|
443
|
+
THooks
|
|
444
|
+
> = {
|
|
445
|
+
schema,
|
|
446
|
+
callbacks,
|
|
447
|
+
typedUow,
|
|
448
|
+
restrictedUow,
|
|
449
|
+
retrievePhase,
|
|
450
|
+
resolveRetrievePhase,
|
|
451
|
+
rejectRetrievePhase,
|
|
452
|
+
retrieveSuccessResult: undefined,
|
|
453
|
+
mutateResult: undefined,
|
|
454
|
+
finalResult: undefined,
|
|
455
|
+
serviceCalls,
|
|
456
|
+
};
|
|
317
457
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
458
|
+
return {
|
|
459
|
+
[TX_RESULT_BRAND]: true as const,
|
|
460
|
+
// Cast through unknown to avoid type incompatibility issues with generic constraints
|
|
461
|
+
_internal: internal as unknown as TxResultInternal<
|
|
462
|
+
AnySchema,
|
|
463
|
+
unknown[],
|
|
464
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
465
|
+
any,
|
|
466
|
+
readonly TxResult<unknown>[],
|
|
467
|
+
unknown,
|
|
468
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
469
|
+
any,
|
|
470
|
+
HooksMap
|
|
471
|
+
>,
|
|
472
|
+
};
|
|
325
473
|
}
|
|
326
474
|
|
|
327
475
|
/**
|
|
328
|
-
* Options for executing
|
|
476
|
+
* Options for executing transactions
|
|
329
477
|
*/
|
|
330
|
-
export interface
|
|
478
|
+
export interface ExecuteTxOptions {
|
|
331
479
|
/**
|
|
332
480
|
* Factory function that creates or resets a UOW instance for each attempt
|
|
333
481
|
*/
|
|
@@ -353,61 +501,229 @@ export interface ExecuteRestrictedUnitOfWorkOptions {
|
|
|
353
501
|
* Callback invoked after successful mutation phase.
|
|
354
502
|
* Use this for post-mutation processing like hook execution.
|
|
355
503
|
*/
|
|
356
|
-
|
|
504
|
+
onAfterMutate?: (uow: IUnitOfWork) => Promise<void>;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Recursively collect all TxResults from a service call tree.
|
|
509
|
+
* Returns them in a flat array in dependency order (serviceCalls before their dependents).
|
|
510
|
+
* Skips undefined values (which can occur with optional service patterns like
|
|
511
|
+
* optionalService?.method()).
|
|
512
|
+
*/
|
|
513
|
+
function collectAllTxResults(
|
|
514
|
+
txResults: readonly (TxResult<unknown> | undefined)[],
|
|
515
|
+
): TxResult<unknown>[] {
|
|
516
|
+
const collected: TxResult<unknown>[] = [];
|
|
517
|
+
const seen = new Set<TxResult<unknown>>();
|
|
518
|
+
|
|
519
|
+
function collect(txResult: TxResult<unknown> | undefined) {
|
|
520
|
+
if (txResult === undefined) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (seen.has(txResult)) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
seen.add(txResult);
|
|
528
|
+
|
|
529
|
+
// First collect serviceCalls (so they come before this TxResult)
|
|
530
|
+
const serviceCalls = txResult._internal.serviceCalls;
|
|
531
|
+
if (serviceCalls) {
|
|
532
|
+
for (const serviceCall of serviceCalls) {
|
|
533
|
+
collect(serviceCall);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
collected.push(txResult);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
for (const txResult of txResults) {
|
|
541
|
+
collect(txResult);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return collected;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Execute a single TxResult's callbacks after retrieve phase completes.
|
|
549
|
+
* This processes retrieveSuccess, mutate, and success callbacks in order.
|
|
550
|
+
*/
|
|
551
|
+
async function processTxResultAfterRetrieve<T>(
|
|
552
|
+
txResult: TxResult<T>,
|
|
553
|
+
baseUow: IUnitOfWork,
|
|
554
|
+
): Promise<void> {
|
|
555
|
+
const internal = txResult._internal;
|
|
556
|
+
const callbacks = internal.callbacks;
|
|
557
|
+
|
|
558
|
+
// Wait for retrieve phase to complete
|
|
559
|
+
const retrieveResults = await internal.retrievePhase;
|
|
560
|
+
|
|
561
|
+
// Collect serviceCalls' retrieve success results (or mutate results if no retrieve was provided)
|
|
562
|
+
// When a serviceCall has no retrieve/retrieveSuccess but has mutate, its mutate has already run
|
|
563
|
+
// (due to service call execution order), so we use its mutate result as the "retrieve success result".
|
|
564
|
+
const serviceResults: unknown[] = [];
|
|
565
|
+
if (internal.serviceCalls) {
|
|
566
|
+
for (const serviceCall of internal.serviceCalls) {
|
|
567
|
+
if (serviceCall === undefined) {
|
|
568
|
+
serviceResults.push(undefined);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const serviceCallInternal = serviceCall._internal;
|
|
573
|
+
// Check if this is a mutate-only service call (empty array sentinel with mutate callback)
|
|
574
|
+
// In that case, prefer mutateResult over the empty array retrieveSuccessResult
|
|
575
|
+
if (
|
|
576
|
+
serviceCallInternal.retrieveSuccessResult !== undefined &&
|
|
577
|
+
!(
|
|
578
|
+
Array.isArray(serviceCallInternal.retrieveSuccessResult) &&
|
|
579
|
+
serviceCallInternal.retrieveSuccessResult.length === 0 &&
|
|
580
|
+
serviceCallInternal.callbacks.mutate
|
|
581
|
+
)
|
|
582
|
+
) {
|
|
583
|
+
serviceResults.push(serviceCallInternal.retrieveSuccessResult);
|
|
584
|
+
} else if (serviceCallInternal.mutateResult !== undefined) {
|
|
585
|
+
serviceResults.push(serviceCallInternal.mutateResult);
|
|
586
|
+
} else {
|
|
587
|
+
serviceResults.push(serviceCallInternal.retrieveSuccessResult);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (callbacks.retrieveSuccess) {
|
|
593
|
+
internal.retrieveSuccessResult = callbacks.retrieveSuccess(
|
|
594
|
+
retrieveResults,
|
|
595
|
+
serviceResults as ExtractServiceRetrieveResults<readonly TxResult<unknown>[]>,
|
|
596
|
+
);
|
|
597
|
+
} else {
|
|
598
|
+
internal.retrieveSuccessResult = retrieveResults as typeof internal.retrieveSuccessResult;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (callbacks.mutate) {
|
|
602
|
+
const mutateCtx = {
|
|
603
|
+
uow: internal.schema
|
|
604
|
+
? baseUow.forSchema(internal.schema)
|
|
605
|
+
: (undefined as unknown as TypedUnitOfWork<AnySchema, [], unknown, HooksMap>),
|
|
606
|
+
// At this point retrieveSuccessResult has been set (either by retrieveSuccess
|
|
607
|
+
// callback or defaulted to retrieveResults)
|
|
608
|
+
retrieveResult: internal.retrieveSuccessResult as NonNullable<
|
|
609
|
+
typeof internal.retrieveSuccessResult
|
|
610
|
+
>,
|
|
611
|
+
serviceIntermediateResult: serviceResults as ExtractServiceRetrieveResults<
|
|
612
|
+
readonly TxResult<unknown>[]
|
|
613
|
+
>,
|
|
614
|
+
};
|
|
615
|
+
internal.mutateResult = callbacks.mutate(mutateCtx);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (internal.typedUow) {
|
|
619
|
+
internal.typedUow.signalReadyForMutation();
|
|
620
|
+
} else {
|
|
621
|
+
// For TxResults without retrieve callback, signal via the restricted UOW
|
|
622
|
+
internal.restrictedUow.signalReadyForMutation();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Execute a single TxResult's success callback after mutations complete.
|
|
628
|
+
*/
|
|
629
|
+
async function processTxResultAfterMutate<T>(txResult: TxResult<T>): Promise<T> {
|
|
630
|
+
const internal = txResult._internal;
|
|
631
|
+
const callbacks = internal.callbacks;
|
|
632
|
+
|
|
633
|
+
const serviceIntermediateResults: unknown[] = [];
|
|
634
|
+
const serviceFinalResults: unknown[] = [];
|
|
635
|
+
if (internal.serviceCalls) {
|
|
636
|
+
for (const serviceCall of internal.serviceCalls) {
|
|
637
|
+
if (serviceCall === undefined) {
|
|
638
|
+
serviceIntermediateResults.push(undefined);
|
|
639
|
+
serviceFinalResults.push(undefined);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Mirror the logic from processTxResultAfterRetrieve/executeTx:
|
|
644
|
+
// For mutate-only serviceCalls (no retrieve phase, just mutations), use mutateResult instead of retrieveSuccessResult
|
|
645
|
+
const serviceCallInternal = serviceCall._internal;
|
|
646
|
+
// Check if this is a mutate-only service call (empty array sentinel with mutate callback)
|
|
647
|
+
// In that case, prefer mutateResult over the empty array retrieveSuccessResult
|
|
648
|
+
if (
|
|
649
|
+
serviceCallInternal.retrieveSuccessResult !== undefined &&
|
|
650
|
+
!(
|
|
651
|
+
Array.isArray(serviceCallInternal.retrieveSuccessResult) &&
|
|
652
|
+
serviceCallInternal.retrieveSuccessResult.length === 0 &&
|
|
653
|
+
serviceCallInternal.callbacks.mutate
|
|
654
|
+
)
|
|
655
|
+
) {
|
|
656
|
+
serviceIntermediateResults.push(serviceCallInternal.retrieveSuccessResult);
|
|
657
|
+
} else if (serviceCallInternal.mutateResult !== undefined) {
|
|
658
|
+
serviceIntermediateResults.push(serviceCallInternal.mutateResult);
|
|
659
|
+
} else {
|
|
660
|
+
serviceIntermediateResults.push(serviceCallInternal.retrieveSuccessResult);
|
|
661
|
+
}
|
|
662
|
+
serviceFinalResults.push(serviceCallInternal.finalResult);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (callbacks.success) {
|
|
667
|
+
const successCtx = {
|
|
668
|
+
retrieveResult: internal.retrieveSuccessResult as NonNullable<
|
|
669
|
+
typeof internal.retrieveSuccessResult
|
|
670
|
+
>,
|
|
671
|
+
mutateResult: internal.mutateResult,
|
|
672
|
+
serviceResult: serviceFinalResults as ExtractServiceFinalResults<
|
|
673
|
+
readonly TxResult<unknown>[]
|
|
674
|
+
>,
|
|
675
|
+
serviceIntermediateResult: serviceIntermediateResults as ExtractServiceRetrieveResults<
|
|
676
|
+
readonly TxResult<unknown>[]
|
|
677
|
+
>,
|
|
678
|
+
};
|
|
679
|
+
internal.finalResult = callbacks.success(successCtx) as T;
|
|
680
|
+
} else if (callbacks.mutate) {
|
|
681
|
+
internal.finalResult = (await awaitPromisesInObject(internal.mutateResult)) as T;
|
|
682
|
+
} else if (callbacks.retrieveSuccess || callbacks.retrieve) {
|
|
683
|
+
internal.finalResult = internal.retrieveSuccessResult as T;
|
|
684
|
+
} else {
|
|
685
|
+
internal.finalResult = serviceFinalResults as T;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return internal.finalResult as T;
|
|
357
689
|
}
|
|
358
690
|
|
|
359
691
|
/**
|
|
360
|
-
* Execute a
|
|
692
|
+
* Execute a transaction with the unified TxResult pattern.
|
|
361
693
|
*
|
|
362
|
-
* This
|
|
363
|
-
* a context object with forSchema, executeRetrieve, and executeMutate methods. The user can
|
|
364
|
-
* create schema-specific UOWs via forSchema, then call executeRetrieve() and executeMutate()
|
|
365
|
-
* to execute the retrieval and mutation phases. The entire callback is re-executed on optimistic
|
|
366
|
-
* concurrency conflicts, ensuring retries work properly.
|
|
694
|
+
* This is the handler-level function that actually executes TxResults with retry support.
|
|
367
695
|
*
|
|
368
|
-
* @param
|
|
696
|
+
* @param callbacks - Transaction callbacks (serviceCalls, retrieve, retrieveSuccess, mutate, success)
|
|
369
697
|
* @param options - Configuration including UOW factory, retry policy, and abort signal
|
|
370
|
-
* @returns Promise resolving to the
|
|
371
|
-
* @throws Error if retries are exhausted or callback throws an error
|
|
698
|
+
* @returns Promise resolving to the result determined by return type priority
|
|
372
699
|
*
|
|
373
700
|
* @example
|
|
374
701
|
* ```ts
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
* await executeRetrieve();
|
|
382
|
-
*
|
|
383
|
-
* const profileId = uow.create("profiles", { userId });
|
|
384
|
-
*
|
|
385
|
-
* // Execute mutation phase
|
|
386
|
-
* await executeMutate();
|
|
387
|
-
*
|
|
388
|
-
* return { userId, profileId };
|
|
389
|
-
* },
|
|
390
|
-
* {
|
|
391
|
-
* createUnitOfWork: () => db.createUnitOfWork(),
|
|
392
|
-
* retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5 })
|
|
393
|
-
* }
|
|
394
|
-
* );
|
|
395
|
-
* ```
|
|
702
|
+
* // Simple retrieve + transform
|
|
703
|
+
* const user = await executeTx({
|
|
704
|
+
* retrieve: (ctx) => ctx.forSchema(usersSchema).find("users", ...),
|
|
705
|
+
* retrieveSuccess: ([users]) => users[0] ?? null,
|
|
706
|
+
* }, { createUnitOfWork });
|
|
707
|
+
* @internal Used by HandlerTxBuilder.execute()
|
|
396
708
|
*/
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
709
|
+
async function executeTx(
|
|
710
|
+
callbacks: HandlerTxCallbacks<
|
|
711
|
+
unknown[],
|
|
712
|
+
unknown,
|
|
713
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
714
|
+
readonly (TxResult<any, any> | undefined)[],
|
|
715
|
+
unknown,
|
|
716
|
+
unknown,
|
|
717
|
+
HooksMap
|
|
718
|
+
>,
|
|
719
|
+
options: ExecuteTxOptions,
|
|
720
|
+
): Promise<unknown> {
|
|
721
|
+
type TRetrieveResults = unknown[];
|
|
722
|
+
type TRetrieveSuccessResult = unknown;
|
|
723
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
724
|
+
type TServiceCalls = readonly (TxResult<any, any> | undefined)[];
|
|
725
|
+
type TMutateResult = unknown;
|
|
726
|
+
type THooks = HooksMap;
|
|
411
727
|
const retryPolicy =
|
|
412
728
|
options.retryPolicy ??
|
|
413
729
|
new ExponentialBackoffRetryPolicy({
|
|
@@ -421,84 +737,1171 @@ export async function executeRestrictedUnitOfWork<TResult, THooks extends HooksM
|
|
|
421
737
|
while (true) {
|
|
422
738
|
// Check if aborted before starting attempt
|
|
423
739
|
if (signal?.aborted) {
|
|
424
|
-
throw new Error("
|
|
740
|
+
throw new Error("Transaction execution aborted");
|
|
425
741
|
}
|
|
426
742
|
|
|
427
743
|
try {
|
|
428
744
|
// Create a fresh UOW for this attempt
|
|
429
745
|
const baseUow = options.createUnitOfWork();
|
|
430
746
|
|
|
431
|
-
|
|
747
|
+
// Create handler context
|
|
748
|
+
const context: HandlerTxContext<THooks> = {
|
|
432
749
|
forSchema: <S extends AnySchema, H extends HooksMap = THooks>(schema: S, hooks?: H) => {
|
|
433
750
|
return baseUow.forSchema(schema, hooks);
|
|
434
751
|
},
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
executeMutate: async () => {
|
|
439
|
-
if (baseUow.state === "executed") {
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
752
|
+
idempotencyKey: baseUow.idempotencyKey,
|
|
753
|
+
currentAttempt: attempt,
|
|
754
|
+
};
|
|
442
755
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
756
|
+
// Call serviceCalls factory if provided - this creates TxResults that schedule operations
|
|
757
|
+
let serviceCalls: TServiceCalls | undefined;
|
|
758
|
+
if (callbacks.serviceCalls) {
|
|
759
|
+
serviceCalls = callbacks.serviceCalls();
|
|
760
|
+
}
|
|
446
761
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
762
|
+
// Call retrieve callback - it returns a TypedUnitOfWork with scheduled operations or void
|
|
763
|
+
const typedUowFromRetrieve = callbacks.retrieve?.(context);
|
|
764
|
+
|
|
765
|
+
const allServiceCallTxResults = serviceCalls ? collectAllTxResults([...serviceCalls]) : [];
|
|
766
|
+
|
|
767
|
+
await baseUow.executeRetrieve();
|
|
451
768
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
769
|
+
// Get retrieve results from TypedUnitOfWork's retrievalPhase or default to empty array
|
|
770
|
+
const retrieveResult: TRetrieveResults = typedUowFromRetrieve
|
|
771
|
+
? await typedUowFromRetrieve.retrievalPhase
|
|
772
|
+
: ([] as unknown as TRetrieveResults);
|
|
773
|
+
|
|
774
|
+
for (const txResult of allServiceCallTxResults) {
|
|
775
|
+
await processTxResultAfterRetrieve(txResult, baseUow);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const serviceResults: unknown[] = [];
|
|
779
|
+
if (serviceCalls) {
|
|
780
|
+
for (const serviceCall of serviceCalls) {
|
|
781
|
+
if (serviceCall === undefined) {
|
|
782
|
+
serviceResults.push(undefined);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
const serviceCallInternal = serviceCall._internal;
|
|
786
|
+
// Check if this is a mutate-only service call (empty array sentinel with mutate callback)
|
|
787
|
+
// In that case, prefer mutateResult over the empty array retrieveSuccessResult
|
|
788
|
+
if (
|
|
789
|
+
serviceCallInternal.retrieveSuccessResult !== undefined &&
|
|
790
|
+
!(
|
|
791
|
+
Array.isArray(serviceCallInternal.retrieveSuccessResult) &&
|
|
792
|
+
serviceCallInternal.retrieveSuccessResult.length === 0 &&
|
|
793
|
+
serviceCallInternal.callbacks.mutate
|
|
794
|
+
)
|
|
795
|
+
) {
|
|
796
|
+
serviceResults.push(serviceCallInternal.retrieveSuccessResult);
|
|
797
|
+
} else if (serviceCallInternal.mutateResult !== undefined) {
|
|
798
|
+
serviceResults.push(serviceCallInternal.mutateResult);
|
|
799
|
+
} else {
|
|
800
|
+
serviceResults.push(serviceCallInternal.retrieveSuccessResult);
|
|
455
801
|
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Call retrieveSuccess if provided
|
|
806
|
+
let retrieveSuccessResult: TRetrieveSuccessResult;
|
|
807
|
+
if (callbacks.retrieveSuccess) {
|
|
808
|
+
retrieveSuccessResult = callbacks.retrieveSuccess(
|
|
809
|
+
retrieveResult,
|
|
810
|
+
serviceResults as ExtractServiceRetrieveResults<TServiceCalls>,
|
|
811
|
+
);
|
|
812
|
+
} else {
|
|
813
|
+
retrieveSuccessResult = retrieveResult as unknown as TRetrieveSuccessResult;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
let mutateResult: TMutateResult | undefined;
|
|
817
|
+
if (callbacks.mutate) {
|
|
818
|
+
const mutateCtx: HandlerTxMutateContext<
|
|
819
|
+
TRetrieveSuccessResult,
|
|
820
|
+
ExtractServiceRetrieveResults<TServiceCalls>,
|
|
821
|
+
THooks
|
|
822
|
+
> = {
|
|
823
|
+
...context,
|
|
824
|
+
retrieveResult: retrieveSuccessResult,
|
|
825
|
+
serviceIntermediateResult: serviceResults as ExtractServiceRetrieveResults<TServiceCalls>,
|
|
826
|
+
};
|
|
827
|
+
mutateResult = callbacks.mutate(mutateCtx);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (options.onBeforeMutate) {
|
|
831
|
+
options.onBeforeMutate(baseUow);
|
|
832
|
+
}
|
|
833
|
+
const result = await baseUow.executeMutations();
|
|
834
|
+
if (!result.success) {
|
|
835
|
+
throw new ConcurrencyConflictError();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Process each serviceCall TxResult's success callback
|
|
839
|
+
for (const txResult of allServiceCallTxResults) {
|
|
840
|
+
await processTxResultAfterMutate(txResult);
|
|
841
|
+
}
|
|
456
842
|
|
|
457
|
-
|
|
458
|
-
|
|
843
|
+
const serviceFinalResults: unknown[] = [];
|
|
844
|
+
if (serviceCalls) {
|
|
845
|
+
for (const serviceCall of serviceCalls) {
|
|
846
|
+
if (serviceCall === undefined) {
|
|
847
|
+
serviceFinalResults.push(undefined);
|
|
848
|
+
continue;
|
|
459
849
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
};
|
|
850
|
+
serviceFinalResults.push(serviceCall._internal.finalResult);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
464
853
|
|
|
465
|
-
|
|
466
|
-
|
|
854
|
+
let finalResult: unknown;
|
|
855
|
+
if (callbacks.success) {
|
|
856
|
+
// The success context type is determined by the overload - we construct it at runtime
|
|
857
|
+
// and the type safety is guaranteed by the discriminated overloads
|
|
858
|
+
const successCtx = {
|
|
859
|
+
retrieveResult: retrieveSuccessResult,
|
|
860
|
+
mutateResult,
|
|
861
|
+
serviceResult: serviceFinalResults as ExtractServiceFinalResults<TServiceCalls>,
|
|
862
|
+
serviceIntermediateResult: serviceResults as ExtractServiceRetrieveResults<TServiceCalls>,
|
|
863
|
+
} as Parameters<NonNullable<typeof callbacks.success>>[0];
|
|
864
|
+
finalResult = callbacks.success(successCtx);
|
|
865
|
+
} else if (callbacks.mutate) {
|
|
866
|
+
finalResult = await awaitPromisesInObject(mutateResult);
|
|
867
|
+
} else if (callbacks.retrieveSuccess || callbacks.retrieve) {
|
|
868
|
+
finalResult = retrieveSuccessResult;
|
|
869
|
+
} else {
|
|
870
|
+
finalResult = serviceFinalResults;
|
|
871
|
+
}
|
|
467
872
|
|
|
468
|
-
|
|
469
|
-
|
|
873
|
+
if (options.onAfterMutate) {
|
|
874
|
+
await options.onAfterMutate(baseUow);
|
|
875
|
+
}
|
|
470
876
|
|
|
471
|
-
|
|
472
|
-
return awaitedResult;
|
|
877
|
+
return await awaitPromisesInObject(finalResult);
|
|
473
878
|
} catch (error) {
|
|
474
879
|
if (signal?.aborted) {
|
|
475
|
-
throw new Error("
|
|
880
|
+
throw new Error("Transaction execution aborted");
|
|
476
881
|
}
|
|
477
882
|
|
|
478
883
|
// Only retry concurrency conflicts, not other errors
|
|
479
884
|
if (!(error instanceof ConcurrencyConflictError)) {
|
|
480
|
-
// Not a concurrency conflict - throw immediately without retry
|
|
481
885
|
throw error;
|
|
482
886
|
}
|
|
483
887
|
|
|
484
888
|
if (!retryPolicy.shouldRetry(attempt, error, signal)) {
|
|
485
|
-
// No more retries - check again if aborted or throw conflict error
|
|
486
889
|
if (signal?.aborted) {
|
|
487
|
-
throw new Error("
|
|
890
|
+
throw new Error("Transaction execution aborted");
|
|
488
891
|
}
|
|
489
|
-
throw new
|
|
490
|
-
cause: error,
|
|
491
|
-
});
|
|
892
|
+
throw new ConcurrencyConflictError();
|
|
492
893
|
}
|
|
493
894
|
|
|
494
|
-
// Wait before retrying
|
|
495
895
|
const delayMs = retryPolicy.getDelayMs(attempt);
|
|
496
896
|
if (delayMs > 0) {
|
|
497
897
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
498
898
|
}
|
|
499
899
|
|
|
500
|
-
// Increment attempt counter for next iteration
|
|
501
900
|
attempt++;
|
|
502
901
|
}
|
|
503
902
|
}
|
|
504
903
|
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Error thrown when a Unit of Work execution fails due to optimistic concurrency conflict.
|
|
907
|
+
* This error triggers automatic retry behavior in executeTx.
|
|
908
|
+
*/
|
|
909
|
+
export class ConcurrencyConflictError extends Error {
|
|
910
|
+
constructor(message = "Optimistic concurrency conflict detected") {
|
|
911
|
+
super(message);
|
|
912
|
+
this.name = "ConcurrencyConflictError";
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Type utility that unwraps promises 1 level deep in objects, arrays, or direct promises
|
|
918
|
+
* Handles tuples, arrays, objects, and direct promises
|
|
919
|
+
*/
|
|
920
|
+
export type AwaitedPromisesInObject<T> =
|
|
921
|
+
// First check if it's a Promise
|
|
922
|
+
T extends Promise<infer U>
|
|
923
|
+
? Awaited<U>
|
|
924
|
+
: // Check for arrays with known length (tuples) - preserves tuple structure
|
|
925
|
+
T extends readonly [unknown, ...unknown[]]
|
|
926
|
+
? { [K in keyof T]: AwaitedPromisesInObject<T[K]> }
|
|
927
|
+
: T extends [unknown, ...unknown[]]
|
|
928
|
+
? { [K in keyof T]: AwaitedPromisesInObject<T[K]> }
|
|
929
|
+
: // Check for regular arrays (unknown length)
|
|
930
|
+
T extends (infer U)[]
|
|
931
|
+
? Awaited<U>[]
|
|
932
|
+
: T extends readonly (infer U)[]
|
|
933
|
+
? readonly Awaited<U>[]
|
|
934
|
+
: // Check for objects
|
|
935
|
+
T extends Record<string, unknown>
|
|
936
|
+
? {
|
|
937
|
+
[K in keyof T]: T[K] extends Promise<infer U> ? Awaited<U> : T[K];
|
|
938
|
+
}
|
|
939
|
+
: // Otherwise return as-is
|
|
940
|
+
T;
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Await promises in an object 1 level deep
|
|
944
|
+
*/
|
|
945
|
+
async function awaitPromisesInObject<T>(obj: T): Promise<AwaitedPromisesInObject<T>> {
|
|
946
|
+
if (obj === null || obj === undefined) {
|
|
947
|
+
return obj as AwaitedPromisesInObject<T>;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (typeof obj !== "object") {
|
|
951
|
+
return obj as AwaitedPromisesInObject<T>;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Check if it's a Promise
|
|
955
|
+
if (obj instanceof Promise) {
|
|
956
|
+
return (await obj) as AwaitedPromisesInObject<T>;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Check if it's an array
|
|
960
|
+
if (Array.isArray(obj)) {
|
|
961
|
+
const awaited = await Promise.all(
|
|
962
|
+
obj.map((item) => (item instanceof Promise ? item : Promise.resolve(item))),
|
|
963
|
+
);
|
|
964
|
+
return awaited as AwaitedPromisesInObject<T>;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (obj.constructor !== Object) {
|
|
968
|
+
return obj as AwaitedPromisesInObject<T>;
|
|
969
|
+
}
|
|
970
|
+
const result = {} as T;
|
|
971
|
+
const entries = Object.entries(obj as Record<string, unknown>);
|
|
972
|
+
const awaitedEntries = await Promise.all(
|
|
973
|
+
entries.map(async ([key, value]) => {
|
|
974
|
+
const awaitedValue = value instanceof Promise ? await value : value;
|
|
975
|
+
return [key, awaitedValue] as const;
|
|
976
|
+
}),
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
for (const [key, value] of awaitedEntries) {
|
|
980
|
+
(result as Record<string, unknown>)[key] = value;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return result as AwaitedPromisesInObject<T>;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// ============================================================================
|
|
987
|
+
// Builder Pattern Types and Classes
|
|
988
|
+
// ============================================================================
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Context passed to service-level mutate callback in builder pattern.
|
|
992
|
+
*/
|
|
993
|
+
export interface ServiceBuilderMutateContext<
|
|
994
|
+
TSchema extends AnySchema,
|
|
995
|
+
TRetrieveSuccessResult,
|
|
996
|
+
TServiceResult extends readonly unknown[],
|
|
997
|
+
THooks extends HooksMap,
|
|
998
|
+
> {
|
|
999
|
+
/** Unit of work for scheduling mutations */
|
|
1000
|
+
uow: TypedUnitOfWork<TSchema, [], unknown, THooks>;
|
|
1001
|
+
/** Result from transformRetrieve callback (or raw retrieve results if no transformRetrieve) */
|
|
1002
|
+
retrieveResult: TRetrieveSuccessResult;
|
|
1003
|
+
/** Array of retrieve success results from service calls (intermediate results, not final: retrieve results if service has retrieve, mutate result if service only mutates) */
|
|
1004
|
+
serviceIntermediateResult: TServiceResult;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Context passed to handler-level mutate callback in builder pattern.
|
|
1009
|
+
*/
|
|
1010
|
+
export interface HandlerBuilderMutateContext<
|
|
1011
|
+
TRetrieveSuccessResult,
|
|
1012
|
+
TServiceResult extends readonly unknown[],
|
|
1013
|
+
THooks extends HooksMap,
|
|
1014
|
+
> {
|
|
1015
|
+
/** Get a typed Unit of Work for the given schema */
|
|
1016
|
+
forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
|
|
1017
|
+
schema: S,
|
|
1018
|
+
hooks?: H,
|
|
1019
|
+
) => TypedUnitOfWork<S, [], unknown, H>;
|
|
1020
|
+
/** Unique key for this transaction (for idempotency/deduplication) */
|
|
1021
|
+
idempotencyKey: string;
|
|
1022
|
+
/** Current attempt number (0-based) */
|
|
1023
|
+
currentAttempt: number;
|
|
1024
|
+
/** Result from transformRetrieve callback (or raw retrieve results if no transformRetrieve) */
|
|
1025
|
+
retrieveResult: TRetrieveSuccessResult;
|
|
1026
|
+
/** Array of retrieve success results from service calls (intermediate results, not final: retrieve results if service has retrieve, mutate result if service only mutates) */
|
|
1027
|
+
serviceIntermediateResult: TServiceResult;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Context passed to transform callback when mutate IS provided.
|
|
1032
|
+
*/
|
|
1033
|
+
export interface BuilderTransformContextWithMutate<
|
|
1034
|
+
TRetrieveSuccessResult,
|
|
1035
|
+
TMutateResult,
|
|
1036
|
+
TServiceFinalResult extends readonly unknown[],
|
|
1037
|
+
TServiceIntermediateResult extends readonly unknown[],
|
|
1038
|
+
> {
|
|
1039
|
+
/** Result from transformRetrieve callback (or raw retrieve results if no transformRetrieve) */
|
|
1040
|
+
retrieveResult: TRetrieveSuccessResult;
|
|
1041
|
+
/** Result from mutate callback */
|
|
1042
|
+
mutateResult: TMutateResult;
|
|
1043
|
+
/** Array of final results from service calls (after success/transform callbacks) */
|
|
1044
|
+
serviceResult: TServiceFinalResult;
|
|
1045
|
+
/** Array of retrieve success results from service calls (same as what mutate receives: retrieve results if service has retrieve, mutate result if service only mutates) */
|
|
1046
|
+
serviceIntermediateResult: TServiceIntermediateResult;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Context passed to transform callback when mutate is NOT provided.
|
|
1051
|
+
*/
|
|
1052
|
+
export interface BuilderTransformContextWithoutMutate<
|
|
1053
|
+
TRetrieveSuccessResult,
|
|
1054
|
+
TServiceFinalResult extends readonly unknown[],
|
|
1055
|
+
TServiceIntermediateResult extends readonly unknown[],
|
|
1056
|
+
> {
|
|
1057
|
+
/** Result from transformRetrieve callback (or raw retrieve results if no transformRetrieve) */
|
|
1058
|
+
retrieveResult: TRetrieveSuccessResult;
|
|
1059
|
+
/** No mutate callback was provided */
|
|
1060
|
+
mutateResult: undefined;
|
|
1061
|
+
/** Array of final results from service calls (after success/transform callbacks) */
|
|
1062
|
+
serviceResult: TServiceFinalResult;
|
|
1063
|
+
/** Array of retrieve success results from service calls (same as what mutate receives: retrieve results if service has retrieve, mutate result if service only mutates) */
|
|
1064
|
+
serviceIntermediateResult: TServiceIntermediateResult;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Infer the final result type from builder state:
|
|
1069
|
+
* 1. transform → TTransformResult
|
|
1070
|
+
* 2. mutate → AwaitedPromisesInObject<TMutateResult>
|
|
1071
|
+
* 3. transformRetrieve → TRetrieveSuccessResult
|
|
1072
|
+
* 4. retrieve → TRetrieveResults
|
|
1073
|
+
* 5. withServiceCalls → ExtractServiceFinalResults<TServiceCalls>
|
|
1074
|
+
*/
|
|
1075
|
+
export type InferBuilderResultType<
|
|
1076
|
+
TRetrieveResults extends unknown[],
|
|
1077
|
+
TRetrieveSuccessResult,
|
|
1078
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1079
|
+
TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
|
|
1080
|
+
TMutateResult,
|
|
1081
|
+
TTransformResult,
|
|
1082
|
+
HasTransform extends boolean,
|
|
1083
|
+
HasMutate extends boolean,
|
|
1084
|
+
HasTransformRetrieve extends boolean,
|
|
1085
|
+
HasRetrieve extends boolean,
|
|
1086
|
+
> = HasTransform extends true
|
|
1087
|
+
? TTransformResult
|
|
1088
|
+
: HasMutate extends true
|
|
1089
|
+
? AwaitedPromisesInObject<TMutateResult>
|
|
1090
|
+
: HasTransformRetrieve extends true
|
|
1091
|
+
? TRetrieveSuccessResult
|
|
1092
|
+
: HasRetrieve extends true
|
|
1093
|
+
? TRetrieveResults
|
|
1094
|
+
: ExtractServiceFinalResults<TServiceCalls>;
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Infer the retrieve success result type for the builder:
|
|
1098
|
+
* - If transformRetrieve exists: TRetrieveSuccessResult
|
|
1099
|
+
* - Else if retrieve exists: TRetrieveResults (raw retrieve results)
|
|
1100
|
+
* - Else if mutate exists: AwaitedPromisesInObject<TMutateResult>
|
|
1101
|
+
* (mutate result becomes retrieve result for dependents)
|
|
1102
|
+
* - Else: TRetrieveResults (raw retrieve results, typically [])
|
|
1103
|
+
*/
|
|
1104
|
+
export type InferBuilderRetrieveSuccessResult<
|
|
1105
|
+
TRetrieveResults extends unknown[],
|
|
1106
|
+
TRetrieveSuccessResult,
|
|
1107
|
+
TMutateResult,
|
|
1108
|
+
HasTransformRetrieve extends boolean,
|
|
1109
|
+
HasRetrieve extends boolean,
|
|
1110
|
+
HasMutate extends boolean,
|
|
1111
|
+
> = HasTransformRetrieve extends true
|
|
1112
|
+
? TRetrieveSuccessResult
|
|
1113
|
+
: HasRetrieve extends true
|
|
1114
|
+
? TRetrieveResults
|
|
1115
|
+
: HasMutate extends true
|
|
1116
|
+
? AwaitedPromisesInObject<TMutateResult>
|
|
1117
|
+
: TRetrieveResults;
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Internal state for ServiceTxBuilder
|
|
1121
|
+
*/
|
|
1122
|
+
interface ServiceTxBuilderState<
|
|
1123
|
+
TSchema extends AnySchema,
|
|
1124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1125
|
+
TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
|
|
1126
|
+
TRetrieveResults extends unknown[],
|
|
1127
|
+
TRetrieveSuccessResult,
|
|
1128
|
+
TMutateResult,
|
|
1129
|
+
TTransformResult,
|
|
1130
|
+
THooks extends HooksMap,
|
|
1131
|
+
> {
|
|
1132
|
+
schema: TSchema;
|
|
1133
|
+
baseUow: IUnitOfWork;
|
|
1134
|
+
hooks?: THooks;
|
|
1135
|
+
withServiceCallsFn?: () => TServiceCalls;
|
|
1136
|
+
retrieveFn?: (
|
|
1137
|
+
uow: TypedUnitOfWork<TSchema, [], unknown, THooks>,
|
|
1138
|
+
) => TypedUnitOfWork<TSchema, TRetrieveResults, unknown, THooks>;
|
|
1139
|
+
transformRetrieveFn?: (
|
|
1140
|
+
retrieveResult: TRetrieveResults,
|
|
1141
|
+
serviceRetrieveResult: ExtractServiceRetrieveResults<TServiceCalls>,
|
|
1142
|
+
) => TRetrieveSuccessResult;
|
|
1143
|
+
mutateFn?: (
|
|
1144
|
+
ctx: ServiceBuilderMutateContext<
|
|
1145
|
+
TSchema,
|
|
1146
|
+
TRetrieveSuccessResult,
|
|
1147
|
+
ExtractServiceRetrieveResults<TServiceCalls>,
|
|
1148
|
+
THooks
|
|
1149
|
+
>,
|
|
1150
|
+
) => TMutateResult;
|
|
1151
|
+
transformFn?: (
|
|
1152
|
+
ctx:
|
|
1153
|
+
| BuilderTransformContextWithMutate<
|
|
1154
|
+
TRetrieveSuccessResult,
|
|
1155
|
+
TMutateResult,
|
|
1156
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
1157
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
1158
|
+
>
|
|
1159
|
+
| BuilderTransformContextWithoutMutate<
|
|
1160
|
+
TRetrieveSuccessResult,
|
|
1161
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
1162
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
1163
|
+
>,
|
|
1164
|
+
) => TTransformResult;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Builder for service-level transactions.
|
|
1169
|
+
* Uses a fluent API to build up transaction callbacks with proper type inference.
|
|
1170
|
+
*
|
|
1171
|
+
* @example
|
|
1172
|
+
* ```ts
|
|
1173
|
+
* return serviceTx(schema)
|
|
1174
|
+
* .withServiceCalls(() => [otherService.getData()])
|
|
1175
|
+
* .retrieve((uow) => uow.find("users", ...))
|
|
1176
|
+
* .transformRetrieve(([users], serviceResult) => users[0])
|
|
1177
|
+
* .mutate(({ uow, retrieveResult, serviceIntermediateResult }) =>
|
|
1178
|
+
* uow.create("records", { ... })
|
|
1179
|
+
* )
|
|
1180
|
+
* .transform(({ mutateResult, serviceResult, serviceIntermediateResult }) => ({ id: mutateResult }))
|
|
1181
|
+
* .build();
|
|
1182
|
+
* ```
|
|
1183
|
+
*/
|
|
1184
|
+
export class ServiceTxBuilder<
|
|
1185
|
+
TSchema extends AnySchema,
|
|
1186
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1187
|
+
TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
|
|
1188
|
+
TRetrieveResults extends unknown[],
|
|
1189
|
+
TRetrieveSuccessResult,
|
|
1190
|
+
TMutateResult,
|
|
1191
|
+
TTransformResult,
|
|
1192
|
+
HasRetrieve extends boolean,
|
|
1193
|
+
HasTransformRetrieve extends boolean,
|
|
1194
|
+
HasMutate extends boolean,
|
|
1195
|
+
HasTransform extends boolean,
|
|
1196
|
+
THooks extends HooksMap,
|
|
1197
|
+
> {
|
|
1198
|
+
readonly #state: ServiceTxBuilderState<
|
|
1199
|
+
TSchema,
|
|
1200
|
+
TServiceCalls,
|
|
1201
|
+
TRetrieveResults,
|
|
1202
|
+
TRetrieveSuccessResult,
|
|
1203
|
+
TMutateResult,
|
|
1204
|
+
TTransformResult,
|
|
1205
|
+
THooks
|
|
1206
|
+
>;
|
|
1207
|
+
|
|
1208
|
+
constructor(
|
|
1209
|
+
state: ServiceTxBuilderState<
|
|
1210
|
+
TSchema,
|
|
1211
|
+
TServiceCalls,
|
|
1212
|
+
TRetrieveResults,
|
|
1213
|
+
TRetrieveSuccessResult,
|
|
1214
|
+
TMutateResult,
|
|
1215
|
+
TTransformResult,
|
|
1216
|
+
THooks
|
|
1217
|
+
>,
|
|
1218
|
+
) {
|
|
1219
|
+
this.#state = state;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Add dependencies to execute before this transaction.
|
|
1224
|
+
*/
|
|
1225
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1226
|
+
withServiceCalls<TNewDeps extends readonly (TxResult<any, any> | undefined)[]>(
|
|
1227
|
+
fn: () => TNewDeps,
|
|
1228
|
+
): ServiceTxBuilder<
|
|
1229
|
+
TSchema,
|
|
1230
|
+
TNewDeps,
|
|
1231
|
+
TRetrieveResults,
|
|
1232
|
+
TRetrieveSuccessResult,
|
|
1233
|
+
TMutateResult,
|
|
1234
|
+
TTransformResult,
|
|
1235
|
+
HasRetrieve,
|
|
1236
|
+
HasTransformRetrieve,
|
|
1237
|
+
HasMutate,
|
|
1238
|
+
HasTransform,
|
|
1239
|
+
THooks
|
|
1240
|
+
> {
|
|
1241
|
+
return new ServiceTxBuilder({
|
|
1242
|
+
...this.#state,
|
|
1243
|
+
withServiceCallsFn: fn,
|
|
1244
|
+
} as ServiceTxBuilderState<
|
|
1245
|
+
TSchema,
|
|
1246
|
+
TNewDeps,
|
|
1247
|
+
TRetrieveResults,
|
|
1248
|
+
TRetrieveSuccessResult,
|
|
1249
|
+
TMutateResult,
|
|
1250
|
+
TTransformResult,
|
|
1251
|
+
THooks
|
|
1252
|
+
>);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Add retrieval operations to the transaction.
|
|
1257
|
+
*/
|
|
1258
|
+
retrieve<TNewRetrieveResults extends unknown[]>(
|
|
1259
|
+
fn: (
|
|
1260
|
+
uow: TypedUnitOfWork<TSchema, [], unknown, THooks>,
|
|
1261
|
+
) => TypedUnitOfWork<TSchema, TNewRetrieveResults, unknown, THooks>,
|
|
1262
|
+
): ServiceTxBuilder<
|
|
1263
|
+
TSchema,
|
|
1264
|
+
TServiceCalls,
|
|
1265
|
+
TNewRetrieveResults,
|
|
1266
|
+
TNewRetrieveResults, // Default TRetrieveSuccessResult to TNewRetrieveResults
|
|
1267
|
+
TMutateResult,
|
|
1268
|
+
TTransformResult,
|
|
1269
|
+
true, // HasRetrieve = true
|
|
1270
|
+
false, // Reset HasTransformRetrieve since retrieve results changed
|
|
1271
|
+
HasMutate,
|
|
1272
|
+
HasTransform,
|
|
1273
|
+
THooks
|
|
1274
|
+
> {
|
|
1275
|
+
return new ServiceTxBuilder({
|
|
1276
|
+
...this.#state,
|
|
1277
|
+
retrieveFn: fn,
|
|
1278
|
+
transformRetrieveFn: undefined, // Clear any existing transformRetrieve since results shape changed
|
|
1279
|
+
} as unknown as ServiceTxBuilderState<
|
|
1280
|
+
TSchema,
|
|
1281
|
+
TServiceCalls,
|
|
1282
|
+
TNewRetrieveResults,
|
|
1283
|
+
TNewRetrieveResults,
|
|
1284
|
+
TMutateResult,
|
|
1285
|
+
TTransformResult,
|
|
1286
|
+
THooks
|
|
1287
|
+
>);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Transform retrieve results before passing to mutate.
|
|
1292
|
+
*/
|
|
1293
|
+
transformRetrieve<TNewRetrieveSuccessResult>(
|
|
1294
|
+
fn: (
|
|
1295
|
+
retrieveResult: TRetrieveResults,
|
|
1296
|
+
serviceResult: ExtractServiceRetrieveResults<TServiceCalls>,
|
|
1297
|
+
) => TNewRetrieveSuccessResult,
|
|
1298
|
+
): ServiceTxBuilder<
|
|
1299
|
+
TSchema,
|
|
1300
|
+
TServiceCalls,
|
|
1301
|
+
TRetrieveResults,
|
|
1302
|
+
TNewRetrieveSuccessResult,
|
|
1303
|
+
TMutateResult,
|
|
1304
|
+
TTransformResult,
|
|
1305
|
+
HasRetrieve,
|
|
1306
|
+
true, // HasTransformRetrieve = true
|
|
1307
|
+
HasMutate,
|
|
1308
|
+
HasTransform,
|
|
1309
|
+
THooks
|
|
1310
|
+
> {
|
|
1311
|
+
return new ServiceTxBuilder({
|
|
1312
|
+
...this.#state,
|
|
1313
|
+
transformRetrieveFn: fn,
|
|
1314
|
+
} as unknown as ServiceTxBuilderState<
|
|
1315
|
+
TSchema,
|
|
1316
|
+
TServiceCalls,
|
|
1317
|
+
TRetrieveResults,
|
|
1318
|
+
TNewRetrieveSuccessResult,
|
|
1319
|
+
TMutateResult,
|
|
1320
|
+
TTransformResult,
|
|
1321
|
+
THooks
|
|
1322
|
+
>);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Add mutation operations based on retrieve results.
|
|
1327
|
+
*/
|
|
1328
|
+
mutate<TNewMutateResult>(
|
|
1329
|
+
fn: (
|
|
1330
|
+
ctx: ServiceBuilderMutateContext<
|
|
1331
|
+
TSchema,
|
|
1332
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1333
|
+
ExtractServiceRetrieveResults<TServiceCalls>,
|
|
1334
|
+
THooks
|
|
1335
|
+
>,
|
|
1336
|
+
) => TNewMutateResult,
|
|
1337
|
+
): ServiceTxBuilder<
|
|
1338
|
+
TSchema,
|
|
1339
|
+
TServiceCalls,
|
|
1340
|
+
TRetrieveResults,
|
|
1341
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1342
|
+
TNewMutateResult,
|
|
1343
|
+
TTransformResult,
|
|
1344
|
+
HasRetrieve,
|
|
1345
|
+
HasTransformRetrieve,
|
|
1346
|
+
true, // HasMutate = true
|
|
1347
|
+
HasTransform,
|
|
1348
|
+
THooks
|
|
1349
|
+
> {
|
|
1350
|
+
return new ServiceTxBuilder({
|
|
1351
|
+
...this.#state,
|
|
1352
|
+
mutateFn: fn,
|
|
1353
|
+
} as unknown as ServiceTxBuilderState<
|
|
1354
|
+
TSchema,
|
|
1355
|
+
TServiceCalls,
|
|
1356
|
+
TRetrieveResults,
|
|
1357
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1358
|
+
TNewMutateResult,
|
|
1359
|
+
TTransformResult,
|
|
1360
|
+
THooks
|
|
1361
|
+
>);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Add final transformation after mutations complete.
|
|
1366
|
+
*/
|
|
1367
|
+
transform<TNewTransformResult>(
|
|
1368
|
+
fn: (
|
|
1369
|
+
ctx: HasMutate extends true
|
|
1370
|
+
? BuilderTransformContextWithMutate<
|
|
1371
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1372
|
+
TMutateResult,
|
|
1373
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
1374
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
1375
|
+
>
|
|
1376
|
+
: BuilderTransformContextWithoutMutate<
|
|
1377
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1378
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
1379
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
1380
|
+
>,
|
|
1381
|
+
) => TNewTransformResult,
|
|
1382
|
+
): ServiceTxBuilder<
|
|
1383
|
+
TSchema,
|
|
1384
|
+
TServiceCalls,
|
|
1385
|
+
TRetrieveResults,
|
|
1386
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1387
|
+
TMutateResult,
|
|
1388
|
+
TNewTransformResult,
|
|
1389
|
+
HasRetrieve,
|
|
1390
|
+
HasTransformRetrieve,
|
|
1391
|
+
HasMutate,
|
|
1392
|
+
true, // HasTransform = true
|
|
1393
|
+
THooks
|
|
1394
|
+
> {
|
|
1395
|
+
return new ServiceTxBuilder({
|
|
1396
|
+
...this.#state,
|
|
1397
|
+
transformFn: fn,
|
|
1398
|
+
} as unknown as ServiceTxBuilderState<
|
|
1399
|
+
TSchema,
|
|
1400
|
+
TServiceCalls,
|
|
1401
|
+
TRetrieveResults,
|
|
1402
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1403
|
+
TMutateResult,
|
|
1404
|
+
TNewTransformResult,
|
|
1405
|
+
THooks
|
|
1406
|
+
>);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Build and return the TxResult.
|
|
1411
|
+
*/
|
|
1412
|
+
build(): TxResult<
|
|
1413
|
+
InferBuilderResultType<
|
|
1414
|
+
TRetrieveResults,
|
|
1415
|
+
TRetrieveSuccessResult,
|
|
1416
|
+
TServiceCalls,
|
|
1417
|
+
TMutateResult,
|
|
1418
|
+
TTransformResult,
|
|
1419
|
+
HasTransform,
|
|
1420
|
+
HasMutate,
|
|
1421
|
+
HasTransformRetrieve,
|
|
1422
|
+
HasRetrieve
|
|
1423
|
+
>,
|
|
1424
|
+
InferBuilderRetrieveSuccessResult<
|
|
1425
|
+
TRetrieveResults,
|
|
1426
|
+
TRetrieveSuccessResult,
|
|
1427
|
+
TMutateResult,
|
|
1428
|
+
HasTransformRetrieve,
|
|
1429
|
+
HasRetrieve,
|
|
1430
|
+
HasMutate
|
|
1431
|
+
>
|
|
1432
|
+
> {
|
|
1433
|
+
const state = this.#state;
|
|
1434
|
+
|
|
1435
|
+
// Convert builder state to legacy callbacks format
|
|
1436
|
+
const callbacks: ServiceTxCallbacks<
|
|
1437
|
+
TSchema,
|
|
1438
|
+
TRetrieveResults,
|
|
1439
|
+
TRetrieveSuccessResult,
|
|
1440
|
+
TServiceCalls,
|
|
1441
|
+
TMutateResult,
|
|
1442
|
+
TTransformResult,
|
|
1443
|
+
THooks
|
|
1444
|
+
> = {
|
|
1445
|
+
serviceCalls: state.withServiceCallsFn,
|
|
1446
|
+
retrieve: state.retrieveFn,
|
|
1447
|
+
retrieveSuccess: state.transformRetrieveFn,
|
|
1448
|
+
mutate: state.mutateFn
|
|
1449
|
+
? (ctx) => {
|
|
1450
|
+
return state.mutateFn!({
|
|
1451
|
+
uow: ctx.uow,
|
|
1452
|
+
retrieveResult: ctx.retrieveResult,
|
|
1453
|
+
serviceIntermediateResult: ctx.serviceIntermediateResult,
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
: undefined,
|
|
1457
|
+
success: state.transformFn
|
|
1458
|
+
? (ctx) => {
|
|
1459
|
+
return state.transformFn!({
|
|
1460
|
+
retrieveResult: ctx.retrieveResult,
|
|
1461
|
+
mutateResult: ctx.mutateResult,
|
|
1462
|
+
serviceResult: ctx.serviceResult,
|
|
1463
|
+
serviceIntermediateResult: ctx.serviceIntermediateResult,
|
|
1464
|
+
} as BuilderTransformContextWithMutate<
|
|
1465
|
+
TRetrieveSuccessResult,
|
|
1466
|
+
TMutateResult,
|
|
1467
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
1468
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
1469
|
+
>);
|
|
1470
|
+
}
|
|
1471
|
+
: undefined,
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
// Use the existing createServiceTx implementation
|
|
1475
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1476
|
+
return createServiceTx(state.schema, callbacks as any, state.baseUow) as unknown as TxResult<
|
|
1477
|
+
InferBuilderResultType<
|
|
1478
|
+
TRetrieveResults,
|
|
1479
|
+
TRetrieveSuccessResult,
|
|
1480
|
+
TServiceCalls,
|
|
1481
|
+
TMutateResult,
|
|
1482
|
+
TTransformResult,
|
|
1483
|
+
HasTransform,
|
|
1484
|
+
HasMutate,
|
|
1485
|
+
HasTransformRetrieve,
|
|
1486
|
+
HasRetrieve
|
|
1487
|
+
>,
|
|
1488
|
+
InferBuilderRetrieveSuccessResult<
|
|
1489
|
+
TRetrieveResults,
|
|
1490
|
+
TRetrieveSuccessResult,
|
|
1491
|
+
TMutateResult,
|
|
1492
|
+
HasTransformRetrieve,
|
|
1493
|
+
HasRetrieve,
|
|
1494
|
+
HasMutate
|
|
1495
|
+
>
|
|
1496
|
+
>;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Create a new ServiceTxBuilder for the given schema.
|
|
1502
|
+
*/
|
|
1503
|
+
export function createServiceTxBuilder<TSchema extends AnySchema, THooks extends HooksMap = {}>(
|
|
1504
|
+
schema: TSchema,
|
|
1505
|
+
baseUow: IUnitOfWork,
|
|
1506
|
+
hooks?: THooks,
|
|
1507
|
+
): ServiceTxBuilder<
|
|
1508
|
+
TSchema,
|
|
1509
|
+
readonly [],
|
|
1510
|
+
[],
|
|
1511
|
+
[],
|
|
1512
|
+
unknown,
|
|
1513
|
+
unknown,
|
|
1514
|
+
false,
|
|
1515
|
+
false,
|
|
1516
|
+
false,
|
|
1517
|
+
false,
|
|
1518
|
+
THooks
|
|
1519
|
+
> {
|
|
1520
|
+
return new ServiceTxBuilder({
|
|
1521
|
+
schema,
|
|
1522
|
+
baseUow,
|
|
1523
|
+
hooks,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Internal state for HandlerTxBuilder
|
|
1529
|
+
*/
|
|
1530
|
+
interface HandlerTxBuilderState<
|
|
1531
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1532
|
+
TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
|
|
1533
|
+
TRetrieveResults extends unknown[],
|
|
1534
|
+
TRetrieveSuccessResult,
|
|
1535
|
+
TMutateResult,
|
|
1536
|
+
TTransformResult,
|
|
1537
|
+
THooks extends HooksMap,
|
|
1538
|
+
> {
|
|
1539
|
+
options: ExecuteTxOptions;
|
|
1540
|
+
hooks?: THooks;
|
|
1541
|
+
withServiceCallsFn?: () => TServiceCalls;
|
|
1542
|
+
retrieveFn?: (context: {
|
|
1543
|
+
forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
|
|
1544
|
+
schema: S,
|
|
1545
|
+
hooks?: H,
|
|
1546
|
+
) => TypedUnitOfWork<S, [], unknown, H>;
|
|
1547
|
+
idempotencyKey: string;
|
|
1548
|
+
currentAttempt: number;
|
|
1549
|
+
}) => TypedUnitOfWork<AnySchema, TRetrieveResults, unknown, HooksMap> | void;
|
|
1550
|
+
transformRetrieveFn?: (
|
|
1551
|
+
retrieveResult: TRetrieveResults,
|
|
1552
|
+
serviceResult: ExtractServiceRetrieveResults<TServiceCalls>,
|
|
1553
|
+
) => TRetrieveSuccessResult;
|
|
1554
|
+
mutateFn?: (
|
|
1555
|
+
ctx: HandlerBuilderMutateContext<
|
|
1556
|
+
TRetrieveSuccessResult,
|
|
1557
|
+
ExtractServiceRetrieveResults<TServiceCalls>,
|
|
1558
|
+
THooks
|
|
1559
|
+
>,
|
|
1560
|
+
) => TMutateResult;
|
|
1561
|
+
transformFn?: (
|
|
1562
|
+
ctx:
|
|
1563
|
+
| BuilderTransformContextWithMutate<
|
|
1564
|
+
TRetrieveSuccessResult,
|
|
1565
|
+
TMutateResult,
|
|
1566
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
1567
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
1568
|
+
>
|
|
1569
|
+
| BuilderTransformContextWithoutMutate<
|
|
1570
|
+
TRetrieveSuccessResult,
|
|
1571
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
1572
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
1573
|
+
>,
|
|
1574
|
+
) => TTransformResult;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Builder for handler-level transactions.
|
|
1579
|
+
* Uses a fluent API to build up transaction callbacks with proper type inference.
|
|
1580
|
+
*
|
|
1581
|
+
* @example
|
|
1582
|
+
* ```ts
|
|
1583
|
+
* const result = await handlerTx()
|
|
1584
|
+
* .withServiceCalls(() => [userService.getUser(id)])
|
|
1585
|
+
* .mutate(({ forSchema, idempotencyKey, currentAttempt, serviceIntermediateResult }) => {
|
|
1586
|
+
* return forSchema(ordersSchema).create("orders", { ... });
|
|
1587
|
+
* })
|
|
1588
|
+
* .transform(({ mutateResult, serviceResult }) => ({ ... }))
|
|
1589
|
+
* .execute();
|
|
1590
|
+
* ```
|
|
1591
|
+
*/
|
|
1592
|
+
export class HandlerTxBuilder<
|
|
1593
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1594
|
+
TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
|
|
1595
|
+
TRetrieveResults extends unknown[],
|
|
1596
|
+
TRetrieveSuccessResult,
|
|
1597
|
+
TMutateResult,
|
|
1598
|
+
TTransformResult,
|
|
1599
|
+
HasRetrieve extends boolean,
|
|
1600
|
+
HasTransformRetrieve extends boolean,
|
|
1601
|
+
HasMutate extends boolean,
|
|
1602
|
+
HasTransform extends boolean,
|
|
1603
|
+
THooks extends HooksMap,
|
|
1604
|
+
> {
|
|
1605
|
+
readonly #state: HandlerTxBuilderState<
|
|
1606
|
+
TServiceCalls,
|
|
1607
|
+
TRetrieveResults,
|
|
1608
|
+
TRetrieveSuccessResult,
|
|
1609
|
+
TMutateResult,
|
|
1610
|
+
TTransformResult,
|
|
1611
|
+
THooks
|
|
1612
|
+
>;
|
|
1613
|
+
|
|
1614
|
+
constructor(
|
|
1615
|
+
state: HandlerTxBuilderState<
|
|
1616
|
+
TServiceCalls,
|
|
1617
|
+
TRetrieveResults,
|
|
1618
|
+
TRetrieveSuccessResult,
|
|
1619
|
+
TMutateResult,
|
|
1620
|
+
TTransformResult,
|
|
1621
|
+
THooks
|
|
1622
|
+
>,
|
|
1623
|
+
) {
|
|
1624
|
+
this.#state = state;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Add dependencies to execute before this transaction.
|
|
1629
|
+
*/
|
|
1630
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1631
|
+
withServiceCalls<TNewDeps extends readonly (TxResult<any, any> | undefined)[]>(
|
|
1632
|
+
fn: () => TNewDeps,
|
|
1633
|
+
): HandlerTxBuilder<
|
|
1634
|
+
TNewDeps,
|
|
1635
|
+
TRetrieveResults,
|
|
1636
|
+
TRetrieveSuccessResult,
|
|
1637
|
+
TMutateResult,
|
|
1638
|
+
TTransformResult,
|
|
1639
|
+
HasRetrieve,
|
|
1640
|
+
HasTransformRetrieve,
|
|
1641
|
+
HasMutate,
|
|
1642
|
+
HasTransform,
|
|
1643
|
+
THooks
|
|
1644
|
+
> {
|
|
1645
|
+
return new HandlerTxBuilder({
|
|
1646
|
+
...this.#state,
|
|
1647
|
+
withServiceCallsFn: fn,
|
|
1648
|
+
} as HandlerTxBuilderState<
|
|
1649
|
+
TNewDeps,
|
|
1650
|
+
TRetrieveResults,
|
|
1651
|
+
TRetrieveSuccessResult,
|
|
1652
|
+
TMutateResult,
|
|
1653
|
+
TTransformResult,
|
|
1654
|
+
THooks
|
|
1655
|
+
>);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Add retrieval operations to the transaction.
|
|
1660
|
+
* Return a TypedUnitOfWork from forSchema().find() to get typed results.
|
|
1661
|
+
*/
|
|
1662
|
+
retrieve<TNewRetrieveResults extends unknown[]>(
|
|
1663
|
+
fn: (context: {
|
|
1664
|
+
forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
|
|
1665
|
+
schema: S,
|
|
1666
|
+
hooks?: H,
|
|
1667
|
+
) => TypedUnitOfWork<S, [], unknown, H>;
|
|
1668
|
+
idempotencyKey: string;
|
|
1669
|
+
currentAttempt: number;
|
|
1670
|
+
}) => TypedUnitOfWork<AnySchema, TNewRetrieveResults, unknown, HooksMap> | void,
|
|
1671
|
+
): HandlerTxBuilder<
|
|
1672
|
+
TServiceCalls,
|
|
1673
|
+
TNewRetrieveResults,
|
|
1674
|
+
TNewRetrieveResults, // Default TRetrieveSuccessResult to TNewRetrieveResults
|
|
1675
|
+
TMutateResult,
|
|
1676
|
+
TTransformResult,
|
|
1677
|
+
true, // HasRetrieve = true
|
|
1678
|
+
false, // Reset HasTransformRetrieve since retrieve results changed
|
|
1679
|
+
HasMutate,
|
|
1680
|
+
HasTransform,
|
|
1681
|
+
THooks
|
|
1682
|
+
> {
|
|
1683
|
+
return new HandlerTxBuilder({
|
|
1684
|
+
...this.#state,
|
|
1685
|
+
retrieveFn: fn,
|
|
1686
|
+
transformRetrieveFn: undefined, // Clear any existing transformRetrieve since results shape changed
|
|
1687
|
+
} as unknown as HandlerTxBuilderState<
|
|
1688
|
+
TServiceCalls,
|
|
1689
|
+
TNewRetrieveResults,
|
|
1690
|
+
TNewRetrieveResults,
|
|
1691
|
+
TMutateResult,
|
|
1692
|
+
TTransformResult,
|
|
1693
|
+
THooks
|
|
1694
|
+
>);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Transform retrieve results before passing to mutate.
|
|
1699
|
+
*/
|
|
1700
|
+
transformRetrieve<TNewRetrieveSuccessResult>(
|
|
1701
|
+
fn: (
|
|
1702
|
+
retrieveResult: TRetrieveResults,
|
|
1703
|
+
serviceResult: ExtractServiceRetrieveResults<TServiceCalls>,
|
|
1704
|
+
) => TNewRetrieveSuccessResult,
|
|
1705
|
+
): HandlerTxBuilder<
|
|
1706
|
+
TServiceCalls,
|
|
1707
|
+
TRetrieveResults,
|
|
1708
|
+
TNewRetrieveSuccessResult,
|
|
1709
|
+
TMutateResult,
|
|
1710
|
+
TTransformResult,
|
|
1711
|
+
HasRetrieve,
|
|
1712
|
+
true, // HasTransformRetrieve = true
|
|
1713
|
+
HasMutate,
|
|
1714
|
+
HasTransform,
|
|
1715
|
+
THooks
|
|
1716
|
+
> {
|
|
1717
|
+
return new HandlerTxBuilder({
|
|
1718
|
+
...this.#state,
|
|
1719
|
+
transformRetrieveFn: fn,
|
|
1720
|
+
} as unknown as HandlerTxBuilderState<
|
|
1721
|
+
TServiceCalls,
|
|
1722
|
+
TRetrieveResults,
|
|
1723
|
+
TNewRetrieveSuccessResult,
|
|
1724
|
+
TMutateResult,
|
|
1725
|
+
TTransformResult,
|
|
1726
|
+
THooks
|
|
1727
|
+
>);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* Add mutation operations based on retrieve results.
|
|
1732
|
+
*/
|
|
1733
|
+
mutate<TNewMutateResult>(
|
|
1734
|
+
fn: (
|
|
1735
|
+
ctx: HandlerBuilderMutateContext<
|
|
1736
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1737
|
+
ExtractServiceRetrieveResults<TServiceCalls>,
|
|
1738
|
+
THooks
|
|
1739
|
+
>,
|
|
1740
|
+
) => TNewMutateResult,
|
|
1741
|
+
): HandlerTxBuilder<
|
|
1742
|
+
TServiceCalls,
|
|
1743
|
+
TRetrieveResults,
|
|
1744
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1745
|
+
TNewMutateResult,
|
|
1746
|
+
TTransformResult,
|
|
1747
|
+
HasRetrieve,
|
|
1748
|
+
HasTransformRetrieve,
|
|
1749
|
+
true, // HasMutate = true
|
|
1750
|
+
HasTransform,
|
|
1751
|
+
THooks
|
|
1752
|
+
> {
|
|
1753
|
+
return new HandlerTxBuilder({
|
|
1754
|
+
...this.#state,
|
|
1755
|
+
mutateFn: fn,
|
|
1756
|
+
} as unknown as HandlerTxBuilderState<
|
|
1757
|
+
TServiceCalls,
|
|
1758
|
+
TRetrieveResults,
|
|
1759
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1760
|
+
TNewMutateResult,
|
|
1761
|
+
TTransformResult,
|
|
1762
|
+
THooks
|
|
1763
|
+
>);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Add final transformation after mutations complete.
|
|
1768
|
+
*/
|
|
1769
|
+
transform<TNewTransformResult>(
|
|
1770
|
+
fn: (
|
|
1771
|
+
ctx: HasMutate extends true
|
|
1772
|
+
? BuilderTransformContextWithMutate<
|
|
1773
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1774
|
+
TMutateResult,
|
|
1775
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
1776
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
1777
|
+
>
|
|
1778
|
+
: BuilderTransformContextWithoutMutate<
|
|
1779
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1780
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
1781
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
1782
|
+
>,
|
|
1783
|
+
) => TNewTransformResult,
|
|
1784
|
+
): HandlerTxBuilder<
|
|
1785
|
+
TServiceCalls,
|
|
1786
|
+
TRetrieveResults,
|
|
1787
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1788
|
+
TMutateResult,
|
|
1789
|
+
TNewTransformResult,
|
|
1790
|
+
HasRetrieve,
|
|
1791
|
+
HasTransformRetrieve,
|
|
1792
|
+
HasMutate,
|
|
1793
|
+
true, // HasTransform = true
|
|
1794
|
+
THooks
|
|
1795
|
+
> {
|
|
1796
|
+
return new HandlerTxBuilder({
|
|
1797
|
+
...this.#state,
|
|
1798
|
+
transformFn: fn,
|
|
1799
|
+
} as unknown as HandlerTxBuilderState<
|
|
1800
|
+
TServiceCalls,
|
|
1801
|
+
TRetrieveResults,
|
|
1802
|
+
HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
|
|
1803
|
+
TMutateResult,
|
|
1804
|
+
TNewTransformResult,
|
|
1805
|
+
THooks
|
|
1806
|
+
>);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Execute the transaction and return the result.
|
|
1811
|
+
*/
|
|
1812
|
+
execute(): Promise<
|
|
1813
|
+
AwaitedPromisesInObject<
|
|
1814
|
+
InferBuilderResultType<
|
|
1815
|
+
TRetrieveResults,
|
|
1816
|
+
TRetrieveSuccessResult,
|
|
1817
|
+
TServiceCalls,
|
|
1818
|
+
TMutateResult,
|
|
1819
|
+
TTransformResult,
|
|
1820
|
+
HasTransform,
|
|
1821
|
+
HasMutate,
|
|
1822
|
+
HasTransformRetrieve,
|
|
1823
|
+
HasRetrieve
|
|
1824
|
+
>
|
|
1825
|
+
>
|
|
1826
|
+
> {
|
|
1827
|
+
const state = this.#state;
|
|
1828
|
+
|
|
1829
|
+
// Convert builder state to legacy callbacks format
|
|
1830
|
+
const callbacks: HandlerTxCallbacks<
|
|
1831
|
+
TRetrieveResults,
|
|
1832
|
+
TRetrieveSuccessResult,
|
|
1833
|
+
TServiceCalls,
|
|
1834
|
+
TMutateResult,
|
|
1835
|
+
TTransformResult,
|
|
1836
|
+
THooks
|
|
1837
|
+
> = {
|
|
1838
|
+
serviceCalls: state.withServiceCallsFn,
|
|
1839
|
+
retrieve: state.retrieveFn
|
|
1840
|
+
? (context) => {
|
|
1841
|
+
return state.retrieveFn!({
|
|
1842
|
+
forSchema: context.forSchema,
|
|
1843
|
+
idempotencyKey: context.idempotencyKey,
|
|
1844
|
+
currentAttempt: context.currentAttempt,
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
: undefined,
|
|
1848
|
+
retrieveSuccess: state.transformRetrieveFn,
|
|
1849
|
+
mutate: state.mutateFn
|
|
1850
|
+
? (ctx) => {
|
|
1851
|
+
return state.mutateFn!({
|
|
1852
|
+
forSchema: ctx.forSchema,
|
|
1853
|
+
idempotencyKey: ctx.idempotencyKey,
|
|
1854
|
+
currentAttempt: ctx.currentAttempt,
|
|
1855
|
+
retrieveResult: ctx.retrieveResult,
|
|
1856
|
+
serviceIntermediateResult: ctx.serviceIntermediateResult,
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
: undefined,
|
|
1860
|
+
success: state.transformFn
|
|
1861
|
+
? (ctx) => {
|
|
1862
|
+
return state.transformFn!({
|
|
1863
|
+
retrieveResult: ctx.retrieveResult,
|
|
1864
|
+
mutateResult: ctx.mutateResult,
|
|
1865
|
+
serviceResult: ctx.serviceResult,
|
|
1866
|
+
serviceIntermediateResult: ctx.serviceIntermediateResult,
|
|
1867
|
+
} as BuilderTransformContextWithMutate<
|
|
1868
|
+
TRetrieveSuccessResult,
|
|
1869
|
+
TMutateResult,
|
|
1870
|
+
ExtractServiceFinalResults<TServiceCalls>,
|
|
1871
|
+
ExtractServiceRetrieveResults<TServiceCalls>
|
|
1872
|
+
>);
|
|
1873
|
+
}
|
|
1874
|
+
: undefined,
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
// Use the existing executeTx implementation
|
|
1878
|
+
return executeTx(callbacks as Parameters<typeof executeTx>[0], state.options) as Promise<
|
|
1879
|
+
AwaitedPromisesInObject<
|
|
1880
|
+
InferBuilderResultType<
|
|
1881
|
+
TRetrieveResults,
|
|
1882
|
+
TRetrieveSuccessResult,
|
|
1883
|
+
TServiceCalls,
|
|
1884
|
+
TMutateResult,
|
|
1885
|
+
TTransformResult,
|
|
1886
|
+
HasTransform,
|
|
1887
|
+
HasMutate,
|
|
1888
|
+
HasTransformRetrieve,
|
|
1889
|
+
HasRetrieve
|
|
1890
|
+
>
|
|
1891
|
+
>
|
|
1892
|
+
>;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/**
|
|
1897
|
+
* Create a new HandlerTxBuilder with the given options.
|
|
1898
|
+
*/
|
|
1899
|
+
export function createHandlerTxBuilder<THooks extends HooksMap = {}>(
|
|
1900
|
+
options: ExecuteTxOptions,
|
|
1901
|
+
hooks?: THooks,
|
|
1902
|
+
): HandlerTxBuilder<readonly [], [], [], unknown, unknown, false, false, false, false, THooks> {
|
|
1903
|
+
return new HandlerTxBuilder({
|
|
1904
|
+
options,
|
|
1905
|
+
hooks,
|
|
1906
|
+
});
|
|
1907
|
+
}
|