Warn user if custom migration source is being reset (#3839)

This commit is contained in:
Igor Savin 2020-05-06 00:13:09 +02:00 committed by GitHub
parent 38f54ce8f0
commit c0afe14cde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 305 additions and 160 deletions

View File

@ -163,19 +163,20 @@ function invoke(env) {
.command('migrate:latest')
.description(' Run all migrations that have not yet been run.')
.option('--verbose', 'verbose')
.action(() => {
initKnex(env, commander.opts())
.then((instance) => instance.migrate.latest())
.then(([batchNo, log]) => {
if (log.length === 0) {
success(color.cyan('Already up to date'));
}
success(
color.green(`Batch ${batchNo} run: ${log.length} migrations`) +
(argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
);
})
.catch(exit);
.action(async () => {
try {
const instance = await initKnex(env, commander.opts());
const [batchNo, log] = await instance.migrate.latest();
if (log.length === 0) {
success(color.cyan('Already up to date'));
}
success(
color.green(`Batch ${batchNo} run: ${log.length} migrations`) +
(argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
);
} catch (err) {
exit(err);
}
});
commander

View File

@ -6,7 +6,7 @@ const isFunction = require('lodash/isFunction');
const isString = require('lodash/isString');
class Logger {
constructor(config) {
constructor(config = {}) {
const {
log: {
debug,

View File

@ -43,7 +43,10 @@ class Migrator {
this.knex.userParams = this.knex.userParams || {};
}
this.config = getMergedConfig(this.knex.client.config.migrations);
this.config = getMergedConfig(
this.knex.client.config.migrations,
this.knex.client.logger
);
this.generator = new MigrationGenerator(this.knex.client.config.migrations);
this._activeMigration = {
fileName: null,
@ -53,7 +56,7 @@ class Migrator {
// Migrators to the latest configuration.
async latest(config) {
this._disableProcessing();
this.config = getMergedConfig(config, this.config);
this.config = getMergedConfig(config, this.config, this.knex.client.logger);
const allAndCompleted = await migrationListResolver.listAllAndCompleted(
this.config,
@ -93,7 +96,7 @@ class Migrator {
// Runs the next migration that has not yet been run
up(config) {
this._disableProcessing();
this.config = getMergedConfig(config, this.config);
this.config = getMergedConfig(config, this.config, this.knex.client.logger);
return migrationListResolver
.listAllAndCompleted(this.config, this.knex)
@ -154,7 +157,11 @@ class Migrator {
this._disableProcessing();
return new Promise((resolve, reject) => {
try {
this.config = getMergedConfig(config, this.config);
this.config = getMergedConfig(
config,
this.config,
this.knex.client.logger
);
} catch (e) {
reject(e);
}
@ -186,7 +193,7 @@ class Migrator {
down(config) {
this._disableProcessing();
this.config = getMergedConfig(config, this.config);
this.config = getMergedConfig(config, this.config, this.knex.client.logger);
return migrationListResolver
.listAllAndCompleted(this.config, this.knex)
@ -229,7 +236,7 @@ class Migrator {
status(config) {
this._disableProcessing();
this.config = getMergedConfig(config, this.config);
this.config = getMergedConfig(config, this.config, this.knex.client.logger);
return Promise.all([
getTable(this.knex, this.config.tableName, this.config.schemaName).select(
@ -243,7 +250,7 @@ class Migrator {
// If no migrations have been run yet, return "none".
currentVersion(config) {
this._disableProcessing();
this.config = getMergedConfig(config, this.config);
this.config = getMergedConfig(config, this.config, this.knex.client.logger);
return migrationListResolver
.listCompleted(this.config.tableName, this.config.schemaName, this.knex)
@ -256,7 +263,7 @@ class Migrator {
// list all migrations
async list(config) {
this._disableProcessing();
this.config = getMergedConfig(config, this.config);
this.config = getMergedConfig(config, this.config, this.knex.client.logger);
const [all, completed] = await migrationListResolver.listAllAndCompleted(
this.config,
@ -276,7 +283,7 @@ class Migrator {
}
async forceFreeMigrationsLock(config) {
this.config = getMergedConfig(config, this.config);
this.config = getMergedConfig(config, this.config, this.knex.client.logger);
const { schemaName, tableName } = this.config;
const lockTableName = getLockTableName(tableName);
const { knex } = this;
@ -294,7 +301,7 @@ class Migrator {
// Creates a new migration, with a given name.
make(name, config) {
this.config = getMergedConfig(config, this.config);
this.config = getMergedConfig(config, this.config, this.knex.client.logger);
return this.generator.make(name, this.config);
}

View File

@ -2,6 +2,8 @@ const {
FsMigrations,
DEFAULT_LOAD_EXTENSIONS,
} = require('./sources/fs-migrations');
const Logger = require('../logger');
const defaultLogger = new Logger();
const CONFIG_DEFAULT = Object.freeze({
extension: 'js',
@ -14,7 +16,7 @@ const CONFIG_DEFAULT = Object.freeze({
sortDirsSeparately: false,
});
function getMergedConfig(config, currentConfig) {
function getMergedConfig(config, currentConfig, logger = defaultLogger) {
// config is the user specified config, mergedConfig has defaults and current config
// applied to it.
const mergedConfig = Object.assign(
@ -32,6 +34,9 @@ function getMergedConfig(config, currentConfig) {
config.sortDirsSeparately !== undefined ||
config.loadExtensions)
) {
logger.warn(
'FS-related option specified for migration configuration. This resets migrationSource to default FsMigrations'
);
mergedConfig.migrationSource = null;
}

View File

@ -7,7 +7,7 @@ const { FileTestHelper, execCommand } = require('cli-testlab');
const KNEX = path.normalize(__dirname + '/../../bin/cli.js');
describe('knexfile resolution', () => {
describe('knexfile', () => {
/**
* @type FileTestHelper
*/
@ -26,13 +26,14 @@ describe('knexfile resolution', () => {
process.env.KNEX_PATH = '../knex.js';
});
context('--cwd is NOT specified', function () {
context('and --knexfile is also NOT specified', function () {
it('Resolves default knexfile in working directory correctly', () => {
const path = process.cwd() + '/knexfile.js';
fileHelper.createFile(
path,
`
describe('knexfile resolution', () => {
context('--cwd is NOT specified', function () {
context('and --knexfile is also NOT specified', function () {
it('Resolves default knexfile in working directory correctly', () => {
const path = process.cwd() + '/knexfile.js';
fileHelper.createFile(
path,
`
module.exports = {
client: 'sqlite3',
connection: {
@ -43,116 +44,11 @@ module.exports = {
},
};
`,
{ isPathAbsolute: true }
);
{ isPathAbsolute: true }
);
return execCommand(
`node ${KNEX} migrate:latest --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
});
context('but --knexfile is specified', function () {
it('Run migrations with knexfile passed', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile/knexfile.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
it('Run migrations with knexfile returning function passed', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile/knexfile_func.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
it('Run migrations with knexfile with async passed', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile/knexfile_async.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
it('Run migrations with knexfile with promise passed', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile/knexfile_promise.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
it("changes the process's cwd to the directory that contains the knexfile", () => {
const knexfile = 'test/jake-util/knexfile-relative/knexfile.js';
const expectedCWD = tildify(path.resolve(path.dirname(knexfile)));
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile-relative/knexfile.js --knexpath=../knex.js`,
{
expectedOutput: `Working directory changed to ${expectedCWD}`,
}
);
});
// This addresses the issue that was reported here:
//
// https://github.com/knex/knex/issues/3660
//
context(
'and the knexfile itself resolves paths relative to process.cwd()',
function () {
it("changes the process's cwd to the directory that contains the knexfile before opening the knexfile", () => {
const knexfile = 'test/jake-util/knexfile-relative/knexfile.js';
const expectedCWD = tildify(path.resolve(path.dirname(knexfile)));
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile-relative/knexfile-with-resolve.js --knexpath=../knex.js`,
{
expectedOutput: `Working directory changed to ${expectedCWD}`,
}
);
});
}
);
// FYI: This is only true because the Knex CLI changes the CWD to
// the directory of the knexfile.
it('Resolves migrations relatively to knexfile', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile-relative/knexfile.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 2 migrations',
}
);
});
it('Throws informative error when no knexfile is found', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexpath=../knex.js`,
{
expectedErrorMessage: 'No configuration file found',
}
);
});
});
});
context('--cwd is specified', function () {
context('and --knexfile is also specified', function () {
context('and --knexfile is a relative path', function () {
it('resolves --knexfile relative to --cwd', function () {
return execCommand(
`node ${KNEX} migrate:latest --cwd=test/jake-util/knexfile --knexfile=knexfile.js`,
`node ${KNEX} migrate:latest --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
@ -160,33 +56,162 @@ module.exports = {
});
});
context('and --knexfile is an absolute path', function () {
it('uses the indicated knexfile', function () {
// Notice: the Knexfile is using Typescript. This means that Knex
// is pre-loading the appropriate Typescript modules before loading
// the Knexfile.
const knexfile = path.resolve(
'test/jake-util/knexfile-ts/custom-config.ts'
);
context('but --knexfile is specified', function () {
it('Run migrations with knexfile passed', () => {
return execCommand(
`node ${KNEX} migrate:latest --cwd=test/jake-util/knexfile --knexfile=${knexfile}`,
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile/knexfile.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 4 migrations',
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
it('Run migrations with knexfile returning function passed', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile/knexfile_func.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
it('Run migrations with knexfile with async passed', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile/knexfile_async.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
it('Run migrations with knexfile with promise passed', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile/knexfile_promise.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
it("changes the process's cwd to the directory that contains the knexfile", () => {
const knexfile = 'test/jake-util/knexfile-relative/knexfile.js';
const expectedCWD = tildify(path.resolve(path.dirname(knexfile)));
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile-relative/knexfile.js --knexpath=../knex.js`,
{
expectedOutput: `Working directory changed to ${expectedCWD}`,
}
);
});
// This addresses the issue that was reported here:
//
// https://github.com/knex/knex/issues/3660
//
context(
'and the knexfile itself resolves paths relative to process.cwd()',
function () {
it("changes the process's cwd to the directory that contains the knexfile before opening the knexfile", () => {
const knexfile = 'test/jake-util/knexfile-relative/knexfile.js';
const expectedCWD = tildify(path.resolve(path.dirname(knexfile)));
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile-relative/knexfile-with-resolve.js --knexpath=../knex.js`,
{
expectedOutput: `Working directory changed to ${expectedCWD}`,
}
);
});
}
);
// FYI: This is only true because the Knex CLI changes the CWD to
// the directory of the knexfile.
it('Resolves migrations relatively to knexfile', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile-relative/knexfile.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 2 migrations',
}
);
});
it('Throws informative error when no knexfile is found', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexpath=../knex.js`,
{
expectedErrorMessage: 'No configuration file found',
}
);
});
});
});
context('but --knexfile is NOT specified', function () {
it('resolves knexfile relative to the specified cwd', () => {
return execCommand(
`node ${KNEX} migrate:latest --cwd=test/jake-util/knexfile`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
context('--cwd is specified', function () {
context('and --knexfile is also specified', function () {
context('and --knexfile is a relative path', function () {
it('resolves --knexfile relative to --cwd', function () {
return execCommand(
`node ${KNEX} migrate:latest --cwd=test/jake-util/knexfile --knexfile=knexfile.js`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
});
context('and --knexfile is an absolute path', function () {
it('uses the indicated knexfile', function () {
// Notice: the Knexfile is using Typescript. This means that Knex
// is pre-loading the appropriate Typescript modules before loading
// the Knexfile.
const knexfile = path.resolve(
'test/jake-util/knexfile-ts/custom-config.ts'
);
return execCommand(
`node ${KNEX} migrate:latest --cwd=test/jake-util/knexfile --knexfile=${knexfile}`,
{
expectedOutput: 'Batch 1 run: 4 migrations',
}
);
});
});
});
context('but --knexfile is NOT specified', function () {
it('resolves knexfile relative to the specified cwd', () => {
return execCommand(
`node ${KNEX} migrate:latest --cwd=test/jake-util/knexfile`,
{
expectedOutput: 'Batch 1 run: 1 migrations',
}
);
});
});
});
});
describe('knexfile supports custom migrationSource', () => {
it('works correctly with migrationSource specified', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile-custom-migration-source/knexfile.js --knexpath=../knex.js`,
{
expectedOutput: 'Batch 1 run: 2 migrations',
}
);
});
it('ignores migrationSource if migration directory is specified', () => {
return execCommand(
`node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile-custom-migration-source/knexfile-with-directory.js --knexpath=../knex.js`,
{
expectedOutput: [
'Batch 1 run: 1 migrations',
'FS-related option specified for migration configuration. This resets migrationSource to default FsMigrations',
],
}
);
});
});
});

View File

@ -978,7 +978,7 @@ module.exports = function (knex) {
describe('migrationSource config as class', function () {
const migrations = {
migration: {
migration1: {
up(knex) {
return knex.schema.createTable('migration_source_test_1', function (
t

View File

@ -0,0 +1,54 @@
const migrations = {
migration1: {
up(knex) {
return knex.schema.createTable('migration_source_test_1', function (t) {
t.increments();
t.string('name');
});
},
down(knex) {
return knex.schema.dropTable('migration_source_test_1');
},
},
migration2: {
up(knex) {
return knex.schema.createTable('migration_source_test_2', function (t) {
t.increments();
t.string('name');
});
},
down(knex) {
return knex.schema.dropTable('migration_source_test_2');
},
},
};
class MigrationSource {
getMigrations() {
return Promise.resolve(Object.keys(migrations));
}
getMigrationName(migration) {
return 'migration1';
}
getMigration(migration) {
return migrations[migration];
}
}
const migrationSource = new MigrationSource();
module.exports = {
client: 'sqlite3',
connection: {
filename: __dirname + '/../test.sqlite3',
},
// Note that if any FS-related parameters are provided, such as directory, custom migration source is ignored
migrations: {
directory: __dirname + '/../knexfile_migrations',
migrationSource,
},
seeds: {
directory: __dirname + '/../knexfile_seeds',
},
};

View File

@ -0,0 +1,53 @@
const migrations = {
migration1: {
up(knex) {
return knex.schema.createTable('migration_source_test_1', function (t) {
t.increments();
t.string('name');
});
},
down(knex) {
return knex.schema.dropTable('migration_source_test_1');
},
},
migration2: {
up(knex) {
return knex.schema.createTable('migration_source_test_2', function (t) {
t.increments();
t.string('name');
});
},
down(knex) {
return knex.schema.dropTable('migration_source_test_2');
},
},
};
class MigrationSource {
getMigrations() {
return Promise.resolve(Object.keys(migrations));
}
getMigrationName(migration) {
return 'migration1';
}
getMigration(migration) {
return migrations[migration];
}
}
const migrationSource = new MigrationSource();
module.exports = {
client: 'sqlite3',
connection: {
filename: __dirname + '/../test.sqlite3',
},
// Note that if any FS-related parameters are provided, such as directory, custom migration source is ignored
migrations: {
migrationSource,
},
seeds: {
directory: __dirname + '/../knexfile_seeds',
},
};