@fragno-dev/db 0.1.14 → 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 (183) hide show
  1. package/.turbo/turbo-build.log +179 -139
  2. package/CHANGELOG.md +24 -0
  3. package/dist/adapters/adapters.d.ts +15 -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 +3 -1
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  8. package/dist/adapters/drizzle/drizzle-adapter.js +9 -2
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
  10. package/dist/adapters/drizzle/drizzle-query.js +2 -2
  11. package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
  12. package/dist/adapters/drizzle/drizzle-uow-compiler.js +27 -8
  13. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  14. package/dist/adapters/drizzle/drizzle-uow-decoder.js +22 -15
  15. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
  16. package/dist/adapters/drizzle/drizzle-uow-executor.js +18 -7
  17. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -1
  18. package/dist/adapters/drizzle/generate.d.ts +4 -1
  19. package/dist/adapters/drizzle/generate.d.ts.map +1 -1
  20. package/dist/adapters/drizzle/generate.js +11 -18
  21. package/dist/adapters/drizzle/generate.js.map +1 -1
  22. package/dist/adapters/kysely/kysely-adapter.d.ts +3 -1
  23. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  24. package/dist/adapters/kysely/kysely-adapter.js +7 -1
  25. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  26. package/dist/adapters/kysely/kysely-query-builder.js +1 -1
  27. package/dist/adapters/kysely/kysely-query-compiler.js +3 -2
  28. package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -1
  29. package/dist/adapters/kysely/kysely-query.d.ts +1 -0
  30. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
  31. package/dist/adapters/kysely/kysely-query.js +25 -18
  32. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  33. package/dist/adapters/kysely/kysely-shared.d.ts +3 -0
  34. package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -1
  35. package/dist/adapters/kysely/kysely-shared.js +16 -1
  36. package/dist/adapters/kysely/kysely-shared.js.map +1 -1
  37. package/dist/adapters/kysely/kysely-uow-compiler.js +34 -11
  38. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  39. package/dist/adapters/kysely/kysely-uow-executor.js +8 -4
  40. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
  41. package/dist/adapters/kysely/migration/execute-base.js +1 -1
  42. package/dist/adapters/kysely/migration/execute-base.js.map +1 -1
  43. package/dist/db-fragment-definition-builder.d.ts +152 -0
  44. package/dist/db-fragment-definition-builder.d.ts.map +1 -0
  45. package/dist/db-fragment-definition-builder.js +137 -0
  46. package/dist/db-fragment-definition-builder.js.map +1 -0
  47. package/dist/fragments/internal-fragment.d.ts +19 -0
  48. package/dist/fragments/internal-fragment.d.ts.map +1 -0
  49. package/dist/fragments/internal-fragment.js +39 -0
  50. package/dist/fragments/internal-fragment.js.map +1 -0
  51. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  52. package/dist/migration-engine/generation-engine.js +35 -15
  53. package/dist/migration-engine/generation-engine.js.map +1 -1
  54. package/dist/mod.d.ts +8 -20
  55. package/dist/mod.d.ts.map +1 -1
  56. package/dist/mod.js +7 -35
  57. package/dist/mod.js.map +1 -1
  58. package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js +165 -0
  59. package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js.map +1 -0
  60. package/dist/packages/fragno/dist/api/bind-services.js +20 -0
  61. package/dist/packages/fragno/dist/api/bind-services.js.map +1 -0
  62. package/dist/packages/fragno/dist/api/error.js +48 -0
  63. package/dist/packages/fragno/dist/api/error.js.map +1 -0
  64. package/dist/packages/fragno/dist/api/fragment-definition-builder.js +320 -0
  65. package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -0
  66. package/dist/packages/fragno/dist/api/fragment-instantiator.js +487 -0
  67. package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -0
  68. package/dist/packages/fragno/dist/api/fragno-response.js +73 -0
  69. package/dist/packages/fragno/dist/api/fragno-response.js.map +1 -0
  70. package/dist/packages/fragno/dist/api/internal/response-stream.js +81 -0
  71. package/dist/packages/fragno/dist/api/internal/response-stream.js.map +1 -0
  72. package/dist/packages/fragno/dist/api/internal/route.js +10 -0
  73. package/dist/packages/fragno/dist/api/internal/route.js.map +1 -0
  74. package/dist/packages/fragno/dist/api/mutable-request-state.js +97 -0
  75. package/dist/packages/fragno/dist/api/mutable-request-state.js.map +1 -0
  76. package/dist/packages/fragno/dist/api/request-context-storage.js +43 -0
  77. package/dist/packages/fragno/dist/api/request-context-storage.js.map +1 -0
  78. package/dist/packages/fragno/dist/api/request-input-context.js +118 -0
  79. package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -0
  80. package/dist/packages/fragno/dist/api/request-middleware.js +83 -0
  81. package/dist/packages/fragno/dist/api/request-middleware.js.map +1 -0
  82. package/dist/packages/fragno/dist/api/request-output-context.js +119 -0
  83. package/dist/packages/fragno/dist/api/request-output-context.js.map +1 -0
  84. package/dist/packages/fragno/dist/api/route.js +17 -0
  85. package/dist/packages/fragno/dist/api/route.js.map +1 -0
  86. package/dist/packages/fragno/dist/internal/symbols.js +10 -0
  87. package/dist/packages/fragno/dist/internal/symbols.js.map +1 -0
  88. package/dist/query/cursor.d.ts +10 -2
  89. package/dist/query/cursor.d.ts.map +1 -1
  90. package/dist/query/cursor.js +11 -4
  91. package/dist/query/cursor.js.map +1 -1
  92. package/dist/query/execute-unit-of-work.d.ts +123 -0
  93. package/dist/query/execute-unit-of-work.d.ts.map +1 -0
  94. package/dist/query/execute-unit-of-work.js +184 -0
  95. package/dist/query/execute-unit-of-work.js.map +1 -0
  96. package/dist/query/query.d.ts +2 -2
  97. package/dist/query/query.d.ts.map +1 -1
  98. package/dist/query/result-transform.js +4 -2
  99. package/dist/query/result-transform.js.map +1 -1
  100. package/dist/query/retry-policy.d.ts +88 -0
  101. package/dist/query/retry-policy.d.ts.map +1 -0
  102. package/dist/query/retry-policy.js +61 -0
  103. package/dist/query/retry-policy.js.map +1 -0
  104. package/dist/query/unit-of-work.d.ts +104 -50
  105. package/dist/query/unit-of-work.d.ts.map +1 -1
  106. package/dist/query/unit-of-work.js +384 -194
  107. package/dist/query/unit-of-work.js.map +1 -1
  108. package/dist/schema/serialize.js +12 -7
  109. package/dist/schema/serialize.js.map +1 -1
  110. package/dist/with-database.d.ts +28 -0
  111. package/dist/with-database.d.ts.map +1 -0
  112. package/dist/with-database.js +34 -0
  113. package/dist/with-database.js.map +1 -0
  114. package/package.json +9 -2
  115. package/src/adapters/adapters.ts +16 -0
  116. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +80 -16
  117. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +158 -2
  118. package/src/adapters/drizzle/drizzle-adapter.test.ts +3 -51
  119. package/src/adapters/drizzle/drizzle-adapter.ts +20 -7
  120. package/src/adapters/drizzle/drizzle-query.ts +1 -2
  121. package/src/adapters/drizzle/drizzle-uow-compiler-mysql.test.ts +1442 -0
  122. package/src/adapters/drizzle/drizzle-uow-compiler-sqlite.test.ts +1414 -0
  123. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +21 -4
  124. package/src/adapters/drizzle/drizzle-uow-compiler.ts +44 -3
  125. package/src/adapters/drizzle/drizzle-uow-decoder.ts +32 -22
  126. package/src/adapters/drizzle/drizzle-uow-executor.ts +41 -8
  127. package/src/adapters/drizzle/generate.test.ts +102 -269
  128. package/src/adapters/drizzle/generate.ts +12 -30
  129. package/src/adapters/drizzle/test-utils.ts +36 -5
  130. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +64 -20
  131. package/src/adapters/kysely/kysely-adapter-sqlite.test.ts +156 -0
  132. package/src/adapters/kysely/kysely-adapter.ts +9 -1
  133. package/src/adapters/kysely/kysely-query-compiler.ts +3 -8
  134. package/src/adapters/kysely/kysely-query.ts +34 -25
  135. package/src/adapters/kysely/kysely-shared.ts +34 -0
  136. package/src/adapters/kysely/kysely-uow-compiler.test.ts +61 -73
  137. package/src/adapters/kysely/kysely-uow-compiler.ts +44 -12
  138. package/src/adapters/kysely/kysely-uow-executor.ts +26 -7
  139. package/src/adapters/kysely/kysely-uow-joins.test.ts +31 -48
  140. package/src/adapters/kysely/migration/execute-base.ts +1 -1
  141. package/src/db-fragment-definition-builder.test.ts +887 -0
  142. package/src/db-fragment-definition-builder.ts +506 -0
  143. package/src/db-fragment-instantiator.test.ts +467 -0
  144. package/src/db-fragment-integration.test.ts +408 -0
  145. package/src/fragments/internal-fragment.test.ts +160 -0
  146. package/src/fragments/internal-fragment.ts +85 -0
  147. package/src/migration-engine/generation-engine.test.ts +58 -15
  148. package/src/migration-engine/generation-engine.ts +78 -25
  149. package/src/mod.ts +25 -52
  150. package/src/query/cursor.test.ts +119 -0
  151. package/src/query/cursor.ts +17 -4
  152. package/src/query/execute-unit-of-work.test.ts +1310 -0
  153. package/src/query/execute-unit-of-work.ts +463 -0
  154. package/src/query/query.ts +2 -2
  155. package/src/query/result-transform.test.ts +129 -0
  156. package/src/query/result-transform.ts +4 -1
  157. package/src/query/retry-policy.test.ts +217 -0
  158. package/src/query/retry-policy.ts +141 -0
  159. package/src/query/unit-of-work-coordinator.test.ts +833 -0
  160. package/src/query/unit-of-work-types.test.ts +2 -2
  161. package/src/query/unit-of-work.test.ts +873 -191
  162. package/src/query/unit-of-work.ts +602 -409
  163. package/src/schema/serialize.ts +22 -11
  164. package/src/with-database.ts +140 -0
  165. package/tsdown.config.ts +1 -0
  166. package/dist/bind-services.d.ts +0 -7
  167. package/dist/bind-services.d.ts.map +0 -1
  168. package/dist/bind-services.js +0 -14
  169. package/dist/bind-services.js.map +0 -1
  170. package/dist/fragment.d.ts +0 -173
  171. package/dist/fragment.d.ts.map +0 -1
  172. package/dist/fragment.js +0 -191
  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/bind-services.test.ts +0 -214
  177. package/src/bind-services.ts +0 -37
  178. package/src/db-fragment.test.ts +0 -800
  179. package/src/fragment.ts +0 -727
  180. package/src/query/unit-of-work-multi-schema.test.ts +0 -64
  181. package/src/shared/settings-schema.ts +0 -61
  182. package/src/uow-context-integration.test.ts +0 -102
  183. package/src/uow-context.test.ts +0 -182
@@ -157,6 +157,7 @@ export type RetrievalOperation<
157
157
  indexName: string;
158
158
  options: FindOptions<TTable, SelectClause<TTable>>;
159
159
  withCursor?: boolean;
160
+ withSingleResult?: boolean;
160
161
  }
161
162
  | {
162
163
  type: "count";
@@ -198,6 +199,13 @@ export type MutationOperation<
198
199
  table: TTable["name"];
199
200
  id: FragnoId | string;
200
201
  checkVersion: boolean;
202
+ }
203
+ | {
204
+ type: "check";
205
+ schema: TSchema;
206
+ namespace?: string;
207
+ table: TTable["name"];
208
+ id: FragnoId;
201
209
  };
202
210
 
203
211
  /**
@@ -211,6 +219,12 @@ export interface CompiledMutation<TOutput> {
211
219
  * null means don't check affected rows (e.g., for create operations).
212
220
  */
213
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;
214
228
  }
215
229
 
216
230
  /**
@@ -862,13 +876,14 @@ export function buildJoinIndexed<TTable extends AnyTable, TJoinOut>(
862
876
  }
863
877
 
864
878
  /**
865
- * Base interface for Unit of Work with schema-agnostic methods only.
866
- * This allows UOW instances to be passed between services that use different schemas.
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.
867
881
  */
868
- export interface IUnitOfWorkBase {
882
+ export interface IUnitOfWork {
869
883
  // Getters (schema-agnostic)
870
884
  readonly state: UOWState;
871
885
  readonly name: string | undefined;
886
+ readonly nonce: string;
872
887
  readonly retrievalPhase: Promise<unknown[]>;
873
888
  readonly mutationPhase: Promise<void>;
874
889
 
@@ -881,30 +896,238 @@ export interface IUnitOfWorkBase {
881
896
  getMutationOperations(): ReadonlyArray<MutationOperation<AnySchema>>;
882
897
  getCreatedIds(): FragnoId[];
883
898
 
899
+ // Parent-child relationships
900
+ restrict(): IUnitOfWork;
901
+
902
+ // Reset for retry support
903
+ reset(): void;
904
+
884
905
  // Schema-specific view (for cross-schema operations)
885
906
  forSchema<TOtherSchema extends AnySchema>(
886
907
  schema: TOtherSchema,
887
908
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
888
- ): UnitOfWorkSchemaView<TOtherSchema, [], any>;
909
+ ): TypedUnitOfWork<TOtherSchema, [], any>;
889
910
  }
890
911
 
891
- export function createUnitOfWork<
892
- const TSchema extends AnySchema,
893
- const TRetrievalResults extends unknown[] = [],
894
- const TRawInput = unknown,
895
- >(
896
- schema: TSchema,
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(
897
923
  compiler: UOWCompiler<unknown>,
898
- executor: UOWExecutor<unknown, TRawInput>,
899
- decoder: UOWDecoder<TRawInput>,
924
+ executor: UOWExecutor<unknown, unknown>,
925
+ decoder: UOWDecoder<unknown>,
926
+ schemaNamespaceMap?: WeakMap<AnySchema, string>,
900
927
  name?: string,
901
- ): UnitOfWork<TSchema, TRetrievalResults, TRawInput> {
902
- return new UnitOfWork(schema, compiler, executor, decoder, name);
928
+ ): UnitOfWork {
929
+ return new UnitOfWork(compiler, executor, decoder, name, undefined, schemaNamespaceMap);
903
930
  }
904
931
 
905
932
  export interface UnitOfWorkConfig {
906
933
  dryRun?: boolean;
907
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
+ }
908
1131
  }
909
1132
 
910
1133
  /**
@@ -914,19 +1137,22 @@ export interface UnitOfWorkConfig {
914
1137
  * 1. Retrieval phase: Read operations to fetch entities with their versions
915
1138
  * 2. Mutation phase: Write operations that check versions before committing
916
1139
  *
1140
+ * This is the untyped base storage. Use TypedUnitOfWork for type-safe operations.
1141
+ *
917
1142
  * @example
918
1143
  * ```ts
919
1144
  * const uow = queryEngine.createUnitOfWork("update-user-balance");
1145
+ * const typedUow = uow.forSchema(mySchema);
920
1146
  *
921
1147
  * // Retrieval phase
922
- * uow.find("users", (b) => b.where("primary", (eb) => eb("id", "=", userId)));
1148
+ * typedUow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)));
923
1149
  *
924
1150
  * // Execute retrieval and transition to mutation phase
925
1151
  * const [users] = await uow.executeRetrieve();
926
1152
  *
927
1153
  * // Mutation phase with version check
928
1154
  * const user = users[0];
929
- * uow.update("users", user.id, (b) => b.set({ balance: newBalance }).check());
1155
+ * typedUow.update("users", user.id, (b) => b.set({ balance: newBalance }).check());
930
1156
  *
931
1157
  * // Execute mutations
932
1158
  * const { success } = await uow.executeMutations();
@@ -935,20 +1161,14 @@ export interface UnitOfWorkConfig {
935
1161
  * }
936
1162
  * ```
937
1163
  */
938
- export class UnitOfWork<
939
- const TSchema extends AnySchema,
940
- const TRetrievalResults extends unknown[] = [],
941
- const TRawInput = unknown,
942
- > implements IUnitOfWorkBase
943
- {
944
- #schema: TSchema;
945
-
1164
+ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
946
1165
  #name?: string;
947
1166
  #config?: UnitOfWorkConfig;
1167
+ #nonce: string;
948
1168
 
949
1169
  #state: UOWState = "building-retrieval";
950
1170
 
951
- // Operations can now come from any schema
1171
+ // Operations can come from any schema
952
1172
  #retrievalOps: RetrievalOperation<AnySchema>[] = [];
953
1173
  #mutationOps: MutationOperation<AnySchema>[] = [];
954
1174
 
@@ -957,17 +1177,21 @@ export class UnitOfWork<
957
1177
  #decoder: UOWDecoder<TRawInput>;
958
1178
  #schemaNamespaceMap?: WeakMap<AnySchema, string>;
959
1179
 
960
- #retrievalResults?: TRetrievalResults;
1180
+ #retrievalResults?: unknown[];
961
1181
  #createdInternalIds: (bigint | null)[] = [];
962
1182
 
963
1183
  // Phase coordination promises
964
- #retrievalPhaseResolve?: (value: TRetrievalResults) => void;
965
- #mutationPhaseResolve?: () => void;
966
- #retrievalPhasePromise: Promise<TRetrievalResults>;
967
- #mutationPhasePromise: Promise<void>;
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();
968
1193
 
969
1194
  constructor(
970
- schema: TSchema,
971
1195
  compiler: UOWCompiler<unknown>,
972
1196
  executor: UOWExecutor<unknown, TRawInput>,
973
1197
  decoder: UOWDecoder<TRawInput>,
@@ -975,381 +1199,196 @@ export class UnitOfWork<
975
1199
  config?: UnitOfWorkConfig,
976
1200
  schemaNamespaceMap?: WeakMap<AnySchema, string>,
977
1201
  ) {
978
- this.#schema = schema;
979
1202
  this.#compiler = compiler;
980
1203
  this.#executor = executor;
981
1204
  this.#decoder = decoder;
1205
+ this.#schemaNamespaceMap = schemaNamespaceMap;
982
1206
  this.#name = name;
983
1207
  this.#config = config;
984
- this.#schemaNamespaceMap = schemaNamespaceMap;
985
-
986
- // Initialize phase coordination promises
987
- this.#retrievalPhasePromise = new Promise<TRetrievalResults>((resolve) => {
988
- this.#retrievalPhaseResolve = resolve;
989
- });
990
- this.#mutationPhasePromise = new Promise<void>((resolve) => {
991
- this.#mutationPhaseResolve = resolve;
992
- });
993
- }
994
-
995
- get schema(): TSchema {
996
- return this.#schema;
997
- }
998
-
999
- get $results(): Prettify<TRetrievalResults> {
1000
- throw new Error("type only");
1208
+ this.#nonce = config?.nonce ?? crypto.randomUUID();
1001
1209
  }
1002
1210
 
1003
1211
  /**
1004
- * Get a schema-specific view of this UOW for type-safe operations
1005
- * Returns a wrapper that uses a different schema for operations.
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.
1006
1214
  * The namespace is automatically resolved from the schema-namespace map.
1007
1215
  */
1008
- forSchema<TOtherSchema extends AnySchema>(
1216
+ forSchema<TOtherSchema extends AnySchema, TRawInput>(
1009
1217
  schema: TOtherSchema,
1010
- ): UnitOfWorkSchemaView<TOtherSchema, [], TRawInput> {
1218
+ ): TypedUnitOfWork<TOtherSchema, [], TRawInput> {
1011
1219
  // Look up namespace from map
1012
1220
  const resolvedNamespace = this.#schemaNamespaceMap?.get(schema);
1013
1221
 
1014
- // Safe cast: UnitOfWorkSchemaView starts with empty result types
1015
- // As operations are added, the types will accumulate correctly
1016
- return new UnitOfWorkSchemaView(
1017
- schema,
1018
- resolvedNamespace,
1019
- this as unknown as UnitOfWork<AnySchema, unknown[], TRawInput>,
1020
- );
1222
+ return new TypedUnitOfWork(schema, resolvedNamespace, this as unknown as UnitOfWork<TRawInput>);
1021
1223
  }
1022
1224
 
1023
- get state(): UOWState {
1024
- return this.#state;
1025
- }
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);
1026
1240
 
1027
- get name(): string | undefined {
1028
- return this.#name;
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;
1029
1260
  }
1030
1261
 
1031
1262
  /**
1032
- * Promise that resolves when the retrieval phase is executed
1033
- * Service methods can await this to coordinate multi-phase logic
1263
+ * Signal that this child is ready for retrieval phase execution.
1264
+ * Only valid for restricted (child) UOWs.
1034
1265
  */
1035
- get retrievalPhase(): Promise<TRetrievalResults> {
1036
- return this.#retrievalPhasePromise;
1266
+ signalReadyForRetrieval(): void {
1267
+ this.#coordinator.signalReadyForRetrieval();
1037
1268
  }
1038
1269
 
1039
1270
  /**
1040
- * Promise that resolves when the mutation phase is executed
1041
- * Service methods can await this to coordinate multi-phase logic
1271
+ * Signal that this child is ready for mutation phase execution.
1272
+ * Only valid for restricted (child) UOWs.
1042
1273
  */
1043
- get mutationPhase(): Promise<void> {
1044
- return this.#mutationPhasePromise;
1274
+ signalReadyForMutation(): void {
1275
+ this.#coordinator.signalReadyForMutation();
1045
1276
  }
1046
1277
 
1047
1278
  /**
1048
- * Execute the retrieval phase and transition to mutation phase
1049
- * Returns all results from find operations
1279
+ * Reset the UOW to initial state for retry support.
1280
+ * Clears operations, resets state, and resets phase promises.
1050
1281
  */
1051
- async executeRetrieve(): Promise<TRetrievalResults> {
1052
- if (this.#state !== "building-retrieval") {
1053
- throw new Error(
1054
- `Cannot execute retrieval from state ${this.#state}. Must be in building-retrieval state.`,
1055
- );
1056
- }
1057
-
1058
- if (this.#retrievalOps.length === 0) {
1059
- this.#state = "building-mutation";
1060
- const emptyResults = [] as unknown as TRetrievalResults;
1061
- this.#retrievalPhaseResolve?.(emptyResults);
1062
- return emptyResults;
1282
+ reset(): void {
1283
+ if (this.#coordinator.isRestricted) {
1284
+ throw new Error("reset() cannot be called on restricted child UOWs");
1063
1285
  }
1064
1286
 
1065
- // Compile retrieval operations using single compiler
1066
- const retrievalBatch: unknown[] = [];
1067
- for (const op of this.#retrievalOps) {
1068
- const compiled = this.#compiler.compileRetrievalOperation(op);
1069
- if (compiled !== null) {
1070
- this.#config?.onQuery?.(compiled);
1071
- retrievalBatch.push(compiled);
1072
- }
1073
- }
1287
+ // Clear operations
1288
+ this.#retrievalOps = [];
1289
+ this.#mutationOps = [];
1290
+ this.#retrievalResults = undefined;
1291
+ this.#createdInternalIds = [];
1074
1292
 
1075
- if (this.#config?.dryRun) {
1076
- this.#state = "executed";
1077
- return [] as unknown as TRetrievalResults;
1078
- }
1293
+ // Reset state
1294
+ this.#state = "building-retrieval";
1295
+ this.#retrievalError = null;
1296
+ this.#mutationError = null;
1079
1297
 
1080
- // Execute all operations together (ideally in same transaction)
1081
- const rawResults = await this.#executor.executeRetrievalPhase(retrievalBatch);
1298
+ // Reset phase promises
1299
+ this.#retrievalPhaseDeferred.reset();
1300
+ this.#mutationPhaseDeferred.reset();
1082
1301
 
1083
- // Decode results using single decoder
1084
- const results = this.#decoder(rawResults, this.#retrievalOps);
1302
+ // Reset child coordination
1303
+ this.#coordinator.reset();
1304
+ }
1085
1305
 
1086
- // Store results and transition to mutation phase
1087
- this.#retrievalResults = results as TRetrievalResults;
1088
- this.#state = "building-mutation";
1306
+ get state(): UOWState {
1307
+ return this.#state;
1308
+ }
1089
1309
 
1090
- // Resolve the retrieval phase promise to unblock waiting service methods
1091
- this.#retrievalPhaseResolve?.(this.#retrievalResults);
1310
+ get name(): string | undefined {
1311
+ return this.#name;
1312
+ }
1092
1313
 
1093
- return this.#retrievalResults;
1314
+ get nonce(): string {
1315
+ return this.#nonce;
1094
1316
  }
1095
1317
 
1096
1318
  /**
1097
- * Add a find operation using a builder callback (retrieval phase only)
1319
+ * Promise that resolves when the retrieval phase is executed
1320
+ * Service methods can await this to coordinate multi-phase logic
1098
1321
  */
1099
- find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1100
- tableName: TTableName,
1101
- builderFn: (
1102
- builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1103
- ) => TBuilderResult,
1104
- ): UnitOfWork<
1105
- TSchema,
1106
- [
1107
- ...TRetrievalResults,
1108
- SelectResult<
1109
- TSchema["tables"][TTableName],
1110
- ExtractJoinOut<TBuilderResult>,
1111
- Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1112
- >[],
1113
- ],
1114
- TRawInput
1115
- >;
1116
- find<TTableName extends keyof TSchema["tables"] & string>(
1117
- tableName: TTableName,
1118
- ): UnitOfWork<
1119
- TSchema,
1120
- [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], {}, true>[]],
1121
- TRawInput
1122
- >;
1123
- find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1124
- tableName: TTableName,
1125
- builderFn?: (
1126
- builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1127
- ) => TBuilderResult,
1128
- ): UnitOfWork<
1129
- TSchema,
1130
- [
1131
- ...TRetrievalResults,
1132
- SelectResult<
1133
- TSchema["tables"][TTableName],
1134
- ExtractJoinOut<TBuilderResult>,
1135
- Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1136
- >[],
1137
- ],
1138
- TRawInput
1139
- > {
1140
- if (this.#state !== "building-retrieval") {
1141
- throw new Error(
1142
- `find() can only be called during retrieval phase. Current state: ${this.#state}`,
1143
- );
1144
- }
1145
-
1146
- const table = this.#schema.tables[tableName];
1147
- if (!table) {
1148
- throw new Error(`Table ${tableName} not found in schema`);
1149
- }
1150
-
1151
- // Create builder, pass to callback (or use default), then extract configuration
1152
- const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1153
- if (builderFn) {
1154
- builderFn(builder);
1155
- } else {
1156
- // Default to primary index with no filter
1157
- builder.whereIndex("primary");
1158
- }
1159
- const { indexName, options, type } = builder.build();
1160
-
1161
- this.#retrievalOps.push({
1162
- type,
1163
- schema: this.#schema,
1164
- // Safe: we know the table is part of the schema from the find() method
1165
- table: table as TSchema["tables"][TTableName],
1166
- indexName,
1167
- // Safe: we're storing the options for later compilation
1168
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1169
- options: options as any,
1170
- });
1171
-
1172
- // Safe: return type is correctly specified in the method signature
1173
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1174
- return this as any;
1322
+ get retrievalPhase(): Promise<unknown[]> {
1323
+ return this.#retrievalPhaseDeferred.promise;
1175
1324
  }
1176
1325
 
1177
1326
  /**
1178
- * Add a find operation with cursor metadata (retrieval phase only)
1327
+ * Promise that resolves when the mutation phase is executed
1328
+ * Service methods can await this to coordinate multi-phase logic
1179
1329
  */
1180
- findWithCursor<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1181
- tableName: TTableName,
1182
- builderFn: (
1183
- // We omit "build" because we don't want to expose it to the user
1184
- builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1185
- ) => TBuilderResult,
1186
- ): UnitOfWork<
1187
- TSchema,
1188
- [
1189
- ...TRetrievalResults,
1190
- CursorResult<
1191
- SelectResult<
1192
- TSchema["tables"][TTableName],
1193
- ExtractJoinOut<TBuilderResult>,
1194
- Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1195
- >
1196
- >,
1197
- ],
1198
- TRawInput
1199
- > {
1200
- if (this.#state !== "building-retrieval") {
1201
- throw new Error(
1202
- `findWithCursor() can only be called during retrieval phase. Current state: ${this.#state}`,
1203
- );
1204
- }
1205
-
1206
- const table = this.#schema.tables[tableName];
1207
- if (!table) {
1208
- throw new Error(`Table ${tableName} not found in schema`);
1209
- }
1210
-
1211
- // Create builder and pass to callback
1212
- const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1213
- builderFn(builder);
1214
- const { indexName, options, type } = builder.build();
1215
-
1216
- this.#retrievalOps.push({
1217
- type,
1218
- schema: this.#schema,
1219
- // Safe: we know the table is part of the schema from the findWithCursor() method
1220
- table: table as TSchema["tables"][TTableName],
1221
- indexName,
1222
- // Safe: we're storing the options for later compilation
1223
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1224
- options: options as any,
1225
- withCursor: true,
1226
- });
1227
-
1228
- // Safe: return type is correctly specified in the method signature
1229
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1230
- return this as any;
1330
+ get mutationPhase(): Promise<void> {
1331
+ return this.#mutationPhaseDeferred.promise;
1231
1332
  }
1232
1333
 
1233
1334
  /**
1234
- * Add a create operation (mutation phase only)
1235
- * Returns a FragnoId with the external ID that can be used immediately in subsequent operations
1335
+ * Execute the retrieval phase and transition to mutation phase
1336
+ * Returns all results from find operations
1236
1337
  */
1237
- create<TableName extends keyof TSchema["tables"] & string>(
1238
- table: TableName,
1239
- values: TableToInsertValues<TSchema["tables"][TableName]>,
1240
- ): FragnoId {
1241
- if (this.#state === "executed") {
1242
- throw new Error(`create() can only be called during mutation phase.`);
1338
+ async executeRetrieve(): Promise<unknown[]> {
1339
+ if (this.#coordinator.isRestricted) {
1340
+ throw new Error("executeRetrieve() cannot be called on restricted child UOWs");
1243
1341
  }
1244
1342
 
1245
- const tableSchema = this.#schema.tables[table];
1246
- if (!tableSchema) {
1247
- throw new Error(`Table ${table} not found in schema`);
1343
+ if (this.#state !== "building-retrieval") {
1344
+ throw new Error(
1345
+ `Cannot execute retrieval from state ${this.#state}. Must be in building-retrieval state.`,
1346
+ );
1248
1347
  }
1249
1348
 
1250
- const idColumn = tableSchema.getIdColumn();
1251
- let externalId: string;
1252
- let updatedValues = values;
1253
-
1254
- // Check if ID value is provided in values
1255
- const providedIdValue = (values as Record<string, unknown>)[idColumn.ormName];
1349
+ try {
1350
+ // Wait for all children to signal readiness
1351
+ await this.#coordinator.retrievalReadinessPromise;
1256
1352
 
1257
- if (providedIdValue !== undefined) {
1258
- // Extract string from FragnoId or use string directly
1259
- if (
1260
- typeof providedIdValue === "object" &&
1261
- providedIdValue !== null &&
1262
- "externalId" in providedIdValue
1263
- ) {
1264
- externalId = (providedIdValue as FragnoId).externalId;
1265
- } else {
1266
- externalId = providedIdValue as string;
1353
+ if (this.#retrievalOps.length === 0) {
1354
+ this.#state = "building-mutation";
1355
+ const emptyResults: unknown[] = [];
1356
+ this.#retrievalPhaseDeferred.resolve(emptyResults);
1357
+ return emptyResults;
1267
1358
  }
1268
- } else {
1269
- // Generate using the column's default configuration
1270
- const generated = idColumn.generateDefaultValue();
1271
- if (generated === undefined) {
1272
- throw new Error(
1273
- `No ID value provided and ID column ${idColumn.ormName} has no default generator`,
1274
- );
1275
- }
1276
- externalId = generated as string;
1277
1359
 
1278
- // Add the generated ID to values so it's used in the insert
1279
- updatedValues = {
1280
- ...values,
1281
- [idColumn.ormName]: externalId,
1282
- } as TableToInsertValues<TSchema["tables"][TableName]>;
1283
- }
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
+ }
1284
1369
 
1285
- this.#mutationOps.push({
1286
- type: "create",
1287
- schema: this.#schema,
1288
- table,
1289
- values: updatedValues,
1290
- generatedExternalId: externalId,
1291
- });
1370
+ if (this.#config?.dryRun) {
1371
+ this.#state = "executed";
1372
+ const emptyResults: unknown[] = [];
1373
+ this.#retrievalPhaseDeferred.resolve(emptyResults);
1374
+ return emptyResults;
1375
+ }
1292
1376
 
1293
- return FragnoId.fromExternal(externalId, 0);
1294
- }
1377
+ const rawResults = await this.#executor.executeRetrievalPhase(retrievalBatch);
1295
1378
 
1296
- /**
1297
- * Add an update operation using a builder callback (mutation phase only)
1298
- */
1299
- update<TableName extends keyof TSchema["tables"] & string>(
1300
- table: TableName,
1301
- id: FragnoId | string,
1302
- builderFn: (
1303
- // We omit "build" because we don't want to expose it to the user
1304
- builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
1305
- ) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"> | void,
1306
- ): void {
1307
- if (this.#state === "executed") {
1308
- throw new Error(`update() can only be called during mutation phase.`);
1309
- }
1379
+ const results = this.#decoder(rawResults, this.#retrievalOps);
1310
1380
 
1311
- // Create builder, pass to callback, then extract configuration
1312
- const builder = new UpdateBuilder<TSchema["tables"][TableName]>(table, id);
1313
- builderFn(builder);
1314
- const { id: opId, checkVersion, set } = builder.build();
1381
+ // Store results and transition to mutation phase
1382
+ this.#retrievalResults = results;
1383
+ this.#state = "building-mutation";
1315
1384
 
1316
- this.#mutationOps.push({
1317
- type: "update",
1318
- schema: this.#schema,
1319
- table,
1320
- id: opId,
1321
- checkVersion,
1322
- set,
1323
- });
1324
- }
1385
+ this.#retrievalPhaseDeferred.resolve(this.#retrievalResults);
1325
1386
 
1326
- /**
1327
- * Add a delete operation using a builder callback (mutation phase only)
1328
- */
1329
- delete<TableName extends keyof TSchema["tables"] & string>(
1330
- table: TableName,
1331
- id: FragnoId | string,
1332
- builderFn?: (
1333
- // We omit "build" because we don't want to expose it to the user
1334
- builder: Omit<DeleteBuilder, "build">,
1335
- ) => Omit<DeleteBuilder, "build"> | void,
1336
- ): void {
1337
- if (this.#state === "executed") {
1338
- throw new Error(`delete() can only be called during mutation phase.`);
1387
+ return this.#retrievalResults;
1388
+ } catch (error) {
1389
+ this.#retrievalError = error instanceof Error ? error : new Error(String(error));
1390
+ throw error;
1339
1391
  }
1340
-
1341
- // Create builder, optionally pass to callback, then extract configuration
1342
- const builder = new DeleteBuilder(table, id);
1343
- builderFn?.(builder);
1344
- const { id: opId, checkVersion } = builder.build();
1345
-
1346
- this.#mutationOps.push({
1347
- type: "delete",
1348
- schema: this.#schema,
1349
- table,
1350
- id: opId,
1351
- checkVersion,
1352
- });
1353
1392
  }
1354
1393
 
1355
1394
  /**
@@ -1357,41 +1396,54 @@ export class UnitOfWork<
1357
1396
  * Returns success flag indicating if mutations completed without conflicts
1358
1397
  */
1359
1398
  async executeMutations(): Promise<{ success: boolean }> {
1399
+ if (this.#coordinator.isRestricted) {
1400
+ throw new Error("executeMutations() cannot be called on restricted child UOWs");
1401
+ }
1402
+
1360
1403
  if (this.#state === "executed") {
1361
1404
  throw new Error(`Cannot execute mutations from state ${this.#state}.`);
1362
1405
  }
1363
1406
 
1364
- // Compile mutation operations using single compiler
1365
- const mutationBatch: CompiledMutation<unknown>[] = [];
1366
- for (const op of this.#mutationOps) {
1367
- const compiled = this.#compiler.compileMutationOperation(op);
1368
- if (compiled !== null) {
1369
- this.#config?.onQuery?.(compiled);
1370
- mutationBatch.push(compiled);
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
+ }
1371
1419
  }
1372
- }
1373
1420
 
1374
- if (this.#config?.dryRun) {
1375
- this.#state = "executed";
1376
- return {
1377
- success: true,
1378
- };
1379
- }
1421
+ if (this.#config?.dryRun) {
1422
+ this.#state = "executed";
1423
+ this.#mutationPhaseDeferred.resolve();
1424
+ return {
1425
+ success: true,
1426
+ };
1427
+ }
1380
1428
 
1381
- // Execute mutation phase
1382
- const result = await this.#executor.executeMutationPhase(mutationBatch);
1383
- this.#state = "executed";
1429
+ // Execute mutation phase
1430
+ const result = await this.#executor.executeMutationPhase(mutationBatch);
1431
+ this.#state = "executed";
1384
1432
 
1385
- if (result.success) {
1386
- this.#createdInternalIds = result.createdInternalIds;
1387
- }
1433
+ if (result.success) {
1434
+ this.#createdInternalIds = result.createdInternalIds;
1435
+ }
1388
1436
 
1389
- // Resolve the mutation phase promise to unblock waiting service methods
1390
- this.#mutationPhaseResolve?.();
1437
+ // Resolve the mutation phase promise to unblock waiting service methods
1438
+ this.#mutationPhaseDeferred.resolve();
1391
1439
 
1392
- return {
1393
- success: result.success,
1394
- };
1440
+ return {
1441
+ success: result.success,
1442
+ };
1443
+ } catch (error) {
1444
+ this.#mutationError = error instanceof Error ? error : new Error(String(error));
1445
+ throw error;
1446
+ }
1395
1447
  }
1396
1448
 
1397
1449
  /**
@@ -1410,18 +1462,26 @@ export class UnitOfWork<
1410
1462
 
1411
1463
  /**
1412
1464
  * @internal
1413
- * Add a retrieval operation (used by SchemaView)
1465
+ * Add a retrieval operation (used by TypedUnitOfWork)
1414
1466
  */
1415
1467
  addRetrievalOperation(op: RetrievalOperation<AnySchema>): number {
1468
+ if (this.#state !== "building-retrieval") {
1469
+ throw new Error(
1470
+ `Cannot add retrieval operation in state ${this.#state}. Must be in building-retrieval state.`,
1471
+ );
1472
+ }
1416
1473
  this.#retrievalOps.push(op);
1417
1474
  return this.#retrievalOps.length - 1;
1418
1475
  }
1419
1476
 
1420
1477
  /**
1421
1478
  * @internal
1422
- * Add a mutation operation (used by SchemaView)
1479
+ * Add a mutation operation (used by TypedUnitOfWork)
1423
1480
  */
1424
1481
  addMutationOperation(op: MutationOperation<AnySchema>): void {
1482
+ if (this.#state === "executed") {
1483
+ throw new Error(`Cannot add mutation operation in executed state.`);
1484
+ }
1425
1485
  this.#mutationOps.push(op);
1426
1486
  }
1427
1487
 
@@ -1494,28 +1554,26 @@ export class UnitOfWork<
1494
1554
  }
1495
1555
 
1496
1556
  /**
1497
- * A lightweight wrapper around a parent UOW that provides type-safe operations for a different schema.
1498
- * All operations are stored in the parent UOW, but this wrapper ensures the correct schema is used.
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.
1499
1560
  */
1500
- export class UnitOfWorkSchemaView<
1561
+ export class TypedUnitOfWork<
1501
1562
  const TSchema extends AnySchema,
1502
1563
  const TRetrievalResults extends unknown[] = [],
1503
1564
  const TRawInput = unknown,
1504
- > implements IUnitOfWorkBase
1565
+ > implements IUnitOfWork
1505
1566
  {
1506
1567
  #schema: TSchema;
1507
1568
  #namespace?: string;
1508
- #parent: UnitOfWork<AnySchema, unknown[], TRawInput>;
1569
+ #uow: UnitOfWork<TRawInput>;
1509
1570
  #operationIndices: number[] = [];
1571
+ #cachedRetrievalPhase?: Promise<TRetrievalResults>;
1510
1572
 
1511
- constructor(
1512
- schema: TSchema,
1513
- namespace: string | undefined,
1514
- parent: UnitOfWork<AnySchema, unknown[], TRawInput>,
1515
- ) {
1573
+ constructor(schema: TSchema, namespace: string | undefined, uow: UnitOfWork<TRawInput>) {
1516
1574
  this.#schema = schema;
1517
1575
  this.#namespace = namespace;
1518
- this.#parent = parent;
1576
+ this.#uow = uow;
1519
1577
  }
1520
1578
 
1521
1579
  get $results(): Prettify<TRetrievalResults> {
@@ -1527,43 +1585,81 @@ export class UnitOfWorkSchemaView<
1527
1585
  }
1528
1586
 
1529
1587
  get name(): string | undefined {
1530
- return this.#parent.name;
1588
+ return this.#uow.name;
1589
+ }
1590
+
1591
+ get nonce(): string {
1592
+ return this.#uow.nonce;
1531
1593
  }
1532
1594
 
1533
1595
  get state() {
1534
- return this.#parent.state;
1596
+ return this.#uow.state;
1535
1597
  }
1536
1598
 
1537
1599
  get retrievalPhase(): Promise<TRetrievalResults> {
1538
- // Filter parent's results to only include operations from this view
1539
- return this.#parent.retrievalPhase.then((allResults) => {
1540
- const filteredResults = this.#operationIndices.map((index) => allResults[index]);
1541
- return filteredResults as TRetrievalResults;
1542
- });
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;
1543
1617
  }
1544
1618
 
1545
1619
  get mutationPhase(): Promise<void> {
1546
- return this.#parent.mutationPhase;
1620
+ return this.#uow.mutationPhase;
1547
1621
  }
1548
1622
 
1549
1623
  getRetrievalOperations() {
1550
- return this.#parent.getRetrievalOperations();
1624
+ return this.#uow.getRetrievalOperations();
1551
1625
  }
1552
1626
 
1553
1627
  getMutationOperations() {
1554
- return this.#parent.getMutationOperations();
1628
+ return this.#uow.getMutationOperations();
1555
1629
  }
1556
1630
 
1557
1631
  getCreatedIds() {
1558
- return this.#parent.getCreatedIds();
1632
+ return this.#uow.getCreatedIds();
1559
1633
  }
1560
1634
 
1561
- async executeRetrieve(): Promise<unknown[]> {
1562
- return this.#parent.executeRetrieve();
1635
+ async executeRetrieve(): Promise<TRetrievalResults> {
1636
+ return this.#uow.executeRetrieve() as Promise<TRetrievalResults>;
1563
1637
  }
1564
1638
 
1565
1639
  async executeMutations(): Promise<{ success: boolean }> {
1566
- return this.#parent.executeMutations();
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);
1567
1663
  }
1568
1664
 
1569
1665
  find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
@@ -1571,7 +1667,7 @@ export class UnitOfWorkSchemaView<
1571
1667
  builderFn: (
1572
1668
  builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1573
1669
  ) => TBuilderResult,
1574
- ): UnitOfWorkSchemaView<
1670
+ ): TypedUnitOfWork<
1575
1671
  TSchema,
1576
1672
  [
1577
1673
  ...TRetrievalResults,
@@ -1585,7 +1681,7 @@ export class UnitOfWorkSchemaView<
1585
1681
  >;
1586
1682
  find<TTableName extends keyof TSchema["tables"] & string>(
1587
1683
  tableName: TTableName,
1588
- ): UnitOfWorkSchemaView<
1684
+ ): TypedUnitOfWork<
1589
1685
  TSchema,
1590
1686
  [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], {}, true>[]],
1591
1687
  TRawInput
@@ -1595,7 +1691,7 @@ export class UnitOfWorkSchemaView<
1595
1691
  builderFn?: (
1596
1692
  builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1597
1693
  ) => TBuilderResult,
1598
- ): UnitOfWorkSchemaView<
1694
+ ): TypedUnitOfWork<
1599
1695
  TSchema,
1600
1696
  [
1601
1697
  ...TRetrievalResults,
@@ -1620,7 +1716,81 @@ export class UnitOfWorkSchemaView<
1620
1716
  }
1621
1717
  const { indexName, options, type } = builder.build();
1622
1718
 
1623
- const operationIndex = this.#parent.addRetrievalOperation({
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({
1624
1794
  type,
1625
1795
  schema: this.#schema,
1626
1796
  namespace: this.#namespace,
@@ -1628,6 +1798,7 @@ export class UnitOfWorkSchemaView<
1628
1798
  indexName,
1629
1799
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1630
1800
  options: options as any,
1801
+ withSingleResult: true,
1631
1802
  });
1632
1803
 
1633
1804
  // Track which operation index belongs to this view
@@ -1643,7 +1814,7 @@ export class UnitOfWorkSchemaView<
1643
1814
  builderFn: (
1644
1815
  builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1645
1816
  ) => TBuilderResult,
1646
- ): UnitOfWorkSchemaView<
1817
+ ): TypedUnitOfWork<
1647
1818
  TSchema,
1648
1819
  [
1649
1820
  ...TRetrievalResults,
@@ -1666,7 +1837,7 @@ export class UnitOfWorkSchemaView<
1666
1837
  builderFn(builder);
1667
1838
  const { indexName, options, type } = builder.build();
1668
1839
 
1669
- const operationIndex = this.#parent.addRetrievalOperation({
1840
+ const operationIndex = this.#uow.addRetrievalOperation({
1670
1841
  type,
1671
1842
  schema: this.#schema,
1672
1843
  namespace: this.#namespace,
@@ -1729,7 +1900,7 @@ export class UnitOfWorkSchemaView<
1729
1900
  } as TableToInsertValues<TSchema["tables"][TableName]>;
1730
1901
  }
1731
1902
 
1732
- this.#parent.addMutationOperation({
1903
+ this.#uow.addMutationOperation({
1733
1904
  type: "create",
1734
1905
  schema: this.#schema,
1735
1906
  namespace: this.#namespace,
@@ -1752,7 +1923,7 @@ export class UnitOfWorkSchemaView<
1752
1923
  builderFn(builder);
1753
1924
  const { id: opId, checkVersion, set } = builder.build();
1754
1925
 
1755
- this.#parent.addMutationOperation({
1926
+ this.#uow.addMutationOperation({
1756
1927
  type: "update",
1757
1928
  schema: this.#schema,
1758
1929
  namespace: this.#namespace,
@@ -1772,7 +1943,7 @@ export class UnitOfWorkSchemaView<
1772
1943
  builderFn?.(builder);
1773
1944
  const { id: opId, checkVersion } = builder.build();
1774
1945
 
1775
- this.#parent.addMutationOperation({
1946
+ this.#uow.addMutationOperation({
1776
1947
  type: "delete",
1777
1948
  schema: this.#schema,
1778
1949
  namespace: this.#namespace,
@@ -1782,10 +1953,32 @@ export class UnitOfWorkSchemaView<
1782
1953
  });
1783
1954
  }
1784
1955
 
1785
- forSchema<TOtherSchema extends AnySchema>(
1786
- schema: TOtherSchema,
1787
- ): UnitOfWorkSchemaView<TOtherSchema, [], TRawInput> {
1788
- // Delegate to the parent's forSchema to create a new view
1789
- return this.#parent.forSchema(schema);
1956
+ /**
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.
1959
+ *
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
+ * ```
1971
+ */
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
+ });
1790
1983
  }
1791
1984
  }