@geekmidas/testkit 10.0.0-alpha.28 → 10.0.0-alpha.29
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/transactions.cjs +17 -8
- package/dist/transactions.cjs.map +1 -1
- package/dist/transactions.d.cts +10 -4
- package/dist/transactions.d.cts.map +1 -1
- package/dist/transactions.d.mts +10 -4
- package/dist/transactions.d.mts.map +1 -1
- package/dist/transactions.mjs +17 -8
- package/dist/transactions.mjs.map +1 -1
- package/package.json +7 -7
package/dist/transactions.cjs
CHANGED
|
@@ -37,16 +37,25 @@ function connectionConfig(url) {
|
|
|
37
37
|
options: `-c search_path=${searchPath}`
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Open a connection to `url` and begin the transaction a test will roll back.
|
|
42
|
+
*
|
|
43
|
+
* `config` is what the database's own clients are built with — its plugins
|
|
44
|
+
* above all. The code under test is handed this Kysely in place of its own, so
|
|
45
|
+
* one built without them runs different queries than production does.
|
|
46
|
+
*/
|
|
47
|
+
async function openBoundTransaction(url, config = {}) {
|
|
42
48
|
const client = new pg.default.Client(connectionConfig(url));
|
|
43
49
|
await client.connect();
|
|
44
50
|
await client.query("BEGIN");
|
|
45
51
|
return {
|
|
46
|
-
db: new kysely.Kysely({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
db: new kysely.Kysely({
|
|
53
|
+
...config,
|
|
54
|
+
dialect: new kysely.PostgresDialect({ pool: {
|
|
55
|
+
connect: async () => savepointing(client),
|
|
56
|
+
end: async () => {}
|
|
57
|
+
} })
|
|
58
|
+
}),
|
|
50
59
|
async rollback() {
|
|
51
60
|
try {
|
|
52
61
|
await client.query("ROLLBACK");
|
|
@@ -91,10 +100,10 @@ function savepointing(client) {
|
|
|
91
100
|
*/
|
|
92
101
|
var TransactionRegistry = class {
|
|
93
102
|
open = /* @__PURE__ */ new Map();
|
|
94
|
-
get(key, url) {
|
|
103
|
+
get(key, url, config) {
|
|
95
104
|
let transaction = this.open.get(key);
|
|
96
105
|
if (!transaction) {
|
|
97
|
-
transaction = openBoundTransaction(url);
|
|
106
|
+
transaction = openBoundTransaction(url, config);
|
|
98
107
|
this.open.set(key, transaction);
|
|
99
108
|
}
|
|
100
109
|
return transaction.then(({ db }) => db);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transactions.cjs","names":["Kysely","PostgresDialect"],"sources":["../src/transactions.ts"],"sourcesContent":["/**\n * A transaction per database, per test — as each runs deployed.\n *\n * An app's database and a schema tenant such as the auth server's are separate\n * databases to the code using them: separate connections, credentials, roles\n * and search paths. A test keeps them that way. Each gets its own connection,\n * opened into a transaction the first time the test touches it, and every one\n * is rolled back when the test ends.\n *\n * Code under test must be free to use transactions itself, and better-auth\n * does — it also calls `db.connection()`, which Kysely refuses on a\n * `Transaction`. So what a test hands over is not a `Transaction` but a Kysely\n * over the one connection already inside the test's transaction, on which a\n * `BEGIN` becomes a savepoint and a `COMMIT` releases it. Nothing the code does\n * can end the transaction the test will roll back.\n */\n\nimport { Kysely, PostgresDialect } from 'kysely';\nimport pg from 'pg';\n\n/** How to reach one database: a URL, and its search path as a startup option. */\nexport interface ConnectionConfig {\n\tconnectionString: string;\n\toptions?: string;\n}\n\n/**\n * The connection settings for a database URL.\n *\n * `?search_path=` is not a libpq parameter: a URL carrying it connects and then\n * resolves every name against `public`. Postgres takes it as a startup option,\n * which is what a schema tenant's URL needs to reach its own tables.\n */\nexport function connectionConfig(url: string): ConnectionConfig {\n\tconst parsed = new URL(url);\n\tconst searchPath = parsed.searchParams.get('search_path');\n\tif (!searchPath) return { connectionString: url };\n\n\tparsed.searchParams.delete('search_path');\n\treturn {\n\t\tconnectionString: parsed.toString(),\n\t\toptions: `-c search_path=${searchPath}`,\n\t};\n}\n\n/** One database's connection, inside a transaction the test will roll back. */\nexport interface BoundTransaction {\n\t/** Everything queried through this runs inside the test's transaction. */\n\treadonly db: Kysely<any>;\n\t/** Undo everything, and let the connection go. */\n\trollback(): Promise<void>;\n}\n\n
|
|
1
|
+
{"version":3,"file":"transactions.cjs","names":["Kysely","PostgresDialect"],"sources":["../src/transactions.ts"],"sourcesContent":["/**\n * A transaction per database, per test — as each runs deployed.\n *\n * An app's database and a schema tenant such as the auth server's are separate\n * databases to the code using them: separate connections, credentials, roles\n * and search paths. A test keeps them that way. Each gets its own connection,\n * opened into a transaction the first time the test touches it, and every one\n * is rolled back when the test ends.\n *\n * Code under test must be free to use transactions itself, and better-auth\n * does — it also calls `db.connection()`, which Kysely refuses on a\n * `Transaction`. So what a test hands over is not a `Transaction` but a Kysely\n * over the one connection already inside the test's transaction, on which a\n * `BEGIN` becomes a savepoint and a `COMMIT` releases it. Nothing the code does\n * can end the transaction the test will roll back.\n */\n\nimport { Kysely, type KyselyConfig, PostgresDialect } from 'kysely';\nimport pg from 'pg';\n\n/** How to reach one database: a URL, and its search path as a startup option. */\nexport interface ConnectionConfig {\n\tconnectionString: string;\n\toptions?: string;\n}\n\n/**\n * The connection settings for a database URL.\n *\n * `?search_path=` is not a libpq parameter: a URL carrying it connects and then\n * resolves every name against `public`. Postgres takes it as a startup option,\n * which is what a schema tenant's URL needs to reach its own tables.\n */\nexport function connectionConfig(url: string): ConnectionConfig {\n\tconst parsed = new URL(url);\n\tconst searchPath = parsed.searchParams.get('search_path');\n\tif (!searchPath) return { connectionString: url };\n\n\tparsed.searchParams.delete('search_path');\n\treturn {\n\t\tconnectionString: parsed.toString(),\n\t\toptions: `-c search_path=${searchPath}`,\n\t};\n}\n\n/** One database's connection, inside a transaction the test will roll back. */\nexport interface BoundTransaction {\n\t/** Everything queried through this runs inside the test's transaction. */\n\treadonly db: Kysely<any>;\n\t/** Undo everything, and let the connection go. */\n\trollback(): Promise<void>;\n}\n\n/**\n * Open a connection to `url` and begin the transaction a test will roll back.\n *\n * `config` is what the database's own clients are built with — its plugins\n * above all. The code under test is handed this Kysely in place of its own, so\n * one built without them runs different queries than production does.\n */\nexport async function openBoundTransaction(\n\turl: string,\n\tconfig: Omit<KyselyConfig, 'dialect'> = {},\n): Promise<BoundTransaction> {\n\tconst client = new pg.Client(connectionConfig(url));\n\tawait client.connect();\n\tawait client.query('BEGIN');\n\n\treturn {\n\t\tdb: new Kysely({\n\t\t\t...config,\n\t\t\tdialect: new PostgresDialect({\n\t\t\t\t// A \"pool\" of the one connection. Released never, ended never: the\n\t\t\t\t// test owns it, and it closes in `rollback`.\n\t\t\t\tpool: {\n\t\t\t\t\tconnect: async () => savepointing(client),\n\t\t\t\t\tend: async () => {},\n\t\t\t\t} as unknown as pg.Pool,\n\t\t\t}),\n\t\t}),\n\t\tasync rollback() {\n\t\t\ttry {\n\t\t\t\tawait client.query('ROLLBACK');\n\t\t\t} finally {\n\t\t\t\tawait client.end();\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * The client, with transaction control turned into savepoints.\n *\n * Kysely issues `begin`/`commit`/`rollback` as plain statements; this is the\n * one place they can be caught. A depth counter names the savepoints, so\n * nested transactions nest.\n */\nfunction savepointing(client: pg.Client): pg.PoolClient {\n\tlet depth = 0;\n\tconst query = client.query.bind(client) as (...args: unknown[]) => unknown;\n\n\tconst wrapped = Object.create(client) as pg.PoolClient;\n\tObject.assign(wrapped, {\n\t\tquery: (text: unknown, ...rest: unknown[]) => {\n\t\t\tconst statement =\n\t\t\t\ttypeof text === 'string' ? text.trim().toLowerCase() : undefined;\n\n\t\t\tif (statement === 'begin' || statement?.startsWith('start transaction')) {\n\t\t\t\tdepth++;\n\t\t\t\treturn query(`SAVEPOINT test_sp_${depth}`);\n\t\t\t}\n\t\t\tif (statement === 'commit') {\n\t\t\t\treturn query(`RELEASE SAVEPOINT test_sp_${depth--}`);\n\t\t\t}\n\t\t\tif (statement === 'rollback') {\n\t\t\t\treturn query(`ROLLBACK TO SAVEPOINT test_sp_${depth--}`);\n\t\t\t}\n\t\t\treturn query(text, ...rest);\n\t\t},\n\t\trelease: () => {},\n\t});\n\treturn wrapped;\n}\n\n/**\n * The transactions one test has opened, by database.\n *\n * `get` opens a database's transaction the first time it is asked for and\n * hands back the same one after that — including to a caller that asks while\n * it is still opening. `rollbackAll` ends every one of them.\n */\nexport class TransactionRegistry<TKey = string> {\n\tprivate readonly open = new Map<TKey, Promise<BoundTransaction>>();\n\n\tget(\n\t\tkey: TKey,\n\t\turl: string,\n\t\tconfig?: Omit<KyselyConfig, 'dialect'>,\n\t): Promise<Kysely<any>> {\n\t\tlet transaction = this.open.get(key);\n\t\tif (!transaction) {\n\t\t\ttransaction = openBoundTransaction(url, config);\n\t\t\tthis.open.set(key, transaction);\n\t\t}\n\t\treturn transaction.then(({ db }) => db);\n\t}\n\n\tasync rollbackAll(): Promise<void> {\n\t\tconst transactions = [...this.open.values()];\n\t\tthis.open.clear();\n\n\t\tconst results = await Promise.allSettled(\n\t\t\ttransactions.map(async (transaction) => (await transaction).rollback()),\n\t\t);\n\t\tconst failed = results.find(\n\t\t\t(result): result is PromiseRejectedResult => result.status === 'rejected',\n\t\t);\n\t\tif (failed) throw failed.reason;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,iBAAiB,KAA+B;CAC/D,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,aAAa,OAAO,aAAa,IAAI,aAAa;CACxD,IAAI,CAAC,YAAY,OAAO,EAAE,kBAAkB,IAAI;CAEhD,OAAO,aAAa,OAAO,aAAa;CACxC,OAAO;EACN,kBAAkB,OAAO,SAAS;EAClC,SAAS,kBAAkB;CAC5B;AACD;;;;;;;;AAiBA,eAAsB,qBACrB,KACA,SAAwC,CAAC,GACb;CAC5B,MAAM,SAAS,IAAI,GAAA,QAAG,OAAO,iBAAiB,GAAG,CAAC;CAClD,MAAM,OAAO,QAAQ;CACrB,MAAM,OAAO,MAAM,OAAO;CAE1B,OAAO;EACN,IAAI,IAAIA,OAAAA,OAAO;GACd,GAAG;GACH,SAAS,IAAIC,OAAAA,gBAAgB,EAG5B,MAAM;IACL,SAAS,YAAY,aAAa,MAAM;IACxC,KAAK,YAAY,CAAC;GACnB,EACD,CAAC;EACF,CAAC;EACD,MAAM,WAAW;GAChB,IAAI;IACH,MAAM,OAAO,MAAM,UAAU;GAC9B,UAAU;IACT,MAAM,OAAO,IAAI;GAClB;EACD;CACD;AACD;;;;;;;;AASA,SAAS,aAAa,QAAkC;CACvD,IAAI,QAAQ;CACZ,MAAM,QAAQ,OAAO,MAAM,KAAK,MAAM;CAEtC,MAAM,UAAU,OAAO,OAAO,MAAM;CACpC,OAAO,OAAO,SAAS;EACtB,QAAQ,MAAe,GAAG,SAAoB;GAC7C,MAAM,YACL,OAAO,SAAS,WAAW,KAAK,KAAK,CAAC,CAAC,YAAY,IAAI,KAAA;GAExD,IAAI,cAAc,WAAW,WAAW,WAAW,mBAAmB,GAAG;IACxE;IACA,OAAO,MAAM,qBAAqB,OAAO;GAC1C;GACA,IAAI,cAAc,UACjB,OAAO,MAAM,6BAA6B,SAAS;GAEpD,IAAI,cAAc,YACjB,OAAO,MAAM,iCAAiC,SAAS;GAExD,OAAO,MAAM,MAAM,GAAG,IAAI;EAC3B;EACA,eAAe,CAAC;CACjB,CAAC;CACD,OAAO;AACR;;;;;;;;AASA,IAAa,sBAAb,MAAgD;CAC/C,uBAAwB,IAAI,IAAqC;CAEjE,IACC,KACA,KACA,QACuB;EACvB,IAAI,cAAc,KAAK,KAAK,IAAI,GAAG;EACnC,IAAI,CAAC,aAAa;GACjB,cAAc,qBAAqB,KAAK,MAAM;GAC9C,KAAK,KAAK,IAAI,KAAK,WAAW;EAC/B;EACA,OAAO,YAAY,MAAM,EAAE,SAAS,EAAE;CACvC;CAEA,MAAM,cAA6B;EAClC,MAAM,eAAe,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC;EAC3C,KAAK,KAAK,MAAM;EAKhB,MAAM,UAAS,MAHO,QAAQ,WAC7B,aAAa,IAAI,OAAO,iBAAiB,MAAM,YAAA,CAAa,SAAS,CAAC,CACvE,EAAA,CACuB,MACrB,WAA4C,OAAO,WAAW,UAChE;EACA,IAAI,QAAQ,MAAM,OAAO;CAC1B;AACD"}
|
package/dist/transactions.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Kysely } from "kysely";
|
|
1
|
+
import { Kysely, KyselyConfig } from "kysely";
|
|
2
2
|
//#region src/transactions.d.ts
|
|
3
3
|
/** How to reach one database: a URL, and its search path as a startup option. */
|
|
4
4
|
export interface ConnectionConfig {
|
|
@@ -20,8 +20,14 @@ export interface BoundTransaction {
|
|
|
20
20
|
/** Undo everything, and let the connection go. */
|
|
21
21
|
rollback(): Promise<void>;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Open a connection to `url` and begin the transaction a test will roll back.
|
|
25
|
+
*
|
|
26
|
+
* `config` is what the database's own clients are built with — its plugins
|
|
27
|
+
* above all. The code under test is handed this Kysely in place of its own, so
|
|
28
|
+
* one built without them runs different queries than production does.
|
|
29
|
+
*/
|
|
30
|
+
export declare function openBoundTransaction(url: string, config?: Omit<KyselyConfig, 'dialect'>): Promise<BoundTransaction>;
|
|
25
31
|
/**
|
|
26
32
|
* The transactions one test has opened, by database.
|
|
27
33
|
*
|
|
@@ -31,7 +37,7 @@ export declare function openBoundTransaction(url: string): Promise<BoundTransact
|
|
|
31
37
|
*/
|
|
32
38
|
export declare class TransactionRegistry<TKey = string> {
|
|
33
39
|
private readonly open;
|
|
34
|
-
get(key: TKey, url: string): Promise<Kysely<any>>;
|
|
40
|
+
get(key: TKey, url: string, config?: Omit<KyselyConfig, 'dialect'>): Promise<Kysely<any>>;
|
|
35
41
|
rollbackAll(): Promise<void>;
|
|
36
42
|
}
|
|
37
43
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transactions.d.cts","names":[],"sources":["../src/transactions.ts"],"mappings":";;;iBAqBiB;EAChB;EACA;;;;;;;;;wBAUe,iBAAiB,cAAc;;iBAa9B;;WAEP,IAAI;;EAEb,YAAY
|
|
1
|
+
{"version":3,"file":"transactions.d.cts","names":[],"sources":["../src/transactions.ts"],"mappings":";;;iBAqBiB;EAChB;EACA;;;;;;;;;wBAUe,iBAAiB,cAAc;;iBAa9B;;WAEP,IAAI;;EAEb,YAAY;;;;;;;;;wBAUS,qBACrB,aACA,SAAQ,KAAK,2BACX,QAAQ;;;;;;;;qBAoEE,oBAAoB;mBACf;EAEjB,IACC,KAAK,MACL,aACA,SAAS,KAAK,2BACZ,QAAQ;EASL,eAAe"}
|
package/dist/transactions.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Kysely } from "kysely";
|
|
1
|
+
import { Kysely, KyselyConfig } from "kysely";
|
|
2
2
|
//#region src/transactions.d.ts
|
|
3
3
|
/** How to reach one database: a URL, and its search path as a startup option. */
|
|
4
4
|
export interface ConnectionConfig {
|
|
@@ -20,8 +20,14 @@ export interface BoundTransaction {
|
|
|
20
20
|
/** Undo everything, and let the connection go. */
|
|
21
21
|
rollback(): Promise<void>;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Open a connection to `url` and begin the transaction a test will roll back.
|
|
25
|
+
*
|
|
26
|
+
* `config` is what the database's own clients are built with — its plugins
|
|
27
|
+
* above all. The code under test is handed this Kysely in place of its own, so
|
|
28
|
+
* one built without them runs different queries than production does.
|
|
29
|
+
*/
|
|
30
|
+
export declare function openBoundTransaction(url: string, config?: Omit<KyselyConfig, 'dialect'>): Promise<BoundTransaction>;
|
|
25
31
|
/**
|
|
26
32
|
* The transactions one test has opened, by database.
|
|
27
33
|
*
|
|
@@ -31,7 +37,7 @@ export declare function openBoundTransaction(url: string): Promise<BoundTransact
|
|
|
31
37
|
*/
|
|
32
38
|
export declare class TransactionRegistry<TKey = string> {
|
|
33
39
|
private readonly open;
|
|
34
|
-
get(key: TKey, url: string): Promise<Kysely<any>>;
|
|
40
|
+
get(key: TKey, url: string, config?: Omit<KyselyConfig, 'dialect'>): Promise<Kysely<any>>;
|
|
35
41
|
rollbackAll(): Promise<void>;
|
|
36
42
|
}
|
|
37
43
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transactions.d.mts","names":[],"sources":["../src/transactions.ts"],"mappings":";;;iBAqBiB;EAChB;EACA;;;;;;;;;wBAUe,iBAAiB,cAAc;;iBAa9B;;WAEP,IAAI;;EAEb,YAAY
|
|
1
|
+
{"version":3,"file":"transactions.d.mts","names":[],"sources":["../src/transactions.ts"],"mappings":";;;iBAqBiB;EAChB;EACA;;;;;;;;;wBAUe,iBAAiB,cAAc;;iBAa9B;;WAEP,IAAI;;EAEb,YAAY;;;;;;;;;wBAUS,qBACrB,aACA,SAAQ,KAAK,2BACX,QAAQ;;;;;;;;qBAoEE,oBAAoB;mBACf;EAEjB,IACC,KAAK,MACL,aACA,SAAS,KAAK,2BACZ,QAAQ;EASL,eAAe"}
|
package/dist/transactions.mjs
CHANGED
|
@@ -34,16 +34,25 @@ function connectionConfig(url) {
|
|
|
34
34
|
options: `-c search_path=${searchPath}`
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Open a connection to `url` and begin the transaction a test will roll back.
|
|
39
|
+
*
|
|
40
|
+
* `config` is what the database's own clients are built with — its plugins
|
|
41
|
+
* above all. The code under test is handed this Kysely in place of its own, so
|
|
42
|
+
* one built without them runs different queries than production does.
|
|
43
|
+
*/
|
|
44
|
+
async function openBoundTransaction(url, config = {}) {
|
|
39
45
|
const client = new pg.Client(connectionConfig(url));
|
|
40
46
|
await client.connect();
|
|
41
47
|
await client.query("BEGIN");
|
|
42
48
|
return {
|
|
43
|
-
db: new Kysely({
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
db: new Kysely({
|
|
50
|
+
...config,
|
|
51
|
+
dialect: new PostgresDialect({ pool: {
|
|
52
|
+
connect: async () => savepointing(client),
|
|
53
|
+
end: async () => {}
|
|
54
|
+
} })
|
|
55
|
+
}),
|
|
47
56
|
async rollback() {
|
|
48
57
|
try {
|
|
49
58
|
await client.query("ROLLBACK");
|
|
@@ -88,10 +97,10 @@ function savepointing(client) {
|
|
|
88
97
|
*/
|
|
89
98
|
var TransactionRegistry = class {
|
|
90
99
|
open = /* @__PURE__ */ new Map();
|
|
91
|
-
get(key, url) {
|
|
100
|
+
get(key, url, config) {
|
|
92
101
|
let transaction = this.open.get(key);
|
|
93
102
|
if (!transaction) {
|
|
94
|
-
transaction = openBoundTransaction(url);
|
|
103
|
+
transaction = openBoundTransaction(url, config);
|
|
95
104
|
this.open.set(key, transaction);
|
|
96
105
|
}
|
|
97
106
|
return transaction.then(({ db }) => db);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transactions.mjs","names":[],"sources":["../src/transactions.ts"],"sourcesContent":["/**\n * A transaction per database, per test — as each runs deployed.\n *\n * An app's database and a schema tenant such as the auth server's are separate\n * databases to the code using them: separate connections, credentials, roles\n * and search paths. A test keeps them that way. Each gets its own connection,\n * opened into a transaction the first time the test touches it, and every one\n * is rolled back when the test ends.\n *\n * Code under test must be free to use transactions itself, and better-auth\n * does — it also calls `db.connection()`, which Kysely refuses on a\n * `Transaction`. So what a test hands over is not a `Transaction` but a Kysely\n * over the one connection already inside the test's transaction, on which a\n * `BEGIN` becomes a savepoint and a `COMMIT` releases it. Nothing the code does\n * can end the transaction the test will roll back.\n */\n\nimport { Kysely, PostgresDialect } from 'kysely';\nimport pg from 'pg';\n\n/** How to reach one database: a URL, and its search path as a startup option. */\nexport interface ConnectionConfig {\n\tconnectionString: string;\n\toptions?: string;\n}\n\n/**\n * The connection settings for a database URL.\n *\n * `?search_path=` is not a libpq parameter: a URL carrying it connects and then\n * resolves every name against `public`. Postgres takes it as a startup option,\n * which is what a schema tenant's URL needs to reach its own tables.\n */\nexport function connectionConfig(url: string): ConnectionConfig {\n\tconst parsed = new URL(url);\n\tconst searchPath = parsed.searchParams.get('search_path');\n\tif (!searchPath) return { connectionString: url };\n\n\tparsed.searchParams.delete('search_path');\n\treturn {\n\t\tconnectionString: parsed.toString(),\n\t\toptions: `-c search_path=${searchPath}`,\n\t};\n}\n\n/** One database's connection, inside a transaction the test will roll back. */\nexport interface BoundTransaction {\n\t/** Everything queried through this runs inside the test's transaction. */\n\treadonly db: Kysely<any>;\n\t/** Undo everything, and let the connection go. */\n\trollback(): Promise<void>;\n}\n\n
|
|
1
|
+
{"version":3,"file":"transactions.mjs","names":[],"sources":["../src/transactions.ts"],"sourcesContent":["/**\n * A transaction per database, per test — as each runs deployed.\n *\n * An app's database and a schema tenant such as the auth server's are separate\n * databases to the code using them: separate connections, credentials, roles\n * and search paths. A test keeps them that way. Each gets its own connection,\n * opened into a transaction the first time the test touches it, and every one\n * is rolled back when the test ends.\n *\n * Code under test must be free to use transactions itself, and better-auth\n * does — it also calls `db.connection()`, which Kysely refuses on a\n * `Transaction`. So what a test hands over is not a `Transaction` but a Kysely\n * over the one connection already inside the test's transaction, on which a\n * `BEGIN` becomes a savepoint and a `COMMIT` releases it. Nothing the code does\n * can end the transaction the test will roll back.\n */\n\nimport { Kysely, type KyselyConfig, PostgresDialect } from 'kysely';\nimport pg from 'pg';\n\n/** How to reach one database: a URL, and its search path as a startup option. */\nexport interface ConnectionConfig {\n\tconnectionString: string;\n\toptions?: string;\n}\n\n/**\n * The connection settings for a database URL.\n *\n * `?search_path=` is not a libpq parameter: a URL carrying it connects and then\n * resolves every name against `public`. Postgres takes it as a startup option,\n * which is what a schema tenant's URL needs to reach its own tables.\n */\nexport function connectionConfig(url: string): ConnectionConfig {\n\tconst parsed = new URL(url);\n\tconst searchPath = parsed.searchParams.get('search_path');\n\tif (!searchPath) return { connectionString: url };\n\n\tparsed.searchParams.delete('search_path');\n\treturn {\n\t\tconnectionString: parsed.toString(),\n\t\toptions: `-c search_path=${searchPath}`,\n\t};\n}\n\n/** One database's connection, inside a transaction the test will roll back. */\nexport interface BoundTransaction {\n\t/** Everything queried through this runs inside the test's transaction. */\n\treadonly db: Kysely<any>;\n\t/** Undo everything, and let the connection go. */\n\trollback(): Promise<void>;\n}\n\n/**\n * Open a connection to `url` and begin the transaction a test will roll back.\n *\n * `config` is what the database's own clients are built with — its plugins\n * above all. The code under test is handed this Kysely in place of its own, so\n * one built without them runs different queries than production does.\n */\nexport async function openBoundTransaction(\n\turl: string,\n\tconfig: Omit<KyselyConfig, 'dialect'> = {},\n): Promise<BoundTransaction> {\n\tconst client = new pg.Client(connectionConfig(url));\n\tawait client.connect();\n\tawait client.query('BEGIN');\n\n\treturn {\n\t\tdb: new Kysely({\n\t\t\t...config,\n\t\t\tdialect: new PostgresDialect({\n\t\t\t\t// A \"pool\" of the one connection. Released never, ended never: the\n\t\t\t\t// test owns it, and it closes in `rollback`.\n\t\t\t\tpool: {\n\t\t\t\t\tconnect: async () => savepointing(client),\n\t\t\t\t\tend: async () => {},\n\t\t\t\t} as unknown as pg.Pool,\n\t\t\t}),\n\t\t}),\n\t\tasync rollback() {\n\t\t\ttry {\n\t\t\t\tawait client.query('ROLLBACK');\n\t\t\t} finally {\n\t\t\t\tawait client.end();\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * The client, with transaction control turned into savepoints.\n *\n * Kysely issues `begin`/`commit`/`rollback` as plain statements; this is the\n * one place they can be caught. A depth counter names the savepoints, so\n * nested transactions nest.\n */\nfunction savepointing(client: pg.Client): pg.PoolClient {\n\tlet depth = 0;\n\tconst query = client.query.bind(client) as (...args: unknown[]) => unknown;\n\n\tconst wrapped = Object.create(client) as pg.PoolClient;\n\tObject.assign(wrapped, {\n\t\tquery: (text: unknown, ...rest: unknown[]) => {\n\t\t\tconst statement =\n\t\t\t\ttypeof text === 'string' ? text.trim().toLowerCase() : undefined;\n\n\t\t\tif (statement === 'begin' || statement?.startsWith('start transaction')) {\n\t\t\t\tdepth++;\n\t\t\t\treturn query(`SAVEPOINT test_sp_${depth}`);\n\t\t\t}\n\t\t\tif (statement === 'commit') {\n\t\t\t\treturn query(`RELEASE SAVEPOINT test_sp_${depth--}`);\n\t\t\t}\n\t\t\tif (statement === 'rollback') {\n\t\t\t\treturn query(`ROLLBACK TO SAVEPOINT test_sp_${depth--}`);\n\t\t\t}\n\t\t\treturn query(text, ...rest);\n\t\t},\n\t\trelease: () => {},\n\t});\n\treturn wrapped;\n}\n\n/**\n * The transactions one test has opened, by database.\n *\n * `get` opens a database's transaction the first time it is asked for and\n * hands back the same one after that — including to a caller that asks while\n * it is still opening. `rollbackAll` ends every one of them.\n */\nexport class TransactionRegistry<TKey = string> {\n\tprivate readonly open = new Map<TKey, Promise<BoundTransaction>>();\n\n\tget(\n\t\tkey: TKey,\n\t\turl: string,\n\t\tconfig?: Omit<KyselyConfig, 'dialect'>,\n\t): Promise<Kysely<any>> {\n\t\tlet transaction = this.open.get(key);\n\t\tif (!transaction) {\n\t\t\ttransaction = openBoundTransaction(url, config);\n\t\t\tthis.open.set(key, transaction);\n\t\t}\n\t\treturn transaction.then(({ db }) => db);\n\t}\n\n\tasync rollbackAll(): Promise<void> {\n\t\tconst transactions = [...this.open.values()];\n\t\tthis.open.clear();\n\n\t\tconst results = await Promise.allSettled(\n\t\t\ttransactions.map(async (transaction) => (await transaction).rollback()),\n\t\t);\n\t\tconst failed = results.find(\n\t\t\t(result): result is PromiseRejectedResult => result.status === 'rejected',\n\t\t);\n\t\tif (failed) throw failed.reason;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,iBAAiB,KAA+B;CAC/D,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,aAAa,OAAO,aAAa,IAAI,aAAa;CACxD,IAAI,CAAC,YAAY,OAAO,EAAE,kBAAkB,IAAI;CAEhD,OAAO,aAAa,OAAO,aAAa;CACxC,OAAO;EACN,kBAAkB,OAAO,SAAS;EAClC,SAAS,kBAAkB;CAC5B;AACD;;;;;;;;AAiBA,eAAsB,qBACrB,KACA,SAAwC,CAAC,GACb;CAC5B,MAAM,SAAS,IAAI,GAAG,OAAO,iBAAiB,GAAG,CAAC;CAClD,MAAM,OAAO,QAAQ;CACrB,MAAM,OAAO,MAAM,OAAO;CAE1B,OAAO;EACN,IAAI,IAAI,OAAO;GACd,GAAG;GACH,SAAS,IAAI,gBAAgB,EAG5B,MAAM;IACL,SAAS,YAAY,aAAa,MAAM;IACxC,KAAK,YAAY,CAAC;GACnB,EACD,CAAC;EACF,CAAC;EACD,MAAM,WAAW;GAChB,IAAI;IACH,MAAM,OAAO,MAAM,UAAU;GAC9B,UAAU;IACT,MAAM,OAAO,IAAI;GAClB;EACD;CACD;AACD;;;;;;;;AASA,SAAS,aAAa,QAAkC;CACvD,IAAI,QAAQ;CACZ,MAAM,QAAQ,OAAO,MAAM,KAAK,MAAM;CAEtC,MAAM,UAAU,OAAO,OAAO,MAAM;CACpC,OAAO,OAAO,SAAS;EACtB,QAAQ,MAAe,GAAG,SAAoB;GAC7C,MAAM,YACL,OAAO,SAAS,WAAW,KAAK,KAAK,CAAC,CAAC,YAAY,IAAI,KAAA;GAExD,IAAI,cAAc,WAAW,WAAW,WAAW,mBAAmB,GAAG;IACxE;IACA,OAAO,MAAM,qBAAqB,OAAO;GAC1C;GACA,IAAI,cAAc,UACjB,OAAO,MAAM,6BAA6B,SAAS;GAEpD,IAAI,cAAc,YACjB,OAAO,MAAM,iCAAiC,SAAS;GAExD,OAAO,MAAM,MAAM,GAAG,IAAI;EAC3B;EACA,eAAe,CAAC;CACjB,CAAC;CACD,OAAO;AACR;;;;;;;;AASA,IAAa,sBAAb,MAAgD;CAC/C,uBAAwB,IAAI,IAAqC;CAEjE,IACC,KACA,KACA,QACuB;EACvB,IAAI,cAAc,KAAK,KAAK,IAAI,GAAG;EACnC,IAAI,CAAC,aAAa;GACjB,cAAc,qBAAqB,KAAK,MAAM;GAC9C,KAAK,KAAK,IAAI,KAAK,WAAW;EAC/B;EACA,OAAO,YAAY,MAAM,EAAE,SAAS,EAAE;CACvC;CAEA,MAAM,cAA6B;EAClC,MAAM,eAAe,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC;EAC3C,KAAK,KAAK,MAAM;EAKhB,MAAM,UAAS,MAHO,QAAQ,WAC7B,aAAa,IAAI,OAAO,iBAAiB,MAAM,YAAA,CAAa,SAAS,CAAC,CACvE,EAAA,CACuB,MACrB,WAA4C,OAAO,WAAW,UAChE;EACA,IAAI,QAAQ,MAAM,OAAO;CAC1B;AACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/testkit",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.29",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -161,17 +161,17 @@
|
|
|
161
161
|
"dependencies": {
|
|
162
162
|
"@faker-js/faker": "~10.6.0",
|
|
163
163
|
"tough-cookie": "~6.0.2",
|
|
164
|
-
"@geekmidas/envkit": "^10.0.0-alpha.
|
|
165
|
-
"@geekmidas/
|
|
166
|
-
"@geekmidas/
|
|
164
|
+
"@geekmidas/envkit": "^10.0.0-alpha.29",
|
|
165
|
+
"@geekmidas/services": "^10.0.0-alpha.29",
|
|
166
|
+
"@geekmidas/logger": "^10.0.0-alpha.29"
|
|
167
167
|
},
|
|
168
168
|
"devDependencies": {
|
|
169
169
|
"@better-auth/test-utils": "~1.7.6",
|
|
170
170
|
"@types/pg": "~8.23.1",
|
|
171
171
|
"better-auth": "~1.7.6",
|
|
172
|
-
"@geekmidas/envkit": "^10.0.0-alpha.
|
|
173
|
-
"@geekmidas/logger": "^10.0.0-alpha.
|
|
174
|
-
"@geekmidas/services": "^10.0.0-alpha.
|
|
172
|
+
"@geekmidas/envkit": "^10.0.0-alpha.29",
|
|
173
|
+
"@geekmidas/logger": "^10.0.0-alpha.29",
|
|
174
|
+
"@geekmidas/services": "^10.0.0-alpha.29"
|
|
175
175
|
},
|
|
176
176
|
"repository": {
|
|
177
177
|
"type": "git",
|