@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.
- package/.turbo/turbo-build.log +34 -30
- package/CHANGELOG.md +49 -0
- package/dist/adapters/generic-sql/query/where-builder.js +1 -1
- package/dist/db-fragment-definition-builder.d.ts +31 -39
- package/dist/db-fragment-definition-builder.d.ts.map +1 -1
- package/dist/db-fragment-definition-builder.js +20 -16
- 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 +367 -80
- 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 +448 -148
- 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 +35 -11
- package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.js +49 -19
- 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 +2 -3
- package/dist/schema/create.d.ts.map +1 -1
- package/dist/schema/create.js +2 -5
- 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/dist/sql-driver/dialects/durable-object-dialect.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +1 -0
- 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 +78 -88
- 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 -122
- package/src/hooks/hooks.test.ts +268 -264
- 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 +1582 -998
- package/src/query/unit-of-work/execute-unit-of-work.ts +1746 -343
- package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
- package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +269 -21
- package/src/query/unit-of-work/unit-of-work.test.ts +64 -0
- package/src/query/unit-of-work/unit-of-work.ts +65 -30
- package/src/schema/create.ts +2 -5
- 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
|
@@ -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
|
|
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.
|
|
720
|
-
expect(typeof contexts.handlerContext.
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
651
|
-
|
|
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
|
-
|
|
665
|
+
serviceTx,
|
|
665
666
|
};
|
|
666
667
|
|
|
667
|
-
|
|
668
|
-
|
|
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
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
)
|