@fragno-dev/db 0.2.1 → 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 (60) hide show
  1. package/.turbo/turbo-build.log +34 -30
  2. package/CHANGELOG.md +32 -0
  3. package/dist/adapters/generic-sql/query/where-builder.js +1 -1
  4. package/dist/db-fragment-definition-builder.d.ts +27 -89
  5. package/dist/db-fragment-definition-builder.d.ts.map +1 -1
  6. package/dist/db-fragment-definition-builder.js +16 -56
  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 +351 -100
  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 +431 -263
  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 +17 -8
  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 +24 -8
  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 +3 -1
  30. package/dist/schema/create.d.ts.map +1 -1
  31. package/dist/schema/create.js +2 -1
  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/package.json +1 -1
  38. package/src/adapters/drizzle/drizzle-adapter-sqlite3.test.ts +41 -25
  39. package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +39 -25
  40. package/src/db-fragment-definition-builder.test.ts +58 -42
  41. package/src/db-fragment-definition-builder.ts +58 -248
  42. package/src/db-fragment-instantiator.test.ts +64 -88
  43. package/src/db-fragment-integration.test.ts +292 -142
  44. package/src/fragments/internal-fragment.test.ts +272 -266
  45. package/src/fragments/internal-fragment.ts +155 -121
  46. package/src/hooks/hooks.test.ts +248 -256
  47. package/src/hooks/hooks.ts +74 -63
  48. package/src/mod.ts +14 -4
  49. package/src/query/unit-of-work/execute-unit-of-work.test.ts +1494 -1464
  50. package/src/query/unit-of-work/execute-unit-of-work.ts +1685 -590
  51. package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
  52. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +20 -20
  53. package/src/query/unit-of-work/unit-of-work.test.ts +64 -0
  54. package/src/query/unit-of-work/unit-of-work.ts +26 -13
  55. package/src/schema/create.ts +2 -0
  56. package/src/schema/generate-id.test.ts +57 -0
  57. package/src/schema/generate-id.ts +38 -0
  58. package/src/shared/config.ts +0 -10
  59. package/src/shared/connection-pool.ts +0 -24
  60. package/src/shared/prisma.ts +0 -45
@@ -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,14 +13,11 @@ import {
14
13
  type ServiceConstructorFn,
15
14
  } from "@fragno-dev/core";
16
15
  import {
17
- executeRestrictedUnitOfWork,
18
- executeTxArray,
19
- executeTxCallbacks,
20
- executeServiceTx,
21
- type AwaitedPromisesInObject,
22
- type ExecuteRestrictedUnitOfWorkOptions,
23
- type HandlerTxCallbacks,
24
- type ServiceTxCallbacks,
16
+ createServiceTxBuilder,
17
+ createHandlerTxBuilder,
18
+ ServiceTxBuilder,
19
+ HandlerTxBuilder,
20
+ type ExecuteTxOptions,
25
21
  } from "./query/unit-of-work/execute-unit-of-work";
26
22
  import {
27
23
  prepareHookMutations,
@@ -69,35 +65,38 @@ export type ImplicitDatabaseDependencies<TSchema extends AnySchema> = {
69
65
  */
70
66
  export type DatabaseServiceContext<THooks extends HooksMap> = RequestThisContext & {
71
67
  /**
72
- * Get a typed, restricted Unit of Work for the given schema.
73
- * @param schema - Schema to get a typed view for
74
- * @returns TypedUnitOfWork (restricted version without execute methods)
75
- */
76
- uow<TSchema extends AnySchema>(schema: TSchema): TypedUnitOfWork<TSchema, [], unknown, THooks>;
77
-
78
- /**
79
- * Execute a transaction with two-phase callbacks (retrieve + mutate).
80
- *
81
- * @param schema - Schema to use for the transaction
82
- * @param callbacks - Object containing retrieve and mutate callbacks
83
- * @returns Promise resolving to the mutation result with promises awaited 1 level deep
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.
84
71
  *
85
72
  * @example
86
73
  * ```ts
87
- * return this.tx(schema, {
88
- * retrieve: (uow) => uow.findFirst("users", ...),
89
- * mutate: async (uow, [user]) => {
90
- * await validateUser(user);
91
- * uow.update("users", user.id, ...).check();
92
- * return { ok: true };
93
- * }
94
- * });
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();
95
83
  * ```
96
84
  */
97
- tx<TSchema extends AnySchema, TRetrievalResults extends unknown[], TMutationResult = void>(
85
+ serviceTx<TSchema extends AnySchema>(
98
86
  schema: TSchema,
99
- callbacks: ServiceTxCallbacks<TSchema, TRetrievalResults, TMutationResult, THooks>,
100
- ): Promise<AwaitedPromisesInObject<TMutationResult>>;
87
+ ): ServiceTxBuilder<
88
+ TSchema,
89
+ readonly [],
90
+ [],
91
+ [],
92
+ unknown,
93
+ unknown,
94
+ false,
95
+ false,
96
+ false,
97
+ false,
98
+ THooks
99
+ >;
101
100
  };
102
101
 
103
102
  /**
@@ -105,92 +104,24 @@ export type DatabaseServiceContext<THooks extends HooksMap> = RequestThisContext
105
104
  */
106
105
  export type DatabaseHandlerContext<THooks extends HooksMap = {}> = RequestThisContext & {
107
106
  /**
108
- * Execute a Unit of Work with explicit phase control and automatic retry support.
109
- * This method provides an API where users call forSchema to create a schema-specific
110
- * UOW, then call executeRetrieve() and executeMutate() to execute the phases. The entire
111
- * callback is re-executed on optimistic concurrency conflicts, ensuring retries work properly.
112
- * Automatically provides the UOW factory from context.
113
- * Promises in the returned object are awaited 1 level deep.
114
- *
115
- * @param callback - Async function that receives a context with forSchema, executeRetrieve, executeMutate, nonce, and currentAttempt
116
- * @param options - Optional configuration for retry policy and abort signal
117
- * @returns Promise resolving to the callback's return value with promises awaited 1 level deep
118
- * @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.
119
110
  *
120
111
  * @example
121
112
  * ```ts
122
- * const result = await this.uow(async ({ forSchema, executeRetrieve, executeMutate, nonce, currentAttempt }) => {
123
- * const uow = forSchema(schema);
124
- * const userId = uow.create("users", { name: "John" });
125
- *
126
- * // Execute retrieval phase
127
- * await executeRetrieve();
128
- *
129
- * const profileId = uow.create("profiles", { userId });
130
- *
131
- * // Execute mutation phase
132
- * await executeMutate();
133
- *
134
- * return { userId, profileId };
135
- * });
136
- * ```
137
- */
138
- uow<TResult>(
139
- callback: (context: {
140
- forSchema: <TSchema extends AnySchema, H extends HooksMap = THooks>(
141
- schema: TSchema,
142
- hooks?: H,
143
- ) => TypedUnitOfWork<TSchema, [], unknown, H>;
144
- executeRetrieve: () => Promise<void>;
145
- executeMutate: () => Promise<void>;
146
- nonce: string;
147
- currentAttempt: number;
148
- }) => Promise<TResult> | TResult,
149
- options?: Omit<ExecuteRestrictedUnitOfWorkOptions, "createUnitOfWork">,
150
- ): Promise<AwaitedPromisesInObject<TResult>>;
151
-
152
- /**
153
- * Execute a transaction with automatic retry support.
154
- * Provides two overloads: array syntax (common case) and callback syntax (advanced).
155
- *
156
- * Array syntax - pass a factory function that returns service calls:
157
- * ```ts
158
- * const [transfer, notify] = await this.tx(
159
- * () => [
160
- * services.transfer({ from, to, amount }),
161
- * services.notify({ userId, message })
162
- * ]
163
- * );
164
- * ```
165
- *
166
- * Callback syntax - for handlers that need direct UOW access:
167
- * ```ts
168
- * const result = await this.tx({
169
- * retrieve: ({ forSchema }) => {
170
- * const uow = forSchema(schema);
171
- * uow.find("users", ...);
172
- * return services.transfer({ ... });
173
- * },
174
- * mutate: ({ forSchema }, transferPromise) => {
175
- * const uow = forSchema(schema);
176
- * uow.create("auditLog", { ... });
177
- * return transferPromise;
178
- * }
179
- * });
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();
180
120
  * ```
181
- *
182
- * Note: Handler callbacks are synchronous only to prevent accidentally awaiting services in wrong place.
183
121
  */
184
- // Overload 1: Array syntax (common case) - accepts a factory
185
- tx<T extends readonly unknown[]>(
186
- servicesFactory: () => readonly [...{ [K in keyof T]: Promise<T[K]> }],
187
- options?: Omit<ExecuteRestrictedUnitOfWorkOptions, "createUnitOfWork">,
188
- ): Promise<{ [K in keyof T]: T[K] }>;
189
- // Overload 2: Callback syntax (advanced, sync callbacks only)
190
- tx<TRetrieveResult, TMutationResult>(
191
- callbacks: HandlerTxCallbacks<TRetrieveResult, TMutationResult, THooks>,
192
- options?: Omit<ExecuteRestrictedUnitOfWorkOptions, "createUnitOfWork">,
193
- ): Promise<AwaitedPromisesInObject<TMutationResult>>;
122
+ handlerTx(
123
+ options?: Omit<ExecuteTxOptions, "createUnitOfWork">,
124
+ ): HandlerTxBuilder<readonly [], [], [], unknown, unknown, false, false, false, false, THooks>;
194
125
  };
195
126
 
196
127
  /**
@@ -487,7 +418,7 @@ export class DatabaseFragmentDefinitionBuilder<
487
418
  * ```ts
488
419
  * .provideHooks(({ defineHook, config }) => ({
489
420
  * onSubscribe: defineHook(async function (payload: { email: string }) {
490
- * // 'this' context available (HookServiceContext with nonce)
421
+ * // 'this' context available (HookServiceContext with idempotencyKey)
491
422
  * await config.onSubscribe?.(payload.email);
492
423
  * }),
493
424
  * }))
@@ -719,55 +650,23 @@ export class DatabaseFragmentDefinitionBuilder<
719
650
  }
720
651
  : undefined;
721
652
 
722
- function forSchema<TSchema extends AnySchema>(
723
- schema: TSchema,
724
- ): TypedUnitOfWork<TSchema, [], unknown, THooks> {
653
+ // Builder API: serviceTx using createServiceTxBuilder
654
+ function serviceTx<TSchema extends AnySchema>(schema: TSchema) {
725
655
  const uow = storage.getStore()?.uow;
726
656
  if (!uow) {
727
657
  throw new Error(
728
658
  "No UnitOfWork in context. Service must be called within a route handler OR using `withUnitOfWork`.",
729
659
  );
730
660
  }
731
-
732
- return uow.restrict().forSchema<TSchema, THooks>(schema);
661
+ return createServiceTxBuilder<TSchema, THooks>(schema, uow, hooksConfig?.hooks);
733
662
  }
734
663
 
735
- const serviceTx = async <
736
- TSchema extends AnySchema,
737
- TRetrievalResults extends unknown[],
738
- TMutationResult = void,
739
- >(
740
- schema: TSchema,
741
- callbacks: ServiceTxCallbacks<TSchema, TRetrievalResults, TMutationResult, THooks>,
742
- ): Promise<AwaitedPromisesInObject<TMutationResult>> => {
743
- const uow = storage.getStore()?.uow;
744
- if (!uow) {
745
- throw new Error(
746
- "No UnitOfWork in context. Service must be called within a route handler OR using `withUnitOfWork`.",
747
- );
748
- }
749
-
750
- return executeServiceTx(schema, callbacks, uow);
751
- };
752
-
753
664
  const serviceContext: DatabaseServiceContext<THooks> = {
754
- uow: forSchema,
755
- tx: serviceTx,
665
+ serviceTx,
756
666
  };
757
667
 
758
- async function uow<TResult>(
759
- callback: (context: {
760
- forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
761
- schema: S,
762
- hooks?: H,
763
- ) => TypedUnitOfWork<S, [], unknown, H>;
764
- executeRetrieve: () => Promise<void>;
765
- executeMutate: () => Promise<void>;
766
- nonce: string;
767
- currentAttempt: number;
768
- }) => Promise<TResult> | TResult,
769
- execOptions?: Omit<ExecuteRestrictedUnitOfWorkOptions, "createUnitOfWork">,
770
- ): Promise<AwaitedPromisesInObject<TResult>> {
668
+ // Builder API: handlerTx using createHandlerTxBuilder
669
+ function handlerTx(execOptions?: Omit<ExecuteTxOptions, "createUnitOfWork">) {
771
670
  const currentStorage = storage.getStore();
772
671
  if (!currentStorage) {
773
672
  throw new Error(
@@ -775,23 +674,10 @@ export class DatabaseFragmentDefinitionBuilder<
775
674
  );
776
675
  }
777
676
 
778
- const wrappedCallback = async (context: {
779
- forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
780
- schema: S,
781
- hooks?: H,
782
- ) => TypedUnitOfWork<S, [], unknown, H>;
783
- executeRetrieve: () => Promise<void>;
784
- executeMutate: () => Promise<void>;
785
- nonce: string;
786
- currentAttempt: number;
787
- }): Promise<TResult> => {
788
- return callback(context);
789
- };
790
-
791
677
  const userOnBeforeMutate = execOptions?.onBeforeMutate;
792
- const userOnSuccess = execOptions?.onSuccess;
678
+ const userOnAfterMutate = execOptions?.onAfterMutate;
793
679
 
794
- return executeRestrictedUnitOfWork(wrappedCallback, {
680
+ return createHandlerTxBuilder<THooks>({
795
681
  ...execOptions,
796
682
  createUnitOfWork: () => {
797
683
  currentStorage.uow.reset();
@@ -801,8 +687,7 @@ export class DatabaseFragmentDefinitionBuilder<
801
687
  hooksConfig.internalFragment.$internal.deps.namespace,
802
688
  );
803
689
  }
804
- // Safe cast: currentStorage.uow is always a UnitOfWork instance
805
- return currentStorage.uow as UnitOfWork;
690
+ return currentStorage.uow;
806
691
  },
807
692
  onBeforeMutate: (uow) => {
808
693
  if (hooksConfig) {
@@ -812,94 +697,19 @@ export class DatabaseFragmentDefinitionBuilder<
812
697
  userOnBeforeMutate(uow);
813
698
  }
814
699
  },
815
- onSuccess: async (uow) => {
700
+ onAfterMutate: async (uow) => {
816
701
  if (hooksConfig) {
817
702
  await processHooks(hooksConfig);
818
703
  }
819
- if (userOnSuccess) {
820
- await userOnSuccess(uow);
704
+ if (userOnAfterMutate) {
705
+ await userOnAfterMutate(uow);
821
706
  }
822
707
  },
823
708
  });
824
709
  }
825
710
 
826
- // Handler tx method with two overloads
827
- function handlerTx<T extends readonly unknown[]>(
828
- servicesFactory: () => readonly [...{ [K in keyof T]: Promise<T[K]> }],
829
- execOptions?: Omit<ExecuteRestrictedUnitOfWorkOptions, "createUnitOfWork">,
830
- ): Promise<{ [K in keyof T]: T[K] }>;
831
- function handlerTx<TRetrieveResult, TMutationResult>(
832
- callbacks: HandlerTxCallbacks<TRetrieveResult, TMutationResult, THooks>,
833
- execOptions?: Omit<ExecuteRestrictedUnitOfWorkOptions, "createUnitOfWork">,
834
- ): Promise<AwaitedPromisesInObject<TMutationResult>>;
835
- async function handlerTx<T extends readonly unknown[], TRetrieveResult, TMutationResult>(
836
- factoryOrCallbacks:
837
- | (() => readonly [...{ [K in keyof T]: Promise<T[K]> }])
838
- | HandlerTxCallbacks<TRetrieveResult, TMutationResult, THooks>,
839
- execOptions?: Omit<ExecuteRestrictedUnitOfWorkOptions, "createUnitOfWork">,
840
- ): Promise<{ [K in keyof T]: T[K] } | AwaitedPromisesInObject<TMutationResult>> {
841
- const currentStorage = storage.getStore();
842
- if (!currentStorage) {
843
- throw new Error(
844
- "No storage in context. Handler must be called within a request context.",
845
- );
846
- }
847
-
848
- const userOnBeforeMutate = execOptions?.onBeforeMutate;
849
- const userOnSuccess = execOptions?.onSuccess;
850
-
851
- const createUow = () => {
852
- currentStorage.uow.reset();
853
-
854
- if (hooksConfig) {
855
- currentStorage.uow.registerSchema(
856
- hooksConfig.internalFragment.$internal.deps.schema,
857
- hooksConfig.internalFragment.$internal.deps.namespace,
858
- );
859
- }
860
- // Safe cast: currentStorage.uow is always a UnitOfWork instance
861
- return currentStorage.uow as UnitOfWork;
862
- };
863
-
864
- const options: ExecuteRestrictedUnitOfWorkOptions = {
865
- ...execOptions,
866
- createUnitOfWork: createUow,
867
- onBeforeMutate: (uow) => {
868
- if (hooksConfig) {
869
- prepareHookMutations(uow, hooksConfig);
870
- }
871
- if (userOnBeforeMutate) {
872
- userOnBeforeMutate(uow);
873
- }
874
- },
875
- onSuccess: async (uow) => {
876
- if (hooksConfig) {
877
- await processHooks(hooksConfig);
878
- }
879
- if (userOnSuccess) {
880
- await userOnSuccess(uow);
881
- }
882
- },
883
- };
884
-
885
- // Check if it's a function (array syntax factory) or callbacks object (callback syntax)
886
- if (typeof factoryOrCallbacks === "function") {
887
- // Array syntax - factoryOrCallbacks is a factory function
888
- return executeTxArray(
889
- factoryOrCallbacks as () => readonly [...{ [K in keyof T]: Promise<T[K]> }],
890
- options,
891
- ) as Promise<{ [K in keyof T]: T[K] }>;
892
- } else {
893
- return executeTxCallbacks(
894
- factoryOrCallbacks as HandlerTxCallbacks<TRetrieveResult, TMutationResult, THooks>,
895
- options,
896
- );
897
- }
898
- }
899
-
900
711
  const handlerContext: DatabaseHandlerContext<THooks> = {
901
- uow,
902
- tx: handlerTx,
712
+ handlerTx,
903
713
  };
904
714
 
905
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
  )