supabase-test
Version:
supabase-test offers isolated, role-aware, and rollback-friendly PostgreSQL environments for integration tests — giving developers realistic test coverage without external state pollution
183 lines (179 loc) • 6.67 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DbAdmin = void 0;
const logger_1 = require("@launchql/logger");
const child_process_1 = require("child_process");
const fs_1 = require("fs");
const pg_env_1 = require("pg-env");
const roles_1 = require("./roles");
const stream_1 = require("./stream");
const log = new logger_1.Logger('db-admin');
class DbAdmin {
config;
verbose;
roleConfig;
constructor(config, verbose = false, roleConfig) {
this.config = config;
this.verbose = verbose;
this.roleConfig = roleConfig;
this.config = (0, pg_env_1.getPgEnvOptions)(config);
}
getEnv() {
return {
PGHOST: this.config.host,
PGPORT: String(this.config.port),
PGUSER: this.config.user,
PGPASSWORD: this.config.password
};
}
run(command) {
try {
(0, child_process_1.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);
}
// TODO: make adminRole a configurable option
// ONLY granting admin role for testing purposes, normally the db connection for apps won't have admin role
// DO NOT USE THIS FOR PRODUCTION
async createUserRole(user, password, dbName) {
const anonRole = (0, roles_1.getRoleName)('anonymous', this.roleConfig);
const authRole = (0, roles_1.getRoleName)('authenticated', this.roleConfig);
const adminRole = (0, roles_1.getRoleName)('administrator', this.roleConfig);
const sql = `
DO $$
BEGIN
-- Create role if it doesn't exist
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${user}') THEN
CREATE ROLE ${user} LOGIN PASSWORD '${password}';
END IF;
-- Grant anonymous role if not already granted
IF NOT EXISTS (
SELECT 1 FROM pg_auth_members am
JOIN pg_roles r1 ON am.roleid = r1.oid
JOIN pg_roles r2 ON am.member = r2.oid
WHERE r1.rolname = '${anonRole}' AND r2.rolname = '${user}'
) THEN
GRANT ${anonRole} TO ${user};
END IF;
-- Grant authenticated role if not already granted
IF NOT EXISTS (
SELECT 1 FROM pg_auth_members am
JOIN pg_roles r1 ON am.roleid = r1.oid
JOIN pg_roles r2 ON am.member = r2.oid
WHERE r1.rolname = '${authRole}' AND r2.rolname = '${user}'
) THEN
GRANT ${authRole} TO ${user};
END IF;
-- Grant administrator role if not already granted
IF NOT EXISTS (
SELECT 1 FROM pg_auth_members am
JOIN pg_roles r1 ON am.roleid = r1.oid
JOIN pg_roles r2 ON am.member = r2.oid
WHERE r1.rolname = '${adminRole}' AND r2.rolname = '${user}'
) THEN
GRANT ${adminRole} TO ${user};
END IF;
END $$;
`.trim();
await this.streamSql(sql, dbName);
}
loadSql(file, dbName) {
if (!(0, fs_1.existsSync)(file)) {
throw new Error(`Missing SQL file: ${file}`);
}
this.run(`psql -f ${file} ${dbName}`);
}
async streamSql(sql, dbName) {
await (0, stream_1.streamSql)({
...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);
}
}
exports.DbAdmin = DbAdmin;