@geekmidas/testkit 0.0.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.
- package/README.md +383 -0
- package/package.json +26 -0
- package/src/Factory.ts +58 -0
- package/src/KyselyFactory.ts +183 -0
- package/src/KyselyPostgresMigrator.ts +0 -0
- package/src/ObjectionFactory.ts +66 -0
- package/src/PostgresKyselyMigrator.ts +33 -0
- package/src/PostgresMigrator.ts +76 -0
- package/src/__tests__/KyselyFactory.spec.ts +83 -0
- package/src/example.ts +45 -0
- package/test/globalSetup.ts +53 -0
- package/test/migrations/1749664623372_user.ts +22 -0
- package/vitest.config.ts +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# @geekmidas/testkit
|
|
2
|
+
|
|
3
|
+
> Type-safe testing utilities and database factories for modern TypeScript applications
|
|
4
|
+
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](https://www.typescriptlang.org)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## 🚀 Overview
|
|
10
|
+
|
|
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
|
+
|
|
13
|
+
### Key Features
|
|
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
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install --save-dev @geekmidas/testkit
|
|
26
|
+
# or
|
|
27
|
+
pnpm add -D @geekmidas/testkit
|
|
28
|
+
# or
|
|
29
|
+
yarn add -D @geekmidas/testkit
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 🛠️ Quick Start
|
|
33
|
+
|
|
34
|
+
### With Kysely
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { KyselyFactory } from '@geekmidas/testkit/kysely';
|
|
38
|
+
import { Kysely } from 'kysely';
|
|
39
|
+
|
|
40
|
+
// Define your database schema
|
|
41
|
+
interface Database {
|
|
42
|
+
users: {
|
|
43
|
+
id: number;
|
|
44
|
+
name: string;
|
|
45
|
+
email: string;
|
|
46
|
+
createdAt: Date;
|
|
47
|
+
};
|
|
48
|
+
posts: {
|
|
49
|
+
id: number;
|
|
50
|
+
title: string;
|
|
51
|
+
content: string;
|
|
52
|
+
userId: number;
|
|
53
|
+
publishedAt: Date | null;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 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
|
+
});
|
|
75
|
+
|
|
76
|
+
// Initialize factory
|
|
77
|
+
const builders = { user: userBuilder, post: postBuilder };
|
|
78
|
+
const factory = new KyselyFactory(builders, {}, db);
|
|
79
|
+
|
|
80
|
+
// Use in tests
|
|
81
|
+
describe('User Service', () => {
|
|
82
|
+
it('should create a user with posts', async () => {
|
|
83
|
+
const user = await factory.insert('user', {
|
|
84
|
+
name: 'Jane Smith',
|
|
85
|
+
email: 'jane@example.com',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const posts = await factory.insertMany(3, 'post', {
|
|
89
|
+
userId: user.id,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(posts).toHaveLength(3);
|
|
93
|
+
expect(posts[0].userId).toBe(user.id);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### With Objection.js
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { ObjectionFactory } from '@geekmidas/testkit/objection';
|
|
102
|
+
import { Model } from 'objection';
|
|
103
|
+
|
|
104
|
+
// Define your models
|
|
105
|
+
class User extends Model {
|
|
106
|
+
static tableName = 'users';
|
|
107
|
+
id!: number;
|
|
108
|
+
name!: string;
|
|
109
|
+
email!: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
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
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Use in tests
|
|
130
|
+
const factory = new ObjectionFactory({ user: userBuilder }, {});
|
|
131
|
+
const user = await factory.insert('user', { name: 'Jane Doe' });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 🏗️ Core Concepts
|
|
135
|
+
|
|
136
|
+
### Builders
|
|
137
|
+
|
|
138
|
+
Builders define how to create test data for each table. They specify:
|
|
139
|
+
|
|
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
|
|
144
|
+
|
|
145
|
+
```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
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Seeds
|
|
166
|
+
|
|
167
|
+
Seeds are functions that create complex test scenarios with multiple related entities:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const blogSeed = async (factory: Factory) => {
|
|
171
|
+
const author = await factory.insert('user', {
|
|
172
|
+
name: 'Blog Author',
|
|
173
|
+
role: 'author',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const categories = await factory.insertMany(3, 'category');
|
|
177
|
+
|
|
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
|
+
}));
|
|
183
|
+
|
|
184
|
+
return { author, categories, posts };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Use in tests
|
|
188
|
+
const data = await factory.seed('blog');
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Transaction Support
|
|
192
|
+
|
|
193
|
+
TestKit supports transaction-based test isolation:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
describe('User Service', () => {
|
|
197
|
+
let trx: Transaction<Database>;
|
|
198
|
+
let factory: KyselyFactory;
|
|
199
|
+
|
|
200
|
+
beforeEach(async () => {
|
|
201
|
+
trx = await db.transaction();
|
|
202
|
+
factory = new KyselyFactory(builders, seeds, trx);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
afterEach(async () => {
|
|
206
|
+
await trx.rollback();
|
|
207
|
+
});
|
|
208
|
+
|
|
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
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## 📚 Advanced Usage
|
|
218
|
+
|
|
219
|
+
### Database Migration
|
|
220
|
+
|
|
221
|
+
TestKit includes utilities for managing test database migrations:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { PostgresKyselyMigrator } from '@geekmidas/testkit/migrator/postgres-kysely';
|
|
225
|
+
|
|
226
|
+
const migrator = new PostgresKyselyMigrator({
|
|
227
|
+
database: 'test_db',
|
|
228
|
+
connection: {
|
|
229
|
+
host: 'localhost',
|
|
230
|
+
port: 5432,
|
|
231
|
+
user: 'postgres',
|
|
232
|
+
password: 'password',
|
|
233
|
+
},
|
|
234
|
+
migrationFolder: './migrations',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// In test setup
|
|
238
|
+
beforeAll(async () => {
|
|
239
|
+
const cleanup = await migrator.start();
|
|
240
|
+
// Database is created and migrations are run
|
|
241
|
+
|
|
242
|
+
// Store cleanup function for later
|
|
243
|
+
globalThis.cleanupDb = cleanup;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
afterAll(async () => {
|
|
247
|
+
await globalThis.cleanupDb?.();
|
|
248
|
+
// Database is dropped
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Custom Factories
|
|
253
|
+
|
|
254
|
+
You can extend the base Factory class for custom implementations:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { Factory } from '@geekmidas/testkit/factory';
|
|
258
|
+
|
|
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 };
|
|
264
|
+
}
|
|
265
|
+
|
|
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
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Dynamic Attributes
|
|
278
|
+
|
|
279
|
+
Create dynamic attributes for each record in batch operations:
|
|
280
|
+
|
|
281
|
+
```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
|
+
}));
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Conditional Auto-insertion
|
|
290
|
+
|
|
291
|
+
Control whether builders automatically insert data:
|
|
292
|
+
|
|
293
|
+
```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
|
+
});
|
|
302
|
+
|
|
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();
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## 🔧 API Reference
|
|
310
|
+
|
|
311
|
+
### KyselyFactory
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
class KyselyFactory<TBuilders, TSeeds> extends Factory {
|
|
315
|
+
constructor(
|
|
316
|
+
builders: TBuilders,
|
|
317
|
+
seeds: TSeeds,
|
|
318
|
+
db: Kysely<any> | Transaction<any>
|
|
319
|
+
);
|
|
320
|
+
|
|
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>(
|
|
331
|
+
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]>>;
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### ObjectionFactory
|
|
344
|
+
|
|
345
|
+
```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
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Builder Configuration
|
|
358
|
+
|
|
359
|
+
```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
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## 🧪 Testing Best Practices
|
|
370
|
+
|
|
371
|
+
1. **Use Transactions**: Always wrap tests in transactions for isolation
|
|
372
|
+
2. **Create Minimal Data**: Only create the data necessary for each test
|
|
373
|
+
3. **Use Seeds for Complex Scenarios**: Encapsulate complex setups in seeds
|
|
374
|
+
4. **Leverage Type Safety**: Let TypeScript catch schema mismatches
|
|
375
|
+
5. **Clean Up Resources**: Always clean up database connections and transactions
|
|
376
|
+
|
|
377
|
+
## 🤝 Contributing
|
|
378
|
+
|
|
379
|
+
We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details.
|
|
380
|
+
|
|
381
|
+
## 📄 License
|
|
382
|
+
|
|
383
|
+
This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geekmidas/testkit",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./objection": "./src/ObjectionFactory.ts",
|
|
8
|
+
"./kysely": "./src/KyselyFactory.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/pg": "~8.15.4"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"registry": "https://registry.npmjs.org/",
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"kysely": "~0.28.2",
|
|
20
|
+
"pg": "~8.16.3",
|
|
21
|
+
"knex": "~3.1.0",
|
|
22
|
+
"objection": "~3.1.5",
|
|
23
|
+
"db-errors": "~0.2.3",
|
|
24
|
+
"@geekmidas/envkit": "0.0.1"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/Factory.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export abstract class Factory<
|
|
2
|
+
Builders extends Record<string, any>,
|
|
3
|
+
Seeds extends Record<string, any>,
|
|
4
|
+
> {
|
|
5
|
+
static createSeed<Seed extends FactorySeed>(seedFn: Seed): Seed {
|
|
6
|
+
return seedFn;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Inserts an object into the database using a builder function.
|
|
10
|
+
*
|
|
11
|
+
* @param builderName - The name of the builder to use
|
|
12
|
+
* @param attrs - The attributes to insert
|
|
13
|
+
*/
|
|
14
|
+
abstract insert<K extends keyof Builders>(
|
|
15
|
+
builderName: K,
|
|
16
|
+
attrs?: Parameters<Builders[K]>[0],
|
|
17
|
+
): Promise<Awaited<ReturnType<Builders[K]>>>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Inserts multiple objects into the database
|
|
21
|
+
*
|
|
22
|
+
* @param count - Number of objects to insert
|
|
23
|
+
* @param builderName - The name of the builder to use
|
|
24
|
+
* @param attrs - The attributes to insert
|
|
25
|
+
*/
|
|
26
|
+
abstract insertMany<K extends keyof Builders>(
|
|
27
|
+
count: number,
|
|
28
|
+
builderName: K,
|
|
29
|
+
attrs?:
|
|
30
|
+
| Parameters<Builders[K]>[0]
|
|
31
|
+
| ((idx: number) => Parameters<Builders[K]>[0]),
|
|
32
|
+
): Promise<Awaited<ReturnType<Builders[K]>>[]>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Seeds the database using a seed function.
|
|
36
|
+
*
|
|
37
|
+
* @param seedName - The name of the seed to use
|
|
38
|
+
* @returns The result of the seed function
|
|
39
|
+
* @param attrs - The attributes to pass to the seed function
|
|
40
|
+
*/
|
|
41
|
+
abstract seed<K extends keyof Seeds>(
|
|
42
|
+
seedName: K,
|
|
43
|
+
attrs?: Parameters<Seeds[K]>[0],
|
|
44
|
+
): ReturnType<Seeds[K]>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type MixedFactoryBuilder<
|
|
48
|
+
Attrs = any,
|
|
49
|
+
Factory = any,
|
|
50
|
+
Result = any,
|
|
51
|
+
DB = any,
|
|
52
|
+
> = (attrs: Attrs, factory: Factory, db: DB) => Result | Promise<Result>;
|
|
53
|
+
|
|
54
|
+
export type FactorySeed<Attrs = any, Factory = any, Result = any, DB = any> = (
|
|
55
|
+
attrs: Attrs,
|
|
56
|
+
factory: Factory,
|
|
57
|
+
db: DB,
|
|
58
|
+
) => Promise<Result>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ControlledTransaction,
|
|
3
|
+
Insertable,
|
|
4
|
+
Kysely,
|
|
5
|
+
Selectable,
|
|
6
|
+
} from 'kysely';
|
|
7
|
+
import { Factory, type FactorySeed } from './Factory.ts';
|
|
8
|
+
|
|
9
|
+
export class KyselyFactory<
|
|
10
|
+
DB,
|
|
11
|
+
Builders extends Record<string, any>,
|
|
12
|
+
Seeds extends Record<string, any>,
|
|
13
|
+
> extends Factory<Builders, Seeds> {
|
|
14
|
+
static createSeed<Seed extends FactorySeed>(seedFn: Seed): Seed {
|
|
15
|
+
return Factory.createSeed(seedFn);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private builders: Builders,
|
|
20
|
+
private seeds: Seeds,
|
|
21
|
+
private db: Kysely<DB> | ControlledTransaction<DB, []>,
|
|
22
|
+
) {
|
|
23
|
+
super();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static createBuilder<
|
|
27
|
+
DB,
|
|
28
|
+
TableName extends keyof DB & string,
|
|
29
|
+
Attrs extends Partial<Insertable<DB[TableName]>> = Partial<
|
|
30
|
+
Insertable<DB[TableName]>
|
|
31
|
+
>,
|
|
32
|
+
Factory = any,
|
|
33
|
+
Result = Selectable<DB[TableName]>,
|
|
34
|
+
>(config: {
|
|
35
|
+
table: TableName;
|
|
36
|
+
defaults?: (
|
|
37
|
+
attrs: Attrs,
|
|
38
|
+
factory: Factory,
|
|
39
|
+
db: Kysely<DB>,
|
|
40
|
+
) =>
|
|
41
|
+
| Partial<Insertable<DB[TableName]>>
|
|
42
|
+
| Promise<Partial<Insertable<DB[TableName]>>>;
|
|
43
|
+
transform?: (
|
|
44
|
+
data: Partial<Insertable<DB[TableName]>>,
|
|
45
|
+
factory: Factory,
|
|
46
|
+
db: Kysely<DB>,
|
|
47
|
+
) =>
|
|
48
|
+
| Partial<Insertable<DB[TableName]>>
|
|
49
|
+
| Promise<Partial<Insertable<DB[TableName]>>>;
|
|
50
|
+
relations?: (
|
|
51
|
+
record: Result,
|
|
52
|
+
attrs: Attrs,
|
|
53
|
+
factory: Factory,
|
|
54
|
+
db: Kysely<DB>,
|
|
55
|
+
) => Promise<void>;
|
|
56
|
+
autoInsert?: boolean;
|
|
57
|
+
}): (attrs: Attrs, factory: Factory, db: Kysely<DB>) => Promise<Result> {
|
|
58
|
+
return async (attrs: Attrs, factory: Factory, db: Kysely<DB>) => {
|
|
59
|
+
// Start with attributes
|
|
60
|
+
let data: Partial<Insertable<DB[TableName]>> = { ...attrs };
|
|
61
|
+
|
|
62
|
+
// Apply defaults
|
|
63
|
+
if (config.defaults) {
|
|
64
|
+
const defaults = await config.defaults(attrs, factory, db);
|
|
65
|
+
data = { ...defaults, ...data };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Apply transformations
|
|
69
|
+
if (config.transform) {
|
|
70
|
+
data = await config.transform(data, factory, db);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle insertion based on autoInsert flag
|
|
74
|
+
if (config.autoInsert !== false) {
|
|
75
|
+
// Auto insert is enabled by default
|
|
76
|
+
const result = await db
|
|
77
|
+
.insertInto(config.table)
|
|
78
|
+
.values(data as Insertable<DB[TableName]>)
|
|
79
|
+
.returningAll()
|
|
80
|
+
.executeTakeFirst();
|
|
81
|
+
|
|
82
|
+
if (!result) {
|
|
83
|
+
throw new Error(`Failed to insert into ${config.table}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle relations if defined
|
|
87
|
+
if (config.relations) {
|
|
88
|
+
await config.relations(result as Result, attrs, factory, db);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result as Result;
|
|
92
|
+
} else {
|
|
93
|
+
// Return object for factory to handle insertion
|
|
94
|
+
return { table: config.table, data } as any;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async insert<K extends keyof Builders>(
|
|
100
|
+
builderName: K,
|
|
101
|
+
attrs?: Parameters<Builders[K]>[0],
|
|
102
|
+
): Promise<Awaited<ReturnType<Builders[K]>>> {
|
|
103
|
+
if (!(builderName in this.builders)) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Factory "${
|
|
106
|
+
builderName as string
|
|
107
|
+
}" does not exist. Make sure it is correct and registered in src/test/setup.ts`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = await this.builders[builderName](attrs || {}, this, this.db);
|
|
112
|
+
|
|
113
|
+
// For Kysely, we expect the builder to return an object with table and data properties
|
|
114
|
+
// or to handle the insertion itself and return the inserted record
|
|
115
|
+
if (
|
|
116
|
+
result &&
|
|
117
|
+
typeof result === 'object' &&
|
|
118
|
+
'table' in result &&
|
|
119
|
+
'data' in result
|
|
120
|
+
) {
|
|
121
|
+
// If the builder returns {table: string, data: object}, we insert it
|
|
122
|
+
const inserted = await this.db
|
|
123
|
+
.insertInto(result.table)
|
|
124
|
+
.values(result.data)
|
|
125
|
+
.returningAll()
|
|
126
|
+
.executeTakeFirst();
|
|
127
|
+
|
|
128
|
+
return inserted as any;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Otherwise, assume the builder handled the insertion itself
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Method overloads for better type inference
|
|
136
|
+
async insertMany<K extends keyof Builders>(
|
|
137
|
+
count: number,
|
|
138
|
+
builderName: K,
|
|
139
|
+
attrs?: Parameters<Builders[K]>[0],
|
|
140
|
+
): Promise<Awaited<ReturnType<Builders[K]>>[]>;
|
|
141
|
+
async insertMany<K extends keyof Builders>(
|
|
142
|
+
count: number,
|
|
143
|
+
builderName: K,
|
|
144
|
+
attrs: (idx: number) => Parameters<Builders[K]>[0],
|
|
145
|
+
): Promise<Awaited<ReturnType<Builders[K]>>[]>;
|
|
146
|
+
async insertMany<K extends keyof Builders>(
|
|
147
|
+
count: number,
|
|
148
|
+
builderName: K,
|
|
149
|
+
attrs?: any,
|
|
150
|
+
): Promise<Awaited<ReturnType<Builders[K]>>[]> {
|
|
151
|
+
if (!(builderName in this.builders)) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Builder "${
|
|
154
|
+
builderName as string
|
|
155
|
+
}" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const promises: Promise<any>[] = [];
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < count; i++) {
|
|
162
|
+
const newAttrs = typeof attrs === 'function' ? attrs(i) : attrs;
|
|
163
|
+
promises.push(this.insert(builderName, newAttrs));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return Promise.all(promises);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
seed<K extends keyof Seeds>(
|
|
170
|
+
seedName: K,
|
|
171
|
+
attrs?: Parameters<Seeds[K]>[0],
|
|
172
|
+
): ReturnType<Seeds[K]> {
|
|
173
|
+
if (!(seedName in this.seeds)) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Seed "${
|
|
176
|
+
seedName as string
|
|
177
|
+
}" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return this.seeds[seedName](attrs || {}, this, this.db);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
import { Factory, type FactorySeed } from './Factory.ts';
|
|
3
|
+
|
|
4
|
+
export class ObjectionFactory<
|
|
5
|
+
Builders extends Record<string, any>,
|
|
6
|
+
Seeds extends Record<string, any>,
|
|
7
|
+
> extends Factory<Builders, Seeds> {
|
|
8
|
+
static createSeed<Seed extends FactorySeed>(seedFn: Seed): Seed {
|
|
9
|
+
return Factory.createSeed(seedFn);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private builders: Builders,
|
|
14
|
+
private seeds: Seeds,
|
|
15
|
+
private db: Knex,
|
|
16
|
+
) {
|
|
17
|
+
super();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
insert(factory, attrs = {}) {
|
|
21
|
+
if (!(factory in this.builders)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Factory "${
|
|
24
|
+
factory as string
|
|
25
|
+
}" does not exist. Make sure it is correct and registered in src/test/setup.ts`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return this.builders[factory](attrs, {}, this.db).then((record: any) => {
|
|
30
|
+
return record.$query(this.db).insertGraph(record).execute();
|
|
31
|
+
}) as any;
|
|
32
|
+
}
|
|
33
|
+
insertMany(count, builderName, attrs = {}) {
|
|
34
|
+
if (!(builderName in this.builders)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Builder "${
|
|
37
|
+
builderName as string
|
|
38
|
+
}" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const records: any[] = [];
|
|
43
|
+
for (let i = 0; i < count; i++) {
|
|
44
|
+
const newAttrs = typeof attrs === 'function' ? (attrs as any)(i) : attrs;
|
|
45
|
+
|
|
46
|
+
records.push(
|
|
47
|
+
this.builders[builderName](newAttrs, {}, this.db).then((record: any) =>
|
|
48
|
+
record.$query(this.db).insertGraph(record).execute(),
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Promise.all(records);
|
|
54
|
+
}
|
|
55
|
+
seed(seedName, attrs = {}) {
|
|
56
|
+
if (!(seedName in this.seeds)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Seed "${
|
|
59
|
+
seedName as string
|
|
60
|
+
}" is not registered in this factory. Make sure it is correct and registered in src/test/setup.ts`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return this.seeds[seedName](attrs, this, this.db);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Kysely, type MigrationProvider, Migrator } from 'kysely';
|
|
2
|
+
import { PostgresMigrator } from './PostgresMigrator';
|
|
3
|
+
|
|
4
|
+
const logger = console;
|
|
5
|
+
|
|
6
|
+
export class PostgresKyselyMigrator extends PostgresMigrator {
|
|
7
|
+
constructor(
|
|
8
|
+
private options: {
|
|
9
|
+
uri: string;
|
|
10
|
+
db: Kysely<any>;
|
|
11
|
+
provider: MigrationProvider;
|
|
12
|
+
},
|
|
13
|
+
) {
|
|
14
|
+
super(options.uri);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async migrate(): Promise<void> {
|
|
18
|
+
const migrator = new Migrator({
|
|
19
|
+
db: this.options.db,
|
|
20
|
+
provider: this.options.provider,
|
|
21
|
+
});
|
|
22
|
+
const migrations = await migrator.migrateToLatest();
|
|
23
|
+
|
|
24
|
+
if (migrations.error) {
|
|
25
|
+
logger.error(migrations.error, `Failed to apply migrations`);
|
|
26
|
+
throw migrations.error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await this.options.db.destroy();
|
|
30
|
+
|
|
31
|
+
logger.log(`Applied ${migrations.results?.length} migrations successfully`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Client } from 'pg';
|
|
2
|
+
|
|
3
|
+
async function setupClient(uri: string) {
|
|
4
|
+
const url = new URL(uri);
|
|
5
|
+
|
|
6
|
+
const db = new Client({
|
|
7
|
+
user: url.username,
|
|
8
|
+
password: url.password,
|
|
9
|
+
host: url.hostname,
|
|
10
|
+
port: parseInt(url.port),
|
|
11
|
+
database: 'postgres',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
let database = url.pathname.slice(1);
|
|
15
|
+
if (database.includes('?')) {
|
|
16
|
+
database = database.substring(0, database.indexOf('?'));
|
|
17
|
+
}
|
|
18
|
+
return { database, db };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const logger = console;
|
|
22
|
+
|
|
23
|
+
export abstract class PostgresMigrator {
|
|
24
|
+
constructor(private uri: string) {}
|
|
25
|
+
|
|
26
|
+
abstract migrate(): Promise<void>;
|
|
27
|
+
|
|
28
|
+
private static async create(
|
|
29
|
+
uri: string,
|
|
30
|
+
): Promise<{ alreadyExisted: boolean }> {
|
|
31
|
+
const { database, db } = await setupClient(uri);
|
|
32
|
+
try {
|
|
33
|
+
await db.connect();
|
|
34
|
+
const result = await db.query(
|
|
35
|
+
`SELECT * FROM pg_catalog.pg_database WHERE datname = '${database}'`,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (result.rowCount === 0) {
|
|
39
|
+
await db.query(`CREATE DATABASE "${database}"`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
alreadyExisted: result.rowCount ? result.rowCount > 0 : false,
|
|
44
|
+
};
|
|
45
|
+
} finally {
|
|
46
|
+
await db.end();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private static async drop(uri: string): Promise<void> {
|
|
51
|
+
const { database, db } = await setupClient(uri);
|
|
52
|
+
try {
|
|
53
|
+
await db.connect();
|
|
54
|
+
await db.query(`DROP DATABASE "${database}"`);
|
|
55
|
+
} finally {
|
|
56
|
+
await db.end();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async start() {
|
|
61
|
+
const { database, db } = await setupClient(this.uri);
|
|
62
|
+
try {
|
|
63
|
+
await PostgresMigrator.create(this.uri);
|
|
64
|
+
// Implement migration logic here
|
|
65
|
+
await this.migrate();
|
|
66
|
+
logger.log(`Migrating database: ${database}`);
|
|
67
|
+
// Example: await db.query('CREATE TABLE example (id SERIAL PRIMARY KEY)');
|
|
68
|
+
} finally {
|
|
69
|
+
await db.end();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return async () => {
|
|
73
|
+
await PostgresMigrator.drop(this.uri);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CamelCasePlugin,
|
|
3
|
+
type ControlledTransaction,
|
|
4
|
+
Kysely,
|
|
5
|
+
PostgresDialect,
|
|
6
|
+
} from 'kysely';
|
|
7
|
+
import pg from 'pg';
|
|
8
|
+
import {
|
|
9
|
+
afterAll,
|
|
10
|
+
afterEach,
|
|
11
|
+
beforeAll,
|
|
12
|
+
beforeEach,
|
|
13
|
+
describe,
|
|
14
|
+
expect,
|
|
15
|
+
it,
|
|
16
|
+
} from 'vitest';
|
|
17
|
+
import { TEST_DATABASE_CONFIG } from '../../test/globalSetup';
|
|
18
|
+
import { KyselyFactory } from '../KyselyFactory';
|
|
19
|
+
|
|
20
|
+
describe('KyselyFactory', () => {
|
|
21
|
+
interface Database {
|
|
22
|
+
users: {
|
|
23
|
+
id: number;
|
|
24
|
+
name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
};
|
|
28
|
+
posts: {
|
|
29
|
+
id: number;
|
|
30
|
+
title: string;
|
|
31
|
+
content: string;
|
|
32
|
+
userId: number;
|
|
33
|
+
createdAt: Date;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let db: Kysely<Database>;
|
|
38
|
+
let trx: ControlledTransaction<Database, []>;
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
db = new Kysely({
|
|
41
|
+
dialect: new PostgresDialect({
|
|
42
|
+
pool: new pg.Pool(TEST_DATABASE_CONFIG),
|
|
43
|
+
}),
|
|
44
|
+
plugins: [new CamelCasePlugin()],
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
beforeEach(async () => {
|
|
49
|
+
trx = await db.startTransaction().execute();
|
|
50
|
+
});
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await trx.rollback().execute();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterAll(async () => {
|
|
56
|
+
await db.destroy();
|
|
57
|
+
});
|
|
58
|
+
it('KyselyFactory.insert', async () => {
|
|
59
|
+
const userBuilder = KyselyFactory.createBuilder<Database, 'users'>({
|
|
60
|
+
table: 'users',
|
|
61
|
+
defaults: async (attrs) => ({
|
|
62
|
+
name: 'John Doe',
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const builders = {
|
|
67
|
+
user: userBuilder,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const factory = new KyselyFactory<Database, typeof builders, {}>(
|
|
71
|
+
builders,
|
|
72
|
+
{},
|
|
73
|
+
trx,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const user = await factory.insert('user', {
|
|
77
|
+
email: `user${Date.now()}@example.com`,
|
|
78
|
+
createdAt: new Date(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(user).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/example.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { KyselyFactory } from './KyselyFactory';
|
|
2
|
+
|
|
3
|
+
interface Database {
|
|
4
|
+
users: {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
email: string;
|
|
8
|
+
createdAt: Date;
|
|
9
|
+
};
|
|
10
|
+
posts: {
|
|
11
|
+
id: number;
|
|
12
|
+
title: string;
|
|
13
|
+
content: string;
|
|
14
|
+
userId: number;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const userBuilder = KyselyFactory.createBuilder<Database, 'users'>({
|
|
20
|
+
table: 'users',
|
|
21
|
+
defaults: async (attrs) => ({
|
|
22
|
+
name: 'John Doe',
|
|
23
|
+
email: `user${Date.now()}@example.com`,
|
|
24
|
+
createdAt: new Date(),
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const builders = {
|
|
29
|
+
user: userBuilder,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type Builders = typeof builders;
|
|
33
|
+
export type Seeds = Record<string, any>;
|
|
34
|
+
|
|
35
|
+
const factory = new KyselyFactory<Database, Builders, Seeds>(
|
|
36
|
+
builders,
|
|
37
|
+
{},
|
|
38
|
+
{} as any,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
factory.insert('user', {
|
|
42
|
+
name: 'Jane Doe',
|
|
43
|
+
email: `user${Date.now()}@example.com`,
|
|
44
|
+
createdAt: new Date(),
|
|
45
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import pg from 'pg';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CamelCasePlugin,
|
|
7
|
+
FileMigrationProvider,
|
|
8
|
+
Kysely,
|
|
9
|
+
PostgresDialect,
|
|
10
|
+
} from 'kysely';
|
|
11
|
+
|
|
12
|
+
import { PostgresKyselyMigrator } from '../src/PostgresKyselyMigrator';
|
|
13
|
+
|
|
14
|
+
const TEST_DATABASE_NAME = 'geekmidas_test';
|
|
15
|
+
|
|
16
|
+
const logger = console;
|
|
17
|
+
|
|
18
|
+
export const TEST_DATABASE_CONFIG = {
|
|
19
|
+
host: 'localhost',
|
|
20
|
+
port: 5432,
|
|
21
|
+
user: 'geekmidas',
|
|
22
|
+
password: 'geekmidas',
|
|
23
|
+
database: TEST_DATABASE_NAME,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// password: get('Database.password').string(),
|
|
27
|
+
// user: get('Database.username').string(),
|
|
28
|
+
// database: get('Database.database').string(),
|
|
29
|
+
// host: get('Database.host').string(),
|
|
30
|
+
// port: get('Database.port').number().default(5432),
|
|
31
|
+
|
|
32
|
+
export default async function globalSetup() {
|
|
33
|
+
const uri = `postgres://${TEST_DATABASE_CONFIG.user}:${TEST_DATABASE_CONFIG.password}@${TEST_DATABASE_CONFIG.host}:${TEST_DATABASE_CONFIG.port}/${TEST_DATABASE_CONFIG.database}`;
|
|
34
|
+
|
|
35
|
+
const migrationFolder = path.resolve(__dirname, './migrations');
|
|
36
|
+
|
|
37
|
+
const migrationProcessor = new PostgresKyselyMigrator({
|
|
38
|
+
uri,
|
|
39
|
+
db: new Kysely({
|
|
40
|
+
dialect: new PostgresDialect({
|
|
41
|
+
pool: new pg.Pool(TEST_DATABASE_CONFIG),
|
|
42
|
+
}),
|
|
43
|
+
plugins: [new CamelCasePlugin()],
|
|
44
|
+
}),
|
|
45
|
+
provider: new FileMigrationProvider({
|
|
46
|
+
fs,
|
|
47
|
+
path,
|
|
48
|
+
migrationFolder,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return migrationProcessor.start();
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Kysely, sql } from 'kysely';
|
|
2
|
+
|
|
3
|
+
// `any` is required here since migrations should be frozen in time. alternatively, keep a "snapshot" db interface.
|
|
4
|
+
export async function up(db: Kysely<any>): Promise<void> {
|
|
5
|
+
await db.schema
|
|
6
|
+
.createTable('users')
|
|
7
|
+
.addColumn('id', 'bigserial', (col) => col.primaryKey())
|
|
8
|
+
.addColumn('email', 'varchar', (col) => col.notNull().unique())
|
|
9
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
10
|
+
.addColumn('created_at', 'timestamp', (col) =>
|
|
11
|
+
col.defaultTo(sql`now()`).notNull(),
|
|
12
|
+
)
|
|
13
|
+
.addColumn('updated_at', 'timestamp', (col) =>
|
|
14
|
+
col.defaultTo(sql`now()`).notNull(),
|
|
15
|
+
)
|
|
16
|
+
.execute();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// `any` is required here since migrations should be frozen in time. alternatively, keep a "snapshot" db interface.
|
|
20
|
+
export async function down(db: Kysely<any>): Promise<void> {
|
|
21
|
+
await db.schema.dropTable('users').execute();
|
|
22
|
+
}
|