@kwaeri/mysql-migrator
Version:
The @kwaeri/migration component of the @kwaer/cli user-executable framework.
745 lines • 37 kB
JavaScript
/**
* SPDX-PackageName: kwaeri/mysql-migrator
* SPDX-PackageVersion: 0.7.0
* SPDX-FileCopyrightText: © 2014 - 2022 Richard Winters <kirvedx@gmail.com> and contributors
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception OR MIT
*/
;
// INCLUDES
import { kdt } from '@kwaeri/developer-tools';
import { MigratorServiceProvider } from '@kwaeri/migrator';
import { Filesystem } from '@kwaeri/filesystem';
import { Configuration } from '@kwaeri/configuration';
import { MySQLDriver } from '@kwaeri/mysql-database-driver';
import { Console } from '@kwaeri/console';
import debug from 'debug';
// DEFINES
const _ = new kdt(), output = new Console({ color: false, background: false, decor: [] });
/* Configure Debug module support */
const DEBUG = debug('kue:mysql-migrator');
export const PARAMATERIZATION = {
TYPE: {
"mysql": true
},
LANG: {
'javascript': true
},
EXT: {
'javascript': 'js'
}
};
const DEFAULT_GENERATOR_OPTIONS = {
environment: "default",
quest: "migrate",
specification: "",
version: "",
args: {
// "step-back": "5",
},
subCommands: [],
stepBack: 0,
configuration: {
project: {
name: "",
type: "",
tech: "",
root: ".",
author: {
first: "",
last: "",
fullName: "",
email: ""
},
copyright: "",
copyrightEmail: "",
license: {
identifier: "",
},
repository: ""
}
}
};
/**
* The Migrator Class
*
* The { Migrator } class handles everything that has to do with the migration
* system, including; Setup, Management, and Execution of migrations.
*/
export class MysqlMigrator extends MigratorServiceProvider {
/**
* @var { string }
*/
path = 'data/migrations';
/**
* @var { string }
*/
confPath = 'conf';
/**
* @var { string }
*/
file = 'migrations.json';
/**
* @var {string }
*/
version = '0.1.10';
/**
* @var { string }
*/
table = 'nodekit_migrations';
/**
* @var { Date }
*/
start;
/**
* @var { Date }
*/
end;
/**
* @var { Date }
*/
processStart;
/**
* @var { Date }
*/
processEnd;
/**
* @var { Configuration }
*/
configuration;
/**
* @var { any }
*/
conf;
/**
* @var { Configuration }
*/
databaseConfiguration;
/**
* @var { any }
*/
databaseConf;
/**
* @var { string }
*/
databaseType = 'mysql';
/**
* @var
*/
databaseProvider = null;
/**
* @var { DatabaseDriver }
*/
dbo;
/**
* Class constructor
*/
constructor(handler, configuration) {
super(handler);
// Organize all of the uncertainty:
this.version = (configuration && configuration.version) ? configuration.version : "0.1.10",
this.databaseType = (configuration && configuration.type) ? configuration.type : "mysql",
this.table = (configuration && configuration.table) ? configuration.table : "nodekit_migrations";
// Get the current environment:
const environment = (configuration && configuration.environment) ? configuration.environment : "default";
// Instantiate a new configuration object to wrap the migrations configuration:
this.configuration = new Configuration("conf", "migrations.json", { plainText: false, type: "migrations" });
// Grab the database configuration pre-emptively as well, if it exists:
this.databaseConfiguration = new Configuration('conf', `database.${environment}.json`, { plainText: false, type: 'database' });
// Store the databse provider type:
this.databaseProvider = (configuration && configuration.databaseProvider) ? configuration.databaseProvider : MySQLDriver;
}
getServiceProviderSubscriptions(options) {
return {
commands: {
"migrations": false // Specifications for the 'add' command, false if there are none
},
required: {
"migrations": {}
},
optional: {
"migrations": {
"step-back": {
"for": false, // Or false, if it's not related to an option/value, rather only to the specification/command.
"flag": false, // True insists that no value is given. Its existance equates to <option>=1, the lack of its
"values": null // existence is similar to <option>=0.
},
}
}
};
}
getServiceProviderSubscriptionHelpText(options) {
return {
helpText: {
"commands": {
"migrations": {
"description": "The 'migrations' command automates the handling of migrations for an existing project.",
"specifications": { // ⇦ For the specifications
//"example-spec": {
// "description": "",
// "options": {
// "required": { // ⇦ Required options are specific to the specification
// "example-option": { // ⇦ List options
// "description": "",
// "values": []
// }
// },
// "optional": { // ⇦ Optional options can be
// "specification": { // ⇦ For the specification
// "example-option": { // ⇦ List options
// "description": "",
// "values": []
// }
// }, // ⇦ For the various required options that allow optional flags
// "required-option": { // ⇦ For a required option
// "required-value": { // ⇦ For the required option's value, can be 'any'
// "example-option": { // ⇦ List options
// "description": "",
// "values": false
// }
// },
// }
// }
// }
//}
},
"options": {
"optional": {
"command": {
"step-back": {
"description": "Specify the number of migrations to undo from those that are applied.",
"values": null
}
},
//"optional-option": { // ⇦ For the optional options of the command
// "optional-value": { // ⇦ For the optional options value, can be 'any'
// "example-option": { // ⇦ List options
// "description": "",
// "values": []
// }
// }
//}
}
}
}
}
}
};
}
/**
* Method to resettle the { NodeKitProjectGeneratorOptions }. Essentially we
* merge NodeKitOptions with FilesystemDescriptor by combining provided
* command options with either a stored configuration or sane default.
*
* @param { NodeKitOptions } options
*
* @returns { NodeKitOptions } The options object, with the configuration partially populated with user-provided information
*/
async assembleOptions(options) {
// Setting some defaults
let returnable, nko, nkcb, nkpb, nkab;
//returnable = ( _.extend( defaults, returnable ) as NodeKitProjectGeneratorOptions );
returnable = _.extend(options, DEFAULT_GENERATOR_OPTIONS);
DEBUG(`[ASSEMBLE_OPTIONS] Sanitize settings`);
//returnable.name = ( returnable.subCommands[0] && returnable.subCommands[0] !== "" ) ? returnable.subCommands[0] : "MyNewMySQLMigration";
//returnable.fileSafeName = Filesystem.getFileSafeName( returnable.name );
//returnable.type = ( returnable.args.type in PARAMATERIZATION.TYPE ) ? returnable.args.type : 'mysql';
returnable.stepBack = (returnable.args['step-back']) ? returnable.args['step-back'] : 0;
DEBUG(`[ASSEMBLE_OPTIONS] Get configuration`);
//const { confExists, projectConf } = await this.getConfiguration();
//returnable.configuration = projectConf
//if( confExists )
//{
// DEBUG( `ASSEMBLE_OPTIONS] Inject 'fileSafeName'to stored project configuration.` );
// returnable.configuration.project.fileSafeName = Filesystem.getFileSafeName( returnable.configuration.project.name );
// returnable.configuration.project.root = ( projectConf.project.root === "" || projectConf.project.root === "." ) ? `${Filesystem.getPathToCWD()}` : projectConf.project.root;
//}
//else
//{
// DEBUG( `ASSEMBLE_OPTIONS] Set project configuration 'tech' to a sane default.` );
// returnable.configuration.project.tech = ( returnable.args.lang in PARAMATERIZATION.LANG ) ? Object.keys( PARAMATERIZATION.LANG )[returnable.args.lang] : "javascript";
//}
//returnable.extension = PARAMATERIZATION.EXT[returnable.configuration.project.tech];
//returnable.path = returnable.configuration.project.root;
return Promise.resolve(returnable);
}
/**
* A method which asynchronously executes the necessary steps for creating
* a migration within a project file structure
*
* @param { NodeKitOptions } options An object which specifies parameters for this method
*
* @return { Promise<any> }
*/
async renderService(options) {
try {
this.updateProgress('MYSQL_MIGRATOR_RENDER_SERVICE', {
progressLevel: 0,
notice: `Preparing to ${(options.args.stepBack) ? (options.args.stepBack > 0) ? `revert ${options.args.stepBack}` : `revert` : `apply`}' migrations`
});
DEBUG(`[RENDER_MIGRATION_GENERATOR_SERVICE] Resettle options`);
options = await this.assembleOptions(options);
DEBUG(`[RENDER_MIGRATION_GENERATOR_SERVICE] Resolve 'runCreateMySQLMigrationRoutine()'`);
const { result } = await this.migrate(options);
this.updateProgress("MYSQL_MIGRATOR_RENDER_SERVICE", { progressLevel: -1 });
return Promise.resolve({ result, type: "run_migrations" });
}
catch (error) {
return Promise.reject(new Error(`[RENDER_MIGRATION_GENERATOR_SERVICE]: ${error}`));
}
}
/**
* Checks if the migration system has been installed
*
* @pram { void }
*
* @return { Promise<undefined|null> } A promise with a boolean value indicating whether the migration system is installed.
*/
async checkInstall() {
// Check that both the filesystem and database portions of the
// migration system are installed, if not - return null
if ((await this.checkFilesystemInstall()) === null || (await this.checkDatabaseInstall()) !== undefined)
return Promise.resolve(null);
// Otherwise, pass the check
return Promise.resolve(undefined);
}
/**
* Checks the actual database portion of the migration system is installed
*
* @returns { Promise<undefined } If the check passes
*/
async checkDatabaseInstall() {
// Get essentials for the check:
const conf = await this.getConf(true);
const dbo = await this.getDbo();
// Run a query to determine if the migrations table exists::
const checkMigrationsDatabaseResult = await dbo
//
// information_schema queries are handled differently prior to MySQL 8;
// If you do not capitalize the column name in the select clause, or set
// a lowercase alias for it - you will have inconsistent use of
// capitalization across the different MySQL versions. The simplest
// solution is to capitalize the column name (i.e. TABLE_NAME) so that
// the column name is capitalized in the result when querying both MySQL
// 5 and MySQL 8 (which is the default behavior in 8, regardless of
// select case).
//
// Alternatively, you can alias (i.e. TABLE_NAME as table_name) to force
// MySQL 8 and MySQL 5 to both use lowercase for a column name:
//
// https://stackoverflow.com/a/54541147/2041005
//
.query(`SELECT TABLE_NAME FROM information_schema.tables ` +
`WHERE table_schema = '${conf.database}' ` +
`AND table_name = '${this.table}';`);
DEBUG(checkMigrationsDatabaseResult);
DEBUG(checkMigrationsDatabaseResult.rows);
// Check if there was some sort of error with our query:
if (!checkMigrationsDatabaseResult || !checkMigrationsDatabaseResult.rows || _.empty(checkMigrationsDatabaseResult.rows))
return Promise.reject(new Error(`[MIGRATOR][CHECK_INSTALL] Error checking for the migrations table. If the migrations table does not exist, delete '${this.file}' in '${this.confPath}' and generate a new migration ('kue add migration --type mysql <MigrationName>') to force install the migrations system.`));
// Check for TABLE_NAME; if the table was specified in lower case in the
// query, but unaliased, we'd need to check for both uppercase and lower
// here to ensure the check did not fail.
if ((checkMigrationsDatabaseResult.rows?.[0]?.TABLE_NAME !== this.table))
return Promise.resolve(null);
// And return undefined indicating
return Promise.resolve(undefined);
}
/**
* Checks the actual filesystem portion of the migration system is installed
*
* @returns {Promise<undefined|null>}
*/
async checkFilesystemInstall() {
// If not ⇨ return false (It will generate the migration configuration):
if (!(await this.getConf()))
return Promise.resolve(null);
// Otherwise, return undefined if there were no issues
return Promise.resolve(undefined);
}
/**
* Gets the migration or database configuration
*
* @param { boolean } database True to get the database configuration, false to get the migration configuration
*
* @returns { Configuration } A {@link Configuration} object
*/
async getConf(database = false) {
const conf = database ? "databaseConf" : "conf";
//try {
if (this[conf] === undefined)
this[conf] = database ? await this.databaseConfiguration.get() : await this.configuration.get();
// IF there's no migration conf, return null
//if( ( !this[conf] || !this[conf].success ) ) ⇦ Updated configuration implementation
if (!this[conf])
if (conf === 'conf')
return Promise.resolve(null);
else
return Promise.reject(new Error(`[MYSQL_MIGRATOR][GET_CONF] There was an issue reading the ${(database ? `database ` : ``)}configuration. `));
//}
//catch( error ) {
// return Promise.reject( error );
//}
DEBUG(`'${conf}' configuration successfully fetched:`);
DEBUG(this[conf]);
// Otherwise, return the conf
//return Promise.resolve( this[conf].configuration ); ⇦ Updated configuration implementation
return Promise.resolve(this[conf]);
}
/**
* Get the DBO; If `undefined` then attempt to create it, otherwise return
* the existing dbo or resolve `null` in the event that this method
* fails.
*
* @returns A new {@link DatabaseDriver} object
*/
async getDbo() {
try {
// Get a database object::
if (!this.dbo) {
const dbo = new this.databaseProvider(await this.getConf(true)); //this.dbo = new Driver( await this.getConf( true ), this.databaseProvider ).get();
if (!dbo)
return Promise.reject(new Error(`There was an issue getting the Database Provider`));
this.dbo = dbo;
}
return Promise.resolve(this.dbo);
}
catch (error) {
DEBUG(`${error}`);
return Promise.reject(`${error}`);
}
}
/**
* Installs the migration system by calling the component install routines
*
* @param { string } content The migration configuration file content
*
* @return { Promise<T> } A promise that indicates the result of installing the migration system
*/
async install(content) {
// Call the component install routine methods
if (await this.installFilesystemComponent(content) === undefined &&
await this.installDatabaseComponent() === undefined)
return Promise.resolve(undefined);
// Return a resolved promise:denoting a false result if any
// component install routine fails without throwing
return Promise.resolve(null);
}
/**
* Handles the migration's filesystem portion of the install
*
* @param { string } content The migration configuration file content
*
* @returns { Promise<undefined> }
*/
async installFilesystemComponent(content) {
// Create the migration file:
if (!(await this.createFile(this.confPath, this.file, content)))
return Promise.reject(new Error(`[MIGRATOR][INSTALL] There was an issue writing the migration configuration to disk. `));
return Promise.resolve(undefined);
}
/**
* Handles the migration's database portion of the install
*
* @returns { Promise<undefined> }
*/
async installDatabaseComponent() {
// Create the migrations table:
const dbo = await this.getDbo();
const createMigrationsTable = await dbo
.query(`create table if not exists ${this.table} ` +
`( id int(11) not null auto_increment, ` +
`date varchar(255), ` +
`name varchar(255), ` +
`sequel text, ` +
`applied int(11), ` +
`primary key (id), ` +
`unique key unique_date (date) );`);
// If the query failed:
if (!createMigrationsTable || !createMigrationsTable.rows)
return Promise.reject(new Error(`[MIGRATOR][INSTALL] There was an error installing migrations into the project. `));
return Promise.resolve(undefined);
}
/**
* Applies or reverts [a] migration(s)
*
* @param options Parameters that shape the migration command
*
* @returns { Promise<MigrationPromise> }
*/
async migrate(options) {
// Mark the start of the migration procedure
DEBUG(`Set migration 'start' time`);
this.start = new Date();
// Check that the migration system is already installed - if it is NOT
// we will need to install it and generate a migration prior to running
// any migration(s):
DEBUG(`Check migration system installation`);
if (await this.checkInstall() === null)
return Promise.reject(new Error(`[MIGRATOR][RUN] Unable to verify whether the migration system is installed. If the 'data/migrations' path and/or the 'data/migrations/migrations.json' configuration are missing - generate a migration in order to force installation on your system. `));
// Next we need to get a list of files within the 'data/migrations' directory:
DEBUG(`Get list of available migrations`);
//let { hierarchy, files, map } = await this.getAvailableMigrations();
let { files, map } = await this.getAvailableMigrations();
// Next we need a list of migrations which have already been applied.
DEBUG(`Get list of applied migrations`);
let applied = await this.getAppliedMigrations();
// Since we sort by the date and name it should already be sorted appropriately, queue
// up the migrations to process, and process them - keeping in mind whether we are
// processing forward or stepping back:
let processed = 0, workingSet = this.getWorkingSet(files, applied, options.stepBack), total = workingSet.length;
if (workingSet.length) {
this.updateProgress('MYSQL_MIGRATOR_RENDER_SERVICE', { progressLevel: 0, notice: `Processing migrations...` });
for (let toProcess in workingSet) {
// Because of the simplicity in the logic behind applying migrations, we need to
// ensure that if we are applying migrations - that we do not attempt to apply
// any migrations we iterate over that have already been applied. However, if
// we're reverting, we actually needed to ensure that bit - because were ONLY
// reverting migrations that have for sure been applied ⇨ So we have to create
// a condition where this code flow is entered under appropriate circumstance.
//
// Check for existence of the migration 'toProcess' in our collection of applied
// migrations. If a non-negative index (>= 0) value returns - it's been applied.
const isApplied = applied.indexOf(workingSet[toProcess]) >= 0;
// Then, apply migrations which have not yet been applied - or revert migrations
// that have already been applied:
if ((!options.stepBack && !isApplied) || (options.stepBack && isApplied)) {
this.processStart = new Date();
// Make this a lil' easier to read
const target = map[workingSet[toProcess]];
DEBUG(`target: ${target}`);
DEBUG(target);
this.updateProgress('MYSQL_MIGRATOR_RENDER_SERVICE', {
progressLevel: ((processed * 100) / total),
notice: this.printProcessInfo(target.name, (options.stepBack) ? true : false)
});
//this.printProcessInfo( target.name, ( options.stepBack ) ? true : false );
DEBUG(`Enqueue migration '${target.name}' at '${Filesystem.getPathToCWD()}/${target.path.toString()}`);
await this.enqueueMigration(target, (options.stepBack) ? true : false);
// Require and instantiate the migration
const applicableMigration = (await import(`${Filesystem.getPathToCWD()}/${target.path.toString()}`)).default, applicableMigrator = new applicableMigration();
// Composition rocks:
applicableMigrator.dbo = this.dbo;
DEBUG(`${(options.stepBack) ? `Revert ` : `Apply `} migration '${target.name}`);
// Run the migration:
const migrationProcessed = (!options.stepBack) ?
await applicableMigrator.up() :
await applicableMigrator.down();
if (!migrationProcessed || !migrationProcessed.result || !migrationProcessed.rows)
return Promise.reject(new Error(`[MIGRATION][${target.name}]: There was an issue processing the migration. `));
DEBUG(`Migration '${target.name}' ${(options.stepBack) ? 'reverted' : 'applied'}...`);
DEBUG(`Dequeue migration '${target.name}`);
await this.dequeueMigration(target, (options.stepBack) ? true : false);
this.processEnd = new Date();
this.updateProgress('MYSQL_MIGRATOR_RENDER_SERVICE', { log: this.printProcessInfo(target.name, (options.stepBack) ? true : false, true) });
//this.printProcessInfo( target.name, ( options.stepBack ) ? true : false, true );
processed++;
}
}
this.end = new Date();
this.updateProgress('MYSQL_MIGRATOR_RENDER_SERVICE', {
progressLevel: ((processed * 100) / total),
notice: this.printServiceInfo(false, options.stepBack, processed)
});
//this.printServiceInfo( false, options.stepBack, processed );
}
else {
this.end = new Date();
this.updateProgress('MYSQL_MIGRATOR_RENDER_SERVICE', { progressLevel: 100, notice: this.printServiceInfo(true) });
//this.printServiceInfo( true );
/*
console.log
(
output.normalize().buffer( `[` ).color( 'gray' ).decor( ['bright'] )
.buffer( this.end.toLocaleTimeString( "en-us", this.timeOptions ) )
.normalize().buffer( `] ` ).decor( ['bright'] )
.buffer( `${(options.stepBack)? `No applied migrations exist.` : `No migrations exist.`}` ).color( 'cyan' )
.color( 'white' ).buffer( ` after ` )
.color( 'magenta' ).buffer( `${this.end.getTime() - this.start.getTime()} ms` ).normalize().dump()
);
*/
}
// Return some info
return Promise.resolve({ result: true, migrations: files });
}
async getAvailableMigrations() {
const hierarchy = { root: { type: "directory", children: {} } };
hierarchy.root.children = await this.getDirectoryStructure(this.path.toString());
// We know the directory structure so lets loop through each month of each year
// and make a list of all migrations:
let files = [], map = {}, count = 0;
// Loop through each year
//for( let year in ( hierarchy.root.children as any ).files ) {
for (let year in hierarchy.root.children) {
// For each year, loop through each month:
//for( let month in ( hierarchy.root.children as any ).files[year].children ) {
for (let month in hierarchy.root.children[year].children) {
// For each month, add the migrations to our list:
//for( let migration in ( hierarchy.root.children as any ).files[year].children[month].children ) {
for (let migration in hierarchy.root.children[year].children[month].children) {
// Create a simple array to store the migration files in, this is the device
// we'll use to sort and ensure the proper sequence of migrations:
files.push(migration);
// Build a map to keep necessary data available to us:
map[files[count]] = { timestamp: migration.split('_')[0], name: migration.split('_')[1].split('.cjs')[0], path: this.path + '/' + year + '/' + month + '/' + migration };
DEBUG(`Migration '${map[files[count]].name}' found at '${map[files[count]].path}'`);
count++;
}
}
}
// Now sort the keys array so we apply migrations in order!:
files = files.sort(this.alphaNumericSort);
// return Promise.resolve( { hierarchy, files, map } );
return Promise.resolve({ files, map });
}
async getAppliedMigrations() {
// Get the database object:
const dbo = await this.getDbo();
// Query the migrations table for applied migrations:
const appliedMigrations = await dbo
.query(`select * from ${this.table} ` +
`where applied=1 ` +
`order by date asc, name asc;`);
// If the query failed:
if (!appliedMigrations || !appliedMigrations.rows)
return Promise.reject(new Error(`[MIGRATOR][GET_APPLIED_MIGRATIONS] There was an error getting a list of applied migrations from ${this.table} table in the database. `));
// Now we need to take any rows, and rebuild the migration filename to use as a
// comparitor so as to ensure we do not rerun the respective migration as we
// loop through our list of migrations and apply any unapplied migrations:
let applied = [];
for (let record in appliedMigrations.rows) {
applied.push(`${appliedMigrations.rows[record].date}_${appliedMigrations.rows[record].name}.cjs`);
}
DEBUG(`[MIGRATOR][GET_APPLIED_MIGRATIONS]: Found '${applied.length}' migrations in '${this.table}'.`);
return Promise.resolve(applied);
}
getWorkingSet(available, applied, stepBack) {
console.log(output.normalize().buffer(`[`).color('gray').decor(['bright'])
.buffer(new Date().toLocaleTimeString("en-us", this.timeOptions))
.normalize().buffer(`] `).decor(['bright'])
.buffer(`${(stepBack) ? `Stepping back ${stepBack} migrations.\n` : 'Processing migrations forward.\n'}`).normalize().dump());
let workingSet = [];
if (!stepBack) {
// If we're not stepping back the working set is the files we have in our data/migrations
// directory, and we'll apply only migrations which haven't already been applied:
workingSet = available;
}
else {
// If we ARE stepping back, the working set will be only the migrations we are going to
// revert -> so we'll slice the working set such that we only have the number of migrations
// that we are stepping back, to work with:
workingSet = applied;
// Now let's only get the migrations we're going to revert:
//workingSet = workingSet.slice( workingSet.length - stepBack, workingSet.length );
workingSet = workingSet.slice(-stepBack);
// Finally, let's re-sort them in reverse:
workingSet = workingSet.reverse();
}
return workingSet;
}
/**
* Stages the migration for processing.
*
* Checks if the migration has been queued for processing, and if not - queues it. In
* the event that it has already been queued, we note that whether we are applying or
* reverting the migration.
*
* @param { any } target The target migration
* @param { Number } stepBack The number of migrations to revert
*
* @returns { Promise<undefined> } A promise that resolves undefined
*/
async enqueueMigration(target, stepBack = false) {
// As part of the queueing process, check whether the migration is already added to
// the migrations table and set as unapplied:
const migrationIsQueued = await this.dbo
.query(`select * from ${this.table} ` +
`where date='${target.timestamp}' and name='${target.name}' and applied=0;`);
if (!migrationIsQueued || !migrationIsQueued.rows)
return Promise.reject(new Error(`[MIGRATION][MIGRATE] There was an issue checking whether a migration had been queued to be applied in the '${this.table}' database table. `));
// If not, we need to queue it in the database first, then process it. When reverting
// migrations, this will conveniently never matter:
if (migrationIsQueued.rows.length <= 0 && !stepBack) {
DEBUG(`Queueing migration '${target.name}'`);
const queueMigration = await this.dbo
.query(`insert into ${this.table} ` +
`(date, name, applied) ` +
`values( '${target.timestamp}', '${target.name}', 0);`);
if (!queueMigration || !queueMigration.rows)
return Promise.reject(new Error(`[MIGRATION][${target.name}] There was an issue queueing the migration in the ${this.table} table in the database. `));
DEBUG(`Migration '${target.name}' staged to apply.`);
}
else {
// If it's not queued and is in fact a reversion, just note it
if (migrationIsQueued.rows.length <= 0 && stepBack)
DEBUG(`Migration '${target.name}' staged to revert`);
// If it's somehow already queued, it can't possibly be a migration being processed
// for reversion, because migrations can only be processed for reversion if they've
// been applied, already - that's the rule we've created... so
if (migrationIsQueued.rows.length > 0)
DEBUG(`Migration '${target.name}' previously queued, resuming...`);
}
return Promise.resolve(undefined);
}
/**
* If migrations are being applied, it marks the queued migration as 'applied'. If
* migrations are being reverted, it marks the reverted migration as unapplied.
*
* @param { any } target The target migration
* @param { number } stepBack The number of migrations to revert
*
* @returns { Promise<undefined> } An undefined promise
*/
async dequeueMigration(target, stepBack = false) {
// We then need to update the table record such that it reflects
// that we have applied the migration:
const markApplied = await this.dbo
.query(`update ${this.table} set applied=${(stepBack) ? '0' : '1'} ` +
`where name='${target.name}' and date='${target.timestamp}';`);
if (!markApplied || !markApplied.rows)
return Promise.reject(new Error(`[MIGRATION][${target.name}]: There was an issue marking a${(stepBack) ? ' reverted migration as unapplied' : 'n applied migration as completed.'}`));
}
/**
* Prints current process information
*
* @param { string } target The name of the migration
* @param { boolean } stepBack True if the migration is moving backward, false if forward
* @param { boolean } finished True if the process is completed
*/
printProcessInfo(target, stepBack = false, finished = false) {
let _o = output.normalize().buffer(`[`).color('gray').decor(['bright'])
.buffer(new Date().toLocaleTimeString("en-us", this.timeOptions))
.normalize().buffer(`] `).decor(['bright']);
_o = (finished) ? _o.buffer(`${(stepBack) ? `Reverting '` : `Applying '`}`) :
_o.buffer(`${(stepBack) ? `Finished reverting '` : `Finished applying '`}`);
_o = _o.color('cyan').buffer(`${target}`).color('white').buffer(`' `);
if (finished)
_o = _o.buffer(`after `).color('magenta')
.buffer(`${this?.processEnd?.getTime() - this?.processStart?.getTime()} ms\n`);
//console.log ( _o.normalize().dump() );
return _o.normalize().dump();
}
/**
* Prints current process information
*
* @param { string } target The name of the migration
* @param { boolean } revert True if the migration is moving backward, false if forward
* @param { boolean } finished True if the process is completed
*/
printServiceInfo(emptySet = true, stepBack = false, processed) {
let _o = output.normalize().buffer(`[`).color('gray').decor(['bright'])
.buffer(this?.end?.toLocaleTimeString("en-us", this.timeOptions))
.normalize().buffer(`] `).decor(['bright']);
// If we had a workingSet, we have one potential output:
if (!emptySet)
_o = _o.buffer(`${(processed && processed > 0) ? `${(stepBack) ? `Successfully reverted ${stepBack} migrations.` : `Successfully applied ${processed} migrations.`}` :
`${(stepBack) ? `Nothing to revert,` : `Migrations are up to date.`}`}`);
else
_o = _o.buffer(`${(stepBack) ? `No applied migrations exist.` : `No migrations exist.`}`);
// And follow with a display of lapsed time for the service or migration, accordingly
_o = _o.color('white').buffer(` after `)
.color('magenta').buffer(`${this?.end?.getTime() - this?.start?.getTime()} ms`).normalize();
//console.log( _o.dump() );
return _o.dump();
}
}
//# sourceMappingURL=mysql-migrator.mjs.map