From 7d13deb0d81988157ab49ec34042d78ee0c22cd1 Mon Sep 17 00:00:00 2001 From: Kuzzy Date: Wed, 25 Aug 2021 11:54:36 +0300 Subject: [PATCH] Extend config to make possible to tune DB records for passed migrations more flexible * Add `nameField` property to customize name of record key where migration name stored * Add `dateField` property to customize name of record key where migration date stored * Add `namePrefix` and `nameWithoutExtension` properties to make possible to customize migration name * Add additional logging in `up` method * Fix linting errors --- README.md | 14 +++++- lib/actions/down.js | 16 ++++--- lib/actions/status.js | 33 +++++++++----- lib/actions/up.js | 42 +++++++++++++----- lib/env/config.js | 12 +++++- lib/utils/name.js | 9 ++++ samples/migrate-mongo-config.js | 12 ++++++ test/actions/down.test.js | 8 +++- test/actions/status.test.js | 76 ++++++++++++++++++++++++++++++--- test/actions/up.test.js | 8 +++- test/utils/has-callback.test.js | 32 ++++---------- 11 files changed, 203 insertions(+), 59 deletions(-) create mode 100644 lib/utils/name.js diff --git a/README.md b/README.md index 4a697f1..ef1d5ed 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,20 @@ module.exports = { // The mongodb collection where the applied changes are stored. Only edit this when really necessary. changelogCollectionName: "changelog", + // Field in collection to store migration name + nameField: 'fileName', + + // Prefix for migration name + namePrefix: '', + + // Whether name should include file extension + nameWithoutExtension: false, + + // Field in collection to store migration applied date + dateField: 'appliedAt', + // The file extension to create migrations and search for in migration dir - migrationFileExtension: ".js" + migrationFileExtension: ".js", // Enable the algorithm to create a checksum of the file contents and use that in the comparison to determin // if the file should be run. Requires that scripts are coded to be run multiple times. diff --git a/lib/actions/down.js b/lib/actions/down.js index e952192..3005e1c 100644 --- a/lib/actions/down.js +++ b/lib/actions/down.js @@ -10,12 +10,17 @@ const hasCallback = require('../utils/has-callback'); module.exports = async (db, client) => { const downgraded = []; const statusItems = await status(db); - const appliedItems = statusItems.filter(item => item.appliedAt !== "PENDING"); + const { + changelogCollectionName, + dateField, + nameField, + } = await config.read(); + const appliedItems = statusItems.filter(item => item[dateField] !== "PENDING"); const lastAppliedItem = _.last(appliedItems); if (lastAppliedItem) { try { - const migration = await migrationsDir.loadMigration(lastAppliedItem.fileName); + const migration = await migrationsDir.loadMigration(lastAppliedItem.file); const down = hasCallback(migration.down) ? promisify(migration.down) : migration.down; if (hasCallback(migration.down) && fnArgs(migration.down).length < 3) { @@ -27,14 +32,13 @@ module.exports = async (db, client) => { } catch (err) { throw new Error( - `Could not migrate down ${lastAppliedItem.fileName}: ${err.message}` + `Could not migrate down ${lastAppliedItem.file}: ${err.message}` ); } - const { changelogCollectionName } = await config.read(); const changelogCollection = db.collection(changelogCollectionName); try { - await changelogCollection.deleteOne({ fileName: lastAppliedItem.fileName }); - downgraded.push(lastAppliedItem.fileName); + await changelogCollection.deleteOne({ [nameField]: lastAppliedItem[nameField] }); + downgraded.push(lastAppliedItem.file); } catch (err) { throw new Error(`Could not update changelog: ${err.message}`); } diff --git a/lib/actions/status.js b/lib/actions/status.js index a1f2bfc..710bb1c 100644 --- a/lib/actions/status.js +++ b/lib/actions/status.js @@ -1,29 +1,42 @@ const { find } = require("lodash"); const migrationsDir = require("../env/migrationsDir"); const config = require("../env/config"); +const getName = require('../utils/name'); module.exports = async db => { await migrationsDir.shouldExist(); await config.shouldExist(); - const fileNames = await migrationsDir.getFileNames(); + const configObject = await config.read() + const { + changelogCollectionName, + useFileHash, + nameField, + dateField, + } = configObject; - const { changelogCollectionName, useFileHash } = await config.read(); const changelogCollection = db.collection(changelogCollectionName); - const changelog = await changelogCollection.find({}).toArray(); + const fileNames = await migrationsDir.getFileNames(); + const changelog = await changelogCollection.find({}).toArray(); const useFileHashTest = useFileHash === true; - const statusTable = await Promise.all(fileNames.map(async (fileName) => { + return Promise.all(fileNames.map(async (fileName) => { + const migrationName = getName(configObject, fileName); + let fileHash; - let findTest = { fileName }; + let findTest = { [nameField]: migrationName }; if (useFileHashTest) { fileHash = await migrationsDir.loadFileHash(fileName); - findTest = { fileName, fileHash }; + findTest = { [nameField]: migrationName, fileHash }; } + const itemInLog = find(changelog, findTest); - const appliedAt = itemInLog ? itemInLog.appliedAt.toJSON() : "PENDING"; - return useFileHash ? { fileName, fileHash, appliedAt } : { fileName, appliedAt }; + const appliedAt = itemInLog ? itemInLog[dateField].toJSON() : "PENDING"; + return { + [nameField]: migrationName, + [dateField]: appliedAt, + file: fileName, + ...(useFileHash && { fileHash }), + }; })); - - return statusTable; }; diff --git a/lib/actions/up.js b/lib/actions/up.js index 4862573..9b74714 100644 --- a/lib/actions/up.js +++ b/lib/actions/up.js @@ -1,21 +1,34 @@ +/* eslint no-console: 0 */ const _ = require("lodash"); const pEachSeries = require("p-each-series"); const { promisify } = require("util"); -const fnArgs = require('fn-args'); +const fnArgs = require("fn-args"); const status = require("./status"); const config = require("../env/config"); const migrationsDir = require("../env/migrationsDir"); const hasCallback = require('../utils/has-callback'); +const getName = require('../utils/name'); module.exports = async (db, client) => { const statusItems = await status(db); - const pendingItems = _.filter(statusItems, { appliedAt: "PENDING" }); + const configObject = await config.read() + const { + changelogCollectionName, + useFileHash, + dateField, + nameField, + } = configObject; + + const pendingItems = _.filter(statusItems, { [dateField]: "PENDING" }); const migrated = []; const migrateItem = async item => { + console.info(`Migrating ${item.file}`); + const timeTaken = `${item.file} was successfully migrated`; + console.time(timeTaken); try { - const migration = await migrationsDir.loadMigration(item.fileName); + const migration = await migrationsDir.loadMigration(item.file); const up = hasCallback(migration.up) ? promisify(migration.up) : migration.up; if (hasCallback(migration.up) && fnArgs(migration.up).length < 3) { @@ -27,27 +40,36 @@ module.exports = async (db, client) => { } catch (err) { const error = new Error( - `Could not migrate up ${item.fileName}: ${err.message}` + `Could not migrate up ${item.file}: ${err.message}` ); error.stack = err.stack; error.migrated = migrated; throw error; } - const { changelogCollectionName, useFileHash } = await config.read(); const changelogCollection = db.collection(changelogCollectionName); - - const { fileName, fileHash } = item; + const { file, fileHash } = item; + const migrationName = getName(configObject, file); const appliedAt = new Date(); try { - await changelogCollection.insertOne(useFileHash === true ? { fileName, fileHash, appliedAt } : { fileName, appliedAt }); + await changelogCollection.insertOne({ + [nameField]: migrationName, + [dateField]: appliedAt, + ...(useFileHash === true && { fileHash }), + }); } catch (err) { throw new Error(`Could not update changelog: ${err.message}`); } - migrated.push(item.fileName); + migrated.push(item.file); + console.timeEnd(timeTaken); }; - await pEachSeries(pendingItems, migrateItem); + if (pendingItems.length) + await pEachSeries(pendingItems, migrateItem); + else + console.info('Nothing to migrate'); + + return migrated; }; diff --git a/lib/env/config.js b/lib/env/config.js index 084278f..8a3094f 100644 --- a/lib/env/config.js +++ b/lib/env/config.js @@ -3,6 +3,8 @@ const path = require("path"); const { get } = require("lodash"); const DEFAULT_CONFIG_FILE_NAME = "migrate-mongo-config.js"; +const DEFAULT_NAME_FIELD = 'fileName'; +const DEFAULT_DATE_FIELD = 'appliedAt'; let customConfigContent = null; @@ -60,6 +62,14 @@ module.exports = { return customConfigContent; } const configPath = getConfigPath(); - return Promise.resolve(require(configPath)); // eslint-disable-line + const config = await Promise.resolve(require(configPath)); // eslint-disable-line + + if (!config.nameField) + config.nameField = DEFAULT_NAME_FIELD; + + if (!config.dateField) + config.dateField = DEFAULT_DATE_FIELD; + + return config; } }; diff --git a/lib/utils/name.js b/lib/utils/name.js new file mode 100644 index 0000000..b03a213 --- /dev/null +++ b/lib/utils/name.js @@ -0,0 +1,9 @@ +module.exports = (config, fileName) => { + let migrationName = fileName; + if (config.namePrefix) + migrationName = `${config.namePrefix}${migrationName}`; + if (config.nameWithoutExtension) + migrationName = migrationName.substring(0, migrationName.lastIndexOf('.')) || migrationName; + + return migrationName +} \ No newline at end of file diff --git a/samples/migrate-mongo-config.js b/samples/migrate-mongo-config.js index 7993812..5ef280c 100644 --- a/samples/migrate-mongo-config.js +++ b/samples/migrate-mongo-config.js @@ -22,6 +22,18 @@ const config = { // The mongodb collection where the applied changes are stored. Only edit this when really necessary. changelogCollectionName: "changelog", + // Field in collection to store migration name + nameField: 'fileName', + + // Prefix for migration name + namePrefix: '', + + // Whether name should include file extension + nameWithoutExtension: false, + + // Field in collection to store migration applied date + dateField: 'appliedAt', + // The file extension to create migrations and search for in migration dir migrationFileExtension: ".js", diff --git a/test/actions/down.test.js b/test/actions/down.test.js index 5cbe891..376e71f 100644 --- a/test/actions/down.test.js +++ b/test/actions/down.test.js @@ -17,10 +17,12 @@ describe("down", () => { return sinon.stub().returns( Promise.resolve([ { + file: "20160609113224-first_migration.js", fileName: "20160609113224-first_migration.js", appliedAt: new Date() }, { + file: "20160609113225-last_migration.js", fileName: "20160609113225-last_migration.js", appliedAt: new Date() } @@ -31,7 +33,11 @@ describe("down", () => { function mockConfig() { return { shouldExist: sinon.stub().returns(Promise.resolve()), - read: sinon.stub().returns({ changelogCollectionName: "changelog" }) + read: sinon.stub().returns({ + changelogCollectionName: "changelog", + dateField: "appliedAt", + nameField: "fileName", + }) }; } diff --git a/test/actions/status.test.js b/test/actions/status.test.js index 5b9f68e..0f58e57 100644 --- a/test/actions/status.test.js +++ b/test/actions/status.test.js @@ -44,7 +44,9 @@ describe("status", () => { return { shouldExist: sinon.stub().returns(Promise.resolve()), read: sinon.stub().returns({ - changelogCollectionName: "changelog" + changelogCollectionName: "changelog", + dateField: "appliedAt", + nameField: "fileName", }) }; } @@ -86,7 +88,19 @@ describe("status", () => { function enabledFileHash(configContent) { configContent.read.returns({ changelogCollectionName: "changelog", - useFileHash: true + useFileHash: true, + dateField: "appliedAt", + nameField: "fileName", + }) + } + + function makeCustomConfig(configContent) { + configContent.read.returns({ + changelogCollectionName: "changelog", + dateField: "run_on", + nameField: "name", + namePrefix: "/", + nameWithoutExtension: true, }) } @@ -109,6 +123,23 @@ describe("status", () => { }) } + function addCustomFieldsToChangelog(changelog) { + changelog.find.returns({ + toArray: sinon.stub().returns( + Promise.resolve([ + { + name: "/20160509113224-first_migration", + run_on: new Date("2016-06-03T20:10:12.123Z") + }, + { + name: "/20160512091701-second_migration", + run_on: new Date("2016-06-09T20:10:12.123Z") + } + ]) + ) + }) + } + beforeEach(() => { changelogCollection = mockChangelogCollection(); @@ -199,15 +230,18 @@ describe("status", () => { expect(statusItems).to.deep.equal([ { appliedAt: "2016-06-03T20:10:12.123Z", - fileName: "20160509113224-first_migration.js" + file: "20160509113224-first_migration.js", + fileName: "20160509113224-first_migration.js", }, { appliedAt: "2016-06-09T20:10:12.123Z", - fileName: "20160512091701-second_migration.js" + file: "20160512091701-second_migration.js", + fileName: "20160512091701-second_migration.js", }, { appliedAt: "PENDING", - fileName: "20160513155321-third_migration.js" + file: "20160513155321-third_migration.js", + fileName: "20160513155321-third_migration.js", } ]); }); @@ -218,22 +252,48 @@ describe("status", () => { expect(statusItems).to.deep.equal([ { appliedAt: "PENDING", + file: "20160509113224-first_migration.js", fileName: "20160509113224-first_migration.js", fileHash: "0f295f21f63c66dc78d8dc091ce3c8bab8c56d8b74fb35a0c99f6d9953e37d1a", }, { appliedAt: "PENDING", + file: "20160512091701-second_migration.js", fileName: "20160512091701-second_migration.js", fileHash: "18b4d9c95a8678ae3a6dd3ae5b8961737a6c3dd65e3e655a5f5718d97a0bff70", }, { appliedAt: "PENDING", + file: "20160513155321-third_migration.js", fileName: "20160513155321-third_migration.js", fileHash: "1f9eb3b5eb70b2fb5b83fa0c660d859082f0bb615e835d29943d26fb0d352022", } ]); }); + it("it should use custom field names and migration names from config", async () => { + addCustomFieldsToChangelog(changelogCollection); + makeCustomConfig(config); + const statusItems = await status(db); + expect(statusItems).to.deep.equal([ + { + run_on: "2016-06-03T20:10:12.123Z", + file: "20160509113224-first_migration.js", + name: "/20160509113224-first_migration", + }, + { + run_on: "2016-06-09T20:10:12.123Z", + file: "20160512091701-second_migration.js", + name: "/20160512091701-second_migration", + }, + { + run_on: "PENDING", + file: "20160513155321-third_migration.js", + name: "/20160513155321-third_migration", + } + ]); + }); + it("it should mark new scripts as pending with a file hash", async () => { enabledFileHash(config); addHashToChangeLog(changelogCollection); @@ -242,16 +302,19 @@ describe("status", () => { { appliedAt: "2016-06-03T20:10:12.123Z", fileName: "20160509113224-first_migration.js", + file: "20160509113224-first_migration.js", fileHash: "0f295f21f63c66dc78d8dc091ce3c8bab8c56d8b74fb35a0c99f6d9953e37d1a", }, { appliedAt: "2016-06-09T20:10:12.123Z", fileName: "20160512091701-second_migration.js", + file: "20160512091701-second_migration.js", fileHash: "18b4d9c95a8678ae3a6dd3ae5b8961737a6c3dd65e3e655a5f5718d97a0bff70", }, { appliedAt: "PENDING", fileName: "20160513155321-third_migration.js", + file: "20160513155321-third_migration.js", fileHash: "1f9eb3b5eb70b2fb5b83fa0c660d859082f0bb615e835d29943d26fb0d352022", } ]); @@ -278,16 +341,19 @@ describe("status", () => { { appliedAt: "2016-06-03T20:10:12.123Z", fileName: "20160509113224-first_migration.js", + file: "20160509113224-first_migration.js", fileHash: "0f295f21f63c66dc78d8dc091ce3c8bab8c56d8b74fb35a0c99f6d9953e37d1a", }, { appliedAt: "PENDING", fileName: "20160512091701-second_migration.js", + file: "20160512091701-second_migration.js", fileHash: "18b4d9c95a8678ae3a6dd3ae5b8961737a6c3dd65e3e655a5f5718d97a0bff71", // this hash is different }, { appliedAt: "PENDING", fileName: "20160513155321-third_migration.js", + file: "20160513155321-third_migration.js", fileHash: "1f9eb3b5eb70b2fb5b83fa0c660d859082f0bb615e835d29943d26fb0d352022", } ]); diff --git a/test/actions/up.test.js b/test/actions/up.test.js index 8508122..10682a4 100644 --- a/test/actions/up.test.js +++ b/test/actions/up.test.js @@ -19,18 +19,22 @@ describe("up", () => { return sinon.stub().returns( Promise.resolve([ { + file: "20160605123224-first_applied_migration.js", fileName: "20160605123224-first_applied_migration.js", appliedAt: new Date() }, { + file: "20160606093207-second_applied_migration.js", fileName: "20160606093207-second_applied_migration.js", appliedAt: new Date() }, { + file: "20160607173840-first_pending_migration.js", fileName: "20160607173840-first_pending_migration.js", appliedAt: "PENDING" }, { + file: "20160608060209-second_pending_migration.js", fileName: "20160608060209-second_pending_migration.js", appliedAt: "PENDING" } @@ -42,7 +46,9 @@ describe("up", () => { return { shouldExist: sinon.stub().returns(Promise.resolve()), read: sinon.stub().returns({ - changelogCollectionName: "changelog" + changelogCollectionName: "changelog", + dateField: "appliedAt", + nameField: "fileName", }) }; } diff --git a/test/utils/has-callback.test.js b/test/utils/has-callback.test.js index bffdefa..95ef1dd 100644 --- a/test/utils/has-callback.test.js +++ b/test/utils/has-callback.test.js @@ -5,51 +5,35 @@ const hasCallback = require('../../lib/utils/has-callback'); describe('has-callback', () => { it('should return true when last argument is called `callback`', () => { - expect(hasCallback((db, callback) => { - return callback(); - })).to.equal(true); + expect(hasCallback((db, callback) => callback())).to.equal(true); }); it('should return true when last argument is called `callback_`', () => { - expect(hasCallback((db, callback_) => { - return callback_(); - })).to.equal(true); + expect(hasCallback((db, callback_) => callback_())).to.equal(true); }); it('should return true when last argument is called `cb`', () => { - expect(hasCallback((db, cb) => { - return cb(); - })).to.equal(true); + expect(hasCallback((db, cb) => cb())).to.equal(true); }); it('should return true when last argument is called `cb_`', () => { - expect(hasCallback((db, cb_) => { - return cb_(); - })).to.equal(true); + expect(hasCallback((db, cb_) => cb_())).to.equal(true); }); it('should return true when last argument is called `next`', () => { - expect(hasCallback((db, next) => { - return next(); - })).to.equal(true); + expect(hasCallback((db, next) => next())).to.equal(true); }); it('should return true when last argument is called `next_`', () => { - expect(hasCallback((db, next_) => { - return next_(); - })).to.equal(true); + expect(hasCallback((db, next_) => next_())).to.equal(true); }); it('should return true when last argument is called `done`', () => { - expect(hasCallback((db, done) => { - return done(); - })).to.equal(true); + expect(hasCallback((db, done) => done())).to.equal(true); }); it('should return true when last argument is called `done_`', () => { - expect(hasCallback((db, done_) => { - return done_(); - })).to.equal(true); + expect(hasCallback((db, done_) => done_())).to.equal(true); }); });