@geekmidas/testkit 0.0.5 → 0.0.7
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/dist/Factory-WMhTNZ9S.cjs +56 -0
- package/dist/Factory-z2m01hMj.mjs +50 -0
- package/dist/Factory.cjs +1 -1
- package/dist/Factory.mjs +1 -1
- package/dist/KyselyFactory-Bdq1s1Go.cjs +215 -0
- package/dist/KyselyFactory-ELiHgHVv.mjs +210 -0
- package/dist/KyselyFactory.cjs +3 -3
- package/dist/KyselyFactory.mjs +3 -3
- package/dist/ObjectionFactory-89p-FFEw.mjs +178 -0
- package/dist/ObjectionFactory-C47B03Ot.cjs +183 -0
- package/dist/ObjectionFactory.cjs +2 -2
- package/dist/ObjectionFactory.mjs +2 -2
- package/dist/PostgresKyselyMigrator-Bs31emFd.cjs +87 -0
- package/dist/PostgresKyselyMigrator-ChIpZFYB.mjs +81 -0
- package/dist/PostgresKyselyMigrator.cjs +2 -2
- package/dist/PostgresKyselyMigrator.mjs +2 -2
- package/dist/PostgresMigrator-BtAWdLss.cjs +151 -0
- package/dist/PostgresMigrator-BzqksJcW.mjs +145 -0
- package/dist/PostgresMigrator.cjs +1 -1
- package/dist/PostgresMigrator.mjs +1 -1
- package/dist/VitestKyselyTransactionIsolator-AfxPJEwR.mjs +58 -0
- package/dist/VitestKyselyTransactionIsolator-YWnSJiIH.cjs +63 -0
- package/dist/VitestKyselyTransactionIsolator.cjs +2 -2
- package/dist/VitestKyselyTransactionIsolator.mjs +2 -2
- package/dist/VitestObjectionTransactionIsolator-0uX6DW5G.cjs +66 -0
- package/dist/VitestObjectionTransactionIsolator-BZRYy8iW.mjs +61 -0
- package/dist/VitestObjectionTransactionIsolator.cjs +4 -0
- package/dist/VitestObjectionTransactionIsolator.mjs +4 -0
- package/dist/VitestTransactionIsolator-DcOz0LZF.cjs +129 -0
- package/dist/VitestTransactionIsolator-kFL36T8x.mjs +117 -0
- package/dist/VitestTransactionIsolator.cjs +1 -1
- package/dist/VitestTransactionIsolator.mjs +1 -1
- package/dist/__tests__/Factory.spec.cjs +1 -1
- package/dist/__tests__/Factory.spec.mjs +1 -1
- package/dist/__tests__/KyselyFactory.spec.cjs +41 -40
- package/dist/__tests__/KyselyFactory.spec.mjs +42 -41
- package/dist/__tests__/ObjectionFactory.spec.cjs +3 -3
- package/dist/__tests__/ObjectionFactory.spec.mjs +3 -3
- package/dist/__tests__/PostgresMigrator.spec.cjs +2 -2
- package/dist/__tests__/PostgresMigrator.spec.mjs +2 -2
- package/dist/__tests__/faker.spec.cjs +1 -1
- package/dist/__tests__/faker.spec.mjs +1 -1
- package/dist/__tests__/integration.spec.cjs +11 -8
- package/dist/__tests__/integration.spec.mjs +12 -9
- package/dist/example.cjs +3 -3
- package/dist/example.mjs +3 -3
- package/dist/faker-CxKkEeYi.mjs +227 -0
- package/dist/faker-SMN4ira4.cjs +263 -0
- package/dist/faker.cjs +1 -1
- package/dist/faker.mjs +1 -1
- package/dist/helpers-CKMlwSYT.mjs +47 -0
- package/dist/helpers-H4hO5SZR.cjs +53 -0
- package/dist/helpers.cjs +2 -5
- package/dist/helpers.mjs +2 -4
- package/dist/kysely-B-GOhABm.cjs +72 -0
- package/dist/kysely-CqfoKVXs.mjs +67 -0
- package/dist/kysely.cjs +11 -15
- package/dist/kysely.mjs +9 -15
- package/dist/objection.cjs +86 -3
- package/dist/objection.mjs +83 -3
- package/package.json +2 -2
- package/src/Factory.ts +97 -0
- package/src/KyselyFactory.ts +180 -0
- package/src/ObjectionFactory.ts +145 -3
- package/src/PostgresKyselyMigrator.ts +54 -0
- package/src/PostgresMigrator.ts +90 -0
- package/src/VitestKyselyTransactionIsolator.ts +46 -0
- package/src/VitestObjectionTransactionIsolator.ts +73 -0
- package/src/VitestTransactionIsolator.ts +99 -2
- package/src/__tests__/KyselyFactory.spec.ts +4 -6
- package/src/__tests__/integration.spec.ts +8 -3
- package/src/faker.ts +158 -7
- package/src/helpers.ts +35 -18
- package/src/kysely.ts +66 -1
- package/src/objection.ts +95 -0
- package/dist/Factory-DREHoms3.cjs +0 -15
- package/dist/Factory-DlzMkMzb.mjs +0 -9
- package/dist/KyselyFactory-BX7Kv2uP.cjs +0 -65
- package/dist/KyselyFactory-pOMOFQWE.mjs +0 -60
- package/dist/ObjectionFactory-BlkzSEqo.cjs +0 -41
- package/dist/ObjectionFactory-ChuX8sZN.mjs +0 -36
- package/dist/PostgresKyselyMigrator-D8fm35-s.mjs +0 -27
- package/dist/PostgresKyselyMigrator-JTY2LfwD.cjs +0 -33
- package/dist/PostgresMigrator-Bz-tnjB6.cjs +0 -67
- package/dist/PostgresMigrator-CEoRKTdq.mjs +0 -61
- package/dist/VitestKyselyTransactionIsolator-BS3R-V0I.mjs +0 -12
- package/dist/VitestKyselyTransactionIsolator-DWSTKIe3.cjs +0 -17
- package/dist/VitestTransactionIsolator-BjVXqFs6.mjs +0 -40
- package/dist/VitestTransactionIsolator-Bx2c4OzK.cjs +0 -52
- package/dist/faker-BwaXA_RF.mjs +0 -85
- package/dist/faker-caz-8zt8.cjs +0 -121
- package/dist/helpers-B9Jdk_C7.cjs +0 -31
- package/dist/helpers-DOiGIkaU.mjs +0 -19
- /package/dist/{helpers-DKEBHABj.cjs → helpers-Bnm3Jy9X.cjs} +0 -0
- /package/dist/{helpers-BfuX-cjN.mjs → helpers-CukcFAU9.mjs} +0 -0
package/src/PostgresMigrator.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { Client } from 'pg';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Creates a PostgreSQL client connected to the 'postgres' database.
|
|
5
|
+
* Extracts connection details from the provided URI.
|
|
6
|
+
*
|
|
7
|
+
* @param uri - PostgreSQL connection URI
|
|
8
|
+
* @returns Object containing the target database name and client instance
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const { database, db } = await setupClient('postgresql://user:pass@localhost:5432/mydb');
|
|
13
|
+
* // database = 'mydb'
|
|
14
|
+
* // db = Client instance connected to 'postgres' database
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
3
17
|
async function setupClient(uri: string) {
|
|
4
18
|
const url = new URL(uri);
|
|
5
19
|
|
|
@@ -18,13 +32,59 @@ async function setupClient(uri: string) {
|
|
|
18
32
|
return { database, db };
|
|
19
33
|
}
|
|
20
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Default logger instance for migration operations.
|
|
37
|
+
*/
|
|
21
38
|
const logger = console;
|
|
22
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Abstract base class for PostgreSQL database migration utilities.
|
|
42
|
+
* Provides database creation, migration, and cleanup functionality for testing.
|
|
43
|
+
* Subclasses must implement the migrate() method to define migration logic.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* class MyMigrator extends PostgresMigrator {
|
|
48
|
+
* async migrate(): Promise<void> {
|
|
49
|
+
* // Run your migrations here
|
|
50
|
+
* await this.runMigrations();
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* // Use in tests
|
|
55
|
+
* const migrator = new MyMigrator('postgresql://localhost:5432/test_db');
|
|
56
|
+
* const cleanup = await migrator.start();
|
|
57
|
+
*
|
|
58
|
+
* // Run tests...
|
|
59
|
+
*
|
|
60
|
+
* // Clean up
|
|
61
|
+
* await cleanup();
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
23
64
|
export abstract class PostgresMigrator {
|
|
65
|
+
/**
|
|
66
|
+
* Creates a new PostgresMigrator instance.
|
|
67
|
+
*
|
|
68
|
+
* @param uri - PostgreSQL connection URI
|
|
69
|
+
*/
|
|
24
70
|
constructor(private uri: string) {}
|
|
25
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Abstract method to be implemented by subclasses.
|
|
74
|
+
* Should contain the migration logic for setting up database schema.
|
|
75
|
+
*
|
|
76
|
+
* @returns Promise that resolves when migrations are complete
|
|
77
|
+
*/
|
|
26
78
|
abstract migrate(): Promise<void>;
|
|
27
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Creates a PostgreSQL database if it doesn't already exist.
|
|
82
|
+
* Connects to the 'postgres' database to check and create the target database.
|
|
83
|
+
*
|
|
84
|
+
* @param uri - PostgreSQL connection URI
|
|
85
|
+
* @returns Object indicating whether the database already existed
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
28
88
|
private static async create(
|
|
29
89
|
uri: string,
|
|
30
90
|
): Promise<{ alreadyExisted: boolean }> {
|
|
@@ -47,6 +107,14 @@ export abstract class PostgresMigrator {
|
|
|
47
107
|
}
|
|
48
108
|
}
|
|
49
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Drops a PostgreSQL database.
|
|
112
|
+
* Used for cleanup after tests are complete.
|
|
113
|
+
*
|
|
114
|
+
* @param uri - PostgreSQL connection URI
|
|
115
|
+
* @throws Error if database cannot be dropped
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
50
118
|
private static async drop(uri: string): Promise<void> {
|
|
51
119
|
const { database, db } = await setupClient(uri);
|
|
52
120
|
try {
|
|
@@ -57,6 +125,28 @@ export abstract class PostgresMigrator {
|
|
|
57
125
|
}
|
|
58
126
|
}
|
|
59
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Starts the migration process by creating the database and running migrations.
|
|
130
|
+
* Returns a cleanup function that will drop the database when called.
|
|
131
|
+
*
|
|
132
|
+
* @returns Async cleanup function that drops the created database
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const migrator = new MyMigrator('postgresql://localhost:5432/test_db');
|
|
137
|
+
*
|
|
138
|
+
* // Start migrations and get cleanup function
|
|
139
|
+
* const cleanup = await migrator.start();
|
|
140
|
+
*
|
|
141
|
+
* try {
|
|
142
|
+
* // Run your tests here
|
|
143
|
+
* await runTests();
|
|
144
|
+
* } finally {
|
|
145
|
+
* // Always clean up
|
|
146
|
+
* await cleanup();
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
60
150
|
async start() {
|
|
61
151
|
const { database, db } = await setupClient(this.uri);
|
|
62
152
|
try {
|
|
@@ -4,12 +4,58 @@ import {
|
|
|
4
4
|
VitestPostgresTransactionIsolator,
|
|
5
5
|
} from './VitestTransactionIsolator';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Kysely-specific implementation of the Vitest transaction isolator.
|
|
9
|
+
* Provides automatic transaction rollback for test isolation using Kysely's transaction API.
|
|
10
|
+
* Each test runs within a database transaction that is rolled back after completion,
|
|
11
|
+
* ensuring a clean state between tests without the overhead of recreating data.
|
|
12
|
+
*
|
|
13
|
+
* @template Database - The database schema type
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { VitestKyselyTransactionIsolator } from '@geekmidas/testkit';
|
|
18
|
+
* import { db } from './database';
|
|
19
|
+
*
|
|
20
|
+
* // Create isolator instance
|
|
21
|
+
* const isolator = new VitestKyselyTransactionIsolator<Database>();
|
|
22
|
+
*
|
|
23
|
+
* // In your test setup
|
|
24
|
+
* beforeEach(async () => {
|
|
25
|
+
* await isolator.start(db);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* afterEach(async () => {
|
|
29
|
+
* await isolator.rollback();
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Tests run in isolated transactions
|
|
33
|
+
* it('should create user', async () => {
|
|
34
|
+
* const user = await db.insertInto('users')
|
|
35
|
+
* .values({ name: 'Test User' })
|
|
36
|
+
* .returningAll()
|
|
37
|
+
* .executeTakeFirst();
|
|
38
|
+
*
|
|
39
|
+
* expect(user).toBeDefined();
|
|
40
|
+
* // This data will be rolled back after the test
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
7
44
|
export class VitestKyselyTransactionIsolator<
|
|
8
45
|
Database,
|
|
9
46
|
> extends VitestPostgresTransactionIsolator<
|
|
10
47
|
Kysely<Database>,
|
|
11
48
|
Transaction<Database>
|
|
12
49
|
> {
|
|
50
|
+
/**
|
|
51
|
+
* Creates a Kysely transaction with the specified isolation level.
|
|
52
|
+
* Implements the abstract transact method from VitestPostgresTransactionIsolator.
|
|
53
|
+
*
|
|
54
|
+
* @param conn - The Kysely database connection
|
|
55
|
+
* @param level - The transaction isolation level
|
|
56
|
+
* @param fn - The function to execute within the transaction
|
|
57
|
+
* @returns Promise that resolves when the transaction completes
|
|
58
|
+
*/
|
|
13
59
|
async transact(
|
|
14
60
|
conn: Kysely<Database>,
|
|
15
61
|
level: IsolationLevel,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
import {
|
|
3
|
+
type IsolationLevel,
|
|
4
|
+
VitestPostgresTransactionIsolator,
|
|
5
|
+
} from './VitestTransactionIsolator';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Objection.js-specific implementation of the Vitest transaction isolator.
|
|
9
|
+
* Provides automatic transaction rollback for test isolation using Objection.js and Knex transaction API.
|
|
10
|
+
* Each test runs within a database transaction that is rolled back after completion,
|
|
11
|
+
* ensuring a clean state between tests without the overhead of recreating data.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { VitestObjectionTransactionIsolator } from '@geekmidas/testkit';
|
|
16
|
+
* import { knex } from './database';
|
|
17
|
+
* import { User } from './models';
|
|
18
|
+
* import { test } from 'vitest';
|
|
19
|
+
*
|
|
20
|
+
* // Create isolator instance
|
|
21
|
+
* const isolator = new VitestObjectionTransactionIsolator(test);
|
|
22
|
+
*
|
|
23
|
+
* // Use with wrapped test API
|
|
24
|
+
* const isolatedTest = isolator.wrapVitestWithTransaction(knex);
|
|
25
|
+
*
|
|
26
|
+
* isolatedTest('should create user', async ({ trx }) => {
|
|
27
|
+
* const user = await User.query(trx)
|
|
28
|
+
* .insert({ name: 'Test User' });
|
|
29
|
+
*
|
|
30
|
+
* expect(user).toBeDefined();
|
|
31
|
+
* // This data will be rolled back after the test
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class VitestObjectionTransactionIsolator extends VitestPostgresTransactionIsolator<
|
|
36
|
+
Knex,
|
|
37
|
+
Knex.Transaction
|
|
38
|
+
> {
|
|
39
|
+
/**
|
|
40
|
+
* Creates a Knex transaction with the specified isolation level.
|
|
41
|
+
* Implements the abstract transact method from VitestPostgresTransactionIsolator.
|
|
42
|
+
* This transaction can be used with Objection.js models via Model.query(trx).
|
|
43
|
+
*
|
|
44
|
+
* @param conn - The Knex database connection
|
|
45
|
+
* @param level - The transaction isolation level
|
|
46
|
+
* @param fn - The function to execute within the transaction
|
|
47
|
+
* @returns Promise that resolves when the transaction completes
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* await isolator.transact(knex, IsolationLevel.REPEATABLE_READ, async (trx) => {
|
|
52
|
+
* // Use transaction with Objection models
|
|
53
|
+
* await User.query(trx).insert({ name: 'Test' });
|
|
54
|
+
* await Post.query(trx).where('userId', user.id).delete();
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
async transact(
|
|
59
|
+
conn: Knex,
|
|
60
|
+
level: IsolationLevel,
|
|
61
|
+
fn: (trx: Knex.Transaction) => Promise<void>,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const isolationLevel = level.toUpperCase() as Lowercase<IsolationLevel>;
|
|
64
|
+
await conn.transaction(
|
|
65
|
+
async (trx) => {
|
|
66
|
+
await fn(trx);
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
isolationLevel,
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,32 +1,129 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TestAPI } from 'vitest';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Type definition for test fixtures that provide transaction access.
|
|
5
|
+
* Used with Vitest's test.extend() API to inject transactions into tests.
|
|
6
|
+
*
|
|
7
|
+
* @template Transaction - The transaction type specific to the database driver
|
|
8
|
+
*/
|
|
3
9
|
export interface DatabaseFixtures<Transaction> {
|
|
10
|
+
/**
|
|
11
|
+
* The database transaction available to the test.
|
|
12
|
+
* All database operations should use this transaction to ensure proper rollback.
|
|
13
|
+
*/
|
|
4
14
|
trx: Transaction;
|
|
5
15
|
}
|
|
6
16
|
|
|
17
|
+
/**
|
|
18
|
+
* PostgreSQL transaction isolation levels.
|
|
19
|
+
* Controls the visibility of concurrent transactions.
|
|
20
|
+
*
|
|
21
|
+
* @see https://www.postgresql.org/docs/current/transaction-iso.html
|
|
22
|
+
*/
|
|
7
23
|
export enum IsolationLevel {
|
|
24
|
+
/**
|
|
25
|
+
* Lowest isolation level. Allows dirty reads.
|
|
26
|
+
* Not recommended for testing.
|
|
27
|
+
*/
|
|
8
28
|
READ_UNCOMMITTED = 'READ UNCOMMITTED',
|
|
29
|
+
/**
|
|
30
|
+
* Default PostgreSQL isolation level.
|
|
31
|
+
* Prevents dirty reads but allows non-repeatable reads.
|
|
32
|
+
*/
|
|
9
33
|
READ_COMMITTED = 'READ COMMITTED',
|
|
34
|
+
/**
|
|
35
|
+
* Prevents dirty reads and non-repeatable reads.
|
|
36
|
+
* Recommended for most test scenarios.
|
|
37
|
+
*/
|
|
10
38
|
REPEATABLE_READ = 'REPEATABLE READ',
|
|
39
|
+
/**
|
|
40
|
+
* Highest isolation level. Prevents all phenomena.
|
|
41
|
+
* May cause performance overhead in tests.
|
|
42
|
+
*/
|
|
11
43
|
SERIALIZABLE = 'SERIALIZABLE',
|
|
12
44
|
}
|
|
13
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Abstract base class for implementing database transaction isolation in Vitest tests.
|
|
48
|
+
* Provides automatic transaction rollback after each test to maintain test isolation.
|
|
49
|
+
* Subclasses must implement the transact() method for their specific database driver.
|
|
50
|
+
*
|
|
51
|
+
* @template Connection - The database connection type
|
|
52
|
+
* @template Transaction - The transaction type
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // Implement for your database driver
|
|
57
|
+
* class MyDatabaseIsolator extends VitestPostgresTransactionIsolator<MyDB, MyTx> {
|
|
58
|
+
* async transact(conn: MyDB, level: IsolationLevel, fn: (tx: MyTx) => Promise<void>) {
|
|
59
|
+
* await conn.transaction(level, fn);
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* // Use in tests
|
|
64
|
+
* const isolator = new MyDatabaseIsolator(test);
|
|
65
|
+
* const isolatedTest = isolator.wrapVitestWithTransaction(db);
|
|
66
|
+
*
|
|
67
|
+
* isolatedTest('should create user', async ({ trx }) => {
|
|
68
|
+
* await trx.insert('users', { name: 'Test' });
|
|
69
|
+
* // Data is automatically rolled back after test
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
14
73
|
export abstract class VitestPostgresTransactionIsolator<
|
|
15
74
|
Connection,
|
|
16
75
|
Transaction,
|
|
17
76
|
> {
|
|
77
|
+
/**
|
|
78
|
+
* Abstract method to create a transaction with the specified isolation level.
|
|
79
|
+
* Must be implemented by subclasses for specific database drivers.
|
|
80
|
+
*
|
|
81
|
+
* @param conn - The database connection
|
|
82
|
+
* @param isolationLevel - The transaction isolation level
|
|
83
|
+
* @param fn - The function to execute within the transaction
|
|
84
|
+
* @returns Promise that resolves when the transaction completes
|
|
85
|
+
*/
|
|
18
86
|
abstract transact(
|
|
19
87
|
conn: Connection,
|
|
20
88
|
isolationLevel: IsolationLevel,
|
|
21
89
|
fn: (trx: Transaction) => Promise<void>,
|
|
22
90
|
): Promise<void>;
|
|
23
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Creates a new VitestPostgresTransactionIsolator instance.
|
|
94
|
+
*
|
|
95
|
+
* @param api - The Vitest test API (usually the `test` export from vitest)
|
|
96
|
+
*/
|
|
97
|
+
constructor(private readonly api: TestAPI) {}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Creates a wrapped version of Vitest's test API that provides transaction isolation.
|
|
101
|
+
* Each test will run within a database transaction that is automatically rolled back.
|
|
102
|
+
*
|
|
103
|
+
* @param conn - The database connection to use
|
|
104
|
+
* @param setup - Optional setup function to run within the transaction before each test
|
|
105
|
+
* @param level - The transaction isolation level (defaults to REPEATABLE_READ)
|
|
106
|
+
* @returns A wrapped test API with transaction support
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* const isolatedTest = isolator.wrapVitestWithTransaction(db, async (trx) => {
|
|
111
|
+
* // Optional setup: create common test data
|
|
112
|
+
* await trx.insert('settings', { key: 'test', value: 'true' });
|
|
113
|
+
* });
|
|
114
|
+
*
|
|
115
|
+
* isolatedTest('test with transaction', async ({ trx }) => {
|
|
116
|
+
* const user = await trx.insert('users', { name: 'Test' });
|
|
117
|
+
* expect(user).toBeDefined();
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
24
121
|
wrapVitestWithTransaction(
|
|
25
122
|
conn: Connection,
|
|
26
123
|
setup?: (trx: Transaction) => Promise<void>,
|
|
27
124
|
level: IsolationLevel = IsolationLevel.REPEATABLE_READ,
|
|
28
125
|
) {
|
|
29
|
-
return
|
|
126
|
+
return this.api.extend<DatabaseFixtures<Transaction>>({
|
|
30
127
|
// This fixture automatically provides a transaction to each test
|
|
31
128
|
trx: async ({}, use) => {
|
|
32
129
|
// Create a custom error class for rollback
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import type { ControlledTransaction, Kysely } from 'kysely';
|
|
2
1
|
import pg from 'pg';
|
|
3
|
-
import { describe, expect } from 'vitest';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
4
3
|
import { TEST_DATABASE_CONFIG } from '../../test/globalSetup';
|
|
5
4
|
import { type TestDatabase, createTestTables } from '../../test/helpers';
|
|
6
5
|
import { KyselyFactory } from '../KyselyFactory';
|
|
7
|
-
import { createKyselyDb
|
|
6
|
+
import { createKyselyDb } from '../helpers';
|
|
7
|
+
import { wrapVitestKyselyTransaction } from '../kysely';
|
|
8
8
|
|
|
9
9
|
const db = createKyselyDb<TestDatabase>(TEST_DATABASE_CONFIG);
|
|
10
10
|
const itWithTransaction = wrapVitestKyselyTransaction<TestDatabase>(
|
|
11
|
+
it,
|
|
11
12
|
db,
|
|
12
13
|
createTestTables,
|
|
13
14
|
);
|
|
@@ -17,9 +18,6 @@ pg.types.setTypeParser(int8TypeId, (val) => {
|
|
|
17
18
|
return parseInt(val, 10);
|
|
18
19
|
});
|
|
19
20
|
describe('KyselyFactory', () => {
|
|
20
|
-
let db: Kysely<TestDatabase>;
|
|
21
|
-
let trx: ControlledTransaction<TestDatabase, []>;
|
|
22
|
-
|
|
23
21
|
describe('KyselyFactory.insert', () => {
|
|
24
22
|
itWithTransaction(
|
|
25
23
|
'should insert a record with defaults',
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { beforeAll, describe, expect } from 'vitest';
|
|
1
|
+
import { it as base, beforeAll, describe, expect } from 'vitest';
|
|
2
2
|
import { TEST_DATABASE_CONFIG } from '../../test/globalSetup';
|
|
3
3
|
import { type TestDatabase, createTestTables } from '../../test/helpers';
|
|
4
4
|
import { KyselyFactory } from '../KyselyFactory';
|
|
5
|
-
import { createKyselyDb
|
|
5
|
+
import { createKyselyDb } from '../helpers';
|
|
6
|
+
import { wrapVitestKyselyTransaction } from '../kysely';
|
|
6
7
|
|
|
7
8
|
const db = createKyselyDb<TestDatabase>(TEST_DATABASE_CONFIG);
|
|
8
|
-
const it = wrapVitestKyselyTransaction<TestDatabase>(
|
|
9
|
+
const it = wrapVitestKyselyTransaction<TestDatabase>(
|
|
10
|
+
base,
|
|
11
|
+
db,
|
|
12
|
+
createTestTables,
|
|
13
|
+
);
|
|
9
14
|
describe('Testkit Integration Tests', () => {
|
|
10
15
|
beforeAll(async () => {});
|
|
11
16
|
describe('Complex Factory Scenarios', () => {
|
package/src/faker.ts
CHANGED
|
@@ -3,15 +3,39 @@ import { faker as baseFaker } from '@faker-js/faker';
|
|
|
3
3
|
// NOTE: This is a simple way to extend `faker` with additional methods
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Atomic counter implementation for thread-safe sequence generation
|
|
6
|
+
* Atomic counter implementation for thread-safe sequence generation.
|
|
7
|
+
* Provides a clean abstraction for generating sequential numbers in tests.
|
|
8
|
+
* While JavaScript is single-threaded, this class makes the intent explicit.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const counter = new AtomicCounter(100);
|
|
13
|
+
* console.log(counter.increment()); // 101
|
|
14
|
+
* console.log(counter.increment()); // 102
|
|
15
|
+
* console.log(counter.get()); // 102
|
|
16
|
+
* counter.reset(200);
|
|
17
|
+
* console.log(counter.increment()); // 201
|
|
18
|
+
* ```
|
|
7
19
|
*/
|
|
8
20
|
class AtomicCounter {
|
|
21
|
+
/**
|
|
22
|
+
* The current counter value.
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
9
25
|
private value: number;
|
|
10
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Creates a new atomic counter.
|
|
29
|
+
* @param initialValue - The starting value (default: 0)
|
|
30
|
+
*/
|
|
11
31
|
constructor(initialValue = 0) {
|
|
12
32
|
this.value = initialValue;
|
|
13
33
|
}
|
|
14
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Increments the counter and returns the new value.
|
|
37
|
+
* @returns The incremented value
|
|
38
|
+
*/
|
|
15
39
|
increment(): number {
|
|
16
40
|
// In Node.js, JavaScript is single-threaded within the event loop,
|
|
17
41
|
// so this operation is already atomic. However, this class provides
|
|
@@ -19,17 +43,42 @@ class AtomicCounter {
|
|
|
19
43
|
return ++this.value;
|
|
20
44
|
}
|
|
21
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Gets the current counter value without incrementing.
|
|
48
|
+
* @returns The current value
|
|
49
|
+
*/
|
|
22
50
|
get(): number {
|
|
23
51
|
return this.value;
|
|
24
52
|
}
|
|
25
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Resets the counter to a specific value.
|
|
56
|
+
* @param value - The new value (default: 0)
|
|
57
|
+
*/
|
|
26
58
|
reset(value = 0): void {
|
|
27
59
|
this.value = value;
|
|
28
60
|
}
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
/**
|
|
32
|
-
*
|
|
64
|
+
* Generates random timestamp fields for database records.
|
|
65
|
+
* Creates a createdAt date in the past and an updatedAt date between creation and now.
|
|
66
|
+
* Milliseconds are set to 0 for cleaner database storage.
|
|
67
|
+
*
|
|
68
|
+
* @returns Object with createdAt and updatedAt Date fields
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const { createdAt, updatedAt } = timestamps();
|
|
73
|
+
* console.log(createdAt); // 2023-05-15T10:30:00.000Z
|
|
74
|
+
* console.log(updatedAt); // 2023-11-20T14:45:00.000Z
|
|
75
|
+
*
|
|
76
|
+
* // Use in factory
|
|
77
|
+
* const user = {
|
|
78
|
+
* name: 'John Doe',
|
|
79
|
+
* ...timestamps()
|
|
80
|
+
* };
|
|
81
|
+
* ```
|
|
33
82
|
*/
|
|
34
83
|
export function timestamps(): Timestamps {
|
|
35
84
|
const createdAt = faker.date.past();
|
|
@@ -45,7 +94,18 @@ export function timestamps(): Timestamps {
|
|
|
45
94
|
}
|
|
46
95
|
|
|
47
96
|
/**
|
|
48
|
-
*
|
|
97
|
+
* Generates a reverse domain name identifier.
|
|
98
|
+
* Useful for creating unique identifiers that follow domain naming conventions.
|
|
99
|
+
*
|
|
100
|
+
* @param suffix - Optional suffix to append to the identifier
|
|
101
|
+
* @returns A reverse domain name string (e.g., "com.example.feature123")
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* console.log(identifier()); // "com.example.widget1"
|
|
106
|
+
* console.log(identifier('user')); // "org.acme.user"
|
|
107
|
+
* console.log(identifier('api')); // "net.demo.api"
|
|
108
|
+
* ```
|
|
49
109
|
*/
|
|
50
110
|
export function identifier(suffix?: string): string {
|
|
51
111
|
return [
|
|
@@ -55,9 +115,33 @@ export function identifier(suffix?: string): string {
|
|
|
55
115
|
].join('.');
|
|
56
116
|
}
|
|
57
117
|
|
|
58
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Storage for named sequence counters.
|
|
120
|
+
* Each sequence maintains its own independent counter.
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
59
123
|
const sequences = new Map<string, AtomicCounter>();
|
|
60
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Generates sequential numbers for a named sequence.
|
|
127
|
+
* Useful for creating unique IDs or numbered test data.
|
|
128
|
+
* Each named sequence maintains its own counter.
|
|
129
|
+
*
|
|
130
|
+
* @param name - The sequence name (default: 'default')
|
|
131
|
+
* @returns The next number in the sequence
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* console.log(sequence()); // 1
|
|
136
|
+
* console.log(sequence()); // 2
|
|
137
|
+
* console.log(sequence('user')); // 1
|
|
138
|
+
* console.log(sequence('user')); // 2
|
|
139
|
+
* console.log(sequence()); // 3
|
|
140
|
+
*
|
|
141
|
+
* // Use in factories
|
|
142
|
+
* const email = `user${sequence('email')}@example.com`;
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
61
145
|
export function sequence(name = 'default'): number {
|
|
62
146
|
if (!sequences.has(name)) {
|
|
63
147
|
sequences.set(name, new AtomicCounter());
|
|
@@ -68,7 +152,22 @@ export function sequence(name = 'default'): number {
|
|
|
68
152
|
}
|
|
69
153
|
|
|
70
154
|
/**
|
|
71
|
-
* Resets a sequence counter to a specific value
|
|
155
|
+
* Resets a named sequence counter to a specific value.
|
|
156
|
+
* Useful for resetting sequences between test suites.
|
|
157
|
+
*
|
|
158
|
+
* @param name - The sequence name to reset (default: 'default')
|
|
159
|
+
* @param value - The new starting value (default: 0)
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* sequence('user'); // 1
|
|
164
|
+
* sequence('user'); // 2
|
|
165
|
+
* resetSequence('user');
|
|
166
|
+
* sequence('user'); // 1
|
|
167
|
+
*
|
|
168
|
+
* resetSequence('order', 1000);
|
|
169
|
+
* sequence('order'); // 1001
|
|
170
|
+
* ```
|
|
72
171
|
*/
|
|
73
172
|
export function resetSequence(name = 'default', value = 0): void {
|
|
74
173
|
if (sequences.has(name)) {
|
|
@@ -80,19 +179,61 @@ export function resetSequence(name = 'default', value = 0): void {
|
|
|
80
179
|
}
|
|
81
180
|
|
|
82
181
|
/**
|
|
83
|
-
* Resets all sequence counters
|
|
182
|
+
* Resets all sequence counters.
|
|
183
|
+
* Useful for cleaning up between test suites to ensure predictable sequences.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* // In test setup
|
|
188
|
+
* beforeEach(() => {
|
|
189
|
+
* resetAllSequences();
|
|
190
|
+
* });
|
|
191
|
+
*
|
|
192
|
+
* it('starts sequences from 1', () => {
|
|
193
|
+
* expect(sequence()).toBe(1);
|
|
194
|
+
* expect(sequence('user')).toBe(1);
|
|
195
|
+
* });
|
|
196
|
+
* ```
|
|
84
197
|
*/
|
|
85
198
|
export function resetAllSequences(): void {
|
|
86
199
|
sequences.clear();
|
|
87
200
|
}
|
|
88
201
|
|
|
89
202
|
/**
|
|
90
|
-
*
|
|
203
|
+
* Generates a random price as a number.
|
|
204
|
+
* Converts faker's string price to a numeric value.
|
|
205
|
+
*
|
|
206
|
+
* @returns A random price number
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* const productPrice = price(); // 29.99
|
|
211
|
+
* const total = price() * quantity; // Numeric calculation
|
|
212
|
+
* ```
|
|
91
213
|
*/
|
|
92
214
|
function price(): number {
|
|
93
215
|
return +faker.commerce.price();
|
|
94
216
|
}
|
|
95
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Enhanced faker instance with additional utility methods for testing.
|
|
220
|
+
* Extends @faker-js/faker with custom methods for common test data generation patterns.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* import { faker } from '@geekmidas/testkit';
|
|
225
|
+
*
|
|
226
|
+
* // Use standard faker methods
|
|
227
|
+
* const name = faker.person.fullName();
|
|
228
|
+
* const email = faker.internet.email();
|
|
229
|
+
*
|
|
230
|
+
* // Use custom extensions
|
|
231
|
+
* const { createdAt, updatedAt } = faker.timestamps();
|
|
232
|
+
* const id = faker.identifier('user');
|
|
233
|
+
* const orderNumber = faker.sequence('order');
|
|
234
|
+
* const productPrice = faker.price();
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
96
237
|
export const faker = Object.freeze(
|
|
97
238
|
Object.assign({}, baseFaker, {
|
|
98
239
|
timestamps,
|
|
@@ -104,9 +245,19 @@ export const faker = Object.freeze(
|
|
|
104
245
|
}),
|
|
105
246
|
);
|
|
106
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Type definition for timestamp fields.
|
|
250
|
+
* Used by the timestamps() function to generate date fields.
|
|
251
|
+
*/
|
|
107
252
|
export type Timestamps = {
|
|
253
|
+
/** The creation date */
|
|
108
254
|
createdAt: Date;
|
|
255
|
+
/** The last update date */
|
|
109
256
|
updatedAt: Date;
|
|
110
257
|
};
|
|
111
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Type definition for the enhanced faker factory.
|
|
261
|
+
* Includes all standard faker methods plus custom extensions.
|
|
262
|
+
*/
|
|
112
263
|
export type FakerFactory = typeof faker;
|