Skip to content

Commit 38fea55

Browse files
authored
feat: add utils and testing utils (#4)
1 parent 2504b76 commit 38fea55

11 files changed

Lines changed: 381 additions & 18 deletions

File tree

README.md

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,109 @@
99
## 🚀 Getting started
1010

1111
```typescript
12-
import { PostgresDialect } from "https://deno.land/x/kysely_deno_postgres_dialect/mod.ts";
13-
14-
const dialect = new KyselyDenoPostgresDialect({
15-
pool: new Pool(
16-
{
17-
database: "postgres",
18-
hostname: "localhost",
19-
password: "postgres",
20-
port: 5432,
21-
user: "postgres",
12+
import {
13+
KyselyDenoPostgresDialect,
14+
setup,
15+
} from "https://deno.land/x/kysely_deno_postgres_dialect/mod.ts";
16+
17+
setup(() => {
18+
const dialect = new KyselyDenoPostgresDialect({
19+
pool: new Pool(
20+
{
21+
database: "postgres",
22+
hostname: "localhost",
23+
password: "postgres",
24+
port: 5432,
25+
user: "postgres",
26+
},
27+
10,
28+
true,
29+
),
30+
});
31+
32+
return new Kysely<Database>({
33+
dialect,
34+
});
35+
});
36+
37+
// run queries
38+
```
39+
40+
## 🩺 Testing
41+
42+
See detail at `./tests/testing/utils_test.ts`.
43+
44+
> [!IMPORTANT]\
45+
> To fix `leaking resources` error, you should end all connections between
46+
> cases.
47+
48+
```typescript
49+
import {
50+
KyselyDenoPostgresDialect,
51+
PostgresConnection,
52+
setup,
53+
wrapTransaction,
54+
} from "https://deno.land/x/kysely_deno_postgres_dialect/mod.ts";
55+
import { setupTesting } from "https://deno.land/x/kysely_deno_postgres_dialect/testing.ts";
56+
57+
const connections: PostgresConnection[] = [];
58+
59+
setup(() => {
60+
const dialect = new KyselyDenoPostgresDialect({
61+
pool: new Pool(
62+
{
63+
database: "postgres",
64+
hostname: "localhost",
65+
password: "postgres",
66+
port: 5432,
67+
user: "postgres",
68+
},
69+
10,
70+
true,
71+
),
72+
onCreateConnection: (connection: DatabaseConnection) => {
73+
connections.push(connection as PostgresConnection);
2274
},
23-
10,
24-
true,
25-
),
75+
});
76+
77+
return new Kysely<Database>({
78+
dialect,
79+
});
2680
});
2781

28-
export const db = new Kysely<Database>({
29-
dialect,
82+
async function endConnections() {
83+
for (const connection of connections) {
84+
await connection.end();
85+
}
86+
}
87+
88+
// test files
89+
90+
const {
91+
beforeAllFn,
92+
beforeEachFn,
93+
afterEachFn,
94+
afterAllFn,
95+
} = setupTesting(stub);
96+
97+
describe("tests", () => {
98+
beforeAll(async () => {
99+
await beforeAllFn();
100+
});
101+
afterAll(async () => {
102+
await afterAllFn();
103+
});
104+
beforeEach(async () => {
105+
await beforeEachFn();
106+
});
107+
afterEach(async () => {
108+
// note: fix Leaking resources error
109+
await endConnections();
110+
await afterEachFn();
111+
});
112+
113+
it("works", async () => {
114+
// snip
115+
});
30116
});
31117
```

deps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export {
1111
PostgresQueryCompiler,
1212
type QueryCompiler,
1313
type QueryResult,
14+
Transaction,
1415
type TransactionSettings,
1516
} from "https://esm.sh/kysely@0.26.3";
1617
export { Pool, PoolClient } from "https://deno.land/x/postgres@v0.17.0/mod.ts";

mod.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { PostgresDriver } from "./src/postgres-driver.ts";
1414
import { PostgresDialectConfig } from "./src/postgres-dialect-config.ts";
1515

1616
export { type DatabaseConnection, kysely, Pool, postgres } from "./deps.ts";
17+
export { type PostgresConnection } from "./src/postgres-connection.ts";
1718

1819
export class KyselyDenoPostgresDialect implements Dialect {
1920
readonly #config: PostgresDialectConfig;
@@ -39,3 +40,5 @@ export class KyselyDenoPostgresDialect implements Dialect {
3940
return new PostgresIntrospector(db);
4041
}
4142
}
43+
44+
export { getDB, setup, wrapTransaction } from "./src/utils.ts";

src/postgres-connection.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,8 @@ export class PostgresConnection implements DatabaseConnection {
4949
[PRIVATE_RELEASE_METHOD](): void {
5050
this.#client.release();
5151
}
52+
53+
async end(): Promise<void> {
54+
await this.#client.end();
55+
}
5256
}

src/postgres-dialect-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ export interface PostgresDialectConfig {
1111
*
1212
* https://deno-postgres.com/#/?id=connection-pools
1313
*/
14-
pool: Pool | (() => Promise<Pool>);
14+
pool: Pool | (() => Promise<Pool> | Pool);
1515

1616
/**
1717
* Called once for each created connection.
1818
*/
19-
onCreateConnection?: (connection: DatabaseConnection) => Promise<void>;
19+
onCreateConnection?: (connection: DatabaseConnection) => Promise<void> | void;
2020
}

src/testing/begin.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// deno-lint-ignore-file
2+
// https://github.com/kysely-org/kysely/issues/257#issuecomment-1676079354
3+
4+
import { Kysely, Transaction } from "../../deps.ts";
5+
6+
class Deferred<T> {
7+
readonly #promise: Promise<T>;
8+
#resolve?: (value: T | PromiseLike<T>) => void;
9+
#reject?: (reason?: any) => void;
10+
11+
constructor() {
12+
this.#promise = new Promise<T>((resolve, reject) => {
13+
this.#resolve = resolve;
14+
this.#reject = reject;
15+
});
16+
}
17+
18+
get promise(): Promise<T> {
19+
return this.#promise;
20+
}
21+
22+
resolve(value: T | PromiseLike<T>): void {
23+
this.#resolve?.(value);
24+
}
25+
26+
reject(reason?: any): void {
27+
this.#reject?.(reason);
28+
}
29+
}
30+
31+
export default async function begin<T>(db: Kysely<T>) {
32+
const connection = new Deferred<Transaction<T>>();
33+
const result = new Deferred<any>();
34+
35+
const execution = db.transaction().execute(async (trx) => {
36+
connection.resolve(trx);
37+
await result.promise;
38+
}).catch((_err) => {
39+
// Swallow the exception here
40+
});
41+
42+
const trx = await connection.promise;
43+
44+
return {
45+
trx,
46+
async rollback() {
47+
result.reject(new Error("rollback"));
48+
await execution;
49+
},
50+
};
51+
}

src/testing/utils.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Transaction as KyselyTransaction } from "../../deps.ts";
2+
import { _internals, Callback, closeDB, getDB } from "../utils.ts";
3+
import begin from "./begin.ts";
4+
5+
type Maybe<T> = T | undefined;
6+
7+
// deno-lint-ignore no-explicit-any
8+
type Transaction = KyselyTransaction<any>;
9+
let transaction: Maybe<Transaction>;
10+
11+
let rollbackFn: Maybe<() => Promise<void>>;
12+
13+
async function doWrapTransactionStub<T, U>(
14+
callback: Callback<T, U>,
15+
): Promise<U> {
16+
if (!transaction) {
17+
throw new Error("Transaction not initialized");
18+
}
19+
20+
try {
21+
const result = await callback(transaction);
22+
return result;
23+
} catch (error) {
24+
console.error("Transaction failed: ", error);
25+
26+
throw error;
27+
}
28+
}
29+
30+
type Stub = { restore: () => unknown };
31+
32+
/**
33+
* Setup a database for testing.
34+
*
35+
* @param stub - The stub to use for wrapping the transaction.
36+
* @returns The `beforeAllFn`, `beforeEachFn`, `afterEachFn` and `afterAllFn` functions to use in your test suite.
37+
* @example
38+
* ```ts
39+
* import { stub } from "https://deno.land/std@0.210.0/testing/mock.ts";
40+
* const { beforeEachFn, afterEachFn } = setupTesting(stub)
41+
* ```
42+
*/
43+
export function setupTesting<T>(
44+
stub: (...args: unknown[]) => Stub,
45+
) {
46+
let transactionStub: Maybe<Stub>;
47+
48+
// deno-lint-ignore require-await
49+
const beforeAllFn = async () => {
50+
getDB<T>();
51+
};
52+
53+
const beforeEachFn = async () => {
54+
const db = getDB<T>();
55+
56+
const { trx, rollback } = await begin(db);
57+
transaction = trx;
58+
rollbackFn = rollback;
59+
60+
transactionStub = stub(
61+
_internals,
62+
"doWrapTransaction",
63+
doWrapTransactionStub,
64+
);
65+
};
66+
67+
const afterEachFn = async () => {
68+
if (transaction) {
69+
await rollbackFn?.();
70+
transaction = undefined;
71+
}
72+
73+
transactionStub?.restore();
74+
};
75+
76+
const afterAllFn = async () => {
77+
await closeDB();
78+
};
79+
80+
return { beforeAllFn, beforeEachFn, afterEachFn, afterAllFn };
81+
}

src/utils.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Kysely, Transaction } from "../deps.ts";
2+
3+
type Maybe<T> = T | undefined;
4+
5+
type DB<T> = Kysely<T>;
6+
// deno-lint-ignore no-explicit-any
7+
let db: Maybe<DB<any>>;
8+
9+
type SetupFn<T> = () => DB<T>;
10+
// deno-lint-ignore no-explicit-any
11+
let setupFn: Maybe<SetupFn<any>>;
12+
13+
export function setup<T>(setup: SetupFn<T>): void {
14+
setupFn = setup;
15+
}
16+
17+
function newDB<T>(): DB<T> {
18+
if (setupFn) {
19+
db = setupFn();
20+
return db;
21+
}
22+
23+
throw new Error("Please call setup() before using the database");
24+
}
25+
26+
export function getDB<T>(): Kysely<T> {
27+
return db || newDB<T>();
28+
}
29+
30+
export async function closeDB() {
31+
if (db) {
32+
await db.destroy();
33+
db = undefined;
34+
}
35+
}
36+
37+
export type Callback<T, U> = (trx: Transaction<T>) => Promise<U>;
38+
39+
async function doWrapTransaction<T, U>(callback: Callback<T, U>): Promise<U> {
40+
try {
41+
return await getDB<T>().transaction().execute(callback);
42+
} finally {
43+
await closeDB();
44+
}
45+
}
46+
47+
export async function wrapTransaction<T, U>(
48+
callback: Callback<T, U>,
49+
): Promise<U> {
50+
return await _internals.doWrapTransaction<T, U>(callback);
51+
}
52+
53+
export const _internals = { doWrapTransaction };

testing.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { setupTesting } from "./src/testing/utils.ts";

tests/support/database.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import { Kysely } from "../../deps.ts";
1+
import { DatabaseConnection, Kysely } from "../../deps.ts";
22
import { Database } from "./types.ts"; // this is the Database interface we defined earlier
33
import { KyselyDenoPostgresDialect, Pool } from "../../mod.ts";
4+
import { PostgresConnection } from "../../src/postgres-connection.ts";
5+
6+
const connections: PostgresConnection[] = [];
47

58
const dialect = new KyselyDenoPostgresDialect({
69
pool: new Pool(
@@ -14,8 +17,17 @@ const dialect = new KyselyDenoPostgresDialect({
1417
10,
1518
true,
1619
),
20+
onCreateConnection: (connection: DatabaseConnection) => {
21+
connections.push(connection as PostgresConnection);
22+
},
1723
});
1824

1925
export const db = new Kysely<Database>({
2026
dialect,
2127
});
28+
29+
export async function endConnections() {
30+
for (const connection of connections) {
31+
await connection.end();
32+
}
33+
}

0 commit comments

Comments
 (0)