@fragno-dev/db 0.2.1 → 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.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +34 -30
  2. package/CHANGELOG.md +32 -0
  3. package/dist/adapters/generic-sql/query/where-builder.js +1 -1
  4. package/dist/db-fragment-definition-builder.d.ts +27 -89
  5. package/dist/db-fragment-definition-builder.d.ts.map +1 -1
  6. package/dist/db-fragment-definition-builder.js +16 -56
  7. package/dist/db-fragment-definition-builder.js.map +1 -1
  8. package/dist/fragments/internal-fragment.d.ts +94 -8
  9. package/dist/fragments/internal-fragment.d.ts.map +1 -1
  10. package/dist/fragments/internal-fragment.js +56 -55
  11. package/dist/fragments/internal-fragment.js.map +1 -1
  12. package/dist/hooks/hooks.d.ts +5 -3
  13. package/dist/hooks/hooks.d.ts.map +1 -1
  14. package/dist/hooks/hooks.js +38 -37
  15. package/dist/hooks/hooks.js.map +1 -1
  16. package/dist/mod.d.ts +3 -3
  17. package/dist/mod.d.ts.map +1 -1
  18. package/dist/mod.js +4 -4
  19. package/dist/mod.js.map +1 -1
  20. package/dist/query/unit-of-work/execute-unit-of-work.d.ts +351 -100
  21. package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
  22. package/dist/query/unit-of-work/execute-unit-of-work.js +431 -263
  23. package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
  24. package/dist/query/unit-of-work/unit-of-work.d.ts +17 -8
  25. package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
  26. package/dist/query/unit-of-work/unit-of-work.js +24 -8
  27. package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
  28. package/dist/query/value-decoding.js +1 -1
  29. package/dist/schema/create.d.ts +3 -1
  30. package/dist/schema/create.d.ts.map +1 -1
  31. package/dist/schema/create.js +2 -1
  32. package/dist/schema/create.js.map +1 -1
  33. package/dist/schema/generate-id.d.ts +20 -0
  34. package/dist/schema/generate-id.d.ts.map +1 -0
  35. package/dist/schema/generate-id.js +28 -0
  36. package/dist/schema/generate-id.js.map +1 -0
  37. package/package.json +1 -1
  38. package/src/adapters/drizzle/drizzle-adapter-sqlite3.test.ts +41 -25
  39. package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +39 -25
  40. package/src/db-fragment-definition-builder.test.ts +58 -42
  41. package/src/db-fragment-definition-builder.ts +58 -248
  42. package/src/db-fragment-instantiator.test.ts +64 -88
  43. package/src/db-fragment-integration.test.ts +292 -142
  44. package/src/fragments/internal-fragment.test.ts +272 -266
  45. package/src/fragments/internal-fragment.ts +155 -121
  46. package/src/hooks/hooks.test.ts +248 -256
  47. package/src/hooks/hooks.ts +74 -63
  48. package/src/mod.ts +14 -4
  49. package/src/query/unit-of-work/execute-unit-of-work.test.ts +1494 -1464
  50. package/src/query/unit-of-work/execute-unit-of-work.ts +1685 -590
  51. package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
  52. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +20 -20
  53. package/src/query/unit-of-work/unit-of-work.test.ts +64 -0
  54. package/src/query/unit-of-work/unit-of-work.ts +26 -13
  55. package/src/schema/create.ts +2 -0
  56. package/src/schema/generate-id.test.ts +57 -0
  57. package/src/schema/generate-id.ts +38 -0
  58. package/src/shared/config.ts +0 -10
  59. package/src/shared/connection-pool.ts +0 -24
  60. package/src/shared/prisma.ts +0 -45
@@ -1,331 +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 { NoRetryPolicy, ExponentialBackoffRetryPolicy, type RetryPolicy } from "./retry-policy";
5
- import type { FragnoId } from "../../schema/create";
4
+ import { ExponentialBackoffRetryPolicy, type RetryPolicy } from "./retry-policy";
6
5
 
7
6
  /**
8
- * Error thrown when a Unit of Work execution fails due to optimistic concurrency conflict.
9
- * This error triggers automatic retry behavior in executeRestrictedUnitOfWork.
7
+ * Symbol to identify TxResult objects
10
8
  */
11
- export class ConcurrencyConflictError extends Error {
12
- constructor(message = "Optimistic concurrency conflict detected") {
13
- super(message);
14
- this.name = "ConcurrencyConflictError";
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
- * Type utility that unwraps promises 1 level deep in objects, arrays, or direct promises
20
- * Handles tuples, arrays, objects, and direct promises
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 AwaitedPromisesInObject<T> =
23
- // First check if it's a Promise
24
- T extends Promise<infer U>
25
- ? Awaited<U>
26
- : // Check for arrays with known length (tuples) - preserves tuple structure
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
- * Await promises in an object 1 level deep
36
+ * Extract the final result type from a TxResult.
37
+ * Handles undefined (for optional service patterns like optionalService?.method()).
46
38
  */
47
- async function awaitPromisesInObject<T>(obj: T): Promise<AwaitedPromisesInObject<T>> {
48
- if (obj === null || obj === undefined) {
49
- return obj as AwaitedPromisesInObject<T>;
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
- if (typeof obj !== "object") {
53
- return obj as AwaitedPromisesInObject<T>;
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
- // Check if it's a Promise
57
- if (obj instanceof Promise) {
58
- return (await obj) as AwaitedPromisesInObject<T>;
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
- // Check if it's an array
62
- if (Array.isArray(obj)) {
63
- const awaited = await Promise.all(
64
- obj.map((item) => (item instanceof Promise ? item : Promise.resolve(item))),
65
- );
66
- return awaited as AwaitedPromisesInObject<T>;
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
- // It's a plain object - await promises in each property
70
- const result = {} as T;
71
- const entries = Object.entries(obj as Record<string, unknown>);
72
- const awaitedEntries = await Promise.all(
73
- entries.map(async ([key, value]) => {
74
- const awaitedValue = value instanceof Promise ? await value : value;
75
- return [key, awaitedValue] as const;
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
- for (const [key, value] of awaitedEntries) {
80
- (result as Record<string, unknown>)[key] = value;
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
- return result as AwaitedPromisesInObject<T>;
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
- * Result of executing a Unit of Work with retry support
88
- * Promises in mutationResult are unwrapped 1 level deep
127
+ * Context passed to success callback when mutate is NOT provided
89
128
  */
90
- export type ExecuteUnitOfWorkResult<TRetrievalResults, TMutationResult> =
91
- | {
92
- success: true;
93
- results: TRetrievalResults;
94
- mutationResult: AwaitedPromisesInObject<TMutationResult>;
95
- createdIds: FragnoId[];
96
- nonce: string;
97
- }
98
- | {
99
- success: false;
100
- reason: "conflict";
101
- }
102
- | {
103
- success: false;
104
- reason: "aborted";
105
- }
106
- | {
107
- success: false;
108
- reason: "error";
109
- error: unknown;
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 executing a Unit of Work
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 ExecuteUnitOfWorkCallbacks<
177
+ export interface ServiceTxCallbacks<
116
178
  TSchema extends AnySchema,
117
- TRetrievalResults extends unknown[],
118
- TMutationResult,
119
- TRawInput,
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
- * Retrieval phase callback - adds retrieval operations to the UOW
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, [], TRawInput>,
126
- ) => TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>;
196
+ uow: TypedUnitOfWork<TSchema, [], unknown, THooks>,
197
+ ) => TypedUnitOfWork<TSchema, TRetrieveResults, unknown, THooks>;
127
198
 
128
199
  /**
129
- * Mutation phase callback - receives UOW and retrieval results, adds mutation operations
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
- uow: TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>,
133
- results: TRetrievalResults,
134
- ) => TMutationResult | Promise<TMutationResult>;
211
+ ctx: ServiceTxMutateContext<
212
+ TSchema,
213
+ TRetrieveSuccessResult,
214
+ ExtractServiceRetrieveResults<TServiceCalls>,
215
+ THooks
216
+ >,
217
+ ) => TMutateResult;
135
218
 
136
219
  /**
137
- * Success callback - invoked after successful execution
138
- * Promises in mutationResult are already unwrapped 1 level deep
220
+ * Success callback - final transformation after mutations complete.
139
221
  */
140
- onSuccess?: (result: {
141
- results: TRetrievalResults;
142
- mutationResult: AwaitedPromisesInObject<TMutationResult>;
143
- createdIds: FragnoId[];
144
- nonce: string;
145
- }) => void | Promise<void>;
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
- * Options for executing a Unit of Work
233
+ * Callbacks for handler-level executeTx.
234
+ * Uses context-based callbacks that provide forSchema() method.
150
235
  */
151
- export interface ExecuteUnitOfWorkOptions<TSchema extends AnySchema, TRawInput> {
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
- * Factory function that creates or resets a UOW instance for each attempt
246
+ * Service calls - other TxResults to execute first.
154
247
  */
155
- createUnitOfWork: () => TypedUnitOfWork<TSchema, [], TRawInput>;
248
+ serviceCalls?: () => TServiceCalls;
156
249
 
157
250
  /**
158
- * Retry policy for handling optimistic concurrency conflicts
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
- retryPolicy?: RetryPolicy;
254
+ retrieve?: (
255
+ context: HandlerTxContext<THooks>,
256
+ ) => TypedUnitOfWork<AnySchema, TRetrieveResults, unknown, HooksMap> | void;
161
257
 
162
258
  /**
163
- * Abort signal to cancel execution
259
+ * Transform retrieve results before passing to mutate.
164
260
  */
165
- signal?: AbortSignal;
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
- * Create a bound version of executeUnitOfWork with a pre-configured UOW factory.
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
- export function createExecuteUnitOfWork<TSchema extends AnySchema, TRawInput>(
187
- createUnitOfWork: () => TypedUnitOfWork<TSchema, [], TRawInput>,
188
- ) {
189
- return async function <TRetrievalResults extends unknown[], TMutationResult = void>(
190
- callbacks: ExecuteUnitOfWorkCallbacks<TSchema, TRetrievalResults, TMutationResult, TRawInput>,
191
- options?: Omit<ExecuteUnitOfWorkOptions<TSchema, TRawInput>, "createUnitOfWork">,
192
- ): Promise<ExecuteUnitOfWorkResult<TRetrievalResults, TMutationResult>> {
193
- return executeUnitOfWork(callbacks, { ...options, createUnitOfWork });
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
- * Execute a Unit of Work with automatic retry support for optimistic concurrency conflicts.
199
- *
200
- * This function orchestrates the two-phase execution (retrieval + mutation) with retry logic.
201
- * It creates fresh UOW instances for each attempt.
333
+ * TxResult represents a transaction definition (not yet executed).
334
+ * It describes the work to be done: retrieve operations, transformations, and mutations.
202
335
  *
203
- * @param callbacks - Object containing retrieve, mutate, and onSuccess callbacks
204
- * @param options - Configuration including UOW factory, retry policy, and abort signal
205
- * @returns Promise resolving to the execution result
336
+ * Service methods return TxResult objects, and the handler's executeTx function
337
+ * orchestrates their execution with retry support.
206
338
  *
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 async function executeUnitOfWork<
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
- TRetrievalResults extends unknown[],
230
- TMutationResult = void,
231
- TRawInput = unknown,
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
- callbacks: ExecuteUnitOfWorkCallbacks<TSchema, TRetrievalResults, TMutationResult, TRawInput>,
234
- options: ExecuteUnitOfWorkOptions<TSchema, TRawInput>,
235
- ): Promise<ExecuteUnitOfWorkResult<TRetrievalResults, TMutationResult>> {
236
- // Validate that at least one of retrieve or mutate is provided
237
- if (!callbacks.retrieve && !callbacks.mutate) {
238
- throw new Error("At least one of 'retrieve' or 'mutate' callbacks must be provided");
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>();
393
+
394
+ // Get a restricted view that signals readiness
395
+ const restrictedUow = baseUow.restrict({ readyFor: "none" });
396
+
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();
402
+ }
403
+ } catch (error) {
404
+ restrictedUow.signalReadyForRetrieval();
405
+ restrictedUow.signalReadyForMutation();
406
+ retrievePhase.catch(() => {});
407
+ rejectRetrievePhase(error);
408
+ throw error;
239
409
  }
240
-
241
- const retryPolicy = options.retryPolicy ?? new NoRetryPolicy();
242
- const signal = options.signal;
243
- let attempt = 0;
244
-
245
- while (true) {
246
- // Check if aborted before starting attempt
247
- if (signal?.aborted) {
248
- return { success: false, reason: "aborted" };
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);
249
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
- try {
252
- // Create a fresh UOW for this attempt
253
- const uow = options.createUnitOfWork();
254
-
255
- // Apply retrieval phase if provided
256
- let retrievalUow: TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>;
257
- if (callbacks.retrieve) {
258
- retrievalUow = callbacks.retrieve(uow);
259
- } else {
260
- // No retrieval phase, use empty UOW with type cast
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
- const delayMs = retryPolicy.getDelayMs(attempt);
313
- if (delayMs > 0) {
314
- await new Promise((resolve) => setTimeout(resolve, delayMs));
315
- }
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
+ };
316
457
 
317
- attempt++;
318
- } catch (error) {
319
- // An error was thrown during execution
320
- return { success: false, reason: "error", error };
321
- }
322
- }
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
+ };
323
473
  }
324
474
 
325
475
  /**
326
- * Options for executing a Unit of Work with restricted access
476
+ * Options for executing transactions
327
477
  */
328
- export interface ExecuteRestrictedUnitOfWorkOptions {
478
+ export interface ExecuteTxOptions {
329
479
  /**
330
480
  * Factory function that creates or resets a UOW instance for each attempt
331
481
  */
@@ -351,112 +501,229 @@ export interface ExecuteRestrictedUnitOfWorkOptions {
351
501
  * Callback invoked after successful mutation phase.
352
502
  * Use this for post-mutation processing like hook execution.
353
503
  */
354
- onSuccess?: (uow: IUnitOfWork) => Promise<void>;
504
+ onAfterMutate?: (uow: IUnitOfWork) => Promise<void>;
355
505
  }
356
506
 
357
507
  /**
358
- * Context provided to handler tx callbacks
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()).
359
512
  */
360
- export interface TxPhaseContext<THooks extends HooksMap> {
361
- /**
362
- * Get a typed Unit of Work for the given schema
363
- */
364
- forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
365
- schema: S,
366
- hooks?: H,
367
- ) => TypedUnitOfWork<S, [], unknown, H>;
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;
368
545
  }
369
546
 
370
547
  /**
371
- * Handler callbacks for tx() - SYNCHRONOUS ONLY (no Promise return allowed)
372
- * This prevents accidentally awaiting services in the wrong place
548
+ * Execute a single TxResult's callbacks after retrieve phase completes.
549
+ * This processes retrieveSuccess, mutate, and success callbacks in order.
373
550
  */
374
- export interface HandlerTxCallbacks<TRetrieveResult, TMutationResult, THooks extends HooksMap> {
375
- /**
376
- * Retrieval phase callback - schedules retrievals and optionally calls services
377
- * Must be synchronous - cannot await promises
378
- */
379
- retrieve?: (context: TxPhaseContext<THooks>) => TRetrieveResult;
380
- /**
381
- * Mutation phase callback - receives retrieve result, schedules mutations
382
- * Must be synchronous - cannot await promises (but may return a promise to be awaited)
383
- */
384
- mutate?: (context: TxPhaseContext<THooks>, retrieveResult: TRetrieveResult) => TMutationResult;
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
+ }
385
624
  }
386
625
 
387
- export interface ServiceTxCallbacks<
388
- TSchema extends AnySchema,
389
- TRetrievalResults extends unknown[],
390
- TMutationResult,
391
- THooks extends HooksMap,
392
- > {
393
- /**
394
- * Retrieval phase callback - schedules retrievals, returns typed UOW
395
- */
396
- retrieve?: (
397
- uow: TypedUnitOfWork<TSchema, [], unknown, THooks>,
398
- ) => TypedUnitOfWork<TSchema, TRetrievalResults, unknown, THooks>;
399
- /**
400
- * Mutation phase callback - receives retrieval results, schedules mutations and hooks
401
- */
402
- mutate?: (
403
- uow: TypedUnitOfWork<TSchema, TRetrievalResults, unknown, THooks>,
404
- results: TRetrievalResults,
405
- ) => TMutationResult | Promise<TMutationResult>;
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;
406
689
  }
407
690
 
408
691
  /**
409
- * Execute a Unit of Work with explicit phase control and automatic retry support.
692
+ * Execute a transaction with the unified TxResult pattern.
410
693
  *
411
- * This function provides an alternative API where users write a single callback that receives
412
- * a context object with forSchema, executeRetrieve, and executeMutate methods. The user can
413
- * create schema-specific UOWs via forSchema, then call executeRetrieve() and executeMutate()
414
- * to execute the retrieval and mutation phases. The entire callback is re-executed on optimistic
415
- * concurrency conflicts, ensuring retries work properly.
694
+ * This is the handler-level function that actually executes TxResults with retry support.
416
695
  *
417
- * @param callback - Async function that receives a context with forSchema, executeRetrieve, executeMutate, nonce, and currentAttempt
696
+ * @param callbacks - Transaction callbacks (serviceCalls, retrieve, retrieveSuccess, mutate, success)
418
697
  * @param options - Configuration including UOW factory, retry policy, and abort signal
419
- * @returns Promise resolving to the callback's return value
420
- * @throws Error if retries are exhausted or callback throws an error
698
+ * @returns Promise resolving to the result determined by return type priority
421
699
  *
422
700
  * @example
423
701
  * ```ts
424
- * const { userId, profileId } = await executeRestrictedUnitOfWork(
425
- * async ({ forSchema, executeRetrieve, executeMutate, nonce, currentAttempt }) => {
426
- * const uow = forSchema(schema);
427
- * const userId = uow.create("users", { name: "John" });
428
- *
429
- * // Execute retrieval phase
430
- * await executeRetrieve();
431
- *
432
- * const profileId = uow.create("profiles", { userId });
433
- *
434
- * // Execute mutation phase
435
- * await executeMutate();
436
- *
437
- * return { userId, profileId };
438
- * },
439
- * {
440
- * createUnitOfWork: () => db.createUnitOfWork(),
441
- * retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5 })
442
- * }
443
- * );
444
- * ```
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()
445
708
  */
446
- export async function executeRestrictedUnitOfWork<TResult, THooks extends HooksMap = {}>(
447
- callback: (context: {
448
- forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
449
- schema: S,
450
- hooks?: H,
451
- ) => TypedUnitOfWork<S, [], unknown, H>;
452
- executeRetrieve: () => Promise<void>;
453
- executeMutate: () => Promise<void>;
454
- nonce: string;
455
- currentAttempt: number;
456
- }) => Promise<TResult>,
457
- options: ExecuteRestrictedUnitOfWorkOptions,
458
- ): Promise<AwaitedPromisesInObject<TResult>> {
459
- // Default retry policy with small, fast retries for optimistic concurrency
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;
460
727
  const retryPolicy =
461
728
  options.retryPolicy ??
462
729
  new ExponentialBackoffRetryPolicy({
@@ -470,153 +737,147 @@ export async function executeRestrictedUnitOfWork<TResult, THooks extends HooksM
470
737
  while (true) {
471
738
  // Check if aborted before starting attempt
472
739
  if (signal?.aborted) {
473
- throw new Error("Unit of Work execution aborted");
740
+ throw new Error("Transaction execution aborted");
474
741
  }
475
742
 
476
743
  try {
477
744
  // Create a fresh UOW for this attempt
478
745
  const baseUow = options.createUnitOfWork();
479
746
 
480
- const context = {
747
+ // Create handler context
748
+ const context: HandlerTxContext<THooks> = {
481
749
  forSchema: <S extends AnySchema, H extends HooksMap = THooks>(schema: S, hooks?: H) => {
482
750
  return baseUow.forSchema(schema, hooks);
483
751
  },
484
- executeRetrieve: async () => {
485
- await baseUow.executeRetrieve();
486
- },
487
- executeMutate: async () => {
488
- if (baseUow.state === "executed") {
489
- return;
490
- }
491
-
492
- if (baseUow.state === "building-retrieval") {
493
- await baseUow.executeRetrieve();
494
- }
495
-
496
- // Add hook mutations before executing
497
- if (options.onBeforeMutate) {
498
- options.onBeforeMutate(baseUow);
499
- }
500
-
501
- const result = await baseUow.executeMutations();
502
- if (!result.success) {
503
- throw new ConcurrencyConflictError();
504
- }
505
-
506
- if (options.onSuccess) {
507
- await options.onSuccess(baseUow);
508
- }
509
- },
510
- nonce: baseUow.nonce,
752
+ idempotencyKey: baseUow.idempotencyKey,
511
753
  currentAttempt: attempt,
512
754
  };
513
755
 
514
- // Execute the callback which will call executeRetrieve and executeMutate
515
- const result = await callback(context);
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
+ }
516
761
 
517
- // Await promises in the result object (1 level deep)
518
- const awaitedResult = await awaitPromisesInObject(result);
762
+ // Call retrieve callback - it returns a TypedUnitOfWork with scheduled operations or void
763
+ const typedUowFromRetrieve = callbacks.retrieve?.(context);
519
764
 
520
- // Return the awaited result
521
- return awaitedResult;
522
- } catch (error) {
523
- if (signal?.aborted) {
524
- throw new Error("Unit of Work execution aborted");
525
- }
765
+ const allServiceCallTxResults = serviceCalls ? collectAllTxResults([...serviceCalls]) : [];
526
766
 
527
- // Only retry concurrency conflicts, not other errors
528
- if (!(error instanceof ConcurrencyConflictError)) {
529
- // Not a concurrency conflict - throw immediately without retry
530
- throw error;
767
+ await baseUow.executeRetrieve();
768
+
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);
531
776
  }
532
777
 
533
- if (!retryPolicy.shouldRetry(attempt, error, signal)) {
534
- // No more retries - check again if aborted or throw conflict error
535
- if (signal?.aborted) {
536
- throw new Error("Unit of Work execution aborted");
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);
801
+ }
537
802
  }
538
- throw new Error("Unit of Work execution failed: optimistic concurrency conflict", {
539
- cause: error,
540
- });
541
803
  }
542
804
 
543
- const delayMs = retryPolicy.getDelayMs(attempt);
544
- if (delayMs > 0) {
545
- await new Promise((resolve) => setTimeout(resolve, delayMs));
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;
546
814
  }
547
815
 
548
- attempt++;
549
- }
550
- }
551
- }
552
-
553
- /**
554
- * Execute a transaction with array syntax (handler context).
555
- * Takes a factory function that creates an array of service promises, enabling proper retry support.
556
- *
557
- * @param servicesFactory - Function that creates an array of service promises
558
- * @param options - Configuration including UOW factory, retry policy, and abort signal
559
- * @returns Promise resolving to array of awaited service results
560
- *
561
- * @example
562
- * ```ts
563
- * const [result1, result2] = await executeTxArray(
564
- * () => [
565
- * executeServiceTx(schema, callbacks1, uow),
566
- * executeServiceTx(schema, callbacks2, uow)
567
- * ],
568
- * { createUnitOfWork }
569
- * );
570
- * ```
571
- */
572
- export async function executeTxArray<T extends readonly unknown[]>(
573
- servicesFactory: () => readonly [...{ [K in keyof T]: Promise<T[K]> }],
574
- options: ExecuteRestrictedUnitOfWorkOptions,
575
- ): Promise<{ [K in keyof T]: T[K] }> {
576
- const retryPolicy =
577
- options.retryPolicy ??
578
- new ExponentialBackoffRetryPolicy({
579
- maxRetries: 5,
580
- initialDelayMs: 10,
581
- maxDelayMs: 100,
582
- });
583
- const signal = options.signal;
584
- let attempt = 0;
585
-
586
- while (true) {
587
- // Check if aborted before starting attempt
588
- if (signal?.aborted) {
589
- throw new Error("Unit of Work execution aborted");
590
- }
591
-
592
- try {
593
- // Create a fresh UOW for this attempt
594
- const baseUow = options.createUnitOfWork();
595
-
596
- // Call factory to create fresh service promises for this attempt
597
- const services = servicesFactory();
598
-
599
- await baseUow.executeRetrieve();
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
+ }
600
829
 
601
830
  if (options.onBeforeMutate) {
602
831
  options.onBeforeMutate(baseUow);
603
832
  }
604
-
605
833
  const result = await baseUow.executeMutations();
606
834
  if (!result.success) {
607
835
  throw new ConcurrencyConflictError();
608
836
  }
609
837
 
610
- if (options.onSuccess) {
611
- await options.onSuccess(baseUow);
838
+ // Process each serviceCall TxResult's success callback
839
+ for (const txResult of allServiceCallTxResults) {
840
+ await processTxResultAfterMutate(txResult);
612
841
  }
613
842
 
614
- // Now await all service promises - they should all resolve now that mutations executed
615
- const results = await Promise.all(services);
616
- return results as { [K in keyof T]: T[K] };
843
+ const serviceFinalResults: unknown[] = [];
844
+ if (serviceCalls) {
845
+ for (const serviceCall of serviceCalls) {
846
+ if (serviceCall === undefined) {
847
+ serviceFinalResults.push(undefined);
848
+ continue;
849
+ }
850
+ serviceFinalResults.push(serviceCall._internal.finalResult);
851
+ }
852
+ }
853
+
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
+ }
872
+
873
+ if (options.onAfterMutate) {
874
+ await options.onAfterMutate(baseUow);
875
+ }
876
+
877
+ return await awaitPromisesInObject(finalResult);
617
878
  } catch (error) {
618
879
  if (signal?.aborted) {
619
- throw new Error("Unit of Work execution aborted");
880
+ throw new Error("Transaction execution aborted");
620
881
  }
621
882
 
622
883
  // Only retry concurrency conflicts, not other errors
@@ -626,11 +887,9 @@ export async function executeTxArray<T extends readonly unknown[]>(
626
887
 
627
888
  if (!retryPolicy.shouldRetry(attempt, error, signal)) {
628
889
  if (signal?.aborted) {
629
- throw new Error("Unit of Work execution aborted");
890
+ throw new Error("Transaction execution aborted");
630
891
  }
631
- throw new Error("Unit of Work execution failed: optimistic concurrency conflict", {
632
- cause: error,
633
- });
892
+ throw new ConcurrencyConflictError();
634
893
  }
635
894
 
636
895
  const delayMs = retryPolicy.getDelayMs(attempt);
@@ -644,169 +903,1005 @@ export async function executeTxArray<T extends readonly unknown[]>(
644
903
  }
645
904
 
646
905
  /**
647
- * Execute a transaction with callback syntax (handler context).
648
- * Callbacks are synchronous only to prevent accidentally awaiting services in wrong place.
649
- *
650
- * @param callbacks - Object containing retrieve and mutate callbacks
651
- * @param options - Configuration including UOW factory, retry policy, and abort signal
652
- * @returns Promise resolving to the mutation result with promises awaited 1 level deep
906
+ * Error thrown when a Unit of Work execution fails due to optimistic concurrency conflict.
907
+ * This error triggers automatic retry behavior in executeTx.
653
908
  */
654
- export async function executeTxCallbacks<
655
- TRetrieveResult,
656
- TMutationResult,
657
- THooks extends HooksMap = {},
658
- >(
659
- callbacks: HandlerTxCallbacks<TRetrieveResult, TMutationResult, THooks>,
660
- options: ExecuteRestrictedUnitOfWorkOptions,
661
- ): Promise<AwaitedPromisesInObject<TMutationResult>> {
662
- const retryPolicy =
663
- options.retryPolicy ??
664
- new ExponentialBackoffRetryPolicy({
665
- maxRetries: 5,
666
- initialDelayMs: 10,
667
- maxDelayMs: 100,
668
- });
669
- const signal = options.signal;
670
- let attempt = 0;
909
+ export class ConcurrencyConflictError extends Error {
910
+ constructor(message = "Optimistic concurrency conflict detected") {
911
+ super(message);
912
+ this.name = "ConcurrencyConflictError";
913
+ }
914
+ }
671
915
 
672
- while (true) {
673
- // Check if aborted before starting attempt
674
- if (signal?.aborted) {
675
- throw new Error("Unit of Work execution aborted");
676
- }
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;
677
941
 
678
- try {
679
- // Create a fresh UOW for this attempt
680
- const baseUow = options.createUnitOfWork();
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
+ }
681
949
 
682
- const context: TxPhaseContext<THooks> = {
683
- forSchema: <S extends AnySchema, H extends HooksMap = THooks>(schema: S, hooks?: H) => {
684
- return baseUow.forSchema(schema, hooks);
685
- },
686
- };
950
+ if (typeof obj !== "object") {
951
+ return obj as AwaitedPromisesInObject<T>;
952
+ }
687
953
 
688
- let retrieveResult: TRetrieveResult;
689
- if (callbacks.retrieve) {
690
- retrieveResult = callbacks.retrieve(context);
691
- } else {
692
- retrieveResult = undefined as TRetrieveResult;
693
- }
954
+ // Check if it's a Promise
955
+ if (obj instanceof Promise) {
956
+ return (await obj) as AwaitedPromisesInObject<T>;
957
+ }
694
958
 
695
- await baseUow.executeRetrieve();
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
+ }
696
966
 
697
- let mutationResult: TMutationResult;
698
- if (callbacks.mutate) {
699
- mutationResult = callbacks.mutate(context, retrieveResult);
700
- } else {
701
- mutationResult = retrieveResult as unknown as TMutationResult;
702
- }
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
+ );
703
978
 
704
- const awaitedMutationResult = await awaitPromisesInObject(mutationResult);
979
+ for (const [key, value] of awaitedEntries) {
980
+ (result as Record<string, unknown>)[key] = value;
981
+ }
705
982
 
706
- if (options.onBeforeMutate) {
707
- options.onBeforeMutate(baseUow);
708
- }
983
+ return result as AwaitedPromisesInObject<T>;
984
+ }
709
985
 
710
- const result = await baseUow.executeMutations();
711
- if (!result.success) {
712
- throw new ConcurrencyConflictError();
713
- }
986
+ // ============================================================================
987
+ // Builder Pattern Types and Classes
988
+ // ============================================================================
714
989
 
715
- if (options.onSuccess) {
716
- await options.onSuccess(baseUow);
717
- }
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
+ }
718
1006
 
719
- return awaitedMutationResult;
720
- } catch (error) {
721
- if (signal?.aborted) {
722
- throw new Error("Unit of Work execution aborted");
723
- }
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
+ }
724
1029
 
725
- // Only retry concurrency conflicts, not other errors
726
- if (!(error instanceof ConcurrencyConflictError)) {
727
- throw error;
728
- }
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
+ }
729
1048
 
730
- if (!retryPolicy.shouldRetry(attempt, error, signal)) {
731
- if (signal?.aborted) {
732
- throw new Error("Unit of Work execution aborted");
733
- }
734
- throw new Error("Unit of Work execution failed: optimistic concurrency conflict", {
735
- cause: error,
736
- });
737
- }
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
+ }
738
1066
 
739
- const delayMs = retryPolicy.getDelayMs(attempt);
740
- if (delayMs > 0) {
741
- await new Promise((resolve) => setTimeout(resolve, delayMs));
742
- }
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>;
743
1095
 
744
- attempt++;
745
- }
746
- }
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;
747
1165
  }
748
1166
 
749
1167
  /**
750
- * Execute a transaction for service context.
751
- * Service callbacks can be async for ergonomic async work.
1168
+ * Builder for service-level transactions.
1169
+ * Uses a fluent API to build up transaction callbacks with proper type inference.
752
1170
  *
753
- * @param schema - Schema to use for the transaction
754
- * @param callbacks - Object containing retrieve and mutate callbacks
755
- * @param baseUow - Base Unit of Work (restricted) to use
756
- * @returns Promise resolving to the mutation result with promises awaited 1 level deep
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
+ * ```
757
1183
  */
758
- export async function executeServiceTx<
1184
+ export class ServiceTxBuilder<
759
1185
  TSchema extends AnySchema,
760
- TRetrievalResults extends unknown[],
761
- TMutationResult,
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,
762
1196
  THooks extends HooksMap,
763
- >(
764
- schema: TSchema,
765
- callbacks: ServiceTxCallbacks<TSchema, TRetrievalResults, TMutationResult, THooks>,
766
- baseUow: IUnitOfWork,
767
- ): Promise<AwaitedPromisesInObject<TMutationResult>> {
768
- const typedUow = baseUow.restrict({ readyFor: "none" }).forSchema<TSchema, THooks>(schema);
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
+ }
769
1221
 
770
- let retrievalUow: TypedUnitOfWork<TSchema, TRetrievalResults, unknown, THooks>;
771
- try {
772
- if (callbacks.retrieve) {
773
- retrievalUow = callbacks.retrieve(typedUow);
774
- } else {
775
- // Safe cast: when there's no retrieve callback, TRetrievalResults should be []
776
- retrievalUow = typedUow as unknown as TypedUnitOfWork<
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<
777
1331
  TSchema,
778
- TRetrievalResults,
779
- unknown,
1332
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1333
+ ExtractServiceRetrieveResults<TServiceCalls>,
780
1334
  THooks
781
- >;
782
- }
783
- } catch (error) {
784
- typedUow.signalReadyForRetrieval();
785
- typedUow.signalReadyForMutation();
786
- throw error;
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
+ >);
787
1362
  }
788
1363
 
789
- typedUow.signalReadyForRetrieval();
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
+ }
790
1408
 
791
- // Safe cast: retrievalPhase returns the correct type based on the UOW's type parameters
792
- const results = (await retrievalUow.retrievalPhase) as TRetrievalResults;
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
+ };
793
1473
 
794
- let mutationResult: TMutationResult;
795
- try {
796
- if (callbacks.mutate) {
797
- mutationResult = await callbacks.mutate(retrievalUow, results);
798
- } else {
799
- // Safe cast: when there's no mutate callback, TMutationResult should be void
800
- mutationResult = undefined as TMutationResult;
801
- }
802
- } catch (error) {
803
- typedUow.signalReadyForMutation();
804
- throw error;
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
+ >;
805
1497
  }
1498
+ }
806
1499
 
807
- typedUow.signalReadyForMutation();
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
+ }
808
1526
 
809
- await retrievalUow.mutationPhase;
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
+ }
810
1576
 
811
- return await awaitPromisesInObject(mutationResult);
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
+ });
812
1907
  }