@hirosystems/api-toolkit
Version:
API development toolkit
134 lines • 5.38 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.runMigrations = runMigrations;
exports.cycleMigrations = cycleMigrations;
exports.databaseHasData = databaseHasData;
exports.dangerousDropAllTables = dangerousDropAllTables;
const node_pg_migrate_1 = require("node-pg-migrate");
const logger_1 = require("../logger");
const connection_1 = require("./connection");
const values_1 = require("../helpers/values");
/**
* Run migrations in one direction.
* @param dir - Migrations directory
* @param direction - Migration direction (`'down'` or `'up'`)
* @param connectionArgs - Postgres connection args
* @param opts - Migration options
*/
async function runMigrations(dir, direction, connectionArgs, opts) {
if (!opts?.dangerousAllowDataLoss && direction !== 'up' && !values_1.isTestEnv && !values_1.isDevEnv) {
throw new Error('Whoa there! This is a testing function that will drop all data from PG. ' +
'Set NODE_ENV to "test" or "development" to enable migration testing.');
}
const args = (0, connection_1.standardizedConnectionArgs)(connectionArgs, 'migrations');
await (0, node_pg_migrate_1.default)({
dir,
direction,
count: Infinity,
ignorePattern: '.*map',
databaseUrl: typeof args === 'string'
? args
: {
host: args.host,
port: args.port,
user: args.user,
password: args.password,
database: args.database,
},
migrationsTable: opts?.migrationsTable ?? 'pgmigrations',
schema: typeof args === 'string' ? 'public' : args.schema,
logger: opts?.logger ?? {
info: msg => (opts?.logMigrations === true ? logger_1.logger.info(msg) : {}),
warn: msg => logger_1.logger.warn(msg),
error: msg => logger_1.logger.error(msg),
},
});
}
/**
* Cycle migrations down and up.
* @param dir - Migrations directory
* @param connectionArgs - Postgres connection args
* @param opts - Migration options
*/
async function cycleMigrations(dir, connectionArgs, opts) {
await runMigrations(dir, 'down', connectionArgs, opts);
if (opts?.checkForEmptyData &&
(await databaseHasData(connectionArgs, {
ignoreMigrationTables: true,
migrationsTable: opts.migrationsTable,
}))) {
throw new Error('Migration down process did not completely remove DB tables');
}
await runMigrations(dir, 'up', connectionArgs, opts);
}
/**
* Check the `pg_class` table for any data structures contained in the database. We will consider
* any and all results here as "data" contained in the DB, since anything that is not a completely
* empty DB could lead to strange errors when running the API. See:
* https://www.postgresql.org/docs/current/catalog-pg-class.html
* @returns `boolean` if the DB has data
*/
async function databaseHasData(connectionArgs, opts) {
const sql = await (0, connection_1.connectPostgres)({
usageName: 'contains-data-check',
connectionArgs: (0, connection_1.standardizedConnectionArgs)(connectionArgs, 'contains-data-check'),
});
try {
const ignoreMigrationTables = opts?.ignoreMigrationTables ?? false;
const tableName = opts?.migrationsTable ?? 'pgmigrations';
const result = await sql `
SELECT COUNT(*)
FROM pg_class c
JOIN pg_namespace s ON s.oid = c.relnamespace
WHERE s.nspname = ${sql.options.connection.search_path}
${ignoreMigrationTables ? sql `AND c.relname NOT LIKE ${tableName}::text || '%'` : sql ``}
`;
return result.count > 0 && result[0].count > 0;
}
catch (error) {
if (error.message?.includes('does not exist')) {
return false;
}
throw error;
}
finally {
await sql.end();
}
}
/**
* Drops all tables from the Postgres DB. DANGEROUS!!!
*/
async function dangerousDropAllTables(connectionArgs, opts) {
if (opts?.acknowledgePotentialCatastrophicConsequences !== 'yes') {
throw new Error('Dangerous usage error.');
}
const sql = await (0, connection_1.connectPostgres)({
usageName: 'dangerous-drop-all-tables',
connectionArgs: (0, connection_1.standardizedConnectionArgs)(connectionArgs, 'dangerous-drop-all-tables'),
});
const schema = sql.options.connection.search_path;
try {
await sql.begin(async (sql) => {
const relNamesQuery = async (kind) => sql `
SELECT relname
FROM pg_class c
JOIN pg_namespace s ON s.oid = c.relnamespace
WHERE s.nspname = ${schema} AND c.relkind = ${kind}
`;
// Remove materialized views first and tables second.
// Using CASCADE in these DROP statements also removes associated indexes and constraints.
const views = await relNamesQuery('m');
for (const view of views) {
await sql `DROP MATERIALIZED VIEW IF EXISTS ${sql(view.relname)} CASCADE`;
}
const tables = await relNamesQuery('r');
for (const table of tables) {
await sql `DROP TABLE IF EXISTS ${sql(table.relname)} CASCADE`;
}
});
}
finally {
await sql.end();
}
}
//# sourceMappingURL=migrations.js.map