pgsql-test 2.10.0 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin.js +33 -0
- package/connect.js +5 -1
- package/esm/admin.js +33 -0
- package/esm/connect.js +5 -1
- package/esm/manager.js +5 -2
- package/esm/test-client.js +46 -1
- package/manager.d.ts +2 -2
- package/manager.js +5 -2
- package/package.json +7 -7
- package/test-client.d.ts +19 -3
- package/test-client.js +46 -1
package/admin.js
CHANGED
|
@@ -105,17 +105,50 @@ class DbAdmin {
|
|
|
105
105
|
const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
|
|
106
106
|
await this.streamSql(sql, db);
|
|
107
107
|
}
|
|
108
|
+
// TODO: make adminRole a configurable option
|
|
109
|
+
// ONLY granting admin role for testing purposes, normally the db connection for apps won't have admin role
|
|
110
|
+
// DO NOT USE THIS FOR PRODUCTION
|
|
108
111
|
async createUserRole(user, password, dbName) {
|
|
109
112
|
const anonRole = (0, roles_1.getRoleName)('anonymous', this.roleConfig);
|
|
110
113
|
const authRole = (0, roles_1.getRoleName)('authenticated', this.roleConfig);
|
|
114
|
+
const adminRole = (0, roles_1.getRoleName)('administrator', this.roleConfig);
|
|
111
115
|
const sql = `
|
|
112
116
|
DO $$
|
|
113
117
|
BEGIN
|
|
118
|
+
-- Create role if it doesn't exist
|
|
114
119
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${user}') THEN
|
|
115
120
|
CREATE ROLE ${user} LOGIN PASSWORD '${password}';
|
|
121
|
+
END IF;
|
|
122
|
+
|
|
123
|
+
-- Grant anonymous role if not already granted
|
|
124
|
+
IF NOT EXISTS (
|
|
125
|
+
SELECT 1 FROM pg_auth_members am
|
|
126
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
127
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
128
|
+
WHERE r1.rolname = '${anonRole}' AND r2.rolname = '${user}'
|
|
129
|
+
) THEN
|
|
116
130
|
GRANT ${anonRole} TO ${user};
|
|
131
|
+
END IF;
|
|
132
|
+
|
|
133
|
+
-- Grant authenticated role if not already granted
|
|
134
|
+
IF NOT EXISTS (
|
|
135
|
+
SELECT 1 FROM pg_auth_members am
|
|
136
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
137
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
138
|
+
WHERE r1.rolname = '${authRole}' AND r2.rolname = '${user}'
|
|
139
|
+
) THEN
|
|
117
140
|
GRANT ${authRole} TO ${user};
|
|
118
141
|
END IF;
|
|
142
|
+
|
|
143
|
+
-- Grant administrator role if not already granted
|
|
144
|
+
IF NOT EXISTS (
|
|
145
|
+
SELECT 1 FROM pg_auth_members am
|
|
146
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
147
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
148
|
+
WHERE r1.rolname = '${adminRole}' AND r2.rolname = '${user}'
|
|
149
|
+
) THEN
|
|
150
|
+
GRANT ${adminRole} TO ${user};
|
|
151
|
+
END IF;
|
|
119
152
|
END $$;
|
|
120
153
|
`.trim();
|
|
121
154
|
await this.streamSql(sql, dbName);
|
package/connect.js
CHANGED
|
@@ -68,10 +68,14 @@ const getConnections = async (cn = {}, seedAdapters = [seed_1.seed.launchql()])
|
|
|
68
68
|
throw error;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
const
|
|
71
|
+
const dbConfig = {
|
|
72
72
|
...config,
|
|
73
73
|
user: connOpts.connection.user,
|
|
74
74
|
password: connOpts.connection.password
|
|
75
|
+
};
|
|
76
|
+
const db = manager.getClient(dbConfig, {
|
|
77
|
+
auth: connOpts.auth,
|
|
78
|
+
roles: connOpts.roles
|
|
75
79
|
});
|
|
76
80
|
db.setContext({ role: (0, roles_1.getDefaultRole)(connOpts) });
|
|
77
81
|
return { pg, db, teardown, manager, admin };
|
package/esm/admin.js
CHANGED
|
@@ -102,17 +102,50 @@ export class DbAdmin {
|
|
|
102
102
|
const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
|
|
103
103
|
await this.streamSql(sql, db);
|
|
104
104
|
}
|
|
105
|
+
// TODO: make adminRole a configurable option
|
|
106
|
+
// ONLY granting admin role for testing purposes, normally the db connection for apps won't have admin role
|
|
107
|
+
// DO NOT USE THIS FOR PRODUCTION
|
|
105
108
|
async createUserRole(user, password, dbName) {
|
|
106
109
|
const anonRole = getRoleName('anonymous', this.roleConfig);
|
|
107
110
|
const authRole = getRoleName('authenticated', this.roleConfig);
|
|
111
|
+
const adminRole = getRoleName('administrator', this.roleConfig);
|
|
108
112
|
const sql = `
|
|
109
113
|
DO $$
|
|
110
114
|
BEGIN
|
|
115
|
+
-- Create role if it doesn't exist
|
|
111
116
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${user}') THEN
|
|
112
117
|
CREATE ROLE ${user} LOGIN PASSWORD '${password}';
|
|
118
|
+
END IF;
|
|
119
|
+
|
|
120
|
+
-- Grant anonymous role if not already granted
|
|
121
|
+
IF NOT EXISTS (
|
|
122
|
+
SELECT 1 FROM pg_auth_members am
|
|
123
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
124
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
125
|
+
WHERE r1.rolname = '${anonRole}' AND r2.rolname = '${user}'
|
|
126
|
+
) THEN
|
|
113
127
|
GRANT ${anonRole} TO ${user};
|
|
128
|
+
END IF;
|
|
129
|
+
|
|
130
|
+
-- Grant authenticated role if not already granted
|
|
131
|
+
IF NOT EXISTS (
|
|
132
|
+
SELECT 1 FROM pg_auth_members am
|
|
133
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
134
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
135
|
+
WHERE r1.rolname = '${authRole}' AND r2.rolname = '${user}'
|
|
136
|
+
) THEN
|
|
114
137
|
GRANT ${authRole} TO ${user};
|
|
115
138
|
END IF;
|
|
139
|
+
|
|
140
|
+
-- Grant administrator role if not already granted
|
|
141
|
+
IF NOT EXISTS (
|
|
142
|
+
SELECT 1 FROM pg_auth_members am
|
|
143
|
+
JOIN pg_roles r1 ON am.roleid = r1.oid
|
|
144
|
+
JOIN pg_roles r2 ON am.member = r2.oid
|
|
145
|
+
WHERE r1.rolname = '${adminRole}' AND r2.rolname = '${user}'
|
|
146
|
+
) THEN
|
|
147
|
+
GRANT ${adminRole} TO ${user};
|
|
148
|
+
END IF;
|
|
116
149
|
END $$;
|
|
117
150
|
`.trim();
|
|
118
151
|
await this.streamSql(sql, dbName);
|
package/esm/connect.js
CHANGED
|
@@ -64,10 +64,14 @@ export const getConnections = async (cn = {}, seedAdapters = [seed.launchql()])
|
|
|
64
64
|
throw error;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
const
|
|
67
|
+
const dbConfig = {
|
|
68
68
|
...config,
|
|
69
69
|
user: connOpts.connection.user,
|
|
70
70
|
password: connOpts.connection.password
|
|
71
|
+
};
|
|
72
|
+
const db = manager.getClient(dbConfig, {
|
|
73
|
+
auth: connOpts.auth,
|
|
74
|
+
roles: connOpts.roles
|
|
71
75
|
});
|
|
72
76
|
db.setContext({ role: getDefaultRole(connOpts) });
|
|
73
77
|
return { pg, db, teardown, manager, admin };
|
package/esm/manager.js
CHANGED
|
@@ -68,11 +68,14 @@ export class PgTestConnector {
|
|
|
68
68
|
}
|
|
69
69
|
return this.pgPools.get(key);
|
|
70
70
|
}
|
|
71
|
-
getClient(config) {
|
|
71
|
+
getClient(config, opts = {}) {
|
|
72
72
|
if (this.shuttingDown) {
|
|
73
73
|
throw new Error('PgTestConnector is shutting down; no new clients allowed');
|
|
74
74
|
}
|
|
75
|
-
const client = new PgTestClient(config, {
|
|
75
|
+
const client = new PgTestClient(config, {
|
|
76
|
+
trackConnect: (p) => this.registerConnect(p),
|
|
77
|
+
...opts
|
|
78
|
+
});
|
|
76
79
|
this.clients.add(client);
|
|
77
80
|
const key = this.dbKey(config);
|
|
78
81
|
this.seenDbConfigs.set(key, config);
|
package/esm/test-client.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { Client } from 'pg';
|
|
2
|
+
import { getRoleName } from './roles';
|
|
2
3
|
export class PgTestClient {
|
|
3
4
|
config;
|
|
4
5
|
client;
|
|
6
|
+
opts;
|
|
5
7
|
ctxStmts = '';
|
|
8
|
+
contextSettings = {};
|
|
6
9
|
_ended = false;
|
|
7
10
|
connectPromise = null;
|
|
8
11
|
constructor(config, opts = {}) {
|
|
12
|
+
this.opts = opts;
|
|
9
13
|
this.config = config;
|
|
10
14
|
this.client = new Client({
|
|
11
15
|
host: this.config.host,
|
|
@@ -56,12 +60,53 @@ export class PgTestClient {
|
|
|
56
60
|
await this.commit();
|
|
57
61
|
}
|
|
58
62
|
setContext(ctx) {
|
|
59
|
-
this.
|
|
63
|
+
Object.assign(this.contextSettings, ctx);
|
|
64
|
+
this.ctxStmts = Object.entries(this.contextSettings)
|
|
60
65
|
.map(([key, val]) => val === null
|
|
61
66
|
? `SELECT set_config('${key}', NULL, true);`
|
|
62
67
|
: `SELECT set_config('${key}', '${val}', true);`)
|
|
63
68
|
.join('\n');
|
|
64
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Set authentication context for the current session.
|
|
72
|
+
* Configures role and user ID using cascading defaults from options → opts.auth → RoleMapping.
|
|
73
|
+
*/
|
|
74
|
+
auth(options = {}) {
|
|
75
|
+
const role = options.role ?? this.opts.auth?.role ?? getRoleName('authenticated', this.opts);
|
|
76
|
+
const userIdKey = options.userIdKey ?? this.opts.auth?.userIdKey ?? 'jwt.claims.user_id';
|
|
77
|
+
const userId = options.userId ?? this.opts.auth?.userId ?? null;
|
|
78
|
+
this.setContext({
|
|
79
|
+
role,
|
|
80
|
+
[userIdKey]: userId !== null ? String(userId) : null
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
85
|
+
* Maintains test isolation by creating a savepoint and reapplying session context.
|
|
86
|
+
*/
|
|
87
|
+
async publish() {
|
|
88
|
+
await this.commit(); // make data visible to other sessions
|
|
89
|
+
await this.begin(); // fresh tx
|
|
90
|
+
await this.savepoint(); // keep rollback harness
|
|
91
|
+
await this.ctxQuery(); // reapply all setContext()
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Clear all session context variables and reset to default anonymous role.
|
|
95
|
+
*/
|
|
96
|
+
clearContext() {
|
|
97
|
+
const defaultRole = getRoleName('anonymous', this.opts);
|
|
98
|
+
const nulledSettings = {};
|
|
99
|
+
Object.keys(this.contextSettings).forEach(key => {
|
|
100
|
+
nulledSettings[key] = null;
|
|
101
|
+
});
|
|
102
|
+
nulledSettings.role = defaultRole;
|
|
103
|
+
this.ctxStmts = Object.entries(nulledSettings)
|
|
104
|
+
.map(([key, val]) => val === null
|
|
105
|
+
? `SELECT set_config('${key}', NULL, true);`
|
|
106
|
+
: `SELECT set_config('${key}', '${val}', true);`)
|
|
107
|
+
.join('\n');
|
|
108
|
+
this.contextSettings = { role: defaultRole };
|
|
109
|
+
}
|
|
65
110
|
async any(query, values) {
|
|
66
111
|
const result = await this.query(query, values);
|
|
67
112
|
return result.rows;
|
package/manager.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
2
|
import { PgConfig } from 'pg-env';
|
|
3
|
-
import { PgTestClient } from './test-client';
|
|
3
|
+
import { PgTestClient, PgTestClientOpts } from './test-client';
|
|
4
4
|
export declare class PgTestConnector {
|
|
5
5
|
private static instance;
|
|
6
6
|
private readonly clients;
|
|
@@ -17,7 +17,7 @@ export declare class PgTestConnector {
|
|
|
17
17
|
private registerConnect;
|
|
18
18
|
private awaitPendingConnects;
|
|
19
19
|
getPool(config: PgConfig): Pool;
|
|
20
|
-
getClient(config: PgConfig): PgTestClient;
|
|
20
|
+
getClient(config: PgConfig, opts?: Partial<PgTestClientOpts>): PgTestClient;
|
|
21
21
|
closeAll(): Promise<void>;
|
|
22
22
|
close(): void;
|
|
23
23
|
drop(config: PgConfig): void;
|
package/manager.js
CHANGED
|
@@ -71,11 +71,14 @@ class PgTestConnector {
|
|
|
71
71
|
}
|
|
72
72
|
return this.pgPools.get(key);
|
|
73
73
|
}
|
|
74
|
-
getClient(config) {
|
|
74
|
+
getClient(config, opts = {}) {
|
|
75
75
|
if (this.shuttingDown) {
|
|
76
76
|
throw new Error('PgTestConnector is shutting down; no new clients allowed');
|
|
77
77
|
}
|
|
78
|
-
const client = new test_client_1.PgTestClient(config, {
|
|
78
|
+
const client = new test_client_1.PgTestClient(config, {
|
|
79
|
+
trackConnect: (p) => this.registerConnect(p),
|
|
80
|
+
...opts
|
|
81
|
+
});
|
|
79
82
|
this.clients.add(client);
|
|
80
83
|
const key = this.dbKey(config);
|
|
81
84
|
this.seenDbConfigs.set(key, config);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgsql-test",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.1",
|
|
4
4
|
"author": "Dan Lynch <pyramation@gmail.com>",
|
|
5
5
|
"description": "pgsql-test offers isolated, role-aware, and rollback-friendly PostgreSQL environments for integration tests — giving developers realistic test coverage without external state pollution",
|
|
6
6
|
"main": "index.js",
|
|
@@ -60,14 +60,14 @@
|
|
|
60
60
|
"@types/pg-copy-streams": "^1.2.5"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@launchql/core": "^2.11.
|
|
64
|
-
"@launchql/env": "^2.4.
|
|
65
|
-
"@launchql/server-utils": "^2.4.
|
|
66
|
-
"@launchql/types": "^2.
|
|
63
|
+
"@launchql/core": "^2.11.1",
|
|
64
|
+
"@launchql/env": "^2.4.1",
|
|
65
|
+
"@launchql/server-utils": "^2.4.1",
|
|
66
|
+
"@launchql/types": "^2.6.0",
|
|
67
67
|
"pg": "^8.16.0",
|
|
68
|
-
"pg-cache": "^1.3.
|
|
68
|
+
"pg-cache": "^1.3.1",
|
|
69
69
|
"pg-copy-streams": "^6.0.6",
|
|
70
70
|
"pg-env": "^1.1.0"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "720642536863e65eda54f91d69eccebca4ac07d1"
|
|
73
73
|
}
|
package/test-client.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { Client, QueryResult } from 'pg';
|
|
2
2
|
import { PgConfig } from 'pg-env';
|
|
3
|
-
|
|
3
|
+
import { AuthOptions, PgTestConnectionOptions } from '@launchql/types';
|
|
4
|
+
export type PgTestClientOpts = {
|
|
4
5
|
deferConnect?: boolean;
|
|
5
6
|
trackConnect?: (p: Promise<any>) => void;
|
|
6
|
-
}
|
|
7
|
+
} & Partial<PgTestConnectionOptions>;
|
|
7
8
|
export declare class PgTestClient {
|
|
8
9
|
config: PgConfig;
|
|
9
10
|
client: Client;
|
|
11
|
+
private opts;
|
|
10
12
|
private ctxStmts;
|
|
13
|
+
private contextSettings;
|
|
11
14
|
private _ended;
|
|
12
15
|
private connectPromise;
|
|
13
16
|
constructor(config: PgConfig, opts?: PgTestClientOpts);
|
|
@@ -20,6 +23,20 @@ export declare class PgTestClient {
|
|
|
20
23
|
beforeEach(): Promise<void>;
|
|
21
24
|
afterEach(): Promise<void>;
|
|
22
25
|
setContext(ctx: Record<string, string | null>): void;
|
|
26
|
+
/**
|
|
27
|
+
* Set authentication context for the current session.
|
|
28
|
+
* Configures role and user ID using cascading defaults from options → opts.auth → RoleMapping.
|
|
29
|
+
*/
|
|
30
|
+
auth(options?: AuthOptions): void;
|
|
31
|
+
/**
|
|
32
|
+
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
33
|
+
* Maintains test isolation by creating a savepoint and reapplying session context.
|
|
34
|
+
*/
|
|
35
|
+
publish(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Clear all session context variables and reset to default anonymous role.
|
|
38
|
+
*/
|
|
39
|
+
clearContext(): void;
|
|
23
40
|
any<T = any>(query: string, values?: any[]): Promise<T[]>;
|
|
24
41
|
one<T = any>(query: string, values?: any[]): Promise<T>;
|
|
25
42
|
oneOrNone<T = any>(query: string, values?: any[]): Promise<T | null>;
|
|
@@ -30,4 +47,3 @@ export declare class PgTestClient {
|
|
|
30
47
|
query<T = any>(query: string, values?: any[]): Promise<QueryResult<T>>;
|
|
31
48
|
ctxQuery(): Promise<void>;
|
|
32
49
|
}
|
|
33
|
-
export {};
|
package/test-client.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PgTestClient = void 0;
|
|
4
4
|
const pg_1 = require("pg");
|
|
5
|
+
const roles_1 = require("./roles");
|
|
5
6
|
class PgTestClient {
|
|
6
7
|
config;
|
|
7
8
|
client;
|
|
9
|
+
opts;
|
|
8
10
|
ctxStmts = '';
|
|
11
|
+
contextSettings = {};
|
|
9
12
|
_ended = false;
|
|
10
13
|
connectPromise = null;
|
|
11
14
|
constructor(config, opts = {}) {
|
|
15
|
+
this.opts = opts;
|
|
12
16
|
this.config = config;
|
|
13
17
|
this.client = new pg_1.Client({
|
|
14
18
|
host: this.config.host,
|
|
@@ -59,12 +63,53 @@ class PgTestClient {
|
|
|
59
63
|
await this.commit();
|
|
60
64
|
}
|
|
61
65
|
setContext(ctx) {
|
|
62
|
-
this.
|
|
66
|
+
Object.assign(this.contextSettings, ctx);
|
|
67
|
+
this.ctxStmts = Object.entries(this.contextSettings)
|
|
63
68
|
.map(([key, val]) => val === null
|
|
64
69
|
? `SELECT set_config('${key}', NULL, true);`
|
|
65
70
|
: `SELECT set_config('${key}', '${val}', true);`)
|
|
66
71
|
.join('\n');
|
|
67
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Set authentication context for the current session.
|
|
75
|
+
* Configures role and user ID using cascading defaults from options → opts.auth → RoleMapping.
|
|
76
|
+
*/
|
|
77
|
+
auth(options = {}) {
|
|
78
|
+
const role = options.role ?? this.opts.auth?.role ?? (0, roles_1.getRoleName)('authenticated', this.opts);
|
|
79
|
+
const userIdKey = options.userIdKey ?? this.opts.auth?.userIdKey ?? 'jwt.claims.user_id';
|
|
80
|
+
const userId = options.userId ?? this.opts.auth?.userId ?? null;
|
|
81
|
+
this.setContext({
|
|
82
|
+
role,
|
|
83
|
+
[userIdKey]: userId !== null ? String(userId) : null
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
88
|
+
* Maintains test isolation by creating a savepoint and reapplying session context.
|
|
89
|
+
*/
|
|
90
|
+
async publish() {
|
|
91
|
+
await this.commit(); // make data visible to other sessions
|
|
92
|
+
await this.begin(); // fresh tx
|
|
93
|
+
await this.savepoint(); // keep rollback harness
|
|
94
|
+
await this.ctxQuery(); // reapply all setContext()
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Clear all session context variables and reset to default anonymous role.
|
|
98
|
+
*/
|
|
99
|
+
clearContext() {
|
|
100
|
+
const defaultRole = (0, roles_1.getRoleName)('anonymous', this.opts);
|
|
101
|
+
const nulledSettings = {};
|
|
102
|
+
Object.keys(this.contextSettings).forEach(key => {
|
|
103
|
+
nulledSettings[key] = null;
|
|
104
|
+
});
|
|
105
|
+
nulledSettings.role = defaultRole;
|
|
106
|
+
this.ctxStmts = Object.entries(nulledSettings)
|
|
107
|
+
.map(([key, val]) => val === null
|
|
108
|
+
? `SELECT set_config('${key}', NULL, true);`
|
|
109
|
+
: `SELECT set_config('${key}', '${val}', true);`)
|
|
110
|
+
.join('\n');
|
|
111
|
+
this.contextSettings = { role: defaultRole };
|
|
112
|
+
}
|
|
68
113
|
async any(query, values) {
|
|
69
114
|
const result = await this.query(query, values);
|
|
70
115
|
return result.rows;
|