pgsql-test 2.19.0 โ 2.20.0
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/README.md +5 -22
- package/admin.d.ts +2 -20
- package/admin.js +4 -124
- package/connect.d.ts +5 -1
- package/connect.js +6 -4
- package/esm/admin.js +4 -124
- package/esm/connect.js +5 -3
- package/esm/index.js +0 -1
- package/esm/manager.js +23 -14
- package/esm/test-client.js +3 -123
- package/index.d.ts +0 -1
- package/index.js +0 -1
- package/manager.d.ts +3 -1
- package/manager.js +23 -14
- package/package.json +4 -4
- package/test-client.d.ts +3 -39
- package/test-client.js +3 -123
- package/context-utils.d.ts +0 -8
- package/context-utils.js +0 -28
- package/esm/context-utils.js +0 -25
- package/esm/roles.js +0 -32
- package/esm/stream.js +0 -96
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/stream.d.ts +0 -33
- package/stream.js +0 -99
package/manager.js
CHANGED
|
@@ -87,7 +87,8 @@ class PgTestConnector {
|
|
|
87
87
|
log.info(`๐ New PgTestClient connected to ${config.database}`);
|
|
88
88
|
return client;
|
|
89
89
|
}
|
|
90
|
-
async closeAll() {
|
|
90
|
+
async closeAll(opts = {}) {
|
|
91
|
+
const { keepDb = false } = opts;
|
|
91
92
|
this.beginTeardown();
|
|
92
93
|
await this.awaitPendingConnects();
|
|
93
94
|
log.info('๐งน Closing all PgTestClients...');
|
|
@@ -107,20 +108,28 @@ class PgTestConnector {
|
|
|
107
108
|
end(pool);
|
|
108
109
|
}
|
|
109
110
|
this.pgPools.clear();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
111
|
+
if (keepDb) {
|
|
112
|
+
log.info('๐ฆ Keeping databases (keepDb=true)...');
|
|
113
|
+
const dbNames = Array.from(this.seenDbConfigs.values()).map(c => c.database);
|
|
114
|
+
log.info(`๐ฆ Preserved databases: ${dbNames.join(', ')}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
log.info('๐๏ธ Dropping seen databases...');
|
|
118
|
+
await Promise.all(Array.from(this.seenDbConfigs.values()).map(async (config) => {
|
|
119
|
+
try {
|
|
120
|
+
const rootPg = (0, pg_env_1.getPgEnvOptions)(this.config);
|
|
121
|
+
const admin = new admin_1.DbAdmin({ ...config, user: rootPg.user, password: rootPg.password }, this.verbose);
|
|
122
|
+
admin.drop();
|
|
123
|
+
log.warn(`๐งจ Dropped database: ${config.database}`);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
log.error(`โ Failed to drop database ${config.database}:`, err);
|
|
127
|
+
}
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
122
130
|
this.seenDbConfigs.clear();
|
|
123
|
-
|
|
131
|
+
const action = keepDb ? 'preserved' : 'dropped';
|
|
132
|
+
log.success(`โ
All PgTestClients closed, pools disposed, databases ${action}.`);
|
|
124
133
|
this.pendingConnects.clear();
|
|
125
134
|
this.shuttingDown = false;
|
|
126
135
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgsql-test",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
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,7 +60,6 @@
|
|
|
60
60
|
"makage": "^0.1.9"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@pgpmjs/core": "^4.3.0",
|
|
64
63
|
"@pgpmjs/env": "^2.8.11",
|
|
65
64
|
"@pgpmjs/logger": "^1.3.5",
|
|
66
65
|
"@pgpmjs/server-utils": "^2.8.11",
|
|
@@ -68,7 +67,8 @@
|
|
|
68
67
|
"pg": "^8.16.3",
|
|
69
68
|
"pg-cache": "^1.6.11",
|
|
70
69
|
"pg-env": "^1.2.4",
|
|
71
|
-
"pgsql-
|
|
70
|
+
"pgsql-client": "^1.1.0",
|
|
71
|
+
"pgsql-seed": "^0.2.2"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "09721f934b339bd8c55d2e633abaded6d20b0f65"
|
|
74
74
|
}
|
package/test-client.d.ts
CHANGED
|
@@ -1,53 +1,17 @@
|
|
|
1
|
-
import { Client, QueryResult } from 'pg';
|
|
2
1
|
import { PgConfig } from 'pg-env';
|
|
3
|
-
import {
|
|
2
|
+
import { PgClient, PgClientOpts } from 'pgsql-client';
|
|
4
3
|
import { type JsonSeedMap } from 'pgsql-seed';
|
|
5
4
|
import { type CsvSeedMap } from 'pgsql-seed';
|
|
6
|
-
export type PgTestClientOpts =
|
|
7
|
-
|
|
8
|
-
trackConnect?: (p: Promise<any>) => void;
|
|
9
|
-
} & Partial<PgTestConnectionOptions>;
|
|
10
|
-
export declare class PgTestClient {
|
|
11
|
-
config: PgConfig;
|
|
12
|
-
client: Client;
|
|
13
|
-
private opts;
|
|
14
|
-
private ctxStmts;
|
|
15
|
-
private contextSettings;
|
|
16
|
-
private _ended;
|
|
17
|
-
private connectPromise;
|
|
5
|
+
export type PgTestClientOpts = PgClientOpts;
|
|
6
|
+
export declare class PgTestClient extends PgClient {
|
|
18
7
|
constructor(config: PgConfig, opts?: PgTestClientOpts);
|
|
19
|
-
private ensureConnected;
|
|
20
|
-
close(): Promise<void>;
|
|
21
|
-
begin(): Promise<void>;
|
|
22
|
-
savepoint(name?: string): Promise<void>;
|
|
23
|
-
rollback(name?: string): Promise<void>;
|
|
24
|
-
commit(): Promise<void>;
|
|
25
8
|
beforeEach(): Promise<void>;
|
|
26
9
|
afterEach(): Promise<void>;
|
|
27
|
-
setContext(ctx: Record<string, string | null>): void;
|
|
28
|
-
/**
|
|
29
|
-
* Set authentication context for the current session.
|
|
30
|
-
* Configures role and user ID using cascading defaults from options โ opts.auth โ RoleMapping.
|
|
31
|
-
*/
|
|
32
|
-
auth(options?: AuthOptions): void;
|
|
33
10
|
/**
|
|
34
11
|
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
35
12
|
* Maintains test isolation by creating a savepoint and reapplying session context.
|
|
36
13
|
*/
|
|
37
14
|
publish(): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Clear all session context variables and reset to default anonymous role.
|
|
40
|
-
*/
|
|
41
|
-
clearContext(): void;
|
|
42
|
-
any<T = any>(query: string, values?: any[]): Promise<T[]>;
|
|
43
|
-
one<T = any>(query: string, values?: any[]): Promise<T>;
|
|
44
|
-
oneOrNone<T = any>(query: string, values?: any[]): Promise<T | null>;
|
|
45
|
-
many<T = any>(query: string, values?: any[]): Promise<T[]>;
|
|
46
|
-
manyOrNone<T = any>(query: string, values?: any[]): Promise<T[]>;
|
|
47
|
-
none(query: string, values?: any[]): Promise<void>;
|
|
48
|
-
result(query: string, values?: any[]): Promise<import('pg').QueryResult>;
|
|
49
|
-
ctxQuery(): Promise<void>;
|
|
50
|
-
query<T = any>(query: string, values?: any[]): Promise<QueryResult<T>>;
|
|
51
15
|
loadJson(data: JsonSeedMap): Promise<void>;
|
|
52
16
|
loadSql(files: string[]): Promise<void>;
|
|
53
17
|
loadCsv(tables: CsvSeedMap): Promise<void>;
|
package/test-client.js
CHANGED
|
@@ -1,63 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PgTestClient = void 0;
|
|
4
|
-
const
|
|
5
|
-
const roles_1 = require("./roles");
|
|
6
|
-
const context_utils_1 = require("./context-utils");
|
|
4
|
+
const pgsql_client_1 = require("pgsql-client");
|
|
7
5
|
const pgsql_seed_1 = require("pgsql-seed");
|
|
8
6
|
const pgsql_seed_2 = require("pgsql-seed");
|
|
9
7
|
const pgsql_seed_3 = require("pgsql-seed");
|
|
10
8
|
const pgsql_seed_4 = require("pgsql-seed");
|
|
11
|
-
class PgTestClient {
|
|
12
|
-
config;
|
|
13
|
-
client;
|
|
14
|
-
opts;
|
|
15
|
-
ctxStmts = '';
|
|
16
|
-
contextSettings = {};
|
|
17
|
-
_ended = false;
|
|
18
|
-
connectPromise = null;
|
|
9
|
+
class PgTestClient extends pgsql_client_1.PgClient {
|
|
19
10
|
constructor(config, opts = {}) {
|
|
20
|
-
|
|
21
|
-
this.config = config;
|
|
22
|
-
this.client = new pg_1.Client({
|
|
23
|
-
host: this.config.host,
|
|
24
|
-
port: this.config.port,
|
|
25
|
-
database: this.config.database,
|
|
26
|
-
user: this.config.user,
|
|
27
|
-
password: this.config.password
|
|
28
|
-
});
|
|
29
|
-
if (!opts.deferConnect) {
|
|
30
|
-
this.connectPromise = this.client.connect();
|
|
31
|
-
if (opts.trackConnect)
|
|
32
|
-
opts.trackConnect(this.connectPromise);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
async ensureConnected() {
|
|
36
|
-
if (this.connectPromise) {
|
|
37
|
-
try {
|
|
38
|
-
await this.connectPromise;
|
|
39
|
-
}
|
|
40
|
-
catch { }
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async close() {
|
|
44
|
-
if (!this._ended) {
|
|
45
|
-
this._ended = true;
|
|
46
|
-
await this.ensureConnected();
|
|
47
|
-
this.client.end();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
async begin() {
|
|
51
|
-
await this.client.query('BEGIN;');
|
|
52
|
-
}
|
|
53
|
-
async savepoint(name = 'lqlsavepoint') {
|
|
54
|
-
await this.client.query(`SAVEPOINT "${name}";`);
|
|
55
|
-
}
|
|
56
|
-
async rollback(name = 'lqlsavepoint') {
|
|
57
|
-
await this.client.query(`ROLLBACK TO SAVEPOINT "${name}";`);
|
|
58
|
-
}
|
|
59
|
-
async commit() {
|
|
60
|
-
await this.client.query('COMMIT;');
|
|
11
|
+
super(config, opts);
|
|
61
12
|
}
|
|
62
13
|
async beforeEach() {
|
|
63
14
|
await this.begin();
|
|
@@ -67,23 +18,6 @@ class PgTestClient {
|
|
|
67
18
|
await this.rollback();
|
|
68
19
|
await this.commit();
|
|
69
20
|
}
|
|
70
|
-
setContext(ctx) {
|
|
71
|
-
Object.assign(this.contextSettings, ctx);
|
|
72
|
-
this.ctxStmts = (0, context_utils_1.generateContextStatements)(this.contextSettings);
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Set authentication context for the current session.
|
|
76
|
-
* Configures role and user ID using cascading defaults from options โ opts.auth โ RoleMapping.
|
|
77
|
-
*/
|
|
78
|
-
auth(options = {}) {
|
|
79
|
-
const role = options.role ?? this.opts.auth?.role ?? (0, roles_1.getRoleName)('authenticated', this.opts);
|
|
80
|
-
const userIdKey = options.userIdKey ?? this.opts.auth?.userIdKey ?? 'jwt.claims.user_id';
|
|
81
|
-
const userId = options.userId ?? this.opts.auth?.userId ?? null;
|
|
82
|
-
this.setContext({
|
|
83
|
-
role,
|
|
84
|
-
[userIdKey]: userId !== null ? String(userId) : null
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
21
|
/**
|
|
88
22
|
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
89
23
|
* Maintains test isolation by creating a savepoint and reapplying session context.
|
|
@@ -94,60 +28,6 @@ class PgTestClient {
|
|
|
94
28
|
await this.savepoint(); // keep rollback harness
|
|
95
29
|
await this.ctxQuery(); // reapply all setContext()
|
|
96
30
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Clear all session context variables and reset to default anonymous role.
|
|
99
|
-
*/
|
|
100
|
-
clearContext() {
|
|
101
|
-
const defaultRole = (0, roles_1.getRoleName)('anonymous', this.opts);
|
|
102
|
-
const nulledSettings = {};
|
|
103
|
-
Object.keys(this.contextSettings).forEach(key => {
|
|
104
|
-
nulledSettings[key] = null;
|
|
105
|
-
});
|
|
106
|
-
nulledSettings.role = defaultRole;
|
|
107
|
-
this.ctxStmts = (0, context_utils_1.generateContextStatements)(nulledSettings);
|
|
108
|
-
this.contextSettings = { role: defaultRole };
|
|
109
|
-
}
|
|
110
|
-
async any(query, values) {
|
|
111
|
-
const result = await this.query(query, values);
|
|
112
|
-
return result.rows;
|
|
113
|
-
}
|
|
114
|
-
async one(query, values) {
|
|
115
|
-
const rows = await this.any(query, values);
|
|
116
|
-
if (rows.length !== 1) {
|
|
117
|
-
throw new Error('Expected exactly one result');
|
|
118
|
-
}
|
|
119
|
-
return rows[0];
|
|
120
|
-
}
|
|
121
|
-
async oneOrNone(query, values) {
|
|
122
|
-
const rows = await this.any(query, values);
|
|
123
|
-
return rows[0] || null;
|
|
124
|
-
}
|
|
125
|
-
async many(query, values) {
|
|
126
|
-
const rows = await this.any(query, values);
|
|
127
|
-
if (rows.length === 0)
|
|
128
|
-
throw new Error('Expected many rows, got none');
|
|
129
|
-
return rows;
|
|
130
|
-
}
|
|
131
|
-
async manyOrNone(query, values) {
|
|
132
|
-
return this.any(query, values);
|
|
133
|
-
}
|
|
134
|
-
async none(query, values) {
|
|
135
|
-
await this.query(query, values);
|
|
136
|
-
}
|
|
137
|
-
async result(query, values) {
|
|
138
|
-
return this.query(query, values);
|
|
139
|
-
}
|
|
140
|
-
async ctxQuery() {
|
|
141
|
-
if (this.ctxStmts) {
|
|
142
|
-
await this.client.query(this.ctxStmts);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// NOTE: all queries should call ctxQuery() before executing the query
|
|
146
|
-
async query(query, values) {
|
|
147
|
-
await this.ctxQuery();
|
|
148
|
-
const result = await this.client.query(query, values);
|
|
149
|
-
return result;
|
|
150
|
-
}
|
|
151
31
|
async loadJson(data) {
|
|
152
32
|
await this.ctxQuery();
|
|
153
33
|
await (0, pgsql_seed_1.insertJsonMap)(this.client, data);
|
package/context-utils.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { PgTestClientContext } from '@pgpmjs/types';
|
|
2
|
-
/**
|
|
3
|
-
* Generate SQL statements to set PostgreSQL session context variables
|
|
4
|
-
* Uses SET LOCAL ROLE for the 'role' key and set_config() for other variables
|
|
5
|
-
* @param context - Context settings to apply
|
|
6
|
-
* @returns SQL string with SET LOCAL ROLE and set_config() statements
|
|
7
|
-
*/
|
|
8
|
-
export declare function generateContextStatements(context: PgTestClientContext): string;
|
package/context-utils.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.generateContextStatements = generateContextStatements;
|
|
4
|
-
/**
|
|
5
|
-
* Generate SQL statements to set PostgreSQL session context variables
|
|
6
|
-
* Uses SET LOCAL ROLE for the 'role' key and set_config() for other variables
|
|
7
|
-
* @param context - Context settings to apply
|
|
8
|
-
* @returns SQL string with SET LOCAL ROLE and set_config() statements
|
|
9
|
-
*/
|
|
10
|
-
function generateContextStatements(context) {
|
|
11
|
-
return Object.entries(context)
|
|
12
|
-
.map(([key, val]) => {
|
|
13
|
-
if (key === 'role') {
|
|
14
|
-
if (val === null || val === undefined) {
|
|
15
|
-
return 'SET LOCAL ROLE NONE;';
|
|
16
|
-
}
|
|
17
|
-
const escapedRole = val.replace(/"/g, '""');
|
|
18
|
-
return `SET LOCAL ROLE "${escapedRole}";`;
|
|
19
|
-
}
|
|
20
|
-
// Use set_config for other context variables
|
|
21
|
-
if (val === null || val === undefined) {
|
|
22
|
-
return `SELECT set_config('${key}', NULL, true);`;
|
|
23
|
-
}
|
|
24
|
-
const escapedVal = val.replace(/'/g, "''");
|
|
25
|
-
return `SELECT set_config('${key}', '${escapedVal}', true);`;
|
|
26
|
-
})
|
|
27
|
-
.join('\n');
|
|
28
|
-
}
|
package/esm/context-utils.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate SQL statements to set PostgreSQL session context variables
|
|
3
|
-
* Uses SET LOCAL ROLE for the 'role' key and set_config() for other variables
|
|
4
|
-
* @param context - Context settings to apply
|
|
5
|
-
* @returns SQL string with SET LOCAL ROLE and set_config() statements
|
|
6
|
-
*/
|
|
7
|
-
export function generateContextStatements(context) {
|
|
8
|
-
return Object.entries(context)
|
|
9
|
-
.map(([key, val]) => {
|
|
10
|
-
if (key === 'role') {
|
|
11
|
-
if (val === null || val === undefined) {
|
|
12
|
-
return 'SET LOCAL ROLE NONE;';
|
|
13
|
-
}
|
|
14
|
-
const escapedRole = val.replace(/"/g, '""');
|
|
15
|
-
return `SET LOCAL ROLE "${escapedRole}";`;
|
|
16
|
-
}
|
|
17
|
-
// Use set_config for other context variables
|
|
18
|
-
if (val === null || val === undefined) {
|
|
19
|
-
return `SELECT set_config('${key}', NULL, true);`;
|
|
20
|
-
}
|
|
21
|
-
const escapedVal = val.replace(/'/g, "''");
|
|
22
|
-
return `SELECT set_config('${key}', '${escapedVal}', true);`;
|
|
23
|
-
})
|
|
24
|
-
.join('\n');
|
|
25
|
-
}
|
package/esm/roles.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default role mapping configuration
|
|
3
|
-
*/
|
|
4
|
-
export const DEFAULT_ROLE_MAPPING = {
|
|
5
|
-
anonymous: 'anonymous',
|
|
6
|
-
authenticated: 'authenticated',
|
|
7
|
-
administrator: 'administrator',
|
|
8
|
-
default: 'anonymous'
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Get resolved role mapping with defaults
|
|
12
|
-
*/
|
|
13
|
-
export const getRoleMapping = (options) => {
|
|
14
|
-
return {
|
|
15
|
-
...DEFAULT_ROLE_MAPPING,
|
|
16
|
-
...(options?.roles || {})
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Get role name by key with fallback to default mapping
|
|
21
|
-
*/
|
|
22
|
-
export const getRoleName = (roleKey, options) => {
|
|
23
|
-
const mapping = getRoleMapping(options);
|
|
24
|
-
return mapping[roleKey];
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Get default role name
|
|
28
|
-
*/
|
|
29
|
-
export const getDefaultRole = (options) => {
|
|
30
|
-
const mapping = getRoleMapping(options);
|
|
31
|
-
return mapping.default;
|
|
32
|
-
};
|
package/esm/stream.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { getSpawnEnvWithPg } from 'pg-env';
|
|
3
|
-
import { Readable } from 'stream';
|
|
4
|
-
function setArgs(config) {
|
|
5
|
-
const args = [
|
|
6
|
-
'-U', config.user,
|
|
7
|
-
'-h', config.host,
|
|
8
|
-
'-d', config.database
|
|
9
|
-
];
|
|
10
|
-
if (config.port) {
|
|
11
|
-
args.push('-p', String(config.port));
|
|
12
|
-
}
|
|
13
|
-
return args;
|
|
14
|
-
}
|
|
15
|
-
// Converts a string to a readable stream (replaces streamify-string)
|
|
16
|
-
function stringToStream(text) {
|
|
17
|
-
const stream = new Readable({
|
|
18
|
-
read() {
|
|
19
|
-
this.push(text);
|
|
20
|
-
this.push(null);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
return stream;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Executes SQL statements by streaming them to psql.
|
|
27
|
-
*
|
|
28
|
-
* IMPORTANT: PostgreSQL stderr handling
|
|
29
|
-
* -------------------------------------
|
|
30
|
-
* PostgreSQL sends different message types to stderr, not just errors:
|
|
31
|
-
* - ERROR: Actual SQL errors (should fail)
|
|
32
|
-
* - WARNING: Potential issues (informational)
|
|
33
|
-
* - NOTICE: Informational messages (should NOT fail)
|
|
34
|
-
* - INFO: Informational messages (should NOT fail)
|
|
35
|
-
* - DEBUG: Debug messages (should NOT fail)
|
|
36
|
-
*
|
|
37
|
-
* Example scenario that previously caused false failures:
|
|
38
|
-
* When running SQL like:
|
|
39
|
-
* GRANT administrator TO app_user;
|
|
40
|
-
*
|
|
41
|
-
* If app_user is already a member of administrator, PostgreSQL outputs:
|
|
42
|
-
* NOTICE: role "app_user" is already a member of role "administrator"
|
|
43
|
-
*
|
|
44
|
-
* This is NOT an error - the GRANT succeeded (it's idempotent). But because
|
|
45
|
-
* this message goes to stderr, the old implementation would reject the promise
|
|
46
|
-
* and fail the test, even though nothing was wrong.
|
|
47
|
-
*
|
|
48
|
-
* Solution:
|
|
49
|
-
* 1. Buffer stderr instead of rejecting immediately on any output
|
|
50
|
-
* 2. Use ON_ERROR_STOP=1 so psql exits with non-zero code on actual SQL errors
|
|
51
|
-
* 3. Only reject if the exit code is non-zero, using buffered stderr as the error message
|
|
52
|
-
*
|
|
53
|
-
* This way, NOTICE/WARNING messages are collected but don't cause failures,
|
|
54
|
-
* while actual SQL errors still properly fail with meaningful error messages.
|
|
55
|
-
*/
|
|
56
|
-
export async function streamSql(config, sql) {
|
|
57
|
-
const args = [
|
|
58
|
-
...setArgs(config),
|
|
59
|
-
// ON_ERROR_STOP=1 makes psql exit with a non-zero code when it encounters
|
|
60
|
-
// an actual SQL error. Without this, psql might continue executing subsequent
|
|
61
|
-
// statements and exit with code 0 even if some statements failed.
|
|
62
|
-
'-v', 'ON_ERROR_STOP=1'
|
|
63
|
-
];
|
|
64
|
-
return new Promise((resolve, reject) => {
|
|
65
|
-
const sqlStream = stringToStream(sql);
|
|
66
|
-
// Buffer stderr instead of rejecting immediately. This allows us to collect
|
|
67
|
-
// all output (including harmless NOTICE messages) and only use it for error
|
|
68
|
-
// reporting if the process actually fails.
|
|
69
|
-
let stderrBuffer = '';
|
|
70
|
-
const proc = spawn('psql', args, {
|
|
71
|
-
env: getSpawnEnvWithPg(config)
|
|
72
|
-
});
|
|
73
|
-
sqlStream.pipe(proc.stdin);
|
|
74
|
-
// Collect stderr output. We don't reject here because stderr may contain
|
|
75
|
-
// harmless NOTICE/WARNING messages that shouldn't cause test failures.
|
|
76
|
-
proc.stderr.on('data', (data) => {
|
|
77
|
-
stderrBuffer += data.toString();
|
|
78
|
-
});
|
|
79
|
-
// Determine success/failure based on exit code, not stderr content.
|
|
80
|
-
// Exit code 0 = success (even if there were NOTICE messages on stderr)
|
|
81
|
-
// Exit code non-zero = actual error occurred
|
|
82
|
-
proc.on('close', (code) => {
|
|
83
|
-
if (code !== 0) {
|
|
84
|
-
// Include the buffered stderr in the error message for debugging
|
|
85
|
-
reject(new Error(stderrBuffer || `psql exited with code ${code}`));
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
resolve();
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
// Handle spawn errors (e.g., psql not found)
|
|
92
|
-
proc.on('error', (error) => {
|
|
93
|
-
reject(error);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
}
|
package/roles.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { PgTestConnectionOptions, RoleMapping } from '@pgpmjs/types';
|
|
2
|
-
/**
|
|
3
|
-
* Default role mapping configuration
|
|
4
|
-
*/
|
|
5
|
-
export declare const DEFAULT_ROLE_MAPPING: Required<RoleMapping>;
|
|
6
|
-
/**
|
|
7
|
-
* Get resolved role mapping with defaults
|
|
8
|
-
*/
|
|
9
|
-
export declare const getRoleMapping: (options?: PgTestConnectionOptions) => Required<RoleMapping>;
|
|
10
|
-
/**
|
|
11
|
-
* Get role name by key with fallback to default mapping
|
|
12
|
-
*/
|
|
13
|
-
export declare const getRoleName: (roleKey: keyof Omit<RoleMapping, "default">, options?: PgTestConnectionOptions) => string;
|
|
14
|
-
/**
|
|
15
|
-
* Get default role name
|
|
16
|
-
*/
|
|
17
|
-
export declare const getDefaultRole: (options?: PgTestConnectionOptions) => string;
|
package/roles.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getDefaultRole = exports.getRoleName = exports.getRoleMapping = exports.DEFAULT_ROLE_MAPPING = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Default role mapping configuration
|
|
6
|
-
*/
|
|
7
|
-
exports.DEFAULT_ROLE_MAPPING = {
|
|
8
|
-
anonymous: 'anonymous',
|
|
9
|
-
authenticated: 'authenticated',
|
|
10
|
-
administrator: 'administrator',
|
|
11
|
-
default: 'anonymous'
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* Get resolved role mapping with defaults
|
|
15
|
-
*/
|
|
16
|
-
const getRoleMapping = (options) => {
|
|
17
|
-
return {
|
|
18
|
-
...exports.DEFAULT_ROLE_MAPPING,
|
|
19
|
-
...(options?.roles || {})
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
exports.getRoleMapping = getRoleMapping;
|
|
23
|
-
/**
|
|
24
|
-
* Get role name by key with fallback to default mapping
|
|
25
|
-
*/
|
|
26
|
-
const getRoleName = (roleKey, options) => {
|
|
27
|
-
const mapping = (0, exports.getRoleMapping)(options);
|
|
28
|
-
return mapping[roleKey];
|
|
29
|
-
};
|
|
30
|
-
exports.getRoleName = getRoleName;
|
|
31
|
-
/**
|
|
32
|
-
* Get default role name
|
|
33
|
-
*/
|
|
34
|
-
const getDefaultRole = (options) => {
|
|
35
|
-
const mapping = (0, exports.getRoleMapping)(options);
|
|
36
|
-
return mapping.default;
|
|
37
|
-
};
|
|
38
|
-
exports.getDefaultRole = getDefaultRole;
|
package/stream.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { PgConfig } from 'pg-env';
|
|
2
|
-
/**
|
|
3
|
-
* Executes SQL statements by streaming them to psql.
|
|
4
|
-
*
|
|
5
|
-
* IMPORTANT: PostgreSQL stderr handling
|
|
6
|
-
* -------------------------------------
|
|
7
|
-
* PostgreSQL sends different message types to stderr, not just errors:
|
|
8
|
-
* - ERROR: Actual SQL errors (should fail)
|
|
9
|
-
* - WARNING: Potential issues (informational)
|
|
10
|
-
* - NOTICE: Informational messages (should NOT fail)
|
|
11
|
-
* - INFO: Informational messages (should NOT fail)
|
|
12
|
-
* - DEBUG: Debug messages (should NOT fail)
|
|
13
|
-
*
|
|
14
|
-
* Example scenario that previously caused false failures:
|
|
15
|
-
* When running SQL like:
|
|
16
|
-
* GRANT administrator TO app_user;
|
|
17
|
-
*
|
|
18
|
-
* If app_user is already a member of administrator, PostgreSQL outputs:
|
|
19
|
-
* NOTICE: role "app_user" is already a member of role "administrator"
|
|
20
|
-
*
|
|
21
|
-
* This is NOT an error - the GRANT succeeded (it's idempotent). But because
|
|
22
|
-
* this message goes to stderr, the old implementation would reject the promise
|
|
23
|
-
* and fail the test, even though nothing was wrong.
|
|
24
|
-
*
|
|
25
|
-
* Solution:
|
|
26
|
-
* 1. Buffer stderr instead of rejecting immediately on any output
|
|
27
|
-
* 2. Use ON_ERROR_STOP=1 so psql exits with non-zero code on actual SQL errors
|
|
28
|
-
* 3. Only reject if the exit code is non-zero, using buffered stderr as the error message
|
|
29
|
-
*
|
|
30
|
-
* This way, NOTICE/WARNING messages are collected but don't cause failures,
|
|
31
|
-
* while actual SQL errors still properly fail with meaningful error messages.
|
|
32
|
-
*/
|
|
33
|
-
export declare function streamSql(config: PgConfig, sql: string): Promise<void>;
|
package/stream.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.streamSql = streamSql;
|
|
4
|
-
const child_process_1 = require("child_process");
|
|
5
|
-
const pg_env_1 = require("pg-env");
|
|
6
|
-
const stream_1 = require("stream");
|
|
7
|
-
function setArgs(config) {
|
|
8
|
-
const args = [
|
|
9
|
-
'-U', config.user,
|
|
10
|
-
'-h', config.host,
|
|
11
|
-
'-d', config.database
|
|
12
|
-
];
|
|
13
|
-
if (config.port) {
|
|
14
|
-
args.push('-p', String(config.port));
|
|
15
|
-
}
|
|
16
|
-
return args;
|
|
17
|
-
}
|
|
18
|
-
// Converts a string to a readable stream (replaces streamify-string)
|
|
19
|
-
function stringToStream(text) {
|
|
20
|
-
const stream = new stream_1.Readable({
|
|
21
|
-
read() {
|
|
22
|
-
this.push(text);
|
|
23
|
-
this.push(null);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
return stream;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Executes SQL statements by streaming them to psql.
|
|
30
|
-
*
|
|
31
|
-
* IMPORTANT: PostgreSQL stderr handling
|
|
32
|
-
* -------------------------------------
|
|
33
|
-
* PostgreSQL sends different message types to stderr, not just errors:
|
|
34
|
-
* - ERROR: Actual SQL errors (should fail)
|
|
35
|
-
* - WARNING: Potential issues (informational)
|
|
36
|
-
* - NOTICE: Informational messages (should NOT fail)
|
|
37
|
-
* - INFO: Informational messages (should NOT fail)
|
|
38
|
-
* - DEBUG: Debug messages (should NOT fail)
|
|
39
|
-
*
|
|
40
|
-
* Example scenario that previously caused false failures:
|
|
41
|
-
* When running SQL like:
|
|
42
|
-
* GRANT administrator TO app_user;
|
|
43
|
-
*
|
|
44
|
-
* If app_user is already a member of administrator, PostgreSQL outputs:
|
|
45
|
-
* NOTICE: role "app_user" is already a member of role "administrator"
|
|
46
|
-
*
|
|
47
|
-
* This is NOT an error - the GRANT succeeded (it's idempotent). But because
|
|
48
|
-
* this message goes to stderr, the old implementation would reject the promise
|
|
49
|
-
* and fail the test, even though nothing was wrong.
|
|
50
|
-
*
|
|
51
|
-
* Solution:
|
|
52
|
-
* 1. Buffer stderr instead of rejecting immediately on any output
|
|
53
|
-
* 2. Use ON_ERROR_STOP=1 so psql exits with non-zero code on actual SQL errors
|
|
54
|
-
* 3. Only reject if the exit code is non-zero, using buffered stderr as the error message
|
|
55
|
-
*
|
|
56
|
-
* This way, NOTICE/WARNING messages are collected but don't cause failures,
|
|
57
|
-
* while actual SQL errors still properly fail with meaningful error messages.
|
|
58
|
-
*/
|
|
59
|
-
async function streamSql(config, sql) {
|
|
60
|
-
const args = [
|
|
61
|
-
...setArgs(config),
|
|
62
|
-
// ON_ERROR_STOP=1 makes psql exit with a non-zero code when it encounters
|
|
63
|
-
// an actual SQL error. Without this, psql might continue executing subsequent
|
|
64
|
-
// statements and exit with code 0 even if some statements failed.
|
|
65
|
-
'-v', 'ON_ERROR_STOP=1'
|
|
66
|
-
];
|
|
67
|
-
return new Promise((resolve, reject) => {
|
|
68
|
-
const sqlStream = stringToStream(sql);
|
|
69
|
-
// Buffer stderr instead of rejecting immediately. This allows us to collect
|
|
70
|
-
// all output (including harmless NOTICE messages) and only use it for error
|
|
71
|
-
// reporting if the process actually fails.
|
|
72
|
-
let stderrBuffer = '';
|
|
73
|
-
const proc = (0, child_process_1.spawn)('psql', args, {
|
|
74
|
-
env: (0, pg_env_1.getSpawnEnvWithPg)(config)
|
|
75
|
-
});
|
|
76
|
-
sqlStream.pipe(proc.stdin);
|
|
77
|
-
// Collect stderr output. We don't reject here because stderr may contain
|
|
78
|
-
// harmless NOTICE/WARNING messages that shouldn't cause test failures.
|
|
79
|
-
proc.stderr.on('data', (data) => {
|
|
80
|
-
stderrBuffer += data.toString();
|
|
81
|
-
});
|
|
82
|
-
// Determine success/failure based on exit code, not stderr content.
|
|
83
|
-
// Exit code 0 = success (even if there were NOTICE messages on stderr)
|
|
84
|
-
// Exit code non-zero = actual error occurred
|
|
85
|
-
proc.on('close', (code) => {
|
|
86
|
-
if (code !== 0) {
|
|
87
|
-
// Include the buffered stderr in the error message for debugging
|
|
88
|
-
reject(new Error(stderrBuffer || `psql exited with code ${code}`));
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
resolve();
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
// Handle spawn errors (e.g., psql not found)
|
|
95
|
-
proc.on('error', (error) => {
|
|
96
|
-
reject(error);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
}
|