pgsql-client 0.0.1 → 1.1.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/esm/admin.js ADDED
@@ -0,0 +1,126 @@
1
+ import { generateCreateUserWithGrantsSQL, generateGrantRoleSQL } from '@pgpmjs/core';
2
+ import { Logger } from '@pgpmjs/logger';
3
+ import { execSync } from 'child_process';
4
+ import { existsSync } from 'fs';
5
+ import { getPgEnvOptions } from 'pg-env';
6
+ import { getRoleName } from './roles';
7
+ import { streamSql as stream } from './stream';
8
+ const log = new Logger('db-admin');
9
+ export class DbAdmin {
10
+ config;
11
+ verbose;
12
+ roleConfig;
13
+ constructor(config, verbose = false, roleConfig) {
14
+ this.config = getPgEnvOptions(config);
15
+ this.verbose = verbose;
16
+ this.roleConfig = roleConfig;
17
+ }
18
+ getEnv() {
19
+ return {
20
+ PGHOST: this.config.host,
21
+ PGPORT: String(this.config.port),
22
+ PGUSER: this.config.user,
23
+ PGPASSWORD: this.config.password
24
+ };
25
+ }
26
+ run(command) {
27
+ try {
28
+ execSync(command, {
29
+ stdio: this.verbose ? 'inherit' : 'pipe',
30
+ env: {
31
+ ...process.env,
32
+ ...this.getEnv()
33
+ }
34
+ });
35
+ if (this.verbose)
36
+ log.success(`Executed: ${command}`);
37
+ }
38
+ catch (err) {
39
+ log.error(`Command failed: ${command}`);
40
+ if (this.verbose)
41
+ log.error(err.message);
42
+ throw err;
43
+ }
44
+ }
45
+ safeDropDb(name) {
46
+ try {
47
+ this.run(`dropdb "${name}"`);
48
+ }
49
+ catch (err) {
50
+ if (!err.message.includes('does not exist')) {
51
+ log.warn(`Could not drop database ${name}: ${err.message}`);
52
+ }
53
+ }
54
+ }
55
+ drop(dbName) {
56
+ this.safeDropDb(dbName ?? this.config.database);
57
+ }
58
+ dropTemplate(dbName) {
59
+ this.run(`psql -c "UPDATE pg_database SET datistemplate='false' WHERE datname='${dbName}'"`);
60
+ this.drop(dbName);
61
+ }
62
+ create(dbName) {
63
+ const db = dbName ?? this.config.database;
64
+ this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} "${db}"`);
65
+ }
66
+ createFromTemplate(template, dbName) {
67
+ const db = dbName ?? this.config.database;
68
+ this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} -e "${db}" -T "${template}"`);
69
+ }
70
+ installExtensions(extensions, dbName) {
71
+ const db = dbName ?? this.config.database;
72
+ const extList = typeof extensions === 'string' ? extensions.split(',') : extensions;
73
+ for (const extension of extList) {
74
+ this.run(`psql --dbname "${db}" -c 'CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;'`);
75
+ }
76
+ }
77
+ connectionString(dbName) {
78
+ const { user, password, host, port } = this.config;
79
+ const db = dbName ?? this.config.database;
80
+ return `postgres://${user}:${password}@${host}:${port}/${db}`;
81
+ }
82
+ createTemplateFromBase(base, template) {
83
+ this.run(`createdb -T "${base}" "${template}"`);
84
+ this.run(`psql -c "UPDATE pg_database SET datistemplate = true WHERE datname = '${template}';"`);
85
+ }
86
+ cleanupTemplate(template) {
87
+ try {
88
+ this.run(`psql -c "UPDATE pg_database SET datistemplate = false WHERE datname = '${template}'"`);
89
+ }
90
+ catch {
91
+ log.warn(`Skipping failed UPDATE of datistemplate for ${template}`);
92
+ }
93
+ this.safeDropDb(template);
94
+ }
95
+ async grantRole(role, user, dbName) {
96
+ const db = dbName ?? this.config.database;
97
+ const sql = generateGrantRoleSQL(role, user);
98
+ await this.streamSql(sql, db);
99
+ }
100
+ async grantConnect(role, dbName) {
101
+ const db = dbName ?? this.config.database;
102
+ const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
103
+ await this.streamSql(sql, db);
104
+ }
105
+ // ONLY granting admin role for testing purposes, normally the db connection for apps won't have admin role
106
+ // DO NOT USE THIS FOR PRODUCTION
107
+ async createUserRole(user, password, dbName, useLocksForRoles = false) {
108
+ const anonRole = getRoleName('anonymous', this.roleConfig);
109
+ const authRole = getRoleName('authenticated', this.roleConfig);
110
+ const adminRole = getRoleName('administrator', this.roleConfig);
111
+ const sql = generateCreateUserWithGrantsSQL(user, password, [anonRole, authRole, adminRole], useLocksForRoles);
112
+ await this.streamSql(sql, dbName);
113
+ }
114
+ loadSql(file, dbName) {
115
+ if (!existsSync(file)) {
116
+ throw new Error(`Missing SQL file: ${file}`);
117
+ }
118
+ this.run(`psql -f ${file} ${dbName}`);
119
+ }
120
+ async streamSql(sql, dbName) {
121
+ await stream({
122
+ ...this.config,
123
+ database: dbName
124
+ }, sql);
125
+ }
126
+ }
package/esm/client.js ADDED
@@ -0,0 +1,126 @@
1
+ import { Client } from 'pg';
2
+ import { getRoleName } from './roles';
3
+ import { generateContextStatements } from './context-utils';
4
+ export class PgClient {
5
+ config;
6
+ client;
7
+ opts;
8
+ ctxStmts = '';
9
+ contextSettings = {};
10
+ _ended = false;
11
+ connectPromise = null;
12
+ constructor(config, opts = {}) {
13
+ this.opts = opts;
14
+ this.config = config;
15
+ this.client = new Client({
16
+ host: this.config.host,
17
+ port: this.config.port,
18
+ database: this.config.database,
19
+ user: this.config.user,
20
+ password: this.config.password
21
+ });
22
+ if (!opts.deferConnect) {
23
+ this.connectPromise = this.client.connect();
24
+ if (opts.trackConnect)
25
+ opts.trackConnect(this.connectPromise);
26
+ }
27
+ }
28
+ async ensureConnected() {
29
+ if (this.connectPromise) {
30
+ try {
31
+ await this.connectPromise;
32
+ }
33
+ catch { }
34
+ }
35
+ }
36
+ async close() {
37
+ if (!this._ended) {
38
+ this._ended = true;
39
+ await this.ensureConnected();
40
+ await this.client.end();
41
+ }
42
+ }
43
+ async begin() {
44
+ await this.client.query('BEGIN;');
45
+ }
46
+ async savepoint(name = 'lqlsavepoint') {
47
+ await this.client.query(`SAVEPOINT "${name}";`);
48
+ }
49
+ async rollback(name = 'lqlsavepoint') {
50
+ await this.client.query(`ROLLBACK TO SAVEPOINT "${name}";`);
51
+ }
52
+ async commit() {
53
+ await this.client.query('COMMIT;');
54
+ }
55
+ setContext(ctx) {
56
+ Object.assign(this.contextSettings, ctx);
57
+ this.ctxStmts = generateContextStatements(this.contextSettings);
58
+ }
59
+ /**
60
+ * Set authentication context for the current session.
61
+ * Configures role and user ID using cascading defaults from options -> opts.auth -> RoleMapping.
62
+ */
63
+ auth(options = {}) {
64
+ const role = options.role ?? this.opts.auth?.role ?? getRoleName('authenticated', this.opts);
65
+ const userIdKey = options.userIdKey ?? this.opts.auth?.userIdKey ?? 'jwt.claims.user_id';
66
+ const userId = options.userId ?? this.opts.auth?.userId ?? null;
67
+ this.setContext({
68
+ role,
69
+ [userIdKey]: userId !== null ? String(userId) : null
70
+ });
71
+ }
72
+ /**
73
+ * Clear all session context variables and reset to default anonymous role.
74
+ */
75
+ clearContext() {
76
+ const defaultRole = getRoleName('anonymous', this.opts);
77
+ const nulledSettings = {};
78
+ Object.keys(this.contextSettings).forEach(key => {
79
+ nulledSettings[key] = null;
80
+ });
81
+ nulledSettings.role = defaultRole;
82
+ this.ctxStmts = generateContextStatements(nulledSettings);
83
+ this.contextSettings = { role: defaultRole };
84
+ }
85
+ async any(query, values) {
86
+ const result = await this.query(query, values);
87
+ return result.rows;
88
+ }
89
+ async one(query, values) {
90
+ const rows = await this.any(query, values);
91
+ if (rows.length !== 1) {
92
+ throw new Error('Expected exactly one result');
93
+ }
94
+ return rows[0];
95
+ }
96
+ async oneOrNone(query, values) {
97
+ const rows = await this.any(query, values);
98
+ return rows[0] || null;
99
+ }
100
+ async many(query, values) {
101
+ const rows = await this.any(query, values);
102
+ if (rows.length === 0)
103
+ throw new Error('Expected many rows, got none');
104
+ return rows;
105
+ }
106
+ async manyOrNone(query, values) {
107
+ return this.any(query, values);
108
+ }
109
+ async none(query, values) {
110
+ await this.query(query, values);
111
+ }
112
+ async result(query, values) {
113
+ return this.query(query, values);
114
+ }
115
+ async ctxQuery() {
116
+ if (this.ctxStmts) {
117
+ await this.client.query(this.ctxStmts);
118
+ }
119
+ }
120
+ // NOTE: all queries should call ctxQuery() before executing the query
121
+ async query(query, values) {
122
+ await this.ctxQuery();
123
+ const result = await this.client.query(query, values);
124
+ return result;
125
+ }
126
+ }
@@ -0,0 +1,25 @@
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/index.js CHANGED
@@ -1,36 +1,5 @@
1
- function setContext(ctx) {
2
- return Object.keys(ctx || {}).reduce((m, el) => {
3
- m.push(`SELECT set_config('${el}', '${ctx[el]}', true);`);
4
- return m;
5
- }, []);
6
- }
7
- async function execContext(client, ctx) {
8
- const local = setContext(ctx);
9
- for (const query of local) {
10
- await client.query(query);
11
- }
12
- }
13
- export default async ({ client, context = {}, query = '', variables = [] }) => {
14
- const isPool = 'connect' in client;
15
- const shouldRelease = isPool;
16
- let pgClient = null;
17
- try {
18
- pgClient = isPool ? await client.connect() : client;
19
- await pgClient.query('BEGIN');
20
- await execContext(pgClient, context);
21
- const result = await pgClient.query(query, variables);
22
- await pgClient.query('COMMIT');
23
- return result;
24
- }
25
- catch (error) {
26
- if (pgClient) {
27
- await pgClient.query('ROLLBACK').catch(() => { });
28
- }
29
- throw error;
30
- }
31
- finally {
32
- if (shouldRelease && pgClient && 'release' in pgClient) {
33
- pgClient.release();
34
- }
35
- }
36
- };
1
+ export * from './admin';
2
+ export * from './client';
3
+ export * from './context-utils';
4
+ export * from './roles';
5
+ export { streamSql } from './stream';
package/esm/roles.js ADDED
@@ -0,0 +1,32 @@
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 ADDED
@@ -0,0 +1,96 @@
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/index.d.ts CHANGED
@@ -1,9 +1,5 @@
1
- import { ClientBase, Pool, QueryResult } from 'pg';
2
- interface ExecOptions {
3
- client: Pool | ClientBase;
4
- context?: Record<string, string>;
5
- query: string;
6
- variables?: any[];
7
- }
8
- declare const _default: ({ client, context, query, variables }: ExecOptions) => Promise<QueryResult>;
9
- export default _default;
1
+ export * from './admin';
2
+ export * from './client';
3
+ export * from './context-utils';
4
+ export * from './roles';
5
+ export { streamSql } from './stream';
package/index.js CHANGED
@@ -1,38 +1,23 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- function setContext(ctx) {
4
- return Object.keys(ctx || {}).reduce((m, el) => {
5
- m.push(`SELECT set_config('${el}', '${ctx[el]}', true);`);
6
- return m;
7
- }, []);
8
- }
9
- async function execContext(client, ctx) {
10
- const local = setContext(ctx);
11
- for (const query of local) {
12
- await client.query(query);
13
- }
14
- }
15
- exports.default = async ({ client, context = {}, query = '', variables = [] }) => {
16
- const isPool = 'connect' in client;
17
- const shouldRelease = isPool;
18
- let pgClient = null;
19
- try {
20
- pgClient = isPool ? await client.connect() : client;
21
- await pgClient.query('BEGIN');
22
- await execContext(pgClient, context);
23
- const result = await pgClient.query(query, variables);
24
- await pgClient.query('COMMIT');
25
- return result;
26
- }
27
- catch (error) {
28
- if (pgClient) {
29
- await pgClient.query('ROLLBACK').catch(() => { });
30
- }
31
- throw error;
32
- }
33
- finally {
34
- if (shouldRelease && pgClient && 'release' in pgClient) {
35
- pgClient.release();
36
- }
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
37
7
  }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
38
15
  };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.streamSql = void 0;
18
+ __exportStar(require("./admin"), exports);
19
+ __exportStar(require("./client"), exports);
20
+ __exportStar(require("./context-utils"), exports);
21
+ __exportStar(require("./roles"), exports);
22
+ var stream_1 = require("./stream");
23
+ Object.defineProperty(exports, "streamSql", { enumerable: true, get: function () { return stream_1.streamSql; } });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pgsql-client",
3
- "version": "0.0.1",
3
+ "version": "1.1.1",
4
4
  "author": "Constructive <developers@constructive.io>",
5
- "description": "pgsql client",
5
+ "description": "PostgreSQL client utilities with query helpers, RLS context management, and database administration",
6
6
  "main": "index.js",
7
7
  "module": "esm/index.js",
8
8
  "types": "index.d.ts",
@@ -19,18 +19,37 @@
19
19
  "bugs": {
20
20
  "url": "https://github.com/constructive-io/constructive/issues"
21
21
  },
22
- "dependencies": {
23
- "pg": "^8.16.3"
22
+ "keywords": [
23
+ "postgres",
24
+ "postgresql",
25
+ "client",
26
+ "pg",
27
+ "rls",
28
+ "row-level-security",
29
+ "database",
30
+ "admin",
31
+ "query-helpers",
32
+ "constructive"
33
+ ],
34
+ "scripts": {
35
+ "clean": "makage clean",
36
+ "prepack": "npm run build",
37
+ "build": "makage build",
38
+ "build:dev": "makage build --dev",
39
+ "lint": "eslint . --fix",
40
+ "test": "jest --passWithNoTests",
41
+ "test:watch": "jest --watch"
24
42
  },
25
43
  "devDependencies": {
26
44
  "@types/pg": "^8.16.0",
27
45
  "makage": "^0.1.9"
28
46
  },
29
- "keywords": [
30
- "postgresql",
31
- "query",
32
- "context",
33
- "pg",
34
- "graphile"
35
- ]
47
+ "dependencies": {
48
+ "@pgpmjs/core": "^4.5.1",
49
+ "@pgpmjs/logger": "^1.3.5",
50
+ "@pgpmjs/types": "^2.12.8",
51
+ "pg": "^8.16.3",
52
+ "pg-env": "^1.2.4"
53
+ },
54
+ "gitHead": "6883e3b93da28078483bc6aa25862613ef4405b2"
36
55
  }
package/roles.d.ts ADDED
@@ -0,0 +1,17 @@
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 ADDED
@@ -0,0 +1,38 @@
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 ADDED
@@ -0,0 +1,33 @@
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>;