Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce new command to run manual scripts #233

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,75 @@
* are also meant to be the public interface for the developers using it as a package.
*/

import { Knex } from 'knex';

import * as init from './init';
import { log } from './util/logger';
import { existsDir } from './util/fs';
import { withTransaction, mapToConnectionReferences, DatabaseConnections } from './util/db';

import Configuration from './domain/Configuration';
import { RunScriptParams } from './domain/RunScriptParams';
import SynchronizeParams from './domain/SynchronizeParams';
import ConnectionReference from './domain/ConnectionReference';
import OperationParams from './domain/operation/OperationParams';
import OperationResult from './domain/operation/OperationResult';

// Service
import { executeProcesses } from './service/execution';
import { runSynchronize, runPrune } from './service/sync';
import { getMigrationPath, invokeMigrationApi, KnexMigrationAPI } from './migration/service/knexMigrator';
import { runSynchronize, runPrune, runScript } from './service/sync';
import {
getManualScriptPath,
getMigrationPath,
invokeMigrationApi,
KnexMigrationAPI
} from './migration/service/knexMigrator';
import { runScriptWithLog } from './service/sqlRunner';

/**
* Run scripts for all the configured database connections.
*
* @param {Configuration} config
* @param {DatabaseConnections} conn
* @param {RunScriptParams} options
* @returns {Promise<OperationResult[]>}
*/
export async function runScriptAPI(config: Configuration, conn: DatabaseConnections, options?: RunScriptParams) {
log('Run Script');
const scriptPath = getManualScriptPath(config);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not name things as manual here. Just getScriptPath should be good.

const dirExist = await existsDir(scriptPath);

if (!dirExist) {
log('Script directory does not exist');
}

const params: RunScriptParams = {
...options
};

const connections = filterConnections(mapToConnectionReferences(conn), params.only);

const processes = connections.map(connection => () =>
withTransaction(
connection,
trx =>
runScript(trx, {
config,
params,
connectionId: connection.id,
migrateFunc: (
t: Knex.Transaction,
files: string[],
connectionId: string,
runSQLScripts: (t: Knex.Transaction, filteredScript: string[]) => Promise<void>
) => runScriptWithLog(t, files, connectionId, config.manual.tableName, runSQLScripts)
}),
params['dry-run']
)
);

return executeProcesses(processes, config);
}

/**
* Synchronize all the configured database connections.
Expand Down
115 changes: 115 additions & 0 deletions src/commands/run-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Command, flags } from '@oclif/command';
import { bold, red, magenta, cyan } from 'chalk';

import { runScriptAPI } from '../api';
import { dbLogger } from '../util/logger';
import { loadConfig, resolveConnections } from '..';
import { validateScriptFileName } from '../util/fs';
import { printLine, printError, printInfo } from '../util/io';
import OperationResult from '../domain/operation/OperationResult';

class RunScript extends Command {
static description = 'Run the provided manual scripts.';

static flags = {
'dry-run': flags.boolean({ description: 'Dry run script.', default: false }),
only: flags.string({
helpValue: 'CONNECTION_ID(s)',
description: 'Filter provided connection(s). Comma separated ids eg: id1,id2'
}),
file: flags.string({
required: true,
helpValue: 'Script Name',
parse: validateScriptFileName,
description: 'Name of the manual SQL/JS/TS script'
}),
'connection-resolver': flags.string({
helpValue: 'PATH',
description: 'Path to the connection resolver.'
}),
config: flags.string({
char: 'c',
description: 'Custom configuration file.'
})
};

/**
* Started event handler.
*/
onStarted = async (result: OperationResult) => {
await printLine(bold(` ▸ ${result.connectionId}`));

await printInfo(' [✓] Manual script run - started');
};

/**
* Success handler.
*/
onSuccess = async (result: OperationResult) => {
const log = dbLogger(result.connectionId);
const [num, list] = result.data;
const alreadyUpToDate = num && list.length === 0;

log('Up to date: ', alreadyUpToDate);

await printInfo(` [✓] Manual script run - completed (${result.timeElapsed}s)`);

if (alreadyUpToDate) {
await printInfo(' Already up to date.\n');

return;
}

// Completed migrations.
for (const item of list) {
await printLine(cyan(` - ${item}`));
}

await printInfo(` Ran ${list.length} scripts.\n`);
};

/**
* Failure handler.
*/
onFailed = async (result: OperationResult) => {
await printLine(bold(red(` [✓] Manual script run - Failed\n`)));
};

/**
* CLI command execution handler.
*
* @returns {Promise<void>}
*/
async run(): Promise<void> {
const { flags: parsedFlags } = this.parse(RunScript);
const isDryRun = parsedFlags['dry-run'];
const config = await loadConfig(parsedFlags.config);

const connections = await resolveConnections(config, parsedFlags['connection-resolver']);

if (isDryRun) await printLine(magenta('\n• DRY RUN STARTED\n'));

const results = await runScriptAPI(config, connections, {
...parsedFlags,
onStarted: this.onStarted,
onSuccess: this.onSuccess,
onFailed: this.onFailed
});

const failedCount = results.filter(({ success }) => !success).length;

if (failedCount === 0) {
if (isDryRun) await printLine(magenta('• DRY RUN ENDED\n'));

return process.exit(0);
}

printError(`Error: Script failed for ${failedCount} connection(s).`);

if (isDryRun) await printLine(magenta('\n• DRY RUN ENDED\n'));

process.exit(-1);
}
}

export default RunScript;
10 changes: 10 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ export function getSqlBasePath(config: Configuration): string {
return path.join(config.basePath, 'sql');
}

/**
* Get manual scripts path from config.
*
* @param {Configuration} config
* @returns {string}
*/
export function getManualScriptBasePath(config: Configuration): string {
return path.join(config.basePath, config.manual.directory);
}

/**
* Load config yaml file.
*
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export const DEFAULT_CONFIG: Configuration = {
directory: 'migration',
tableName: 'knex_migrations', // Note: This is Knex's default value. Just keeping it same.
sourceType: 'sql'
},
manual: {
directory: 'manual',
tableName: 'manual_scripts'
}
};

Expand Down
4 changes: 4 additions & 0 deletions src/domain/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ interface Configuration {
tableName: string;
sourceType: 'sql' | 'javascript' | 'typescript';
};
manual: {
directory: string;
tableName: string;
};
}

export default Configuration;
14 changes: 14 additions & 0 deletions src/domain/RunScriptContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Knex } from 'knex';

import { RunScriptParams } from './RunScriptParams';
import OperationContext from './operation/OperationContext';

export interface RunScriptContext extends OperationContext {
params: RunScriptParams;
migrateFunc: (
trx: Knex.Transaction,
files: string[],
connectionId: string,
runSQLScripts: (trx: Knex.Transaction, filteredScripts: string[]) => Promise<void>
) => Promise<(number | string[])[]>;
}
5 changes: 5 additions & 0 deletions src/domain/RunScriptParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import OperationParams from './operation/OperationParams';

export interface RunScriptParams extends OperationParams {
file?: string;
}
14 changes: 14 additions & 0 deletions src/migration/service/knexMigrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,17 @@ export function getMigrationPath(config: Configuration): string {

return migrationPath;
}

/**
* Get manual scripts directory path.
*
* @param {Configuration} config
* @returns {string}
*/
export function getManualScriptPath(config: Configuration): string {
const { basePath, manual } = config;

const scriptPath = path.isAbsolute(manual.directory) ? manual.directory : path.join(basePath, manual.directory);

return scriptPath;
}
55 changes: 55 additions & 0 deletions src/migration/service/migrator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Knex } from 'knex';
import * as path from 'path';

import { glob, exists } from '../../util/fs';
import { getManualScriptPath } from './knexMigrator';
import { resolveFile } from '../../service/sqlRunner';
import FileExtensions from '../../enum/FileExtensions';
import SqlMigrationEntry from '../domain/SqlMigrationEntry';
import { RunScriptContext } from '../../domain/RunScriptContext';
import JavaScriptMigrationEntry from '../domain/JavaScriptMigrationEntry';

const FILE_PATTERN_JS = /(.+).js$/;
Expand Down Expand Up @@ -127,3 +130,55 @@ export async function resolveJavaScriptMigrations(

return Promise.all(migrationPromises);
}

/**
* Run exposed function from manual JS/TS scripts.
*
* @param {Knex.Transaction} trx
* @param {RunScriptContext} context
* @param {string} connectionId
* @param {string[]} filteredScripts
* @param {string} extension
* @returns {Promise<void>}
*/
export async function runJSTSScripts(
trx: Knex.Transaction,
context: RunScriptContext,
connectionId: string,
filteredScripts: string[],
extension: string
) {
const migrationNames = filteredScripts;

let mRequire: NodeRequire = require;

if (extension === FileExtensions.TS) {
// Transpile & execute ts files required on the fly
require('ts-node').register({
transpileOnly: true
});
} else {
// On the fly es6 => commonJS
mRequire = require('esm')(module);
}

const migrationPromises = migrationNames.map(async name => {
const { main } = mRequire(path.resolve(`${getManualScriptPath(context.config)}/${extension}`, name));

return {
main,
name
};
});

const fileFuncInfos = await Promise.all(migrationPromises);

if (!fileFuncInfos.length) {
return;
}

const func = fileFuncInfos[0].main;

// Execute the function
await func(trx, connectionId);
}
Loading
Loading