UNPKG

sql-watch-lib

Version:

sql-watch-lib is a library that enables rapid SQL development by automatically applying idempotent SQL scripts to a database on file change.

829 lines (828 loc) 37.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqlWatch = exports.WatchOptionsDefault = exports.DirectoriesDefault = exports.TestOption = exports.Environment = exports.SqlConnection = exports.loggerDefault = void 0; const fs_1 = require("fs"); const path_1 = require("path"); const ssh2_1 = __importDefault(require("ssh2")); const chokidar_1 = __importDefault(require("chokidar")); const postgres_1 = __importStar(require("postgres")); // https://github.com/porsager/postgres const prompt_sync_1 = __importDefault(require("prompt-sync")); // TODO: Decouple pino from the library const pino_1 = __importDefault(require("pino")); /** * sql-watch uses itself to setup create the sql_watch schema used to maintain * state. We don't want any logging to occur at time of setup. * Set this to value to false if you need to debug the sql_watch schema setup * process. */ const SQL_WATCH_SCRIPT_SILENT = true; /** * By default, pino logs are a json format. Example: * {"level":30,"msg":"APPLIED ./db/scripts/prerun/20_session-two.sql"} * * pino-pretty provides a more human friendly format: * INFO: APPLIED ./db/scripts/prerun/20_session-two.sql */ exports.loggerDefault = { level: 'info', transport: { target: 'pino-pretty', }, // Disable display of the process id and host name base: { pid: undefined, hostname: undefined, }, // Disable the display of the timestamp timestamp: false, }; class SqlConnection { constructor(logger, connection = {}) { this._logger = logger; // Environment variables override connection options. const { env } = process; const host = env.PGHOST || (connection === null || connection === void 0 ? void 0 : connection.host); const port = Number(env.PGPORT) || (connection === null || connection === void 0 ? void 0 : connection.port) || 5432; const user = env.PGUSER || (connection === null || connection === void 0 ? void 0 : connection.user); const password = env.PGPASSWORD || (connection === null || connection === void 0 ? void 0 : connection.password); const dbname = env.PGDATABASE || (connection === null || connection === void 0 ? void 0 : connection.dbname); const schema = env.PGSCHEMA || (connection === null || connection === void 0 ? void 0 : connection.schema); const socket = undefined; let sshConnection; if (!host || !user || !password || !dbname) { const missingOptions = []; if (!host) { missingOptions.push('host'); } if (!user) { missingOptions.push('user'); } if (!password) { missingOptions.push('password'); } if (!dbname) { missingOptions.push('database'); } const errorMessage = `Connection missing required options: ${missingOptions.join(', ')}. Required options can be set with environment variables or via the connection parameter`; throw Error(errorMessage); } // see https://github.com/mscdex/ssh2 and https://github.com/porsager/postgres#custom-socket if (env.SSH_HOST !== undefined) { const sshHost = env.SSH_HOST; const sshPort = Number(env.SSH_PORT); const sshUser = env.SSH_USER; const sshPrivateKeyPath = env.SSH_PRIVATE_KEY_PATH; if (!sshHost || !sshPort || !sshUser || !sshPrivateKeyPath) { const missingSshOptions = []; if (!sshHost) { missingSshOptions.push('ssh host'); } if (!sshPort) { missingSshOptions.push('ssh port'); } if (!sshUser) { missingSshOptions.push('ssh user'); } if (!sshPrivateKeyPath) { missingSshOptions.push('ssh private key path'); } const errorMessage = `When the ssh option is set, then the following options are also required: ${missingSshOptions.join(', ')}. Required ssh options can be set with environment variables or via the connection parameter`; throw Error(errorMessage); } const privateKey = (0, fs_1.readFileSync)(sshPrivateKeyPath, 'utf8'); sshConnection = { host: sshHost, port: sshPort, user: sshUser, privateKeyPath: sshPrivateKeyPath, privateKey, }; } this._connectionOptions = { host, port, user, password, dbname, schema, ssh: sshConnection, socket, }; this._connection = this.createConnection(); } /** * Creates and returns a postgres connection. * @returns A postgres connection (see https://github.com/porsager/postgres) */ createConnection() { const finalConnection = this._connectionOptions; // https://github.com/porsager/postgres#all-postgres-options const options = Object.assign(Object.assign({}, finalConnection), { // If we get the error "UNDEFINED_VALUE: Undefined values are not allowed" // then it probably means we have something like // select * from x where y = ${ undefined }. So, we aren't going to enable // the transform option. // transform: { undefined: null }, onnotice: (notice) => { var _a; // Let's not pollute sql-watch's output by showing postgresql messages // that will be common with idempotent sql such as already exists, // does not exist, etc. if (!notice.message.includes('already exists') // CREATE IF NOT EXIST ... && (notice.severity !== 'INFO') && (!notice.message.includes('does not exist')) // DROP IF EXISTS ... ) { const severity = notice.severity.includes('NOTICE') ? '' : `${notice.severity}: `; this._logger.info(`${severity}${notice.message}`); const noticeDetails = (_a = notice.detail) === null || _a === void 0 ? void 0 : _a.split('\n'); if (noticeDetails) { for (const noticeDetail of noticeDetails) { this._logger.info(`${severity} ${noticeDetail}`); } } } } }); if (finalConnection === null || finalConnection === void 0 ? void 0 : finalConnection.ssh) { this._logger.debug('SSH: Connecting to database using an ssh Tunnel.'); const sshConnection = { host: finalConnection.ssh.host, port: finalConnection.ssh.port, username: finalConnection.ssh.user, privateKey: finalConnection.ssh.privateKey, }; options.socket = () => new Promise((resolve2, reject2) => { const ssh = new ssh2_1.default.Client(); ssh .on('error', reject2) .on('ready', () => { this._logger.debug(`SSH: Client ready to connect. Forwarding localhost:${finalConnection.port} to ${finalConnection.host}:${finalConnection.port}`); ssh.forwardOut('localhost', finalConnection.port, finalConnection.host, finalConnection.port, (err, socket) => (err ? reject2(err) : resolve2(socket))); }) .connect(sshConnection); }); } return (0, postgres_1.default)(options); } /** * Build at the query part of the uri. * @returns Database connection query part (what follows after ? in uri) */ getConnectionParams() { const con = this._connectionOptions; return con.schema ? `?search_path=${con.schema}` : ''; } /** * Returns the active connection to the database. */ get connection() { return this._connection; } /** * Generates a uri connection which includes the password * @returns A postgresql uri connection of the form: * postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...] */ get connectionUri() { const con = this._connectionOptions; return `postgresql://${con.user}:${con.password}@${con.host}:${con.port}/${con.dbname}${this.getConnectionParams()}`; } /** * Generates a uri connection without the password. Useful for logging. * @param connection Connection options. * @returns A postgresql uri connection of the form: * postgresql://[user[:*****]@][netloc][:port][/dbname][?param1=value1&...] */ get connectionUriNoPwd() { const con = this._connectionOptions; return `postgresql://${con.user}:*****@${con.host}:${con.port}/${con.dbname}${this.getConnectionParams()}`; } /** * Returns final connection options */ get connectionOptions() { return this._connectionOptions; } } exports.SqlConnection = SqlConnection; /** * Supported development environments. */ var Environment; (function (Environment) { Environment["Development"] = "development"; Environment["Staging"] = "staging"; Environment["Production"] = "production"; Environment["Test"] = "test"; Environment["Other"] = "other"; })(Environment = exports.Environment || (exports.Environment = {})); /** * sql-watch test options. A file is considered a test file if it matches * the one or more patterns in 'WatchOptions.testExtensions'. */ var TestOption; (function (TestOption) { /** * Tests are always ran. */ TestOption["Always"] = "always"; /** * Only tests are run. All other sql script is not run. */ TestOption["Only"] = "only"; /** * Tests are not run. */ TestOption["Skip"] = "skip"; })(TestOption = exports.TestOption || (exports.TestOption = {})); exports.DirectoriesDefault = { rootDirectory: './db/scripts', run: '/run', preRun: '/prerun', postRun: '/postrun', reset: '/reset', seed: '/seed', }; /** * Default sql-watch options. */ exports.WatchOptionsDefault = { reset: false, watch: false, bypass: false, alwaysRun: false, loggerOptions: { level: 'info', }, verbose: false, seed: false, runTests: TestOption.Always, extensions: ['.sql'], testExtensions: ['.spec.sql', '.test.sql'], directories: Object.assign({}, exports.DirectoriesDefault), sqlWatchSchemaName: 'sql_watch', }; /** * SqlWatch watches for changes to sql files: running sql files as needed when * changes are made. */ class SqlWatch { dirWithRoot(directory) { if (!directory.startsWith('/')) { throw new Error(`Directories must start with / which is missing from '${directory}'`); } return `${this.options.directories.rootDirectory}${directory}`; } /** * Sets up SqlWatch, verifying the configuration, setting up a logger and * a sql connection to the database. * TODO: Decouple the logger from SqlWatch. * @param options Configuration options. */ constructor(options, logger = undefined) { // if (options === undefined || options === null) { // throw new Error('The SqlWatch parameter \'option\' was null or undefined'); // } var _a, _b; this.isSetup = false; // Set up the logger const loggerConfig = Object.assign(Object.assign({}, exports.loggerDefault), { level: ((_a = options.loggerOptions) === null || _a === void 0 ? void 0 : _a.level) ? (_b = options.loggerOptions) === null || _b === void 0 ? void 0 : _b.level : 'info', }); this.logger = logger || (0, pino_1.default)(loggerConfig); this.options = Object.assign(Object.assign({}, exports.WatchOptionsDefault), options); this.sqlConnection = new SqlConnection(this.logger, options.connection); const dirs = this.options.directories; this.runDirectories = { rootDirectory: dirs.rootDirectory, reset: dirs.reset ? (0, path_1.resolve)(this.dirWithRoot(dirs.reset)) : undefined, preRun: dirs.preRun ? (0, path_1.resolve)(this.dirWithRoot(dirs.preRun)) : undefined, run: (0, path_1.resolve)(this.dirWithRoot(dirs.run)), postRun: dirs.postRun ? (0, path_1.resolve)(this.dirWithRoot(dirs.postRun)) : undefined, seed: dirs.seed ? (0, path_1.resolve)(this.dirWithRoot(dirs.seed)) : undefined, }; this.createDirs(); this.sql = this.sqlConnection.connection; // Don't setup the watcher when we are initializing if (options.watch && !options.init) { this.watcher = this.setupWatcher(); } else { this.watcher = undefined; } } getSql() { return this.sql; } /** * Checks if a file name should be ran based on the options.extension. If a * file has the extension, it is considered runnable. * @param filename The file name * @returns True if the file should be ran by sql-watch. False if the * file should not be run by sql-watch. */ isRunnableExtension(filename) { for (const extension of this.options.extensions) { if (filename.endsWith(`${extension}`)) return true; } return false; } isTestExtension(filename) { for (const extension of this.options.testExtensions) { if (filename.endsWith(`${extension}`)) return true; } return false; } getFilesToRun(files) { return files // Find all files that are actually runnable: both test sql and non-test sql .filter((filename) => (this.isRunnableExtension(filename) ? filename : undefined)) // Remove test files if configured as such .filter((filename) => { const isTestFile = this.isTestExtension(filename); switch (this.options.runTests) { case TestOption.Always: { return filename; } case TestOption.Only: { return isTestFile ? filename : undefined; } case TestOption.Skip: { return isTestFile ? undefined : filename; } default: { throw new Error(`Non existent test option '${this.options.runTests}'`); } } }); } setupWatcher() { this.logger.debug(`Watching ${this.options.directories.rootDirectory}`); const watcher = chokidar_1.default.watch(this.options.directories.rootDirectory, { persistent: true, awaitWriteFinish: true, ignoreInitial: true }); watcher .on('add', (path) => __awaiter(this, void 0, void 0, function* () { this.logger.debug(`File ${path} has been added`); yield this.run(false, path); })) .on('change', (path) => __awaiter(this, void 0, void 0, function* () { this.logger.debug(`File ${path} has been changed`); yield this.run(false, path); })) .on('unlink', () => __awaiter(this, void 0, void 0, function* () { // removed something, so we should re-run everything. This does not yield this.run(true); })) .on('error', (error) => __awaiter(this, void 0, void 0, function* () { this.logger.debug('There was an error ', error); // NOTE: User is already notified of the error })); return watcher; } // TODO: Refactor this a bit // Try our best to show where the error is logPostgreSqlError(file, error) { const position = Number(error.position) || 0; const { query } = error; const lines = query.split('\n'); const numLines = query.split('\n').map((line, index) => { const lineNum = String(index + 1).padStart(6, ' '); return `${lineNum}: ${line}`; }); let atPosition = 0; let errorLineNumber = 0; for (let line = 0; line < lines.length; line += 1) { atPosition += lines[line].length; if (atPosition > position) { errorLineNumber = line - 1; if (errorLineNumber > 0) { numLines[errorLineNumber] = numLines[errorLineNumber].replace(' ', '*'); // Align output for logging. ERROR is 1 letter longer than INFO. numLines[errorLineNumber] = numLines[errorLineNumber].replace(' ', ''); } break; } } this.logger.error(`${file}:${errorLineNumber + 1} ${error.name} (${error.code}) ${error.message}`); if (errorLineNumber < 0) { this.logger.warn('Unable to determine error line number.'); } if (this.options.verbose) { for (let line = 0; line < numLines.length; line += 1) { if (line === errorLineNumber) { this.logger.error(`${numLines[line]}`); } else { this.logger.info(`${numLines[line]}`); } } } else if (errorLineNumber > 0) { if (errorLineNumber > 1) { this.logger.info(`${numLines[errorLineNumber - 1]}`); } this.logger.error(`${numLines[errorLineNumber]}`); this.logger.info(`${numLines[errorLineNumber + 1]}`); } } /** * Runs sql located in file on database * @param fileName Name of the file which contains sql script that the postgres * library will run on the database server. */ applyOnFile(fileName) { return __awaiter(this, void 0, void 0, function* () { // await this.sql.begin(async () => { yield this.sql.file(fileName) .catch((err) => { if (err instanceof postgres_1.PostgresError) { this.logPostgreSqlError(fileName, err); } // rethrow the error so we don't try to run anymore files. throw err; }); // }).catch((err: unknown) => { // throw err; // }); }); } setupSqlWatch(environment) { return __awaiter(this, void 0, void 0, function* () { if (!this.isSetup) { try { // Setup schema that Sql Watch requires using itself. We use __dirname // because the sql files are located within the installation (the library) // itself this.logger.debug(`Running Sql Watch setup. ${process.cwd()} ${__dirname}`); yield this.runSql(`${__dirname}/db/scripts/run`, './db/scripts/run', new Date(0), true, SQL_WATCH_SCRIPT_SILENT); yield this.setEnvironment(environment); } catch (err) { if (err instanceof Error) { const error = err; this.logger.error(`${error.name} ${error.message}`); } } finally { this.isSetup = true; } } }); } getLastRunTime() { return __awaiter(this, void 0, void 0, function* () { const { sql } = this; try { const ranAt = yield sql `SELECT ran_at FROM ${sql(this.options.sqlWatchSchemaName)}.last_run;`; // If ranAt result was empty, then it means that this is the first time // anything was ran successfully, so we set that last run time to "0". return ranAt.length === 0 ? new Date(0) : ranAt[0].ran_at; } catch (err) { if (err instanceof postgres_1.PostgresError) { this.logPostgreSqlError('', err); } throw err; } }); } verifyInitialized(sql) { return __awaiter(this, void 0, void 0, function* () { try { yield sql `SELECT environment FROM ${sql(this.options.sqlWatchSchemaName)}.environment`; } catch (err) { if (err instanceof postgres_1.PostgresError && err.code === '42P01') { this.logger.error(`SqlWatch has not been initialized. Have your run sql-watch with the init option? If you feel this is in error please check and verify that the ${this.options.sqlWatchSchemaName}.environment table exists and has a valid environment entry`); return false; } // Have no idea why there was an error so we need to re-throw it. throw err; } return true; }); } getEnvironment(sql) { return __awaiter(this, void 0, void 0, function* () { const environment = yield sql `SELECT environment FROM ${sql(this.options.sqlWatchSchemaName)}.environment`; if (environment.length === 0) { this.logger.warn(`${this.options.sqlWatchSchemaName}.environment had no records when it should contain at least one record. Defaulting environment setting to production`); return Environment.Production; } return environment[0].environment; }); } getRunMetaData() { return __awaiter(this, void 0, void 0, function* () { return { username: process.env.USER || process.env.LOGNAME || process.env.npm_package_author_name || 'unknown', email: process.env.npm_config_email || process.env.npm_package_author_email || 'unknown', node_environment: process.env.NODE_ENV || 'not set', environment: yield this.getEnvironment(this.sql), workingDirectory: process.env.PWD, options: { reset: this.options.reset, watch: this.options.watch, bypass: this.options.bypass, alwaysRun: this.options.alwaysRun, level: this.options.loggerOptions.level, runTests: this.options.runTests, }, }; }); } setLastRunTime(ranAt) { return __awaiter(this, void 0, void 0, function* () { const { sql } = this; const metaData = sql.json(yield this.getRunMetaData()); yield sql ` INSERT INTO ${sql(this.options.sqlWatchSchemaName)}.run(ran_at, meta_data) VALUES (${ranAt}, ${metaData}); `; }); } runSql(resolvedDir, directory, lastRunTime, ignoreLastRunTime = false, silent = false) { return __awaiter(this, void 0, void 0, function* () { const applied = false; if (!silent) { this.logger.debug(`Running ${ignoreLastRunTime ? 'all ' : ''}sql in directory ${resolvedDir}`); } const files = (0, fs_1.readdirSync)(resolvedDir).sort(); if (files.length === 0 && !silent) { this.logger.debug(`No SQL files were found in ${resolvedDir}`); } const filesFiltered = this.getFilesToRun(files); if (filesFiltered.length === 0) { this.logger.debug(`No SQL files were found in ${resolvedDir} after running filters.`); return false; } let startMigrating = false; for (const filename of filesFiltered) { const file = (0, path_1.resolve)(resolvedDir, filename); const stat = (0, fs_1.statSync)(file); const fileDate = new Date(stat.mtime); if (!startMigrating && fileDate >= lastRunTime) { startMigrating = true; } if (startMigrating || ignoreLastRunTime) { // eslint-disable-next-line no-await-in-loop yield this.applyOnFile(file); if (!silent) { this.logger.info(`APPLIED ${directory}/${filename}`); } } else if (!silent && this.options.verbose) { this.logger.info(`SKIPPED ${directory}/${filename}`); } } return applied; }); } doReset() { return __awaiter(this, void 0, void 0, function* () { let startedAt = new Date(); let skip = false; if (this.options.reset) { let doReset = true; const environment = yield this.getEnvironment(this.sql); // We really want to make sure that the developer has explicitly set the // environment to a "non production" environment. So, we check for each // of these known environments instead of just checking for // environment === Environment.Production. if (environment !== Environment.Development && environment !== Environment.Staging && environment !== Environment.Other && environment !== Environment.Test) { const inputPrompt = environment === Environment.Production ? 'Resetting database in PRODUCTION environment!!!' : `Resetting database in the '${environment}' environment.`; const prompt = (0, prompt_sync_1.default)({ sigint: true }); const input = prompt(`WARNING! ${inputPrompt} Type 'DESTROY IT ALL' to continue. `); if (input !== 'DESTROY IT ALL') { doReset = false; skip = true; this.logger.info('Reset cancelled'); } else { this.logger.info('Resetting database in production'); } } else { this.logger.info('Resetting database'); } if (doReset) { startedAt = new Date(); const resetDir = this.runDirectories.reset; const resetRoot = this.dirWithRoot(this.options.directories.reset || ''); if (!resetDir) { this.logger.warn('The sql reset directory (defaulted to ./db/reset) must be defined for reset to work. Please review the options provided.'); } else { // Date(0) because we always run all the reset sql yield this.runSql(resetDir, resetRoot, new Date(0)); // We need to reset the last run time because we have cleaned out the // database. yield this.setLastRunTime(new Date(0)); } } if (!this.options.watch) { // if Sql Watch is executed with --reset flag only then we don't want // to run the skip = true; } } return { startedAt, skip, }; }); } doPreRun() { return __awaiter(this, void 0, void 0, function* () { const preRunDir = this.runDirectories.preRun; if (preRunDir) { const preRunRoot = this.dirWithRoot(this.options.directories.preRun || ''); // Date(0) because we always run all the pre run sql yield this.runSql(preRunDir, preRunRoot, new Date(0)); } }); } doRun(lastRunTime, ignoreLastRunTime = false) { return __awaiter(this, void 0, void 0, function* () { const runDir = this.runDirectories.run; const runRoot = this.dirWithRoot(this.options.directories.run); return this.runSql(runDir, runRoot, lastRunTime, ignoreLastRunTime); }); } doPostRun() { return __awaiter(this, void 0, void 0, function* () { const postRunDir = this.runDirectories.postRun; if (postRunDir) { const postRunRoot = this.dirWithRoot(this.options.directories.postRun || ''); // Date(0) because we always run all the post run sql yield this.runSql(postRunDir, postRunRoot, new Date(0)); } }); } doSeed(lastRunTime, ignoreLastRunTime = false) { return __awaiter(this, void 0, void 0, function* () { const seedDir = this.runDirectories.seed; const seedRoot = this.dirWithRoot(this.options.directories.seed || ''); if (this.options.seed && seedDir) { yield this.runSql(seedDir, seedRoot, lastRunTime, ignoreLastRunTime); } else if (this.options.verbose) { this.logger.info(`SKIPPED ${seedRoot}: all (seed option was false)`); } }); } setEnvironment(environment) { return __awaiter(this, void 0, void 0, function* () { const { sql } = this; yield sql ` UPDATE ${sql(this.options.sqlWatchSchemaName)}.environment SET environment = ${environment}; `; }); } static createDir(root, directory) { if (directory && !(0, fs_1.existsSync)(`${root}/${directory}`)) { (0, fs_1.mkdirSync)(`${root}/${directory}`); } } createDirs() { const root = (0, path_1.resolve)(this.options.directories.rootDirectory); if (!(0, fs_1.existsSync)(root)) { (0, fs_1.mkdirSync)(root, { recursive: true }); } SqlWatch.createDir(root, this.options.directories.postRun); SqlWatch.createDir(root, this.options.directories.preRun); SqlWatch.createDir(root, this.options.directories.reset); SqlWatch.createDir(root, this.options.directories.run); SqlWatch.createDir(root, this.options.directories.seed); } init(environment) { return __awaiter(this, void 0, void 0, function* () { this.createDirs(); yield this.setupSqlWatch(environment); }); } shutdown() { return __awaiter(this, void 0, void 0, function* () { yield this.sql.end({ timeout: 5 }); if (this.watcher) { yield this.watcher.close(); } }); } /** * Runs all the sql script located in the directories configured in * options.directories based on the state of the last run time. * @param ignoreLastRunTime When true, sql files are not skipped base on the * last run time. Note that the runTests options are still honored. * @param fileNameChanged A non empty value means a single file was changed. * @returns When true, watch has been enabled and the calling program * should not exit (if possible). When false, the sql-watch is not watching * watch has not been enabled and the user can exit the program. */ run(ignoreLastRunTime = false, fileNameChanged = '') { return __awaiter(this, void 0, void 0, function* () { try { if (this.options.init) { yield this.init(this.options.init); this.logger.info('Sql Watch successfully:'); this.logger.info(` * created/updated the ${this.options.sqlWatchSchemaName} schema in ${this.sqlConnection.connectionUriNoPwd}`); this.logger.info(` * set the environment in ${this.options.sqlWatchSchemaName}.environment to '${yield this.getEnvironment(this.sql)}'.`); this.logger.info(` * created/updated required script directories in '${this.options.directories.rootDirectory}'.`); } else { const isInitialized = yield this.verifyInitialized(this.sql); if (!isInitialized) { // not initialized so can't run at this time. yield this.shutdown(); return this.options.watch; } const lastRunTime = yield this.getLastRunTime(); let lastRunTimeIgnored = ignoreLastRunTime; if (this.options.directories.preRun && fileNameChanged.includes(this.options.directories.preRun)) { // Something was changed in the prerun, so should re-run everything. lastRunTimeIgnored = true; } if (this.options.directories.seed && fileNameChanged.includes(this.options.directories.seed) && (this.options.seed === false)) { this.logger.warn(`Seed file ./${fileNameChanged} was edited but seed option was file changes was not applied`); } // If reset is selected, then we always need to re-run everything. if (this.options.reset) { lastRunTimeIgnored = true; } const { startedAt, skip } = yield this.doReset(); if (!skip) { yield this.doPreRun(); const appliedRun = yield this.doRun(lastRunTime, lastRunTimeIgnored); // We may have run scripts and made changes to see files when the seed // flag was not set. This assures that on the first run // (aka: fileNameChanged = ''), the seeds files are ran. // If there were changes in the run directory, then we also need to // re-run all the files in the seeds directory (if the flag is set). const seedLastRunTimeIgnore = (fileNameChanged === '' || appliedRun) ? true : lastRunTimeIgnored; yield this.doSeed(lastRunTime, seedLastRunTimeIgnore); yield this.doPostRun(); yield this.setLastRunTime(new Date()); } const finishedAt = new Date(); const finishedMessage = `Finished in ${finishedAt.getTime() - startedAt.getTime()} ms`; this.logger.info(finishedMessage); } } catch (err) { if (!(err instanceof postgres_1.PostgresError)) { const error = err; this.logger.error(`${error.name} ${error.message}`); // stop running sql-watcher yield this.shutdown(); throw err; } // else error logged already. We may not want't to stop sql-watch // if the watch option is true. } if (this.options.init || !this.options.watch) { yield this.shutdown(); } else { this.logger.info('Waiting for changes'); } return this.options.watch; }); } } exports.SqlWatch = SqlWatch;