@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.
- package/.turbo/turbo-build.log +34 -30
- package/CHANGELOG.md +32 -0
- package/dist/adapters/generic-sql/query/where-builder.js +1 -1
- package/dist/db-fragment-definition-builder.d.ts +27 -89
- package/dist/db-fragment-definition-builder.d.ts.map +1 -1
- package/dist/db-fragment-definition-builder.js +16 -56
- package/dist/db-fragment-definition-builder.js.map +1 -1
- package/dist/fragments/internal-fragment.d.ts +94 -8
- package/dist/fragments/internal-fragment.d.ts.map +1 -1
- package/dist/fragments/internal-fragment.js +56 -55
- package/dist/fragments/internal-fragment.js.map +1 -1
- package/dist/hooks/hooks.d.ts +5 -3
- package/dist/hooks/hooks.d.ts.map +1 -1
- package/dist/hooks/hooks.js +38 -37
- package/dist/hooks/hooks.js.map +1 -1
- package/dist/mod.d.ts +3 -3
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +4 -4
- package/dist/mod.js.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.d.ts +351 -100
- package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.js +431 -263
- package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.d.ts +17 -8
- package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.js +24 -8
- package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
- package/dist/query/value-decoding.js +1 -1
- package/dist/schema/create.d.ts +3 -1
- package/dist/schema/create.d.ts.map +1 -1
- package/dist/schema/create.js +2 -1
- package/dist/schema/create.js.map +1 -1
- package/dist/schema/generate-id.d.ts +20 -0
- package/dist/schema/generate-id.d.ts.map +1 -0
- package/dist/schema/generate-id.js +28 -0
- package/dist/schema/generate-id.js.map +1 -0
- package/package.json +1 -1
- package/src/adapters/drizzle/drizzle-adapter-sqlite3.test.ts +41 -25
- package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +39 -25
- package/src/db-fragment-definition-builder.test.ts +58 -42
- package/src/db-fragment-definition-builder.ts +58 -248
- package/src/db-fragment-instantiator.test.ts +64 -88
- package/src/db-fragment-integration.test.ts +292 -142
- package/src/fragments/internal-fragment.test.ts +272 -266
- package/src/fragments/internal-fragment.ts +155 -121
- package/src/hooks/hooks.test.ts +248 -256
- package/src/hooks/hooks.ts +74 -63
- package/src/mod.ts +14 -4
- package/src/query/unit-of-work/execute-unit-of-work.test.ts +1494 -1464
- package/src/query/unit-of-work/execute-unit-of-work.ts +1685 -590
- package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
- package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +20 -20
- package/src/query/unit-of-work/unit-of-work.test.ts +64 -0
- package/src/query/unit-of-work/unit-of-work.ts +26 -13
- package/src/schema/create.ts +2 -0
- package/src/schema/generate-id.test.ts +57 -0
- package/src/schema/generate-id.ts +38 -0
- package/src/shared/config.ts +0 -10
- package/src/shared/connection-pool.ts +0 -24
- 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
type
|
|
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
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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.
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
-
|
|
85
|
+
serviceTx<TSchema extends AnySchema>(
|
|
98
86
|
schema: TSchema,
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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.
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
-
|
|
723
|
-
|
|
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
|
-
|
|
755
|
-
tx: serviceTx,
|
|
665
|
+
serviceTx,
|
|
756
666
|
};
|
|
757
667
|
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
678
|
+
const userOnAfterMutate = execOptions?.onAfterMutate;
|
|
793
679
|
|
|
794
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
700
|
+
onAfterMutate: async (uow) => {
|
|
816
701
|
if (hooksConfig) {
|
|
817
702
|
await processHooks(hooksConfig);
|
|
818
703
|
}
|
|
819
|
-
if (
|
|
820
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
|
83
|
-
expect(this.
|
|
96
|
+
// Access handlerTx from this context
|
|
97
|
+
expect(this.handlerTx).toBeDefined();
|
|
84
98
|
|
|
85
|
-
return json({
|
|
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({
|
|
113
|
+
expect(data).toEqual({ hasHandlerTxBuilder: true });
|
|
100
114
|
});
|
|
101
115
|
|
|
102
|
-
it("should provide schema-typed UOW via
|
|
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.
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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
|
-
|
|
240
|
-
|
|
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 =
|
|
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
|
-
|
|
335
|
-
|
|
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(
|
|
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.
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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.
|
|
501
|
+
return this.serviceTx(testSchema)
|
|
502
|
+
.mutate(({ uow }) => uow)
|
|
503
|
+
.build();
|
|
528
504
|
},
|
|
529
505
|
}),
|
|
530
506
|
)
|