pgsql-seed 0.0.1 โ 0.2.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/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/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/esm/test-client.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { Client } from 'pg';
|
|
2
|
-
import { getRoleName } from './roles';
|
|
3
|
-
import { generateContextStatements } from './context-utils';
|
|
4
|
-
import { insertJson } from './seed/json';
|
|
5
|
-
import { loadCsvMap } from './seed/csv';
|
|
6
|
-
import { loadSqlFiles } from './seed/sql';
|
|
7
|
-
import { deployPgpm } from './seed/pgpm';
|
|
8
|
-
export class PgTestClient {
|
|
9
|
-
config;
|
|
10
|
-
client;
|
|
11
|
-
opts;
|
|
12
|
-
ctxStmts = '';
|
|
13
|
-
contextSettings = {};
|
|
14
|
-
_ended = false;
|
|
15
|
-
connectPromise = null;
|
|
16
|
-
constructor(config, opts = {}) {
|
|
17
|
-
this.opts = opts;
|
|
18
|
-
this.config = config;
|
|
19
|
-
this.client = new Client({
|
|
20
|
-
host: this.config.host,
|
|
21
|
-
port: this.config.port,
|
|
22
|
-
database: this.config.database,
|
|
23
|
-
user: this.config.user,
|
|
24
|
-
password: this.config.password
|
|
25
|
-
});
|
|
26
|
-
if (!opts.deferConnect) {
|
|
27
|
-
this.connectPromise = this.client.connect();
|
|
28
|
-
if (opts.trackConnect)
|
|
29
|
-
opts.trackConnect(this.connectPromise);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async ensureConnected() {
|
|
33
|
-
if (this.connectPromise) {
|
|
34
|
-
try {
|
|
35
|
-
await this.connectPromise;
|
|
36
|
-
}
|
|
37
|
-
catch { }
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
async close() {
|
|
41
|
-
if (!this._ended) {
|
|
42
|
-
this._ended = true;
|
|
43
|
-
await this.ensureConnected();
|
|
44
|
-
this.client.end();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async begin() {
|
|
48
|
-
await this.client.query('BEGIN;');
|
|
49
|
-
}
|
|
50
|
-
async savepoint(name = 'lqlsavepoint') {
|
|
51
|
-
await this.client.query(`SAVEPOINT "${name}";`);
|
|
52
|
-
}
|
|
53
|
-
async rollback(name = 'lqlsavepoint') {
|
|
54
|
-
await this.client.query(`ROLLBACK TO SAVEPOINT "${name}";`);
|
|
55
|
-
}
|
|
56
|
-
async commit() {
|
|
57
|
-
await this.client.query('COMMIT;');
|
|
58
|
-
}
|
|
59
|
-
async beforeEach() {
|
|
60
|
-
await this.begin();
|
|
61
|
-
await this.savepoint();
|
|
62
|
-
}
|
|
63
|
-
async afterEach() {
|
|
64
|
-
await this.rollback();
|
|
65
|
-
await this.commit();
|
|
66
|
-
}
|
|
67
|
-
setContext(ctx) {
|
|
68
|
-
Object.assign(this.contextSettings, ctx);
|
|
69
|
-
this.ctxStmts = generateContextStatements(this.contextSettings);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Set authentication context for the current session.
|
|
73
|
-
* Configures role and user ID using cascading defaults from options โ opts.auth โ RoleMapping.
|
|
74
|
-
*/
|
|
75
|
-
auth(options = {}) {
|
|
76
|
-
const role = options.role ?? this.opts.auth?.role ?? getRoleName('authenticated', this.opts);
|
|
77
|
-
const userIdKey = options.userIdKey ?? this.opts.auth?.userIdKey ?? 'jwt.claims.user_id';
|
|
78
|
-
const userId = options.userId ?? this.opts.auth?.userId ?? null;
|
|
79
|
-
this.setContext({
|
|
80
|
-
role,
|
|
81
|
-
[userIdKey]: userId !== null ? String(userId) : null
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Commit current transaction to make data visible to other connections, then start fresh transaction.
|
|
86
|
-
* Maintains test isolation by creating a savepoint and reapplying session context.
|
|
87
|
-
*/
|
|
88
|
-
async publish() {
|
|
89
|
-
await this.commit(); // make data visible to other sessions
|
|
90
|
-
await this.begin(); // fresh tx
|
|
91
|
-
await this.savepoint(); // keep rollback harness
|
|
92
|
-
await this.ctxQuery(); // reapply all setContext()
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Clear all session context variables and reset to default anonymous role.
|
|
96
|
-
*/
|
|
97
|
-
clearContext() {
|
|
98
|
-
const defaultRole = getRoleName('anonymous', this.opts);
|
|
99
|
-
const nulledSettings = {};
|
|
100
|
-
Object.keys(this.contextSettings).forEach(key => {
|
|
101
|
-
nulledSettings[key] = null;
|
|
102
|
-
});
|
|
103
|
-
nulledSettings.role = defaultRole;
|
|
104
|
-
this.ctxStmts = generateContextStatements(nulledSettings);
|
|
105
|
-
this.contextSettings = { role: defaultRole };
|
|
106
|
-
}
|
|
107
|
-
async any(query, values) {
|
|
108
|
-
const result = await this.query(query, values);
|
|
109
|
-
return result.rows;
|
|
110
|
-
}
|
|
111
|
-
async one(query, values) {
|
|
112
|
-
const rows = await this.any(query, values);
|
|
113
|
-
if (rows.length !== 1) {
|
|
114
|
-
throw new Error('Expected exactly one result');
|
|
115
|
-
}
|
|
116
|
-
return rows[0];
|
|
117
|
-
}
|
|
118
|
-
async oneOrNone(query, values) {
|
|
119
|
-
const rows = await this.any(query, values);
|
|
120
|
-
return rows[0] || null;
|
|
121
|
-
}
|
|
122
|
-
async many(query, values) {
|
|
123
|
-
const rows = await this.any(query, values);
|
|
124
|
-
if (rows.length === 0)
|
|
125
|
-
throw new Error('Expected many rows, got none');
|
|
126
|
-
return rows;
|
|
127
|
-
}
|
|
128
|
-
async manyOrNone(query, values) {
|
|
129
|
-
return this.any(query, values);
|
|
130
|
-
}
|
|
131
|
-
async none(query, values) {
|
|
132
|
-
await this.query(query, values);
|
|
133
|
-
}
|
|
134
|
-
async result(query, values) {
|
|
135
|
-
return this.query(query, values);
|
|
136
|
-
}
|
|
137
|
-
async ctxQuery() {
|
|
138
|
-
if (this.ctxStmts) {
|
|
139
|
-
await this.client.query(this.ctxStmts);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// NOTE: all queries should call ctxQuery() before executing the query
|
|
143
|
-
async query(query, values) {
|
|
144
|
-
await this.ctxQuery();
|
|
145
|
-
const result = await this.client.query(query, values);
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
148
|
-
async loadJson(data) {
|
|
149
|
-
await this.ctxQuery();
|
|
150
|
-
await insertJson(this.client, data);
|
|
151
|
-
}
|
|
152
|
-
async loadSql(files) {
|
|
153
|
-
await this.ctxQuery();
|
|
154
|
-
await loadSqlFiles(this.client, files);
|
|
155
|
-
}
|
|
156
|
-
// NON-RLS load/seed methods:
|
|
157
|
-
async loadCsv(tables) {
|
|
158
|
-
// await this.ctxQuery(); // no point to call ctxQuery() here
|
|
159
|
-
// because POSTGRES doesn't support row-level security on COPY FROM...
|
|
160
|
-
await loadCsvMap(this.client, tables);
|
|
161
|
-
}
|
|
162
|
-
async loadPgpm(cwd, cache = false) {
|
|
163
|
-
// await this.ctxQuery(); // no point to call ctxQuery() here
|
|
164
|
-
// because deployPgpm() has it's own way of getting the client...
|
|
165
|
-
// so for now, we'll expose this but it's limited
|
|
166
|
-
await deployPgpm(this.config, cwd, cache);
|
|
167
|
-
}
|
|
168
|
-
}
|
package/esm/utils.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2
|
-
const idReplacement = (v, idHash) => {
|
|
3
|
-
if (!v)
|
|
4
|
-
return v;
|
|
5
|
-
if (!idHash)
|
|
6
|
-
return '[ID]';
|
|
7
|
-
const key = String(v);
|
|
8
|
-
return idHash[key] !== undefined ? `[ID-${idHash[key]}]` : '[ID]';
|
|
9
|
-
};
|
|
10
|
-
function mapValues(obj, fn) {
|
|
11
|
-
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
12
|
-
acc[key] = fn(value, key);
|
|
13
|
-
return acc;
|
|
14
|
-
}, {});
|
|
15
|
-
}
|
|
16
|
-
export const pruneDates = (row) => mapValues(row, (v, k) => {
|
|
17
|
-
if (!v) {
|
|
18
|
-
return v;
|
|
19
|
-
}
|
|
20
|
-
if (v instanceof Date) {
|
|
21
|
-
return '[DATE]';
|
|
22
|
-
}
|
|
23
|
-
else if (typeof v === 'string' &&
|
|
24
|
-
/(_at|At)$/.test(k) &&
|
|
25
|
-
/^20[0-9]{2}-[0-9]{2}-[0-9]{2}/.test(v)) {
|
|
26
|
-
return '[DATE]';
|
|
27
|
-
}
|
|
28
|
-
return v;
|
|
29
|
-
});
|
|
30
|
-
export const pruneIds = (row, idHash) => mapValues(row, (v, k) => (k === 'id' || (typeof k === 'string' && k.endsWith('_id'))) &&
|
|
31
|
-
(typeof v === 'string' || typeof v === 'number')
|
|
32
|
-
? idReplacement(v, idHash)
|
|
33
|
-
: v);
|
|
34
|
-
export const pruneIdArrays = (row) => mapValues(row, (v, k) => typeof k === 'string' && k.endsWith('_ids') && Array.isArray(v)
|
|
35
|
-
? `[UUIDs-${v.length}]`
|
|
36
|
-
: v);
|
|
37
|
-
export const pruneUUIDs = (row) => mapValues(row, (v, k) => {
|
|
38
|
-
if (typeof v !== 'string') {
|
|
39
|
-
return v;
|
|
40
|
-
}
|
|
41
|
-
if (['uuid', 'queue_name'].includes(k) && uuidRegexp.test(v)) {
|
|
42
|
-
return '[UUID]';
|
|
43
|
-
}
|
|
44
|
-
if (k === 'gravatar' && /^[0-9a-f]{32}$/i.test(v)) {
|
|
45
|
-
return '[gUUID]';
|
|
46
|
-
}
|
|
47
|
-
return v;
|
|
48
|
-
});
|
|
49
|
-
export const pruneHashes = (row) => mapValues(row, (v, k) => typeof k === 'string' &&
|
|
50
|
-
k.endsWith('_hash') &&
|
|
51
|
-
typeof v === 'string' &&
|
|
52
|
-
v.startsWith('$')
|
|
53
|
-
? '[hash]'
|
|
54
|
-
: v);
|
|
55
|
-
export const pruneSchemas = (row) => mapValues(row, (v, k) => typeof v === 'string' && /^zz-/.test(v) ? '[schemahash]' : v);
|
|
56
|
-
export const prunePeoplestamps = (row) => mapValues(row, (v, k) => k.endsWith('_by') && typeof v === 'string' ? '[peoplestamp]' : v);
|
|
57
|
-
export const pruneTokens = (row) => mapValues(row, (v, k) => (k === 'token' || k.endsWith('_token')) && typeof v === 'string'
|
|
58
|
-
? '[token]'
|
|
59
|
-
: v);
|
|
60
|
-
export const composePruners = (...pruners) => (row) => pruners.reduce((acc, pruner) => pruner(acc), row);
|
|
61
|
-
// Default pruners used by prune/snapshot (without IdHash)
|
|
62
|
-
export const defaultPruners = [
|
|
63
|
-
pruneTokens,
|
|
64
|
-
prunePeoplestamps,
|
|
65
|
-
pruneDates,
|
|
66
|
-
pruneIdArrays,
|
|
67
|
-
pruneUUIDs,
|
|
68
|
-
pruneHashes
|
|
69
|
-
];
|
|
70
|
-
// Compose pruners and apply pruneIds with IdHash support
|
|
71
|
-
export const prune = (row, idHash) => {
|
|
72
|
-
const pruned = composePruners(...defaultPruners)(row);
|
|
73
|
-
return pruneIds(pruned, idHash);
|
|
74
|
-
};
|
|
75
|
-
// Factory to create a snapshot function with custom pruners
|
|
76
|
-
export const createSnapshot = (pruners) => {
|
|
77
|
-
const pruneFn = composePruners(...pruners);
|
|
78
|
-
const snap = (obj, idHash) => {
|
|
79
|
-
if (Array.isArray(obj)) {
|
|
80
|
-
return obj.map((el) => snap(el, idHash));
|
|
81
|
-
}
|
|
82
|
-
else if (obj && typeof obj === 'object') {
|
|
83
|
-
const pruned = pruneFn(obj);
|
|
84
|
-
const prunedWithIds = pruneIds(pruned, idHash);
|
|
85
|
-
return mapValues(prunedWithIds, (v) => snap(v, idHash));
|
|
86
|
-
}
|
|
87
|
-
return obj;
|
|
88
|
-
};
|
|
89
|
-
return snap;
|
|
90
|
-
};
|
|
91
|
-
export const snapshot = createSnapshot(defaultPruners);
|
package/manager.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { Pool } from 'pg';
|
|
2
|
-
import { PgConfig } from 'pg-env';
|
|
3
|
-
import { PgTestClient, PgTestClientOpts } from './test-client';
|
|
4
|
-
export declare class PgTestConnector {
|
|
5
|
-
private static instance;
|
|
6
|
-
private readonly clients;
|
|
7
|
-
private readonly pgPools;
|
|
8
|
-
private readonly seenDbConfigs;
|
|
9
|
-
private readonly pendingConnects;
|
|
10
|
-
private config;
|
|
11
|
-
private verbose;
|
|
12
|
-
private shuttingDown;
|
|
13
|
-
private constructor();
|
|
14
|
-
static getInstance(config: PgConfig, verbose?: boolean): PgTestConnector;
|
|
15
|
-
private poolKey;
|
|
16
|
-
private dbKey;
|
|
17
|
-
beginTeardown(): void;
|
|
18
|
-
private registerConnect;
|
|
19
|
-
private awaitPendingConnects;
|
|
20
|
-
getPool(config: PgConfig): Pool;
|
|
21
|
-
getClient(config: PgConfig, opts?: Partial<PgTestClientOpts>): PgTestClient;
|
|
22
|
-
closeAll(): Promise<void>;
|
|
23
|
-
close(): void;
|
|
24
|
-
drop(config: PgConfig): void;
|
|
25
|
-
kill(client: PgTestClient): Promise<void>;
|
|
26
|
-
}
|
package/manager.js
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PgTestConnector = void 0;
|
|
4
|
-
const logger_1 = require("@pgpmjs/logger");
|
|
5
|
-
const pg_1 = require("pg");
|
|
6
|
-
const pg_env_1 = require("pg-env");
|
|
7
|
-
const admin_1 = require("./admin");
|
|
8
|
-
const test_client_1 = require("./test-client");
|
|
9
|
-
const log = new logger_1.Logger('test-connector');
|
|
10
|
-
const SYS_EVENTS = ['SIGTERM'];
|
|
11
|
-
const end = (pool) => {
|
|
12
|
-
try {
|
|
13
|
-
if (pool.ended || pool.ending) {
|
|
14
|
-
log.warn('โ ๏ธ pg pool already ended or ending');
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
pool.end();
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
log.error('โ pg pool termination error:', err);
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
class PgTestConnector {
|
|
24
|
-
static instance;
|
|
25
|
-
clients = new Set();
|
|
26
|
-
pgPools = new Map();
|
|
27
|
-
seenDbConfigs = new Map();
|
|
28
|
-
pendingConnects = new Set();
|
|
29
|
-
config;
|
|
30
|
-
verbose = false;
|
|
31
|
-
shuttingDown = false;
|
|
32
|
-
constructor(config, verbose = false) {
|
|
33
|
-
this.verbose = verbose;
|
|
34
|
-
this.config = config;
|
|
35
|
-
SYS_EVENTS.forEach((event) => {
|
|
36
|
-
process.on(event, () => {
|
|
37
|
-
log.info(`โน Received ${event}, closing all connections...`);
|
|
38
|
-
this.closeAll();
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
static getInstance(config, verbose = false) {
|
|
43
|
-
if (!PgTestConnector.instance) {
|
|
44
|
-
PgTestConnector.instance = new PgTestConnector(config, verbose);
|
|
45
|
-
}
|
|
46
|
-
return PgTestConnector.instance;
|
|
47
|
-
}
|
|
48
|
-
poolKey(config) {
|
|
49
|
-
return `${config.user}@${config.host}:${config.port}/${config.database}`;
|
|
50
|
-
}
|
|
51
|
-
dbKey(config) {
|
|
52
|
-
return `${config.host}:${config.port}/${config.database}`;
|
|
53
|
-
}
|
|
54
|
-
beginTeardown() {
|
|
55
|
-
this.shuttingDown = true;
|
|
56
|
-
}
|
|
57
|
-
registerConnect(p) {
|
|
58
|
-
this.pendingConnects.add(p);
|
|
59
|
-
p.finally(() => this.pendingConnects.delete(p));
|
|
60
|
-
}
|
|
61
|
-
async awaitPendingConnects() {
|
|
62
|
-
const arr = Array.from(this.pendingConnects);
|
|
63
|
-
if (arr.length) {
|
|
64
|
-
await Promise.allSettled(arr);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
getPool(config) {
|
|
68
|
-
const key = this.poolKey(config);
|
|
69
|
-
if (!this.pgPools.has(key)) {
|
|
70
|
-
const pool = new pg_1.Pool(config);
|
|
71
|
-
this.pgPools.set(key, pool);
|
|
72
|
-
log.info(`๐ Created new pg pool: ${key}`);
|
|
73
|
-
}
|
|
74
|
-
return this.pgPools.get(key);
|
|
75
|
-
}
|
|
76
|
-
getClient(config, opts = {}) {
|
|
77
|
-
if (this.shuttingDown) {
|
|
78
|
-
throw new Error('PgTestConnector is shutting down; no new clients allowed');
|
|
79
|
-
}
|
|
80
|
-
const client = new test_client_1.PgTestClient(config, {
|
|
81
|
-
trackConnect: (p) => this.registerConnect(p),
|
|
82
|
-
...opts
|
|
83
|
-
});
|
|
84
|
-
this.clients.add(client);
|
|
85
|
-
const key = this.dbKey(config);
|
|
86
|
-
this.seenDbConfigs.set(key, config);
|
|
87
|
-
log.info(`๐ New PgTestClient connected to ${config.database}`);
|
|
88
|
-
return client;
|
|
89
|
-
}
|
|
90
|
-
async closeAll() {
|
|
91
|
-
this.beginTeardown();
|
|
92
|
-
await this.awaitPendingConnects();
|
|
93
|
-
log.info('๐งน Closing all PgTestClients...');
|
|
94
|
-
await Promise.all(Array.from(this.clients).map(async (client) => {
|
|
95
|
-
try {
|
|
96
|
-
await client.close();
|
|
97
|
-
log.success(`โ
Closed client for ${client.config.database}`);
|
|
98
|
-
}
|
|
99
|
-
catch (err) {
|
|
100
|
-
log.error(`โ Error closing PgTestClient for ${client.config.database}:`, err);
|
|
101
|
-
}
|
|
102
|
-
}));
|
|
103
|
-
this.clients.clear();
|
|
104
|
-
log.info('๐งฏ Disposing pg pools...');
|
|
105
|
-
for (const [key, pool] of this.pgPools.entries()) {
|
|
106
|
-
log.debug(`๐งฏ Disposing pg pool [${key}]`);
|
|
107
|
-
end(pool);
|
|
108
|
-
}
|
|
109
|
-
this.pgPools.clear();
|
|
110
|
-
log.info('๐๏ธ Dropping seen databases...');
|
|
111
|
-
await Promise.all(Array.from(this.seenDbConfigs.values()).map(async (config) => {
|
|
112
|
-
try {
|
|
113
|
-
const rootPg = (0, pg_env_1.getPgEnvOptions)(this.config);
|
|
114
|
-
const admin = new admin_1.DbAdmin({ ...config, user: rootPg.user, password: rootPg.password }, this.verbose);
|
|
115
|
-
admin.drop();
|
|
116
|
-
log.warn(`๐งจ Dropped database: ${config.database}`);
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
log.error(`โ Failed to drop database ${config.database}:`, err);
|
|
120
|
-
}
|
|
121
|
-
}));
|
|
122
|
-
this.seenDbConfigs.clear();
|
|
123
|
-
log.success('โ
All PgTestClients closed, pools disposed, databases dropped.');
|
|
124
|
-
this.pendingConnects.clear();
|
|
125
|
-
this.shuttingDown = false;
|
|
126
|
-
}
|
|
127
|
-
close() {
|
|
128
|
-
this.closeAll();
|
|
129
|
-
}
|
|
130
|
-
drop(config) {
|
|
131
|
-
const key = this.dbKey(config);
|
|
132
|
-
const admin = new admin_1.DbAdmin(config, this.verbose);
|
|
133
|
-
admin.drop();
|
|
134
|
-
log.warn(`๐งจ Dropped database: ${config.database}`);
|
|
135
|
-
this.seenDbConfigs.delete(key);
|
|
136
|
-
}
|
|
137
|
-
async kill(client) {
|
|
138
|
-
await client.close();
|
|
139
|
-
this.drop(client.config);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
exports.PgTestConnector = PgTestConnector;
|
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/seed/adapters.d.ts
DELETED
package/seed/adapters.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.sqlfile = sqlfile;
|
|
4
|
-
exports.fn = fn;
|
|
5
|
-
exports.compose = compose;
|
|
6
|
-
function sqlfile(files) {
|
|
7
|
-
return {
|
|
8
|
-
seed(ctx) {
|
|
9
|
-
for (const file of files) {
|
|
10
|
-
ctx.admin.loadSql(file, ctx.config.database);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
function fn(fn) {
|
|
16
|
-
return {
|
|
17
|
-
seed: fn
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
function compose(adapters) {
|
|
21
|
-
return {
|
|
22
|
-
async seed(ctx) {
|
|
23
|
-
for (const adapter of adapters) {
|
|
24
|
-
await adapter.seed(ctx);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
}
|
package/seed/csv.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Client } from 'pg';
|
|
2
|
-
import type { PgTestClient } from '../test-client';
|
|
3
|
-
import { SeedAdapter } from './types';
|
|
4
|
-
export interface CsvSeedMap {
|
|
5
|
-
[tableName: string]: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Standalone helper function to load CSV files into PostgreSQL tables
|
|
9
|
-
* @param client - PostgreSQL client instance
|
|
10
|
-
* @param tables - Map of table names to CSV file paths
|
|
11
|
-
*/
|
|
12
|
-
export declare function loadCsvMap(client: Client, tables: CsvSeedMap): Promise<void>;
|
|
13
|
-
export declare function csv(tables: CsvSeedMap): SeedAdapter;
|
|
14
|
-
export declare function copyCsvIntoTable(pg: PgTestClient, table: string, filePath: string): Promise<void>;
|
|
15
|
-
export declare function exportTableToCsv(pg: PgTestClient, table: string, filePath: string): Promise<void>;
|