pgsql-test
Version:
pgsql-test offers isolated, role-aware, and rollback-friendly PostgreSQL environments for integration tests — giving developers realistic test coverage without external state pollution
141 lines (140 loc) • 4.83 kB
JavaScript
import { Logger } from '@launchql/logger';
import { execSync } from 'child_process';
import { existsSync } from 'fs';
import { getPgEnvOptions } from 'pg-env';
import { streamSql as stream } from './stream';
const log = new Logger('db-admin');
export class DbAdmin {
config;
verbose;
constructor(config, verbose = false) {
this.config = config;
this.verbose = verbose;
this.config = getPgEnvOptions(config);
}
getEnv() {
return {
PGHOST: this.config.host,
PGPORT: String(this.config.port),
PGUSER: this.config.user,
PGPASSWORD: this.config.password
};
}
run(command) {
try {
execSync(command, {
stdio: this.verbose ? 'inherit' : 'pipe',
env: {
...process.env,
...this.getEnv()
}
});
if (this.verbose)
log.success(`Executed: ${command}`);
}
catch (err) {
log.error(`Command failed: ${command}`);
if (this.verbose)
log.error(err.message);
throw err;
}
}
safeDropDb(name) {
try {
this.run(`dropdb "${name}"`);
}
catch (err) {
if (!err.message.includes('does not exist')) {
log.warn(`Could not drop database ${name}: ${err.message}`);
}
}
}
drop(dbName) {
this.safeDropDb(dbName ?? this.config.database);
}
dropTemplate(dbName) {
this.run(`psql -c "UPDATE pg_database SET datistemplate='false' WHERE datname='${dbName}';"`);
this.drop(dbName);
}
create(dbName) {
const db = dbName ?? this.config.database;
this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} "${db}"`);
}
createFromTemplate(template, dbName) {
const db = dbName ?? this.config.database;
this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} -e "${db}" -T "${template}"`);
}
installExtensions(extensions, dbName) {
const db = dbName ?? this.config.database;
const extList = typeof extensions === 'string' ? extensions.split(',') : extensions;
for (const extension of extList) {
this.run(`psql --dbname "${db}" -c 'CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;'`);
}
}
connectionString(dbName) {
const { user, password, host, port } = this.config;
const db = dbName ?? this.config.database;
return `postgres://${user}:${password}@${host}:${port}/${db}`;
}
createTemplateFromBase(base, template) {
this.run(`createdb -T "${base}" "${template}"`);
this.run(`psql -c "UPDATE pg_database SET datistemplate = true WHERE datname = '${template}';"`);
}
cleanupTemplate(template) {
try {
this.run(`psql -c "UPDATE pg_database SET datistemplate = false WHERE datname = '${template}'"`);
}
catch {
log.warn(`Skipping failed UPDATE of datistemplate for ${template}`);
}
this.safeDropDb(template);
}
async grantRole(role, user, dbName) {
const db = dbName ?? this.config.database;
const sql = `GRANT ${role} TO ${user};`;
await this.streamSql(sql, db);
}
async grantConnect(role, dbName) {
const db = dbName ?? this.config.database;
const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`;
await this.streamSql(sql, db);
}
async createUserRole(user, password, dbName) {
const sql = `
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${user}') THEN
CREATE ROLE ${user} LOGIN PASSWORD '${password}';
GRANT anonymous TO ${user};
GRANT authenticated TO ${user};
END IF;
END $$;
`.trim();
await this.streamSql(sql, dbName);
}
loadSql(file, dbName) {
if (!existsSync(file)) {
throw new Error(`Missing SQL file: ${file}`);
}
this.run(`psql -f ${file} ${dbName}`);
}
async streamSql(sql, dbName) {
await stream({
...this.config,
database: dbName
}, sql);
}
async createSeededTemplate(templateName, adapter) {
const seedDb = this.config.database;
this.create(seedDb);
await adapter.seed({
admin: this,
config: this.config,
pg: null, // placeholder for PgTestClient
connect: null // placeholder for connection factory
});
this.cleanupTemplate(templateName);
this.createTemplateFromBase(seedDb, templateName);
this.drop(seedDb);
}
}