@geekmidas/testkit 0.0.3 → 0.0.5
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/PostgresKyselyMigrator.spec +471 -0
- package/dist/{KyselyFactory-DiiWtMYe.cjs → KyselyFactory-BX7Kv2uP.cjs} +11 -12
- package/dist/{KyselyFactory-DZewtWtJ.mjs → KyselyFactory-pOMOFQWE.mjs} +11 -12
- package/dist/KyselyFactory.cjs +2 -1
- package/dist/KyselyFactory.mjs +2 -1
- package/dist/ObjectionFactory.cjs +1 -1
- package/dist/ObjectionFactory.mjs +1 -1
- package/dist/{PostgresKyselyMigrator-ChMJpPrQ.mjs → PostgresKyselyMigrator-D8fm35-s.mjs} +1 -1
- package/dist/{PostgresKyselyMigrator-rY3hO_-1.cjs → PostgresKyselyMigrator-JTY2LfwD.cjs} +3 -2
- package/dist/PostgresKyselyMigrator.cjs +2 -2
- package/dist/PostgresKyselyMigrator.mjs +2 -2
- package/dist/{PostgresMigrator-BJ2-5A_b.cjs → PostgresMigrator-Bz-tnjB6.cjs} +2 -39
- package/dist/PostgresMigrator.cjs +1 -1
- package/dist/PostgresMigrator.mjs +1 -1
- package/dist/VitestKyselyTransactionIsolator-BS3R-V0I.mjs +12 -0
- package/dist/VitestKyselyTransactionIsolator-DWSTKIe3.cjs +17 -0
- package/dist/VitestKyselyTransactionIsolator.cjs +4 -0
- package/dist/VitestKyselyTransactionIsolator.mjs +4 -0
- package/dist/VitestTransactionIsolator-BjVXqFs6.mjs +40 -0
- package/dist/VitestTransactionIsolator-Bx2c4OzK.cjs +52 -0
- package/dist/VitestTransactionIsolator.cjs +4 -0
- package/dist/VitestTransactionIsolator.mjs +3 -0
- package/dist/__tests__/Factory.spec.cjs +139 -0
- package/dist/__tests__/Factory.spec.mjs +138 -0
- package/dist/__tests__/KyselyFactory.spec.cjs +220 -15008
- package/dist/__tests__/KyselyFactory.spec.mjs +218 -15033
- package/dist/__tests__/ObjectionFactory.spec.cjs +386 -0
- package/dist/__tests__/ObjectionFactory.spec.mjs +385 -0
- package/dist/__tests__/PostgresMigrator.spec.cjs +256 -0
- package/dist/__tests__/PostgresMigrator.spec.mjs +255 -0
- package/dist/__tests__/faker.spec.cjs +115 -0
- package/dist/__tests__/faker.spec.mjs +114 -0
- package/dist/__tests__/integration.spec.cjs +279 -0
- package/dist/__tests__/integration.spec.mjs +278 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/example.cjs +2 -1
- package/dist/example.mjs +2 -1
- package/dist/faker-BwaXA_RF.mjs +85 -0
- package/dist/faker-caz-8zt8.cjs +121 -0
- package/dist/faker.cjs +8 -0
- package/dist/faker.mjs +3 -0
- package/dist/helpers-B9Jdk_C7.cjs +31 -0
- package/dist/helpers-BfuX-cjN.mjs +111 -0
- package/dist/helpers-DKEBHABj.cjs +135 -0
- package/dist/helpers-DOiGIkaU.mjs +19 -0
- package/dist/helpers.cjs +6 -0
- package/dist/helpers.mjs +5 -0
- package/dist/kysely.cjs +15 -4
- package/dist/kysely.mjs +14 -4
- package/dist/objection.cjs +1 -1
- package/dist/objection.mjs +1 -1
- package/package.json +8 -2
- package/src/Factory.ts +3 -1
- package/src/KyselyFactory.ts +30 -36
- package/src/VitestKyselyTransactionIsolator.ts +23 -0
- package/src/VitestTransactionIsolator.ts +70 -0
- package/src/__tests__/Factory.spec.ts +164 -0
- package/src/__tests__/KyselyFactory.spec.ts +432 -64
- package/src/__tests__/ObjectionFactory.spec.ts +532 -0
- package/src/__tests__/PostgresMigrator.spec.ts +366 -0
- package/src/__tests__/faker.spec.ts +142 -0
- package/src/__tests__/integration.spec.ts +442 -0
- package/src/faker.ts +112 -0
- package/src/helpers.ts +28 -0
- package/src/kysely.ts +14 -0
- package/test/globalSetup.ts +41 -40
- package/test/helpers.ts +273 -0
- package/dist/magic-string.es-CxbtJGk_.mjs +0 -1014
- package/dist/magic-string.es-KiPEzMtt.cjs +0 -1015
- /package/dist/{ObjectionFactory-DeFYWbzt.cjs → ObjectionFactory-BlkzSEqo.cjs} +0 -0
- /package/dist/{ObjectionFactory-MAf2m8LI.mjs → ObjectionFactory-ChuX8sZN.mjs} +0 -0
- /package/dist/{PostgresMigrator-BKaNTth5.mjs → PostgresMigrator-CEoRKTdq.mjs} +0 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { beforeAll, describe, expect } from 'vitest';
|
|
2
|
+
import { TEST_DATABASE_CONFIG } from '../../test/globalSetup';
|
|
3
|
+
import { type TestDatabase, createTestTables } from '../../test/helpers';
|
|
4
|
+
import { KyselyFactory } from '../KyselyFactory';
|
|
5
|
+
import { createKyselyDb, wrapVitestKyselyTransaction } from '../helpers';
|
|
6
|
+
|
|
7
|
+
const db = createKyselyDb<TestDatabase>(TEST_DATABASE_CONFIG);
|
|
8
|
+
const it = wrapVitestKyselyTransaction<TestDatabase>(db, createTestTables);
|
|
9
|
+
describe('Testkit Integration Tests', () => {
|
|
10
|
+
beforeAll(async () => {});
|
|
11
|
+
describe('Complex Factory Scenarios', () => {
|
|
12
|
+
it('should handle complex multi-table data creation', async ({ trx }) => {
|
|
13
|
+
// Create builders for all entities
|
|
14
|
+
const userBuilder = KyselyFactory.createBuilder<TestDatabase, 'users'>(
|
|
15
|
+
'users',
|
|
16
|
+
async (attrs) => ({
|
|
17
|
+
name: 'John Doe',
|
|
18
|
+
email: `user${Date.now()}-${Math.random()}@example.com`,
|
|
19
|
+
role: 'user' as const,
|
|
20
|
+
createdAt: new Date(),
|
|
21
|
+
updatedAt: new Date(),
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const postBuilder = KyselyFactory.createBuilder<TestDatabase, 'posts'>(
|
|
26
|
+
'posts',
|
|
27
|
+
async (attrs, factory) => {
|
|
28
|
+
// Create a user if no userId provided
|
|
29
|
+
if (!attrs.userId) {
|
|
30
|
+
const user = await factory.insert('user');
|
|
31
|
+
return {
|
|
32
|
+
title: 'Default Post Title',
|
|
33
|
+
content: 'Default post content...',
|
|
34
|
+
userId: user.id,
|
|
35
|
+
published: false,
|
|
36
|
+
createdAt: new Date(),
|
|
37
|
+
updatedAt: new Date(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
title: 'Default Post Title',
|
|
42
|
+
content: 'Default post content...',
|
|
43
|
+
published: false,
|
|
44
|
+
createdAt: new Date(),
|
|
45
|
+
updatedAt: new Date(),
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const commentBuilder = KyselyFactory.createBuilder<
|
|
51
|
+
TestDatabase,
|
|
52
|
+
'comments'
|
|
53
|
+
>('comments', async (attrs, factory) => {
|
|
54
|
+
let postId = attrs.postId;
|
|
55
|
+
let userId = attrs.userId;
|
|
56
|
+
|
|
57
|
+
// Create post if not provided
|
|
58
|
+
if (!postId) {
|
|
59
|
+
const post = await factory.insert('post');
|
|
60
|
+
postId = post.id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Create user if not provided
|
|
64
|
+
if (!userId) {
|
|
65
|
+
const user = await factory.insert('user');
|
|
66
|
+
userId = user.id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
content: 'Default comment content',
|
|
71
|
+
postId,
|
|
72
|
+
userId,
|
|
73
|
+
createdAt: new Date(),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const builders = {
|
|
78
|
+
user: userBuilder,
|
|
79
|
+
post: postBuilder,
|
|
80
|
+
comment: commentBuilder,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const factory = new KyselyFactory<TestDatabase, typeof builders, {}>(
|
|
84
|
+
builders,
|
|
85
|
+
{},
|
|
86
|
+
trx,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Create a complete blog structure
|
|
90
|
+
const author = await factory.insert('user', {
|
|
91
|
+
name: 'Jane Author',
|
|
92
|
+
email: 'jane@author.com',
|
|
93
|
+
role: 'admin',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const posts = await factory.insertMany(3, 'post', (idx) => ({
|
|
97
|
+
title: `Post ${idx + 1}`,
|
|
98
|
+
content: `Content for post ${idx + 1}`,
|
|
99
|
+
userId: author.id,
|
|
100
|
+
published: idx < 2, // First two posts are published
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
// Create comments on the first post
|
|
104
|
+
const comments = await factory.insertMany(5, 'comment', (idx) => ({
|
|
105
|
+
content: `Comment ${idx + 1} on first post`,
|
|
106
|
+
postId: posts[0].id,
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
// Verify the data structure
|
|
110
|
+
expect(author.name).toBe('Jane Author');
|
|
111
|
+
expect(author.role).toBe('admin');
|
|
112
|
+
|
|
113
|
+
expect(posts).toHaveLength(3);
|
|
114
|
+
expect(posts[0].title).toBe('Post 1');
|
|
115
|
+
expect(posts[0].published).toBe(true);
|
|
116
|
+
expect(posts[2].published).toBe(false);
|
|
117
|
+
|
|
118
|
+
expect(comments).toHaveLength(5);
|
|
119
|
+
comments.forEach((comment, idx) => {
|
|
120
|
+
expect(comment.content).toBe(`Comment ${idx + 1} on first post`);
|
|
121
|
+
expect(comment.postId).toBe(posts[0].id);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Verify relationships in database
|
|
125
|
+
const authorPosts = await trx
|
|
126
|
+
.selectFrom('posts')
|
|
127
|
+
.selectAll()
|
|
128
|
+
.where('userId', '=', author.id)
|
|
129
|
+
.execute();
|
|
130
|
+
|
|
131
|
+
expect(authorPosts).toHaveLength(3);
|
|
132
|
+
|
|
133
|
+
const firstPostComments = await trx
|
|
134
|
+
.selectFrom('comments')
|
|
135
|
+
.selectAll()
|
|
136
|
+
.where('postId', '=', posts[0].id)
|
|
137
|
+
.execute();
|
|
138
|
+
|
|
139
|
+
expect(firstPostComments).toHaveLength(5);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should handle seeds for complex scenarios', async ({ trx }) => {
|
|
143
|
+
const c = await trx
|
|
144
|
+
.selectFrom('users')
|
|
145
|
+
.select(trx.fn.count('id').as('count'))
|
|
146
|
+
.executeTakeFirst();
|
|
147
|
+
|
|
148
|
+
const userBuilder = KyselyFactory.createBuilder<TestDatabase, 'users'>(
|
|
149
|
+
'users',
|
|
150
|
+
async (attrs) => ({
|
|
151
|
+
name: 'Default User',
|
|
152
|
+
email: `user${Date.now()}-${Math.random()}@example.com`,
|
|
153
|
+
role: 'user' as const,
|
|
154
|
+
createdAt: new Date(),
|
|
155
|
+
updatedAt: new Date(),
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const postBuilder = KyselyFactory.createBuilder<TestDatabase, 'posts'>(
|
|
160
|
+
'posts',
|
|
161
|
+
async (attrs, factory) => {
|
|
162
|
+
if (!attrs.userId) {
|
|
163
|
+
const user = await factory.insert('user');
|
|
164
|
+
return {
|
|
165
|
+
title: 'Default Post',
|
|
166
|
+
content: 'Default content',
|
|
167
|
+
userId: user.id,
|
|
168
|
+
published: false,
|
|
169
|
+
createdAt: new Date(),
|
|
170
|
+
updatedAt: new Date(),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
title: 'Default Post',
|
|
175
|
+
content: 'Default content',
|
|
176
|
+
published: false,
|
|
177
|
+
createdAt: new Date(),
|
|
178
|
+
updatedAt: new Date(),
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const builders = {
|
|
184
|
+
user: userBuilder,
|
|
185
|
+
post: postBuilder,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Create complex seeds
|
|
189
|
+
const seeds = {
|
|
190
|
+
blogWithAdminAndPosts: KyselyFactory.createSeed(
|
|
191
|
+
async (attrs: { postCount?: number }, factory: any, db: any) => {
|
|
192
|
+
// Create admin user
|
|
193
|
+
const admin = await factory.insert('user', {
|
|
194
|
+
name: 'Blog Admin',
|
|
195
|
+
email: 'admin@blog.com',
|
|
196
|
+
role: 'admin',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Create multiple posts
|
|
200
|
+
const postCount = attrs.postCount || 3;
|
|
201
|
+
const posts = await factory.insertMany(
|
|
202
|
+
postCount,
|
|
203
|
+
'post',
|
|
204
|
+
(idx) => ({
|
|
205
|
+
title: `Admin Post ${idx + 1}`,
|
|
206
|
+
content: `Content for admin post ${idx + 1}`,
|
|
207
|
+
userId: admin.id,
|
|
208
|
+
published: true,
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
admin,
|
|
214
|
+
posts,
|
|
215
|
+
summary: {
|
|
216
|
+
adminId: admin.id,
|
|
217
|
+
postIds: posts.map((p) => p.id),
|
|
218
|
+
totalPosts: posts.length,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
),
|
|
223
|
+
|
|
224
|
+
usersWithPosts: KyselyFactory.createSeed(
|
|
225
|
+
async (
|
|
226
|
+
attrs: { userCount?: number; postsPerUser?: number },
|
|
227
|
+
factory: any,
|
|
228
|
+
db: any,
|
|
229
|
+
) => {
|
|
230
|
+
const userCount = attrs.userCount || 2;
|
|
231
|
+
const postsPerUser = attrs.postsPerUser || 2;
|
|
232
|
+
|
|
233
|
+
const results: Array<{ user: any; posts: any[] }> = [];
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < userCount; i++) {
|
|
236
|
+
const user = await factory.insert('user', {
|
|
237
|
+
name: `User ${i + 1}`,
|
|
238
|
+
email: `user${i + 1}@example.com`,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const posts = await factory.insertMany(
|
|
242
|
+
postsPerUser,
|
|
243
|
+
'post',
|
|
244
|
+
(postIdx) => ({
|
|
245
|
+
title: `User ${i + 1} Post ${postIdx + 1}`,
|
|
246
|
+
content: `Content from user ${i + 1}, post ${postIdx + 1}`,
|
|
247
|
+
userId: user.id,
|
|
248
|
+
published: postIdx === 0, // Only first post is published
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
results.push({ user, posts });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return results;
|
|
256
|
+
},
|
|
257
|
+
),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const factory = new KyselyFactory<
|
|
261
|
+
TestDatabase,
|
|
262
|
+
typeof builders,
|
|
263
|
+
typeof seeds
|
|
264
|
+
>(builders, seeds, trx);
|
|
265
|
+
|
|
266
|
+
// Test first seed
|
|
267
|
+
const blogData = await factory.seed('blogWithAdminAndPosts', {
|
|
268
|
+
postCount: 5,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(blogData.admin.name).toBe('Blog Admin');
|
|
272
|
+
expect(blogData.admin.role).toBe('admin');
|
|
273
|
+
expect(blogData.posts).toHaveLength(5);
|
|
274
|
+
expect(blogData.summary.totalPosts).toBe(5);
|
|
275
|
+
|
|
276
|
+
// Test second seed
|
|
277
|
+
const userData = await factory.seed('usersWithPosts', {
|
|
278
|
+
userCount: 3,
|
|
279
|
+
postsPerUser: 4,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(userData).toHaveLength(3);
|
|
283
|
+
|
|
284
|
+
// Verify total counts in database
|
|
285
|
+
const totalUsers = await trx
|
|
286
|
+
.selectFrom('users')
|
|
287
|
+
.select(trx.fn.count('id').as('count'))
|
|
288
|
+
.executeTakeFirst();
|
|
289
|
+
|
|
290
|
+
const totalPosts = await trx
|
|
291
|
+
.selectFrom('posts')
|
|
292
|
+
.select(trx.fn.count('id').as('count'))
|
|
293
|
+
.executeTakeFirst();
|
|
294
|
+
|
|
295
|
+
// 1 admin + 3 users = 4 total users
|
|
296
|
+
expect(Number(totalUsers?.count)).toBe(4);
|
|
297
|
+
// 5 admin posts + (3 users * 4 posts) = 17 total posts
|
|
298
|
+
expect(Number(totalPosts?.count)).toBe(17);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should handle transaction isolation properly', async ({ trx }) => {
|
|
302
|
+
const userBuilder = KyselyFactory.createBuilder<TestDatabase, 'users'>(
|
|
303
|
+
'users',
|
|
304
|
+
async (attrs, factory, db, faker) => ({
|
|
305
|
+
name: 'Test User',
|
|
306
|
+
email: faker.internet.email(),
|
|
307
|
+
role: 'user' as const,
|
|
308
|
+
createdAt: new Date(),
|
|
309
|
+
updatedAt: new Date(),
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const builders = { user: userBuilder };
|
|
314
|
+
|
|
315
|
+
const factory1 = new KyselyFactory<TestDatabase, typeof builders, {}>(
|
|
316
|
+
builders,
|
|
317
|
+
{},
|
|
318
|
+
trx,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// Create user in transaction
|
|
322
|
+
const user = await factory1.insert('user', {
|
|
323
|
+
name: 'Transaction User',
|
|
324
|
+
email: 'transaction@test.com',
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Verify user exists in transaction
|
|
328
|
+
const userInTrx = await trx
|
|
329
|
+
.selectFrom('users')
|
|
330
|
+
.selectAll()
|
|
331
|
+
.where('id', '=', user.id)
|
|
332
|
+
.executeTakeFirst();
|
|
333
|
+
|
|
334
|
+
expect(userInTrx).toBeDefined();
|
|
335
|
+
expect(userInTrx?.name).toBe('Transaction User');
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('Performance and Edge Cases', () => {
|
|
340
|
+
it('should handle creating many records efficiently', async ({ trx }) => {
|
|
341
|
+
const userBuilder = KyselyFactory.createBuilder<TestDatabase, 'users'>(
|
|
342
|
+
'users',
|
|
343
|
+
async (attrs, factory, db, faker) => ({
|
|
344
|
+
name: `User ${Math.random()}`,
|
|
345
|
+
email: faker.internet.email().toLowerCase(),
|
|
346
|
+
role: 'user' as const,
|
|
347
|
+
createdAt: new Date(),
|
|
348
|
+
updatedAt: new Date(),
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const builders = { user: userBuilder };
|
|
353
|
+
const factory = new KyselyFactory<TestDatabase, typeof builders, {}>(
|
|
354
|
+
builders,
|
|
355
|
+
{},
|
|
356
|
+
trx,
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const startTime = Date.now();
|
|
360
|
+
|
|
361
|
+
// Create 100 users
|
|
362
|
+
const users = await factory.insertMany(100, 'user');
|
|
363
|
+
|
|
364
|
+
const endTime = Date.now();
|
|
365
|
+
const duration = endTime - startTime;
|
|
366
|
+
|
|
367
|
+
expect(users).toHaveLength(100);
|
|
368
|
+
expect(duration).toBeLessThan(5000); // Should complete in under 5 seconds
|
|
369
|
+
|
|
370
|
+
// Verify all users are unique
|
|
371
|
+
const emails = users.map((u) => u.email);
|
|
372
|
+
const uniqueEmails = new Set(emails);
|
|
373
|
+
expect(uniqueEmails.size).toBe(100);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should handle complex attribute generation', async ({ trx }) => {
|
|
377
|
+
const userBuilder = KyselyFactory.createBuilder<TestDatabase, 'users'>(
|
|
378
|
+
'users',
|
|
379
|
+
async (attrs, factory, db, faker) => {
|
|
380
|
+
return {
|
|
381
|
+
name: `Generated User ${attrs.id}`,
|
|
382
|
+
email: faker.internet.email().toLowerCase(),
|
|
383
|
+
role: 'user',
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const postBuilder = KyselyFactory.createBuilder<TestDatabase, 'posts'>(
|
|
389
|
+
'posts',
|
|
390
|
+
async (attrs, factory) => {
|
|
391
|
+
let userId = attrs.userId;
|
|
392
|
+
if (!userId) {
|
|
393
|
+
const user = await factory.insert('user');
|
|
394
|
+
userId = user.id;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
title: `Auto-generated Post`,
|
|
399
|
+
content: `This is auto-generated content for post. Lorem ipsum dolor sit amet.`,
|
|
400
|
+
published: true,
|
|
401
|
+
userId,
|
|
402
|
+
createdAt: new Date(),
|
|
403
|
+
updatedAt: new Date(),
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const builders = {
|
|
409
|
+
user: userBuilder,
|
|
410
|
+
post: postBuilder,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const factory = new KyselyFactory<TestDatabase, typeof builders, {}>(
|
|
414
|
+
builders,
|
|
415
|
+
{},
|
|
416
|
+
trx,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// Create posts which will auto-create users
|
|
420
|
+
const posts = await factory.insertMany(10, 'post', (i) => ({
|
|
421
|
+
published: i % 2 === 0,
|
|
422
|
+
}));
|
|
423
|
+
|
|
424
|
+
expect(posts).toHaveLength(10);
|
|
425
|
+
|
|
426
|
+
// Check email normalization
|
|
427
|
+
const users = await trx.selectFrom('users').selectAll().execute();
|
|
428
|
+
|
|
429
|
+
users.forEach((user) => {
|
|
430
|
+
expect(user.email).toBe(user.email.toLowerCase());
|
|
431
|
+
expect(user.name).not.toMatch(/^\s|\s$/); // No leading/trailing spaces
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Check published pattern
|
|
435
|
+
const publishedPosts = posts.filter((p) => p.published);
|
|
436
|
+
const unpublishedPosts = posts.filter((p) => !p.published);
|
|
437
|
+
|
|
438
|
+
expect(publishedPosts).toHaveLength(5); // Every other post
|
|
439
|
+
expect(unpublishedPosts).toHaveLength(5);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
});
|
package/src/faker.ts
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { faker as baseFaker } from '@faker-js/faker';
|
|
2
|
+
|
|
3
|
+
// NOTE: This is a simple way to extend `faker` with additional methods
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Atomic counter implementation for thread-safe sequence generation
|
|
7
|
+
*/
|
|
8
|
+
class AtomicCounter {
|
|
9
|
+
private value: number;
|
|
10
|
+
|
|
11
|
+
constructor(initialValue = 0) {
|
|
12
|
+
this.value = initialValue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
increment(): number {
|
|
16
|
+
// In Node.js, JavaScript is single-threaded within the event loop,
|
|
17
|
+
// so this operation is already atomic. However, this class provides
|
|
18
|
+
// a cleaner abstraction and makes the intent explicit.
|
|
19
|
+
return ++this.value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get(): number {
|
|
23
|
+
return this.value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
reset(value = 0): void {
|
|
27
|
+
this.value = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sets the `insertedAt` and `updatedAt` to a random date in the past.
|
|
33
|
+
*/
|
|
34
|
+
export function timestamps(): Timestamps {
|
|
35
|
+
const createdAt = faker.date.past();
|
|
36
|
+
const updatedAt = faker.date.between({
|
|
37
|
+
from: createdAt,
|
|
38
|
+
to: new Date(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
createdAt.setMilliseconds(0);
|
|
42
|
+
updatedAt.setMilliseconds(0);
|
|
43
|
+
|
|
44
|
+
return { createdAt, updatedAt };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns a reverse domain name identifier.
|
|
49
|
+
*/
|
|
50
|
+
export function identifier(suffix?: string): string {
|
|
51
|
+
return [
|
|
52
|
+
faker.internet.domainSuffix(),
|
|
53
|
+
faker.internet.domainWord(),
|
|
54
|
+
suffix ? suffix : faker.internet.domainWord() + sequence('identifier'),
|
|
55
|
+
].join('.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Atomic sequences for thread-safe counter generation
|
|
59
|
+
const sequences = new Map<string, AtomicCounter>();
|
|
60
|
+
|
|
61
|
+
export function sequence(name = 'default'): number {
|
|
62
|
+
if (!sequences.has(name)) {
|
|
63
|
+
sequences.set(name, new AtomicCounter());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const counter = sequences.get(name) as AtomicCounter;
|
|
67
|
+
return counter.increment();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resets a sequence counter to a specific value (default: 0)
|
|
72
|
+
*/
|
|
73
|
+
export function resetSequence(name = 'default', value = 0): void {
|
|
74
|
+
if (sequences.has(name)) {
|
|
75
|
+
const counter = sequences.get(name) as AtomicCounter;
|
|
76
|
+
counter.reset(value);
|
|
77
|
+
} else {
|
|
78
|
+
sequences.set(name, new AtomicCounter(value));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resets all sequence counters
|
|
84
|
+
*/
|
|
85
|
+
export function resetAllSequences(): void {
|
|
86
|
+
sequences.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns a random price number.
|
|
91
|
+
*/
|
|
92
|
+
function price(): number {
|
|
93
|
+
return +faker.commerce.price();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const faker = Object.freeze(
|
|
97
|
+
Object.assign({}, baseFaker, {
|
|
98
|
+
timestamps,
|
|
99
|
+
identifier,
|
|
100
|
+
sequence,
|
|
101
|
+
resetSequence,
|
|
102
|
+
resetAllSequences,
|
|
103
|
+
price,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
export type Timestamps = {
|
|
108
|
+
createdAt: Date;
|
|
109
|
+
updatedAt: Date;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export type FakerFactory = typeof faker;
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CamelCasePlugin,
|
|
3
|
+
Kysely,
|
|
4
|
+
PostgresDialect,
|
|
5
|
+
type Transaction,
|
|
6
|
+
} from 'kysely';
|
|
7
|
+
import pg from 'pg';
|
|
8
|
+
import { VitestKyselyTransactionIsolator } from './VitestKyselyTransactionIsolator';
|
|
9
|
+
import { IsolationLevel } from './VitestTransactionIsolator';
|
|
10
|
+
|
|
11
|
+
export function wrapVitestKyselyTransaction<Database>(
|
|
12
|
+
db: Kysely<Database>,
|
|
13
|
+
setup?: (trx: Transaction<Database>) => Promise<void>,
|
|
14
|
+
level: IsolationLevel = IsolationLevel.REPEATABLE_READ,
|
|
15
|
+
) {
|
|
16
|
+
const wrapper = new VitestKyselyTransactionIsolator<Database>();
|
|
17
|
+
|
|
18
|
+
return wrapper.wrapVitestWithTransaction(db, setup, level);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createKyselyDb<Database>(config: any): Kysely<Database> {
|
|
22
|
+
return new Kysely({
|
|
23
|
+
dialect: new PostgresDialect({
|
|
24
|
+
pool: new pg.Pool(config),
|
|
25
|
+
}),
|
|
26
|
+
plugins: [new CamelCasePlugin()],
|
|
27
|
+
});
|
|
28
|
+
}
|
package/src/kysely.ts
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
1
|
+
import type { Kysely, Transaction } from 'kysely';
|
|
2
|
+
import { VitestKyselyTransactionIsolator } from './VitestKyselyTransactionIsolator';
|
|
3
|
+
import { IsolationLevel } from './VitestTransactionIsolator';
|
|
4
|
+
|
|
1
5
|
export { KyselyFactory } from './KyselyFactory';
|
|
2
6
|
export { PostgresKyselyMigrator } from './PostgresKyselyMigrator';
|
|
7
|
+
|
|
8
|
+
export function wrapVitestKyselyTransaction<Database>(
|
|
9
|
+
db: Kysely<Database>,
|
|
10
|
+
setup?: (trx: Transaction<Database>) => Promise<void>,
|
|
11
|
+
level: IsolationLevel = IsolationLevel.REPEATABLE_READ,
|
|
12
|
+
) {
|
|
13
|
+
const wrapper = new VitestKyselyTransactionIsolator<Database>();
|
|
14
|
+
|
|
15
|
+
return wrapper.wrapVitestWithTransaction(db, setup, level);
|
|
16
|
+
}
|
package/test/globalSetup.ts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
import
|
|
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';
|
|
1
|
+
import { Client } from 'pg';
|
|
13
2
|
|
|
14
3
|
const TEST_DATABASE_NAME = 'geekmidas_test';
|
|
15
4
|
|
|
16
|
-
const logger = console;
|
|
17
|
-
|
|
18
5
|
export const TEST_DATABASE_CONFIG = {
|
|
19
6
|
host: 'localhost',
|
|
20
7
|
port: 5432,
|
|
@@ -23,31 +10,45 @@ export const TEST_DATABASE_CONFIG = {
|
|
|
23
10
|
database: TEST_DATABASE_NAME,
|
|
24
11
|
};
|
|
25
12
|
|
|
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
13
|
export default async function globalSetup() {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
14
|
+
const adminConfig = {
|
|
15
|
+
host: TEST_DATABASE_CONFIG.host,
|
|
16
|
+
port: TEST_DATABASE_CONFIG.port,
|
|
17
|
+
user: TEST_DATABASE_CONFIG.user,
|
|
18
|
+
password: TEST_DATABASE_CONFIG.password,
|
|
19
|
+
database: 'postgres', // Connect to default postgres database
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const client = new Client(adminConfig);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await client.connect();
|
|
26
|
+
|
|
27
|
+
// Check if test database exists
|
|
28
|
+
const result = await client.query(
|
|
29
|
+
`SELECT * FROM pg_catalog.pg_database WHERE datname = $1`,
|
|
30
|
+
[TEST_DATABASE_NAME],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Create test database if it doesn't exist
|
|
34
|
+
if (result.rowCount === 0) {
|
|
35
|
+
await client.query(`CREATE DATABASE "${TEST_DATABASE_NAME}"`);
|
|
36
|
+
} else {
|
|
37
|
+
}
|
|
38
|
+
} finally {
|
|
39
|
+
await client.end();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Return cleanup function that drops the database
|
|
43
|
+
return async () => {
|
|
44
|
+
const cleanupClient = new Client(adminConfig);
|
|
45
|
+
try {
|
|
46
|
+
await cleanupClient.connect();
|
|
47
|
+
await cleanupClient.query(
|
|
48
|
+
`DROP DATABASE IF EXISTS "${TEST_DATABASE_NAME}"`,
|
|
49
|
+
);
|
|
50
|
+
} finally {
|
|
51
|
+
await cleanupClient.end();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
53
54
|
}
|