UNPKG

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
"use strict"; 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;