@fragno-dev/db 0.2.0 → 0.2.2

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 (62) hide show
  1. package/.turbo/turbo-build.log +34 -30
  2. package/CHANGELOG.md +49 -0
  3. package/dist/adapters/generic-sql/query/where-builder.js +1 -1
  4. package/dist/db-fragment-definition-builder.d.ts +31 -39
  5. package/dist/db-fragment-definition-builder.d.ts.map +1 -1
  6. package/dist/db-fragment-definition-builder.js +20 -16
  7. package/dist/db-fragment-definition-builder.js.map +1 -1
  8. package/dist/fragments/internal-fragment.d.ts +94 -8
  9. package/dist/fragments/internal-fragment.d.ts.map +1 -1
  10. package/dist/fragments/internal-fragment.js +56 -55
  11. package/dist/fragments/internal-fragment.js.map +1 -1
  12. package/dist/hooks/hooks.d.ts +5 -3
  13. package/dist/hooks/hooks.d.ts.map +1 -1
  14. package/dist/hooks/hooks.js +38 -37
  15. package/dist/hooks/hooks.js.map +1 -1
  16. package/dist/mod.d.ts +3 -3
  17. package/dist/mod.d.ts.map +1 -1
  18. package/dist/mod.js +4 -4
  19. package/dist/mod.js.map +1 -1
  20. package/dist/query/unit-of-work/execute-unit-of-work.d.ts +367 -80
  21. package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
  22. package/dist/query/unit-of-work/execute-unit-of-work.js +448 -148
  23. package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
  24. package/dist/query/unit-of-work/unit-of-work.d.ts +35 -11
  25. package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
  26. package/dist/query/unit-of-work/unit-of-work.js +49 -19
  27. package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
  28. package/dist/query/value-decoding.js +1 -1
  29. package/dist/schema/create.d.ts +2 -3
  30. package/dist/schema/create.d.ts.map +1 -1
  31. package/dist/schema/create.js +2 -5
  32. package/dist/schema/create.js.map +1 -1
  33. package/dist/schema/generate-id.d.ts +20 -0
  34. package/dist/schema/generate-id.d.ts.map +1 -0
  35. package/dist/schema/generate-id.js +28 -0
  36. package/dist/schema/generate-id.js.map +1 -0
  37. package/dist/sql-driver/dialects/durable-object-dialect.d.ts.map +1 -1
  38. package/package.json +3 -3
  39. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +1 -0
  40. package/src/adapters/drizzle/drizzle-adapter-sqlite3.test.ts +41 -25
  41. package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +39 -25
  42. package/src/db-fragment-definition-builder.test.ts +58 -42
  43. package/src/db-fragment-definition-builder.ts +78 -88
  44. package/src/db-fragment-instantiator.test.ts +64 -88
  45. package/src/db-fragment-integration.test.ts +292 -142
  46. package/src/fragments/internal-fragment.test.ts +272 -266
  47. package/src/fragments/internal-fragment.ts +155 -122
  48. package/src/hooks/hooks.test.ts +268 -264
  49. package/src/hooks/hooks.ts +74 -63
  50. package/src/mod.ts +14 -4
  51. package/src/query/unit-of-work/execute-unit-of-work.test.ts +1582 -998
  52. package/src/query/unit-of-work/execute-unit-of-work.ts +1746 -343
  53. package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
  54. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +269 -21
  55. package/src/query/unit-of-work/unit-of-work.test.ts +64 -0
  56. package/src/query/unit-of-work/unit-of-work.ts +65 -30
  57. package/src/schema/create.ts +2 -5
  58. package/src/schema/generate-id.test.ts +57 -0
  59. package/src/schema/generate-id.ts +38 -0
  60. package/src/shared/config.ts +0 -10
  61. package/src/shared/connection-pool.ts +0 -24
  62. package/src/shared/prisma.ts +0 -45
@@ -722,54 +722,54 @@ describe("UOW Coordinator - Parent-Child Execution", () => {
722
722
  // If we got here without Node.js throwing an unhandled rejection, the test passes
723
723
  });
724
724
 
725
- it("should inherit nonce from parent to children for idempotent operations", () => {
725
+ it("should inherit idempotencyKey from parent to children for idempotent operations", () => {
726
726
  const executor = createMockExecutor();
727
727
  const parentUow = createUnitOfWork(createCompiler(), executor, createMockDecoder());
728
728
 
729
- // Parent UOW should have a nonce
730
- const parentNonce = parentUow.nonce;
731
- expect(parentNonce).toBeDefined();
732
- expect(typeof parentNonce).toBe("string");
733
- expect(parentNonce.length).toBeGreaterThan(0);
729
+ // Parent UOW should have an idempotencyKey
730
+ const parentIdempotencyKey = parentUow.idempotencyKey;
731
+ expect(parentIdempotencyKey).toBeDefined();
732
+ expect(typeof parentIdempotencyKey).toBe("string");
733
+ expect(parentIdempotencyKey.length).toBeGreaterThan(0);
734
734
 
735
735
  // Create first child
736
736
  const child1 = parentUow.restrict();
737
- expect(child1.nonce).toBe(parentNonce);
737
+ expect(child1.idempotencyKey).toBe(parentIdempotencyKey);
738
738
 
739
739
  // Create second child (sibling to child1)
740
740
  const child2 = parentUow.restrict();
741
- expect(child2.nonce).toBe(parentNonce);
741
+ expect(child2.idempotencyKey).toBe(parentIdempotencyKey);
742
742
 
743
743
  // Create nested child (child of child1)
744
744
  const grandchild = child1.restrict();
745
- expect(grandchild.nonce).toBe(parentNonce);
745
+ expect(grandchild.idempotencyKey).toBe(parentIdempotencyKey);
746
746
 
747
- // All UOWs in the hierarchy should share the same nonce
748
- expect(parentUow.nonce).toBe(child1.nonce);
749
- expect(child1.nonce).toBe(child2.nonce);
750
- expect(child2.nonce).toBe(grandchild.nonce);
747
+ // All UOWs in the hierarchy should share the same idempotencyKey
748
+ expect(parentUow.idempotencyKey).toBe(child1.idempotencyKey);
749
+ expect(child1.idempotencyKey).toBe(child2.idempotencyKey);
750
+ expect(child2.idempotencyKey).toBe(grandchild.idempotencyKey);
751
751
  });
752
752
 
753
- it("should generate different nonces for separate UOW hierarchies", () => {
753
+ it("should generate different idempotencyKeys for separate UOW hierarchies", () => {
754
754
  const executor = createMockExecutor();
755
755
 
756
756
  // Create two separate parent UOWs
757
757
  const parentUow1 = createUnitOfWork(createCompiler(), executor, createMockDecoder());
758
758
  const parentUow2 = createUnitOfWork(createCompiler(), executor, createMockDecoder());
759
759
 
760
- // They should have different nonces
761
- expect(parentUow1.nonce).not.toBe(parentUow2.nonce);
760
+ // They should have different idempotencyKeys
761
+ expect(parentUow1.idempotencyKey).not.toBe(parentUow2.idempotencyKey);
762
762
 
763
- // But children within each hierarchy should inherit their parent's nonce
763
+ // But children within each hierarchy should inherit their parent's idempotencyKey
764
764
  const child1 = parentUow1.restrict();
765
765
  const child2 = parentUow2.restrict();
766
766
 
767
- expect(child1.nonce).toBe(parentUow1.nonce);
768
- expect(child2.nonce).toBe(parentUow2.nonce);
769
- expect(child1.nonce).not.toBe(child2.nonce);
767
+ expect(child1.idempotencyKey).toBe(parentUow1.idempotencyKey);
768
+ expect(child2.idempotencyKey).toBe(parentUow2.idempotencyKey);
769
+ expect(child1.idempotencyKey).not.toBe(child2.idempotencyKey);
770
770
  });
771
771
 
772
- it.skip("should not cause unhandled rejection when service method awaits retrievalPhase and executeRetrieve fails", async () => {
772
+ it("should not cause unhandled rejection when service method awaits retrievalPhase and executeRetrieve fails", async () => {
773
773
  const testSchema = schema((s) =>
774
774
  s.addTable("settings", (t) =>
775
775
  t
@@ -828,4 +828,252 @@ describe("UOW Coordinator - Parent-Child Execution", () => {
828
828
 
829
829
  expect(await deferred.promise).toContain('relation "settings" does not exist');
830
830
  });
831
+
832
+ it("should allow child UOW to call getCreatedIds() after parent executes mutations", async () => {
833
+ const testSchema = schema((s) =>
834
+ s.addTable("products", (t) =>
835
+ t.addColumn("id", idColumn()).addColumn("name", "string").addColumn("price", "integer"),
836
+ ),
837
+ );
838
+
839
+ const executor = createMockExecutor();
840
+ const parentUow = createUnitOfWork(createCompiler(), executor, createMockDecoder());
841
+
842
+ // Service method that creates a product using a child UOW and returns the child
843
+ const createProduct = (name: string, price: number) => {
844
+ const childUow = parentUow.restrict();
845
+ const productId = childUow.forSchema(testSchema).create("products", { name, price });
846
+ // Return both the child UOW and the product ID reference
847
+ return { childUow, productId };
848
+ };
849
+
850
+ // Call service to create a product via child UOW
851
+ const { childUow, productId } = createProduct("Widget", 1999);
852
+
853
+ // Parent executes mutations
854
+ await parentUow.executeMutations();
855
+
856
+ // Child should be able to call getCreatedIds() after parent has executed
857
+ // This tests that child checks parent's state, not its own stale state
858
+ const createdIds = childUow.getCreatedIds();
859
+
860
+ expect(createdIds).toHaveLength(1);
861
+ expect(createdIds[0].externalId).toBe(productId.externalId);
862
+ });
863
+
864
+ it("should preserve internal IDs in child UOW when using two-phase pattern with mutationPhase await", async () => {
865
+ const testSchema = schema((s) =>
866
+ s.addTable("orders", (t) =>
867
+ t
868
+ .addColumn("id", idColumn())
869
+ .addColumn("customerId", "string")
870
+ .addColumn("total", "integer"),
871
+ ),
872
+ );
873
+
874
+ const executor = createMockExecutor();
875
+ const parentUow = createUnitOfWork(createCompiler(), executor, createMockDecoder());
876
+
877
+ // Service method that uses two-phase pattern (common with hooks/async operations)
878
+ // This simulates a service that creates a record and needs to return the internal ID
879
+ const createOrder = async (customerId: string, total: number) => {
880
+ const childUow = parentUow.restrict();
881
+ const typedUow = childUow.forSchema(testSchema);
882
+
883
+ const orderId = typedUow.create("orders", { customerId, total });
884
+
885
+ // Service awaits mutationPhase to coordinate with parent execution
886
+ await childUow.mutationPhase;
887
+
888
+ // After mutationPhase resolves, service should be able to get internal IDs
889
+ const createdIds = childUow.getCreatedIds();
890
+ const foundId = createdIds.find((id) => id.externalId === orderId.externalId);
891
+
892
+ return {
893
+ externalId: orderId.externalId,
894
+ internalId: foundId?.internalId,
895
+ };
896
+ };
897
+
898
+ // Handler orchestrates the service call and mutation execution
899
+ const handler = async () => {
900
+ const orderPromise = createOrder("customer-123", 9999);
901
+
902
+ // Execute mutations - this should resolve the service's mutationPhase await
903
+ await parentUow.executeMutations();
904
+
905
+ // Now the service can complete and return the result with internal ID
906
+ return await orderPromise;
907
+ };
908
+
909
+ const result = await handler();
910
+
911
+ // The key assertion: internal ID should be defined (not undefined)
912
+ // This tests that child UOW preserves shared reference to parent's createdInternalIds array
913
+ expect(result.internalId).toBeDefined();
914
+ expect(typeof result.internalId).toBe("bigint");
915
+ expect(result.internalId).toBeGreaterThan(0n);
916
+ });
917
+
918
+ it("should fail when handler executes mutations before service finishes scheduling them (anti-pattern)", async () => {
919
+ const testSchema = schema((s) =>
920
+ s.addTable("totp_secret", (t) =>
921
+ t
922
+ .addColumn("id", idColumn())
923
+ .addColumn("userId", "string")
924
+ .addColumn("secret", "string")
925
+ .addColumn("backupCodes", "string")
926
+ .createIndex("idx_totp_user", ["userId"]),
927
+ ),
928
+ );
929
+
930
+ // Create executor that returns empty results (no existing record)
931
+ const customExecutor: UOWExecutor<CompiledQuery, unknown> = {
932
+ executeRetrievalPhase: async (queries: CompiledQuery[]) => {
933
+ // Return empty array for each query (no existing records)
934
+ return queries.map(() => []);
935
+ },
936
+ executeMutationPhase: async (mutations: CompiledMutation<CompiledQuery>[]) => {
937
+ return {
938
+ success: true,
939
+ createdInternalIds: mutations.map(() => BigInt(Math.floor(Math.random() * 1000))),
940
+ };
941
+ },
942
+ };
943
+
944
+ const parentUow = createUnitOfWork(createCompiler(), customExecutor, createMockDecoder());
945
+
946
+ // Service that has async work AFTER awaiting retrievalPhase but BEFORE scheduling mutations
947
+ // This is a problematic pattern that can lead to race conditions
948
+ const enableTotp = async (userId: string) => {
949
+ const childUow = parentUow.restrict();
950
+ const typedUow = childUow
951
+ .forSchema(testSchema)
952
+ .findFirst("totp_secret", (b) =>
953
+ b.whereIndex("idx_totp_user", (eb) => eb("userId", "=", userId)),
954
+ );
955
+
956
+ // Service awaits retrieval phase
957
+ const [existing] = await typedUow.retrievalPhase;
958
+
959
+ if (existing) {
960
+ throw new Error("TOTP already enabled");
961
+ }
962
+
963
+ // Simulate async work (like hashing backup codes) that yields control
964
+ await new Promise((resolve) => setTimeout(resolve, 10));
965
+
966
+ // By the time we get here, if the handler has already called executeMutate(),
967
+ // the UOW will be in "executed" state and this will fail
968
+ typedUow.create("totp_secret", {
969
+ userId,
970
+ secret: "TESTSECRET123",
971
+ backupCodes: "[]",
972
+ });
973
+
974
+ await typedUow.mutationPhase;
975
+
976
+ return { success: true };
977
+ };
978
+
979
+ // ANTI-PATTERN: Handler executes both phases immediately without awaiting service
980
+ const badHandler = async () => {
981
+ // Call service - returns promise immediately
982
+ const resultPromise = enableTotp("user-123");
983
+
984
+ // Execute retrieval phase
985
+ await parentUow.executeRetrieve();
986
+
987
+ // Execute mutation phase BEFORE service has finished scheduling mutations
988
+ // This is the bug - service is still running async work and hasn't scheduled mutations yet
989
+ await parentUow.executeMutations();
990
+
991
+ // Now await service promise - but it's too late, UOW is already in "executed" state
992
+ return await resultPromise;
993
+ };
994
+
995
+ // This should throw "Cannot add mutation operation in executed state"
996
+ await expect(badHandler()).rejects.toThrow("Cannot add mutation operation in executed state");
997
+ });
998
+
999
+ it("should succeed when handler awaits service promise between phases (correct pattern)", async () => {
1000
+ const testSchema = schema((s) =>
1001
+ s.addTable("totp_secret", (t) =>
1002
+ t
1003
+ .addColumn("id", idColumn())
1004
+ .addColumn("userId", "string")
1005
+ .addColumn("secret", "string")
1006
+ .addColumn("backupCodes", "string")
1007
+ .createIndex("idx_totp_user", ["userId"]),
1008
+ ),
1009
+ );
1010
+
1011
+ // Create executor that returns empty results (no existing record)
1012
+ const customExecutor: UOWExecutor<CompiledQuery, unknown> = {
1013
+ executeRetrievalPhase: async (queries: CompiledQuery[]) => {
1014
+ return queries.map(() => []);
1015
+ },
1016
+ executeMutationPhase: async (mutations: CompiledMutation<CompiledQuery>[]) => {
1017
+ return {
1018
+ success: true,
1019
+ createdInternalIds: mutations.map(() => BigInt(Math.floor(Math.random() * 1000))),
1020
+ };
1021
+ },
1022
+ };
1023
+
1024
+ const parentUow = createUnitOfWork(createCompiler(), customExecutor, createMockDecoder());
1025
+
1026
+ // Same service as before - has async work between retrieval and mutation scheduling
1027
+ const enableTotp = async (userId: string) => {
1028
+ const childUow = parentUow.restrict();
1029
+ const typedUow = childUow
1030
+ .forSchema(testSchema)
1031
+ .findFirst("totp_secret", (b) =>
1032
+ b.whereIndex("idx_totp_user", (eb) => eb("userId", "=", userId)),
1033
+ );
1034
+
1035
+ const [existing] = await typedUow.retrievalPhase;
1036
+
1037
+ if (existing) {
1038
+ throw new Error("TOTP already enabled");
1039
+ }
1040
+
1041
+ // Simulate async work (like hashing backup codes)
1042
+ await new Promise((resolve) => setTimeout(resolve, 10));
1043
+
1044
+ // Schedule mutation - this will work because handler waits for service to finish
1045
+ typedUow.create("totp_secret", {
1046
+ userId,
1047
+ secret: "TESTSECRET123",
1048
+ backupCodes: "[]",
1049
+ });
1050
+
1051
+ return { success: true };
1052
+ };
1053
+
1054
+ // CORRECT PATTERN: Handler awaits service promise between phase executions
1055
+ const goodHandler = async () => {
1056
+ // Call service first - it schedules retrieval operations synchronously
1057
+ const resultPromise = enableTotp("user-123");
1058
+
1059
+ // Execute retrieval phase - service can now continue past its retrieval await
1060
+ await parentUow.executeRetrieve();
1061
+
1062
+ // Wait for service to finish - it will schedule mutations
1063
+ const result = await resultPromise;
1064
+
1065
+ // Execute mutations that service scheduled
1066
+ await parentUow.executeMutations();
1067
+
1068
+ return result;
1069
+ };
1070
+
1071
+ // This should succeed without errors
1072
+ const result = await goodHandler();
1073
+ expect(result.success).toBe(true);
1074
+
1075
+ // Verify operations were registered
1076
+ expect(parentUow.getRetrievalOperations()).toHaveLength(1);
1077
+ expect(parentUow.getMutationOperations()).toHaveLength(1);
1078
+ });
831
1079
  });
@@ -838,6 +838,70 @@ describe("DeleteBuilder with string ID", () => {
838
838
  });
839
839
  });
840
840
 
841
+ describe("generateId", () => {
842
+ const testSchema = schema((s) =>
843
+ s.addTable("users", (t) =>
844
+ t.addColumn("id", idColumn()).addColumn("email", "string").addColumn("name", "string"),
845
+ ),
846
+ );
847
+
848
+ it("should generate a new FragnoId without creating a record", () => {
849
+ const uow = createUnitOfWork(createMockCompiler(), createMockExecutor(), createMockDecoder());
850
+
851
+ const id = uow.forSchema(testSchema).generateId("users");
852
+
853
+ expect(id).toBeInstanceOf(FragnoId);
854
+ expect(id.externalId).toBeDefined();
855
+ expect(typeof id.externalId).toBe("string");
856
+ expect(id.externalId.length).toBeGreaterThan(0);
857
+ expect(id.version).toBe(0);
858
+
859
+ // No mutation operations should be added
860
+ expect(uow.getMutationOperations()).toHaveLength(0);
861
+ });
862
+
863
+ it("should generate unique IDs on each call", () => {
864
+ const uow = createUnitOfWork(createMockCompiler(), createMockExecutor(), createMockDecoder());
865
+ const typedUow = uow.forSchema(testSchema);
866
+
867
+ const id1 = typedUow.generateId("users");
868
+ const id2 = typedUow.generateId("users");
869
+
870
+ expect(id1.externalId).not.toBe(id2.externalId);
871
+ });
872
+
873
+ it("should allow using generated ID in create", async () => {
874
+ const executor = {
875
+ executeRetrievalPhase: async () => [],
876
+ executeMutationPhase: async () => ({
877
+ success: true,
878
+ createdInternalIds: [1n],
879
+ }),
880
+ };
881
+
882
+ const uow = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
883
+ const typedUow = uow.forSchema(testSchema);
884
+
885
+ const id = typedUow.generateId("users");
886
+ typedUow.create("users", { id, email: "test@example.com", name: "Test" });
887
+
888
+ await uow.executeMutations();
889
+ const createdIds = uow.getCreatedIds();
890
+
891
+ expect(createdIds).toHaveLength(1);
892
+ expect(createdIds[0].externalId).toBe(id.externalId);
893
+ });
894
+
895
+ it("should throw for non-existent table", () => {
896
+ const uow = createUnitOfWork(createMockCompiler(), createMockExecutor(), createMockDecoder());
897
+
898
+ expect(() => {
899
+ // @ts-expect-error - testing runtime error for non-existent table
900
+ uow.forSchema(testSchema).generateId("nonexistent");
901
+ }).toThrow("Table nonexistent not found in schema");
902
+ });
903
+ });
904
+
841
905
  describe("getCreatedIds", () => {
842
906
  const testSchema = schema((s) =>
843
907
  s.addTable("users", (t) =>
@@ -7,6 +7,7 @@ import type {
7
7
  Relation,
8
8
  } from "../../schema/create";
9
9
  import { FragnoId } from "../../schema/create";
10
+ import { generateId } from "../../schema/generate-id";
10
11
  import type { Condition, ConditionBuilder } from "../condition-builder";
11
12
  import type {
12
13
  SelectClause,
@@ -262,13 +263,9 @@ export type MutationResult =
262
263
  * Executor interface for Unit of Work operations
263
264
  */
264
265
  export interface UOWExecutor<TOutput, TRawResult = unknown> {
265
- /**
266
- * Execute the retrieval phase - all queries run in a single transaction for snapshot isolation
267
- */
268
266
  executeRetrievalPhase(retrievalBatch: TOutput[]): Promise<TRawResult[]>;
269
267
 
270
268
  /**
271
- * Execute the mutation phase - all queries run in a transaction with version checks
272
269
  * Returns success status indicating if mutations completed without conflicts,
273
270
  * and internal IDs for create operations (null if database doesn't support RETURNING)
274
271
  */
@@ -903,7 +900,7 @@ export interface IUnitOfWork {
903
900
  // Getters (schema-agnostic)
904
901
  readonly state: UOWState;
905
902
  readonly name: string | undefined;
906
- readonly nonce: string;
903
+ readonly idempotencyKey: string;
907
904
  readonly retrievalPhase: Promise<unknown[]>;
908
905
  readonly mutationPhase: Promise<void>;
909
906
 
@@ -917,7 +914,11 @@ export interface IUnitOfWork {
917
914
  getCreatedIds(): FragnoId[];
918
915
 
919
916
  // Parent-child relationships
920
- restrict(): IUnitOfWork;
917
+ restrict(options?: { readyFor?: "mutation" | "retrieval" | "none" }): IUnitOfWork;
918
+
919
+ // Coordination for restricted UOWs
920
+ signalReadyForRetrieval(): void;
921
+ signalReadyForMutation(): void;
921
922
 
922
923
  // Reset for retry support
923
924
  reset(): void;
@@ -963,7 +964,7 @@ export function createUnitOfWork(
963
964
  export interface UnitOfWorkConfig {
964
965
  dryRun?: boolean;
965
966
  onQuery?: (query: unknown) => void;
966
- nonce?: string;
967
+ idempotencyKey?: string;
967
968
  }
968
969
 
969
970
  /**
@@ -1195,7 +1196,7 @@ class UOWChildCoordinator<TRawInput> {
1195
1196
  export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1196
1197
  #name?: string;
1197
1198
  #config?: UnitOfWorkConfig;
1198
- #nonce: string;
1199
+ #idempotencyKey: string;
1199
1200
 
1200
1201
  #state: UOWState = "building-retrieval";
1201
1202
 
@@ -1239,7 +1240,7 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1239
1240
  this.#schemaNamespaceMap = schemaNamespaceMap ?? new WeakMap();
1240
1241
  this.#name = name;
1241
1242
  this.#config = config;
1242
- this.#nonce = config?.nonce ?? crypto.randomUUID();
1243
+ this.#idempotencyKey = config?.idempotencyKey ?? crypto.randomUUID();
1243
1244
  }
1244
1245
 
1245
1246
  /**
@@ -1275,20 +1276,25 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1275
1276
  * Create a restricted child UOW that cannot execute phases.
1276
1277
  * The child shares the same operation storage but must signal readiness
1277
1278
  * before the parent can execute each phase.
1279
+ *
1280
+ * @param options.readyFor - Controls automatic readiness signaling:
1281
+ * - "mutation" (default): Signals ready for both retrieval and mutation immediately
1282
+ * - "retrieval": Signals ready for retrieval only
1283
+ * - "none": No automatic signaling, caller must signal manually
1278
1284
  */
1279
- restrict(): UnitOfWork<TRawInput> {
1285
+ restrict(options?: { readyFor?: "mutation" | "retrieval" | "none" }): UnitOfWork<TRawInput> {
1286
+ const readyFor = options?.readyFor ?? "mutation";
1287
+
1280
1288
  const child = new UnitOfWork(
1281
1289
  this.#compiler,
1282
1290
  this.#executor,
1283
1291
  this.#decoder,
1284
1292
  this.#name,
1285
- { ...this.#config, nonce: this.#nonce },
1293
+ { ...this.#config, idempotencyKey: this.#idempotencyKey },
1286
1294
  this.#schemaNamespaceMap,
1287
1295
  );
1288
1296
  child.#coordinator.setAsRestricted(this, this.#coordinator);
1289
1297
 
1290
- // Share state with parent
1291
- child.#state = this.#state;
1292
1298
  child.#retrievalOps = this.#retrievalOps;
1293
1299
  child.#mutationOps = this.#mutationOps;
1294
1300
  child.#retrievalResults = this.#retrievalResults;
@@ -1301,10 +1307,13 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1301
1307
 
1302
1308
  this.#coordinator.addChild(child);
1303
1309
 
1304
- // For synchronous usage (the common case), immediately signal readiness
1305
- // This allows services called directly from handlers to work without explicit signaling
1306
- child.signalReadyForRetrieval();
1307
- child.signalReadyForMutation();
1310
+ // Signal readiness based on options
1311
+ if (readyFor === "mutation" || readyFor === "retrieval") {
1312
+ child.signalReadyForRetrieval();
1313
+ }
1314
+ if (readyFor === "mutation") {
1315
+ child.signalReadyForMutation();
1316
+ }
1308
1317
 
1309
1318
  return child;
1310
1319
  }
@@ -1375,15 +1384,15 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1375
1384
  }
1376
1385
 
1377
1386
  get state(): UOWState {
1378
- return this.#state;
1387
+ return this.#coordinator.parent?.state ?? this.#state;
1379
1388
  }
1380
1389
 
1381
1390
  get name(): string | undefined {
1382
1391
  return this.#name;
1383
1392
  }
1384
1393
 
1385
- get nonce(): string {
1386
- return this.#nonce;
1394
+ get idempotencyKey(): string {
1395
+ return this.#idempotencyKey;
1387
1396
  }
1388
1397
 
1389
1398
  /**
@@ -1502,7 +1511,9 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1502
1511
  this.#state = "executed";
1503
1512
 
1504
1513
  if (result.success) {
1505
- this.#createdInternalIds = result.createdInternalIds;
1514
+ // Mutate array in-place to preserve shared references with child UOWs
1515
+ this.#createdInternalIds.length = 0;
1516
+ this.#createdInternalIds.push(...result.createdInternalIds);
1506
1517
  }
1507
1518
 
1508
1519
  // Resolve the mutation phase promise to unblock waiting service methods
@@ -1536,9 +1547,9 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1536
1547
  * Add a retrieval operation (used by TypedUnitOfWork)
1537
1548
  */
1538
1549
  addRetrievalOperation(op: RetrievalOperation<AnySchema>): number {
1539
- if (this.#state !== "building-retrieval") {
1550
+ if (this.state !== "building-retrieval") {
1540
1551
  throw new Error(
1541
- `Cannot add retrieval operation in state ${this.#state}. Must be in building-retrieval state.`,
1552
+ `Cannot add retrieval operation in state ${this.state}. Must be in building-retrieval state.`,
1542
1553
  );
1543
1554
  }
1544
1555
  this.#retrievalOps.push(op);
@@ -1550,7 +1561,7 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1550
1561
  * Add a mutation operation (used by TypedUnitOfWork)
1551
1562
  */
1552
1563
  addMutationOperation(op: MutationOperation<AnySchema>): void {
1553
- if (this.#state === "executed") {
1564
+ if (this.state === "executed") {
1554
1565
  throw new Error(`Cannot add mutation operation in executed state.`);
1555
1566
  }
1556
1567
  this.#mutationOps.push(op);
@@ -1565,9 +1576,9 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1565
1576
  * @returns Array of FragnoIds in the same order as create() calls
1566
1577
  */
1567
1578
  getCreatedIds(): FragnoId[] {
1568
- if (this.#state !== "executed") {
1579
+ if (this.state !== "executed") {
1569
1580
  throw new Error(
1570
- `getCreatedIds() can only be called after executeMutations(). Current state: ${this.#state}`,
1581
+ `getCreatedIds() can only be called after executeMutations(). Current state: ${this.state}`,
1571
1582
  );
1572
1583
  }
1573
1584
 
@@ -1660,8 +1671,8 @@ export class TypedUnitOfWork<
1660
1671
  return this.#uow.name;
1661
1672
  }
1662
1673
 
1663
- get nonce(): string {
1664
- return this.#uow.nonce;
1674
+ get idempotencyKey(): string {
1675
+ return this.#uow.idempotencyKey;
1665
1676
  }
1666
1677
 
1667
1678
  get state() {
@@ -1712,8 +1723,16 @@ export class TypedUnitOfWork<
1712
1723
  return this.#uow.executeMutations();
1713
1724
  }
1714
1725
 
1715
- restrict(): IUnitOfWork {
1716
- return this.#uow.restrict();
1726
+ restrict(options?: { readyFor?: "mutation" | "retrieval" | "none" }): IUnitOfWork {
1727
+ return this.#uow.restrict(options);
1728
+ }
1729
+
1730
+ signalReadyForRetrieval(): void {
1731
+ this.#uow.signalReadyForRetrieval();
1732
+ }
1733
+
1734
+ signalReadyForMutation(): void {
1735
+ this.#uow.signalReadyForMutation();
1717
1736
  }
1718
1737
 
1719
1738
  reset(): void {
@@ -1940,6 +1959,22 @@ export class TypedUnitOfWork<
1940
1959
  return this as any;
1941
1960
  }
1942
1961
 
1962
+ /**
1963
+ * Generate a new ID for a table without creating a record.
1964
+ * This is useful when you need to reference an ID before actually creating the record,
1965
+ * or when you need to pass the ID to external services.
1966
+ *
1967
+ * @example
1968
+ * ```ts
1969
+ * const userId = uow.generateId("users");
1970
+ * // Use userId in related records or pass to external services
1971
+ * uow.create("users", { id: userId, name: "John" });
1972
+ * ```
1973
+ */
1974
+ generateId<TableName extends keyof TSchema["tables"] & string>(tableName: TableName): FragnoId {
1975
+ return generateId(this.#schema, tableName);
1976
+ }
1977
+
1943
1978
  create<TableName extends keyof TSchema["tables"] & string>(
1944
1979
  tableName: TableName,
1945
1980
  values: TableToInsertValues<TSchema["tables"][TableName]>,
@@ -1,5 +1,6 @@
1
1
  import { createId } from "../id";
2
- import { inspect } from "node:util";
2
+
3
+ export { generateId } from "./generate-id";
3
4
 
4
5
  export type AnySchema = Schema<Record<string, AnyTable>>;
5
6
 
@@ -548,10 +549,6 @@ export class FragnoId {
548
549
  valueOf(): string {
549
550
  return this.#externalId;
550
551
  }
551
-
552
- [inspect.custom](): string {
553
- return `FragnoId { externalId: ${this.#externalId}, internalId: ${this.#internalId?.toString()} }`;
554
- }
555
552
  }
556
553
 
557
554
  /**