pgsql-seed 0.0.1 → 0.2.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/LICENSE +23 -0
- package/README.md +39 -625
- package/esm/index.d.ts +2 -0
- package/esm/index.js +12 -7
- package/esm/pgpm.d.ts +37 -0
- package/esm/pgpm.js +52 -0
- package/index.d.ts +2 -7
- package/index.js +20 -23
- package/package.json +27 -32
- package/pgpm.d.ts +37 -0
- package/pgpm.js +56 -0
- package/admin.d.ts +0 -26
- package/admin.js +0 -144
- package/connect.d.ts +0 -19
- package/connect.js +0 -95
- package/context-utils.d.ts +0 -8
- package/context-utils.js +0 -28
- package/esm/admin.js +0 -140
- package/esm/connect.js +0 -90
- package/esm/context-utils.js +0 -25
- package/esm/manager.js +0 -138
- package/esm/roles.js +0 -32
- package/esm/seed/adapters.js +0 -23
- package/esm/seed/csv.js +0 -108
- package/esm/seed/index.js +0 -14
- package/esm/seed/json.js +0 -36
- package/esm/seed/pgpm.js +0 -28
- package/esm/seed/sql.js +0 -15
- package/esm/seed/types.js +0 -1
- package/esm/stream.js +0 -96
- package/esm/test-client.js +0 -168
- package/esm/utils.js +0 -91
- package/manager.d.ts +0 -26
- package/manager.js +0 -142
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/seed/adapters.d.ts +0 -4
- package/seed/adapters.js +0 -28
- package/seed/csv.d.ts +0 -15
- package/seed/csv.js +0 -114
- package/seed/index.d.ts +0 -14
- package/seed/index.js +0 -31
- package/seed/json.d.ts +0 -12
- package/seed/json.js +0 -40
- package/seed/pgpm.d.ts +0 -10
- package/seed/pgpm.js +0 -32
- package/seed/sql.d.ts +0 -7
- package/seed/sql.js +0 -18
- package/seed/types.d.ts +0 -13
- package/seed/types.js +0 -2
- package/stream.d.ts +0 -33
- package/stream.js +0 -99
- package/test-client.d.ts +0 -55
- package/test-client.js +0 -172
- package/utils.d.ts +0 -17
- package/utils.js +0 -105
package/seed/csv.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.loadCsvMap = loadCsvMap;
|
|
4
|
-
exports.csv = csv;
|
|
5
|
-
exports.copyCsvIntoTable = copyCsvIntoTable;
|
|
6
|
-
exports.exportTableToCsv = exportTableToCsv;
|
|
7
|
-
const promises_1 = require("node:stream/promises");
|
|
8
|
-
const logger_1 = require("@pgpmjs/logger");
|
|
9
|
-
const csv_parse_1 = require("csv-parse");
|
|
10
|
-
const fs_1 = require("fs");
|
|
11
|
-
const pg_copy_streams_1 = require("pg-copy-streams");
|
|
12
|
-
const log = new logger_1.Logger('csv');
|
|
13
|
-
/**
|
|
14
|
-
* Standalone helper function to load CSV files into PostgreSQL tables
|
|
15
|
-
* @param client - PostgreSQL client instance
|
|
16
|
-
* @param tables - Map of table names to CSV file paths
|
|
17
|
-
*/
|
|
18
|
-
async function loadCsvMap(client, tables) {
|
|
19
|
-
for (const [table, filePath] of Object.entries(tables)) {
|
|
20
|
-
if (!(0, fs_1.existsSync)(filePath)) {
|
|
21
|
-
throw new Error(`CSV file not found: ${filePath}`);
|
|
22
|
-
}
|
|
23
|
-
log.info(`📥 Seeding "${table}" from ${filePath}`);
|
|
24
|
-
const columns = await parseCsvHeader(filePath);
|
|
25
|
-
const quotedColumns = columns.map(col => `"${col.replace(/"/g, '""')}"`);
|
|
26
|
-
const columnList = quotedColumns.join(', ');
|
|
27
|
-
const copyCommand = `COPY ${table} (${columnList}) FROM STDIN WITH CSV HEADER`;
|
|
28
|
-
log.info(`Using columns: ${columnList}`);
|
|
29
|
-
const stream = client.query((0, pg_copy_streams_1.from)(copyCommand));
|
|
30
|
-
const source = (0, fs_1.createReadStream)(filePath);
|
|
31
|
-
try {
|
|
32
|
-
await (0, promises_1.pipeline)(source, stream);
|
|
33
|
-
log.success(`✅ Successfully seeded "${table}"`);
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
log.error(`❌ COPY failed for "${table}": ${err.message}`);
|
|
37
|
-
throw err;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function csv(tables) {
|
|
42
|
-
return {
|
|
43
|
-
async seed(ctx) {
|
|
44
|
-
for (const [table, filePath] of Object.entries(tables)) {
|
|
45
|
-
if (!(0, fs_1.existsSync)(filePath)) {
|
|
46
|
-
throw new Error(`CSV file not found: ${filePath}`);
|
|
47
|
-
}
|
|
48
|
-
log.info(`📥 Seeding "${table}" from ${filePath}`);
|
|
49
|
-
await copyCsvIntoTable(ctx.pg, table, filePath);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
async function parseCsvHeader(filePath) {
|
|
55
|
-
const file = (0, fs_1.createReadStream)(filePath);
|
|
56
|
-
const parser = (0, csv_parse_1.parse)({
|
|
57
|
-
bom: true,
|
|
58
|
-
to_line: 1,
|
|
59
|
-
skip_empty_lines: true,
|
|
60
|
-
});
|
|
61
|
-
return new Promise((resolve, reject) => {
|
|
62
|
-
const cleanup = (err) => {
|
|
63
|
-
parser.destroy();
|
|
64
|
-
file.destroy();
|
|
65
|
-
if (err)
|
|
66
|
-
reject(err);
|
|
67
|
-
};
|
|
68
|
-
parser.on('readable', () => {
|
|
69
|
-
const row = parser.read();
|
|
70
|
-
if (!row)
|
|
71
|
-
return;
|
|
72
|
-
if (row.length === 0) {
|
|
73
|
-
cleanup(new Error('CSV header has no columns'));
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
cleanup();
|
|
77
|
-
resolve(row);
|
|
78
|
-
});
|
|
79
|
-
parser.on('error', cleanup);
|
|
80
|
-
file.on('error', cleanup);
|
|
81
|
-
file.pipe(parser);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
async function copyCsvIntoTable(pg, table, filePath) {
|
|
85
|
-
const client = pg.client;
|
|
86
|
-
const columns = await parseCsvHeader(filePath);
|
|
87
|
-
const quotedColumns = columns.map(col => `"${col.replace(/"/g, '""')}"`);
|
|
88
|
-
const columnList = quotedColumns.join(', ');
|
|
89
|
-
const copyCommand = `COPY ${table} (${columnList}) FROM STDIN WITH CSV HEADER`;
|
|
90
|
-
log.info(`Using columns: ${columnList}`);
|
|
91
|
-
const stream = client.query((0, pg_copy_streams_1.from)(copyCommand));
|
|
92
|
-
const source = (0, fs_1.createReadStream)(filePath);
|
|
93
|
-
try {
|
|
94
|
-
await (0, promises_1.pipeline)(source, stream);
|
|
95
|
-
log.success(`✅ Successfully seeded "${table}"`);
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
log.error(`❌ COPY failed for "${table}": ${err.message}`);
|
|
99
|
-
throw err;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
async function exportTableToCsv(pg, table, filePath) {
|
|
103
|
-
const client = pg.client;
|
|
104
|
-
const stream = client.query((0, pg_copy_streams_1.to)(`COPY ${table} TO STDOUT WITH CSV HEADER`));
|
|
105
|
-
const target = (0, fs_1.createWriteStream)(filePath);
|
|
106
|
-
try {
|
|
107
|
-
await (0, promises_1.pipeline)(stream, target);
|
|
108
|
-
log.success(`✅ Exported "${table}" to ${filePath}`);
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
log.error(`❌ Failed to export "${table}": ${err.message}`);
|
|
112
|
-
throw err;
|
|
113
|
-
}
|
|
114
|
-
}
|
package/seed/index.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { compose, fn, sqlfile } from './adapters';
|
|
2
|
-
import { csv } from './csv';
|
|
3
|
-
import { json } from './json';
|
|
4
|
-
import { pgpm } from './pgpm';
|
|
5
|
-
export * from './csv';
|
|
6
|
-
export * from './types';
|
|
7
|
-
export declare const seed: {
|
|
8
|
-
pgpm: typeof pgpm;
|
|
9
|
-
json: typeof json;
|
|
10
|
-
csv: typeof csv;
|
|
11
|
-
compose: typeof compose;
|
|
12
|
-
fn: typeof fn;
|
|
13
|
-
sqlfile: typeof sqlfile;
|
|
14
|
-
};
|
package/seed/index.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
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]; } };
|
|
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);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.seed = void 0;
|
|
18
|
-
const adapters_1 = require("./adapters");
|
|
19
|
-
const csv_1 = require("./csv");
|
|
20
|
-
const json_1 = require("./json");
|
|
21
|
-
const pgpm_1 = require("./pgpm");
|
|
22
|
-
__exportStar(require("./csv"), exports);
|
|
23
|
-
__exportStar(require("./types"), exports);
|
|
24
|
-
exports.seed = {
|
|
25
|
-
pgpm: pgpm_1.pgpm,
|
|
26
|
-
json: json_1.json,
|
|
27
|
-
csv: csv_1.csv,
|
|
28
|
-
compose: adapters_1.compose,
|
|
29
|
-
fn: adapters_1.fn,
|
|
30
|
-
sqlfile: adapters_1.sqlfile
|
|
31
|
-
};
|
package/seed/json.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { Client } from 'pg';
|
|
2
|
-
import { SeedAdapter } from './types';
|
|
3
|
-
export interface JsonSeedMap {
|
|
4
|
-
[table: string]: Record<string, any>[];
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Standalone helper function to insert JSON data into PostgreSQL tables
|
|
8
|
-
* @param client - PostgreSQL client instance
|
|
9
|
-
* @param data - Map of table names to arrays of row objects
|
|
10
|
-
*/
|
|
11
|
-
export declare function insertJson(client: Client, data: JsonSeedMap): Promise<void>;
|
|
12
|
-
export declare function json(data: JsonSeedMap): SeedAdapter;
|
package/seed/json.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.insertJson = insertJson;
|
|
4
|
-
exports.json = json;
|
|
5
|
-
/**
|
|
6
|
-
* Standalone helper function to insert JSON data into PostgreSQL tables
|
|
7
|
-
* @param client - PostgreSQL client instance
|
|
8
|
-
* @param data - Map of table names to arrays of row objects
|
|
9
|
-
*/
|
|
10
|
-
async function insertJson(client, data) {
|
|
11
|
-
for (const [table, rows] of Object.entries(data)) {
|
|
12
|
-
if (!Array.isArray(rows) || rows.length === 0)
|
|
13
|
-
continue;
|
|
14
|
-
const columns = Object.keys(rows[0]);
|
|
15
|
-
const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ');
|
|
16
|
-
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
|
|
17
|
-
for (const row of rows) {
|
|
18
|
-
const values = columns.map((c) => row[c]);
|
|
19
|
-
await client.query(sql, values);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function json(data) {
|
|
24
|
-
return {
|
|
25
|
-
async seed(ctx) {
|
|
26
|
-
const { pg } = ctx;
|
|
27
|
-
for (const [table, rows] of Object.entries(data)) {
|
|
28
|
-
if (!Array.isArray(rows) || rows.length === 0)
|
|
29
|
-
continue;
|
|
30
|
-
const columns = Object.keys(rows[0]);
|
|
31
|
-
const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ');
|
|
32
|
-
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
|
|
33
|
-
for (const row of rows) {
|
|
34
|
-
const values = columns.map((c) => row[c]);
|
|
35
|
-
await pg.query(sql, values);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
}
|
package/seed/pgpm.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { PgConfig } from 'pg-env';
|
|
2
|
-
import { SeedAdapter } from './types';
|
|
3
|
-
/**
|
|
4
|
-
* Standalone helper function to deploy pgpm package
|
|
5
|
-
* @param config - PostgreSQL configuration
|
|
6
|
-
* @param cwd - Current working directory (defaults to process.cwd())
|
|
7
|
-
* @param cache - Whether to enable caching (defaults to false)
|
|
8
|
-
*/
|
|
9
|
-
export declare function deployPgpm(config: PgConfig, cwd?: string, cache?: boolean): Promise<void>;
|
|
10
|
-
export declare function pgpm(cwd?: string, cache?: boolean): SeedAdapter;
|
package/seed/pgpm.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.deployPgpm = deployPgpm;
|
|
4
|
-
exports.pgpm = pgpm;
|
|
5
|
-
const core_1 = require("@pgpmjs/core");
|
|
6
|
-
const env_1 = require("@pgpmjs/env");
|
|
7
|
-
/**
|
|
8
|
-
* Standalone helper function to deploy pgpm package
|
|
9
|
-
* @param config - PostgreSQL configuration
|
|
10
|
-
* @param cwd - Current working directory (defaults to process.cwd())
|
|
11
|
-
* @param cache - Whether to enable caching (defaults to false)
|
|
12
|
-
*/
|
|
13
|
-
async function deployPgpm(config, cwd, cache = false) {
|
|
14
|
-
const proj = new core_1.PgpmPackage(cwd ?? process.cwd());
|
|
15
|
-
if (!proj.isInModule())
|
|
16
|
-
return;
|
|
17
|
-
await proj.deploy((0, env_1.getEnvOptions)({
|
|
18
|
-
pg: config,
|
|
19
|
-
deployment: {
|
|
20
|
-
fast: true,
|
|
21
|
-
usePlan: true,
|
|
22
|
-
cache
|
|
23
|
-
}
|
|
24
|
-
}), proj.getModuleName());
|
|
25
|
-
}
|
|
26
|
-
function pgpm(cwd, cache = false) {
|
|
27
|
-
return {
|
|
28
|
-
async seed(ctx) {
|
|
29
|
-
await deployPgpm(ctx.config, cwd ?? ctx.connect.cwd, cache);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
}
|
package/seed/sql.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Client } from 'pg';
|
|
2
|
-
/**
|
|
3
|
-
* Standalone helper function to load SQL files into PostgreSQL
|
|
4
|
-
* @param client - PostgreSQL client instance
|
|
5
|
-
* @param files - Array of SQL file paths to execute
|
|
6
|
-
*/
|
|
7
|
-
export declare function loadSqlFiles(client: Client, files: string[]): Promise<void>;
|
package/seed/sql.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.loadSqlFiles = loadSqlFiles;
|
|
4
|
-
const fs_1 = require("fs");
|
|
5
|
-
/**
|
|
6
|
-
* Standalone helper function to load SQL files into PostgreSQL
|
|
7
|
-
* @param client - PostgreSQL client instance
|
|
8
|
-
* @param files - Array of SQL file paths to execute
|
|
9
|
-
*/
|
|
10
|
-
async function loadSqlFiles(client, files) {
|
|
11
|
-
for (const file of files) {
|
|
12
|
-
if (!(0, fs_1.existsSync)(file)) {
|
|
13
|
-
throw new Error(`SQL file not found: ${file}`);
|
|
14
|
-
}
|
|
15
|
-
const sql = (0, fs_1.readFileSync)(file, 'utf-8');
|
|
16
|
-
await client.query(sql);
|
|
17
|
-
}
|
|
18
|
-
}
|
package/seed/types.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { PgTestConnectionOptions } from '@pgpmjs/types';
|
|
2
|
-
import { PgConfig } from 'pg-env';
|
|
3
|
-
import { DbAdmin } from '../admin';
|
|
4
|
-
import { PgTestClient } from '../test-client';
|
|
5
|
-
export interface SeedContext {
|
|
6
|
-
connect: PgTestConnectionOptions;
|
|
7
|
-
admin: DbAdmin;
|
|
8
|
-
config: PgConfig;
|
|
9
|
-
pg: PgTestClient;
|
|
10
|
-
}
|
|
11
|
-
export interface SeedAdapter {
|
|
12
|
-
seed(ctx: SeedContext): Promise<void> | void;
|
|
13
|
-
}
|
package/seed/types.js
DELETED
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
|
-
}
|
package/test-client.d.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { Client, QueryResult } from 'pg';
|
|
2
|
-
import { PgConfig } from 'pg-env';
|
|
3
|
-
import { AuthOptions, PgTestConnectionOptions } from '@pgpmjs/types';
|
|
4
|
-
import { type JsonSeedMap } from './seed/json';
|
|
5
|
-
import { type CsvSeedMap } from './seed/csv';
|
|
6
|
-
export type PgTestClientOpts = {
|
|
7
|
-
deferConnect?: boolean;
|
|
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;
|
|
18
|
-
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
|
-
beforeEach(): Promise<void>;
|
|
26
|
-
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
|
-
/**
|
|
34
|
-
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
35
|
-
* Maintains test isolation by creating a savepoint and reapplying session context.
|
|
36
|
-
*/
|
|
37
|
-
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
|
-
loadJson(data: JsonSeedMap): Promise<void>;
|
|
52
|
-
loadSql(files: string[]): Promise<void>;
|
|
53
|
-
loadCsv(tables: CsvSeedMap): Promise<void>;
|
|
54
|
-
loadPgpm(cwd?: string, cache?: boolean): Promise<void>;
|
|
55
|
-
}
|
package/test-client.js
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PgTestClient = void 0;
|
|
4
|
-
const pg_1 = require("pg");
|
|
5
|
-
const roles_1 = require("./roles");
|
|
6
|
-
const context_utils_1 = require("./context-utils");
|
|
7
|
-
const json_1 = require("./seed/json");
|
|
8
|
-
const csv_1 = require("./seed/csv");
|
|
9
|
-
const sql_1 = require("./seed/sql");
|
|
10
|
-
const pgpm_1 = require("./seed/pgpm");
|
|
11
|
-
class PgTestClient {
|
|
12
|
-
config;
|
|
13
|
-
client;
|
|
14
|
-
opts;
|
|
15
|
-
ctxStmts = '';
|
|
16
|
-
contextSettings = {};
|
|
17
|
-
_ended = false;
|
|
18
|
-
connectPromise = null;
|
|
19
|
-
constructor(config, opts = {}) {
|
|
20
|
-
this.opts = opts;
|
|
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;');
|
|
61
|
-
}
|
|
62
|
-
async beforeEach() {
|
|
63
|
-
await this.begin();
|
|
64
|
-
await this.savepoint();
|
|
65
|
-
}
|
|
66
|
-
async afterEach() {
|
|
67
|
-
await this.rollback();
|
|
68
|
-
await this.commit();
|
|
69
|
-
}
|
|
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
|
-
/**
|
|
88
|
-
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
89
|
-
* Maintains test isolation by creating a savepoint and reapplying session context.
|
|
90
|
-
*/
|
|
91
|
-
async publish() {
|
|
92
|
-
await this.commit(); // make data visible to other sessions
|
|
93
|
-
await this.begin(); // fresh tx
|
|
94
|
-
await this.savepoint(); // keep rollback harness
|
|
95
|
-
await this.ctxQuery(); // reapply all setContext()
|
|
96
|
-
}
|
|
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
|
-
async loadJson(data) {
|
|
152
|
-
await this.ctxQuery();
|
|
153
|
-
await (0, json_1.insertJson)(this.client, data);
|
|
154
|
-
}
|
|
155
|
-
async loadSql(files) {
|
|
156
|
-
await this.ctxQuery();
|
|
157
|
-
await (0, sql_1.loadSqlFiles)(this.client, files);
|
|
158
|
-
}
|
|
159
|
-
// NON-RLS load/seed methods:
|
|
160
|
-
async loadCsv(tables) {
|
|
161
|
-
// await this.ctxQuery(); // no point to call ctxQuery() here
|
|
162
|
-
// because POSTGRES doesn't support row-level security on COPY FROM...
|
|
163
|
-
await (0, csv_1.loadCsvMap)(this.client, tables);
|
|
164
|
-
}
|
|
165
|
-
async loadPgpm(cwd, cache = false) {
|
|
166
|
-
// await this.ctxQuery(); // no point to call ctxQuery() here
|
|
167
|
-
// because deployPgpm() has it's own way of getting the client...
|
|
168
|
-
// so for now, we'll expose this but it's limited
|
|
169
|
-
await (0, pgpm_1.deployPgpm)(this.config, cwd, cache);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
exports.PgTestClient = PgTestClient;
|