@fragno-dev/test 0.1.11 → 0.1.13
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 +15 -11
- package/CHANGELOG.md +35 -0
- package/dist/adapters.d.ts +3 -11
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +65 -50
- package/dist/adapters.js.map +1 -1
- package/dist/db-test.d.ts +129 -0
- package/dist/db-test.d.ts.map +1 -0
- package/dist/db-test.js +214 -0
- package/dist/db-test.js.map +1 -0
- package/dist/index.d.ts +23 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -91
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
- package/src/adapters.ts +137 -118
- package/src/db-test.test.ts +352 -0
- package/src/db-test.ts +574 -0
- package/src/index.test.ts +287 -546
- package/src/index.ts +39 -241
package/src/index.test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
2
|
import { column, idColumn, schema } from "@fragno-dev/db/schema";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { withDatabase } from "@fragno-dev/db";
|
|
4
|
+
import { defineFragment } from "@fragno-dev/core";
|
|
5
|
+
import { instantiate } from "@fragno-dev/core";
|
|
6
|
+
import { buildDatabaseFragmentsTest } from "./db-test";
|
|
7
|
+
import type { ExtractFragmentServices } from "@fragno-dev/core/route";
|
|
8
8
|
|
|
9
9
|
// Test schema with multiple versions
|
|
10
10
|
const testSchema = schema((s) => {
|
|
@@ -22,518 +22,132 @@ const testSchema = schema((s) => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
// Test fragment definition
|
|
25
|
-
const testFragmentDef =
|
|
26
|
-
.withDatabase(testSchema)
|
|
27
|
-
.
|
|
25
|
+
const testFragmentDef = defineFragment<{}>("test-fragment")
|
|
26
|
+
.extend(withDatabase(testSchema))
|
|
27
|
+
.providesBaseService(({ deps }) => {
|
|
28
28
|
return {
|
|
29
29
|
createUser: async (data: { name: string; email: string; age?: number | null }) => {
|
|
30
|
-
const id = await
|
|
30
|
+
const id = await deps.db.create("users", data);
|
|
31
31
|
return { ...data, id: id.valueOf() };
|
|
32
32
|
},
|
|
33
33
|
getUsers: async () => {
|
|
34
|
-
const users = await
|
|
34
|
+
const users = await deps.db.find("users", (b) =>
|
|
35
35
|
b.whereIndex("idx_users_all", (eb) => eb("id", "!=", "")),
|
|
36
36
|
);
|
|
37
37
|
return users.map((u) => ({ ...u, id: u.id.valueOf() }));
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
it("should use in-memory database by default", async () => {
|
|
58
|
-
const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, [], {
|
|
59
|
-
adapter: { type: "kysely-sqlite" },
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Should be able to create and query users
|
|
63
|
-
const user = await fragment.services.createUser({
|
|
64
|
-
name: "Test User",
|
|
65
|
-
email: "test@example.com",
|
|
66
|
-
age: 25,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
expect(user).toMatchObject({
|
|
70
|
-
id: expect.any(String),
|
|
71
|
-
name: "Test User",
|
|
72
|
-
email: "test@example.com",
|
|
73
|
-
age: 25,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const users = await fragment.services.getUsers();
|
|
77
|
-
expect(users).toHaveLength(1);
|
|
78
|
-
expect(users[0]).toMatchObject(user);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("should create database at specified path", async () => {
|
|
82
|
-
const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, [], {
|
|
83
|
-
adapter: { type: "kysely-sqlite", databasePath: testDbPath },
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Create a user
|
|
87
|
-
await fragment.services.createUser({
|
|
88
|
-
name: "Persisted User",
|
|
89
|
-
email: "persisted@example.com",
|
|
90
|
-
age: 30,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Verify data exists in this instance
|
|
94
|
-
const users = await fragment.services.getUsers();
|
|
95
|
-
expect(users).toHaveLength(1);
|
|
96
|
-
expect(users[0]).toMatchObject({
|
|
97
|
-
name: "Persisted User",
|
|
98
|
-
email: "persisted@example.com",
|
|
99
|
-
age: 30,
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("migrateToVersion option", () => {
|
|
105
|
-
it("should migrate to latest version by default", async () => {
|
|
106
|
-
const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, [], {
|
|
107
|
-
adapter: { type: "kysely-sqlite" },
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Should have the 'age' column from version 2
|
|
111
|
-
const user = await fragment.services.createUser({
|
|
112
|
-
name: "Test User",
|
|
113
|
-
email: "test@example.com",
|
|
114
|
-
age: 25,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
expect(user).toMatchObject({
|
|
118
|
-
id: expect.any(String),
|
|
119
|
-
name: "Test User",
|
|
120
|
-
email: "test@example.com",
|
|
121
|
-
age: 25,
|
|
122
|
-
});
|
|
40
|
+
})
|
|
41
|
+
.build();
|
|
42
|
+
|
|
43
|
+
describe("buildDatabaseFragmentsTest", () => {
|
|
44
|
+
it("should create and use a database fragment", async () => {
|
|
45
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
46
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
47
|
+
.withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
|
|
48
|
+
.build();
|
|
49
|
+
|
|
50
|
+
const fragment = fragments.test;
|
|
51
|
+
|
|
52
|
+
// Should be able to create and query users
|
|
53
|
+
const user = await fragment.services.createUser({
|
|
54
|
+
name: "Test User",
|
|
55
|
+
email: "test@example.com",
|
|
56
|
+
age: 25,
|
|
123
57
|
});
|
|
124
58
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// Query the database directly to check schema
|
|
133
|
-
// In version 1, we should be able to insert without the age column
|
|
134
|
-
const tableName = "users_test-fragment-db";
|
|
135
|
-
await test.kysely
|
|
136
|
-
.insertInto(tableName)
|
|
137
|
-
.values({
|
|
138
|
-
id: "test-id-1",
|
|
139
|
-
name: "V1 User",
|
|
140
|
-
email: "v1@example.com",
|
|
141
|
-
})
|
|
142
|
-
.execute();
|
|
143
|
-
|
|
144
|
-
const result = await test.kysely.selectFrom(tableName).selectAll().execute();
|
|
145
|
-
|
|
146
|
-
expect(result).toHaveLength(1);
|
|
147
|
-
expect(result[0]).toMatchObject({
|
|
148
|
-
id: "test-id-1",
|
|
149
|
-
name: "V1 User",
|
|
150
|
-
email: "v1@example.com",
|
|
151
|
-
});
|
|
152
|
-
// In version 1, the age column should not exist
|
|
153
|
-
expect(result[0]).not.toHaveProperty("age");
|
|
59
|
+
expect(user).toMatchObject({
|
|
60
|
+
id: expect.any(String),
|
|
61
|
+
name: "Test User",
|
|
62
|
+
email: "test@example.com",
|
|
63
|
+
age: 25,
|
|
154
64
|
});
|
|
155
65
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
adapter: { type: "kysely-sqlite" },
|
|
160
|
-
migrateToVersion: 2,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Should be able to use age column
|
|
164
|
-
const user = await fragment.services.createUser({
|
|
165
|
-
name: "V2 User",
|
|
166
|
-
email: "v2@example.com",
|
|
167
|
-
age: 35,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
expect(user).toMatchObject({
|
|
171
|
-
id: expect.any(String),
|
|
172
|
-
name: "V2 User",
|
|
173
|
-
email: "v2@example.com",
|
|
174
|
-
age: 35,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const tableName = "users_test-fragment-db";
|
|
178
|
-
const result = await test.kysely.selectFrom(tableName).selectAll().execute();
|
|
66
|
+
const users = await fragment.services.getUsers();
|
|
67
|
+
expect(users).toHaveLength(1);
|
|
68
|
+
expect(users[0]).toMatchObject(user);
|
|
179
69
|
|
|
180
|
-
|
|
181
|
-
expect(result[0]).toMatchObject({
|
|
182
|
-
name: "V2 User",
|
|
183
|
-
email: "v2@example.com",
|
|
184
|
-
age: 35,
|
|
185
|
-
});
|
|
186
|
-
});
|
|
70
|
+
await test.cleanup();
|
|
187
71
|
});
|
|
188
72
|
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("should work with both databasePath and migrateToVersion", async () => {
|
|
203
|
-
const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, [], {
|
|
204
|
-
adapter: { type: "kysely-sqlite", databasePath: testDbPath },
|
|
205
|
-
migrateToVersion: 2,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Create user at version 2 (with age support)
|
|
209
|
-
const user = await fragment.services.createUser({
|
|
210
|
-
name: "Combined Test",
|
|
211
|
-
email: "combined@example.com",
|
|
212
|
-
age: 40,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
expect(user).toMatchObject({
|
|
216
|
-
id: expect.any(String),
|
|
217
|
-
name: "Combined Test",
|
|
218
|
-
email: "combined@example.com",
|
|
219
|
-
age: 40,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const users = await fragment.services.getUsers();
|
|
223
|
-
expect(users).toHaveLength(1);
|
|
224
|
-
expect(users[0]).toMatchObject({
|
|
225
|
-
name: "Combined Test",
|
|
226
|
-
email: "combined@example.com",
|
|
227
|
-
age: 40,
|
|
228
|
-
});
|
|
229
|
-
});
|
|
73
|
+
it("should throw error for non-database fragment", async () => {
|
|
74
|
+
const nonDbFragmentDef = defineFragment<{}>("non-db-fragment")
|
|
75
|
+
.providesBaseService(() => ({}))
|
|
76
|
+
.build();
|
|
77
|
+
|
|
78
|
+
await expect(
|
|
79
|
+
buildDatabaseFragmentsTest()
|
|
80
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
81
|
+
.withFragment("nonDb", instantiate(nonDbFragmentDef).withConfig({}).withRoutes([]))
|
|
82
|
+
.build(),
|
|
83
|
+
).rejects.toThrow("Fragment 'non-db-fragment' does not have a database schema");
|
|
230
84
|
});
|
|
231
85
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
expect(test.kysely).toBeDefined();
|
|
239
|
-
expect(typeof test.kysely.selectFrom).toBe("function");
|
|
240
|
-
});
|
|
86
|
+
it("should reset database by truncating tables", async () => {
|
|
87
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
88
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
89
|
+
.withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
|
|
90
|
+
.build();
|
|
241
91
|
|
|
242
|
-
|
|
243
|
-
const { test } = await createDatabaseFragmentForTest(testFragmentDef, [], {
|
|
244
|
-
adapter: { type: "kysely-sqlite" },
|
|
245
|
-
});
|
|
92
|
+
const fragment = fragments.test;
|
|
246
93
|
|
|
247
|
-
|
|
248
|
-
|
|
94
|
+
// Create some users
|
|
95
|
+
await fragment.services.createUser({
|
|
96
|
+
name: "User 1",
|
|
97
|
+
email: "user1@example.com",
|
|
98
|
+
age: 25,
|
|
249
99
|
});
|
|
250
100
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
});
|
|
101
|
+
// Verify users exist
|
|
102
|
+
let users = await fragment.services.getUsers();
|
|
103
|
+
expect(users).toHaveLength(1);
|
|
255
104
|
|
|
256
|
-
|
|
257
|
-
|
|
105
|
+
// Reset the database
|
|
106
|
+
await test.resetDatabase();
|
|
258
107
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
definition: {
|
|
263
|
-
name: "non-db-fragment",
|
|
264
|
-
additionalContext: {},
|
|
265
|
-
},
|
|
266
|
-
$requiredOptions: {},
|
|
267
|
-
};
|
|
108
|
+
// Verify database is empty
|
|
109
|
+
users = await fragment.services.getUsers();
|
|
110
|
+
expect(users).toHaveLength(0);
|
|
268
111
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
createDatabaseFragmentForTest(nonDbFragment as any, [], {
|
|
272
|
-
adapter: { type: "kysely-sqlite" },
|
|
273
|
-
}),
|
|
274
|
-
).rejects.toThrow("Fragment 'non-db-fragment' does not have a database schema");
|
|
275
|
-
});
|
|
112
|
+
// Cleanup
|
|
113
|
+
await test.cleanup();
|
|
276
114
|
});
|
|
277
115
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
type
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
createUser: (data: { name: string; email: string; age?: number | null }) => Promise<{
|
|
284
|
-
name: string;
|
|
285
|
-
email: string;
|
|
286
|
-
age?: number | null;
|
|
287
|
-
id: string;
|
|
288
|
-
}>;
|
|
289
|
-
getUsers: () => Promise<{ name: string; email: string; age: number | null; id: string }[]>;
|
|
290
|
-
};
|
|
116
|
+
it("should expose db property for direct ORM queries", async () => {
|
|
117
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
118
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
119
|
+
.withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
|
|
120
|
+
.build();
|
|
291
121
|
|
|
292
|
-
|
|
293
|
-
defineRoute({
|
|
294
|
-
method: "POST",
|
|
295
|
-
path: "/users",
|
|
296
|
-
inputSchema: z.object({
|
|
297
|
-
name: z.string(),
|
|
298
|
-
email: z.string(),
|
|
299
|
-
age: z.number().nullable().optional(),
|
|
300
|
-
}),
|
|
301
|
-
outputSchema: z.object({
|
|
302
|
-
id: z.string(),
|
|
303
|
-
name: z.string(),
|
|
304
|
-
email: z.string(),
|
|
305
|
-
age: z.number().nullable().optional(),
|
|
306
|
-
}),
|
|
307
|
-
handler: async ({ input }, { json }) => {
|
|
308
|
-
if (input) {
|
|
309
|
-
const data = await input.valid();
|
|
310
|
-
const user = await services.createUser(data);
|
|
311
|
-
return json(user);
|
|
312
|
-
}
|
|
313
|
-
return json({ id: "", name: "", email: "", age: null });
|
|
314
|
-
},
|
|
315
|
-
}),
|
|
316
|
-
defineRoute({
|
|
317
|
-
method: "GET",
|
|
318
|
-
path: "/users",
|
|
319
|
-
outputSchema: z.array(
|
|
320
|
-
z.object({
|
|
321
|
-
id: z.string(),
|
|
322
|
-
name: z.string(),
|
|
323
|
-
email: z.string(),
|
|
324
|
-
age: z.number().nullable(),
|
|
325
|
-
}),
|
|
326
|
-
),
|
|
327
|
-
handler: async (_ctx, { json }) => {
|
|
328
|
-
const users = await services.getUsers();
|
|
329
|
-
return json(users);
|
|
330
|
-
},
|
|
331
|
-
}),
|
|
332
|
-
]);
|
|
122
|
+
const fragment = fragments.test;
|
|
333
123
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const createResponse = await fragment.callRoute("POST", "/users", {
|
|
340
|
-
body: { name: "John Doe", email: "john@example.com", age: 30 },
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
expect(createResponse.type).toBe("json");
|
|
344
|
-
if (createResponse.type === "json") {
|
|
345
|
-
expect(createResponse.data).toMatchObject({
|
|
346
|
-
id: expect.any(String),
|
|
347
|
-
name: "John Doe",
|
|
348
|
-
email: "john@example.com",
|
|
349
|
-
age: 30,
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Test getting users
|
|
354
|
-
const getUsersResponse = await fragment.callRoute("GET", "/users");
|
|
355
|
-
|
|
356
|
-
expect(getUsersResponse.type).toBe("json");
|
|
357
|
-
if (getUsersResponse.type === "json") {
|
|
358
|
-
expect(getUsersResponse.data).toHaveLength(1);
|
|
359
|
-
expect(getUsersResponse.data[0]).toMatchObject({
|
|
360
|
-
id: expect.any(String),
|
|
361
|
-
name: "John Doe",
|
|
362
|
-
email: "john@example.com",
|
|
363
|
-
age: 30,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
124
|
+
// Test creating a record directly using test.db
|
|
125
|
+
const userId = await fragment.db.create("users", {
|
|
126
|
+
name: "Direct DB User",
|
|
127
|
+
email: "direct@example.com",
|
|
128
|
+
age: 28,
|
|
366
129
|
});
|
|
367
|
-
});
|
|
368
130
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
{ name: "Kysely SQLite", adapter: { type: "kysely-sqlite" as const } },
|
|
372
|
-
{ name: "Kysely PGLite", adapter: { type: "kysely-pglite" as const } },
|
|
373
|
-
{ name: "Drizzle PGLite", adapter: { type: "drizzle-pglite" as const } },
|
|
374
|
-
];
|
|
375
|
-
|
|
376
|
-
for (const { name, adapter } of adapters) {
|
|
377
|
-
describe(name, () => {
|
|
378
|
-
it("should clear all data and recreate a fresh database", async () => {
|
|
379
|
-
// Don't destructure so we can access the updated fragment through getters after reset
|
|
380
|
-
const result = await createDatabaseFragmentForTest(testFragmentDef, [], {
|
|
381
|
-
adapter,
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// Create some users
|
|
385
|
-
await result.services.createUser({
|
|
386
|
-
name: "User 1",
|
|
387
|
-
email: "user1@example.com",
|
|
388
|
-
age: 25,
|
|
389
|
-
});
|
|
390
|
-
await result.services.createUser({
|
|
391
|
-
name: "User 2",
|
|
392
|
-
email: "user2@example.com",
|
|
393
|
-
age: 30,
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
// Verify users exist
|
|
397
|
-
let users = await result.services.getUsers();
|
|
398
|
-
expect(users).toHaveLength(2);
|
|
399
|
-
|
|
400
|
-
// Reset the database
|
|
401
|
-
await result.test.resetDatabase();
|
|
402
|
-
|
|
403
|
-
// Verify database is empty (accessing through result to get updated fragment)
|
|
404
|
-
users = await result.services.getUsers();
|
|
405
|
-
expect(users).toHaveLength(0);
|
|
406
|
-
|
|
407
|
-
// Verify we can still create new users after reset
|
|
408
|
-
const newUser = await result.services.createUser({
|
|
409
|
-
name: "User After Reset",
|
|
410
|
-
email: "after@example.com",
|
|
411
|
-
age: 35,
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
expect(newUser).toMatchObject({
|
|
415
|
-
id: expect.any(String),
|
|
416
|
-
name: "User After Reset",
|
|
417
|
-
email: "after@example.com",
|
|
418
|
-
age: 35,
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
users = await result.services.getUsers();
|
|
422
|
-
expect(users).toHaveLength(1);
|
|
423
|
-
expect(users[0]).toMatchObject(newUser);
|
|
424
|
-
|
|
425
|
-
// Cleanup
|
|
426
|
-
await result.test.cleanup();
|
|
427
|
-
}, 10000);
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
});
|
|
131
|
+
expect(userId).toBeDefined();
|
|
132
|
+
expect(typeof userId.valueOf()).toBe("string");
|
|
431
133
|
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
{ name: "Drizzle PGLite", adapter: { type: "drizzle-pglite" as const } },
|
|
437
|
-
];
|
|
438
|
-
|
|
439
|
-
for (const { name, adapter } of adapters) {
|
|
440
|
-
describe(name, () => {
|
|
441
|
-
it("should expose db property for direct ORM queries", async () => {
|
|
442
|
-
const { test } = await createDatabaseFragmentForTest(testFragmentDef, [], {
|
|
443
|
-
adapter,
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// Test creating a record directly using test.db
|
|
447
|
-
const userId = await test.db.create("users", {
|
|
448
|
-
name: "Direct DB User",
|
|
449
|
-
email: "direct@example.com",
|
|
450
|
-
age: 28,
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
expect(userId).toBeDefined();
|
|
454
|
-
expect(typeof userId.valueOf()).toBe("string");
|
|
455
|
-
|
|
456
|
-
// Test finding records using test.db
|
|
457
|
-
const users = await test.db.find("users", (b) =>
|
|
458
|
-
b.whereIndex("idx_users_all", (eb) => eb("id", "=", userId)),
|
|
459
|
-
);
|
|
134
|
+
// Test finding records using test.db
|
|
135
|
+
const users = await fragment.db.find("users", (b) =>
|
|
136
|
+
b.whereIndex("idx_users_all", (eb) => eb("id", "=", userId)),
|
|
137
|
+
);
|
|
460
138
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
// Test findFirst using test.db
|
|
470
|
-
const user = await test.db.findFirst("users", (b) =>
|
|
471
|
-
b.whereIndex("idx_users_all", (eb) => eb("id", "=", userId)),
|
|
472
|
-
);
|
|
139
|
+
expect(users).toHaveLength(1);
|
|
140
|
+
expect(users[0]).toMatchObject({
|
|
141
|
+
id: userId,
|
|
142
|
+
name: "Direct DB User",
|
|
143
|
+
email: "direct@example.com",
|
|
144
|
+
age: 28,
|
|
145
|
+
});
|
|
473
146
|
|
|
474
|
-
|
|
475
|
-
id: userId,
|
|
476
|
-
name: "Direct DB User",
|
|
477
|
-
email: "direct@example.com",
|
|
478
|
-
age: 28,
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// Cleanup
|
|
482
|
-
await test.cleanup();
|
|
483
|
-
}, 10000);
|
|
484
|
-
|
|
485
|
-
it("should maintain db property after resetDatabase", async () => {
|
|
486
|
-
const result = await createDatabaseFragmentForTest(testFragmentDef, [], {
|
|
487
|
-
adapter,
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// Create initial data using test.db
|
|
491
|
-
await result.test.db.create("users", {
|
|
492
|
-
name: "Before Reset",
|
|
493
|
-
email: "before@example.com",
|
|
494
|
-
age: 25,
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// Verify db works before reset
|
|
498
|
-
expect(result.test.db).toBeDefined();
|
|
499
|
-
expect(typeof result.test.db.create).toBe("function");
|
|
500
|
-
|
|
501
|
-
// Verify data exists
|
|
502
|
-
let users = await result.test.db.find("users");
|
|
503
|
-
expect(users).toHaveLength(1);
|
|
504
|
-
|
|
505
|
-
// Reset database
|
|
506
|
-
await result.test.resetDatabase();
|
|
507
|
-
|
|
508
|
-
// Verify database was actually reset (no data)
|
|
509
|
-
users = await result.test.db.find("users");
|
|
510
|
-
expect(users).toHaveLength(0);
|
|
511
|
-
|
|
512
|
-
// Verify we can still use the ORM after reset
|
|
513
|
-
const newUserId = await result.test.db.create("users", {
|
|
514
|
-
name: "After Reset",
|
|
515
|
-
email: "after@example.com",
|
|
516
|
-
age: 30,
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
expect(newUserId).toBeDefined();
|
|
520
|
-
|
|
521
|
-
const newUsers = await result.test.db.find("users");
|
|
522
|
-
expect(newUsers).toHaveLength(1);
|
|
523
|
-
expect(newUsers[0]).toMatchObject({
|
|
524
|
-
name: "After Reset",
|
|
525
|
-
email: "after@example.com",
|
|
526
|
-
age: 30,
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// Cleanup
|
|
530
|
-
await result.test.cleanup();
|
|
531
|
-
}, 10000);
|
|
532
|
-
});
|
|
533
|
-
}
|
|
147
|
+
await test.cleanup();
|
|
534
148
|
});
|
|
535
149
|
|
|
536
|
-
|
|
150
|
+
it("should work with multi-table schema", async () => {
|
|
537
151
|
// Simplified auth schema for testing
|
|
538
152
|
const authSchema = schema((s) => {
|
|
539
153
|
return s
|
|
@@ -553,84 +167,211 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
553
167
|
});
|
|
554
168
|
});
|
|
555
169
|
|
|
556
|
-
const authFragmentDef =
|
|
557
|
-
.withDatabase(authSchema)
|
|
558
|
-
.
|
|
170
|
+
const authFragmentDef = defineFragment<{}>("auth-test")
|
|
171
|
+
.extend(withDatabase(authSchema))
|
|
172
|
+
.providesBaseService(({ deps }) => {
|
|
559
173
|
return {
|
|
560
174
|
createUser: async (email: string, passwordHash: string) => {
|
|
561
|
-
const id = await
|
|
175
|
+
const id = await deps.db.create("user", { email, passwordHash });
|
|
562
176
|
return { id: id.valueOf(), email, passwordHash };
|
|
563
177
|
},
|
|
564
178
|
createSession: async (userId: string) => {
|
|
565
179
|
const expiresAt = new Date();
|
|
566
180
|
expiresAt.setDate(expiresAt.getDate() + 30);
|
|
567
|
-
const id = await
|
|
181
|
+
const id = await deps.db.create("session", { userId, expiresAt });
|
|
568
182
|
return { id: id.valueOf(), userId, expiresAt };
|
|
569
183
|
},
|
|
570
|
-
getUserByEmail: async (email: string) => {
|
|
571
|
-
const user = await orm.findFirst("user", (b) =>
|
|
572
|
-
b.whereIndex("idx_user_email", (eb) => eb("email", "=", email)),
|
|
573
|
-
);
|
|
574
|
-
if (!user) {
|
|
575
|
-
return null;
|
|
576
|
-
}
|
|
577
|
-
return { id: user.id.valueOf(), email: user.email, passwordHash: user.passwordHash };
|
|
578
|
-
},
|
|
579
184
|
};
|
|
185
|
+
})
|
|
186
|
+
.build();
|
|
187
|
+
|
|
188
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
189
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
190
|
+
.withFragment("auth", instantiate(authFragmentDef).withConfig({}).withRoutes([]))
|
|
191
|
+
.build();
|
|
192
|
+
|
|
193
|
+
const fragment = fragments.auth;
|
|
194
|
+
|
|
195
|
+
// Create a user
|
|
196
|
+
const user = await fragment.services.createUser("test@test.com", "hashed-password");
|
|
197
|
+
expect(user).toMatchObject({
|
|
198
|
+
id: expect.any(String),
|
|
199
|
+
email: "test@test.com",
|
|
200
|
+
passwordHash: "hashed-password",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Create a session for the user
|
|
204
|
+
const session = await fragment.services.createSession(user.id);
|
|
205
|
+
expect(session).toMatchObject({
|
|
206
|
+
id: expect.any(String),
|
|
207
|
+
userId: user.id,
|
|
208
|
+
expiresAt: expect.any(Date),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await test.cleanup();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("multi-fragment tests", () => {
|
|
216
|
+
// Create two different schemas
|
|
217
|
+
const userSchema = schema((s) => {
|
|
218
|
+
return s.addTable("user", (t) => {
|
|
219
|
+
return t
|
|
220
|
+
.addColumn("id", idColumn())
|
|
221
|
+
.addColumn("name", column("string"))
|
|
222
|
+
.addColumn("email", column("string"))
|
|
223
|
+
.createIndex("idx_user_all", ["id"]);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const postSchema = schema((s) => {
|
|
228
|
+
return s.addTable("post", (t) => {
|
|
229
|
+
return t
|
|
230
|
+
.addColumn("id", idColumn())
|
|
231
|
+
.addColumn("title", column("string"))
|
|
232
|
+
.addColumn("userId", column("string"))
|
|
233
|
+
.createIndex("idx_post_all", ["id"]);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const userFragmentDef = defineFragment<{}>("user-fragment")
|
|
238
|
+
.extend(withDatabase(userSchema))
|
|
239
|
+
.providesBaseService(({ deps }) => {
|
|
240
|
+
return {
|
|
241
|
+
createUser: async (data: { name: string; email: string }) => {
|
|
242
|
+
const id = await deps.db.create("user", data);
|
|
243
|
+
return { ...data, id: id.valueOf() };
|
|
244
|
+
},
|
|
245
|
+
getUsers: async () => {
|
|
246
|
+
const users = await deps.db.find("user", (b) =>
|
|
247
|
+
b.whereIndex("idx_user_all", (eb) => eb("id", "!=", "")),
|
|
248
|
+
);
|
|
249
|
+
return users.map((u) => ({ ...u, id: u.id.valueOf() }));
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
})
|
|
253
|
+
.build();
|
|
254
|
+
|
|
255
|
+
const postFragmentDef = defineFragment<{}>("post-fragment")
|
|
256
|
+
.extend(withDatabase(postSchema))
|
|
257
|
+
.providesBaseService(({ deps }) => {
|
|
258
|
+
return {
|
|
259
|
+
createPost: async (data: { title: string; userId: string }) => {
|
|
260
|
+
const id = await deps.db.create("post", data);
|
|
261
|
+
return { ...data, id: id.valueOf() };
|
|
262
|
+
},
|
|
263
|
+
getPosts: async () => {
|
|
264
|
+
const posts = await deps.db.find("post", (b) =>
|
|
265
|
+
b.whereIndex("idx_post_all", (eb) => eb("id", "!=", "")),
|
|
266
|
+
);
|
|
267
|
+
return posts.map((p) => ({ ...p, id: p.id.valueOf() }));
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
})
|
|
271
|
+
.build();
|
|
272
|
+
|
|
273
|
+
const adapters = [
|
|
274
|
+
{ name: "Kysely SQLite", adapter: { type: "kysely-sqlite" as const } },
|
|
275
|
+
{ name: "Kysely PGLite", adapter: { type: "kysely-pglite" as const } },
|
|
276
|
+
{ name: "Drizzle PGLite", adapter: { type: "drizzle-pglite" as const } },
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
for (const { name, adapter } of adapters) {
|
|
280
|
+
it(`should allow multiple fragments to share the same database adapter - ${name}`, async () => {
|
|
281
|
+
// Create both fragments with shared adapter
|
|
282
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
283
|
+
.withTestAdapter(adapter)
|
|
284
|
+
.withFragment("user", instantiate(userFragmentDef).withConfig({}).withRoutes([]))
|
|
285
|
+
.withFragment("post", instantiate(postFragmentDef).withConfig({}).withRoutes([]))
|
|
286
|
+
.build();
|
|
287
|
+
|
|
288
|
+
// Create a user
|
|
289
|
+
const user = await fragments.user.services.createUser({
|
|
290
|
+
name: "John Doe",
|
|
291
|
+
email: "john@example.com",
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(user).toMatchObject({
|
|
295
|
+
id: expect.any(String),
|
|
296
|
+
name: "John Doe",
|
|
297
|
+
email: "john@example.com",
|
|
580
298
|
});
|
|
581
299
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
];
|
|
587
|
-
|
|
588
|
-
for (const { name, adapter } of adapters) {
|
|
589
|
-
describe(name, () => {
|
|
590
|
-
it("should create user and session", async () => {
|
|
591
|
-
const { fragment, test } = await createDatabaseFragmentForTest(authFragmentDef, [], {
|
|
592
|
-
adapter,
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
// Create a user
|
|
596
|
-
const user = await fragment.services.createUser("test@test.com", "hashed-password");
|
|
597
|
-
expect(user).toMatchObject({
|
|
598
|
-
id: expect.any(String),
|
|
599
|
-
email: "test@test.com",
|
|
600
|
-
passwordHash: "hashed-password",
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// Create a session for the user
|
|
604
|
-
const session = await fragment.services.createSession(user.id);
|
|
605
|
-
expect(session).toMatchObject({
|
|
606
|
-
id: expect.any(String),
|
|
607
|
-
userId: user.id,
|
|
608
|
-
expiresAt: expect.any(Date),
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
// Find user by email
|
|
612
|
-
const foundUser = await fragment.services.getUserByEmail("test@test.com");
|
|
613
|
-
expect(foundUser).toMatchObject({
|
|
614
|
-
id: user.id,
|
|
615
|
-
email: "test@test.com",
|
|
616
|
-
passwordHash: "hashed-password",
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
// Cleanup
|
|
620
|
-
await test.cleanup();
|
|
621
|
-
}, 10000);
|
|
622
|
-
|
|
623
|
-
it("should return null when user not found", async () => {
|
|
624
|
-
const { fragment, test } = await createDatabaseFragmentForTest(authFragmentDef, [], {
|
|
625
|
-
adapter,
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
const notFound = await fragment.services.getUserByEmail("nonexistent@test.com");
|
|
629
|
-
expect(notFound).toBeNull();
|
|
630
|
-
|
|
631
|
-
await test.cleanup();
|
|
632
|
-
}, 10000);
|
|
300
|
+
// Create a post with the user's ID
|
|
301
|
+
const post = await fragments.post.services.createPost({
|
|
302
|
+
title: "My First Post",
|
|
303
|
+
userId: user.id,
|
|
633
304
|
});
|
|
305
|
+
|
|
306
|
+
expect(post).toMatchObject({
|
|
307
|
+
id: expect.any(String),
|
|
308
|
+
title: "My First Post",
|
|
309
|
+
userId: user.id,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Verify data exists
|
|
313
|
+
const users = await fragments.user.services.getUsers();
|
|
314
|
+
expect(users).toHaveLength(1);
|
|
315
|
+
|
|
316
|
+
const posts = await fragments.post.services.getPosts();
|
|
317
|
+
expect(posts).toHaveLength(1);
|
|
318
|
+
expect(posts[0]!.userId).toBe(user.id);
|
|
319
|
+
|
|
320
|
+
// Cleanup (centralized - cleans up all fragments)
|
|
321
|
+
await test.cleanup();
|
|
322
|
+
}, 10000);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("ExtractFragmentServices", () => {
|
|
327
|
+
it("extracts provided services from database fragment with new API", () => {
|
|
328
|
+
const testSchema = schema((s) => s);
|
|
329
|
+
|
|
330
|
+
interface ITestService {
|
|
331
|
+
doSomething: (input: string) => Promise<string>;
|
|
332
|
+
doSomethingElse: (input: number) => Promise<number>;
|
|
634
333
|
}
|
|
334
|
+
|
|
335
|
+
const fragment = defineFragment<{}>("test-db-fragment")
|
|
336
|
+
.extend(withDatabase(testSchema))
|
|
337
|
+
.providesService(
|
|
338
|
+
"test",
|
|
339
|
+
(): ITestService => ({
|
|
340
|
+
doSomething: async (input: string) => input.toUpperCase(),
|
|
341
|
+
doSomethingElse: async (input: number) => input * 2,
|
|
342
|
+
}),
|
|
343
|
+
)
|
|
344
|
+
.build();
|
|
345
|
+
|
|
346
|
+
type Services = ExtractFragmentServices<typeof fragment>;
|
|
347
|
+
|
|
348
|
+
// Should include the provided service
|
|
349
|
+
expectTypeOf<Services>().toMatchObjectType<{
|
|
350
|
+
test: ITestService;
|
|
351
|
+
}>();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("merges base services and provided services in database fragment", () => {
|
|
355
|
+
const testSchema = schema((s) => s);
|
|
356
|
+
|
|
357
|
+
const fragment = defineFragment<{}>("test-db-fragment")
|
|
358
|
+
.extend(withDatabase(testSchema))
|
|
359
|
+
.providesBaseService(() => ({
|
|
360
|
+
internalService: async () => "internal",
|
|
361
|
+
}))
|
|
362
|
+
.providesService("externalService", () => ({
|
|
363
|
+
publicMethod: async () => "public",
|
|
364
|
+
}))
|
|
365
|
+
.build();
|
|
366
|
+
|
|
367
|
+
type Services = ExtractFragmentServices<typeof fragment>;
|
|
368
|
+
|
|
369
|
+
// Should include both base services and provided services
|
|
370
|
+
expectTypeOf<Services>().toMatchObjectType<{
|
|
371
|
+
internalService: () => Promise<string>;
|
|
372
|
+
externalService: {
|
|
373
|
+
publicMethod: () => Promise<string>;
|
|
374
|
+
};
|
|
375
|
+
}>();
|
|
635
376
|
});
|
|
636
377
|
});
|