@fragno-dev/test 2.0.0 → 2.0.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 +41 -31
- package/CHANGELOG.md +69 -0
- package/dist/adapters.d.ts +4 -7
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +18 -302
- package/dist/adapters.js.map +1 -1
- package/dist/db-test.d.ts +120 -18
- package/dist/db-test.d.ts.map +1 -1
- package/dist/db-test.js +203 -55
- package/dist/db-test.js.map +1 -1
- package/dist/durable-hooks.d.ts +6 -2
- package/dist/durable-hooks.d.ts.map +1 -1
- package/dist/durable-hooks.js +10 -5
- package/dist/durable-hooks.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model-checker-actors.d.ts.map +1 -1
- package/dist/model-checker-actors.js +1 -1
- package/dist/model-checker-actors.js.map +1 -1
- package/dist/model-checker-adapter.d.ts +1 -1
- package/dist/model-checker-adapter.d.ts.map +1 -1
- package/dist/model-checker-adapter.js.map +1 -1
- package/dist/model-checker.d.ts.map +1 -1
- package/dist/model-checker.js.map +1 -1
- package/dist/test-adapters/drizzle-pglite.js +116 -0
- package/dist/test-adapters/drizzle-pglite.js.map +1 -0
- package/dist/test-adapters/in-memory.js +39 -0
- package/dist/test-adapters/in-memory.js.map +1 -0
- package/dist/test-adapters/kysely-pglite.js +105 -0
- package/dist/test-adapters/kysely-pglite.js.map +1 -0
- package/dist/test-adapters/kysely-sqlite.js +87 -0
- package/dist/test-adapters/kysely-sqlite.js.map +1 -0
- package/dist/test-adapters/model-checker.js +41 -0
- package/dist/test-adapters/model-checker.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +32 -33
- package/src/adapter-conformance.test.ts +3 -1
- package/src/adapters.ts +24 -455
- package/src/db-roundtrip-guard.test.ts +206 -0
- package/src/db-test.test.ts +131 -77
- package/src/db-test.ts +530 -96
- package/src/durable-hooks.test.ts +58 -0
- package/src/durable-hooks.ts +23 -8
- package/src/index.test.ts +188 -104
- package/src/index.ts +6 -2
- package/src/model-checker-actors.test.ts +5 -2
- package/src/model-checker-actors.ts +2 -1
- package/src/model-checker-adapter.ts +3 -2
- package/src/model-checker.test.ts +4 -1
- package/src/model-checker.ts +4 -3
- package/src/test-adapters/drizzle-pglite.ts +162 -0
- package/src/test-adapters/in-memory.ts +56 -0
- package/src/test-adapters/kysely-pglite.ts +151 -0
- package/src/test-adapters/kysely-sqlite.ts +119 -0
- package/src/test-adapters/model-checker.ts +58 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createDurableHooksProcessor } from "@fragno-dev/db/dispatchers/node";
|
|
4
|
+
|
|
5
|
+
import { drainDurableHooks } from "./durable-hooks";
|
|
6
|
+
|
|
7
|
+
const drainMock = vi.fn<() => Promise<void>>();
|
|
8
|
+
const wakeMock = vi.fn<() => Promise<void>>();
|
|
9
|
+
|
|
10
|
+
vi.mock("@fragno-dev/db/dispatchers/node", () => ({
|
|
11
|
+
createDurableHooksProcessor: vi.fn(() => ({
|
|
12
|
+
drain: drainMock,
|
|
13
|
+
wake: wakeMock,
|
|
14
|
+
})),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe("drainDurableHooks", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
drainMock.mockReset();
|
|
20
|
+
wakeMock.mockReset();
|
|
21
|
+
vi.mocked(createDurableHooksProcessor).mockClear();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("no-ops when durable hooks are not configured", async () => {
|
|
25
|
+
await expect(
|
|
26
|
+
drainDurableHooks({
|
|
27
|
+
$internal: {},
|
|
28
|
+
} as never),
|
|
29
|
+
).resolves.toBeUndefined();
|
|
30
|
+
|
|
31
|
+
expect(createDurableHooksProcessor).not.toHaveBeenCalled();
|
|
32
|
+
expect(drainMock).not.toHaveBeenCalled();
|
|
33
|
+
expect(wakeMock).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("drains until idle by default", async () => {
|
|
37
|
+
await drainDurableHooks({
|
|
38
|
+
$internal: { durableHooksToken: {} },
|
|
39
|
+
} as never);
|
|
40
|
+
|
|
41
|
+
expect(createDurableHooksProcessor).toHaveBeenCalledTimes(1);
|
|
42
|
+
expect(wakeMock).not.toHaveBeenCalled();
|
|
43
|
+
expect(drainMock).toHaveBeenCalledTimes(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("supports single-pass draining", async () => {
|
|
47
|
+
await drainDurableHooks(
|
|
48
|
+
{
|
|
49
|
+
$internal: { durableHooksToken: {} },
|
|
50
|
+
} as never,
|
|
51
|
+
{ mode: "singlePass" },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(createDurableHooksProcessor).toHaveBeenCalledTimes(1);
|
|
55
|
+
expect(wakeMock).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(drainMock).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/durable-hooks.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type AnyFragnoInstantiatedDatabaseFragment,
|
|
4
|
-
} from "@fragno-dev/db";
|
|
1
|
+
import { createDurableHooksProcessor } from "@fragno-dev/db/dispatchers/node";
|
|
2
|
+
|
|
5
3
|
import type { AnyFragnoInstantiatedFragment } from "@fragno-dev/core";
|
|
4
|
+
import { type AnyFragnoInstantiatedDatabaseFragment } from "@fragno-dev/db";
|
|
5
|
+
|
|
6
|
+
export type DrainDurableHooksMode = "untilIdle" | "singlePass";
|
|
7
|
+
|
|
8
|
+
export type DrainDurableHooksOptions = {
|
|
9
|
+
mode?: DrainDurableHooksMode;
|
|
10
|
+
};
|
|
6
11
|
|
|
7
|
-
export async function drainDurableHooks(
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
export async function drainDurableHooks(
|
|
13
|
+
fragment: AnyFragnoInstantiatedFragment,
|
|
14
|
+
options: DrainDurableHooksOptions = {},
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const internal = fragment.$internal as { durableHooksToken?: object } | undefined;
|
|
17
|
+
if (!internal?.durableHooksToken) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const dispatcher = createDurableHooksProcessor([
|
|
21
|
+
fragment as AnyFragnoInstantiatedDatabaseFragment,
|
|
22
|
+
]);
|
|
23
|
+
if (options.mode === "singlePass") {
|
|
24
|
+
await dispatcher.wake();
|
|
10
25
|
return;
|
|
11
26
|
}
|
|
12
|
-
await
|
|
27
|
+
await dispatcher.drain();
|
|
13
28
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { ExtractFragmentServices } from "@fragno-dev/core/route";
|
|
2
4
|
import { column, idColumn, schema } from "@fragno-dev/db/schema";
|
|
3
|
-
|
|
5
|
+
|
|
4
6
|
import { defineFragment } from "@fragno-dev/core";
|
|
5
7
|
import { instantiate } from "@fragno-dev/core";
|
|
8
|
+
import { Cursor, withDatabase } from "@fragno-dev/db";
|
|
9
|
+
|
|
6
10
|
import { buildDatabaseFragmentsTest } from "./db-test";
|
|
7
|
-
import
|
|
11
|
+
import { drainDurableHooks } from "./durable-hooks";
|
|
8
12
|
|
|
9
13
|
// Test schema with multiple versions
|
|
10
14
|
const testSchema = schema("test", (s) => {
|
|
@@ -25,32 +29,41 @@ const testSchema = schema("test", (s) => {
|
|
|
25
29
|
// Test fragment definition
|
|
26
30
|
const testFragmentDef = defineFragment<{}>("test-fragment")
|
|
27
31
|
.extend(withDatabase(testSchema))
|
|
28
|
-
.providesBaseService(({
|
|
29
|
-
|
|
30
|
-
createUser:
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
.providesBaseService(({ defineService }) =>
|
|
33
|
+
defineService({
|
|
34
|
+
createUser: function (data: { name: string; email: string; age?: number | null }) {
|
|
35
|
+
return this.serviceTx(testSchema)
|
|
36
|
+
.mutate(({ uow }) => uow.create("users", data))
|
|
37
|
+
.transform(({ mutateResult }) => ({ ...data, id: mutateResult.valueOf() }))
|
|
38
|
+
.build();
|
|
33
39
|
},
|
|
34
|
-
getUsers:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
getUsers: function () {
|
|
41
|
+
return this.serviceTx(testSchema)
|
|
42
|
+
.retrieve((uow) =>
|
|
43
|
+
uow.find("users", (b) => b.whereIndex("idx_users_all", (eb) => eb("id", "!=", ""))),
|
|
44
|
+
)
|
|
45
|
+
.transformRetrieve(([users]) => users.map((u) => ({ ...u, id: u.id.valueOf() })))
|
|
46
|
+
.build();
|
|
39
47
|
},
|
|
40
|
-
getUsersWithCursor:
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
getUsersWithCursor: function (cursor?: Cursor | string) {
|
|
49
|
+
return this.serviceTx(testSchema)
|
|
50
|
+
.retrieve((uow) =>
|
|
51
|
+
uow.findWithCursor("users", (b) => {
|
|
52
|
+
let builder = b
|
|
53
|
+
.whereIndex("idx_users_name")
|
|
54
|
+
.orderByIndex("idx_users_name", "asc")
|
|
55
|
+
.pageSize(2);
|
|
56
|
+
if (cursor) {
|
|
57
|
+
builder = builder.after(cursor);
|
|
58
|
+
}
|
|
59
|
+
return builder;
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
.transformRetrieve(([result]) => result)
|
|
63
|
+
.build();
|
|
51
64
|
},
|
|
52
|
-
}
|
|
53
|
-
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
54
67
|
.build();
|
|
55
68
|
|
|
56
69
|
describe("buildDatabaseFragmentsTest", () => {
|
|
@@ -63,11 +76,13 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
63
76
|
const fragment = fragments.test;
|
|
64
77
|
|
|
65
78
|
// Should be able to create and query users
|
|
66
|
-
const user = await fragment.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
const user = await fragment.fragment.callServices(() =>
|
|
80
|
+
fragment.services.createUser({
|
|
81
|
+
name: "Test User",
|
|
82
|
+
email: "test@example.com",
|
|
83
|
+
age: 25,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
71
86
|
|
|
72
87
|
expect(user).toMatchObject({
|
|
73
88
|
id: expect.any(String),
|
|
@@ -76,13 +91,24 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
76
91
|
age: 25,
|
|
77
92
|
});
|
|
78
93
|
|
|
79
|
-
const users = await fragment.services.getUsers();
|
|
94
|
+
const users = await fragment.fragment.callServices(() => fragment.services.getUsers());
|
|
80
95
|
expect(users).toHaveLength(1);
|
|
81
96
|
expect(users[0]).toMatchObject(user);
|
|
82
97
|
|
|
83
98
|
await test.cleanup();
|
|
84
99
|
});
|
|
85
100
|
|
|
101
|
+
it("should no-op drainDurableHooks when durable hooks are not configured", async () => {
|
|
102
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
103
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
104
|
+
.withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
|
|
105
|
+
.build();
|
|
106
|
+
|
|
107
|
+
await expect(drainDurableHooks(fragments.test.fragment)).resolves.toBeUndefined();
|
|
108
|
+
|
|
109
|
+
await test.cleanup();
|
|
110
|
+
});
|
|
111
|
+
|
|
86
112
|
it("should throw error for non-database fragment", async () => {
|
|
87
113
|
const nonDbFragmentDef = defineFragment<{}>("non-db-fragment")
|
|
88
114
|
.providesBaseService(() => ({}))
|
|
@@ -102,11 +128,13 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
102
128
|
.withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
|
|
103
129
|
.build();
|
|
104
130
|
|
|
105
|
-
const user = await fragments.test.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
const user = await fragments.test.fragment.callServices(() =>
|
|
132
|
+
fragments.test.services.createUser({
|
|
133
|
+
name: "Memory User",
|
|
134
|
+
email: "memory@example.com",
|
|
135
|
+
age: 31,
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
110
138
|
|
|
111
139
|
expect(user).toMatchObject({
|
|
112
140
|
id: expect.any(String),
|
|
@@ -115,7 +143,9 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
115
143
|
age: 31,
|
|
116
144
|
});
|
|
117
145
|
|
|
118
|
-
const users = await fragments.test.
|
|
146
|
+
const users = await fragments.test.fragment.callServices(() =>
|
|
147
|
+
fragments.test.services.getUsers(),
|
|
148
|
+
);
|
|
119
149
|
expect(users).toHaveLength(1);
|
|
120
150
|
expect(users[0]).toMatchObject(user);
|
|
121
151
|
|
|
@@ -139,20 +169,26 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
139
169
|
];
|
|
140
170
|
|
|
141
171
|
for (const user of users) {
|
|
142
|
-
await fragment.services.createUser(user);
|
|
172
|
+
await fragment.fragment.callServices(() => fragment.services.createUser(user));
|
|
143
173
|
}
|
|
144
174
|
|
|
145
|
-
const firstPage = await fragment.
|
|
175
|
+
const firstPage = await fragment.fragment.callServices(() =>
|
|
176
|
+
fragment.services.getUsersWithCursor(),
|
|
177
|
+
);
|
|
146
178
|
expect(firstPage.items.map((item) => item.name)).toEqual(["Alice", "Brett"]);
|
|
147
179
|
expect(firstPage.hasNextPage).toBe(true);
|
|
148
180
|
expect(firstPage.cursor).toBeDefined();
|
|
149
181
|
|
|
150
|
-
const secondPage = await fragment.
|
|
182
|
+
const secondPage = await fragment.fragment.callServices(() =>
|
|
183
|
+
fragment.services.getUsersWithCursor(firstPage.cursor),
|
|
184
|
+
);
|
|
151
185
|
expect(secondPage.items.map((item) => item.name)).toEqual(["Cora", "Dylan"]);
|
|
152
186
|
expect(secondPage.hasNextPage).toBe(true);
|
|
153
187
|
expect(secondPage.cursor).toBeDefined();
|
|
154
188
|
|
|
155
|
-
const thirdPage = await fragment.
|
|
189
|
+
const thirdPage = await fragment.fragment.callServices(() =>
|
|
190
|
+
fragment.services.getUsersWithCursor(secondPage.cursor),
|
|
191
|
+
);
|
|
156
192
|
expect(thirdPage.items.map((item) => item.name)).toEqual(["Emma"]);
|
|
157
193
|
expect(thirdPage.hasNextPage).toBe(false);
|
|
158
194
|
expect(thirdPage.cursor).toBeUndefined();
|
|
@@ -169,28 +205,30 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
169
205
|
const fragment = fragments.test;
|
|
170
206
|
|
|
171
207
|
// Create some users
|
|
172
|
-
await fragment.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
208
|
+
await fragment.fragment.callServices(() =>
|
|
209
|
+
fragment.services.createUser({
|
|
210
|
+
name: "User 1",
|
|
211
|
+
email: "user1@example.com",
|
|
212
|
+
age: 25,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
177
215
|
|
|
178
216
|
// Verify users exist
|
|
179
|
-
let users = await fragment.services.getUsers();
|
|
217
|
+
let users = await fragment.fragment.callServices(() => fragment.services.getUsers());
|
|
180
218
|
expect(users).toHaveLength(1);
|
|
181
219
|
|
|
182
220
|
// Reset the database
|
|
183
221
|
await test.resetDatabase();
|
|
184
222
|
|
|
185
223
|
// Verify database is empty
|
|
186
|
-
users = await fragment.services.getUsers();
|
|
224
|
+
users = await fragment.fragment.callServices(() => fragment.services.getUsers());
|
|
187
225
|
expect(users).toHaveLength(0);
|
|
188
226
|
|
|
189
227
|
// Cleanup
|
|
190
228
|
await test.cleanup();
|
|
191
229
|
});
|
|
192
230
|
|
|
193
|
-
it("should
|
|
231
|
+
it("should allow handlerTx direct ORM queries", async () => {
|
|
194
232
|
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
195
233
|
.withTestAdapter({ type: "kysely-sqlite" })
|
|
196
234
|
.withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
|
|
@@ -198,20 +236,34 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
198
236
|
|
|
199
237
|
const fragment = fragments.test;
|
|
200
238
|
|
|
201
|
-
// Test creating a record directly using
|
|
202
|
-
const userId = await fragment.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
239
|
+
// Test creating a record directly using handlerTx
|
|
240
|
+
const userId = await fragment.fragment.inContext(async function () {
|
|
241
|
+
return await this.handlerTx()
|
|
242
|
+
.mutate(({ forSchema }) =>
|
|
243
|
+
forSchema(testSchema).create("users", {
|
|
244
|
+
name: "Direct DB User",
|
|
245
|
+
email: "direct@example.com",
|
|
246
|
+
age: 28,
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
249
|
+
.transform(({ mutateResult }) => mutateResult)
|
|
250
|
+
.execute();
|
|
206
251
|
});
|
|
207
252
|
|
|
208
253
|
expect(userId).toBeDefined();
|
|
209
254
|
expect(typeof userId.valueOf()).toBe("string");
|
|
210
255
|
|
|
211
|
-
// Test finding records using
|
|
212
|
-
const users = await fragment.
|
|
213
|
-
|
|
214
|
-
|
|
256
|
+
// Test finding records using handlerTx
|
|
257
|
+
const users = await fragment.fragment.inContext(async function () {
|
|
258
|
+
return await this.handlerTx()
|
|
259
|
+
.retrieve(({ forSchema }) =>
|
|
260
|
+
forSchema(testSchema).find("users", (b) =>
|
|
261
|
+
b.whereIndex("idx_users_all", (eb) => eb("id", "=", userId)),
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
.transformRetrieve(([result]) => result)
|
|
265
|
+
.execute();
|
|
266
|
+
});
|
|
215
267
|
|
|
216
268
|
expect(users).toHaveLength(1);
|
|
217
269
|
expect(users[0]).toMatchObject({
|
|
@@ -246,20 +298,32 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
246
298
|
|
|
247
299
|
const authFragmentDef = defineFragment<{}>("auth-test")
|
|
248
300
|
.extend(withDatabase(authSchema))
|
|
249
|
-
.providesBaseService(({
|
|
250
|
-
|
|
251
|
-
createUser:
|
|
252
|
-
|
|
253
|
-
|
|
301
|
+
.providesBaseService(({ defineService }) =>
|
|
302
|
+
defineService({
|
|
303
|
+
createUser: function (email: string, passwordHash: string) {
|
|
304
|
+
return this.serviceTx(authSchema)
|
|
305
|
+
.mutate(({ uow }) => uow.create("user", { email, passwordHash }))
|
|
306
|
+
.transform(({ mutateResult }) => ({
|
|
307
|
+
id: mutateResult.valueOf(),
|
|
308
|
+
email,
|
|
309
|
+
passwordHash,
|
|
310
|
+
}))
|
|
311
|
+
.build();
|
|
254
312
|
},
|
|
255
|
-
createSession:
|
|
313
|
+
createSession: function (userId: string) {
|
|
256
314
|
const expiresAt = new Date();
|
|
257
315
|
expiresAt.setDate(expiresAt.getDate() + 30);
|
|
258
|
-
|
|
259
|
-
|
|
316
|
+
return this.serviceTx(authSchema)
|
|
317
|
+
.mutate(({ uow }) => uow.create("session", { userId, expiresAt }))
|
|
318
|
+
.transform(({ mutateResult }) => ({
|
|
319
|
+
id: mutateResult.valueOf(),
|
|
320
|
+
userId,
|
|
321
|
+
expiresAt,
|
|
322
|
+
}))
|
|
323
|
+
.build();
|
|
260
324
|
},
|
|
261
|
-
}
|
|
262
|
-
|
|
325
|
+
}),
|
|
326
|
+
)
|
|
263
327
|
.build();
|
|
264
328
|
|
|
265
329
|
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
@@ -270,7 +334,9 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
270
334
|
const fragment = fragments.auth;
|
|
271
335
|
|
|
272
336
|
// Create a user
|
|
273
|
-
const user = await fragment.
|
|
337
|
+
const user = await fragment.fragment.callServices(() =>
|
|
338
|
+
fragment.services.createUser("test@test.com", "hashed-password"),
|
|
339
|
+
);
|
|
274
340
|
expect(user).toMatchObject({
|
|
275
341
|
id: expect.any(String),
|
|
276
342
|
email: "test@test.com",
|
|
@@ -278,7 +344,9 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
278
344
|
});
|
|
279
345
|
|
|
280
346
|
// Create a session for the user
|
|
281
|
-
const session = await fragment.
|
|
347
|
+
const session = await fragment.fragment.callServices(() =>
|
|
348
|
+
fragment.services.createSession(user.id),
|
|
349
|
+
);
|
|
282
350
|
expect(session).toMatchObject({
|
|
283
351
|
id: expect.any(String),
|
|
284
352
|
userId: user.id,
|
|
@@ -313,38 +381,46 @@ describe("multi-fragment tests", () => {
|
|
|
313
381
|
|
|
314
382
|
const userFragmentDef = defineFragment<{}>("user-fragment")
|
|
315
383
|
.extend(withDatabase(userSchema))
|
|
316
|
-
.providesBaseService(({
|
|
317
|
-
|
|
318
|
-
createUser:
|
|
319
|
-
|
|
320
|
-
|
|
384
|
+
.providesBaseService(({ defineService }) =>
|
|
385
|
+
defineService({
|
|
386
|
+
createUser: function (data: { name: string; email: string }) {
|
|
387
|
+
return this.serviceTx(userSchema)
|
|
388
|
+
.mutate(({ uow }) => uow.create("user", data))
|
|
389
|
+
.transform(({ mutateResult }) => ({ ...data, id: mutateResult.valueOf() }))
|
|
390
|
+
.build();
|
|
321
391
|
},
|
|
322
|
-
getUsers:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
392
|
+
getUsers: function () {
|
|
393
|
+
return this.serviceTx(userSchema)
|
|
394
|
+
.retrieve((uow) =>
|
|
395
|
+
uow.find("user", (b) => b.whereIndex("idx_user_all", (eb) => eb("id", "!=", ""))),
|
|
396
|
+
)
|
|
397
|
+
.transformRetrieve(([users]) => users.map((u) => ({ ...u, id: u.id.valueOf() })))
|
|
398
|
+
.build();
|
|
327
399
|
},
|
|
328
|
-
}
|
|
329
|
-
|
|
400
|
+
}),
|
|
401
|
+
)
|
|
330
402
|
.build();
|
|
331
403
|
|
|
332
404
|
const postFragmentDef = defineFragment<{}>("post-fragment")
|
|
333
405
|
.extend(withDatabase(postSchema))
|
|
334
|
-
.providesBaseService(({
|
|
335
|
-
|
|
336
|
-
createPost:
|
|
337
|
-
|
|
338
|
-
|
|
406
|
+
.providesBaseService(({ defineService }) =>
|
|
407
|
+
defineService({
|
|
408
|
+
createPost: function (data: { title: string; userId: string }) {
|
|
409
|
+
return this.serviceTx(postSchema)
|
|
410
|
+
.mutate(({ uow }) => uow.create("post", data))
|
|
411
|
+
.transform(({ mutateResult }) => ({ ...data, id: mutateResult.valueOf() }))
|
|
412
|
+
.build();
|
|
339
413
|
},
|
|
340
|
-
getPosts:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
414
|
+
getPosts: function () {
|
|
415
|
+
return this.serviceTx(postSchema)
|
|
416
|
+
.retrieve((uow) =>
|
|
417
|
+
uow.find("post", (b) => b.whereIndex("idx_post_all", (eb) => eb("id", "!=", ""))),
|
|
418
|
+
)
|
|
419
|
+
.transformRetrieve(([posts]) => posts.map((p) => ({ ...p, id: p.id.valueOf() })))
|
|
420
|
+
.build();
|
|
345
421
|
},
|
|
346
|
-
}
|
|
347
|
-
|
|
422
|
+
}),
|
|
423
|
+
)
|
|
348
424
|
.build();
|
|
349
425
|
|
|
350
426
|
const adapters = [
|
|
@@ -363,10 +439,12 @@ describe("multi-fragment tests", () => {
|
|
|
363
439
|
.build();
|
|
364
440
|
|
|
365
441
|
// Create a user
|
|
366
|
-
const user = await fragments.user.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
442
|
+
const user = await fragments.user.fragment.callServices(() =>
|
|
443
|
+
fragments.user.services.createUser({
|
|
444
|
+
name: "John Doe",
|
|
445
|
+
email: "john@example.com",
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
370
448
|
|
|
371
449
|
expect(user).toMatchObject({
|
|
372
450
|
id: expect.any(String),
|
|
@@ -375,10 +453,12 @@ describe("multi-fragment tests", () => {
|
|
|
375
453
|
});
|
|
376
454
|
|
|
377
455
|
// Create a post with the user's ID
|
|
378
|
-
const post = await fragments.post.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
456
|
+
const post = await fragments.post.fragment.callServices(() =>
|
|
457
|
+
fragments.post.services.createPost({
|
|
458
|
+
title: "My First Post",
|
|
459
|
+
userId: user.id,
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
382
462
|
|
|
383
463
|
expect(post).toMatchObject({
|
|
384
464
|
id: expect.any(String),
|
|
@@ -387,10 +467,14 @@ describe("multi-fragment tests", () => {
|
|
|
387
467
|
});
|
|
388
468
|
|
|
389
469
|
// Verify data exists
|
|
390
|
-
const users = await fragments.user.
|
|
470
|
+
const users = await fragments.user.fragment.callServices(() =>
|
|
471
|
+
fragments.user.services.getUsers(),
|
|
472
|
+
);
|
|
391
473
|
expect(users).toHaveLength(1);
|
|
392
474
|
|
|
393
|
-
const posts = await fragments.post.
|
|
475
|
+
const posts = await fragments.post.fragment.callServices(() =>
|
|
476
|
+
fragments.post.services.getPosts(),
|
|
477
|
+
);
|
|
394
478
|
expect(posts).toHaveLength(1);
|
|
395
479
|
expect(posts[0]!.userId).toBe(user.id);
|
|
396
480
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import type { SimpleQueryInterface } from "@fragno-dev/db/query";
|
|
1
2
|
import type { AnySchema } from "@fragno-dev/db/schema";
|
|
3
|
+
|
|
4
|
+
import type { DatabaseAdapter } from "@fragno-dev/db";
|
|
5
|
+
|
|
2
6
|
import type {
|
|
3
7
|
SupportedAdapter,
|
|
4
8
|
AdapterContext,
|
|
@@ -7,8 +11,6 @@ import type {
|
|
|
7
11
|
DrizzlePgliteAdapter,
|
|
8
12
|
InMemoryAdapterConfig,
|
|
9
13
|
} from "./adapters";
|
|
10
|
-
import type { DatabaseAdapter } from "@fragno-dev/db";
|
|
11
|
-
import type { SimpleQueryInterface } from "@fragno-dev/db/query";
|
|
12
14
|
|
|
13
15
|
// Re-export utilities from @fragno-dev/core/test
|
|
14
16
|
export {
|
|
@@ -29,7 +31,9 @@ export type {
|
|
|
29
31
|
|
|
30
32
|
// Re-export new builder-based database test utilities
|
|
31
33
|
export { buildDatabaseFragmentsTest, DatabaseFragmentsTestBuilder } from "./db-test";
|
|
34
|
+
export type { AnyFragmentResult } from "./db-test";
|
|
32
35
|
export { drainDurableHooks } from "./durable-hooks";
|
|
36
|
+
export type { DrainDurableHooksMode, DrainDurableHooksOptions } from "./durable-hooks";
|
|
33
37
|
export {
|
|
34
38
|
runModelChecker,
|
|
35
39
|
defaultStateHasher,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { column, idColumn, schema } from "@fragno-dev/db/schema";
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import { InMemoryAdapter } from "@fragno-dev/db";
|
|
6
|
+
|
|
5
7
|
import { runModelCheckerWithActors } from "./model-checker-actors";
|
|
8
|
+
import { ModelCheckerAdapter } from "./model-checker-adapter";
|
|
6
9
|
|
|
7
10
|
const testSchema = schema("test", (s) =>
|
|
8
11
|
s.addTable("items", (t) => t.addColumn("id", idColumn()).addColumn("name", column("string"))),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { AnySchema } from "@fragno-dev/db/schema";
|
|
2
1
|
import type { SimpleQueryInterface } from "@fragno-dev/db/query";
|
|
2
|
+
import type { AnySchema } from "@fragno-dev/db/schema";
|
|
3
3
|
import type { UOWInstrumentationContext } from "@fragno-dev/db/unit-of-work";
|
|
4
|
+
|
|
4
5
|
import {
|
|
5
6
|
defaultStateHasher,
|
|
6
7
|
type ModelCheckerBounds,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestContextStorage } from "@fragno-dev/core/internal/request-context-storage";
|
|
1
2
|
import type {
|
|
2
3
|
DatabaseAdapter,
|
|
3
4
|
DatabaseAdapterMetadata,
|
|
@@ -7,15 +8,15 @@ import {
|
|
|
7
8
|
fragnoDatabaseAdapterNameFakeSymbol,
|
|
8
9
|
fragnoDatabaseAdapterVersionFakeSymbol,
|
|
9
10
|
} from "@fragno-dev/db/adapters";
|
|
10
|
-
import type { AnySchema } from "@fragno-dev/db/schema";
|
|
11
11
|
import type { SimpleQueryInterface } from "@fragno-dev/db/query";
|
|
12
|
-
import type {
|
|
12
|
+
import type { AnySchema } from "@fragno-dev/db/schema";
|
|
13
13
|
import type {
|
|
14
14
|
UOWInstrumentation,
|
|
15
15
|
UOWInstrumentationContext,
|
|
16
16
|
UOWInstrumentationInjection,
|
|
17
17
|
UOWInstrumentationFinalizer,
|
|
18
18
|
} from "@fragno-dev/db/unit-of-work";
|
|
19
|
+
|
|
19
20
|
import type { ModelCheckerPhase } from "./model-checker";
|
|
20
21
|
|
|
21
22
|
type SchedulerHook = (ctx: UOWInstrumentationContext, phase: ModelCheckerPhase) => Promise<void>;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { column, idColumn, schema, type FragnoId } from "@fragno-dev/db/schema";
|
|
4
4
|
import type { UnitOfWorkConfig } from "@fragno-dev/db/unit-of-work";
|
|
5
|
+
|
|
6
|
+
import { InMemoryAdapter } from "@fragno-dev/db";
|
|
7
|
+
|
|
5
8
|
import {
|
|
6
9
|
createRawUowTransaction,
|
|
7
10
|
defaultStateHasher,
|
package/src/model-checker.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import type { FragnoRuntime } from "@fragno-dev/core";
|
|
2
1
|
import {
|
|
3
2
|
runWithTraceRecorder,
|
|
4
3
|
type FragnoCoreTraceEvent,
|
|
5
4
|
} from "@fragno-dev/core/internal/trace-context";
|
|
6
|
-
import { FragnoId, FragnoReference, type AnySchema } from "@fragno-dev/db/schema";
|
|
7
5
|
import type { SimpleQueryInterface } from "@fragno-dev/db/query";
|
|
8
|
-
import type
|
|
6
|
+
import { FragnoId, FragnoReference, type AnySchema } from "@fragno-dev/db/schema";
|
|
9
7
|
import type { MutationOperation } from "@fragno-dev/db/unit-of-work";
|
|
10
8
|
|
|
9
|
+
import type { FragnoRuntime } from "@fragno-dev/core";
|
|
10
|
+
import type { TypedUnitOfWork } from "@fragno-dev/db";
|
|
11
|
+
|
|
11
12
|
export type ModelCheckerMode = "exhaustive" | "bounded-exhaustive" | "random" | "infinite";
|
|
12
13
|
export type ModelCheckerPhase = "retrieve" | "mutate";
|
|
13
14
|
|