@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
@@ -8,6 +8,7 @@ import { withDatabase } from "./with-database";
8
8
  import { schema, column, idColumn } from "./schema/create";
9
9
  import type { SimpleQueryInterface } from "./query/simple-query-interface";
10
10
  import type { DatabaseAdapter } from "./adapters/adapters";
11
+ import * as executeUnitOfWork from "./query/unit-of-work/execute-unit-of-work";
11
12
 
12
13
  // Create a test schema
13
14
  const testSchema = schema((s) => {
@@ -697,7 +698,7 @@ describe("DatabaseFragmentDefinitionBuilder", () => {
697
698
  expect(storage.uow).toBeDefined();
698
699
  });
699
700
 
700
- it("should provide DatabaseServiceContext with forSchema and DatabaseHandlerContext with executeRestrictedUnitOfWork", () => {
701
+ it("should provide DatabaseServiceContext with serviceTx and DatabaseHandlerContext with handlerTx", () => {
701
702
  const mockAdapter = createMockAdapter();
702
703
 
703
704
  const definition = withDatabase(testSchema)(defineFragment("db-frag")).build();
@@ -716,8 +717,8 @@ describe("DatabaseFragmentDefinitionBuilder", () => {
716
717
  storage: mockStorage,
717
718
  });
718
719
 
719
- expect(typeof contexts.serviceContext.uow).toBe("function");
720
- expect(typeof contexts.handlerContext.uow).toBe("function");
720
+ expect(typeof contexts.serviceContext.serviceTx).toBe("function");
721
+ expect(typeof contexts.handlerContext.handlerTx).toBe("function");
721
722
  });
722
723
  });
723
724
 
@@ -845,43 +846,58 @@ describe("DatabaseFragmentDefinitionBuilder", () => {
845
846
  });
846
847
  });
847
848
 
848
- // describe("defineService", () => {
849
- // it("getUnitOfWork should be available in defineService", () => {
850
- // const mockAdapter = createMockAdapter();
851
-
852
- // const definition = withDatabase(testSchema)(defineFragment("db-frag"))
853
- // .withDependencies(() => ({ apiKey: "key" }))
854
- // .providesBaseService(({ deps, defineService }) => {
855
- // expect(deps.apiKey).toBe("key");
856
- // expect(deps.db).toBeDefined();
857
- // expect(defineService).toBeDefined();
858
-
859
- // return defineService({
860
- // getUsers: function () {
861
- // return typeof this.getUnitOfWork;
862
- // },
863
- // });
864
- // })
865
- // .build();
866
-
867
- // expect(definition.baseServices).toBeDefined();
868
-
869
- // // Get implicit deps first
870
- // const deps = definition.dependencies!({
871
- // config: {},
872
- // options: { databaseAdapter: mockAdapter },
873
- // });
874
-
875
- // const services = definition.baseServices!({
876
- // config: {},
877
- // options: { databaseAdapter: mockAdapter },
878
- // deps,
879
- // serviceDeps: {},
880
- // privateServices: {},
881
- // defineService: (svc) => svc,
882
- // });
883
-
884
- // expect(services.getUsers()).toBe("function");
885
- // });
886
- // });
849
+ describe("serviceTx hooks propagation", () => {
850
+ it("should pass hooks to createServiceTxBuilder", () => {
851
+ const mockAdapter = createMockAdapter();
852
+
853
+ // Define hooks type
854
+ type TestHooks = {
855
+ onUserCreated: (payload: { email: string }) => void;
856
+ };
857
+
858
+ // Create a fragment with hooks
859
+ const definition = withDatabase(testSchema)(defineFragment("db-frag-with-hooks"))
860
+ .provideHooks<TestHooks>(({ defineHook }) => ({
861
+ onUserCreated: defineHook(function (payload: { email: string }) {
862
+ // Hook implementation
863
+ console.log("User created:", payload.email);
864
+ }),
865
+ }))
866
+ .build();
867
+
868
+ // Create a mock storage
869
+ const mockStorage = {
870
+ getStore: () => ({
871
+ uow: mockAdapter.createQueryEngine(testSchema, "test").createUnitOfWork(),
872
+ }),
873
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
874
+ } as any;
875
+
876
+ // Spy on createServiceTxBuilder
877
+ const createServiceTxBuilderSpy = vi.spyOn(executeUnitOfWork, "createServiceTxBuilder");
878
+
879
+ // Get the contexts which includes serviceTx
880
+ const contexts = definition.createThisContext!({
881
+ config: {},
882
+ options: { databaseAdapter: mockAdapter },
883
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
884
+ deps: {} as any,
885
+ storage: mockStorage,
886
+ });
887
+
888
+ // Call serviceTx - this should pass hooks to createServiceTxBuilder
889
+ contexts.serviceContext.serviceTx(testSchema);
890
+
891
+ // Verify createServiceTxBuilder was called with 3 arguments (schema, uow, hooks)
892
+ expect(createServiceTxBuilderSpy).toHaveBeenCalledOnce();
893
+ const callArgs = createServiceTxBuilderSpy.mock.calls[0];
894
+ expect(callArgs).toHaveLength(3);
895
+ expect(callArgs[0]).toBe(testSchema); // schema
896
+ expect(callArgs[1]).toBeDefined(); // uow
897
+ expect(callArgs[2]).toBeDefined(); // hooks - this is what we're testing
898
+ expect(callArgs[2]).toHaveProperty("onUserCreated");
899
+
900
+ createServiceTxBuilderSpy.mockRestore();
901
+ });
902
+ });
887
903
  });
@@ -2,7 +2,6 @@ import type { AnySchema } from "./schema/create";
2
2
  import type { SimpleQueryInterface } from "./query/simple-query-interface";
3
3
  import type { DatabaseAdapter } from "./adapters/adapters";
4
4
  import type { IUnitOfWork } from "./query/unit-of-work/unit-of-work";
5
- import { TypedUnitOfWork, UnitOfWork } from "./query/unit-of-work/unit-of-work";
6
5
  import type {
7
6
  RequestThisContext,
8
7
  FragnoPublicConfig,
@@ -14,9 +13,11 @@ import {
14
13
  type ServiceConstructorFn,
15
14
  } from "@fragno-dev/core";
16
15
  import {
17
- executeRestrictedUnitOfWork,
18
- type AwaitedPromisesInObject,
19
- type ExecuteRestrictedUnitOfWorkOptions,
16
+ createServiceTxBuilder,
17
+ createHandlerTxBuilder,
18
+ ServiceTxBuilder,
19
+ HandlerTxBuilder,
20
+ type ExecuteTxOptions,
20
21
  } from "./query/unit-of-work/execute-unit-of-work";
21
22
  import {
22
23
  prepareHookMutations,
@@ -64,11 +65,38 @@ export type ImplicitDatabaseDependencies<TSchema extends AnySchema> = {
64
65
  */
65
66
  export type DatabaseServiceContext<THooks extends HooksMap> = RequestThisContext & {
66
67
  /**
67
- * Get a typed, restricted Unit of Work for the given schema.
68
- * @param schema - Schema to get a typed view for
69
- * @returns TypedUnitOfWork (restricted version without execute methods)
68
+ * Create a service-level transaction builder using the fluent API.
69
+ * Returns a builder that can be chained with withServiceCalls, retrieve,
70
+ * transformRetrieve, mutate, transform, and build.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * return this.serviceTx(schema)
75
+ * .withServiceCalls(() => [otherService.getData()])
76
+ * .retrieve((uow) => uow.find("users", ...))
77
+ * .transformRetrieve(([users]) => users[0])
78
+ * .mutate(({ uow, retrieveResult, serviceIntermediateResult }) =>
79
+ * uow.create("records", { ... })
80
+ * )
81
+ * .transform(({ mutateResult, serviceResult }) => ({ id: mutateResult }))
82
+ * .build();
83
+ * ```
70
84
  */
71
- uow<TSchema extends AnySchema>(schema: TSchema): TypedUnitOfWork<TSchema, [], unknown, THooks>;
85
+ serviceTx<TSchema extends AnySchema>(
86
+ schema: TSchema,
87
+ ): ServiceTxBuilder<
88
+ TSchema,
89
+ readonly [],
90
+ [],
91
+ [],
92
+ unknown,
93
+ unknown,
94
+ false,
95
+ false,
96
+ false,
97
+ false,
98
+ THooks
99
+ >;
72
100
  };
73
101
 
74
102
  /**
@@ -76,49 +104,24 @@ export type DatabaseServiceContext<THooks extends HooksMap> = RequestThisContext
76
104
  */
77
105
  export type DatabaseHandlerContext<THooks extends HooksMap = {}> = RequestThisContext & {
78
106
  /**
79
- * Execute a Unit of Work with explicit phase control and automatic retry support.
80
- * This method provides an API where users call forSchema to create a schema-specific
81
- * UOW, then call executeRetrieve() and executeMutate() to execute the phases. The entire
82
- * callback is re-executed on optimistic concurrency conflicts, ensuring retries work properly.
83
- * Automatically provides the UOW factory from context.
84
- * Promises in the returned object are awaited 1 level deep.
85
- *
86
- * @param callback - Async function that receives a context with forSchema, executeRetrieve, executeMutate, nonce, and currentAttempt
87
- * @param options - Optional configuration for retry policy and abort signal
88
- * @returns Promise resolving to the callback's return value with promises awaited 1 level deep
89
- * @throws Error if retries are exhausted or callback throws an error
107
+ * Create a handler-level transaction builder using the fluent API.
108
+ * Returns a builder that can be chained with withServiceCalls, retrieve,
109
+ * transformRetrieve, mutate, transform, and execute.
90
110
  *
91
111
  * @example
92
112
  * ```ts
93
- * const result = await this.uow(async ({ forSchema, executeRetrieve, executeMutate, nonce, currentAttempt }) => {
94
- * const uow = forSchema(schema);
95
- * const userId = uow.create("users", { name: "John" });
96
- *
97
- * // Execute retrieval phase
98
- * await executeRetrieve();
99
- *
100
- * const profileId = uow.create("profiles", { userId });
101
- *
102
- * // Execute mutation phase
103
- * await executeMutate();
104
- *
105
- * return { userId, profileId };
106
- * });
113
+ * const result = await this.handlerTx()
114
+ * .withServiceCalls(() => [userService.getUser(id)])
115
+ * .mutate(({ forSchema, idempotencyKey, currentAttempt, serviceIntermediateResult }) => {
116
+ * return forSchema(ordersSchema).create("orders", { ... });
117
+ * })
118
+ * .transform(({ mutateResult, serviceResult }) => ({ ... }))
119
+ * .execute();
107
120
  * ```
108
121
  */
109
- uow<TResult>(
110
- callback: (context: {
111
- forSchema: <TSchema extends AnySchema, H extends HooksMap = THooks>(
112
- schema: TSchema,
113
- hooks?: H,
114
- ) => TypedUnitOfWork<TSchema, [], unknown, H>;
115
- executeRetrieve: () => Promise<void>;
116
- executeMutate: () => Promise<void>;
117
- nonce: string;
118
- currentAttempt: number;
119
- }) => Promise<TResult> | TResult,
120
- options?: Omit<ExecuteRestrictedUnitOfWorkOptions, "createUnitOfWork">,
121
- ): Promise<AwaitedPromisesInObject<TResult>>;
122
+ handlerTx(
123
+ options?: Omit<ExecuteTxOptions, "createUnitOfWork">,
124
+ ): HandlerTxBuilder<readonly [], [], [], unknown, unknown, false, false, false, false, THooks>;
122
125
  };
123
126
 
124
127
  /**
@@ -415,7 +418,7 @@ export class DatabaseFragmentDefinitionBuilder<
415
418
  * ```ts
416
419
  * .provideHooks(({ defineHook, config }) => ({
417
420
  * onSubscribe: defineHook(async function (payload: { email: string }) {
418
- * // 'this' context available (HookServiceContext with nonce)
421
+ * // 'this' context available (HookServiceContext with idempotencyKey)
419
422
  * await config.onSubscribe?.(payload.email);
420
423
  * }),
421
424
  * }))
@@ -647,39 +650,23 @@ export class DatabaseFragmentDefinitionBuilder<
647
650
  }
648
651
  : undefined;
649
652
 
650
- function forSchema<TSchema extends AnySchema>(
651
- schema: TSchema,
652
- ): TypedUnitOfWork<TSchema, [], unknown, THooks> {
653
+ // Builder API: serviceTx using createServiceTxBuilder
654
+ function serviceTx<TSchema extends AnySchema>(schema: TSchema) {
653
655
  const uow = storage.getStore()?.uow;
654
656
  if (!uow) {
655
657
  throw new Error(
656
658
  "No UnitOfWork in context. Service must be called within a route handler OR using `withUnitOfWork`.",
657
659
  );
658
660
  }
659
-
660
- return uow.restrict().forSchema<TSchema, THooks>(schema);
661
+ return createServiceTxBuilder<TSchema, THooks>(schema, uow, hooksConfig?.hooks);
661
662
  }
662
663
 
663
664
  const serviceContext: DatabaseServiceContext<THooks> = {
664
- uow: forSchema,
665
+ serviceTx,
665
666
  };
666
667
 
667
- async function uow<TResult>(
668
- callback: (context: {
669
- forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
670
- schema: S,
671
- hooks?: H,
672
- ) => TypedUnitOfWork<S, [], unknown, H>;
673
- executeRetrieve: () => Promise<void>;
674
- executeMutate: () => Promise<void>;
675
- nonce: string;
676
- currentAttempt: number;
677
- }) => Promise<TResult> | TResult,
678
- execOptions?: Omit<
679
- ExecuteRestrictedUnitOfWorkOptions,
680
- "createUnitOfWork" | "onBeforeMutate" | "onSuccess"
681
- >,
682
- ): Promise<AwaitedPromisesInObject<TResult>> {
668
+ // Builder API: handlerTx using createHandlerTxBuilder
669
+ function handlerTx(execOptions?: Omit<ExecuteTxOptions, "createUnitOfWork">) {
683
670
  const currentStorage = storage.getStore();
684
671
  if (!currentStorage) {
685
672
  throw new Error(
@@ -687,39 +674,42 @@ export class DatabaseFragmentDefinitionBuilder<
687
674
  );
688
675
  }
689
676
 
690
- const wrappedCallback = async (context: {
691
- forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
692
- schema: S,
693
- hooks?: H,
694
- ) => TypedUnitOfWork<S, [], unknown, H>;
695
- executeRetrieve: () => Promise<void>;
696
- executeMutate: () => Promise<void>;
697
- nonce: string;
698
- currentAttempt: number;
699
- }): Promise<TResult> => {
700
- return callback(context);
701
- };
702
-
703
- return executeRestrictedUnitOfWork(wrappedCallback, {
677
+ const userOnBeforeMutate = execOptions?.onBeforeMutate;
678
+ const userOnAfterMutate = execOptions?.onAfterMutate;
679
+
680
+ return createHandlerTxBuilder<THooks>({
704
681
  ...execOptions,
705
682
  createUnitOfWork: () => {
706
683
  currentStorage.uow.reset();
707
- // Register internal schema for hook mutations
708
684
  if (hooksConfig) {
709
685
  currentStorage.uow.registerSchema(
710
686
  hooksConfig.internalFragment.$internal.deps.schema,
711
687
  hooksConfig.internalFragment.$internal.deps.namespace,
712
688
  );
713
689
  }
714
- return currentStorage.uow as UnitOfWork;
690
+ return currentStorage.uow;
691
+ },
692
+ onBeforeMutate: (uow) => {
693
+ if (hooksConfig) {
694
+ prepareHookMutations(uow, hooksConfig);
695
+ }
696
+ if (userOnBeforeMutate) {
697
+ userOnBeforeMutate(uow);
698
+ }
699
+ },
700
+ onAfterMutate: async (uow) => {
701
+ if (hooksConfig) {
702
+ await processHooks(hooksConfig);
703
+ }
704
+ if (userOnAfterMutate) {
705
+ await userOnAfterMutate(uow);
706
+ }
715
707
  },
716
- onBeforeMutate: hooksConfig ? (uow) => prepareHookMutations(uow, hooksConfig) : undefined,
717
- onSuccess: hooksConfig ? () => processHooks(hooksConfig) : undefined,
718
708
  });
719
709
  }
720
710
 
721
711
  const handlerContext: DatabaseHandlerContext<THooks> = {
722
- uow,
712
+ handlerTx,
723
713
  };
724
714
 
725
715
  return { serviceContext, handlerContext };
@@ -29,11 +29,19 @@ function createMockAdapter(): DatabaseAdapter {
29
29
  findMany: vi.fn(),
30
30
  })),
31
31
  restrict: vi.fn(() => createMockRestrictedUow()),
32
+ signalReadyForRetrieval: vi.fn(),
33
+ signalReadyForMutation: vi.fn(),
34
+ retrievalPhase: Promise.resolve([]),
35
+ mutationPhase: Promise.resolve(),
32
36
  })),
33
37
  restrict: vi.fn(() => createMockRestrictedUow()),
34
38
  table: vi.fn(() => ({
35
39
  findMany: vi.fn(),
36
40
  })),
41
+ signalReadyForRetrieval: vi.fn(),
42
+ signalReadyForMutation: vi.fn(),
43
+ retrievalPhase: Promise.resolve([]),
44
+ mutationPhase: Promise.resolve(),
37
45
  });
38
46
 
39
47
  return {
@@ -43,16 +51,22 @@ function createMockAdapter(): DatabaseAdapter {
43
51
  findMany: vi.fn(),
44
52
  })),
45
53
  restrict: vi.fn(() => createMockRestrictedUow()),
54
+ signalReadyForRetrieval: vi.fn(),
55
+ signalReadyForMutation: vi.fn(),
56
+ retrievalPhase: Promise.resolve([]),
57
+ mutationPhase: Promise.resolve(),
46
58
  })),
47
59
  restrict: vi.fn(() => createMockRestrictedUow()),
48
- executeRetrieve: vi.fn(),
49
- executeMutations: vi.fn(),
60
+ executeRetrieve: vi.fn(async () => {}),
61
+ executeMutations: vi.fn(async () => ({ success: true })),
50
62
  commit: vi.fn(),
51
63
  rollback: vi.fn(),
52
64
  reset: vi.fn(),
53
65
  table: vi.fn(() => ({
54
66
  findMany: vi.fn(),
55
67
  })),
68
+ idempotencyKey: "test-nonce",
69
+ state: "building-retrieval",
56
70
  };
57
71
  }),
58
72
  type: "mock",
@@ -69,7 +83,7 @@ function createMockAdapter(): DatabaseAdapter {
69
83
 
70
84
  describe("db-fragment-instantiator", () => {
71
85
  describe("Unit of Work in request context", () => {
72
- it("should provide executeRestrictedUnitOfWork on this context in route handlers", async () => {
86
+ it("should provide handlerTx on this context in route handlers", async () => {
73
87
  const definition = defineFragment("test-db-fragment")
74
88
  .extend(withDatabase(testSchema))
75
89
  .build();
@@ -79,10 +93,10 @@ describe("db-fragment-instantiator", () => {
79
93
  method: "GET",
80
94
  path: "/test",
81
95
  handler: async function (_input, { json }) {
82
- // Access executeRestrictedUnitOfWork from this context
83
- expect(this.uow).toBeDefined();
96
+ // Access handlerTx from this context
97
+ expect(this.handlerTx).toBeDefined();
84
98
 
85
- return json({ hasExecuteMethod: !!this.uow });
99
+ return json({ hasHandlerTxBuilder: !!this.handlerTx });
86
100
  },
87
101
  }),
88
102
  ]);
@@ -96,10 +110,10 @@ describe("db-fragment-instantiator", () => {
96
110
  const response = await fragment.handler(new Request("http://localhost/api/test"));
97
111
  const data = await response.json();
98
112
 
99
- expect(data).toEqual({ hasExecuteMethod: true });
113
+ expect(data).toEqual({ hasHandlerTxBuilder: true });
100
114
  });
101
115
 
102
- it("should provide schema-typed UOW via executeRestrictedUnitOfWork", async () => {
116
+ it("should provide schema-typed UOW via handlerTx", async () => {
103
117
  const definition = defineFragment("test-db-fragment")
104
118
  .extend(withDatabase(testSchema))
105
119
  .build();
@@ -109,10 +123,12 @@ describe("db-fragment-instantiator", () => {
109
123
  method: "GET",
110
124
  path: "/test",
111
125
  handler: async function (_input, { json }) {
112
- const result = await this.uow(async ({ forSchema }) => {
113
- const uow = forSchema(testSchema);
114
- return { hasSchemaUow: !!uow };
115
- });
126
+ const result = await this.handlerTx()
127
+ .mutate(({ forSchema }) => {
128
+ const uow = forSchema(testSchema);
129
+ return { hasSchemaUow: !!uow };
130
+ })
131
+ .execute();
116
132
 
117
133
  return json(result);
118
134
  },
@@ -189,7 +205,7 @@ describe("db-fragment-instantiator", () => {
189
205
  });
190
206
 
191
207
  describe("database operations with UOW", () => {
192
- it("should allow accessing schema-typed UOW in handlers via executeRestrictedUnitOfWork", async () => {
208
+ it("should allow accessing schema-typed UOW in handlers via handlerTx", async () => {
193
209
  const testSchemaWithCounter = schema((s) => {
194
210
  return s.addTable("counters", (t) => {
195
211
  return t.addColumn("id", idColumn()).addColumn("value", column("integer"));
@@ -205,11 +221,13 @@ describe("db-fragment-instantiator", () => {
205
221
  method: "GET",
206
222
  path: "/counters",
207
223
  handler: async function (_input, { json }) {
208
- const result = await this.uow(async ({ forSchema }) => {
209
- const uow = forSchema(testSchemaWithCounter);
210
- // Verify that we can access the UOW
211
- return { hasCountersTable: !!uow };
212
- });
224
+ const result = await this.handlerTx()
225
+ .mutate(({ forSchema }) => {
226
+ const uow = forSchema(testSchemaWithCounter);
227
+ // Verify that we can access the UOW
228
+ return { hasCountersTable: !!uow };
229
+ })
230
+ .execute();
213
231
 
214
232
  return json(result);
215
233
  },
@@ -230,14 +248,15 @@ describe("db-fragment-instantiator", () => {
230
248
  });
231
249
 
232
250
  describe("service integration with UOW", () => {
233
- it("should allow services to access UOW via forSchema", async () => {
251
+ it("should allow services to access UOW via serviceTx", async () => {
234
252
  const definition = defineFragment("test-db-fragment")
235
253
  .extend(withDatabase(testSchema))
236
254
  .providesBaseService(({ defineService }) =>
237
255
  defineService({
238
256
  checkTypedUowExists: function () {
239
- const uow = this.uow(testSchema);
240
- return !!uow;
257
+ return this.serviceTx(testSchema)
258
+ .mutate(({ uow }) => !!uow)
259
+ .build();
241
260
  },
242
261
  }),
243
262
  )
@@ -249,7 +268,10 @@ describe("db-fragment-instantiator", () => {
249
268
  path: "/check",
250
269
  outputSchema: z.object({ hasTypedUow: z.boolean() }),
251
270
  handler: async function (_input, { json }) {
252
- const hasTypedUow = services.checkTypedUowExists();
271
+ const hasTypedUow = await this.handlerTx()
272
+ .withServiceCalls(() => [services.checkTypedUowExists()] as const)
273
+ .transform(({ serviceResult: [result] }) => result)
274
+ .execute();
253
275
  return json({ hasTypedUow });
254
276
  },
255
277
  }),
@@ -266,73 +288,18 @@ describe("db-fragment-instantiator", () => {
266
288
  assert(response.type === "json");
267
289
  expect(response.data).toEqual({ hasTypedUow: true });
268
290
  });
269
-
270
- it.skip("should share same UOW across multiple service calls from handler", async () => {
271
- const definition = defineFragment("test-db-fragment")
272
- .extend(withDatabase(testSchema))
273
- .providesService("helpers", ({ defineService }) =>
274
- defineService({
275
- logUow: function () {
276
- return this.uow(testSchema);
277
- },
278
- }),
279
- )
280
- .providesService("main", ({ defineService }) =>
281
- defineService({
282
- markUow: function () {
283
- return this.uow(testSchema);
284
- },
285
- }),
286
- )
287
- .build();
288
-
289
- const routes = defineRoutes(definition).create(({ services, defineRoute }) => [
290
- defineRoute({
291
- method: "GET",
292
- path: "/nested",
293
- handler: async function (_input, { json }) {
294
- // Mark the UOW with an ID
295
- const uow1 = services.main.markUow();
296
- const uow2 = services.helpers.logUow();
297
- const uow3 = services.main.markUow();
298
-
299
- console.log({
300
- x: uow1 === uow2,
301
- y: uow2 === uow3,
302
- z: uow1 === uow3,
303
- });
304
-
305
- return json({
306
- same: uow1 === uow2 && uow2 === uow3,
307
- });
308
- },
309
- }),
310
- ]);
311
-
312
- const mockAdapter = createMockAdapter();
313
- const fragment = instantiate(definition)
314
- .withRoutes([routes])
315
- .withOptions({ mountRoute: "/api", databaseAdapter: mockAdapter })
316
- .build();
317
-
318
- const response = await fragment.handler(new Request("http://localhost/api/nested"));
319
- const data = await response.json();
320
-
321
- expect(data).toEqual({
322
- same: true,
323
- });
324
- });
325
291
  });
326
292
 
327
293
  describe("inContext with database fragments", () => {
328
- it("should allow calling services with UOW via inContext", () => {
294
+ it("should allow calling services with UOW via inContext", async () => {
329
295
  const definition = defineFragment("test-db-fragment")
330
296
  .extend(withDatabase(testSchema))
331
297
  .providesBaseService(({ defineService }) =>
332
298
  defineService({
333
299
  getUowExists: function () {
334
- const uow = this.uow(testSchema);
335
- return !!uow;
300
+ return this.serviceTx(testSchema)
301
+ .mutate(({ uow }) => !!uow)
302
+ .build();
336
303
  },
337
304
  }),
338
305
  )
@@ -343,7 +310,12 @@ describe("db-fragment-instantiator", () => {
343
310
  .withOptions({ databaseAdapter: mockAdapter })
344
311
  .build();
345
312
 
346
- const result = fragment.inContext(() => fragment.services.getUowExists());
313
+ const result = await fragment.inContext(async function () {
314
+ return await this.handlerTx()
315
+ .withServiceCalls(() => [fragment.services.getUowExists()] as const)
316
+ .transform(({ serviceResult: [exists] }) => exists)
317
+ .execute();
318
+ });
347
319
  expect(result).toBe(true);
348
320
  });
349
321
  });
@@ -370,10 +342,12 @@ describe("db-fragment-instantiator", () => {
370
342
  method: "GET",
371
343
  path: "/test",
372
344
  handler: async function (_input, { json }) {
373
- const result = await this.uow(async ({ forSchema }) => {
374
- const uow = forSchema(testSchema);
375
- return { hasUow: !!uow };
376
- });
345
+ const result = await this.handlerTx()
346
+ .mutate(({ forSchema }) => {
347
+ const uow = forSchema(testSchema);
348
+ return { hasUow: !!uow };
349
+ })
350
+ .execute();
377
351
  return json(result);
378
352
  },
379
353
  }),
@@ -518,13 +492,15 @@ describe("db-fragment-instantiator", () => {
518
492
  }).toThrow("Database fragment requires a database adapter");
519
493
  });
520
494
 
521
- it("should throw when forSchema called outside request context", () => {
495
+ it("should throw when serviceTx called outside request context", () => {
522
496
  const definition = defineFragment("test-db-fragment")
523
497
  .extend(withDatabase(testSchema))
524
498
  .providesBaseService(({ defineService }) =>
525
499
  defineService({
526
500
  tryGetUow: function () {
527
- return this.uow(testSchema);
501
+ return this.serviceTx(testSchema)
502
+ .mutate(({ uow }) => uow)
503
+ .build();
528
504
  },
529
505
  }),
530
506
  )