@fragno-dev/db 0.1.13 → 0.1.15

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 (178) hide show
  1. package/.turbo/turbo-build.log +179 -132
  2. package/CHANGELOG.md +30 -0
  3. package/dist/adapters/adapters.d.ts +27 -1
  4. package/dist/adapters/adapters.d.ts.map +1 -1
  5. package/dist/adapters/adapters.js.map +1 -1
  6. package/dist/adapters/drizzle/drizzle-adapter.d.ts +5 -1
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  8. package/dist/adapters/drizzle/drizzle-adapter.js +15 -3
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
  10. package/dist/adapters/drizzle/drizzle-query.js +7 -5
  11. package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
  12. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -1
  13. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
  14. package/dist/adapters/drizzle/drizzle-uow-compiler.js +76 -44
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  16. package/dist/adapters/drizzle/drizzle-uow-decoder.js +23 -16
  17. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
  18. package/dist/adapters/drizzle/drizzle-uow-executor.js +18 -7
  19. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -1
  20. package/dist/adapters/drizzle/generate.d.ts +4 -1
  21. package/dist/adapters/drizzle/generate.d.ts.map +1 -1
  22. package/dist/adapters/drizzle/generate.js +11 -18
  23. package/dist/adapters/drizzle/generate.js.map +1 -1
  24. package/dist/adapters/drizzle/shared.d.ts +14 -1
  25. package/dist/adapters/drizzle/shared.d.ts.map +1 -0
  26. package/dist/adapters/kysely/kysely-adapter.d.ts +5 -1
  27. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  28. package/dist/adapters/kysely/kysely-adapter.js +14 -3
  29. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  30. package/dist/adapters/kysely/kysely-query-builder.js +1 -1
  31. package/dist/adapters/kysely/kysely-query-compiler.js +3 -2
  32. package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -1
  33. package/dist/adapters/kysely/kysely-query.d.ts +1 -0
  34. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
  35. package/dist/adapters/kysely/kysely-query.js +28 -19
  36. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  37. package/dist/adapters/kysely/kysely-shared.d.ts +14 -0
  38. package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
  39. package/dist/adapters/kysely/kysely-shared.js +16 -1
  40. package/dist/adapters/kysely/kysely-shared.js.map +1 -1
  41. package/dist/adapters/kysely/kysely-uow-compiler.js +68 -16
  42. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  43. package/dist/adapters/kysely/kysely-uow-executor.js +8 -4
  44. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
  45. package/dist/adapters/kysely/migration/execute-base.js +1 -1
  46. package/dist/adapters/kysely/migration/execute-base.js.map +1 -1
  47. package/dist/db-fragment-definition-builder.d.ts +152 -0
  48. package/dist/db-fragment-definition-builder.d.ts.map +1 -0
  49. package/dist/db-fragment-definition-builder.js +137 -0
  50. package/dist/db-fragment-definition-builder.js.map +1 -0
  51. package/dist/fragments/internal-fragment.d.ts +19 -0
  52. package/dist/fragments/internal-fragment.d.ts.map +1 -0
  53. package/dist/fragments/internal-fragment.js +39 -0
  54. package/dist/fragments/internal-fragment.js.map +1 -0
  55. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  56. package/dist/migration-engine/generation-engine.js +35 -15
  57. package/dist/migration-engine/generation-engine.js.map +1 -1
  58. package/dist/mod.d.ts +8 -18
  59. package/dist/mod.d.ts.map +1 -1
  60. package/dist/mod.js +7 -34
  61. package/dist/mod.js.map +1 -1
  62. package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js +165 -0
  63. package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js.map +1 -0
  64. package/dist/packages/fragno/dist/api/bind-services.js +20 -0
  65. package/dist/packages/fragno/dist/api/bind-services.js.map +1 -0
  66. package/dist/packages/fragno/dist/api/error.js +48 -0
  67. package/dist/packages/fragno/dist/api/error.js.map +1 -0
  68. package/dist/packages/fragno/dist/api/fragment-definition-builder.js +320 -0
  69. package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -0
  70. package/dist/packages/fragno/dist/api/fragment-instantiator.js +487 -0
  71. package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -0
  72. package/dist/packages/fragno/dist/api/fragno-response.js +73 -0
  73. package/dist/packages/fragno/dist/api/fragno-response.js.map +1 -0
  74. package/dist/packages/fragno/dist/api/internal/response-stream.js +81 -0
  75. package/dist/packages/fragno/dist/api/internal/response-stream.js.map +1 -0
  76. package/dist/packages/fragno/dist/api/internal/route.js +10 -0
  77. package/dist/packages/fragno/dist/api/internal/route.js.map +1 -0
  78. package/dist/packages/fragno/dist/api/mutable-request-state.js +97 -0
  79. package/dist/packages/fragno/dist/api/mutable-request-state.js.map +1 -0
  80. package/dist/packages/fragno/dist/api/request-context-storage.js +43 -0
  81. package/dist/packages/fragno/dist/api/request-context-storage.js.map +1 -0
  82. package/dist/packages/fragno/dist/api/request-input-context.js +118 -0
  83. package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -0
  84. package/dist/packages/fragno/dist/api/request-middleware.js +83 -0
  85. package/dist/packages/fragno/dist/api/request-middleware.js.map +1 -0
  86. package/dist/packages/fragno/dist/api/request-output-context.js +119 -0
  87. package/dist/packages/fragno/dist/api/request-output-context.js.map +1 -0
  88. package/dist/packages/fragno/dist/api/route.js +17 -0
  89. package/dist/packages/fragno/dist/api/route.js.map +1 -0
  90. package/dist/packages/fragno/dist/internal/symbols.js +10 -0
  91. package/dist/packages/fragno/dist/internal/symbols.js.map +1 -0
  92. package/dist/query/cursor.d.ts +10 -2
  93. package/dist/query/cursor.d.ts.map +1 -1
  94. package/dist/query/cursor.js +11 -4
  95. package/dist/query/cursor.js.map +1 -1
  96. package/dist/query/execute-unit-of-work.d.ts +123 -0
  97. package/dist/query/execute-unit-of-work.d.ts.map +1 -0
  98. package/dist/query/execute-unit-of-work.js +184 -0
  99. package/dist/query/execute-unit-of-work.js.map +1 -0
  100. package/dist/query/query.d.ts +3 -3
  101. package/dist/query/query.d.ts.map +1 -1
  102. package/dist/query/result-transform.js +4 -2
  103. package/dist/query/result-transform.js.map +1 -1
  104. package/dist/query/retry-policy.d.ts +88 -0
  105. package/dist/query/retry-policy.d.ts.map +1 -0
  106. package/dist/query/retry-policy.js +61 -0
  107. package/dist/query/retry-policy.js.map +1 -0
  108. package/dist/query/unit-of-work.d.ts +171 -32
  109. package/dist/query/unit-of-work.d.ts.map +1 -1
  110. package/dist/query/unit-of-work.js +530 -133
  111. package/dist/query/unit-of-work.js.map +1 -1
  112. package/dist/schema/serialize.js +12 -7
  113. package/dist/schema/serialize.js.map +1 -1
  114. package/dist/with-database.d.ts +28 -0
  115. package/dist/with-database.d.ts.map +1 -0
  116. package/dist/with-database.js +34 -0
  117. package/dist/with-database.js.map +1 -0
  118. package/package.json +10 -3
  119. package/src/adapters/adapters.ts +30 -0
  120. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +86 -17
  121. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +291 -7
  122. package/src/adapters/drizzle/drizzle-adapter.test.ts +3 -51
  123. package/src/adapters/drizzle/drizzle-adapter.ts +35 -7
  124. package/src/adapters/drizzle/drizzle-query.ts +25 -15
  125. package/src/adapters/drizzle/drizzle-uow-compiler-mysql.test.ts +1442 -0
  126. package/src/adapters/drizzle/drizzle-uow-compiler-sqlite.test.ts +1414 -0
  127. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +78 -61
  128. package/src/adapters/drizzle/drizzle-uow-compiler.ts +123 -42
  129. package/src/adapters/drizzle/drizzle-uow-decoder.ts +34 -27
  130. package/src/adapters/drizzle/drizzle-uow-executor.ts +41 -8
  131. package/src/adapters/drizzle/generate.test.ts +102 -269
  132. package/src/adapters/drizzle/generate.ts +12 -30
  133. package/src/adapters/drizzle/test-utils.ts +36 -5
  134. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +66 -22
  135. package/src/adapters/kysely/kysely-adapter-sqlite.test.ts +156 -0
  136. package/src/adapters/kysely/kysely-adapter.ts +25 -2
  137. package/src/adapters/kysely/kysely-query-compiler.ts +3 -8
  138. package/src/adapters/kysely/kysely-query.ts +57 -37
  139. package/src/adapters/kysely/kysely-shared.ts +34 -0
  140. package/src/adapters/kysely/kysely-uow-compiler.test.ts +62 -74
  141. package/src/adapters/kysely/kysely-uow-compiler.ts +92 -24
  142. package/src/adapters/kysely/kysely-uow-executor.ts +26 -7
  143. package/src/adapters/kysely/kysely-uow-joins.test.ts +33 -50
  144. package/src/adapters/kysely/migration/execute-base.ts +1 -1
  145. package/src/db-fragment-definition-builder.test.ts +887 -0
  146. package/src/db-fragment-definition-builder.ts +506 -0
  147. package/src/db-fragment-instantiator.test.ts +467 -0
  148. package/src/db-fragment-integration.test.ts +408 -0
  149. package/src/fragments/internal-fragment.test.ts +160 -0
  150. package/src/fragments/internal-fragment.ts +85 -0
  151. package/src/migration-engine/generation-engine.test.ts +58 -15
  152. package/src/migration-engine/generation-engine.ts +78 -25
  153. package/src/mod.ts +35 -43
  154. package/src/query/cursor.test.ts +119 -0
  155. package/src/query/cursor.ts +17 -4
  156. package/src/query/execute-unit-of-work.test.ts +1310 -0
  157. package/src/query/execute-unit-of-work.ts +463 -0
  158. package/src/query/query.ts +4 -4
  159. package/src/query/result-transform.test.ts +129 -0
  160. package/src/query/result-transform.ts +4 -1
  161. package/src/query/retry-policy.test.ts +217 -0
  162. package/src/query/retry-policy.ts +141 -0
  163. package/src/query/unit-of-work-coordinator.test.ts +833 -0
  164. package/src/query/unit-of-work-types.test.ts +15 -2
  165. package/src/query/unit-of-work.test.ts +878 -200
  166. package/src/query/unit-of-work.ts +963 -321
  167. package/src/schema/serialize.ts +22 -11
  168. package/src/with-database.ts +140 -0
  169. package/tsdown.config.ts +1 -0
  170. package/dist/fragment.d.ts +0 -54
  171. package/dist/fragment.d.ts.map +0 -1
  172. package/dist/fragment.js +0 -92
  173. package/dist/fragment.js.map +0 -1
  174. package/dist/shared/settings-schema.js +0 -36
  175. package/dist/shared/settings-schema.js.map +0 -1
  176. package/src/fragment.test.ts +0 -341
  177. package/src/fragment.ts +0 -198
  178. package/src/shared/settings-schema.ts +0 -61
@@ -0,0 +1,1310 @@
1
+ import { describe, it, expect, vi, assert, expectTypeOf } from "vitest";
2
+ import { schema, idColumn, FragnoId } from "../schema/create";
3
+ import {
4
+ createUnitOfWork,
5
+ type TypedUnitOfWork,
6
+ type UOWCompiler,
7
+ type UOWDecoder,
8
+ type UOWExecutor,
9
+ } from "./unit-of-work";
10
+ import { executeUnitOfWork, executeRestrictedUnitOfWork } from "./execute-unit-of-work";
11
+ import {
12
+ ExponentialBackoffRetryPolicy,
13
+ LinearBackoffRetryPolicy,
14
+ NoRetryPolicy,
15
+ } from "./retry-policy";
16
+ import type { AwaitedPromisesInObject } from "./execute-unit-of-work";
17
+
18
+ // Create test schema
19
+ const testSchema = schema((s) =>
20
+ s.addTable("users", (t) =>
21
+ t
22
+ .addColumn("id", idColumn())
23
+ .addColumn("email", "string")
24
+ .addColumn("name", "string")
25
+ .addColumn("balance", "integer")
26
+ .createIndex("idx_email", ["email"], { unique: true }),
27
+ ),
28
+ );
29
+
30
+ // Type tests for AwaitedPromisesInObject
31
+ describe("AwaitedPromisesInObject type tests", () => {
32
+ it("should unwrap promises in objects", () => {
33
+ type Input = { a: Promise<string>; b: number };
34
+ type Expected = { a: string; b: number };
35
+ type Actual = AwaitedPromisesInObject<Input>;
36
+ expectTypeOf<Actual>().toMatchObjectType<Expected>();
37
+ });
38
+
39
+ it("should unwrap promises in arrays", () => {
40
+ type Input = Promise<string>[];
41
+ type Expected = string[];
42
+ type Actual = AwaitedPromisesInObject<Input>;
43
+ expectTypeOf<Actual>().toEqualTypeOf<Expected>();
44
+ });
45
+
46
+ it("should unwrap direct promises", () => {
47
+ type Input = Promise<{ value: number }>;
48
+ type Expected = { value: number };
49
+ type Actual = AwaitedPromisesInObject<Input>;
50
+ expectTypeOf<Actual>().toEqualTypeOf<Expected>();
51
+ });
52
+
53
+ it("should handle tuples correctly", () => {
54
+ type Input = [Promise<string>, Promise<number>];
55
+ type Actual = AwaitedPromisesInObject<Input>;
56
+
57
+ // Should preserve tuple structure - check first and second elements
58
+ expectTypeOf<Actual[0]>().toEqualTypeOf<string>();
59
+ expectTypeOf<Actual[1]>().toEqualTypeOf<number>();
60
+
61
+ // Verify it's actually a tuple with length 2
62
+ expectTypeOf<Actual["length"]>().toEqualTypeOf<2>();
63
+ });
64
+
65
+ it("should preserve tuple structure with Promise.all pattern", () => {
66
+ type User = { id: string; name: string };
67
+ type Order = { id: string; total: number };
68
+
69
+ type Input = [Promise<User>, Promise<Order[]>];
70
+ type Actual = AwaitedPromisesInObject<Input>;
71
+
72
+ // Check individual elements
73
+ expectTypeOf<Actual[0]>().toMatchObjectType<User>();
74
+ expectTypeOf<Actual[1]>().toEqualTypeOf<Order[]>();
75
+
76
+ // Verify length
77
+ expectTypeOf<Actual["length"]>().toEqualTypeOf<2>();
78
+ });
79
+
80
+ it("should handle readonly tuples", () => {
81
+ type Input = readonly [Promise<string>, Promise<number>];
82
+ type Actual = AwaitedPromisesInObject<Input>;
83
+
84
+ // Check elements
85
+ expectTypeOf<Actual[0]>().toEqualTypeOf<string>();
86
+ expectTypeOf<Actual[1]>().toEqualTypeOf<number>();
87
+ });
88
+
89
+ it("should handle tuples with more than 2 elements", () => {
90
+ type Input = [Promise<string>, Promise<number>, Promise<boolean>];
91
+ type Actual = AwaitedPromisesInObject<Input>;
92
+
93
+ // Check all three elements
94
+ expectTypeOf<Actual[0]>().toEqualTypeOf<string>();
95
+ expectTypeOf<Actual[1]>().toEqualTypeOf<number>();
96
+ expectTypeOf<Actual[2]>().toEqualTypeOf<boolean>();
97
+ expectTypeOf<Actual["length"]>().toEqualTypeOf<3>();
98
+ });
99
+
100
+ it("should handle tuples with mixed promise and non-promise types", () => {
101
+ type Input = [Promise<string>, number, Promise<boolean>];
102
+ type Actual = AwaitedPromisesInObject<Input>;
103
+
104
+ // Non-promises should be preserved as-is
105
+ expectTypeOf<Actual[0]>().toEqualTypeOf<string>();
106
+ expectTypeOf<Actual[1]>().toEqualTypeOf<number>();
107
+ expectTypeOf<Actual[2]>().toEqualTypeOf<boolean>();
108
+ });
109
+ });
110
+
111
+ // Mock compiler that returns null for all operations
112
+ function createMockCompiler(): UOWCompiler<unknown> {
113
+ return {
114
+ compileRetrievalOperation: () => null,
115
+ compileMutationOperation: () => null,
116
+ };
117
+ }
118
+
119
+ // Mock decoder that returns raw results as-is
120
+ function createMockDecoder(): UOWDecoder {
121
+ return (rawResults) => rawResults;
122
+ }
123
+
124
+ // Helper to create a UOW factory that tracks how many times it's called
125
+ function createMockUOWFactory(mutationResults: Array<{ success: boolean }>) {
126
+ const callCount = { value: 0 };
127
+ // Share callIndex across all UOW instances
128
+ let callIndex = 0;
129
+
130
+ const factory = () => {
131
+ callCount.value++;
132
+
133
+ // Create executor that uses shared callIndex
134
+ const executor: UOWExecutor<unknown, unknown> = {
135
+ executeRetrievalPhase: async () => {
136
+ return [
137
+ [
138
+ {
139
+ id: FragnoId.fromExternal("user-1", 1),
140
+ email: "test@example.com",
141
+ name: "Test User",
142
+ balance: 100,
143
+ },
144
+ ],
145
+ ];
146
+ },
147
+ executeMutationPhase: async () => {
148
+ const result = mutationResults[callIndex] || { success: false };
149
+ callIndex++;
150
+ return { ...result, createdInternalIds: [] };
151
+ },
152
+ };
153
+
154
+ return createUnitOfWork(createMockCompiler(), executor, createMockDecoder()).forSchema(
155
+ testSchema,
156
+ );
157
+ };
158
+ return { factory, callCount };
159
+ }
160
+
161
+ describe("executeUnitOfWork", () => {
162
+ describe("validation", () => {
163
+ it("should throw error when neither retrieve nor mutate is provided", async () => {
164
+ const { factory } = createMockUOWFactory([{ success: true }]);
165
+
166
+ await expect(executeUnitOfWork({}, { createUnitOfWork: factory })).rejects.toThrow(
167
+ "At least one of 'retrieve' or 'mutate' callbacks must be provided",
168
+ );
169
+ });
170
+ });
171
+
172
+ describe("success scenarios", () => {
173
+ it("should succeed on first attempt without retries", async () => {
174
+ const { factory } = createMockUOWFactory([{ success: true }]);
175
+ const onSuccess = vi.fn();
176
+
177
+ const result = await executeUnitOfWork(
178
+ {
179
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
180
+ mutate: (uow, [users]) => {
181
+ const newBalance = users[0].balance + 100;
182
+ uow.update("users", users[0].id, (b) => b.set({ balance: newBalance }).check());
183
+ return { newBalance };
184
+ },
185
+ onSuccess,
186
+ },
187
+ { createUnitOfWork: factory },
188
+ );
189
+
190
+ assert(result.success);
191
+ expect(result.mutationResult).toEqual({ newBalance: 200 });
192
+ expect(onSuccess).toHaveBeenCalledExactlyOnceWith({
193
+ results: expect.any(Array),
194
+ mutationResult: { newBalance: 200 },
195
+ createdIds: [],
196
+ nonce: expect.any(String),
197
+ });
198
+ });
199
+ });
200
+
201
+ describe("retry scenarios", () => {
202
+ it("should retry on conflict with eventual success", async () => {
203
+ const { factory, callCount } = createMockUOWFactory([
204
+ { success: false },
205
+ { success: false },
206
+ { success: true },
207
+ ]);
208
+
209
+ const result = await executeUnitOfWork(
210
+ {
211
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
212
+ mutate: async (uow, [users]) => {
213
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }).check());
214
+ },
215
+ },
216
+ {
217
+ createUnitOfWork: factory,
218
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
219
+ },
220
+ );
221
+
222
+ expect(result.success).toBe(true);
223
+ expect(callCount.value).toBe(3); // Initial + 2 retries
224
+ });
225
+
226
+ it("should fail when max retries exceeded", async () => {
227
+ const { factory, callCount } = createMockUOWFactory([
228
+ { success: false },
229
+ { success: false },
230
+ { success: false },
231
+ { success: false },
232
+ ]);
233
+
234
+ const result = await executeUnitOfWork(
235
+ {
236
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
237
+ mutate: async (uow, [users]) => {
238
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
239
+ },
240
+ },
241
+ {
242
+ createUnitOfWork: factory,
243
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 2, initialDelayMs: 1 }),
244
+ },
245
+ );
246
+
247
+ assert(!result.success);
248
+ expect(result.reason).toBe("conflict");
249
+ expect(callCount.value).toBe(3); // Initial + 2 retries
250
+ });
251
+
252
+ it("should create fresh UOW on each retry attempt", async () => {
253
+ const { factory, callCount } = createMockUOWFactory([
254
+ { success: false },
255
+ { success: false },
256
+ { success: true },
257
+ ]);
258
+
259
+ await executeUnitOfWork(
260
+ {
261
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
262
+ mutate: async (uow, [users]) => {
263
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
264
+ },
265
+ },
266
+ {
267
+ createUnitOfWork: factory,
268
+ retryPolicy: new LinearBackoffRetryPolicy({ maxRetries: 3, delayMs: 1 }),
269
+ },
270
+ );
271
+
272
+ expect(callCount.value).toBe(3); // Each attempt creates a new UOW
273
+ });
274
+ });
275
+
276
+ describe("AbortSignal handling", () => {
277
+ it("should abort when signal is aborted before execution", async () => {
278
+ const { factory } = createMockUOWFactory([{ success: false }]);
279
+ const controller = new AbortController();
280
+ controller.abort();
281
+
282
+ const result = await executeUnitOfWork(
283
+ {
284
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
285
+ mutate: async (uow, [users]) => {
286
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
287
+ },
288
+ },
289
+ {
290
+ createUnitOfWork: factory,
291
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5, initialDelayMs: 1 }),
292
+ signal: controller.signal,
293
+ },
294
+ );
295
+
296
+ assert(!result.success);
297
+ expect(result.reason).toBe("aborted");
298
+ });
299
+
300
+ it("should abort when signal is aborted during retry", async () => {
301
+ const { factory } = createMockUOWFactory([{ success: false }, { success: false }]);
302
+ const controller = new AbortController();
303
+
304
+ // Abort after first attempt
305
+ setTimeout(() => controller.abort(), 50);
306
+
307
+ const result = await executeUnitOfWork(
308
+ {
309
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
310
+ mutate: async (uow, [users]) => {
311
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
312
+ },
313
+ },
314
+ {
315
+ createUnitOfWork: factory,
316
+ retryPolicy: new LinearBackoffRetryPolicy({ maxRetries: 5, delayMs: 100 }),
317
+ signal: controller.signal,
318
+ },
319
+ );
320
+
321
+ assert(!result.success);
322
+ expect(result.reason).toBe("aborted");
323
+ });
324
+ });
325
+
326
+ describe("onSuccess callback", () => {
327
+ it("should pass mutation result to onSuccess callback", async () => {
328
+ const { factory } = createMockUOWFactory([{ success: true }]);
329
+ const onSuccess = vi.fn();
330
+
331
+ await executeUnitOfWork(
332
+ {
333
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
334
+ mutate: async () => {
335
+ return { updatedCount: 5 };
336
+ },
337
+ onSuccess,
338
+ },
339
+ { createUnitOfWork: factory },
340
+ );
341
+
342
+ expect(onSuccess).toHaveBeenCalledTimes(1);
343
+ expect(onSuccess).toHaveBeenCalledWith({
344
+ results: expect.any(Array),
345
+ mutationResult: { updatedCount: 5 },
346
+ createdIds: [],
347
+ nonce: expect.any(String),
348
+ });
349
+ });
350
+
351
+ it("should only execute onSuccess callback on success", async () => {
352
+ const { factory } = createMockUOWFactory([{ success: false }]);
353
+ const onSuccess = vi.fn();
354
+
355
+ const result = await executeUnitOfWork(
356
+ {
357
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
358
+ mutate: async (uow, [users]) => {
359
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
360
+ },
361
+ onSuccess,
362
+ },
363
+ {
364
+ createUnitOfWork: factory,
365
+ retryPolicy: new NoRetryPolicy(),
366
+ },
367
+ );
368
+
369
+ assert(!result.success);
370
+ expect(result.reason).toBe("conflict");
371
+ expect(onSuccess).not.toHaveBeenCalled();
372
+ });
373
+
374
+ it("should execute onSuccess only once even after retries", async () => {
375
+ const { factory } = createMockUOWFactory([
376
+ { success: false },
377
+ { success: false },
378
+ { success: true },
379
+ ]);
380
+ const onSuccess = vi.fn();
381
+
382
+ await executeUnitOfWork(
383
+ {
384
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
385
+ mutate: async (uow, [users]) => {
386
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
387
+ },
388
+ onSuccess,
389
+ },
390
+ {
391
+ createUnitOfWork: factory,
392
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
393
+ },
394
+ );
395
+
396
+ expect(onSuccess).toHaveBeenCalledTimes(1);
397
+ });
398
+
399
+ it("should handle async onSuccess callback", async () => {
400
+ const { factory } = createMockUOWFactory([{ success: true }]);
401
+ const onSuccess = vi.fn(async () => {
402
+ await new Promise((resolve) => setTimeout(resolve, 10));
403
+ });
404
+
405
+ await executeUnitOfWork(
406
+ {
407
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
408
+ mutate: async (uow, [users]) => {
409
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
410
+ },
411
+ onSuccess,
412
+ },
413
+ { createUnitOfWork: factory },
414
+ );
415
+
416
+ expect(onSuccess).toHaveBeenCalledTimes(1);
417
+ });
418
+ });
419
+
420
+ describe("error handling", () => {
421
+ it("should return error result when retrieve callback throws", async () => {
422
+ const { factory } = createMockUOWFactory([{ success: true }]);
423
+ const testError = new Error("Retrieve failed");
424
+
425
+ const result = await executeUnitOfWork(
426
+ {
427
+ retrieve: () => {
428
+ throw testError;
429
+ },
430
+ mutate: async () => {},
431
+ },
432
+ { createUnitOfWork: factory },
433
+ );
434
+
435
+ assert(!result.success);
436
+ assert(result.reason === "error");
437
+ expect(result.error).toBe(testError);
438
+ });
439
+
440
+ it("should return error result when mutate callback throws", async () => {
441
+ const { factory } = createMockUOWFactory([{ success: true }]);
442
+ const testError = new Error("Mutate failed");
443
+
444
+ const result = await executeUnitOfWork(
445
+ {
446
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
447
+ mutate: async () => {
448
+ throw testError;
449
+ },
450
+ },
451
+ { createUnitOfWork: factory },
452
+ );
453
+
454
+ assert(!result.success);
455
+ assert(result.reason === "error");
456
+ expect(result.error).toBe(testError);
457
+ });
458
+
459
+ it("should return error result when onSuccess callback throws", async () => {
460
+ const { factory } = createMockUOWFactory([{ success: true }]);
461
+ const testError = new Error("onSuccess failed");
462
+
463
+ const result = await executeUnitOfWork(
464
+ {
465
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
466
+ mutate: async () => {},
467
+ onSuccess: async () => {
468
+ throw testError;
469
+ },
470
+ },
471
+ { createUnitOfWork: factory },
472
+ );
473
+
474
+ assert(!result.success);
475
+ assert(result.reason === "error");
476
+ expect(result.error).toBe(testError);
477
+ });
478
+
479
+ it("should capture non-Error thrown values", async () => {
480
+ const { factory } = createMockUOWFactory([{ success: true }]);
481
+
482
+ const result = await executeUnitOfWork(
483
+ {
484
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
485
+ mutate: async () => {
486
+ throw "string error";
487
+ },
488
+ },
489
+ { createUnitOfWork: factory },
490
+ );
491
+
492
+ assert(!result.success);
493
+ assert(result.reason === "error");
494
+ expect(result.error).toBe("string error");
495
+ });
496
+ });
497
+
498
+ describe("retrieval results", () => {
499
+ it("should pass retrieval results to mutation phase", async () => {
500
+ const { factory } = createMockUOWFactory([{ success: true }]);
501
+ const mutationPhase = vi.fn(async (_uow: unknown, _results: unknown) => {});
502
+
503
+ await executeUnitOfWork(
504
+ {
505
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
506
+ mutate: mutationPhase,
507
+ },
508
+ { createUnitOfWork: factory },
509
+ );
510
+
511
+ expect(mutationPhase).toHaveBeenCalledTimes(1);
512
+ const call = mutationPhase.mock.calls[0];
513
+ assert(call);
514
+ const [_uow, results] = call;
515
+ expect(results).toBeInstanceOf(Array);
516
+ expect(results as unknown[]).toHaveLength(1);
517
+ expect((results as unknown[])[0]).toBeInstanceOf(Array);
518
+ });
519
+
520
+ it("should return retrieval results in the result object", async () => {
521
+ const { factory } = createMockUOWFactory([{ success: true }]);
522
+
523
+ const result = await executeUnitOfWork(
524
+ {
525
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
526
+ mutate: async () => {},
527
+ },
528
+ { createUnitOfWork: factory },
529
+ );
530
+
531
+ assert(result.success);
532
+ expect(result.results).toBeInstanceOf(Array);
533
+ expect(result.results).toHaveLength(1);
534
+ });
535
+ });
536
+
537
+ describe("promise awaiting in mutation result", () => {
538
+ it("should await promises in mutation result object (1 level deep)", async () => {
539
+ const { factory } = createMockUOWFactory([{ success: true }]);
540
+
541
+ const result = await executeUnitOfWork(
542
+ {
543
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
544
+ mutate: async () => {
545
+ return {
546
+ userId: Promise.resolve("user-123"),
547
+ count: Promise.resolve(42),
548
+ data: "plain-value",
549
+ };
550
+ },
551
+ },
552
+ { createUnitOfWork: factory },
553
+ );
554
+
555
+ assert(result.success);
556
+ expect(result.mutationResult).toEqual({
557
+ userId: "user-123",
558
+ count: 42,
559
+ data: "plain-value",
560
+ });
561
+ });
562
+
563
+ it("should await promises in mutation result array", async () => {
564
+ const { factory } = createMockUOWFactory([{ success: true }]);
565
+
566
+ const result = await executeUnitOfWork(
567
+ {
568
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
569
+ mutate: async () => {
570
+ return [Promise.resolve("a"), Promise.resolve("b"), "c"];
571
+ },
572
+ },
573
+ { createUnitOfWork: factory },
574
+ );
575
+
576
+ assert(result.success);
577
+ expect(result.mutationResult).toEqual(["a", "b", "c"]);
578
+ });
579
+
580
+ it("should await direct promise mutation result", async () => {
581
+ const { factory } = createMockUOWFactory([{ success: true }]);
582
+
583
+ const result = await executeUnitOfWork(
584
+ {
585
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
586
+ mutate: async () => {
587
+ return Promise.resolve({ value: "resolved" });
588
+ },
589
+ },
590
+ { createUnitOfWork: factory },
591
+ );
592
+
593
+ assert(result.success);
594
+ expect(result.mutationResult).toEqual({ value: "resolved" });
595
+ });
596
+
597
+ it("should NOT await nested promises (only 1 level deep)", async () => {
598
+ const { factory } = createMockUOWFactory([{ success: true }]);
599
+
600
+ const result = await executeUnitOfWork(
601
+ {
602
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
603
+ mutate: async () => {
604
+ return {
605
+ nested: { promise: Promise.resolve("still-a-promise") },
606
+ };
607
+ },
608
+ },
609
+ { createUnitOfWork: factory },
610
+ );
611
+
612
+ assert(result.success);
613
+ // The nested promise should still be a promise
614
+ expect(result.mutationResult.nested.promise).toBeInstanceOf(Promise);
615
+ });
616
+
617
+ it("should handle mixed types in mutation result", async () => {
618
+ const { factory } = createMockUOWFactory([{ success: true }]);
619
+
620
+ const result = await executeUnitOfWork(
621
+ {
622
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
623
+ mutate: async () => {
624
+ return {
625
+ promise: Promise.resolve(100),
626
+ number: 42,
627
+ string: "test",
628
+ null: null,
629
+ undefined: undefined,
630
+ nested: { value: "nested" },
631
+ };
632
+ },
633
+ },
634
+ { createUnitOfWork: factory },
635
+ );
636
+
637
+ assert(result.success);
638
+ expect(result.mutationResult).toEqual({
639
+ promise: 100,
640
+ number: 42,
641
+ string: "test",
642
+ null: null,
643
+ undefined: undefined,
644
+ nested: { value: "nested" },
645
+ });
646
+ });
647
+
648
+ it("should pass awaited mutation result to onSuccess callback", async () => {
649
+ const { factory } = createMockUOWFactory([{ success: true }]);
650
+ const onSuccess = vi.fn();
651
+
652
+ await executeUnitOfWork(
653
+ {
654
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
655
+ mutate: async () => {
656
+ return {
657
+ userId: Promise.resolve("user-456"),
658
+ status: Promise.resolve("active"),
659
+ };
660
+ },
661
+ onSuccess,
662
+ },
663
+ { createUnitOfWork: factory },
664
+ );
665
+
666
+ expect(onSuccess).toHaveBeenCalledExactlyOnceWith({
667
+ results: expect.any(Array),
668
+ mutationResult: {
669
+ userId: "user-456",
670
+ status: "active",
671
+ },
672
+ createdIds: [],
673
+ nonce: expect.any(String),
674
+ });
675
+ });
676
+ });
677
+ });
678
+
679
+ describe("executeRestrictedUnitOfWork", () => {
680
+ describe("basic success cases", () => {
681
+ it("should execute a simple mutation-only workflow", async () => {
682
+ const { factory, callCount } = createMockUOWFactory([{ success: true }]);
683
+
684
+ const result = await executeRestrictedUnitOfWork(
685
+ async ({ forSchema, executeMutate }) => {
686
+ const uow = forSchema(testSchema);
687
+ const userId = uow.create("users", {
688
+ id: "user-1",
689
+ email: "test@example.com",
690
+ name: "Test User",
691
+ balance: 100,
692
+ });
693
+
694
+ await executeMutate();
695
+
696
+ return { userId: userId.externalId };
697
+ },
698
+ { createUnitOfWork: factory },
699
+ );
700
+
701
+ expect(result).toEqual({ userId: "user-1" });
702
+ expect(callCount.value).toBe(1);
703
+ });
704
+
705
+ it("should execute retrieval and mutation phases", async () => {
706
+ const { factory, callCount } = createMockUOWFactory([{ success: true }]);
707
+
708
+ const result = await executeRestrictedUnitOfWork(
709
+ async ({ forSchema, executeRetrieve, executeMutate }) => {
710
+ const uow = forSchema(testSchema).find("users", (b) => b.whereIndex("primary"));
711
+ await executeRetrieve();
712
+ const [[user]] = await uow.retrievalPhase;
713
+
714
+ uow.update("users", user.id, (b) => b.set({ balance: user.balance + 50 }).check());
715
+ await executeMutate();
716
+
717
+ return { newBalance: user.balance + 50 };
718
+ },
719
+ { createUnitOfWork: factory },
720
+ );
721
+
722
+ expect(result).toEqual({ newBalance: 150 });
723
+ expect(callCount.value).toBe(1);
724
+ });
725
+
726
+ it("should return callback result directly", async () => {
727
+ const { factory } = createMockUOWFactory([{ success: true }]);
728
+
729
+ const result = await executeRestrictedUnitOfWork(
730
+ async () => {
731
+ return { data: "test", count: 42, nested: { value: true } };
732
+ },
733
+ { createUnitOfWork: factory },
734
+ );
735
+
736
+ expect(result).toEqual({ data: "test", count: 42, nested: { value: true } });
737
+ });
738
+ });
739
+
740
+ describe("retry behavior", () => {
741
+ it("should retry on conflict and eventually succeed", async () => {
742
+ const { factory, callCount } = createMockUOWFactory([
743
+ { success: false }, // First attempt fails
744
+ { success: false }, // Second attempt fails
745
+ { success: true }, // Third attempt succeeds
746
+ ]);
747
+
748
+ const callbackExecutions = { count: 0 };
749
+
750
+ const result = await executeRestrictedUnitOfWork(
751
+ async ({ forSchema, executeMutate }) => {
752
+ callbackExecutions.count++;
753
+ const uow = forSchema(testSchema);
754
+
755
+ uow.create("users", {
756
+ id: "user-1",
757
+ email: "test@example.com",
758
+ name: "Test User",
759
+ balance: 100,
760
+ });
761
+
762
+ await executeMutate();
763
+
764
+ return { attempt: callbackExecutions.count };
765
+ },
766
+ { createUnitOfWork: factory },
767
+ );
768
+
769
+ expect(result.attempt).toBe(3);
770
+ expect(callCount.value).toBe(3);
771
+ expect(callbackExecutions.count).toBe(3);
772
+ });
773
+
774
+ it("should throw error when retries are exhausted", async () => {
775
+ const { factory, callCount } = createMockUOWFactory([
776
+ { success: false }, // First attempt fails
777
+ { success: false }, // Second attempt fails
778
+ { success: false }, // Third attempt fails
779
+ { success: false }, // Fourth attempt fails (exceeds default maxRetries: 3)
780
+ ]);
781
+
782
+ await expect(
783
+ executeRestrictedUnitOfWork(
784
+ async ({ executeMutate }) => {
785
+ await executeMutate();
786
+ return { hello: "world" };
787
+ },
788
+ { createUnitOfWork: factory },
789
+ ),
790
+ ).rejects.toThrow("Unit of Work execution failed: optimistic concurrency conflict");
791
+
792
+ // Default policy has maxRetries: 5, so we make 6 attempts (initial + 5 retries)
793
+ expect(callCount.value).toBe(6);
794
+ });
795
+
796
+ it("should respect custom retry policy", async () => {
797
+ const { factory, callCount } = createMockUOWFactory([
798
+ { success: false },
799
+ { success: false },
800
+ { success: false },
801
+ { success: false },
802
+ { success: false },
803
+ { success: true },
804
+ ]);
805
+
806
+ const result = await executeRestrictedUnitOfWork(
807
+ async ({ executeMutate }) => {
808
+ await executeMutate();
809
+ return { done: true };
810
+ },
811
+ {
812
+ createUnitOfWork: factory,
813
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5, initialDelayMs: 1 }),
814
+ },
815
+ );
816
+
817
+ expect(result).toEqual({ done: true });
818
+ expect(callCount.value).toBe(6); // Initial + 5 retries
819
+ });
820
+
821
+ it("should use default ExponentialBackoffRetryPolicy with small delays", async () => {
822
+ const { factory } = createMockUOWFactory([{ success: false }, { success: true }]);
823
+
824
+ const startTime = Date.now();
825
+ await executeRestrictedUnitOfWork(
826
+ async ({ executeMutate }) => {
827
+ await executeMutate();
828
+ return {};
829
+ },
830
+ { createUnitOfWork: factory },
831
+ );
832
+ const elapsed = Date.now() - startTime;
833
+
834
+ // Default policy has initialDelayMs: 10, maxDelayMs: 100
835
+ // First retry delay should be around 10ms
836
+ expect(elapsed).toBeLessThan(200); // Allow some margin
837
+ });
838
+ });
839
+
840
+ describe("error handling", () => {
841
+ it("should throw error from callback", async () => {
842
+ const { factory, callCount } = createMockUOWFactory([{ success: true }]);
843
+
844
+ await expect(
845
+ executeRestrictedUnitOfWork(
846
+ async () => {
847
+ throw new Error("Callback error");
848
+ },
849
+ { createUnitOfWork: factory },
850
+ ),
851
+ ).rejects.toThrow("Unit of Work execution failed: optimistic concurrency conflict");
852
+
853
+ // Should attempt retries even on callback errors
854
+ expect(callCount.value).toBe(6); // Initial + 5 retries (default)
855
+ });
856
+
857
+ it("should wrap callback error as cause", async () => {
858
+ const { factory } = createMockUOWFactory([{ success: true }]);
859
+ const originalError = new Error("Original error");
860
+
861
+ try {
862
+ await executeRestrictedUnitOfWork(
863
+ async () => {
864
+ throw originalError;
865
+ },
866
+ {
867
+ createUnitOfWork: factory,
868
+ retryPolicy: new NoRetryPolicy(), // Don't retry
869
+ },
870
+ );
871
+ expect.fail("Should have thrown");
872
+ } catch (error) {
873
+ expect(error).toBeInstanceOf(Error);
874
+ expect((error as Error).cause).toBe(originalError);
875
+ }
876
+ });
877
+ });
878
+
879
+ describe("abort signal", () => {
880
+ it("should throw when aborted before execution", async () => {
881
+ const { factory } = createMockUOWFactory([{ success: true }]);
882
+ const controller = new AbortController();
883
+ controller.abort();
884
+
885
+ await expect(
886
+ executeRestrictedUnitOfWork(
887
+ async () => {
888
+ return {};
889
+ },
890
+ { createUnitOfWork: factory, signal: controller.signal },
891
+ ),
892
+ ).rejects.toThrow("Unit of Work execution aborted");
893
+ });
894
+
895
+ it("should stop retrying when aborted during retry", async () => {
896
+ const { factory, callCount } = createMockUOWFactory([
897
+ { success: false },
898
+ { success: false },
899
+ { success: true },
900
+ ]);
901
+ const controller = new AbortController();
902
+
903
+ const promise = executeRestrictedUnitOfWork(
904
+ async ({ executeMutate }) => {
905
+ if (callCount.value === 2) {
906
+ controller.abort();
907
+ }
908
+ await executeMutate();
909
+ return {};
910
+ },
911
+ { createUnitOfWork: factory, signal: controller.signal },
912
+ );
913
+
914
+ await expect(promise).rejects.toThrow("Unit of Work execution aborted");
915
+ expect(callCount.value).toBeLessThanOrEqual(2);
916
+ });
917
+ });
918
+
919
+ describe("restricted UOW interface", () => {
920
+ it("should provide access to forSchema", async () => {
921
+ const { factory } = createMockUOWFactory([{ success: true }]);
922
+
923
+ await executeRestrictedUnitOfWork(
924
+ async ({ forSchema }) => {
925
+ const uow = forSchema(testSchema);
926
+ expect(uow).toBeDefined();
927
+ expect(uow.schema).toBe(testSchema);
928
+ return {};
929
+ },
930
+ { createUnitOfWork: factory },
931
+ );
932
+ });
933
+
934
+ it("should allow creating entities via forSchema", async () => {
935
+ const { factory } = createMockUOWFactory([{ success: true }]);
936
+
937
+ const result = await executeRestrictedUnitOfWork(
938
+ async ({ forSchema, executeRetrieve, executeMutate }) => {
939
+ const uow = forSchema(testSchema);
940
+ await executeRetrieve();
941
+
942
+ const userId = uow.create("users", {
943
+ id: "user-123",
944
+ email: "test@example.com",
945
+ name: "Test",
946
+ balance: 0,
947
+ });
948
+
949
+ await executeMutate();
950
+
951
+ return { userId };
952
+ },
953
+ { createUnitOfWork: factory },
954
+ );
955
+
956
+ expect(result.userId).toBeInstanceOf(FragnoId);
957
+ expect(result.userId.externalId).toBe("user-123");
958
+ });
959
+ });
960
+
961
+ describe("promise awaiting in callback result", () => {
962
+ it("should await promises in result object (1 level deep)", async () => {
963
+ const { factory } = createMockUOWFactory([{ success: true }]);
964
+
965
+ const result = await executeRestrictedUnitOfWork(
966
+ async ({ executeMutate }) => {
967
+ await executeMutate();
968
+ return {
969
+ userId: Promise.resolve("user-123"),
970
+ profileId: Promise.resolve("profile-456"),
971
+ status: "completed",
972
+ };
973
+ },
974
+ { createUnitOfWork: factory },
975
+ );
976
+
977
+ expect(result).toEqual({
978
+ userId: "user-123",
979
+ profileId: "profile-456",
980
+ status: "completed",
981
+ });
982
+ });
983
+
984
+ it("should await promises in result array", async () => {
985
+ const { factory } = createMockUOWFactory([{ success: true }]);
986
+
987
+ const result = await executeRestrictedUnitOfWork(
988
+ async ({ executeMutate }) => {
989
+ await executeMutate();
990
+ return [Promise.resolve(1), Promise.resolve(2), 3];
991
+ },
992
+ { createUnitOfWork: factory },
993
+ );
994
+
995
+ expect(result).toEqual([1, 2, 3]);
996
+ });
997
+
998
+ it("should await direct promise result", async () => {
999
+ const { factory } = createMockUOWFactory([{ success: true }]);
1000
+
1001
+ const result = await executeRestrictedUnitOfWork(
1002
+ async ({ executeMutate }) => {
1003
+ await executeMutate();
1004
+ return Promise.resolve({ data: "test" });
1005
+ },
1006
+ { createUnitOfWork: factory },
1007
+ );
1008
+
1009
+ expect(result).toEqual({ data: "test" });
1010
+ });
1011
+
1012
+ it("should NOT await nested promises (only 1 level deep)", async () => {
1013
+ const { factory } = createMockUOWFactory([{ success: true }]);
1014
+
1015
+ const result = await executeRestrictedUnitOfWork(
1016
+ async ({ executeMutate }) => {
1017
+ await executeMutate();
1018
+ return {
1019
+ nested: {
1020
+ promise: Promise.resolve("still-a-promise"),
1021
+ },
1022
+ };
1023
+ },
1024
+ { createUnitOfWork: factory },
1025
+ );
1026
+
1027
+ // The nested promise should still be a promise
1028
+ expect(result.nested.promise).toBeInstanceOf(Promise);
1029
+ });
1030
+
1031
+ it("should handle mixed types in result", async () => {
1032
+ const { factory } = createMockUOWFactory([{ success: true }]);
1033
+
1034
+ const result = await executeRestrictedUnitOfWork(
1035
+ async ({ executeMutate }) => {
1036
+ await executeMutate();
1037
+ return {
1038
+ promise: Promise.resolve("resolved"),
1039
+ number: 42,
1040
+ string: "test",
1041
+ boolean: true,
1042
+ null: null,
1043
+ undefined: undefined,
1044
+ object: { nested: "value" },
1045
+ };
1046
+ },
1047
+ { createUnitOfWork: factory },
1048
+ );
1049
+
1050
+ expect(result).toEqual({
1051
+ promise: "resolved",
1052
+ number: 42,
1053
+ string: "test",
1054
+ boolean: true,
1055
+ null: null,
1056
+ undefined: undefined,
1057
+ object: { nested: "value" },
1058
+ });
1059
+ });
1060
+
1061
+ it("should await promises even after retries", async () => {
1062
+ const { factory, callCount } = createMockUOWFactory([{ success: false }, { success: true }]);
1063
+
1064
+ const result = await executeRestrictedUnitOfWork(
1065
+ async ({ executeMutate }) => {
1066
+ await executeMutate();
1067
+ return {
1068
+ attempt: callCount.value,
1069
+ data: Promise.resolve("final-result"),
1070
+ };
1071
+ },
1072
+ { createUnitOfWork: factory },
1073
+ );
1074
+
1075
+ expect(result).toEqual({
1076
+ attempt: 2,
1077
+ data: "final-result",
1078
+ });
1079
+ });
1080
+
1081
+ it("should handle complex objects with multiple promises at top level", async () => {
1082
+ const { factory } = createMockUOWFactory([{ success: true }]);
1083
+
1084
+ const result = await executeRestrictedUnitOfWork(
1085
+ async ({ executeMutate }) => {
1086
+ await executeMutate();
1087
+ return {
1088
+ userId: Promise.resolve("user-1"),
1089
+ email: Promise.resolve("test@example.com"),
1090
+ count: Promise.resolve(100),
1091
+ active: Promise.resolve(true),
1092
+ metadata: {
1093
+ timestamp: Date.now(),
1094
+ version: 1,
1095
+ },
1096
+ };
1097
+ },
1098
+ { createUnitOfWork: factory },
1099
+ );
1100
+
1101
+ expect(typeof result.userId).toBe("string");
1102
+ expect(result.userId).toBe("user-1");
1103
+ expect(typeof result.email).toBe("string");
1104
+ expect(result.email).toBe("test@example.com");
1105
+ expect(typeof result.count).toBe("number");
1106
+ expect(result.count).toBe(100);
1107
+ expect(typeof result.active).toBe("boolean");
1108
+ expect(result.active).toBe(true);
1109
+ expect(typeof result.metadata.timestamp).toBe("number");
1110
+ expect(result.metadata.version).toBe(1);
1111
+ });
1112
+
1113
+ it("should handle empty object result", async () => {
1114
+ const { factory } = createMockUOWFactory([{ success: true }]);
1115
+
1116
+ const result = await executeRestrictedUnitOfWork(
1117
+ async ({ executeMutate }) => {
1118
+ await executeMutate();
1119
+ return {};
1120
+ },
1121
+ { createUnitOfWork: factory },
1122
+ );
1123
+
1124
+ expect(result).toEqual({});
1125
+ });
1126
+
1127
+ it("should handle primitive result types", async () => {
1128
+ const { factory } = createMockUOWFactory([{ success: true }]);
1129
+
1130
+ const stringResult = await executeRestrictedUnitOfWork(
1131
+ async ({ executeMutate }) => {
1132
+ await executeMutate();
1133
+ return "test-string";
1134
+ },
1135
+ { createUnitOfWork: factory },
1136
+ );
1137
+
1138
+ expect(stringResult).toBe("test-string");
1139
+
1140
+ const { factory: factory2 } = createMockUOWFactory([{ success: true }]);
1141
+ const numberResult = await executeRestrictedUnitOfWork(
1142
+ async ({ executeMutate }) => {
1143
+ await executeMutate();
1144
+ return 42;
1145
+ },
1146
+ { createUnitOfWork: factory2 },
1147
+ );
1148
+
1149
+ expect(numberResult).toBe(42);
1150
+ });
1151
+ });
1152
+
1153
+ describe("tuple return types", () => {
1154
+ it("should await promises in tuple and preserve tuple structure", async () => {
1155
+ const { factory } = createMockUOWFactory([{ success: true }]);
1156
+
1157
+ const result = await executeRestrictedUnitOfWork(
1158
+ async ({ executeMutate }) => {
1159
+ await executeMutate();
1160
+ // Return a tuple with promises
1161
+ return [Promise.resolve("user-123"), Promise.resolve(42)] as const;
1162
+ },
1163
+ { createUnitOfWork: factory },
1164
+ );
1165
+
1166
+ // Runtime behavior: promises should be awaited
1167
+ expect(result).toEqual(["user-123", 42]);
1168
+ expect(result[0]).toBe("user-123");
1169
+ expect(result[1]).toBe(42);
1170
+ });
1171
+
1172
+ it("should handle tuple with mixed promise and non-promise values", async () => {
1173
+ const { factory } = createMockUOWFactory([{ success: true }]);
1174
+
1175
+ const result = await executeRestrictedUnitOfWork(
1176
+ async ({ executeMutate }) => {
1177
+ await executeMutate();
1178
+ // Tuple with mixed types
1179
+ return [Promise.resolve("first"), "second", Promise.resolve(3)] as const;
1180
+ },
1181
+ { createUnitOfWork: factory },
1182
+ );
1183
+
1184
+ expect(result).toEqual(["first", "second", 3]);
1185
+ expect(result[0]).toBe("first");
1186
+ expect(result[1]).toBe("second");
1187
+ expect(result[2]).toBe(3);
1188
+ });
1189
+
1190
+ it("should handle Promise.all pattern with tuple", async () => {
1191
+ const { factory } = createMockUOWFactory([{ success: true }]);
1192
+
1193
+ const result = await executeRestrictedUnitOfWork(
1194
+ async ({ executeMutate }) => {
1195
+ await executeMutate();
1196
+ // Simulate the pattern from db-fragment-integration.test.ts
1197
+ const userPromise = Promise.resolve({ id: "user-1", name: "John" });
1198
+ const ordersPromise = Promise.resolve([
1199
+ { id: "order-1", total: 100 },
1200
+ { id: "order-2", total: 200 },
1201
+ ]);
1202
+ return await Promise.all([userPromise, ordersPromise]);
1203
+ },
1204
+ { createUnitOfWork: factory },
1205
+ );
1206
+
1207
+ // Runtime behavior
1208
+ expect(result).toHaveLength(2);
1209
+ expect(result[0]).toEqual({ id: "user-1", name: "John" });
1210
+ expect(result[1]).toEqual([
1211
+ { id: "order-1", total: 100 },
1212
+ { id: "order-2", total: 200 },
1213
+ ]);
1214
+
1215
+ // Type check: result should be [{ id: string; name: string }, { id: string; total: number }[]]
1216
+ // But with current implementation, it's incorrectly typed as an array union
1217
+ const [user, orders] = result;
1218
+ expect(user).toBeDefined();
1219
+ expect(orders).toBeDefined();
1220
+ });
1221
+
1222
+ it("should handle array (not tuple) with promises", async () => {
1223
+ const { factory } = createMockUOWFactory([{ success: true }]);
1224
+
1225
+ const result = await executeRestrictedUnitOfWork(
1226
+ async ({ executeMutate }) => {
1227
+ await executeMutate();
1228
+ // Regular array (not a tuple)
1229
+ const items = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
1230
+ return items;
1231
+ },
1232
+ { createUnitOfWork: factory },
1233
+ );
1234
+
1235
+ expect(result).toEqual([1, 2, 3]);
1236
+ expect(result).toHaveLength(3);
1237
+ });
1238
+ });
1239
+
1240
+ describe("unhandled rejection handling", () => {
1241
+ it("should not cause unhandled rejection when service method awaits retrievalPhase and executeRetrieve fails", async () => {
1242
+ const settingsSchema = schema((s) =>
1243
+ s.addTable("settings", (t) =>
1244
+ t
1245
+ .addColumn("id", idColumn())
1246
+ .addColumn("key", "string")
1247
+ .addColumn("value", "string")
1248
+ .createIndex("unique_key", ["key"], { unique: true }),
1249
+ ),
1250
+ );
1251
+
1252
+ // Create executor that throws "table does not exist" error
1253
+ const failingExecutor: UOWExecutor<unknown, unknown> = {
1254
+ executeRetrievalPhase: async () => {
1255
+ throw new Error('relation "settings" does not exist');
1256
+ },
1257
+ executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
1258
+ };
1259
+
1260
+ const factory = () =>
1261
+ createUnitOfWork(createMockCompiler(), failingExecutor, createMockDecoder());
1262
+
1263
+ const deferred = Promise.withResolvers<string>();
1264
+
1265
+ // Service method that awaits retrievalPhase (simulating settingsService.get())
1266
+ const getSettingValue = async (typedUow: TypedUnitOfWork<typeof settingsSchema>) => {
1267
+ const uow = typedUow.find("settings", (b) =>
1268
+ b.whereIndex("unique_key", (eb) => eb("key", "=", "version")),
1269
+ );
1270
+ const [results] = await uow.retrievalPhase;
1271
+ return results?.[0];
1272
+ };
1273
+
1274
+ // Execute with executeRestrictedUnitOfWork
1275
+ try {
1276
+ await executeRestrictedUnitOfWork(
1277
+ async ({ forSchema, executeRetrieve }) => {
1278
+ const uow = forSchema(settingsSchema);
1279
+
1280
+ const settingPromise = getSettingValue(uow);
1281
+
1282
+ // Execute retrieval - this will fail
1283
+ await executeRetrieve();
1284
+
1285
+ // Won't reach here
1286
+ return await settingPromise;
1287
+ },
1288
+ {
1289
+ createUnitOfWork: factory,
1290
+ retryPolicy: new NoRetryPolicy(),
1291
+ },
1292
+ );
1293
+ expect.fail("Should have thrown an error");
1294
+ } catch (error) {
1295
+ // The error should be wrapped by executeRestrictedUnitOfWork
1296
+ expect(error).toBeInstanceOf(Error);
1297
+ // Check that the original error is in the cause chain
1298
+ expect((error as Error).cause).toBeInstanceOf(Error);
1299
+ expect(((error as Error).cause as Error).message).toContain(
1300
+ 'relation "settings" does not exist',
1301
+ );
1302
+ deferred.resolve((error as Error).message);
1303
+ }
1304
+
1305
+ // Verify no unhandled rejection occurred
1306
+ // If the test completes without throwing, the promise rejection was properly handled
1307
+ expect(await deferred.promise).toContain("Unit of Work execution failed");
1308
+ });
1309
+ });
1310
+ });