@fragno-dev/test 0.1.4 → 0.1.6
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 +11 -7
- package/CHANGELOG.md +15 -0
- package/dist/adapters.d.ts +32 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +204 -0
- package/dist/adapters.js.map +1 -0
- package/dist/index.d.ts +17 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -43
- package/dist/index.js.map +1 -1
- package/package.json +29 -4
- package/src/adapters.ts +379 -0
- package/src/index.test.ts +157 -29
- package/src/index.ts +120 -83
package/src/adapters.ts
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
import { SQLocalKysely } from "sqlocal/kysely";
|
|
3
|
+
import { KyselyPGlite } from "kysely-pglite";
|
|
4
|
+
import { drizzle } from "drizzle-orm/pglite";
|
|
5
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
6
|
+
import { KyselyAdapter } from "@fragno-dev/db/adapters/kysely";
|
|
7
|
+
import { DrizzleAdapter } from "@fragno-dev/db/adapters/drizzle";
|
|
8
|
+
import type { AnySchema } from "@fragno-dev/db/schema";
|
|
9
|
+
import type { DatabaseAdapter } from "@fragno-dev/db/adapters";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
// Adapter configuration types
|
|
16
|
+
export interface KyselySqliteAdapter {
|
|
17
|
+
type: "kysely-sqlite";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface KyselyPgliteAdapter {
|
|
21
|
+
type: "kysely-pglite";
|
|
22
|
+
databasePath?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DrizzlePgliteAdapter {
|
|
26
|
+
type: "drizzle-pglite";
|
|
27
|
+
databasePath?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SupportedAdapter = KyselySqliteAdapter | KyselyPgliteAdapter | DrizzlePgliteAdapter;
|
|
31
|
+
|
|
32
|
+
// Conditional return types based on adapter
|
|
33
|
+
export type TestContext<T extends SupportedAdapter> = T extends
|
|
34
|
+
| KyselySqliteAdapter
|
|
35
|
+
| KyselyPgliteAdapter
|
|
36
|
+
? {
|
|
37
|
+
readonly kysely: Kysely<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
38
|
+
readonly adapter: DatabaseAdapter<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
39
|
+
resetDatabase: () => Promise<void>;
|
|
40
|
+
cleanup: () => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
: T extends DrizzlePgliteAdapter
|
|
43
|
+
? {
|
|
44
|
+
readonly drizzle: ReturnType<typeof drizzle<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
45
|
+
readonly adapter: DatabaseAdapter<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
46
|
+
resetDatabase: () => Promise<void>;
|
|
47
|
+
cleanup: () => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
: never;
|
|
50
|
+
|
|
51
|
+
// Factory function return type
|
|
52
|
+
interface AdapterFactoryResult<T extends SupportedAdapter> {
|
|
53
|
+
testContext: TestContext<T>;
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
adapter: DatabaseAdapter<any>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create Kysely + SQLite adapter using SQLocalKysely (always in-memory)
|
|
60
|
+
*/
|
|
61
|
+
export async function createKyselySqliteAdapter(
|
|
62
|
+
_config: KyselySqliteAdapter,
|
|
63
|
+
schema: AnySchema,
|
|
64
|
+
namespace: string,
|
|
65
|
+
migrateToVersion?: number,
|
|
66
|
+
): Promise<AdapterFactoryResult<KyselySqliteAdapter>> {
|
|
67
|
+
// Helper to create a new database instance and run migrations
|
|
68
|
+
const createDatabase = async () => {
|
|
69
|
+
// Create SQLocalKysely instance (always in-memory for tests)
|
|
70
|
+
const { dialect } = new SQLocalKysely(":memory:");
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
+
const kysely = new Kysely<any>({
|
|
73
|
+
dialect,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Create KyselyAdapter
|
|
77
|
+
const adapter = new KyselyAdapter({
|
|
78
|
+
db: kysely,
|
|
79
|
+
provider: "sqlite",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Run migrations
|
|
83
|
+
const migrator = adapter.createMigrationEngine(schema, namespace);
|
|
84
|
+
const preparedMigration = migrateToVersion
|
|
85
|
+
? await migrator.prepareMigrationTo(migrateToVersion, {
|
|
86
|
+
updateSettings: false,
|
|
87
|
+
})
|
|
88
|
+
: await migrator.prepareMigration({
|
|
89
|
+
updateSettings: false,
|
|
90
|
+
});
|
|
91
|
+
await preparedMigration.execute();
|
|
92
|
+
|
|
93
|
+
return { kysely, adapter };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Create initial database
|
|
97
|
+
let { kysely, adapter } = await createDatabase();
|
|
98
|
+
|
|
99
|
+
// Reset database function - creates a fresh in-memory database and re-runs migrations
|
|
100
|
+
const resetDatabase = async () => {
|
|
101
|
+
// Destroy the old Kysely instance
|
|
102
|
+
await kysely.destroy();
|
|
103
|
+
|
|
104
|
+
// Create a new database instance
|
|
105
|
+
const newDb = await createDatabase();
|
|
106
|
+
kysely = newDb.kysely;
|
|
107
|
+
adapter = newDb.adapter;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Cleanup function - closes connections (no files to delete for in-memory)
|
|
111
|
+
const cleanup = async () => {
|
|
112
|
+
await kysely.destroy();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
testContext: {
|
|
117
|
+
get kysely() {
|
|
118
|
+
return kysely;
|
|
119
|
+
},
|
|
120
|
+
get adapter() {
|
|
121
|
+
return adapter;
|
|
122
|
+
},
|
|
123
|
+
resetDatabase,
|
|
124
|
+
cleanup,
|
|
125
|
+
},
|
|
126
|
+
get adapter() {
|
|
127
|
+
return adapter;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create Kysely + PGLite adapter using kysely-pglite
|
|
134
|
+
*/
|
|
135
|
+
export async function createKyselyPgliteAdapter(
|
|
136
|
+
config: KyselyPgliteAdapter,
|
|
137
|
+
schema: AnySchema,
|
|
138
|
+
namespace: string,
|
|
139
|
+
migrateToVersion?: number,
|
|
140
|
+
): Promise<AdapterFactoryResult<KyselyPgliteAdapter>> {
|
|
141
|
+
const databasePath = config.databasePath;
|
|
142
|
+
|
|
143
|
+
// Helper to create a new database instance and run migrations
|
|
144
|
+
const createDatabase = async () => {
|
|
145
|
+
// Create KyselyPGlite instance
|
|
146
|
+
const kyselyPglite = await KyselyPGlite.create(databasePath);
|
|
147
|
+
|
|
148
|
+
// Create Kysely instance with PGlite dialect
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
+
const kysely = new Kysely<any>({
|
|
151
|
+
dialect: kyselyPglite.dialect,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Create KyselyAdapter
|
|
155
|
+
const adapter = new KyselyAdapter({
|
|
156
|
+
db: kysely,
|
|
157
|
+
provider: "postgresql",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Run migrations
|
|
161
|
+
const migrator = adapter.createMigrationEngine(schema, namespace);
|
|
162
|
+
const preparedMigration = migrateToVersion
|
|
163
|
+
? await migrator.prepareMigrationTo(migrateToVersion, {
|
|
164
|
+
updateSettings: false,
|
|
165
|
+
})
|
|
166
|
+
: await migrator.prepareMigration({
|
|
167
|
+
updateSettings: false,
|
|
168
|
+
});
|
|
169
|
+
await preparedMigration.execute();
|
|
170
|
+
|
|
171
|
+
return { kysely, adapter, kyselyPglite };
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Create initial database
|
|
175
|
+
let { kysely, adapter, kyselyPglite } = await createDatabase();
|
|
176
|
+
|
|
177
|
+
// Reset database function - creates a fresh database and re-runs migrations
|
|
178
|
+
const resetDatabase = async () => {
|
|
179
|
+
// Close the old instances
|
|
180
|
+
await kysely.destroy();
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await kyselyPglite.client.close();
|
|
184
|
+
} catch {
|
|
185
|
+
// Ignore if already closed
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Create a new database instance
|
|
189
|
+
const newDb = await createDatabase();
|
|
190
|
+
kysely = newDb.kysely;
|
|
191
|
+
adapter = newDb.adapter;
|
|
192
|
+
kyselyPglite = newDb.kyselyPglite;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Cleanup function - closes connections and deletes database directory
|
|
196
|
+
const cleanup = async () => {
|
|
197
|
+
await kysely.destroy();
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await kyselyPglite.client.close();
|
|
201
|
+
} catch {
|
|
202
|
+
// Ignore if already closed
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Delete the database directory if it exists and is a file path
|
|
206
|
+
if (databasePath && databasePath !== ":memory:" && existsSync(databasePath)) {
|
|
207
|
+
await rm(databasePath, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
testContext: {
|
|
213
|
+
get kysely() {
|
|
214
|
+
return kysely;
|
|
215
|
+
},
|
|
216
|
+
get adapter() {
|
|
217
|
+
return adapter;
|
|
218
|
+
},
|
|
219
|
+
resetDatabase,
|
|
220
|
+
cleanup,
|
|
221
|
+
},
|
|
222
|
+
get adapter() {
|
|
223
|
+
return adapter;
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Create Drizzle + PGLite adapter using drizzle-orm/pglite
|
|
230
|
+
*/
|
|
231
|
+
export async function createDrizzlePgliteAdapter(
|
|
232
|
+
config: DrizzlePgliteAdapter,
|
|
233
|
+
schema: AnySchema,
|
|
234
|
+
namespace: string,
|
|
235
|
+
_migrateToVersion?: number,
|
|
236
|
+
): Promise<AdapterFactoryResult<DrizzlePgliteAdapter>> {
|
|
237
|
+
const databasePath = config.databasePath;
|
|
238
|
+
|
|
239
|
+
// Import drizzle-kit for migrations
|
|
240
|
+
const require = createRequire(import.meta.url);
|
|
241
|
+
const { generateDrizzleJson, generateMigration } =
|
|
242
|
+
require("drizzle-kit/api") as typeof import("drizzle-kit/api");
|
|
243
|
+
|
|
244
|
+
// Import generateSchema from the properly exported module
|
|
245
|
+
const { generateSchema } = await import("@fragno-dev/db/adapters/drizzle/generate");
|
|
246
|
+
|
|
247
|
+
// Helper to write schema to file and dynamically import it
|
|
248
|
+
const writeAndLoadSchema = async () => {
|
|
249
|
+
const testDir = join(import.meta.dirname, "_generated", "drizzle-test");
|
|
250
|
+
await mkdir(testDir, { recursive: true }).catch(() => {
|
|
251
|
+
// Ignore error if directory already exists
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const schemaFilePath = join(
|
|
255
|
+
testDir,
|
|
256
|
+
`test-schema-${Date.now()}-${Math.random().toString(36).slice(2, 9)}.ts`,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Generate and write the Drizzle schema to file
|
|
260
|
+
const drizzleSchemaTs = generateSchema([{ namespace: namespace ?? "", schema }], "postgresql");
|
|
261
|
+
await writeFile(schemaFilePath, drizzleSchemaTs, "utf-8");
|
|
262
|
+
|
|
263
|
+
// Dynamically import the generated schema (with cache busting)
|
|
264
|
+
const schemaModule = await import(`${schemaFilePath}?t=${Date.now()}`);
|
|
265
|
+
|
|
266
|
+
const cleanup = async () => {
|
|
267
|
+
await rm(testDir, { recursive: true, force: true });
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return { schemaModule, cleanup };
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Helper to create a new database instance and run migrations
|
|
274
|
+
const createDatabase = async () => {
|
|
275
|
+
// Write schema to file and load it
|
|
276
|
+
const { schemaModule, cleanup } = await writeAndLoadSchema();
|
|
277
|
+
|
|
278
|
+
// Create PGlite instance
|
|
279
|
+
const pglite = new PGlite(databasePath);
|
|
280
|
+
|
|
281
|
+
// Create Drizzle instance with PGlite
|
|
282
|
+
const db = drizzle(pglite, {
|
|
283
|
+
schema: schemaModule,
|
|
284
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
285
|
+
}) as any;
|
|
286
|
+
|
|
287
|
+
// Generate and run migrations
|
|
288
|
+
const migrationStatements = await generateMigration(
|
|
289
|
+
generateDrizzleJson({}), // Empty schema (starting state)
|
|
290
|
+
generateDrizzleJson(schemaModule), // Target schema
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Execute migration SQL
|
|
294
|
+
for (const statement of migrationStatements) {
|
|
295
|
+
await db.execute(statement);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Create DrizzleAdapter
|
|
299
|
+
const adapter = new DrizzleAdapter({
|
|
300
|
+
db: () => db,
|
|
301
|
+
provider: "postgresql",
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return { drizzle: db, adapter, pglite, cleanup };
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Create initial database
|
|
308
|
+
let { drizzle: drizzleDb, adapter, pglite, cleanup: schemaCleanup } = await createDatabase();
|
|
309
|
+
|
|
310
|
+
// Reset database function - creates a fresh database and re-runs migrations
|
|
311
|
+
const resetDatabase = async () => {
|
|
312
|
+
// Close the old instances and cleanup
|
|
313
|
+
await pglite.close();
|
|
314
|
+
await schemaCleanup();
|
|
315
|
+
|
|
316
|
+
// Create a new database instance
|
|
317
|
+
const newDb = await createDatabase();
|
|
318
|
+
drizzleDb = newDb.drizzle;
|
|
319
|
+
adapter = newDb.adapter;
|
|
320
|
+
pglite = newDb.pglite;
|
|
321
|
+
schemaCleanup = newDb.cleanup;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Cleanup function - closes connections and deletes generated files and database directory
|
|
325
|
+
const cleanup = async () => {
|
|
326
|
+
await pglite.close();
|
|
327
|
+
await schemaCleanup();
|
|
328
|
+
|
|
329
|
+
// Delete the database directory if it exists and is a file path
|
|
330
|
+
if (databasePath && databasePath !== ":memory:" && existsSync(databasePath)) {
|
|
331
|
+
await rm(databasePath, { recursive: true, force: true });
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
testContext: {
|
|
337
|
+
get drizzle() {
|
|
338
|
+
return drizzleDb;
|
|
339
|
+
},
|
|
340
|
+
get adapter() {
|
|
341
|
+
return adapter;
|
|
342
|
+
},
|
|
343
|
+
resetDatabase,
|
|
344
|
+
cleanup,
|
|
345
|
+
},
|
|
346
|
+
get adapter() {
|
|
347
|
+
return adapter;
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Create adapter based on configuration
|
|
354
|
+
*/
|
|
355
|
+
export async function createAdapter<T extends SupportedAdapter>(
|
|
356
|
+
adapterConfig: T,
|
|
357
|
+
schema: AnySchema,
|
|
358
|
+
namespace: string,
|
|
359
|
+
migrateToVersion?: number,
|
|
360
|
+
): Promise<AdapterFactoryResult<T>> {
|
|
361
|
+
if (adapterConfig.type === "kysely-sqlite") {
|
|
362
|
+
return createKyselySqliteAdapter(adapterConfig, schema, namespace, migrateToVersion) as Promise<
|
|
363
|
+
AdapterFactoryResult<T>
|
|
364
|
+
>;
|
|
365
|
+
} else if (adapterConfig.type === "kysely-pglite") {
|
|
366
|
+
return createKyselyPgliteAdapter(adapterConfig, schema, namespace, migrateToVersion) as Promise<
|
|
367
|
+
AdapterFactoryResult<T>
|
|
368
|
+
>;
|
|
369
|
+
} else if (adapterConfig.type === "drizzle-pglite") {
|
|
370
|
+
return createDrizzlePgliteAdapter(
|
|
371
|
+
adapterConfig,
|
|
372
|
+
schema,
|
|
373
|
+
namespace,
|
|
374
|
+
migrateToVersion,
|
|
375
|
+
) as Promise<AdapterFactoryResult<T>>;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
throw new Error(`Unsupported adapter type: ${(adapterConfig as SupportedAdapter).type}`);
|
|
379
|
+
}
|
package/src/index.test.ts
CHANGED
|
@@ -55,7 +55,9 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it("should use in-memory database by default", async () => {
|
|
58
|
-
const fragment = await createDatabaseFragmentForTest(testFragmentDef
|
|
58
|
+
const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
59
|
+
adapter: { type: "kysely-sqlite" },
|
|
60
|
+
});
|
|
59
61
|
|
|
60
62
|
// Should be able to create and query users
|
|
61
63
|
const user = await fragment.services.createUser({
|
|
@@ -77,8 +79,8 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
it("should create database at specified path", async () => {
|
|
80
|
-
const fragment = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
81
|
-
databasePath: testDbPath,
|
|
82
|
+
const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
83
|
+
adapter: { type: "kysely-sqlite", databasePath: testDbPath },
|
|
82
84
|
});
|
|
83
85
|
|
|
84
86
|
// Create a user
|
|
@@ -101,7 +103,9 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
101
103
|
|
|
102
104
|
describe("migrateToVersion option", () => {
|
|
103
105
|
it("should migrate to latest version by default", async () => {
|
|
104
|
-
const fragment = await createDatabaseFragmentForTest(testFragmentDef
|
|
106
|
+
const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
107
|
+
adapter: { type: "kysely-sqlite" },
|
|
108
|
+
});
|
|
105
109
|
|
|
106
110
|
// Should have the 'age' column from version 2
|
|
107
111
|
const user = await fragment.services.createUser({
|
|
@@ -120,14 +124,15 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
120
124
|
|
|
121
125
|
it("should migrate to specific version when specified", async () => {
|
|
122
126
|
// Migrate to version 1 (before 'age' column was added)
|
|
123
|
-
const
|
|
127
|
+
const { test } = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
128
|
+
adapter: { type: "kysely-sqlite" },
|
|
124
129
|
migrateToVersion: 1,
|
|
125
130
|
});
|
|
126
131
|
|
|
127
132
|
// Query the database directly to check schema
|
|
128
133
|
// In version 1, we should be able to insert without the age column
|
|
129
134
|
const tableName = "users_test-fragment-db";
|
|
130
|
-
await
|
|
135
|
+
await test.kysely
|
|
131
136
|
.insertInto(tableName)
|
|
132
137
|
.values({
|
|
133
138
|
id: "test-id-1",
|
|
@@ -136,7 +141,7 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
136
141
|
})
|
|
137
142
|
.execute();
|
|
138
143
|
|
|
139
|
-
const result = await
|
|
144
|
+
const result = await test.kysely.selectFrom(tableName).selectAll().execute();
|
|
140
145
|
|
|
141
146
|
expect(result).toHaveLength(1);
|
|
142
147
|
expect(result[0]).toMatchObject({
|
|
@@ -150,7 +155,8 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
150
155
|
|
|
151
156
|
it("should allow creating user with age when migrated to version 2", async () => {
|
|
152
157
|
// Explicitly migrate to version 2
|
|
153
|
-
const fragment = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
158
|
+
const { fragment, test } = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
159
|
+
adapter: { type: "kysely-sqlite" },
|
|
154
160
|
migrateToVersion: 2,
|
|
155
161
|
});
|
|
156
162
|
|
|
@@ -169,7 +175,7 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
169
175
|
});
|
|
170
176
|
|
|
171
177
|
const tableName = "users_test-fragment-db";
|
|
172
|
-
const result = await
|
|
178
|
+
const result = await test.kysely.selectFrom(tableName).selectAll().execute();
|
|
173
179
|
|
|
174
180
|
expect(result).toHaveLength(1);
|
|
175
181
|
expect(result[0]).toMatchObject({
|
|
@@ -194,8 +200,8 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
194
200
|
});
|
|
195
201
|
|
|
196
202
|
it("should work with both databasePath and migrateToVersion", async () => {
|
|
197
|
-
const fragment = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
198
|
-
databasePath: testDbPath,
|
|
203
|
+
const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
204
|
+
adapter: { type: "kysely-sqlite", databasePath: testDbPath },
|
|
199
205
|
migrateToVersion: 2,
|
|
200
206
|
});
|
|
201
207
|
|
|
@@ -225,21 +231,27 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
225
231
|
|
|
226
232
|
describe("fragment initialization", () => {
|
|
227
233
|
it("should provide kysely instance", async () => {
|
|
228
|
-
const
|
|
234
|
+
const { test } = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
235
|
+
adapter: { type: "kysely-sqlite" },
|
|
236
|
+
});
|
|
229
237
|
|
|
230
|
-
expect(
|
|
231
|
-
expect(typeof
|
|
238
|
+
expect(test.kysely).toBeDefined();
|
|
239
|
+
expect(typeof test.kysely.selectFrom).toBe("function");
|
|
232
240
|
});
|
|
233
241
|
|
|
234
242
|
it("should provide adapter instance", async () => {
|
|
235
|
-
const
|
|
243
|
+
const { test } = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
244
|
+
adapter: { type: "kysely-sqlite" },
|
|
245
|
+
});
|
|
236
246
|
|
|
237
|
-
expect(
|
|
238
|
-
expect(typeof
|
|
247
|
+
expect(test.adapter).toBeDefined();
|
|
248
|
+
expect(typeof test.adapter.createMigrationEngine).toBe("function");
|
|
239
249
|
});
|
|
240
250
|
|
|
241
251
|
it("should have all standard fragment test properties", async () => {
|
|
242
|
-
const fragment = await createDatabaseFragmentForTest(testFragmentDef
|
|
252
|
+
const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
253
|
+
adapter: { type: "kysely-sqlite" },
|
|
254
|
+
});
|
|
243
255
|
|
|
244
256
|
expect(fragment.services).toBeDefined();
|
|
245
257
|
expect(fragment.initRoutes).toBeDefined();
|
|
@@ -258,14 +270,18 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
258
270
|
|
|
259
271
|
await expect(
|
|
260
272
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
261
|
-
createDatabaseFragmentForTest(nonDbFragment as any
|
|
273
|
+
createDatabaseFragmentForTest(nonDbFragment as any, {
|
|
274
|
+
adapter: { type: "kysely-sqlite" },
|
|
275
|
+
}),
|
|
262
276
|
).rejects.toThrow("Fragment 'non-db-fragment' does not have a database schema");
|
|
263
277
|
});
|
|
264
278
|
});
|
|
265
279
|
|
|
266
280
|
describe("route handling with defineRoutes", () => {
|
|
267
281
|
it("should handle route factory with multiple routes", async () => {
|
|
268
|
-
const fragment = await createDatabaseFragmentForTest(testFragmentDef
|
|
282
|
+
const { fragment } = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
283
|
+
adapter: { type: "kysely-sqlite" },
|
|
284
|
+
});
|
|
269
285
|
|
|
270
286
|
type Config = {};
|
|
271
287
|
type Deps = {};
|
|
@@ -357,33 +373,36 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
357
373
|
|
|
358
374
|
describe("resetDatabase", () => {
|
|
359
375
|
it("should clear all data and recreate a fresh database", async () => {
|
|
360
|
-
|
|
376
|
+
// Don't destructure so we can access the updated fragment through getters after reset
|
|
377
|
+
const result = await createDatabaseFragmentForTest(testFragmentDef, {
|
|
378
|
+
adapter: { type: "kysely-sqlite" },
|
|
379
|
+
});
|
|
361
380
|
|
|
362
381
|
// Create some users
|
|
363
|
-
await
|
|
382
|
+
await result.services.createUser({
|
|
364
383
|
name: "User 1",
|
|
365
384
|
email: "user1@example.com",
|
|
366
385
|
age: 25,
|
|
367
386
|
});
|
|
368
|
-
await
|
|
387
|
+
await result.services.createUser({
|
|
369
388
|
name: "User 2",
|
|
370
389
|
email: "user2@example.com",
|
|
371
390
|
age: 30,
|
|
372
391
|
});
|
|
373
392
|
|
|
374
393
|
// Verify users exist
|
|
375
|
-
let users = await
|
|
394
|
+
let users = await result.services.getUsers();
|
|
376
395
|
expect(users).toHaveLength(2);
|
|
377
396
|
|
|
378
397
|
// Reset the database
|
|
379
|
-
await
|
|
398
|
+
await result.test.resetDatabase();
|
|
380
399
|
|
|
381
|
-
// Verify database is empty
|
|
382
|
-
users = await
|
|
400
|
+
// Verify database is empty (accessing through result to get updated fragment)
|
|
401
|
+
users = await result.services.getUsers();
|
|
383
402
|
expect(users).toHaveLength(0);
|
|
384
403
|
|
|
385
404
|
// Verify we can still create new users after reset
|
|
386
|
-
const newUser = await
|
|
405
|
+
const newUser = await result.services.createUser({
|
|
387
406
|
name: "User After Reset",
|
|
388
407
|
email: "after@example.com",
|
|
389
408
|
age: 35,
|
|
@@ -396,9 +415,118 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
396
415
|
age: 35,
|
|
397
416
|
});
|
|
398
417
|
|
|
399
|
-
users = await
|
|
418
|
+
users = await result.services.getUsers();
|
|
400
419
|
expect(users).toHaveLength(1);
|
|
401
420
|
expect(users[0]).toMatchObject(newUser);
|
|
402
421
|
});
|
|
403
422
|
});
|
|
423
|
+
|
|
424
|
+
describe("multiple adapters with auth-like schema", () => {
|
|
425
|
+
// Simplified auth schema for testing
|
|
426
|
+
const authSchema = schema((s) => {
|
|
427
|
+
return s
|
|
428
|
+
.addTable("user", (t) => {
|
|
429
|
+
return t
|
|
430
|
+
.addColumn("id", idColumn())
|
|
431
|
+
.addColumn("email", column("string"))
|
|
432
|
+
.addColumn("passwordHash", column("string"))
|
|
433
|
+
.createIndex("idx_user_email", ["email"]);
|
|
434
|
+
})
|
|
435
|
+
.addTable("session", (t) => {
|
|
436
|
+
return t
|
|
437
|
+
.addColumn("id", idColumn())
|
|
438
|
+
.addColumn("userId", column("string"))
|
|
439
|
+
.addColumn("expiresAt", column("timestamp"))
|
|
440
|
+
.createIndex("idx_session_user", ["userId"]);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const authFragmentDef = defineFragmentWithDatabase<{}>("auth-test")
|
|
445
|
+
.withDatabase(authSchema)
|
|
446
|
+
.withServices(({ orm }) => {
|
|
447
|
+
return {
|
|
448
|
+
createUser: async (email: string, passwordHash: string) => {
|
|
449
|
+
const id = await orm.create("user", { email, passwordHash });
|
|
450
|
+
return { id: id.valueOf(), email, passwordHash };
|
|
451
|
+
},
|
|
452
|
+
createSession: async (userId: string) => {
|
|
453
|
+
const expiresAt = new Date();
|
|
454
|
+
expiresAt.setDate(expiresAt.getDate() + 30);
|
|
455
|
+
const id = await orm.create("session", { userId, expiresAt });
|
|
456
|
+
return { id: id.valueOf(), userId, expiresAt };
|
|
457
|
+
},
|
|
458
|
+
getUserByEmail: async (email: string) => {
|
|
459
|
+
const user = await orm.findFirst("user", (b) =>
|
|
460
|
+
b.whereIndex("idx_user_email", (eb) => eb("email", "=", email)),
|
|
461
|
+
);
|
|
462
|
+
if (!user) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
return { id: user.id.valueOf(), email: user.email, passwordHash: user.passwordHash };
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const adapters = [
|
|
471
|
+
{ name: "Kysely SQLite", adapter: { type: "kysely-sqlite" as const } },
|
|
472
|
+
{ name: "Kysely PGLite", adapter: { type: "kysely-pglite" as const } },
|
|
473
|
+
{ name: "Drizzle PGLite", adapter: { type: "drizzle-pglite" as const } },
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
for (const { name, adapter } of adapters) {
|
|
477
|
+
describe(name, () => {
|
|
478
|
+
it(
|
|
479
|
+
"should create user and session",
|
|
480
|
+
async () => {
|
|
481
|
+
const { fragment, test } = await createDatabaseFragmentForTest(authFragmentDef, {
|
|
482
|
+
adapter,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Create a user
|
|
486
|
+
const user = await fragment.services.createUser("test@test.com", "hashed-password");
|
|
487
|
+
expect(user).toMatchObject({
|
|
488
|
+
id: expect.any(String),
|
|
489
|
+
email: "test@test.com",
|
|
490
|
+
passwordHash: "hashed-password",
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Create a session for the user
|
|
494
|
+
const session = await fragment.services.createSession(user.id);
|
|
495
|
+
expect(session).toMatchObject({
|
|
496
|
+
id: expect.any(String),
|
|
497
|
+
userId: user.id,
|
|
498
|
+
expiresAt: expect.any(Date),
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Find user by email
|
|
502
|
+
const foundUser = await fragment.services.getUserByEmail("test@test.com");
|
|
503
|
+
expect(foundUser).toMatchObject({
|
|
504
|
+
id: user.id,
|
|
505
|
+
email: "test@test.com",
|
|
506
|
+
passwordHash: "hashed-password",
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Cleanup
|
|
510
|
+
await test.cleanup();
|
|
511
|
+
},
|
|
512
|
+
{ timeout: 10000 },
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
it(
|
|
516
|
+
"should return null when user not found",
|
|
517
|
+
async () => {
|
|
518
|
+
const { fragment, test } = await createDatabaseFragmentForTest(authFragmentDef, {
|
|
519
|
+
adapter,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const notFound = await fragment.services.getUserByEmail("nonexistent@test.com");
|
|
523
|
+
expect(notFound).toBeNull();
|
|
524
|
+
|
|
525
|
+
await test.cleanup();
|
|
526
|
+
},
|
|
527
|
+
{ timeout: 10000 },
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
});
|
|
404
532
|
});
|