UNPKG

@sqb/migrator

Version:

Database migrator for SQB

230 lines (229 loc) 8.29 kB
import { PgAdapter } from '@sqb/postgres'; import path from 'path'; import { stringifyValueForSQL } from 'postgrejs'; import { MigrationAdapter } from '../migration-adapter.js'; import { isCustomMigrationTask, isInsertDataMigrationTask, isSqlScriptMigrationTask, } from '../migration-package.js'; import { MigrationStatus } from '../types.js'; const pgAdapter = new PgAdapter(); export class PgMigrationAdapter extends MigrationAdapter { _infoSchema = 'public'; _version = 0; _status = MigrationStatus.idle; defaultVariables = { tablespace: 'pg_default', schema: 'public', owner: 'postgres', }; summaryTable = 'migration_summary'; eventTable = 'migration_events'; get packageName() { return this._migrationPackage.name; } get version() { return this._version; } get status() { return this._status; } get infoSchema() { return this._infoSchema; } get summaryTableFull() { return this.infoSchema + '.' + this.summaryTable; } get eventTableFull() { return this.infoSchema + '.' + this.eventTable; } static async create(options) { // Create connection const connection = (await pgAdapter.connect(options.connection)) .intlcon; try { const adapter = new PgMigrationAdapter(); adapter._connection = connection; adapter._migrationPackage = options.migrationPackage; adapter._infoSchema = options.infoSchema || '__migration'; adapter.defaultVariables.schema = options.connection.schema || ''; if (!adapter.defaultVariables.schema) { const r = await connection.query('SELECT CURRENT_SCHEMA ', { objectRows: true, }); adapter.defaultVariables.schema = r.rows?.[0]?.current_schema || 'public'; } // Check if migration schema await connection.query(`CREATE SCHEMA IF NOT EXISTS ${adapter.infoSchema} AUTHORIZATION postgres;`); // Create summary table if not exists await connection.execute(` CREATE TABLE IF NOT EXISTS ${adapter.summaryTableFull} ( package_name varchar not null, status varchar(16) not null, current_version integer not null default 0, created_at timestamp without time zone not null default current_timestamp, updated_at timestamp without time zone, CONSTRAINT pk_${adapter.summaryTable} PRIMARY KEY (package_name) )`); // Create events table if not exists await connection.execute(` CREATE TABLE IF NOT EXISTS ${adapter.eventTableFull} ( id serial not null, package_name varchar not null, version integer not null default 0, event varchar(16) not null, event_time timestamp without time zone not null, title text, message text not null, filename text, details text, CONSTRAINT pk_${adapter.eventTable} PRIMARY KEY (id) )`); // Insert summary record if not exists const r = await connection.query(`SELECT status FROM ${adapter.summaryTableFull} WHERE package_name = $1`, { params: [adapter.packageName], objectRows: true, }); if (!(r && r.rows?.length)) { await connection.query(`insert into ${adapter.summaryTableFull} (package_name, status) values ($1, $2)`, { params: [adapter.packageName, MigrationStatus.idle], }); } await adapter.refresh(); return adapter; } catch (e) { await connection.close(0); throw e; } } async close() { await this._connection.close(); } async refresh() { const r = await this._connection.query(`SELECT * FROM ${this.summaryTableFull} WHERE package_name = $1`, { params: [this.packageName], objectRows: true, }); const row = r.rows && r.rows[0]; if (!row) throw new Error('Summary record did not created'); this._version = row.current_version; this._status = row.status; } async update(info) { let sql = ''; const params = []; if (info.status && info.status !== this.status) { params.push(info.status); sql += ',\n status = $' + params.length; } if (info.version && info.version !== this.version) { params.push(info.version); sql += ',\n current_version = $' + params.length; } if (sql) { params.push(this.packageName); sql = `update ${this.summaryTableFull} set updated_at = current_timestamp` + sql + `\n where package_name =$` + params.length; await this._connection.query(sql, { params }); if (info.status) this._status = info.status; if (info.version) this._version = info.version; } } async writeEvent(event) { const sql = `insert into ${this.eventTableFull} ` + '(package_name, version, event, event_time, title, message, filename, details) ' + 'values ($1, $2, $3, CURRENT_TIMESTAMP, $4, $5, $6, $7)'; await this._connection.query(sql, { params: [ this.packageName, event.version, event.event, event.title, event.message, event.filename, event.details, ], }); } async executeTask(migrationPackage, migration, task, variables) { variables = { ...this.defaultVariables, ...variables, }; if (isSqlScriptMigrationTask(task)) { try { let script; if (typeof task.script === 'function') { script = await task.script({ migrationPackage, migration, task, variables, }); } else script = task.script; if (typeof script !== 'string') return; script = this.replaceVariables(script, variables); await this._connection.execute(script); } catch (e) { let msg = `Error in task "${task.title}"`; if (task.filename) msg += '\n at ' + path.relative(migrationPackage.baseDir, task.filename); if (e.lineNr) { if (!task.filename) e.message += '\n at'; msg += ` (${e.lineNr},${e.colNr}):\n` + e.line; if (e.colNr) msg += '\n' + ' '.repeat(e.colNr - 1) + '^'; } e.message = msg + '\n\n' + e.message; throw e; } return; } if (isCustomMigrationTask(task)) { await task.fn(this._connection, this); return; } if (isInsertDataMigrationTask(task)) { const tableName = this.replaceVariables(task.tableName, variables); const script = task.rows .map(row => this.rowToSql(tableName, row)) .join('\n'); await this._connection.execute(script); } } backupDatabase() { return Promise.resolve(undefined); } lockSchema() { return Promise.resolve(undefined); } restoreDatabase() { return Promise.resolve(undefined); } unlockSchema() { return Promise.resolve(undefined); } rowToSql(tableName, row) { let sql = ''; const keys = Object.keys(row); sql += `insert into ${tableName} (${keys}) values (`; for (let i = 0; i < keys.length; i++) { sql += (i ? ', ' : '') + stringifyValueForSQL(row[keys[i]]); } sql += ');\n'; return sql; } }