@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
@@ -1,11 +1,19 @@
1
1
  import type { AnySchema, AnyTable, Index, IdColumn, AnyColumn, Relation } from "../schema/create";
2
2
  import { FragnoId } from "../schema/create";
3
3
  import type { Condition, ConditionBuilder } from "./condition-builder";
4
- import type { SelectClause, TableToInsertValues, TableToUpdateValues, SelectResult } from "./query";
4
+ import type {
5
+ SelectClause,
6
+ TableToInsertValues,
7
+ TableToUpdateValues,
8
+ SelectResult,
9
+ ExtractSelect,
10
+ ExtractJoinOut,
11
+ } from "./query";
5
12
  import { buildCondition } from "./condition-builder";
6
13
  import type { CompiledJoin } from "./orm/orm";
7
14
  import type { CursorResult } from "./cursor";
8
15
  import { Cursor } from "./cursor";
16
+ import type { Prettify } from "../util/types";
9
17
 
10
18
  /**
11
19
  * Builder for updateMany operations that supports both whereIndex and set chaining
@@ -143,13 +151,18 @@ export type RetrievalOperation<
143
151
  > =
144
152
  | {
145
153
  type: "find";
154
+ schema: TSchema;
155
+ namespace?: string;
146
156
  table: TTable;
147
157
  indexName: string;
148
158
  options: FindOptions<TTable, SelectClause<TTable>>;
149
159
  withCursor?: boolean;
160
+ withSingleResult?: boolean;
150
161
  }
151
162
  | {
152
163
  type: "count";
164
+ schema: TSchema;
165
+ namespace?: string;
153
166
  table: TTable;
154
167
  indexName: string;
155
168
  options: Pick<FindOptions<TTable>, "where" | "useIndex">;
@@ -164,6 +177,8 @@ export type MutationOperation<
164
177
  > =
165
178
  | {
166
179
  type: "update";
180
+ schema: TSchema;
181
+ namespace?: string;
167
182
  table: TTable["name"];
168
183
  id: FragnoId | string;
169
184
  checkVersion: boolean;
@@ -171,15 +186,26 @@ export type MutationOperation<
171
186
  }
172
187
  | {
173
188
  type: "create";
189
+ schema: TSchema;
190
+ namespace?: string;
174
191
  table: TTable["name"];
175
192
  values: TableToInsertValues<TTable>;
176
193
  generatedExternalId: string;
177
194
  }
178
195
  | {
179
196
  type: "delete";
197
+ schema: TSchema;
198
+ namespace?: string;
180
199
  table: TTable["name"];
181
200
  id: FragnoId | string;
182
201
  checkVersion: boolean;
202
+ }
203
+ | {
204
+ type: "check";
205
+ schema: TSchema;
206
+ namespace?: string;
207
+ table: TTable["name"];
208
+ id: FragnoId;
183
209
  };
184
210
 
185
211
  /**
@@ -193,21 +219,27 @@ export interface CompiledMutation<TOutput> {
193
219
  * null means don't check affected rows (e.g., for create operations).
194
220
  */
195
221
  expectedAffectedRows: number | null;
222
+ /**
223
+ * Number of rows this SELECT query must return for the transaction to succeed.
224
+ * Used for check operations to verify version without modifying data.
225
+ * null means this is not a SELECT query that needs row count validation.
226
+ */
227
+ expectedReturnedRows: number | null;
196
228
  }
197
229
 
198
230
  /**
199
231
  * Compiler interface for Unit of Work operations
200
232
  */
201
- export interface UOWCompiler<TSchema extends AnySchema, TOutput> {
233
+ export interface UOWCompiler<TOutput> {
202
234
  /**
203
235
  * Compile a retrieval operation to the adapter's query format
204
236
  */
205
- compileRetrievalOperation(op: RetrievalOperation<TSchema>): TOutput | null;
237
+ compileRetrievalOperation(op: RetrievalOperation<AnySchema>): TOutput | null;
206
238
 
207
239
  /**
208
240
  * Compile a mutation operation to the adapter's query format
209
241
  */
210
- compileMutationOperation(op: MutationOperation<TSchema>): CompiledMutation<TOutput> | null;
242
+ compileMutationOperation(op: MutationOperation<AnySchema>): CompiledMutation<TOutput> | null;
211
243
  }
212
244
 
213
245
  export type MutationResult =
@@ -237,7 +269,7 @@ export interface UOWExecutor<TOutput, TRawResult = unknown> {
237
269
  * Transforms raw database results into application format (e.g., converting raw columns
238
270
  * into FragnoId objects with external ID, internal ID, and version).
239
271
  */
240
- export interface UOWDecoder<TSchema extends AnySchema, TRawInput = unknown> {
272
+ export interface UOWDecoder<TRawInput = unknown> {
241
273
  /**
242
274
  * Decode raw database results from the retrieval phase
243
275
  *
@@ -245,7 +277,7 @@ export interface UOWDecoder<TSchema extends AnySchema, TRawInput = unknown> {
245
277
  * @param operations - Array of retrieval operations that produced these results
246
278
  * @returns Decoded results in application format
247
279
  */
248
- (rawResults: TRawInput[], operations: RetrievalOperation<TSchema>[]): unknown[];
280
+ (rawResults: TRawInput[], operations: RetrievalOperation<AnySchema>[]): unknown[];
249
281
  }
250
282
 
251
283
  /**
@@ -843,27 +875,259 @@ export function buildJoinIndexed<TTable extends AnyTable, TJoinOut>(
843
875
  return compiled;
844
876
  }
845
877
 
846
- export function createUnitOfWork<
847
- const TSchema extends AnySchema,
848
- const TRetrievalResults extends unknown[] = [],
849
- const TRawInput = unknown,
850
- >(
851
- schema: TSchema,
852
- compiler: UOWCompiler<TSchema, unknown>,
853
- executor: UOWExecutor<unknown, TRawInput>,
854
- decoder: UOWDecoder<TSchema, TRawInput>,
878
+ /**
879
+ * Full Unit of Work interface with all operations including execution.
880
+ * This allows UOW instances to be passed between different contexts that use different schemas.
881
+ */
882
+ export interface IUnitOfWork {
883
+ // Getters (schema-agnostic)
884
+ readonly state: UOWState;
885
+ readonly name: string | undefined;
886
+ readonly nonce: string;
887
+ readonly retrievalPhase: Promise<unknown[]>;
888
+ readonly mutationPhase: Promise<void>;
889
+
890
+ // Execution (schema-agnostic)
891
+ executeRetrieve(): Promise<unknown[]>;
892
+ executeMutations(): Promise<{ success: boolean }>;
893
+
894
+ // Inspection (schema-agnostic)
895
+ getRetrievalOperations(): ReadonlyArray<RetrievalOperation<AnySchema>>;
896
+ getMutationOperations(): ReadonlyArray<MutationOperation<AnySchema>>;
897
+ getCreatedIds(): FragnoId[];
898
+
899
+ // Parent-child relationships
900
+ restrict(): IUnitOfWork;
901
+
902
+ // Reset for retry support
903
+ reset(): void;
904
+
905
+ // Schema-specific view (for cross-schema operations)
906
+ forSchema<TOtherSchema extends AnySchema>(
907
+ schema: TOtherSchema,
908
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
909
+ ): TypedUnitOfWork<TOtherSchema, [], any>;
910
+ }
911
+
912
+ /**
913
+ * Restricted UOW interface without execute methods.
914
+ * Useful when you want to allow building operations but not executing them,
915
+ * to prevent deadlocks or enforce execution control at a higher level.
916
+ *
917
+ * Note: This is just a marker interface. Restriction is enforced by the UnitOfWork class itself.
918
+ */
919
+ export interface IUnitOfWorkRestricted
920
+ extends Omit<IUnitOfWork, "executeRetrieve" | "executeMutations"> {}
921
+
922
+ export function createUnitOfWork(
923
+ compiler: UOWCompiler<unknown>,
924
+ executor: UOWExecutor<unknown, unknown>,
925
+ decoder: UOWDecoder<unknown>,
926
+ schemaNamespaceMap?: WeakMap<AnySchema, string>,
855
927
  name?: string,
856
- ): UnitOfWork<TSchema, TRetrievalResults, TRawInput> {
857
- return new UnitOfWork(schema, compiler, executor, decoder, name) as UnitOfWork<
858
- TSchema,
859
- TRetrievalResults,
860
- TRawInput
861
- >;
928
+ ): UnitOfWork {
929
+ return new UnitOfWork(compiler, executor, decoder, name, undefined, schemaNamespaceMap);
862
930
  }
863
931
 
864
932
  export interface UnitOfWorkConfig {
865
933
  dryRun?: boolean;
866
934
  onQuery?: (query: unknown) => void;
935
+ nonce?: string;
936
+ }
937
+
938
+ /**
939
+ * Encapsulates a promise with its resolver/rejecter functions.
940
+ * Simplifies management of deferred promises with built-in error handling.
941
+ */
942
+ class DeferredPromise<T> {
943
+ #resolve?: (value: T) => void;
944
+ #reject?: (error: Error) => void;
945
+ #promise: Promise<T>;
946
+
947
+ constructor() {
948
+ const { promise, resolve, reject } = Promise.withResolvers<T>();
949
+ this.#promise = promise;
950
+ this.#resolve = resolve;
951
+ this.#reject = reject;
952
+ // Attach no-op error handler to prevent unhandled rejection warnings
953
+ this.#promise.catch(() => {});
954
+ }
955
+
956
+ get promise(): Promise<T> {
957
+ return this.#promise;
958
+ }
959
+
960
+ resolve(value: T): void {
961
+ this.#resolve?.(value);
962
+ }
963
+
964
+ reject(error: Error): void {
965
+ this.#reject?.(error);
966
+ }
967
+
968
+ /**
969
+ * Reset to a new promise
970
+ */
971
+ reset(): void {
972
+ const { promise, resolve, reject } = Promise.withResolvers<T>();
973
+ this.#promise = promise;
974
+ this.#resolve = resolve;
975
+ this.#reject = reject;
976
+ // Attach no-op error handler to prevent unhandled rejection warnings
977
+ this.#promise.catch(() => {});
978
+ }
979
+ }
980
+
981
+ /**
982
+ * Tracks readiness signals from a group of children.
983
+ * Maintains a promise that resolves when all registered children have signaled.
984
+ */
985
+ class ReadinessTracker {
986
+ #expectedCount = 0;
987
+ #signalCount = 0;
988
+ #resolve?: () => void;
989
+ #promise: Promise<void> = Promise.resolve();
990
+
991
+ get promise(): Promise<void> {
992
+ return this.#promise;
993
+ }
994
+
995
+ /**
996
+ * Register that we're expecting a signal from a child
997
+ */
998
+ registerChild(): void {
999
+ if (this.#expectedCount === 0) {
1000
+ // First child - create new promise
1001
+ const { promise, resolve } = Promise.withResolvers<void>();
1002
+ this.#promise = promise;
1003
+ this.#resolve = resolve;
1004
+ }
1005
+ this.#expectedCount++;
1006
+ }
1007
+
1008
+ /**
1009
+ * Signal that one child is ready
1010
+ */
1011
+ signal(): void {
1012
+ this.#signalCount++;
1013
+ if (this.#signalCount >= this.#expectedCount && this.#resolve) {
1014
+ this.#resolve();
1015
+ }
1016
+ }
1017
+
1018
+ /**
1019
+ * Reset to initial state
1020
+ */
1021
+ reset(): void {
1022
+ this.#expectedCount = 0;
1023
+ this.#signalCount = 0;
1024
+ this.#resolve = undefined;
1025
+ this.#promise = Promise.resolve();
1026
+ }
1027
+ }
1028
+
1029
+ /**
1030
+ * Manages parent-child relationships and readiness coordination for Unit of Work instances.
1031
+ * This allows parent UOWs to wait for all child UOWs to signal readiness before executing phases.
1032
+ */
1033
+ class UOWChildCoordinator<TRawInput> {
1034
+ #parent: UnitOfWork<TRawInput> | null = null;
1035
+ #parentCoordinator: UOWChildCoordinator<TRawInput> | null = null;
1036
+ #children: Set<UnitOfWork<TRawInput>> = new Set();
1037
+ #isRestricted = false;
1038
+
1039
+ #retrievalTracker = new ReadinessTracker();
1040
+ #mutationTracker = new ReadinessTracker();
1041
+
1042
+ get isRestricted(): boolean {
1043
+ return this.#isRestricted;
1044
+ }
1045
+
1046
+ get parent(): UnitOfWork<TRawInput> | null {
1047
+ return this.#parent;
1048
+ }
1049
+
1050
+ get children(): ReadonlySet<UnitOfWork<TRawInput>> {
1051
+ return this.#children;
1052
+ }
1053
+
1054
+ get retrievalReadinessPromise(): Promise<void> {
1055
+ return this.#retrievalTracker.promise;
1056
+ }
1057
+
1058
+ get mutationReadinessPromise(): Promise<void> {
1059
+ return this.#mutationTracker.promise;
1060
+ }
1061
+
1062
+ /**
1063
+ * Mark this UOW as a restricted child of the given parent
1064
+ */
1065
+ setAsRestricted(
1066
+ parent: UnitOfWork<TRawInput>,
1067
+ parentCoordinator: UOWChildCoordinator<TRawInput>,
1068
+ ): void {
1069
+ this.#parent = parent;
1070
+ this.#parentCoordinator = parentCoordinator;
1071
+ this.#isRestricted = true;
1072
+ }
1073
+
1074
+ /**
1075
+ * Register a child UOW
1076
+ */
1077
+ addChild(child: UnitOfWork<TRawInput>): void {
1078
+ this.#children.add(child);
1079
+ this.#retrievalTracker.registerChild();
1080
+ this.#mutationTracker.registerChild();
1081
+ }
1082
+
1083
+ /**
1084
+ * Signal that this child is ready for retrieval phase execution.
1085
+ * Only valid for restricted (child) UOWs.
1086
+ */
1087
+ signalReadyForRetrieval(): void {
1088
+ if (!this.#parentCoordinator) {
1089
+ throw new Error("signalReadyForRetrieval() can only be called on restricted child UOWs");
1090
+ }
1091
+
1092
+ this.#parentCoordinator.notifyChildReadyForRetrieval();
1093
+ }
1094
+
1095
+ /**
1096
+ * Signal that this child is ready for mutation phase execution.
1097
+ * Only valid for restricted (child) UOWs.
1098
+ */
1099
+ signalReadyForMutation(): void {
1100
+ if (!this.#parentCoordinator) {
1101
+ throw new Error("signalReadyForMutation() can only be called on restricted child UOWs");
1102
+ }
1103
+
1104
+ this.#parentCoordinator.notifyChildReadyForMutation();
1105
+ }
1106
+
1107
+ /**
1108
+ * Notify this coordinator that a child is ready for retrieval (internal use).
1109
+ * Called by child UOWs when they signal readiness.
1110
+ */
1111
+ notifyChildReadyForRetrieval(): void {
1112
+ this.#retrievalTracker.signal();
1113
+ }
1114
+
1115
+ /**
1116
+ * Notify this coordinator that a child is ready for mutation (internal use).
1117
+ * Called by child UOWs when they signal readiness.
1118
+ */
1119
+ notifyChildReadyForMutation(): void {
1120
+ this.#mutationTracker.signal();
1121
+ }
1122
+
1123
+ /**
1124
+ * Reset coordination state for retry support
1125
+ */
1126
+ reset(): void {
1127
+ this.#children.clear();
1128
+ this.#retrievalTracker.reset();
1129
+ this.#mutationTracker.reset();
1130
+ }
867
1131
  }
868
1132
 
869
1133
  /**
@@ -873,19 +1137,22 @@ export interface UnitOfWorkConfig {
873
1137
  * 1. Retrieval phase: Read operations to fetch entities with their versions
874
1138
  * 2. Mutation phase: Write operations that check versions before committing
875
1139
  *
1140
+ * This is the untyped base storage. Use TypedUnitOfWork for type-safe operations.
1141
+ *
876
1142
  * @example
877
1143
  * ```ts
878
1144
  * const uow = queryEngine.createUnitOfWork("update-user-balance");
1145
+ * const typedUow = uow.forSchema(mySchema);
879
1146
  *
880
1147
  * // Retrieval phase
881
- * uow.find("users", (b) => b.where("primary", (eb) => eb("id", "=", userId)));
1148
+ * typedUow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)));
882
1149
  *
883
1150
  * // Execute retrieval and transition to mutation phase
884
1151
  * const [users] = await uow.executeRetrieve();
885
1152
  *
886
1153
  * // Mutation phase with version check
887
1154
  * const user = users[0];
888
- * uow.update("users", user.id, (b) => b.set({ balance: newBalance }).check());
1155
+ * typedUow.update("users", user.id, (b) => b.set({ balance: newBalance }).check());
889
1156
  *
890
1157
  * // Execute mutations
891
1158
  * const { success } = await uow.executeMutations();
@@ -894,46 +1161,146 @@ export interface UnitOfWorkConfig {
894
1161
  * }
895
1162
  * ```
896
1163
  */
897
- export class UnitOfWork<
898
- const TSchema extends AnySchema,
899
- const TRetrievalResults extends unknown[] = [],
900
- const TRawInput = unknown,
901
- > {
902
- #schema: TSchema;
903
-
1164
+ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
904
1165
  #name?: string;
905
1166
  #config?: UnitOfWorkConfig;
1167
+ #nonce: string;
906
1168
 
907
1169
  #state: UOWState = "building-retrieval";
908
1170
 
909
- #retrievalOps: RetrievalOperation<TSchema>[] = [];
910
- #mutationOps: MutationOperation<TSchema>[] = [];
1171
+ // Operations can come from any schema
1172
+ #retrievalOps: RetrievalOperation<AnySchema>[] = [];
1173
+ #mutationOps: MutationOperation<AnySchema>[] = [];
911
1174
 
912
- #compiler: UOWCompiler<TSchema, unknown>;
1175
+ #compiler: UOWCompiler<unknown>;
913
1176
  #executor: UOWExecutor<unknown, TRawInput>;
914
- #decoder: UOWDecoder<TSchema, TRawInput>;
1177
+ #decoder: UOWDecoder<TRawInput>;
1178
+ #schemaNamespaceMap?: WeakMap<AnySchema, string>;
915
1179
 
916
- #retrievalResults?: TRetrievalResults;
1180
+ #retrievalResults?: unknown[];
917
1181
  #createdInternalIds: (bigint | null)[] = [];
918
1182
 
1183
+ // Phase coordination promises
1184
+ #retrievalPhaseDeferred = new DeferredPromise<unknown[]>();
1185
+ #mutationPhaseDeferred = new DeferredPromise<void>();
1186
+
1187
+ // Error tracking
1188
+ #retrievalError: Error | null = null;
1189
+ #mutationError: Error | null = null;
1190
+
1191
+ // Child coordination
1192
+ #coordinator: UOWChildCoordinator<TRawInput> = new UOWChildCoordinator();
1193
+
919
1194
  constructor(
920
- schema: TSchema,
921
- compiler: UOWCompiler<TSchema, unknown>,
1195
+ compiler: UOWCompiler<unknown>,
922
1196
  executor: UOWExecutor<unknown, TRawInput>,
923
- decoder: UOWDecoder<TSchema, TRawInput>,
1197
+ decoder: UOWDecoder<TRawInput>,
924
1198
  name?: string,
925
1199
  config?: UnitOfWorkConfig,
1200
+ schemaNamespaceMap?: WeakMap<AnySchema, string>,
926
1201
  ) {
927
- this.#schema = schema;
928
1202
  this.#compiler = compiler;
929
1203
  this.#executor = executor;
930
1204
  this.#decoder = decoder;
1205
+ this.#schemaNamespaceMap = schemaNamespaceMap;
931
1206
  this.#name = name;
932
1207
  this.#config = config;
1208
+ this.#nonce = config?.nonce ?? crypto.randomUUID();
933
1209
  }
934
1210
 
935
- get schema(): TSchema {
936
- return this.#schema;
1211
+ /**
1212
+ * Get a schema-specific typed view of this UOW for type-safe operations.
1213
+ * Returns a wrapper that provides typed operations for the given schema.
1214
+ * The namespace is automatically resolved from the schema-namespace map.
1215
+ */
1216
+ forSchema<TOtherSchema extends AnySchema, TRawInput>(
1217
+ schema: TOtherSchema,
1218
+ ): TypedUnitOfWork<TOtherSchema, [], TRawInput> {
1219
+ // Look up namespace from map
1220
+ const resolvedNamespace = this.#schemaNamespaceMap?.get(schema);
1221
+
1222
+ return new TypedUnitOfWork(schema, resolvedNamespace, this as unknown as UnitOfWork<TRawInput>);
1223
+ }
1224
+
1225
+ /**
1226
+ * Create a restricted child UOW that cannot execute phases.
1227
+ * The child shares the same operation storage but must signal readiness
1228
+ * before the parent can execute each phase.
1229
+ */
1230
+ restrict(): UnitOfWork<TRawInput> {
1231
+ const child = new UnitOfWork(
1232
+ this.#compiler,
1233
+ this.#executor,
1234
+ this.#decoder,
1235
+ this.#name,
1236
+ { ...this.#config, nonce: this.#nonce },
1237
+ this.#schemaNamespaceMap,
1238
+ );
1239
+ child.#coordinator.setAsRestricted(this, this.#coordinator);
1240
+
1241
+ // Share state with parent
1242
+ child.#state = this.#state;
1243
+ child.#retrievalOps = this.#retrievalOps;
1244
+ child.#mutationOps = this.#mutationOps;
1245
+ child.#retrievalResults = this.#retrievalResults;
1246
+ child.#createdInternalIds = this.#createdInternalIds;
1247
+ child.#retrievalPhaseDeferred = this.#retrievalPhaseDeferred;
1248
+ child.#mutationPhaseDeferred = this.#mutationPhaseDeferred;
1249
+ child.#retrievalError = this.#retrievalError;
1250
+ child.#mutationError = this.#mutationError;
1251
+
1252
+ this.#coordinator.addChild(child);
1253
+
1254
+ // For synchronous usage (the common case), immediately signal readiness
1255
+ // This allows services called directly from handlers to work without explicit signaling
1256
+ child.signalReadyForRetrieval();
1257
+ child.signalReadyForMutation();
1258
+
1259
+ return child;
1260
+ }
1261
+
1262
+ /**
1263
+ * Signal that this child is ready for retrieval phase execution.
1264
+ * Only valid for restricted (child) UOWs.
1265
+ */
1266
+ signalReadyForRetrieval(): void {
1267
+ this.#coordinator.signalReadyForRetrieval();
1268
+ }
1269
+
1270
+ /**
1271
+ * Signal that this child is ready for mutation phase execution.
1272
+ * Only valid for restricted (child) UOWs.
1273
+ */
1274
+ signalReadyForMutation(): void {
1275
+ this.#coordinator.signalReadyForMutation();
1276
+ }
1277
+
1278
+ /**
1279
+ * Reset the UOW to initial state for retry support.
1280
+ * Clears operations, resets state, and resets phase promises.
1281
+ */
1282
+ reset(): void {
1283
+ if (this.#coordinator.isRestricted) {
1284
+ throw new Error("reset() cannot be called on restricted child UOWs");
1285
+ }
1286
+
1287
+ // Clear operations
1288
+ this.#retrievalOps = [];
1289
+ this.#mutationOps = [];
1290
+ this.#retrievalResults = undefined;
1291
+ this.#createdInternalIds = [];
1292
+
1293
+ // Reset state
1294
+ this.#state = "building-retrieval";
1295
+ this.#retrievalError = null;
1296
+ this.#mutationError = null;
1297
+
1298
+ // Reset phase promises
1299
+ this.#retrievalPhaseDeferred.reset();
1300
+ this.#mutationPhaseDeferred.reset();
1301
+
1302
+ // Reset child coordination
1303
+ this.#coordinator.reset();
937
1304
  }
938
1305
 
939
1306
  get state(): UOWState {
@@ -944,13 +1311,33 @@ export class UnitOfWork<
944
1311
  return this.#name;
945
1312
  }
946
1313
 
1314
+ get nonce(): string {
1315
+ return this.#nonce;
1316
+ }
1317
+
1318
+ /**
1319
+ * Promise that resolves when the retrieval phase is executed
1320
+ * Service methods can await this to coordinate multi-phase logic
1321
+ */
1322
+ get retrievalPhase(): Promise<unknown[]> {
1323
+ return this.#retrievalPhaseDeferred.promise;
1324
+ }
1325
+
1326
+ /**
1327
+ * Promise that resolves when the mutation phase is executed
1328
+ * Service methods can await this to coordinate multi-phase logic
1329
+ */
1330
+ get mutationPhase(): Promise<void> {
1331
+ return this.#mutationPhaseDeferred.promise;
1332
+ }
1333
+
947
1334
  /**
948
1335
  * Execute the retrieval phase and transition to mutation phase
949
1336
  * Returns all results from find operations
950
1337
  */
951
- async executeRetrieve(): Promise<TRetrievalResults> {
952
- if (this.#retrievalOps.length === 0) {
953
- return [] as unknown as TRetrievalResults;
1338
+ async executeRetrieve(): Promise<unknown[]> {
1339
+ if (this.#coordinator.isRestricted) {
1340
+ throw new Error("executeRetrieve() cannot be called on restricted child UOWs");
954
1341
  }
955
1342
 
956
1343
  if (this.#state !== "building-retrieval") {
@@ -959,169 +1346,529 @@ export class UnitOfWork<
959
1346
  );
960
1347
  }
961
1348
 
962
- // Compile retrieval operations
963
- const retrievalBatch: unknown[] = [];
964
- for (const op of this.#retrievalOps) {
965
- const compiled = this.#compiler.compileRetrievalOperation(op);
966
- if (compiled !== null) {
967
- this.#config?.onQuery?.(compiled);
1349
+ try {
1350
+ // Wait for all children to signal readiness
1351
+ await this.#coordinator.retrievalReadinessPromise;
968
1352
 
969
- retrievalBatch.push(compiled);
1353
+ if (this.#retrievalOps.length === 0) {
1354
+ this.#state = "building-mutation";
1355
+ const emptyResults: unknown[] = [];
1356
+ this.#retrievalPhaseDeferred.resolve(emptyResults);
1357
+ return emptyResults;
970
1358
  }
971
- }
972
1359
 
973
- if (this.#config?.dryRun) {
974
- this.#state = "executed";
975
- return [] as unknown as TRetrievalResults;
976
- }
1360
+ // Compile retrieval operations using single compiler
1361
+ const retrievalBatch: unknown[] = [];
1362
+ for (const op of this.#retrievalOps) {
1363
+ const compiled = this.#compiler.compileRetrievalOperation(op);
1364
+ if (compiled !== null) {
1365
+ this.#config?.onQuery?.(compiled);
1366
+ retrievalBatch.push(compiled);
1367
+ }
1368
+ }
977
1369
 
978
- const results = this.#decoder(
979
- await this.#executor.executeRetrievalPhase(retrievalBatch),
980
- this.#retrievalOps,
981
- );
1370
+ if (this.#config?.dryRun) {
1371
+ this.#state = "executed";
1372
+ const emptyResults: unknown[] = [];
1373
+ this.#retrievalPhaseDeferred.resolve(emptyResults);
1374
+ return emptyResults;
1375
+ }
982
1376
 
983
- // Store results and transition to mutation phase
984
- this.#retrievalResults = results as TRetrievalResults;
985
- this.#state = "building-mutation";
1377
+ const rawResults = await this.#executor.executeRetrievalPhase(retrievalBatch);
986
1378
 
987
- return this.#retrievalResults;
1379
+ const results = this.#decoder(rawResults, this.#retrievalOps);
1380
+
1381
+ // Store results and transition to mutation phase
1382
+ this.#retrievalResults = results;
1383
+ this.#state = "building-mutation";
1384
+
1385
+ this.#retrievalPhaseDeferred.resolve(this.#retrievalResults);
1386
+
1387
+ return this.#retrievalResults;
1388
+ } catch (error) {
1389
+ this.#retrievalError = error instanceof Error ? error : new Error(String(error));
1390
+ throw error;
1391
+ }
988
1392
  }
989
1393
 
990
1394
  /**
991
- * Add a find operation using a builder callback (retrieval phase only)
1395
+ * Execute the mutation phase
1396
+ * Returns success flag indicating if mutations completed without conflicts
992
1397
  */
993
- find<
994
- TTableName extends keyof TSchema["tables"] & string,
995
- TSelect extends SelectClause<TSchema["tables"][TTableName]> = true,
996
- TJoinOut = {},
997
- >(
998
- tableName: TTableName,
999
- builderFn?: (
1000
- // We omit "build" because we don't want to expose it to the user
1001
- builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1002
- ) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build"> | void,
1003
- ): UnitOfWork<
1004
- TSchema,
1005
- [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>[]],
1006
- TRawInput
1007
- > {
1008
- if (this.#state !== "building-retrieval") {
1009
- throw new Error(
1010
- `find() can only be called during retrieval phase. Current state: ${this.#state}`,
1011
- );
1398
+ async executeMutations(): Promise<{ success: boolean }> {
1399
+ if (this.#coordinator.isRestricted) {
1400
+ throw new Error("executeMutations() cannot be called on restricted child UOWs");
1012
1401
  }
1013
1402
 
1014
- const table = this.#schema.tables[tableName];
1015
- if (!table) {
1016
- throw new Error(`Table ${tableName} not found in schema`);
1403
+ if (this.#state === "executed") {
1404
+ throw new Error(`Cannot execute mutations from state ${this.#state}.`);
1017
1405
  }
1018
1406
 
1019
- // Create builder, pass to callback (or use default), then extract configuration
1020
- const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1021
- if (builderFn) {
1022
- builderFn(builder);
1023
- } else {
1024
- // Default to primary index with no filter
1025
- builder.whereIndex("primary");
1407
+ try {
1408
+ // Wait for all children to signal readiness
1409
+ await this.#coordinator.mutationReadinessPromise;
1410
+
1411
+ // Compile mutation operations using single compiler
1412
+ const mutationBatch: CompiledMutation<unknown>[] = [];
1413
+ for (const op of this.#mutationOps) {
1414
+ const compiled = this.#compiler.compileMutationOperation(op);
1415
+ if (compiled !== null) {
1416
+ this.#config?.onQuery?.(compiled);
1417
+ mutationBatch.push(compiled);
1418
+ }
1419
+ }
1420
+
1421
+ if (this.#config?.dryRun) {
1422
+ this.#state = "executed";
1423
+ this.#mutationPhaseDeferred.resolve();
1424
+ return {
1425
+ success: true,
1426
+ };
1427
+ }
1428
+
1429
+ // Execute mutation phase
1430
+ const result = await this.#executor.executeMutationPhase(mutationBatch);
1431
+ this.#state = "executed";
1432
+
1433
+ if (result.success) {
1434
+ this.#createdInternalIds = result.createdInternalIds;
1435
+ }
1436
+
1437
+ // Resolve the mutation phase promise to unblock waiting service methods
1438
+ this.#mutationPhaseDeferred.resolve();
1439
+
1440
+ return {
1441
+ success: result.success,
1442
+ };
1443
+ } catch (error) {
1444
+ this.#mutationError = error instanceof Error ? error : new Error(String(error));
1445
+ throw error;
1026
1446
  }
1027
- const { indexName, options, type } = builder.build();
1447
+ }
1028
1448
 
1029
- this.#retrievalOps.push({
1030
- type,
1031
- // Safe: we know the table is part of the schema from the find() method
1032
- table: table as TSchema["tables"][TTableName],
1033
- indexName,
1034
- // Safe: we're storing the options for later compilation
1035
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1036
- options: options as any,
1037
- });
1449
+ /**
1450
+ * Get the retrieval operations (for inspection/debugging)
1451
+ */
1452
+ getRetrievalOperations(): ReadonlyArray<RetrievalOperation<AnySchema>> {
1453
+ return this.#retrievalOps;
1454
+ }
1038
1455
 
1039
- return this as unknown as UnitOfWork<
1040
- TSchema,
1041
- [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>[]],
1042
- TRawInput
1043
- >;
1456
+ /**
1457
+ * Get the mutation operations (for inspection/debugging)
1458
+ */
1459
+ getMutationOperations(): ReadonlyArray<MutationOperation<AnySchema>> {
1460
+ return this.#mutationOps;
1044
1461
  }
1045
1462
 
1046
1463
  /**
1047
- * Add a find operation with cursor metadata (retrieval phase only)
1464
+ * @internal
1465
+ * Add a retrieval operation (used by TypedUnitOfWork)
1048
1466
  */
1049
- findWithCursor<
1050
- TTableName extends keyof TSchema["tables"] & string,
1051
- TSelect extends SelectClause<TSchema["tables"][TTableName]> = true,
1052
- TJoinOut = {},
1053
- >(
1054
- tableName: TTableName,
1055
- builderFn: (
1056
- // We omit "build" because we don't want to expose it to the user
1057
- builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1058
- ) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build"> | void,
1059
- ): UnitOfWork<
1060
- TSchema,
1061
- [
1062
- ...TRetrievalResults,
1063
- CursorResult<SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>>,
1064
- ],
1065
- TRawInput
1066
- > {
1467
+ addRetrievalOperation(op: RetrievalOperation<AnySchema>): number {
1067
1468
  if (this.#state !== "building-retrieval") {
1068
1469
  throw new Error(
1069
- `findWithCursor() can only be called during retrieval phase. Current state: ${this.#state}`,
1470
+ `Cannot add retrieval operation in state ${this.#state}. Must be in building-retrieval state.`,
1070
1471
  );
1071
1472
  }
1072
-
1073
- const table = this.#schema.tables[tableName];
1074
- if (!table) {
1075
- throw new Error(`Table ${tableName} not found in schema`);
1076
- }
1077
-
1078
- // Create builder and pass to callback
1079
- const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1080
- builderFn(builder);
1081
- const { indexName, options, type } = builder.build();
1082
-
1083
- this.#retrievalOps.push({
1084
- type,
1085
- // Safe: we know the table is part of the schema from the findWithCursor() method
1086
- table: table as TSchema["tables"][TTableName],
1087
- indexName,
1088
- // Safe: we're storing the options for later compilation
1089
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1090
- options: options as any,
1091
- withCursor: true,
1092
- });
1093
-
1094
- return this as unknown as UnitOfWork<
1095
- TSchema,
1096
- [
1097
- ...TRetrievalResults,
1098
- CursorResult<SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>>,
1099
- ],
1100
- TRawInput
1101
- >;
1473
+ this.#retrievalOps.push(op);
1474
+ return this.#retrievalOps.length - 1;
1102
1475
  }
1103
1476
 
1104
1477
  /**
1105
- * Add a create operation (mutation phase only)
1106
- * Returns a FragnoId with the external ID that can be used immediately in subsequent operations
1478
+ * @internal
1479
+ * Add a mutation operation (used by TypedUnitOfWork)
1107
1480
  */
1108
- create<TableName extends keyof TSchema["tables"] & string>(
1109
- table: TableName,
1110
- values: TableToInsertValues<TSchema["tables"][TableName]>,
1111
- ): FragnoId {
1481
+ addMutationOperation(op: MutationOperation<AnySchema>): void {
1112
1482
  if (this.#state === "executed") {
1113
- throw new Error(`create() can only be called during mutation phase.`);
1483
+ throw new Error(`Cannot add mutation operation in executed state.`);
1114
1484
  }
1485
+ this.#mutationOps.push(op);
1486
+ }
1487
+
1488
+ /**
1489
+ * Get the IDs of created entities after executeMutations() has been called.
1490
+ * Returns FragnoId objects with external IDs (always available) and internal IDs
1491
+ * (available when database supports RETURNING).
1492
+ *
1493
+ * @throws Error if called before executeMutations()
1494
+ * @returns Array of FragnoIds in the same order as create() calls
1495
+ */
1496
+ getCreatedIds(): FragnoId[] {
1497
+ if (this.#state !== "executed") {
1498
+ throw new Error(
1499
+ `getCreatedIds() can only be called after executeMutations(). Current state: ${this.#state}`,
1500
+ );
1501
+ }
1502
+
1503
+ const createdIds: FragnoId[] = [];
1504
+ let createIndex = 0;
1505
+
1506
+ for (const op of this.#mutationOps) {
1507
+ if (op.type === "create") {
1508
+ const internalId = this.#createdInternalIds[createIndex] ?? undefined;
1509
+ createdIds.push(
1510
+ new FragnoId({
1511
+ externalId: op.generatedExternalId,
1512
+ internalId,
1513
+ version: 0, // New records always start at version 0
1514
+ }),
1515
+ );
1516
+ createIndex++;
1517
+ }
1518
+ }
1519
+
1520
+ return createdIds;
1521
+ }
1522
+
1523
+ /**
1524
+ * @internal
1525
+ * Compile the unit of work to executable queries for testing
1526
+ */
1527
+ compile<TOutput>(compiler: UOWCompiler<TOutput>): {
1528
+ name?: string;
1529
+ retrievalBatch: TOutput[];
1530
+ mutationBatch: CompiledMutation<TOutput>[];
1531
+ } {
1532
+ const retrievalBatch: TOutput[] = [];
1533
+ for (const op of this.#retrievalOps) {
1534
+ const compiled = compiler.compileRetrievalOperation(op);
1535
+ if (compiled !== null) {
1536
+ retrievalBatch.push(compiled);
1537
+ }
1538
+ }
1539
+
1540
+ const mutationBatch: CompiledMutation<TOutput>[] = [];
1541
+ for (const op of this.#mutationOps) {
1542
+ const compiled = compiler.compileMutationOperation(op);
1543
+ if (compiled !== null) {
1544
+ mutationBatch.push(compiled);
1545
+ }
1546
+ }
1547
+
1548
+ return {
1549
+ name: this.#name,
1550
+ retrievalBatch,
1551
+ mutationBatch,
1552
+ };
1553
+ }
1554
+ }
1555
+
1556
+ /**
1557
+ * A typed facade around a UnitOfWork that provides type-safe operations for a specific schema.
1558
+ * All operations are stored in the underlying UOW, but this facade ensures type safety and
1559
+ * filters retrieval results to only include operations added through this facade.
1560
+ */
1561
+ export class TypedUnitOfWork<
1562
+ const TSchema extends AnySchema,
1563
+ const TRetrievalResults extends unknown[] = [],
1564
+ const TRawInput = unknown,
1565
+ > implements IUnitOfWork
1566
+ {
1567
+ #schema: TSchema;
1568
+ #namespace?: string;
1569
+ #uow: UnitOfWork<TRawInput>;
1570
+ #operationIndices: number[] = [];
1571
+ #cachedRetrievalPhase?: Promise<TRetrievalResults>;
1572
+
1573
+ constructor(schema: TSchema, namespace: string | undefined, uow: UnitOfWork<TRawInput>) {
1574
+ this.#schema = schema;
1575
+ this.#namespace = namespace;
1576
+ this.#uow = uow;
1577
+ }
1578
+
1579
+ get $results(): Prettify<TRetrievalResults> {
1580
+ throw new Error("type only");
1581
+ }
1582
+
1583
+ get schema(): TSchema {
1584
+ return this.#schema;
1585
+ }
1586
+
1587
+ get name(): string | undefined {
1588
+ return this.#uow.name;
1589
+ }
1590
+
1591
+ get nonce(): string {
1592
+ return this.#uow.nonce;
1593
+ }
1594
+
1595
+ get state() {
1596
+ return this.#uow.state;
1597
+ }
1598
+
1599
+ get retrievalPhase(): Promise<TRetrievalResults> {
1600
+ // Cache the filtered promise to avoid recreating it on every access
1601
+ if (!this.#cachedRetrievalPhase) {
1602
+ this.#cachedRetrievalPhase = this.#uow.retrievalPhase.then((allResults) => {
1603
+ const allOperations = this.#uow.getRetrievalOperations();
1604
+ const filteredResults = this.#operationIndices.map((opIndex) => {
1605
+ const result = allResults[opIndex];
1606
+ const operation = allOperations[opIndex];
1607
+ // Transform array to single item for findFirst operations
1608
+ if (operation?.type === "find" && operation.withSingleResult) {
1609
+ return Array.isArray(result) ? (result[0] ?? null) : result;
1610
+ }
1611
+ return result;
1612
+ });
1613
+ return filteredResults as TRetrievalResults;
1614
+ });
1615
+ }
1616
+ return this.#cachedRetrievalPhase;
1617
+ }
1618
+
1619
+ get mutationPhase(): Promise<void> {
1620
+ return this.#uow.mutationPhase;
1621
+ }
1622
+
1623
+ getRetrievalOperations() {
1624
+ return this.#uow.getRetrievalOperations();
1625
+ }
1626
+
1627
+ getMutationOperations() {
1628
+ return this.#uow.getMutationOperations();
1629
+ }
1630
+
1631
+ getCreatedIds() {
1632
+ return this.#uow.getCreatedIds();
1633
+ }
1634
+
1635
+ async executeRetrieve(): Promise<TRetrievalResults> {
1636
+ return this.#uow.executeRetrieve() as Promise<TRetrievalResults>;
1637
+ }
1638
+
1639
+ async executeMutations(): Promise<{ success: boolean }> {
1640
+ return this.#uow.executeMutations();
1641
+ }
1642
+
1643
+ restrict(): IUnitOfWork {
1644
+ return this.#uow.restrict();
1645
+ }
1646
+
1647
+ reset(): void {
1648
+ return this.#uow.reset();
1649
+ }
1650
+
1651
+ forSchema<TOtherSchema extends AnySchema>(
1652
+ schema: TOtherSchema,
1653
+ ): TypedUnitOfWork<TOtherSchema, [], TRawInput> {
1654
+ return this.#uow.forSchema(schema);
1655
+ }
1656
+
1657
+ compile<TOutput>(compiler: UOWCompiler<TOutput>): {
1658
+ name?: string;
1659
+ retrievalBatch: TOutput[];
1660
+ mutationBatch: CompiledMutation<TOutput>[];
1661
+ } {
1662
+ return this.#uow.compile(compiler);
1663
+ }
1664
+
1665
+ find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1666
+ tableName: TTableName,
1667
+ builderFn: (
1668
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1669
+ ) => TBuilderResult,
1670
+ ): TypedUnitOfWork<
1671
+ TSchema,
1672
+ [
1673
+ ...TRetrievalResults,
1674
+ SelectResult<
1675
+ TSchema["tables"][TTableName],
1676
+ ExtractJoinOut<TBuilderResult>,
1677
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1678
+ >[],
1679
+ ],
1680
+ TRawInput
1681
+ >;
1682
+ find<TTableName extends keyof TSchema["tables"] & string>(
1683
+ tableName: TTableName,
1684
+ ): TypedUnitOfWork<
1685
+ TSchema,
1686
+ [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], {}, true>[]],
1687
+ TRawInput
1688
+ >;
1689
+ find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1690
+ tableName: TTableName,
1691
+ builderFn?: (
1692
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1693
+ ) => TBuilderResult,
1694
+ ): TypedUnitOfWork<
1695
+ TSchema,
1696
+ [
1697
+ ...TRetrievalResults,
1698
+ SelectResult<
1699
+ TSchema["tables"][TTableName],
1700
+ ExtractJoinOut<TBuilderResult>,
1701
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1702
+ >[],
1703
+ ],
1704
+ TRawInput
1705
+ > {
1706
+ const table = this.#schema.tables[tableName];
1707
+ if (!table) {
1708
+ throw new Error(`Table ${tableName} not found in schema`);
1709
+ }
1710
+
1711
+ const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1712
+ if (builderFn) {
1713
+ builderFn(builder);
1714
+ } else {
1715
+ builder.whereIndex("primary");
1716
+ }
1717
+ const { indexName, options, type } = builder.build();
1718
+
1719
+ const operationIndex = this.#uow.addRetrievalOperation({
1720
+ type,
1721
+ schema: this.#schema,
1722
+ namespace: this.#namespace,
1723
+ table: table as TSchema["tables"][TTableName],
1724
+ indexName,
1725
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1726
+ options: options as any,
1727
+ });
1728
+
1729
+ // Track which operation index belongs to this view
1730
+ this.#operationIndices.push(operationIndex);
1731
+
1732
+ // Safe: return type is correctly specified in the method signature
1733
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1734
+ return this as any;
1735
+ }
1736
+
1737
+ findFirst<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1738
+ tableName: TTableName,
1739
+ builderFn: (
1740
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1741
+ ) => TBuilderResult,
1742
+ ): TypedUnitOfWork<
1743
+ TSchema,
1744
+ [
1745
+ ...TRetrievalResults,
1746
+ SelectResult<
1747
+ TSchema["tables"][TTableName],
1748
+ ExtractJoinOut<TBuilderResult>,
1749
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1750
+ > | null,
1751
+ ],
1752
+ TRawInput
1753
+ >;
1754
+ findFirst<TTableName extends keyof TSchema["tables"] & string>(
1755
+ tableName: TTableName,
1756
+ ): TypedUnitOfWork<
1757
+ TSchema,
1758
+ [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], {}, true> | null],
1759
+ TRawInput
1760
+ >;
1761
+ findFirst<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1762
+ tableName: TTableName,
1763
+ builderFn?: (
1764
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1765
+ ) => TBuilderResult,
1766
+ ): TypedUnitOfWork<
1767
+ TSchema,
1768
+ [
1769
+ ...TRetrievalResults,
1770
+ SelectResult<
1771
+ TSchema["tables"][TTableName],
1772
+ ExtractJoinOut<TBuilderResult>,
1773
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1774
+ > | null,
1775
+ ],
1776
+ TRawInput
1777
+ > {
1778
+ const table = this.#schema.tables[tableName];
1779
+ if (!table) {
1780
+ throw new Error(`Table ${tableName} not found in schema`);
1781
+ }
1782
+
1783
+ const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1784
+ if (builderFn) {
1785
+ builderFn(builder);
1786
+ } else {
1787
+ builder.whereIndex("primary");
1788
+ }
1789
+ // Automatically set pageSize to 1 for findFirst
1790
+ builder.pageSize(1);
1791
+ const { indexName, options, type } = builder.build();
1792
+
1793
+ const operationIndex = this.#uow.addRetrievalOperation({
1794
+ type,
1795
+ schema: this.#schema,
1796
+ namespace: this.#namespace,
1797
+ table: table as TSchema["tables"][TTableName],
1798
+ indexName,
1799
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1800
+ options: options as any,
1801
+ withSingleResult: true,
1802
+ });
1803
+
1804
+ // Track which operation index belongs to this view
1805
+ this.#operationIndices.push(operationIndex);
1806
+
1807
+ // Safe: return type is correctly specified in the method signature
1808
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1809
+ return this as any;
1810
+ }
1811
+
1812
+ findWithCursor<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1813
+ tableName: TTableName,
1814
+ builderFn: (
1815
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1816
+ ) => TBuilderResult,
1817
+ ): TypedUnitOfWork<
1818
+ TSchema,
1819
+ [
1820
+ ...TRetrievalResults,
1821
+ CursorResult<
1822
+ SelectResult<
1823
+ TSchema["tables"][TTableName],
1824
+ ExtractJoinOut<TBuilderResult>,
1825
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1826
+ >
1827
+ >,
1828
+ ],
1829
+ TRawInput
1830
+ > {
1831
+ const table = this.#schema.tables[tableName];
1832
+ if (!table) {
1833
+ throw new Error(`Table ${tableName} not found in schema`);
1834
+ }
1835
+
1836
+ const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1837
+ builderFn(builder);
1838
+ const { indexName, options, type } = builder.build();
1839
+
1840
+ const operationIndex = this.#uow.addRetrievalOperation({
1841
+ type,
1842
+ schema: this.#schema,
1843
+ namespace: this.#namespace,
1844
+ table: table as TSchema["tables"][TTableName],
1845
+ indexName,
1846
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1847
+ options: options as any,
1848
+ withCursor: true,
1849
+ });
1850
+
1851
+ // Track which operation index belongs to this view
1852
+ this.#operationIndices.push(operationIndex);
1853
+
1854
+ // Safe: return type is correctly specified in the method signature
1855
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1856
+ return this as any;
1857
+ }
1858
+
1859
+ create<TableName extends keyof TSchema["tables"] & string>(
1860
+ tableName: TableName,
1861
+ values: TableToInsertValues<TSchema["tables"][TableName]>,
1862
+ ): FragnoId {
1863
+ const tableSchema = this.#schema.tables[tableName];
1864
+ if (!tableSchema) {
1865
+ throw new Error(`Table ${tableName} not found in schema`);
1866
+ }
1867
+
1868
+ const idColumn = tableSchema.getIdColumn();
1869
+ let externalId: string;
1870
+ let updatedValues = values;
1115
1871
 
1116
- const tableSchema = this.#schema.tables[table];
1117
- if (!tableSchema) {
1118
- throw new Error(`Table ${table} not found in schema`);
1119
- }
1120
-
1121
- const idColumn = tableSchema.getIdColumn();
1122
- let externalId: string;
1123
- let updatedValues = values;
1124
-
1125
1872
  // Check if ID value is provided in values
1126
1873
  const providedIdValue = (values as Record<string, unknown>)[idColumn.ormName];
1127
1874
 
@@ -1153,9 +1900,11 @@ export class UnitOfWork<
1153
1900
  } as TableToInsertValues<TSchema["tables"][TableName]>;
1154
1901
  }
1155
1902
 
1156
- this.#mutationOps.push({
1903
+ this.#uow.addMutationOperation({
1157
1904
  type: "create",
1158
- table,
1905
+ schema: this.#schema,
1906
+ namespace: this.#namespace,
1907
+ table: tableName,
1159
1908
  values: updatedValues,
1160
1909
  generatedExternalId: externalId,
1161
1910
  });
@@ -1163,180 +1912,73 @@ export class UnitOfWork<
1163
1912
  return FragnoId.fromExternal(externalId, 0);
1164
1913
  }
1165
1914
 
1166
- /**
1167
- * Add an update operation using a builder callback (mutation phase only)
1168
- */
1169
1915
  update<TableName extends keyof TSchema["tables"] & string>(
1170
- table: TableName,
1916
+ tableName: TableName,
1171
1917
  id: FragnoId | string,
1172
1918
  builderFn: (
1173
- // We omit "build" because we don't want to expose it to the user
1174
1919
  builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
1175
1920
  ) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"> | void,
1176
1921
  ): void {
1177
- if (this.#state === "executed") {
1178
- throw new Error(`update() can only be called during mutation phase.`);
1179
- }
1180
-
1181
- // Create builder, pass to callback, then extract configuration
1182
- const builder = new UpdateBuilder<TSchema["tables"][TableName]>(table, id);
1922
+ const builder = new UpdateBuilder<TSchema["tables"][TableName]>(tableName, id);
1183
1923
  builderFn(builder);
1184
1924
  const { id: opId, checkVersion, set } = builder.build();
1185
1925
 
1186
- this.#mutationOps.push({
1926
+ this.#uow.addMutationOperation({
1187
1927
  type: "update",
1188
- table,
1928
+ schema: this.#schema,
1929
+ namespace: this.#namespace,
1930
+ table: tableName,
1189
1931
  id: opId,
1190
1932
  checkVersion,
1191
1933
  set,
1192
1934
  });
1193
1935
  }
1194
1936
 
1195
- /**
1196
- * Add a delete operation using a builder callback (mutation phase only)
1197
- */
1198
1937
  delete<TableName extends keyof TSchema["tables"] & string>(
1199
- table: TableName,
1938
+ tableName: TableName,
1200
1939
  id: FragnoId | string,
1201
- builderFn?: (
1202
- // We omit "build" because we don't want to expose it to the user
1203
- builder: Omit<DeleteBuilder, "build">,
1204
- ) => Omit<DeleteBuilder, "build"> | void,
1940
+ builderFn?: (builder: Omit<DeleteBuilder, "build">) => Omit<DeleteBuilder, "build"> | void,
1205
1941
  ): void {
1206
- if (this.#state === "executed") {
1207
- throw new Error(`delete() can only be called during mutation phase.`);
1208
- }
1209
-
1210
- // Create builder, optionally pass to callback, then extract configuration
1211
- const builder = new DeleteBuilder(table, id);
1942
+ const builder = new DeleteBuilder(tableName, id);
1212
1943
  builderFn?.(builder);
1213
1944
  const { id: opId, checkVersion } = builder.build();
1214
1945
 
1215
- this.#mutationOps.push({
1946
+ this.#uow.addMutationOperation({
1216
1947
  type: "delete",
1217
- table,
1948
+ schema: this.#schema,
1949
+ namespace: this.#namespace,
1950
+ table: tableName,
1218
1951
  id: opId,
1219
1952
  checkVersion,
1220
1953
  });
1221
1954
  }
1222
1955
 
1223
1956
  /**
1224
- * Execute the mutation phase
1225
- * Returns success flag indicating if mutations completed without conflicts
1226
- */
1227
- async executeMutations(): Promise<{ success: boolean }> {
1228
- if (this.#state === "executed") {
1229
- throw new Error(`Cannot execute mutations from state ${this.#state}.`);
1230
- }
1231
-
1232
- // Compile mutation operations
1233
- const mutationBatch: CompiledMutation<unknown>[] = [];
1234
- for (const op of this.#mutationOps) {
1235
- const compiled = this.#compiler.compileMutationOperation(op);
1236
- if (compiled !== null) {
1237
- this.#config?.onQuery?.(compiled);
1238
- mutationBatch.push(compiled);
1239
- }
1240
- }
1241
-
1242
- if (this.#config?.dryRun) {
1243
- this.#state = "executed";
1244
- return {
1245
- success: true,
1246
- };
1247
- }
1248
-
1249
- // Execute mutation phase
1250
- const result = await this.#executor.executeMutationPhase(mutationBatch);
1251
- this.#state = "executed";
1252
-
1253
- if (result.success) {
1254
- this.#createdInternalIds = result.createdInternalIds;
1255
- }
1256
-
1257
- return {
1258
- success: result.success,
1259
- };
1260
- }
1261
-
1262
- /**
1263
- * Get the retrieval operations (for inspection/debugging)
1264
- */
1265
- getRetrievalOperations(): ReadonlyArray<RetrievalOperation<TSchema>> {
1266
- return this.#retrievalOps;
1267
- }
1268
-
1269
- /**
1270
- * Get the mutation operations (for inspection/debugging)
1271
- */
1272
- getMutationOperations(): ReadonlyArray<MutationOperation<TSchema>> {
1273
- return this.#mutationOps;
1274
- }
1275
-
1276
- /**
1277
- * Get the IDs of created entities after executeMutations() has been called.
1278
- * Returns FragnoId objects with external IDs (always available) and internal IDs
1279
- * (available when database supports RETURNING).
1957
+ * Check that a record's version hasn't changed since retrieval.
1958
+ * This is useful for ensuring related records remain unchanged during a transaction.
1280
1959
  *
1281
- * @throws Error if called before executeMutations()
1282
- * @returns Array of FragnoIds in the same order as create() calls
1283
- */
1284
- getCreatedIds(): FragnoId[] {
1285
- if (this.#state !== "executed") {
1286
- throw new Error(
1287
- `getCreatedIds() can only be called after executeMutations(). Current state: ${this.#state}`,
1288
- );
1289
- }
1290
-
1291
- const createdIds: FragnoId[] = [];
1292
- let createIndex = 0;
1293
-
1294
- for (const op of this.#mutationOps) {
1295
- if (op.type === "create") {
1296
- const internalId = this.#createdInternalIds[createIndex] ?? undefined;
1297
- createdIds.push(
1298
- new FragnoId({
1299
- externalId: op.generatedExternalId,
1300
- internalId,
1301
- version: 0, // New records always start at version 0
1302
- }),
1303
- );
1304
- createIndex++;
1305
- }
1306
- }
1307
-
1308
- return createdIds;
1309
- }
1310
-
1311
- /**
1312
- * @internal
1313
- * Compile the unit of work to executable queries for testing
1960
+ * @param tableName - The table name
1961
+ * @param id - The FragnoId with version information (string IDs are not allowed)
1962
+ * @throws Error if the ID is a string without version information
1963
+ *
1964
+ * @example
1965
+ * ```ts
1966
+ * // Ensure both accounts haven't changed before creating a transfer
1967
+ * uow.check("accounts", fromAccount.id);
1968
+ * uow.check("accounts", toAccount.id);
1969
+ * uow.create("transactions", { fromAccountId, toAccountId, amount });
1970
+ * ```
1314
1971
  */
1315
- compile<TOutput>(compiler: UOWCompiler<TSchema, TOutput>): {
1316
- name?: string;
1317
- retrievalBatch: TOutput[];
1318
- mutationBatch: CompiledMutation<TOutput>[];
1319
- } {
1320
- const retrievalBatch: TOutput[] = [];
1321
- for (const op of this.#retrievalOps) {
1322
- const compiled = compiler.compileRetrievalOperation(op);
1323
- if (compiled !== null) {
1324
- retrievalBatch.push(compiled);
1325
- }
1326
- }
1327
-
1328
- const mutationBatch: CompiledMutation<TOutput>[] = [];
1329
- for (const op of this.#mutationOps) {
1330
- const compiled = compiler.compileMutationOperation(op);
1331
- if (compiled !== null) {
1332
- mutationBatch.push(compiled);
1333
- }
1334
- }
1335
-
1336
- return {
1337
- name: this.#name,
1338
- retrievalBatch,
1339
- mutationBatch,
1340
- };
1972
+ check<TableName extends keyof TSchema["tables"] & string>(
1973
+ tableName: TableName,
1974
+ id: FragnoId,
1975
+ ): void {
1976
+ this.#uow.addMutationOperation({
1977
+ type: "check",
1978
+ schema: this.#schema,
1979
+ namespace: this.#namespace,
1980
+ table: tableName,
1981
+ id,
1982
+ });
1341
1983
  }
1342
1984
  }