@fragno-dev/db 0.1.13 → 0.1.14

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 (75) hide show
  1. package/.turbo/turbo-build.log +48 -41
  2. package/CHANGELOG.md +6 -0
  3. package/dist/adapters/adapters.d.ts +13 -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 +2 -0
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  8. package/dist/adapters/drizzle/drizzle-adapter.js +6 -1
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
  10. package/dist/adapters/drizzle/drizzle-query.js +6 -4
  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 +49 -36
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  16. package/dist/adapters/drizzle/drizzle-uow-decoder.js +1 -1
  17. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
  18. package/dist/adapters/drizzle/shared.d.ts +14 -1
  19. package/dist/adapters/drizzle/shared.d.ts.map +1 -0
  20. package/dist/adapters/kysely/kysely-adapter.d.ts +2 -0
  21. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  22. package/dist/adapters/kysely/kysely-adapter.js +7 -2
  23. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  24. package/dist/adapters/kysely/kysely-query.js +5 -3
  25. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  26. package/dist/adapters/kysely/kysely-shared.d.ts +11 -0
  27. package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
  28. package/dist/adapters/kysely/kysely-uow-compiler.js +38 -9
  29. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  30. package/dist/bind-services.d.ts +7 -0
  31. package/dist/bind-services.d.ts.map +1 -0
  32. package/dist/bind-services.js +14 -0
  33. package/dist/bind-services.js.map +1 -0
  34. package/dist/fragment.d.ts +131 -12
  35. package/dist/fragment.d.ts.map +1 -1
  36. package/dist/fragment.js +107 -8
  37. package/dist/fragment.js.map +1 -1
  38. package/dist/mod.d.ts +4 -2
  39. package/dist/mod.d.ts.map +1 -1
  40. package/dist/mod.js +3 -2
  41. package/dist/mod.js.map +1 -1
  42. package/dist/query/query.d.ts +2 -2
  43. package/dist/query/query.d.ts.map +1 -1
  44. package/dist/query/unit-of-work.d.ts +100 -15
  45. package/dist/query/unit-of-work.d.ts.map +1 -1
  46. package/dist/query/unit-of-work.js +214 -7
  47. package/dist/query/unit-of-work.js.map +1 -1
  48. package/package.json +3 -3
  49. package/src/adapters/adapters.ts +14 -0
  50. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +6 -1
  51. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +133 -5
  52. package/src/adapters/drizzle/drizzle-adapter.ts +16 -1
  53. package/src/adapters/drizzle/drizzle-query.ts +26 -15
  54. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +57 -57
  55. package/src/adapters/drizzle/drizzle-uow-compiler.ts +79 -39
  56. package/src/adapters/drizzle/drizzle-uow-decoder.ts +2 -5
  57. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +2 -2
  58. package/src/adapters/kysely/kysely-adapter.ts +16 -1
  59. package/src/adapters/kysely/kysely-query.ts +26 -15
  60. package/src/adapters/kysely/kysely-uow-compiler.test.ts +43 -43
  61. package/src/adapters/kysely/kysely-uow-compiler.ts +50 -14
  62. package/src/adapters/kysely/kysely-uow-joins.test.ts +30 -30
  63. package/src/bind-services.test.ts +214 -0
  64. package/src/bind-services.ts +37 -0
  65. package/src/db-fragment.test.ts +800 -0
  66. package/src/fragment.ts +557 -28
  67. package/src/mod.ts +19 -0
  68. package/src/query/query.ts +2 -2
  69. package/src/query/unit-of-work-multi-schema.test.ts +64 -0
  70. package/src/query/unit-of-work-types.test.ts +13 -0
  71. package/src/query/unit-of-work.test.ts +5 -9
  72. package/src/query/unit-of-work.ts +511 -62
  73. package/src/uow-context-integration.test.ts +102 -0
  74. package/src/uow-context.test.ts +182 -0
  75. package/src/fragment.test.ts +0 -341
@@ -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,6 +151,8 @@ 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>>;
@@ -150,6 +160,8 @@ export type RetrievalOperation<
150
160
  }
151
161
  | {
152
162
  type: "count";
163
+ schema: TSchema;
164
+ namespace?: string;
153
165
  table: TTable;
154
166
  indexName: string;
155
167
  options: Pick<FindOptions<TTable>, "where" | "useIndex">;
@@ -164,6 +176,8 @@ export type MutationOperation<
164
176
  > =
165
177
  | {
166
178
  type: "update";
179
+ schema: TSchema;
180
+ namespace?: string;
167
181
  table: TTable["name"];
168
182
  id: FragnoId | string;
169
183
  checkVersion: boolean;
@@ -171,12 +185,16 @@ export type MutationOperation<
171
185
  }
172
186
  | {
173
187
  type: "create";
188
+ schema: TSchema;
189
+ namespace?: string;
174
190
  table: TTable["name"];
175
191
  values: TableToInsertValues<TTable>;
176
192
  generatedExternalId: string;
177
193
  }
178
194
  | {
179
195
  type: "delete";
196
+ schema: TSchema;
197
+ namespace?: string;
180
198
  table: TTable["name"];
181
199
  id: FragnoId | string;
182
200
  checkVersion: boolean;
@@ -198,16 +216,16 @@ export interface CompiledMutation<TOutput> {
198
216
  /**
199
217
  * Compiler interface for Unit of Work operations
200
218
  */
201
- export interface UOWCompiler<TSchema extends AnySchema, TOutput> {
219
+ export interface UOWCompiler<TOutput> {
202
220
  /**
203
221
  * Compile a retrieval operation to the adapter's query format
204
222
  */
205
- compileRetrievalOperation(op: RetrievalOperation<TSchema>): TOutput | null;
223
+ compileRetrievalOperation(op: RetrievalOperation<AnySchema>): TOutput | null;
206
224
 
207
225
  /**
208
226
  * Compile a mutation operation to the adapter's query format
209
227
  */
210
- compileMutationOperation(op: MutationOperation<TSchema>): CompiledMutation<TOutput> | null;
228
+ compileMutationOperation(op: MutationOperation<AnySchema>): CompiledMutation<TOutput> | null;
211
229
  }
212
230
 
213
231
  export type MutationResult =
@@ -237,7 +255,7 @@ export interface UOWExecutor<TOutput, TRawResult = unknown> {
237
255
  * Transforms raw database results into application format (e.g., converting raw columns
238
256
  * into FragnoId objects with external ID, internal ID, and version).
239
257
  */
240
- export interface UOWDecoder<TSchema extends AnySchema, TRawInput = unknown> {
258
+ export interface UOWDecoder<TRawInput = unknown> {
241
259
  /**
242
260
  * Decode raw database results from the retrieval phase
243
261
  *
@@ -245,7 +263,7 @@ export interface UOWDecoder<TSchema extends AnySchema, TRawInput = unknown> {
245
263
  * @param operations - Array of retrieval operations that produced these results
246
264
  * @returns Decoded results in application format
247
265
  */
248
- (rawResults: TRawInput[], operations: RetrievalOperation<TSchema>[]): unknown[];
266
+ (rawResults: TRawInput[], operations: RetrievalOperation<AnySchema>[]): unknown[];
249
267
  }
250
268
 
251
269
  /**
@@ -843,22 +861,45 @@ export function buildJoinIndexed<TTable extends AnyTable, TJoinOut>(
843
861
  return compiled;
844
862
  }
845
863
 
864
+ /**
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.
867
+ */
868
+ export interface IUnitOfWorkBase {
869
+ // Getters (schema-agnostic)
870
+ readonly state: UOWState;
871
+ readonly name: string | undefined;
872
+ readonly retrievalPhase: Promise<unknown[]>;
873
+ readonly mutationPhase: Promise<void>;
874
+
875
+ // Execution (schema-agnostic)
876
+ executeRetrieve(): Promise<unknown[]>;
877
+ executeMutations(): Promise<{ success: boolean }>;
878
+
879
+ // Inspection (schema-agnostic)
880
+ getRetrievalOperations(): ReadonlyArray<RetrievalOperation<AnySchema>>;
881
+ getMutationOperations(): ReadonlyArray<MutationOperation<AnySchema>>;
882
+ getCreatedIds(): FragnoId[];
883
+
884
+ // Schema-specific view (for cross-schema operations)
885
+ forSchema<TOtherSchema extends AnySchema>(
886
+ schema: TOtherSchema,
887
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
888
+ ): UnitOfWorkSchemaView<TOtherSchema, [], any>;
889
+ }
890
+
846
891
  export function createUnitOfWork<
847
892
  const TSchema extends AnySchema,
848
893
  const TRetrievalResults extends unknown[] = [],
849
894
  const TRawInput = unknown,
850
895
  >(
851
896
  schema: TSchema,
852
- compiler: UOWCompiler<TSchema, unknown>,
897
+ compiler: UOWCompiler<unknown>,
853
898
  executor: UOWExecutor<unknown, TRawInput>,
854
- decoder: UOWDecoder<TSchema, TRawInput>,
899
+ decoder: UOWDecoder<TRawInput>,
855
900
  name?: string,
856
901
  ): UnitOfWork<TSchema, TRetrievalResults, TRawInput> {
857
- return new UnitOfWork(schema, compiler, executor, decoder, name) as UnitOfWork<
858
- TSchema,
859
- TRetrievalResults,
860
- TRawInput
861
- >;
902
+ return new UnitOfWork(schema, compiler, executor, decoder, name);
862
903
  }
863
904
 
864
905
  export interface UnitOfWorkConfig {
@@ -898,7 +939,8 @@ export class UnitOfWork<
898
939
  const TSchema extends AnySchema,
899
940
  const TRetrievalResults extends unknown[] = [],
900
941
  const TRawInput = unknown,
901
- > {
942
+ > implements IUnitOfWorkBase
943
+ {
902
944
  #schema: TSchema;
903
945
 
904
946
  #name?: string;
@@ -906,23 +948,32 @@ export class UnitOfWork<
906
948
 
907
949
  #state: UOWState = "building-retrieval";
908
950
 
909
- #retrievalOps: RetrievalOperation<TSchema>[] = [];
910
- #mutationOps: MutationOperation<TSchema>[] = [];
951
+ // Operations can now come from any schema
952
+ #retrievalOps: RetrievalOperation<AnySchema>[] = [];
953
+ #mutationOps: MutationOperation<AnySchema>[] = [];
911
954
 
912
- #compiler: UOWCompiler<TSchema, unknown>;
955
+ #compiler: UOWCompiler<unknown>;
913
956
  #executor: UOWExecutor<unknown, TRawInput>;
914
- #decoder: UOWDecoder<TSchema, TRawInput>;
957
+ #decoder: UOWDecoder<TRawInput>;
958
+ #schemaNamespaceMap?: WeakMap<AnySchema, string>;
915
959
 
916
960
  #retrievalResults?: TRetrievalResults;
917
961
  #createdInternalIds: (bigint | null)[] = [];
918
962
 
963
+ // Phase coordination promises
964
+ #retrievalPhaseResolve?: (value: TRetrievalResults) => void;
965
+ #mutationPhaseResolve?: () => void;
966
+ #retrievalPhasePromise: Promise<TRetrievalResults>;
967
+ #mutationPhasePromise: Promise<void>;
968
+
919
969
  constructor(
920
970
  schema: TSchema,
921
- compiler: UOWCompiler<TSchema, unknown>,
971
+ compiler: UOWCompiler<unknown>,
922
972
  executor: UOWExecutor<unknown, TRawInput>,
923
- decoder: UOWDecoder<TSchema, TRawInput>,
973
+ decoder: UOWDecoder<TRawInput>,
924
974
  name?: string,
925
975
  config?: UnitOfWorkConfig,
976
+ schemaNamespaceMap?: WeakMap<AnySchema, string>,
926
977
  ) {
927
978
  this.#schema = schema;
928
979
  this.#compiler = compiler;
@@ -930,12 +981,45 @@ export class UnitOfWork<
930
981
  this.#decoder = decoder;
931
982
  this.#name = name;
932
983
  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
+ });
933
993
  }
934
994
 
935
995
  get schema(): TSchema {
936
996
  return this.#schema;
937
997
  }
938
998
 
999
+ get $results(): Prettify<TRetrievalResults> {
1000
+ throw new Error("type only");
1001
+ }
1002
+
1003
+ /**
1004
+ * Get a schema-specific view of this UOW for type-safe operations
1005
+ * Returns a wrapper that uses a different schema for operations.
1006
+ * The namespace is automatically resolved from the schema-namespace map.
1007
+ */
1008
+ forSchema<TOtherSchema extends AnySchema>(
1009
+ schema: TOtherSchema,
1010
+ ): UnitOfWorkSchemaView<TOtherSchema, [], TRawInput> {
1011
+ // Look up namespace from map
1012
+ const resolvedNamespace = this.#schemaNamespaceMap?.get(schema);
1013
+
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
+ );
1021
+ }
1022
+
939
1023
  get state(): UOWState {
940
1024
  return this.#state;
941
1025
  }
@@ -944,28 +1028,46 @@ export class UnitOfWork<
944
1028
  return this.#name;
945
1029
  }
946
1030
 
1031
+ /**
1032
+ * Promise that resolves when the retrieval phase is executed
1033
+ * Service methods can await this to coordinate multi-phase logic
1034
+ */
1035
+ get retrievalPhase(): Promise<TRetrievalResults> {
1036
+ return this.#retrievalPhasePromise;
1037
+ }
1038
+
1039
+ /**
1040
+ * Promise that resolves when the mutation phase is executed
1041
+ * Service methods can await this to coordinate multi-phase logic
1042
+ */
1043
+ get mutationPhase(): Promise<void> {
1044
+ return this.#mutationPhasePromise;
1045
+ }
1046
+
947
1047
  /**
948
1048
  * Execute the retrieval phase and transition to mutation phase
949
1049
  * Returns all results from find operations
950
1050
  */
951
1051
  async executeRetrieve(): Promise<TRetrievalResults> {
952
- if (this.#retrievalOps.length === 0) {
953
- return [] as unknown as TRetrievalResults;
954
- }
955
-
956
1052
  if (this.#state !== "building-retrieval") {
957
1053
  throw new Error(
958
1054
  `Cannot execute retrieval from state ${this.#state}. Must be in building-retrieval state.`,
959
1055
  );
960
1056
  }
961
1057
 
962
- // Compile retrieval operations
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;
1063
+ }
1064
+
1065
+ // Compile retrieval operations using single compiler
963
1066
  const retrievalBatch: unknown[] = [];
964
1067
  for (const op of this.#retrievalOps) {
965
1068
  const compiled = this.#compiler.compileRetrievalOperation(op);
966
1069
  if (compiled !== null) {
967
1070
  this.#config?.onQuery?.(compiled);
968
-
969
1071
  retrievalBatch.push(compiled);
970
1072
  }
971
1073
  }
@@ -975,34 +1077,64 @@ export class UnitOfWork<
975
1077
  return [] as unknown as TRetrievalResults;
976
1078
  }
977
1079
 
978
- const results = this.#decoder(
979
- await this.#executor.executeRetrievalPhase(retrievalBatch),
980
- this.#retrievalOps,
981
- );
1080
+ // Execute all operations together (ideally in same transaction)
1081
+ const rawResults = await this.#executor.executeRetrievalPhase(retrievalBatch);
1082
+
1083
+ // Decode results using single decoder
1084
+ const results = this.#decoder(rawResults, this.#retrievalOps);
982
1085
 
983
1086
  // Store results and transition to mutation phase
984
1087
  this.#retrievalResults = results as TRetrievalResults;
985
1088
  this.#state = "building-mutation";
986
1089
 
1090
+ // Resolve the retrieval phase promise to unblock waiting service methods
1091
+ this.#retrievalPhaseResolve?.(this.#retrievalResults);
1092
+
987
1093
  return this.#retrievalResults;
988
1094
  }
989
1095
 
990
1096
  /**
991
1097
  * Add a find operation using a builder callback (retrieval phase only)
992
1098
  */
993
- find<
994
- TTableName extends keyof TSchema["tables"] & string,
995
- TSelect extends SelectClause<TSchema["tables"][TTableName]> = true,
996
- TJoinOut = {},
997
- >(
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>(
998
1124
  tableName: TTableName,
999
1125
  builderFn?: (
1000
- // We omit "build" because we don't want to expose it to the user
1001
1126
  builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1002
- ) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build"> | void,
1127
+ ) => TBuilderResult,
1003
1128
  ): UnitOfWork<
1004
1129
  TSchema,
1005
- [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>[]],
1130
+ [
1131
+ ...TRetrievalResults,
1132
+ SelectResult<
1133
+ TSchema["tables"][TTableName],
1134
+ ExtractJoinOut<TBuilderResult>,
1135
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1136
+ >[],
1137
+ ],
1006
1138
  TRawInput
1007
1139
  > {
1008
1140
  if (this.#state !== "building-retrieval") {
@@ -1028,6 +1160,7 @@ export class UnitOfWork<
1028
1160
 
1029
1161
  this.#retrievalOps.push({
1030
1162
  type,
1163
+ schema: this.#schema,
1031
1164
  // Safe: we know the table is part of the schema from the find() method
1032
1165
  table: table as TSchema["tables"][TTableName],
1033
1166
  indexName,
@@ -1036,31 +1169,31 @@ export class UnitOfWork<
1036
1169
  options: options as any,
1037
1170
  });
1038
1171
 
1039
- return this as unknown as UnitOfWork<
1040
- TSchema,
1041
- [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>[]],
1042
- TRawInput
1043
- >;
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;
1044
1175
  }
1045
1176
 
1046
1177
  /**
1047
1178
  * Add a find operation with cursor metadata (retrieval phase only)
1048
1179
  */
1049
- findWithCursor<
1050
- TTableName extends keyof TSchema["tables"] & string,
1051
- TSelect extends SelectClause<TSchema["tables"][TTableName]> = true,
1052
- TJoinOut = {},
1053
- >(
1180
+ findWithCursor<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1054
1181
  tableName: TTableName,
1055
1182
  builderFn: (
1056
1183
  // We omit "build" because we don't want to expose it to the user
1057
1184
  builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1058
- ) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build"> | void,
1185
+ ) => TBuilderResult,
1059
1186
  ): UnitOfWork<
1060
1187
  TSchema,
1061
1188
  [
1062
1189
  ...TRetrievalResults,
1063
- CursorResult<SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>>,
1190
+ CursorResult<
1191
+ SelectResult<
1192
+ TSchema["tables"][TTableName],
1193
+ ExtractJoinOut<TBuilderResult>,
1194
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1195
+ >
1196
+ >,
1064
1197
  ],
1065
1198
  TRawInput
1066
1199
  > {
@@ -1082,6 +1215,7 @@ export class UnitOfWork<
1082
1215
 
1083
1216
  this.#retrievalOps.push({
1084
1217
  type,
1218
+ schema: this.#schema,
1085
1219
  // Safe: we know the table is part of the schema from the findWithCursor() method
1086
1220
  table: table as TSchema["tables"][TTableName],
1087
1221
  indexName,
@@ -1091,14 +1225,9 @@ export class UnitOfWork<
1091
1225
  withCursor: true,
1092
1226
  });
1093
1227
 
1094
- return this as unknown as UnitOfWork<
1095
- TSchema,
1096
- [
1097
- ...TRetrievalResults,
1098
- CursorResult<SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>>,
1099
- ],
1100
- TRawInput
1101
- >;
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;
1102
1231
  }
1103
1232
 
1104
1233
  /**
@@ -1155,6 +1284,7 @@ export class UnitOfWork<
1155
1284
 
1156
1285
  this.#mutationOps.push({
1157
1286
  type: "create",
1287
+ schema: this.#schema,
1158
1288
  table,
1159
1289
  values: updatedValues,
1160
1290
  generatedExternalId: externalId,
@@ -1185,6 +1315,7 @@ export class UnitOfWork<
1185
1315
 
1186
1316
  this.#mutationOps.push({
1187
1317
  type: "update",
1318
+ schema: this.#schema,
1188
1319
  table,
1189
1320
  id: opId,
1190
1321
  checkVersion,
@@ -1214,6 +1345,7 @@ export class UnitOfWork<
1214
1345
 
1215
1346
  this.#mutationOps.push({
1216
1347
  type: "delete",
1348
+ schema: this.#schema,
1217
1349
  table,
1218
1350
  id: opId,
1219
1351
  checkVersion,
@@ -1229,7 +1361,7 @@ export class UnitOfWork<
1229
1361
  throw new Error(`Cannot execute mutations from state ${this.#state}.`);
1230
1362
  }
1231
1363
 
1232
- // Compile mutation operations
1364
+ // Compile mutation operations using single compiler
1233
1365
  const mutationBatch: CompiledMutation<unknown>[] = [];
1234
1366
  for (const op of this.#mutationOps) {
1235
1367
  const compiled = this.#compiler.compileMutationOperation(op);
@@ -1254,6 +1386,9 @@ export class UnitOfWork<
1254
1386
  this.#createdInternalIds = result.createdInternalIds;
1255
1387
  }
1256
1388
 
1389
+ // Resolve the mutation phase promise to unblock waiting service methods
1390
+ this.#mutationPhaseResolve?.();
1391
+
1257
1392
  return {
1258
1393
  success: result.success,
1259
1394
  };
@@ -1262,17 +1397,34 @@ export class UnitOfWork<
1262
1397
  /**
1263
1398
  * Get the retrieval operations (for inspection/debugging)
1264
1399
  */
1265
- getRetrievalOperations(): ReadonlyArray<RetrievalOperation<TSchema>> {
1400
+ getRetrievalOperations(): ReadonlyArray<RetrievalOperation<AnySchema>> {
1266
1401
  return this.#retrievalOps;
1267
1402
  }
1268
1403
 
1269
1404
  /**
1270
1405
  * Get the mutation operations (for inspection/debugging)
1271
1406
  */
1272
- getMutationOperations(): ReadonlyArray<MutationOperation<TSchema>> {
1407
+ getMutationOperations(): ReadonlyArray<MutationOperation<AnySchema>> {
1273
1408
  return this.#mutationOps;
1274
1409
  }
1275
1410
 
1411
+ /**
1412
+ * @internal
1413
+ * Add a retrieval operation (used by SchemaView)
1414
+ */
1415
+ addRetrievalOperation(op: RetrievalOperation<AnySchema>): number {
1416
+ this.#retrievalOps.push(op);
1417
+ return this.#retrievalOps.length - 1;
1418
+ }
1419
+
1420
+ /**
1421
+ * @internal
1422
+ * Add a mutation operation (used by SchemaView)
1423
+ */
1424
+ addMutationOperation(op: MutationOperation<AnySchema>): void {
1425
+ this.#mutationOps.push(op);
1426
+ }
1427
+
1276
1428
  /**
1277
1429
  * Get the IDs of created entities after executeMutations() has been called.
1278
1430
  * Returns FragnoId objects with external IDs (always available) and internal IDs
@@ -1312,7 +1464,7 @@ export class UnitOfWork<
1312
1464
  * @internal
1313
1465
  * Compile the unit of work to executable queries for testing
1314
1466
  */
1315
- compile<TOutput>(compiler: UOWCompiler<TSchema, TOutput>): {
1467
+ compile<TOutput>(compiler: UOWCompiler<TOutput>): {
1316
1468
  name?: string;
1317
1469
  retrievalBatch: TOutput[];
1318
1470
  mutationBatch: CompiledMutation<TOutput>[];
@@ -1340,3 +1492,300 @@ export class UnitOfWork<
1340
1492
  };
1341
1493
  }
1342
1494
  }
1495
+
1496
+ /**
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.
1499
+ */
1500
+ export class UnitOfWorkSchemaView<
1501
+ const TSchema extends AnySchema,
1502
+ const TRetrievalResults extends unknown[] = [],
1503
+ const TRawInput = unknown,
1504
+ > implements IUnitOfWorkBase
1505
+ {
1506
+ #schema: TSchema;
1507
+ #namespace?: string;
1508
+ #parent: UnitOfWork<AnySchema, unknown[], TRawInput>;
1509
+ #operationIndices: number[] = [];
1510
+
1511
+ constructor(
1512
+ schema: TSchema,
1513
+ namespace: string | undefined,
1514
+ parent: UnitOfWork<AnySchema, unknown[], TRawInput>,
1515
+ ) {
1516
+ this.#schema = schema;
1517
+ this.#namespace = namespace;
1518
+ this.#parent = parent;
1519
+ }
1520
+
1521
+ get $results(): Prettify<TRetrievalResults> {
1522
+ throw new Error("type only");
1523
+ }
1524
+
1525
+ get schema(): TSchema {
1526
+ return this.#schema;
1527
+ }
1528
+
1529
+ get name(): string | undefined {
1530
+ return this.#parent.name;
1531
+ }
1532
+
1533
+ get state() {
1534
+ return this.#parent.state;
1535
+ }
1536
+
1537
+ 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
+ });
1543
+ }
1544
+
1545
+ get mutationPhase(): Promise<void> {
1546
+ return this.#parent.mutationPhase;
1547
+ }
1548
+
1549
+ getRetrievalOperations() {
1550
+ return this.#parent.getRetrievalOperations();
1551
+ }
1552
+
1553
+ getMutationOperations() {
1554
+ return this.#parent.getMutationOperations();
1555
+ }
1556
+
1557
+ getCreatedIds() {
1558
+ return this.#parent.getCreatedIds();
1559
+ }
1560
+
1561
+ async executeRetrieve(): Promise<unknown[]> {
1562
+ return this.#parent.executeRetrieve();
1563
+ }
1564
+
1565
+ async executeMutations(): Promise<{ success: boolean }> {
1566
+ return this.#parent.executeMutations();
1567
+ }
1568
+
1569
+ find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1570
+ tableName: TTableName,
1571
+ builderFn: (
1572
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1573
+ ) => TBuilderResult,
1574
+ ): UnitOfWorkSchemaView<
1575
+ TSchema,
1576
+ [
1577
+ ...TRetrievalResults,
1578
+ SelectResult<
1579
+ TSchema["tables"][TTableName],
1580
+ ExtractJoinOut<TBuilderResult>,
1581
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1582
+ >[],
1583
+ ],
1584
+ TRawInput
1585
+ >;
1586
+ find<TTableName extends keyof TSchema["tables"] & string>(
1587
+ tableName: TTableName,
1588
+ ): UnitOfWorkSchemaView<
1589
+ TSchema,
1590
+ [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], {}, true>[]],
1591
+ TRawInput
1592
+ >;
1593
+ find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1594
+ tableName: TTableName,
1595
+ builderFn?: (
1596
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1597
+ ) => TBuilderResult,
1598
+ ): UnitOfWorkSchemaView<
1599
+ TSchema,
1600
+ [
1601
+ ...TRetrievalResults,
1602
+ SelectResult<
1603
+ TSchema["tables"][TTableName],
1604
+ ExtractJoinOut<TBuilderResult>,
1605
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1606
+ >[],
1607
+ ],
1608
+ TRawInput
1609
+ > {
1610
+ const table = this.#schema.tables[tableName];
1611
+ if (!table) {
1612
+ throw new Error(`Table ${tableName} not found in schema`);
1613
+ }
1614
+
1615
+ const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1616
+ if (builderFn) {
1617
+ builderFn(builder);
1618
+ } else {
1619
+ builder.whereIndex("primary");
1620
+ }
1621
+ const { indexName, options, type } = builder.build();
1622
+
1623
+ const operationIndex = this.#parent.addRetrievalOperation({
1624
+ type,
1625
+ schema: this.#schema,
1626
+ namespace: this.#namespace,
1627
+ table: table as TSchema["tables"][TTableName],
1628
+ indexName,
1629
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1630
+ options: options as any,
1631
+ });
1632
+
1633
+ // Track which operation index belongs to this view
1634
+ this.#operationIndices.push(operationIndex);
1635
+
1636
+ // Safe: return type is correctly specified in the method signature
1637
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1638
+ return this as any;
1639
+ }
1640
+
1641
+ findWithCursor<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1642
+ tableName: TTableName,
1643
+ builderFn: (
1644
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1645
+ ) => TBuilderResult,
1646
+ ): UnitOfWorkSchemaView<
1647
+ TSchema,
1648
+ [
1649
+ ...TRetrievalResults,
1650
+ CursorResult<
1651
+ SelectResult<
1652
+ TSchema["tables"][TTableName],
1653
+ ExtractJoinOut<TBuilderResult>,
1654
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1655
+ >
1656
+ >,
1657
+ ],
1658
+ TRawInput
1659
+ > {
1660
+ const table = this.#schema.tables[tableName];
1661
+ if (!table) {
1662
+ throw new Error(`Table ${tableName} not found in schema`);
1663
+ }
1664
+
1665
+ const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1666
+ builderFn(builder);
1667
+ const { indexName, options, type } = builder.build();
1668
+
1669
+ const operationIndex = this.#parent.addRetrievalOperation({
1670
+ type,
1671
+ schema: this.#schema,
1672
+ namespace: this.#namespace,
1673
+ table: table as TSchema["tables"][TTableName],
1674
+ indexName,
1675
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1676
+ options: options as any,
1677
+ withCursor: true,
1678
+ });
1679
+
1680
+ // Track which operation index belongs to this view
1681
+ this.#operationIndices.push(operationIndex);
1682
+
1683
+ // Safe: return type is correctly specified in the method signature
1684
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1685
+ return this as any;
1686
+ }
1687
+
1688
+ create<TableName extends keyof TSchema["tables"] & string>(
1689
+ tableName: TableName,
1690
+ values: TableToInsertValues<TSchema["tables"][TableName]>,
1691
+ ): FragnoId {
1692
+ const tableSchema = this.#schema.tables[tableName];
1693
+ if (!tableSchema) {
1694
+ throw new Error(`Table ${tableName} not found in schema`);
1695
+ }
1696
+
1697
+ const idColumn = tableSchema.getIdColumn();
1698
+ let externalId: string;
1699
+ let updatedValues = values;
1700
+
1701
+ // Check if ID value is provided in values
1702
+ const providedIdValue = (values as Record<string, unknown>)[idColumn.ormName];
1703
+
1704
+ if (providedIdValue !== undefined) {
1705
+ // Extract string from FragnoId or use string directly
1706
+ if (
1707
+ typeof providedIdValue === "object" &&
1708
+ providedIdValue !== null &&
1709
+ "externalId" in providedIdValue
1710
+ ) {
1711
+ externalId = (providedIdValue as FragnoId).externalId;
1712
+ } else {
1713
+ externalId = providedIdValue as string;
1714
+ }
1715
+ } else {
1716
+ // Generate using the column's default configuration
1717
+ const generated = idColumn.generateDefaultValue();
1718
+ if (generated === undefined) {
1719
+ throw new Error(
1720
+ `No ID value provided and ID column ${idColumn.ormName} has no default generator`,
1721
+ );
1722
+ }
1723
+ externalId = generated as string;
1724
+
1725
+ // Add the generated ID to values so it's used in the insert
1726
+ updatedValues = {
1727
+ ...values,
1728
+ [idColumn.ormName]: externalId,
1729
+ } as TableToInsertValues<TSchema["tables"][TableName]>;
1730
+ }
1731
+
1732
+ this.#parent.addMutationOperation({
1733
+ type: "create",
1734
+ schema: this.#schema,
1735
+ namespace: this.#namespace,
1736
+ table: tableName,
1737
+ values: updatedValues,
1738
+ generatedExternalId: externalId,
1739
+ });
1740
+
1741
+ return FragnoId.fromExternal(externalId, 0);
1742
+ }
1743
+
1744
+ update<TableName extends keyof TSchema["tables"] & string>(
1745
+ tableName: TableName,
1746
+ id: FragnoId | string,
1747
+ builderFn: (
1748
+ builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
1749
+ ) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"> | void,
1750
+ ): void {
1751
+ const builder = new UpdateBuilder<TSchema["tables"][TableName]>(tableName, id);
1752
+ builderFn(builder);
1753
+ const { id: opId, checkVersion, set } = builder.build();
1754
+
1755
+ this.#parent.addMutationOperation({
1756
+ type: "update",
1757
+ schema: this.#schema,
1758
+ namespace: this.#namespace,
1759
+ table: tableName,
1760
+ id: opId,
1761
+ checkVersion,
1762
+ set,
1763
+ });
1764
+ }
1765
+
1766
+ delete<TableName extends keyof TSchema["tables"] & string>(
1767
+ tableName: TableName,
1768
+ id: FragnoId | string,
1769
+ builderFn?: (builder: Omit<DeleteBuilder, "build">) => Omit<DeleteBuilder, "build"> | void,
1770
+ ): void {
1771
+ const builder = new DeleteBuilder(tableName, id);
1772
+ builderFn?.(builder);
1773
+ const { id: opId, checkVersion } = builder.build();
1774
+
1775
+ this.#parent.addMutationOperation({
1776
+ type: "delete",
1777
+ schema: this.#schema,
1778
+ namespace: this.#namespace,
1779
+ table: tableName,
1780
+ id: opId,
1781
+ checkVersion,
1782
+ });
1783
+ }
1784
+
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);
1790
+ }
1791
+ }