@geekmidas/testkit 0.0.17 → 0.1.1

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.
Files changed (99) hide show
  1. package/README.md +445 -193
  2. package/dist/{Factory-BZ8uMoXl.d.cts → Factory-DE3hE0WO.d.mts} +2 -2
  3. package/dist/{Factory-CRquB4vz.d.mts → Factory-pNV7ZQ7-.d.cts} +2 -2
  4. package/dist/Factory.d.cts +2 -2
  5. package/dist/Factory.d.mts +2 -2
  6. package/dist/{KyselyFactory-BDS_QqRT.d.mts → KyselyFactory-CPZTUuMB.d.cts} +23 -10
  7. package/dist/{KyselyFactory-BcYkC0t2.mjs → KyselyFactory-CXY5gJk2.mjs} +25 -12
  8. package/dist/KyselyFactory-CXY5gJk2.mjs.map +1 -0
  9. package/dist/{KyselyFactory-Cf0o2YxO.cjs → KyselyFactory-DaaCykWP.cjs} +25 -12
  10. package/dist/KyselyFactory-DaaCykWP.cjs.map +1 -0
  11. package/dist/{KyselyFactory-DRQ83r0o.d.cts → KyselyFactory-RAyvZ8Cj.d.mts} +23 -10
  12. package/dist/KyselyFactory.cjs +1 -1
  13. package/dist/KyselyFactory.d.cts +3 -3
  14. package/dist/KyselyFactory.d.mts +3 -3
  15. package/dist/KyselyFactory.mjs +1 -1
  16. package/dist/{ObjectionFactory-C3tHvX1d.d.mts → ObjectionFactory-BlBicEia.d.mts} +26 -13
  17. package/dist/{ObjectionFactory-C4X78k0B.d.cts → ObjectionFactory-BwpN4gMX.d.cts} +26 -13
  18. package/dist/{ObjectionFactory-CDriunkS.cjs → ObjectionFactory-Eb04AOnv.cjs} +28 -15
  19. package/dist/ObjectionFactory-Eb04AOnv.cjs.map +1 -0
  20. package/dist/{ObjectionFactory-8hebmnai.mjs → ObjectionFactory-zf2fLKrL.mjs} +28 -15
  21. package/dist/ObjectionFactory-zf2fLKrL.mjs.map +1 -0
  22. package/dist/ObjectionFactory.cjs +1 -1
  23. package/dist/ObjectionFactory.d.cts +3 -3
  24. package/dist/ObjectionFactory.d.mts +3 -3
  25. package/dist/ObjectionFactory.mjs +1 -1
  26. package/dist/{VitestKyselyTransactionIsolator-COCVfvfr.d.mts → VitestKyselyTransactionIsolator-BqrZDeaT.d.cts} +2 -2
  27. package/dist/{VitestKyselyTransactionIsolator-Cst3vFjb.cjs → VitestKyselyTransactionIsolator-DX_VPKS-.cjs} +2 -2
  28. package/dist/{VitestKyselyTransactionIsolator-Cst3vFjb.cjs.map → VitestKyselyTransactionIsolator-DX_VPKS-.cjs.map} +1 -1
  29. package/dist/{VitestKyselyTransactionIsolator-DYUYVEh9.d.cts → VitestKyselyTransactionIsolator-DiskaURs.d.mts} +2 -2
  30. package/dist/{VitestKyselyTransactionIsolator-BxjlD1YM.mjs → VitestKyselyTransactionIsolator-XDL3ngs_.mjs} +2 -2
  31. package/dist/{VitestKyselyTransactionIsolator-BxjlD1YM.mjs.map → VitestKyselyTransactionIsolator-XDL3ngs_.mjs.map} +1 -1
  32. package/dist/VitestKyselyTransactionIsolator.cjs +2 -2
  33. package/dist/VitestKyselyTransactionIsolator.d.cts +2 -2
  34. package/dist/VitestKyselyTransactionIsolator.d.mts +2 -2
  35. package/dist/VitestKyselyTransactionIsolator.mjs +2 -2
  36. package/dist/{VitestObjectionTransactionIsolator-b973r9O1.d.mts → VitestObjectionTransactionIsolator-CD2ucJpH.d.cts} +2 -2
  37. package/dist/{VitestObjectionTransactionIsolator-DzeF4UAq.cjs → VitestObjectionTransactionIsolator-D_tlOtq8.cjs} +2 -2
  38. package/dist/{VitestObjectionTransactionIsolator-DzeF4UAq.cjs.map → VitestObjectionTransactionIsolator-D_tlOtq8.cjs.map} +1 -1
  39. package/dist/{VitestObjectionTransactionIsolator-CJ4ds5Qv.d.cts → VitestObjectionTransactionIsolator-DhQ8XGva.d.mts} +2 -2
  40. package/dist/{VitestObjectionTransactionIsolator-BU-jXEhz.mjs → VitestObjectionTransactionIsolator-_EhJKu_O.mjs} +2 -2
  41. package/dist/{VitestObjectionTransactionIsolator-BU-jXEhz.mjs.map → VitestObjectionTransactionIsolator-_EhJKu_O.mjs.map} +1 -1
  42. package/dist/VitestObjectionTransactionIsolator.cjs +2 -2
  43. package/dist/VitestObjectionTransactionIsolator.d.cts +2 -2
  44. package/dist/VitestObjectionTransactionIsolator.d.mts +2 -2
  45. package/dist/VitestObjectionTransactionIsolator.mjs +2 -2
  46. package/dist/{VitestTransactionIsolator-CskiiJbW.mjs → VitestTransactionIsolator-BIaMs4c2.mjs} +40 -2
  47. package/dist/VitestTransactionIsolator-BIaMs4c2.mjs.map +1 -0
  48. package/dist/{VitestTransactionIsolator-BQ5FpLtC.cjs → VitestTransactionIsolator-BKIrj3Uy.cjs} +45 -1
  49. package/dist/VitestTransactionIsolator-BKIrj3Uy.cjs.map +1 -0
  50. package/dist/{VitestTransactionIsolator-CsfJBxcb.d.mts → VitestTransactionIsolator-DLdQlfZ5.d.mts} +70 -3
  51. package/dist/{VitestTransactionIsolator-DdLNODZg.d.cts → VitestTransactionIsolator-DfA80g2M.d.cts} +70 -3
  52. package/dist/VitestTransactionIsolator.cjs +3 -2
  53. package/dist/VitestTransactionIsolator.d.cts +2 -2
  54. package/dist/VitestTransactionIsolator.d.mts +2 -2
  55. package/dist/VitestTransactionIsolator.mjs +2 -2
  56. package/dist/better-auth.cjs +8 -11
  57. package/dist/better-auth.cjs.map +1 -1
  58. package/dist/better-auth.d.cts +2 -2
  59. package/dist/better-auth.d.mts +2 -2
  60. package/dist/better-auth.mjs +8 -11
  61. package/dist/better-auth.mjs.map +1 -1
  62. package/dist/{directory-B4oYx02C.d.mts → directory-Mi7tdOuD.d.cts} +3 -3
  63. package/dist/{directory-BUcnztHI.d.cts → directory-Q178x53k.d.mts} +3 -3
  64. package/dist/{faker-Cg76aFNO.d.cts → faker-BSH1EMtg.d.cts} +3 -3
  65. package/dist/{faker-Br8MzXil.d.mts → faker-D9gz7KjY.d.mts} +3 -3
  66. package/dist/faker.d.cts +1 -1
  67. package/dist/faker.d.mts +1 -1
  68. package/dist/kysely.cjs +58 -4
  69. package/dist/kysely.cjs.map +1 -1
  70. package/dist/kysely.d.cts +56 -6
  71. package/dist/kysely.d.mts +56 -6
  72. package/dist/kysely.mjs +57 -5
  73. package/dist/kysely.mjs.map +1 -1
  74. package/dist/objection.cjs +54 -4
  75. package/dist/objection.cjs.map +1 -1
  76. package/dist/objection.d.cts +52 -6
  77. package/dist/objection.d.mts +52 -6
  78. package/dist/objection.mjs +53 -5
  79. package/dist/objection.mjs.map +1 -1
  80. package/dist/os/directory.d.cts +1 -1
  81. package/dist/os/directory.d.mts +1 -1
  82. package/dist/os/index.d.cts +1 -1
  83. package/dist/os/index.d.mts +1 -1
  84. package/package.json +7 -3
  85. package/src/KyselyFactory.ts +29 -16
  86. package/src/ObjectionFactory.ts +34 -19
  87. package/src/VitestTransactionIsolator.ts +126 -2
  88. package/src/__tests__/KyselyFactory.spec.ts +10 -10
  89. package/src/__tests__/ObjectionFactory.spec.ts +9 -12
  90. package/src/__tests__/integration.spec.ts +171 -14
  91. package/src/better-auth.ts +13 -15
  92. package/src/kysely.ts +71 -0
  93. package/src/objection.ts +66 -0
  94. package/dist/KyselyFactory-BcYkC0t2.mjs.map +0 -1
  95. package/dist/KyselyFactory-Cf0o2YxO.cjs.map +0 -1
  96. package/dist/ObjectionFactory-8hebmnai.mjs.map +0 -1
  97. package/dist/ObjectionFactory-CDriunkS.cjs.map +0 -1
  98. package/dist/VitestTransactionIsolator-BQ5FpLtC.cjs.map +0 -1
  99. package/dist/VitestTransactionIsolator-CskiiJbW.mjs.map +0 -1
package/README.md CHANGED
@@ -6,20 +6,21 @@
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.8.2-blue)](https://www.typescriptlang.org)
7
7
  [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
8
8
 
9
- ## 🚀 Overview
9
+ ## Overview
10
10
 
11
11
  **@geekmidas/testkit** provides a comprehensive set of testing utilities designed to simplify database testing in TypeScript applications. It offers factory patterns for creating test data, supports multiple database libraries, and ensures type safety throughout your tests.
12
12
 
13
13
  ### Key Features
14
14
 
15
- - 🏭 **Factory Pattern**: Create test data with minimal boilerplate
16
- - 🔒 **Type Safety**: Full TypeScript support with automatic schema inference
17
- - 🗄️ **Multi-Database Support**: Works with Kysely and Objection.js
18
- - 🔄 **Transaction Isolation**: Built-in support for test isolation
19
- - 🚀 **Performance**: Efficient batch operations and data seeding
20
- - 🧩 **Flexible**: Extensible architecture for custom implementations
15
+ - **Factory Pattern**: Create test data with minimal boilerplate
16
+ - **Type Safety**: Full TypeScript support with automatic schema inference
17
+ - **Multi-Database Support**: Works with Kysely and Objection.js
18
+ - **Transaction Isolation**: Built-in support for test isolation
19
+ - **Enhanced Faker**: Extended faker with timestamps, sequences, and coordinates
20
+ - **AWS Mocks**: Mock Lambda contexts and API Gateway events
21
+ - **Better Auth**: In-memory adapter for authentication testing
21
22
 
22
- ## 📦 Installation
23
+ ## Installation
23
24
 
24
25
  ```bash
25
26
  npm install --save-dev @geekmidas/testkit
@@ -29,9 +30,35 @@ pnpm add -D @geekmidas/testkit
29
30
  yarn add -D @geekmidas/testkit
30
31
  ```
31
32
 
32
- ## 🛠️ Quick Start
33
+ ## Subpath Exports
33
34
 
34
- ### With Kysely
35
+ ```typescript
36
+ // Kysely utilities
37
+ import {
38
+ KyselyFactory,
39
+ wrapVitestKyselyTransaction,
40
+ extendWithFixtures,
41
+ } from '@geekmidas/testkit/kysely';
42
+
43
+ // Objection.js utilities
44
+ import {
45
+ ObjectionFactory,
46
+ wrapVitestObjectionTransaction,
47
+ extendWithFixtures,
48
+ } from '@geekmidas/testkit/objection';
49
+
50
+ // Other utilities
51
+ import { faker } from '@geekmidas/testkit/faker';
52
+ import { waitFor } from '@geekmidas/testkit/timer';
53
+ import { itWithDir } from '@geekmidas/testkit/os';
54
+ import { createMockContext, createMockV1Event, createMockV2Event } from '@geekmidas/testkit/aws';
55
+ import { createMockLogger } from '@geekmidas/testkit/logger';
56
+ import { memoryAdapter } from '@geekmidas/testkit/better-auth';
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ### Database Factories with Kysely
35
62
 
36
63
  ```typescript
37
64
  import { KyselyFactory } from '@geekmidas/testkit/kysely';
@@ -40,41 +67,43 @@ import { Kysely } from 'kysely';
40
67
  // Define your database schema
41
68
  interface Database {
42
69
  users: {
43
- id: number;
70
+ id: string;
44
71
  name: string;
45
72
  email: string;
46
73
  createdAt: Date;
47
74
  };
48
75
  posts: {
49
- id: number;
76
+ id: string;
50
77
  title: string;
51
78
  content: string;
52
- userId: number;
53
- publishedAt: Date | null;
79
+ userId: string;
54
80
  };
55
81
  }
56
82
 
57
83
  // Create builders for your tables
58
- const userBuilder = KyselyFactory.createBuilder<Database, 'users'>({
59
- table: 'users',
60
- defaults: async () => ({
61
- name: 'John Doe',
62
- email: `user${Date.now()}@example.com`,
63
- createdAt: new Date(),
64
- }),
65
- });
66
-
67
- const postBuilder = KyselyFactory.createBuilder<Database, 'posts'>({
68
- table: 'posts',
69
- defaults: async () => ({
70
- title: 'Test Post',
71
- content: 'Lorem ipsum dolor sit amet',
72
- publishedAt: null,
73
- }),
74
- });
84
+ const builders = {
85
+ user: KyselyFactory.createBuilder<Database, 'users'>(
86
+ 'users',
87
+ ({ attrs, faker }) => ({
88
+ id: faker.string.uuid(),
89
+ name: faker.person.fullName(),
90
+ email: faker.internet.email(),
91
+ createdAt: new Date(),
92
+ ...attrs,
93
+ })
94
+ ),
95
+ post: KyselyFactory.createBuilder<Database, 'posts'>(
96
+ 'posts',
97
+ ({ attrs, faker }) => ({
98
+ id: faker.string.uuid(),
99
+ title: 'Test Post',
100
+ content: faker.lorem.paragraph(),
101
+ ...attrs,
102
+ })
103
+ ),
104
+ };
75
105
 
76
106
  // Initialize factory
77
- const builders = { user: userBuilder, post: postBuilder };
78
107
  const factory = new KyselyFactory(builders, {}, db);
79
108
 
80
109
  // Use in tests
@@ -101,124 +130,200 @@ describe('User Service', () => {
101
130
  import { ObjectionFactory } from '@geekmidas/testkit/objection';
102
131
  import { Model } from 'objection';
103
132
 
104
- // Define your models
105
133
  class User extends Model {
106
134
  static tableName = 'users';
107
- id!: number;
135
+ id!: string;
108
136
  name!: string;
109
137
  email!: string;
110
138
  }
111
139
 
112
- class Post extends Model {
113
- static tableName = 'posts';
114
- id!: number;
115
- title!: string;
116
- userId!: number;
117
- }
118
-
119
- // Create builders
120
- const userBuilder = {
121
- table: 'users',
122
- model: User,
123
- defaults: async () => ({
124
- name: 'John Doe',
125
- email: `user${Date.now()}@example.com`,
126
- }),
140
+ const builders = {
141
+ user: ObjectionFactory.createBuilder(
142
+ User,
143
+ ({ attrs, faker }) => ({
144
+ id: faker.string.uuid(),
145
+ name: faker.person.fullName(),
146
+ email: faker.internet.email(),
147
+ ...attrs,
148
+ })
149
+ ),
127
150
  };
128
151
 
129
- // Use in tests
130
- const factory = new ObjectionFactory({ user: userBuilder }, {});
152
+ const factory = new ObjectionFactory(builders, {}, knex);
131
153
  const user = await factory.insert('user', { name: 'Jane Doe' });
132
154
  ```
133
155
 
134
- ## 🏗️ Core Concepts
156
+ ## Enhanced Faker
135
157
 
136
- ### Builders
158
+ The testkit provides an enhanced faker instance with additional utilities for common test data patterns.
137
159
 
138
- Builders define how to create test data for each table. They specify:
160
+ ```typescript
161
+ import { faker } from '@geekmidas/testkit/faker';
162
+
163
+ // Standard faker methods
164
+ const name = faker.person.fullName();
165
+ const email = faker.internet.email();
166
+
167
+ // Generate timestamps for database records
168
+ const { createdAt, updatedAt } = faker.timestamps();
169
+ // createdAt: Date in the past
170
+ // updatedAt: Date between createdAt and now
171
+
172
+ // Sequential numbers (useful for unique IDs)
173
+ faker.sequence(); // 1
174
+ faker.sequence(); // 2
175
+ faker.sequence('user'); // 1 (separate sequence)
176
+ faker.sequence('user'); // 2
177
+
178
+ // Reset sequences between tests
179
+ faker.resetSequence('user');
180
+ faker.resetAllSequences();
181
+
182
+ // Generate prices as numbers
183
+ const price = faker.price(); // 29.99
184
+
185
+ // Generate reverse domain identifiers
186
+ faker.identifier(); // "com.example.widget1"
187
+ faker.identifier('user'); // "org.acme.user"
188
+
189
+ // Generate coordinates within/outside a radius
190
+ const center = { lat: 40.7128, lng: -74.0060 };
191
+ faker.coordinates.within(center, 1000); // Within 1km
192
+ faker.coordinates.outside(center, 1000, 5000); // Between 1km and 5km
193
+ ```
139
194
 
140
- - **Table name**: The database table to insert into
141
- - **Default values**: Function returning default attributes
142
- - **Transformations**: Optional data transformations before insertion
143
- - **Relations**: Optional related data to create after insertion
195
+ ## Timer Utilities
196
+
197
+ Simple async wait utility for tests.
144
198
 
145
199
  ```typescript
146
- const userBuilder = KyselyFactory.createBuilder<Database, 'users'>({
147
- table: 'users',
148
- defaults: async () => ({
149
- id: generateId(),
150
- name: faker.person.fullName(),
151
- email: faker.internet.email(),
152
- createdAt: new Date(),
153
- }),
154
- transform: async (data) => ({
155
- ...data,
156
- email: data.email.toLowerCase(),
157
- }),
158
- relations: async (user, factory) => {
159
- // Create related data after user insertion
160
- await factory.insert('profile', { userId: user.id });
161
- },
200
+ import { waitFor } from '@geekmidas/testkit/timer';
201
+
202
+ it('should process after delay', async () => {
203
+ startBackgroundProcess();
204
+ await waitFor(100); // Wait 100ms
205
+ expect(processComplete).toBe(true);
162
206
  });
163
207
  ```
164
208
 
165
- ### Seeds
209
+ ## OS Utilities
166
210
 
167
- Seeds are functions that create complex test scenarios with multiple related entities:
211
+ Vitest fixture for temporary directory creation with automatic cleanup.
168
212
 
169
213
  ```typescript
170
- const blogSeed = async (factory: Factory) => {
171
- const author = await factory.insert('user', {
172
- name: 'Blog Author',
173
- role: 'author',
214
+ import { itWithDir } from '@geekmidas/testkit/os';
215
+
216
+ // Creates a temp directory before test, removes it after
217
+ itWithDir('should write files to temp dir', async ({ dir }) => {
218
+ const filePath = path.join(dir, 'test.txt');
219
+ await fs.writeFile(filePath, 'hello');
220
+
221
+ const content = await fs.readFile(filePath, 'utf-8');
222
+ expect(content).toBe('hello');
223
+ // Directory is automatically cleaned up after test
224
+ });
225
+ ```
226
+
227
+ ## AWS Testing Utilities
228
+
229
+ Mock AWS Lambda contexts and API Gateway events for testing Lambda handlers.
230
+
231
+ ```typescript
232
+ import {
233
+ createMockContext,
234
+ createMockV1Event,
235
+ createMockV2Event
236
+ } from '@geekmidas/testkit/aws';
237
+
238
+ describe('Lambda Handler', () => {
239
+ it('should handle API Gateway v1 event', async () => {
240
+ const event = createMockV1Event({
241
+ httpMethod: 'POST',
242
+ path: '/users',
243
+ body: JSON.stringify({ name: 'John' }),
244
+ });
245
+ const context = createMockContext();
246
+
247
+ const result = await handler(event, context);
248
+ expect(result.statusCode).toBe(201);
174
249
  });
175
250
 
176
- const categories = await factory.insertMany(3, 'category');
251
+ it('should handle API Gateway v2 event', async () => {
252
+ const event = createMockV2Event({
253
+ routeKey: 'POST /users',
254
+ rawPath: '/users',
255
+ body: JSON.stringify({ name: 'John' }),
256
+ });
257
+ const context = createMockContext();
177
258
 
178
- const posts = await factory.insertMany(5, 'post', (index) => ({
179
- title: `Post ${index + 1}`,
180
- authorId: author.id,
181
- categoryId: categories[index % categories.length].id,
182
- }));
259
+ const result = await handler(event, context);
260
+ expect(result.statusCode).toBe(201);
261
+ });
262
+ });
263
+ ```
183
264
 
184
- return { author, categories, posts };
185
- };
265
+ ## Logger Testing Utilities
186
266
 
187
- // Use in tests
188
- const data = await factory.seed('blog');
267
+ Create mock loggers for testing code that uses `@geekmidas/logger`.
268
+
269
+ ```typescript
270
+ import { createMockLogger } from '@geekmidas/testkit/logger';
271
+
272
+ describe('Service', () => {
273
+ it('should log errors', async () => {
274
+ const logger = createMockLogger();
275
+ const service = new MyService(logger);
276
+
277
+ await service.doSomethingRisky();
278
+
279
+ expect(logger.error).toHaveBeenCalledWith(
280
+ expect.objectContaining({ error: expect.any(Error) }),
281
+ 'Operation failed'
282
+ );
283
+ });
284
+ });
189
285
  ```
190
286
 
191
- ### Transaction Support
287
+ ## Better Auth Testing
192
288
 
193
- TestKit supports transaction-based test isolation:
289
+ In-memory adapter for testing Better Auth without a real database.
194
290
 
195
291
  ```typescript
196
- describe('User Service', () => {
197
- let trx: Transaction<Database>;
198
- let factory: KyselyFactory;
292
+ import { memoryAdapter } from '@geekmidas/testkit/better-auth';
293
+ import { betterAuth } from 'better-auth';
294
+
295
+ describe('Authentication', () => {
296
+ const adapter = memoryAdapter({
297
+ debugLogs: false,
298
+ initialData: {
299
+ user: [{ id: '1', email: 'test@example.com', name: 'Test User' }],
300
+ },
301
+ });
199
302
 
200
- beforeEach(async () => {
201
- trx = await db.transaction();
202
- factory = new KyselyFactory(builders, seeds, trx);
303
+ const auth = betterAuth({
304
+ database: adapter,
305
+ // ... other config
203
306
  });
204
307
 
205
- afterEach(async () => {
206
- await trx.rollback();
308
+ afterEach(() => {
309
+ adapter.clear(); // Reset data between tests
207
310
  });
208
311
 
209
- it('should perform operations in isolation', async () => {
210
- const user = await factory.insert('user');
211
- // Test operations...
212
- // All changes will be rolled back after the test
312
+ it('should create user', async () => {
313
+ await auth.api.signUp({
314
+ email: 'new@example.com',
315
+ password: 'password123',
316
+ });
317
+
318
+ const data = adapter.getAllData();
319
+ expect(data.user).toHaveLength(2);
213
320
  });
214
321
  });
215
322
  ```
216
323
 
217
- ## 📚 Advanced Usage
218
-
219
- ### Database Migration
324
+ ## Database Migration
220
325
 
221
- TestKit includes utilities for managing test database migrations:
326
+ TestKit includes utilities for managing test database migrations.
222
327
 
223
328
  ```typescript
224
329
  import { PostgresKyselyMigrator } from '@geekmidas/testkit/kysely';
@@ -237,147 +342,294 @@ const migrator = new PostgresKyselyMigrator({
237
342
  // In test setup
238
343
  beforeAll(async () => {
239
344
  const cleanup = await migrator.start();
240
- // Database is created and migrations are run
241
-
242
- // Store cleanup function for later
243
345
  globalThis.cleanupDb = cleanup;
244
346
  });
245
347
 
246
348
  afterAll(async () => {
247
349
  await globalThis.cleanupDb?.();
248
- // Database is dropped
249
350
  });
250
351
  ```
251
352
 
252
- ### Custom Factories
353
+ ## Vitest Transaction Isolation
253
354
 
254
- You can extend the base Factory class for custom implementations:
355
+ TestKit provides Vitest-specific helpers for automatic transaction isolation. Each test runs in a transaction that is automatically rolled back after the test completes.
356
+
357
+ ### Basic Usage
255
358
 
256
359
  ```typescript
257
- import { Factory } from '@geekmidas/testkit/factory';
360
+ import { test } from 'vitest';
361
+ import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
362
+ import { db } from './database';
363
+
364
+ // Wrap Vitest's test function with transaction support
365
+ const it = wrapVitestKyselyTransaction<Database>(
366
+ test,
367
+ () => db,
368
+ async (trx) => {
369
+ // Optional: Set up test tables or seed data
370
+ await trx.schema.createTable('users').execute();
371
+ }
372
+ );
373
+
374
+ // Each test gets its own transaction
375
+ it('should create user', async ({ trx }) => {
376
+ const user = await trx
377
+ .insertInto('users')
378
+ .values({ name: 'John' })
379
+ .returningAll()
380
+ .executeTakeFirst();
381
+
382
+ expect(user.name).toBe('John');
383
+ // Transaction is automatically rolled back after test
384
+ });
385
+ ```
386
+
387
+ ### Extending with Fixtures
258
388
 
259
- class MongoFactory extends Factory {
260
- async performInsert(table: string, data: any) {
261
- const collection = this.db.collection(table);
262
- const result = await collection.insertOne(data);
263
- return { ...data, _id: result.insertedId };
389
+ Use `extendWithFixtures` to add factory and other fixtures to your tests:
390
+
391
+ ```typescript
392
+ import { test } from 'vitest';
393
+ import {
394
+ wrapVitestKyselyTransaction,
395
+ extendWithFixtures,
396
+ KyselyFactory,
397
+ } from '@geekmidas/testkit/kysely';
398
+
399
+ // Define builders
400
+ const builders = {
401
+ user: KyselyFactory.createBuilder<Database, 'users'>('users', ({ faker }) => ({
402
+ name: faker.person.fullName(),
403
+ email: faker.internet.email(),
404
+ })),
405
+ post: KyselyFactory.createBuilder<Database, 'posts'>('posts', ({ faker }) => ({
406
+ title: faker.lorem.sentence(),
407
+ content: faker.lorem.paragraphs(),
408
+ })),
409
+ };
410
+
411
+ // Create base test with transaction
412
+ const baseTest = wrapVitestKyselyTransaction<Database>(test, () => db);
413
+
414
+ // Extend with factory fixture
415
+ const it = extendWithFixtures<
416
+ Database,
417
+ { factory: KyselyFactory<Database, typeof builders, {}> }
418
+ >(baseTest, {
419
+ factory: (trx) => new KyselyFactory(builders, {}, trx),
420
+ });
421
+
422
+ // Both trx and factory are available in tests
423
+ it('should create user with factory', async ({ trx, factory }) => {
424
+ const user = await factory.insert('user', { name: 'Jane' });
425
+
426
+ expect(user.id).toBeDefined();
427
+ expect(user.name).toBe('Jane');
428
+
429
+ // Verify in database
430
+ const found = await trx
431
+ .selectFrom('users')
432
+ .where('id', '=', user.id)
433
+ .selectAll()
434
+ .executeTakeFirst();
435
+
436
+ expect(found?.name).toBe('Jane');
437
+ });
438
+
439
+ it('should create related records', async ({ factory }) => {
440
+ const user = await factory.insert('user');
441
+ const posts = await factory.insertMany(3, 'post', { userId: user.id });
442
+
443
+ expect(posts).toHaveLength(3);
444
+ expect(posts[0].userId).toBe(user.id);
445
+ });
446
+ ```
447
+
448
+ ### Multiple Fixtures
449
+
450
+ You can add multiple fixtures that all receive the transaction:
451
+
452
+ ```typescript
453
+ const it = extendWithFixtures<
454
+ Database,
455
+ {
456
+ factory: KyselyFactory<Database, typeof builders, {}>;
457
+ userRepo: UserRepository;
458
+ config: { maxUsers: number };
264
459
  }
460
+ >(baseTest, {
461
+ factory: (trx) => new KyselyFactory(builders, {}, trx),
462
+ userRepo: (trx) => new UserRepository(trx),
463
+ config: () => ({ maxUsers: 100 }), // Fixtures can ignore trx if not needed
464
+ });
465
+
466
+ it('should use multiple fixtures', async ({ factory, userRepo, config }) => {
467
+ const user = await factory.insert('user');
468
+ const found = await userRepo.findById(user.id);
469
+ expect(found).toBeDefined();
470
+ expect(config.maxUsers).toBe(100);
471
+ });
472
+ ```
473
+
474
+ ### With Objection.js
475
+
476
+ ```typescript
477
+ import { wrapVitestObjectionTransaction, extendWithFixtures } from '@geekmidas/testkit/objection';
478
+
479
+ const baseTest = wrapVitestObjectionTransaction(test, () => knex);
265
480
 
266
- async performInsertMany(table: string, data: any[]) {
267
- const collection = this.db.collection(table);
268
- const result = await collection.insertMany(data);
269
- return data.map((item, index) => ({
270
- ...item,
271
- _id: result.insertedIds[index],
272
- }));
481
+ const it = extendWithFixtures<{ factory: ObjectionFactory<typeof builders, {}> }>(
482
+ baseTest,
483
+ {
484
+ factory: (trx) => new ObjectionFactory(builders, {}, trx),
273
485
  }
274
- }
486
+ );
275
487
  ```
276
488
 
277
- ### Dynamic Attributes
489
+ ## Manual Transaction Isolation
278
490
 
279
- Create dynamic attributes for each record in batch operations:
491
+ For more control, you can manage transactions manually:
280
492
 
281
493
  ```typescript
282
- const users = await factory.insertMany(10, 'user', (index) => ({
283
- name: `User ${index + 1}`,
284
- email: `user${index + 1}@example.com`,
285
- isAdmin: index === 0, // First user is admin
286
- }));
494
+ describe('User Service', () => {
495
+ let trx: Transaction<Database>;
496
+ let factory: KyselyFactory;
497
+
498
+ beforeEach(async () => {
499
+ trx = await db.transaction();
500
+ factory = new KyselyFactory(builders, seeds, trx);
501
+ });
502
+
503
+ afterEach(async () => {
504
+ await trx.rollback();
505
+ });
506
+
507
+ it('should perform operations in isolation', async () => {
508
+ const user = await factory.insert('user');
509
+ // All changes will be rolled back after the test
510
+ });
511
+ });
287
512
  ```
288
513
 
289
- ### Conditional Auto-insertion
514
+ ## Seeds
290
515
 
291
- Control whether builders automatically insert data:
516
+ Seeds are functions that create complex test scenarios:
292
517
 
293
518
  ```typescript
294
- const draftBuilder = KyselyFactory.createBuilder<Database, 'posts'>({
295
- table: 'posts',
296
- defaults: async () => ({
297
- title: 'Draft Post',
298
- status: 'draft',
299
- }),
300
- autoInsert: false, // Don't insert automatically
301
- });
519
+ const blogSeed = async (factory: Factory) => {
520
+ const author = await factory.insert('user', {
521
+ name: 'Blog Author',
522
+ role: 'author',
523
+ });
524
+
525
+ const categories = await factory.insertMany(3, 'category');
526
+
527
+ const posts = await factory.insertMany(5, 'post', (index) => ({
528
+ title: `Post ${index + 1}`,
529
+ authorId: author.id,
530
+ categoryId: categories[index % categories.length].id,
531
+ }));
532
+
533
+ return { author, categories, posts };
534
+ };
302
535
 
303
- // Manually handle the data
304
- const draftData = await draftBuilder.build();
305
- // Perform validation or modifications...
306
- const post = await db.insertInto('posts').values(draftData).execute();
536
+ // Use in tests
537
+ const data = await factory.seed('blog');
307
538
  ```
308
539
 
309
- ## 🔧 API Reference
540
+ ## API Reference
310
541
 
311
542
  ### KyselyFactory
312
543
 
313
544
  ```typescript
314
- class KyselyFactory<TBuilders, TSeeds> extends Factory {
545
+ class KyselyFactory<DB, Builders, Seeds> {
315
546
  constructor(
316
- builders: TBuilders,
317
- seeds: TSeeds,
318
- db: Kysely<any> | Transaction<any>
547
+ builders: Builders,
548
+ seeds: Seeds,
549
+ db: Kysely<DB> | ControlledTransaction<DB>
319
550
  );
320
551
 
321
- static createBuilder<TDatabase, TTable>(
322
- config: BuilderConfig<TDatabase, TTable>
323
- ): Builder;
324
-
325
- insert<K extends keyof TBuilders>(
326
- name: K,
327
- overrides?: Partial<BuilderOutput>
328
- ): Promise<BuilderOutput>;
329
-
330
- insertMany<K extends keyof TBuilders>(
552
+ static createBuilder<DB, TableName extends keyof DB & string>(
553
+ table: TableName,
554
+ defaults?: (context: {
555
+ attrs: Partial<Insertable<DB[TableName]>>;
556
+ factory: KyselyFactory;
557
+ db: Kysely<DB>;
558
+ faker: FakerFactory;
559
+ }) => Partial<Insertable<DB[TableName]>> | Promise<...>,
560
+ autoInsert?: boolean
561
+ ): BuilderFunction;
562
+
563
+ insert<K extends keyof Builders>(
564
+ builderName: K,
565
+ attrs?: Partial<BuilderAttrs>
566
+ ): Promise<BuilderResult>;
567
+
568
+ insertMany<K extends keyof Builders>(
331
569
  count: number,
332
- name: K,
333
- overrides?: Partial<BuilderOutput> | ((index: number) => Partial<BuilderOutput>)
334
- ): Promise<BuilderOutput[]>;
335
-
336
- seed<K extends keyof TSeeds>(
337
- name: K,
338
- ...args: Parameters<TSeeds[K]>
339
- ): Promise<ReturnType<TSeeds[K]>>;
570
+ builderName: K,
571
+ attrs?: Partial<BuilderAttrs> | ((idx: number, faker: FakerFactory) => Partial<BuilderAttrs>)
572
+ ): Promise<BuilderResult[]>;
573
+
574
+ seed<K extends keyof Seeds>(
575
+ seedName: K,
576
+ attrs?: SeedAttrs
577
+ ): Promise<SeedResult>;
340
578
  }
341
579
  ```
342
580
 
343
- ### ObjectionFactory
581
+ ### Enhanced Faker
344
582
 
345
583
  ```typescript
346
- class ObjectionFactory<TBuilders, TSeeds> extends Factory {
347
- constructor(
348
- builders: TBuilders,
349
- seeds: TSeeds,
350
- knex?: Knex
351
- );
352
-
353
- // Same methods as KyselyFactory
584
+ interface EnhancedFaker extends Faker {
585
+ timestamps(): { createdAt: Date; updatedAt: Date };
586
+ sequence(name?: string): number;
587
+ resetSequence(name?: string, value?: number): void;
588
+ resetAllSequences(): void;
589
+ identifier(suffix?: string): string;
590
+ price(): number;
591
+ coordinates: {
592
+ within(center: Coordinate, radiusMeters: number): Coordinate;
593
+ outside(center: Coordinate, minRadius: number, maxRadius: number): Coordinate;
594
+ };
354
595
  }
355
596
  ```
356
597
 
357
- ### Builder Configuration
598
+ ### AWS Mocks
358
599
 
359
600
  ```typescript
360
- interface BuilderConfig<TDatabase, TTable> {
361
- table: TTable;
362
- defaults: () => Promise<Insertable<TDatabase[TTable]>>;
363
- transform?: (data: any) => Promise<any>;
364
- relations?: (inserted: any, factory: Factory) => Promise<void>;
365
- autoInsert?: boolean;
366
- }
601
+ function createMockContext(): Context;
602
+ function createMockV1Event(overrides?: Partial<APIGatewayProxyEvent>): APIGatewayProxyEvent;
603
+ function createMockV2Event(overrides?: Partial<APIGatewayProxyEventV2>): APIGatewayProxyEventV2;
604
+ ```
605
+
606
+ ### Memory Adapter (Better Auth)
607
+
608
+ ```typescript
609
+ function memoryAdapter(config?: {
610
+ debugLogs?: boolean;
611
+ usePlural?: boolean;
612
+ initialData?: Record<string, any[]>;
613
+ }): DatabaseAdapter & {
614
+ clear(): void;
615
+ getAllData(): Record<string, any[]>;
616
+ getStore(): Map<string, any>;
617
+ };
367
618
  ```
368
619
 
369
- ## 🧪 Testing Best Practices
620
+ ## Testing Best Practices
370
621
 
371
622
  1. **Use Transactions**: Always wrap tests in transactions for isolation
372
623
  2. **Create Minimal Data**: Only create the data necessary for each test
373
624
  3. **Use Seeds for Complex Scenarios**: Encapsulate complex setups in seeds
374
625
  4. **Leverage Type Safety**: Let TypeScript catch schema mismatches
375
626
  5. **Clean Up Resources**: Always clean up database connections and transactions
627
+ 6. **Reset Sequences**: Call `faker.resetAllSequences()` in `beforeEach` for predictable IDs
376
628
 
377
- ## 🤝 Contributing
629
+ ## Contributing
378
630
 
379
631
  We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details.
380
632
 
381
- ## 📄 License
633
+ ## License
382
634
 
383
- This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
635
+ This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.