UNPKG

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
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); } }