@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,463 @@
1
+ import type { AnySchema } from "../schema/create";
2
+ import type { TypedUnitOfWork, IUnitOfWork } from "./unit-of-work";
3
+ import { NoRetryPolicy, ExponentialBackoffRetryPolicy, type RetryPolicy } from "./retry-policy";
4
+ import type { FragnoId } from "../schema/create";
5
+
6
+ /**
7
+ * Type utility that unwraps promises 1 level deep in objects, arrays, or direct promises
8
+ * Handles tuples, arrays, objects, and direct promises
9
+ */
10
+ export type AwaitedPromisesInObject<T> =
11
+ // First check if it's a Promise
12
+ T extends Promise<infer U>
13
+ ? Awaited<U>
14
+ : // Check for arrays with known length (tuples) - preserves tuple structure
15
+ T extends readonly [unknown, ...unknown[]]
16
+ ? { [K in keyof T]: AwaitedPromisesInObject<T[K]> }
17
+ : T extends [unknown, ...unknown[]]
18
+ ? { [K in keyof T]: AwaitedPromisesInObject<T[K]> }
19
+ : // Check for regular arrays (unknown length)
20
+ T extends (infer U)[]
21
+ ? Awaited<U>[]
22
+ : T extends readonly (infer U)[]
23
+ ? readonly Awaited<U>[]
24
+ : // Check for objects
25
+ T extends Record<string, unknown>
26
+ ? {
27
+ [K in keyof T]: T[K] extends Promise<infer U> ? Awaited<U> : T[K];
28
+ }
29
+ : // Otherwise return as-is
30
+ T;
31
+
32
+ /**
33
+ * Await promises in an object 1 level deep
34
+ */
35
+ async function awaitPromisesInObject<T>(obj: T): Promise<AwaitedPromisesInObject<T>> {
36
+ if (obj === null || obj === undefined) {
37
+ return obj as AwaitedPromisesInObject<T>;
38
+ }
39
+
40
+ if (typeof obj !== "object") {
41
+ return obj as AwaitedPromisesInObject<T>;
42
+ }
43
+
44
+ // Check if it's a Promise
45
+ if (obj instanceof Promise) {
46
+ return (await obj) as AwaitedPromisesInObject<T>;
47
+ }
48
+
49
+ // Check if it's an array
50
+ if (Array.isArray(obj)) {
51
+ const awaited = await Promise.all(
52
+ obj.map((item) => (item instanceof Promise ? item : Promise.resolve(item))),
53
+ );
54
+ return awaited as AwaitedPromisesInObject<T>;
55
+ }
56
+
57
+ // It's a plain object - await promises in each property
58
+ const result = {} as T;
59
+ const entries = Object.entries(obj as Record<string, unknown>);
60
+ const awaitedEntries = await Promise.all(
61
+ entries.map(async ([key, value]) => {
62
+ const awaitedValue = value instanceof Promise ? await value : value;
63
+ return [key, awaitedValue] as const;
64
+ }),
65
+ );
66
+
67
+ for (const [key, value] of awaitedEntries) {
68
+ (result as Record<string, unknown>)[key] = value;
69
+ }
70
+
71
+ return result as AwaitedPromisesInObject<T>;
72
+ }
73
+
74
+ /**
75
+ * Result of executing a Unit of Work with retry support
76
+ * Promises in mutationResult are unwrapped 1 level deep
77
+ */
78
+ export type ExecuteUnitOfWorkResult<TRetrievalResults, TMutationResult> =
79
+ | {
80
+ success: true;
81
+ results: TRetrievalResults;
82
+ mutationResult: AwaitedPromisesInObject<TMutationResult>;
83
+ createdIds: FragnoId[];
84
+ nonce: string;
85
+ }
86
+ | {
87
+ success: false;
88
+ reason: "conflict";
89
+ }
90
+ | {
91
+ success: false;
92
+ reason: "aborted";
93
+ }
94
+ | {
95
+ success: false;
96
+ reason: "error";
97
+ error: unknown;
98
+ };
99
+
100
+ /**
101
+ * Callbacks for executing a Unit of Work
102
+ */
103
+ export interface ExecuteUnitOfWorkCallbacks<
104
+ TSchema extends AnySchema,
105
+ TRetrievalResults extends unknown[],
106
+ TMutationResult,
107
+ TRawInput,
108
+ > {
109
+ /**
110
+ * Retrieval phase callback - adds retrieval operations to the UOW
111
+ */
112
+ retrieve?: (
113
+ uow: TypedUnitOfWork<TSchema, [], TRawInput>,
114
+ ) => TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>;
115
+
116
+ /**
117
+ * Mutation phase callback - receives UOW and retrieval results, adds mutation operations
118
+ */
119
+ mutate?: (
120
+ uow: TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>,
121
+ results: TRetrievalResults,
122
+ ) => TMutationResult | Promise<TMutationResult>;
123
+
124
+ /**
125
+ * Success callback - invoked after successful execution
126
+ * Promises in mutationResult are already unwrapped 1 level deep
127
+ */
128
+ onSuccess?: (result: {
129
+ results: TRetrievalResults;
130
+ mutationResult: AwaitedPromisesInObject<TMutationResult>;
131
+ createdIds: FragnoId[];
132
+ nonce: string;
133
+ }) => void | Promise<void>;
134
+ }
135
+
136
+ /**
137
+ * Options for executing a Unit of Work
138
+ */
139
+ export interface ExecuteUnitOfWorkOptions<TSchema extends AnySchema, TRawInput> {
140
+ /**
141
+ * Factory function that creates or resets a UOW instance for each attempt
142
+ */
143
+ createUnitOfWork: () => TypedUnitOfWork<TSchema, [], TRawInput>;
144
+
145
+ /**
146
+ * Retry policy for handling optimistic concurrency conflicts
147
+ */
148
+ retryPolicy?: RetryPolicy;
149
+
150
+ /**
151
+ * Abort signal to cancel execution
152
+ */
153
+ signal?: AbortSignal;
154
+ }
155
+
156
+ /**
157
+ * Create a bound version of executeUnitOfWork with a pre-configured UOW factory.
158
+ * This is useful for handler contexts where the factory is already known.
159
+ *
160
+ * @param createUnitOfWork - Factory function that creates a fresh UOW instance
161
+ * @returns A bound executeUnitOfWork function that doesn't require the factory parameter
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * const boundExecute = createExecuteUnitOfWork(() => db.createUnitOfWork());
166
+ * const result = await boundExecute({
167
+ * retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
168
+ * mutate: (uow, [users]) => {
169
+ * uow.update("users", users[0].id, (b) => b.set({ balance: newBalance }));
170
+ * }
171
+ * });
172
+ * ```
173
+ */
174
+ export function createExecuteUnitOfWork<TSchema extends AnySchema, TRawInput>(
175
+ createUnitOfWork: () => TypedUnitOfWork<TSchema, [], TRawInput>,
176
+ ) {
177
+ return async function <TRetrievalResults extends unknown[], TMutationResult = void>(
178
+ callbacks: ExecuteUnitOfWorkCallbacks<TSchema, TRetrievalResults, TMutationResult, TRawInput>,
179
+ options?: Omit<ExecuteUnitOfWorkOptions<TSchema, TRawInput>, "createUnitOfWork">,
180
+ ): Promise<ExecuteUnitOfWorkResult<TRetrievalResults, TMutationResult>> {
181
+ return executeUnitOfWork(callbacks, { ...options, createUnitOfWork });
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Execute a Unit of Work with automatic retry support for optimistic concurrency conflicts.
187
+ *
188
+ * This function orchestrates the two-phase execution (retrieval + mutation) with retry logic.
189
+ * It creates fresh UOW instances for each attempt.
190
+ *
191
+ * @param callbacks - Object containing retrieve, mutate, and onSuccess callbacks
192
+ * @param options - Configuration including UOW factory, retry policy, and abort signal
193
+ * @returns Promise resolving to the execution result
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * const result = await executeUnitOfWork(
198
+ * {
199
+ * retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
200
+ * mutate: (uow, [users]) => {
201
+ * const user = users[0];
202
+ * uow.update("users", user.id, (b) => b.set({ balance: newBalance }));
203
+ * },
204
+ * onSuccess: async ({ results, mutationResult }) => {
205
+ * console.log("Update successful!");
206
+ * }
207
+ * },
208
+ * {
209
+ * createUnitOfWork: () => queryEngine.createUnitOfWork(),
210
+ * retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3 })
211
+ * }
212
+ * );
213
+ * ```
214
+ */
215
+ export async function executeUnitOfWork<
216
+ TSchema extends AnySchema,
217
+ TRetrievalResults extends unknown[],
218
+ TMutationResult = void,
219
+ TRawInput = unknown,
220
+ >(
221
+ callbacks: ExecuteUnitOfWorkCallbacks<TSchema, TRetrievalResults, TMutationResult, TRawInput>,
222
+ options: ExecuteUnitOfWorkOptions<TSchema, TRawInput>,
223
+ ): Promise<ExecuteUnitOfWorkResult<TRetrievalResults, TMutationResult>> {
224
+ // Validate that at least one of retrieve or mutate is provided
225
+ if (!callbacks.retrieve && !callbacks.mutate) {
226
+ throw new Error("At least one of 'retrieve' or 'mutate' callbacks must be provided");
227
+ }
228
+
229
+ const retryPolicy = options.retryPolicy ?? new NoRetryPolicy();
230
+ const signal = options.signal;
231
+ let attempt = 0;
232
+
233
+ while (true) {
234
+ // Check if aborted before starting attempt
235
+ if (signal?.aborted) {
236
+ return { success: false, reason: "aborted" };
237
+ }
238
+
239
+ try {
240
+ // Create a fresh UOW for this attempt
241
+ const uow = options.createUnitOfWork();
242
+
243
+ // Apply retrieval phase if provided
244
+ let retrievalUow: TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>;
245
+ if (callbacks.retrieve) {
246
+ retrievalUow = callbacks.retrieve(uow);
247
+ } else {
248
+ // No retrieval phase, use empty UOW with type cast
249
+ // This is safe because when there's no retrieve, TRetrievalResults should be []
250
+ retrievalUow = uow as unknown as TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>;
251
+ }
252
+
253
+ // Execute retrieval phase
254
+ const results = (await retrievalUow.executeRetrieve()) as TRetrievalResults;
255
+
256
+ // Invoke mutation phase callback if provided
257
+ let mutationResult: TMutationResult;
258
+ if (callbacks.mutate) {
259
+ mutationResult = await callbacks.mutate(retrievalUow, results);
260
+ } else {
261
+ mutationResult = undefined as TMutationResult;
262
+ }
263
+
264
+ // Execute mutation phase
265
+ const { success } = await retrievalUow.executeMutations();
266
+
267
+ if (success) {
268
+ // Success! Get created IDs and nonce, then invoke onSuccess if provided
269
+ const createdIds = retrievalUow.getCreatedIds();
270
+ const nonce = retrievalUow.nonce;
271
+
272
+ // Await promises in mutationResult (1 level deep)
273
+ const awaitedMutationResult = await awaitPromisesInObject(mutationResult);
274
+
275
+ if (callbacks.onSuccess) {
276
+ await callbacks.onSuccess({
277
+ results,
278
+ mutationResult: awaitedMutationResult,
279
+ createdIds,
280
+ nonce,
281
+ });
282
+ }
283
+
284
+ return {
285
+ success: true,
286
+ results,
287
+ mutationResult: awaitedMutationResult,
288
+ createdIds,
289
+ nonce,
290
+ };
291
+ }
292
+
293
+ // Failed - check if we should retry
294
+ // attempt represents the number of attempts completed so far
295
+ if (!retryPolicy.shouldRetry(attempt, undefined, signal)) {
296
+ // No more retries
297
+ return { success: false, reason: "conflict" };
298
+ }
299
+
300
+ // Wait before retrying
301
+ const delayMs = retryPolicy.getDelayMs(attempt);
302
+ if (delayMs > 0) {
303
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
304
+ }
305
+
306
+ // Increment attempt counter for next iteration
307
+ attempt++;
308
+ } catch (error) {
309
+ // An error was thrown during execution
310
+ return { success: false, reason: "error", error };
311
+ }
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Options for executing a Unit of Work with restricted access
317
+ */
318
+ export interface ExecuteRestrictedUnitOfWorkOptions {
319
+ /**
320
+ * Factory function that creates or resets a UOW instance for each attempt
321
+ */
322
+ createUnitOfWork: () => IUnitOfWork;
323
+
324
+ /**
325
+ * Retry policy for handling optimistic concurrency conflicts
326
+ */
327
+ retryPolicy?: RetryPolicy;
328
+
329
+ /**
330
+ * Abort signal to cancel execution
331
+ */
332
+ signal?: AbortSignal;
333
+ }
334
+
335
+ /**
336
+ * Execute a Unit of Work with explicit phase control and automatic retry support.
337
+ *
338
+ * This function provides an alternative API where users write a single callback that receives
339
+ * a context object with forSchema, executeRetrieve, and executeMutate methods. The user can
340
+ * create schema-specific UOWs via forSchema, then call executeRetrieve() and executeMutate()
341
+ * to execute the retrieval and mutation phases. The entire callback is re-executed on optimistic
342
+ * concurrency conflicts, ensuring retries work properly.
343
+ *
344
+ * @param callback - Async function that receives a context with forSchema, executeRetrieve, executeMutate, nonce, and currentAttempt
345
+ * @param options - Configuration including UOW factory, retry policy, and abort signal
346
+ * @returns Promise resolving to the callback's return value
347
+ * @throws Error if retries are exhausted or callback throws an error
348
+ *
349
+ * @example
350
+ * ```ts
351
+ * const { userId, profileId } = await executeRestrictedUnitOfWork(
352
+ * async ({ forSchema, executeRetrieve, executeMutate, nonce, currentAttempt }) => {
353
+ * const uow = forSchema(schema);
354
+ * const userId = uow.create("users", { name: "John" });
355
+ *
356
+ * // Execute retrieval phase
357
+ * await executeRetrieve();
358
+ *
359
+ * const profileId = uow.create("profiles", { userId });
360
+ *
361
+ * // Execute mutation phase
362
+ * await executeMutate();
363
+ *
364
+ * return { userId, profileId };
365
+ * },
366
+ * {
367
+ * createUnitOfWork: () => db.createUnitOfWork(),
368
+ * retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5 })
369
+ * }
370
+ * );
371
+ * ```
372
+ */
373
+ export async function executeRestrictedUnitOfWork<TResult>(
374
+ callback: (context: {
375
+ forSchema: <S extends AnySchema>(schema: S) => TypedUnitOfWork<S, [], unknown>;
376
+ executeRetrieve: () => Promise<void>;
377
+ executeMutate: () => Promise<void>;
378
+ nonce: string;
379
+ currentAttempt: number;
380
+ }) => Promise<TResult>,
381
+ options: ExecuteRestrictedUnitOfWorkOptions,
382
+ ): Promise<AwaitedPromisesInObject<TResult>> {
383
+ // Default retry policy with small, fast retries for optimistic concurrency
384
+ const retryPolicy =
385
+ options.retryPolicy ??
386
+ new ExponentialBackoffRetryPolicy({
387
+ maxRetries: 5,
388
+ initialDelayMs: 10,
389
+ maxDelayMs: 100,
390
+ });
391
+ const signal = options.signal;
392
+ let attempt = 0;
393
+
394
+ while (true) {
395
+ // Check if aborted before starting attempt
396
+ if (signal?.aborted) {
397
+ throw new Error("Unit of Work execution aborted");
398
+ }
399
+
400
+ try {
401
+ // Create a fresh UOW for this attempt
402
+ const baseUow = options.createUnitOfWork();
403
+
404
+ // Create context object with forSchema, executeRetrieve, executeMutate, nonce, and currentAttempt
405
+ const context = {
406
+ forSchema: <S extends AnySchema>(schema: S) => {
407
+ return baseUow.forSchema(schema);
408
+ },
409
+ executeRetrieve: async () => {
410
+ await baseUow.executeRetrieve();
411
+ },
412
+ executeMutate: async () => {
413
+ if (baseUow.state === "executed") {
414
+ return;
415
+ }
416
+
417
+ if (baseUow.state === "building-retrieval") {
418
+ await baseUow.executeRetrieve();
419
+ }
420
+
421
+ const result = await baseUow.executeMutations();
422
+ if (!result.success) {
423
+ throw new Error("Mutations failed due to conflict");
424
+ }
425
+ },
426
+ nonce: baseUow.nonce,
427
+ currentAttempt: attempt,
428
+ };
429
+
430
+ // Execute the callback which will call executeRetrieve and executeMutate
431
+ const result = await callback(context);
432
+
433
+ // Await promises in the result object (1 level deep)
434
+ const awaitedResult = await awaitPromisesInObject(result);
435
+
436
+ // Return the awaited result
437
+ return awaitedResult;
438
+ } catch (error) {
439
+ if (signal?.aborted) {
440
+ throw new Error("Unit of Work execution aborted");
441
+ }
442
+
443
+ if (!retryPolicy.shouldRetry(attempt, error, signal)) {
444
+ // No more retries - check again if aborted or throw conflict error
445
+ if (signal?.aborted) {
446
+ throw new Error("Unit of Work execution aborted");
447
+ }
448
+ throw new Error("Unit of Work execution failed: optimistic concurrency conflict", {
449
+ cause: error,
450
+ });
451
+ }
452
+
453
+ // Wait before retrying
454
+ const delayMs = retryPolicy.getDelayMs(attempt);
455
+ if (delayMs > 0) {
456
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
457
+ }
458
+
459
+ // Increment attempt counter for next iteration
460
+ attempt++;
461
+ }
462
+ }
463
+ }
@@ -1,7 +1,7 @@
1
1
  import type { IdColumn, AnySchema, AnyTable, Relation, FragnoId } from "../schema/create";
2
2
  import type { Condition, ConditionBuilder } from "./condition-builder";
3
3
  import type {
4
- UnitOfWork,
4
+ TypedUnitOfWork,
5
5
  FindBuilder,
6
6
  UpdateBuilder,
7
7
  DeleteBuilder,
@@ -82,7 +82,7 @@ export type OrderBy<Column = string> = [columnName: Column, "asc" | "desc"];
82
82
  * Extract Select type parameter from a FindBuilder type (handles Omit wrapper)
83
83
  * @internal
84
84
  */
85
- type ExtractSelect<T> =
85
+ export type ExtractSelect<T> =
86
86
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
87
  T extends FindBuilder<any, infer TSelect, any>
88
88
  ? TSelect
@@ -95,7 +95,7 @@ type ExtractSelect<T> =
95
95
  * Extract JoinOut type parameter from a FindBuilder type (handles Omit wrapper)
96
96
  * @internal
97
97
  */
98
- type ExtractJoinOut<T> =
98
+ export type ExtractJoinOut<T> =
99
99
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
100
  T extends FindBuilder<any, any, infer TJoinOut>
101
101
  ? TJoinOut
@@ -258,5 +258,5 @@ export interface AbstractQuery<TSchema extends AnySchema, TUOWConfig = void> {
258
258
  /**
259
259
  * Create a Unit of Work bound to this query engine
260
260
  */
261
- createUnitOfWork: (name?: string, config?: TUOWConfig) => UnitOfWork<TSchema, []>;
261
+ createUnitOfWork: (name?: string, config?: TUOWConfig) => TypedUnitOfWork<TSchema, [], unknown>;
262
262
  }
@@ -262,6 +262,135 @@ describe("encodeValues", () => {
262
262
  expect(mysqlResult).toEqual({ id: "user123", name: "John" });
263
263
  });
264
264
  });
265
+
266
+ describe("skipDriverConversions parameter", () => {
267
+ it("should skip Date to number conversion for sqlite when skipDriverConversions is true", () => {
268
+ const date = new Date("2024-01-15T10:30:00Z");
269
+ const result = encodeValues({ createdAt: date }, usersTable, false, "sqlite", true);
270
+
271
+ // Date should remain as Date object, not converted to timestamp
272
+ expect(result).toEqual({ createdAt: date });
273
+ });
274
+
275
+ it("should skip boolean to number conversion for sqlite when skipDriverConversions is true", () => {
276
+ const result = encodeValues({ isActive: true }, usersTable, false, "sqlite", true);
277
+
278
+ // Boolean should remain as boolean, not converted to 1
279
+ expect(result).toEqual({ isActive: true });
280
+ });
281
+
282
+ it("should skip bigint to Buffer conversion for sqlite when skipDriverConversions is true", () => {
283
+ const schemaWithBigInt = schema((s) => {
284
+ return s.addTable("items", (t) => {
285
+ return t.addColumn("id", idColumn()).addColumn("count", column("bigint"));
286
+ });
287
+ });
288
+
289
+ const result = encodeValues(
290
+ { count: BigInt(12345) },
291
+ schemaWithBigInt.tables.items,
292
+ false,
293
+ "sqlite",
294
+ true,
295
+ );
296
+
297
+ // BigInt should remain as bigint, not converted to Buffer
298
+ expect(result).toEqual({ count: BigInt(12345) });
299
+ });
300
+
301
+ it("should still perform conversions for sqlite when skipDriverConversions is false", () => {
302
+ const date = new Date("2024-01-15T10:30:00Z");
303
+ const result = encodeValues(
304
+ { createdAt: date, isActive: true },
305
+ usersTable,
306
+ false,
307
+ "sqlite",
308
+ false,
309
+ );
310
+
311
+ // Conversions should happen as normal
312
+ expect(result).toEqual({ createdAt: date.getTime(), isActive: 1 });
313
+ });
314
+
315
+ it("should not affect PostgreSQL encoding (no conversions to skip)", () => {
316
+ const date = new Date("2024-01-15T10:30:00Z");
317
+ const resultWithSkip = encodeValues(
318
+ { createdAt: date },
319
+ usersTable,
320
+ false,
321
+ "postgresql",
322
+ true,
323
+ );
324
+ const resultWithoutSkip = encodeValues(
325
+ { createdAt: date },
326
+ usersTable,
327
+ false,
328
+ "postgresql",
329
+ false,
330
+ );
331
+
332
+ // Both should be the same since PostgreSQL doesn't do these conversions
333
+ expect(resultWithSkip).toEqual({ createdAt: date });
334
+ expect(resultWithoutSkip).toEqual({ createdAt: date });
335
+ });
336
+
337
+ it("should work with complete record encoding when skipDriverConversions is true", () => {
338
+ const date = new Date("2024-01-15T10:30:00Z");
339
+ const result = encodeValues(
340
+ {
341
+ id: "user1",
342
+ name: "Alice",
343
+ email: "alice@example.com",
344
+ age: 30,
345
+ isActive: false,
346
+ createdAt: date,
347
+ },
348
+ usersTable,
349
+ false,
350
+ "sqlite",
351
+ true,
352
+ );
353
+
354
+ // All values should remain in their original types
355
+ expect(result).toEqual({
356
+ id: "user1",
357
+ name: "Alice",
358
+ email: "alice@example.com",
359
+ age: 30,
360
+ isActive: false, // Not converted to 0
361
+ createdAt: date, // Not converted to timestamp
362
+ });
363
+ });
364
+
365
+ it("should still handle FragnoId and ReferenceSubquery correctly with skipDriverConversions", () => {
366
+ const fragnoId = FragnoId.fromExternal("user123", 1);
367
+ const result = encodeValues(
368
+ { id: fragnoId, name: "John" },
369
+ usersTable,
370
+ false,
371
+ "sqlite",
372
+ true,
373
+ );
374
+
375
+ // FragnoId handling should still work
376
+ expect(result).toEqual({
377
+ id: "user123",
378
+ name: "John",
379
+ });
380
+
381
+ // Test ReferenceSubquery
382
+ const refResult = encodeValues(
383
+ { title: "Test Post", userId: "user_external_id" },
384
+ postsTable,
385
+ false,
386
+ "sqlite",
387
+ true,
388
+ );
389
+
390
+ expect(refResult["title"]).toBe("Test Post");
391
+ expect(refResult["userId"]).toBeInstanceOf(ReferenceSubquery);
392
+ });
393
+ });
265
394
  });
266
395
 
267
396
  describe("decodeResult", () => {
@@ -86,6 +86,8 @@ export function generateRuntimeDefault(column: AnyColumn): unknown {
86
86
  * @param table - The table schema definition containing column information
87
87
  * @param generateDefault - Whether to generate default values for undefined columns
88
88
  * @param provider - The SQL provider (sqlite, postgresql, mysql, etc.)
89
+ * @param skipDriverConversions - Skip driver-level type conversions (Date->number, boolean->0/1, bigint->Buffer).
90
+ * Set to true when using ORMs like Drizzle that handle these conversions internally.
89
91
  * @returns A record with database-compatible column names and serialized values
90
92
  *
91
93
  * @example
@@ -104,6 +106,7 @@ export function encodeValues(
104
106
  table: AnyTable,
105
107
  generateDefault: boolean,
106
108
  provider: SQLProvider,
109
+ skipDriverConversions = false,
107
110
  ): Record<string, unknown> {
108
111
  const result: Record<string, unknown> = {};
109
112
 
@@ -155,7 +158,7 @@ export function encodeValues(
155
158
  }
156
159
  }
157
160
 
158
- result[col.name] = serialize(value, col, provider);
161
+ result[col.name] = serialize(value, col, provider, skipDriverConversions);
159
162
  }
160
163
  }
161
164