-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Add migration lifecycle hooks #5541
Merged
kibertoad
merged 23 commits into
knex:master
from
leaselock:timorthi/issue2983-migration-lifecycle-hooks
Jan 13, 2024
Merged
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
3f8e597
feat: add migration lifecycle hooks
timorthi 2d53af4
test: add tests for migration lifecycle hooks
timorthi ffcc089
test: fix test setup
timorthi c822fac
fix: add missing await
timorthi 999cef0
feat: add beforeAll/afterAll to up, down, rollback
timorthi 5dafbbe
Merge branch 'master' of github.com:knex/knex into timorthi/issue2983…
timorthi 9c53658
test: rename to count
timorthi 81d19c1
test: create separate file for lifecycle hooks tests
timorthi 0f45133
test: remove extraneous setup
timorthi d681c86
test: rename to count
timorthi e2bc612
test: add more comprehensive tests
timorthi 25cd3bc
test: force clean slate before each test
timorthi 5bb8d4e
feat: short circuit if no migrations to run
timorthi 6f975bd
refactor: remove unnecessary asyncs
timorthi 772caa1
refactor: remove unnecessary asyncs
timorthi c88ff9c
fix: run hooks and migrations only if there are pending migrations
timorthi 6eaaf7f
Update test/integration2/migrate/migration-lifecycle-integration.spec.js
timorthi 87016d4
test: always return error value
timorthi cce4371
test: simplify expression
timorthi 30b8711
feat: always pass list of migrations in hooks
timorthi cc2680b
feat: add types for lifecycle hooks
timorthi ce0ace0
Merge branch 'timorthi/issue2983-migration-lifecycle-hooks' of github…
timorthi f7a3147
refactor: use brackets to denote array
timorthi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
303 changes: 303 additions & 0 deletions
303
test/integration2/migrate/migration-lifecycle-integration.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,303 @@ | ||
'use strict'; | ||
const sinon = require('sinon'); | ||
const chai = require('chai'); | ||
chai.use(require('chai-as-promised')); | ||
const { expect } = chai; | ||
|
||
const path = require('path'); | ||
const rimraf = require('rimraf'); | ||
const logger = require('../../integration/logger'); | ||
const { getAllDbs, getKnexForDb } = require('../util/knex-instance-provider'); | ||
|
||
describe('Migrations Lifecycle Hooks', function () { | ||
getAllDbs().forEach((db) => { | ||
describe(db, () => { | ||
let knex; | ||
|
||
// Force clean slate before each test | ||
beforeEach(async () => { | ||
rimraf.sync(path.join(__dirname, './migration')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why sync? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not 100% sure - this setup block was copied from |
||
knex = logger(getKnexForDb(db)); | ||
// make sure lock was not left from previous failed test run | ||
await knex.schema.dropTableIfExists('knex_migrations'); | ||
await knex.schema.dropTableIfExists('migration_test_1'); | ||
await knex.schema.dropTableIfExists('migration_test_2'); | ||
await knex.schema.dropTableIfExists('migration_test_2_1'); | ||
await knex.migrate.forceFreeMigrationsLock({ | ||
directory: 'test/integration2/migrate/test', | ||
}); | ||
}); | ||
|
||
describe('knex.migrate.latest', function () { | ||
describe('beforeAll', function () { | ||
it('runs before the migrations batch', async function () { | ||
let count; | ||
await knex.migrate.latest({ | ||
directory: 'test/integration2/migrate/test', | ||
beforeAll: async (knexOrTrx) => { | ||
const data = await knexOrTrx('knex_migrations').select('*'); | ||
count = data.length; | ||
}, | ||
}); | ||
|
||
const data = await knex('knex_migrations').select('*'); | ||
expect(data.length).to.equal(count + 2); | ||
}); | ||
|
||
it('does not run the migration or beforeEach/afterEach/afterAll hooks if it fails', async function () { | ||
const beforeAll = sinon | ||
.stub() | ||
.throws(new Error('force beforeAll hook failure')); | ||
const beforeEach = sinon.stub(); | ||
const afterEach = sinon.stub(); | ||
const afterAll = sinon.stub(); | ||
|
||
await knex.migrate | ||
.latest({ | ||
directory: 'test/integration2/migrate/test', | ||
beforeAll, | ||
beforeEach, | ||
afterEach, | ||
afterAll, | ||
}) | ||
.catch((error) => { | ||
expect(error.message).to.equal('force beforeAll hook failure'); | ||
}); | ||
|
||
// Should not have run the migration | ||
const hasTableCreatedByMigration = await knex.schema.hasTable( | ||
'migration_test_1' | ||
); | ||
expect(hasTableCreatedByMigration).to.be.false; | ||
|
||
// Should not have called the other hooks | ||
expect(beforeEach.called).to.be.false; | ||
expect(afterEach.called).to.be.false; | ||
expect(afterAll.called).to.be.false; | ||
}); | ||
}); | ||
|
||
describe('afterAll', function () { | ||
it('runs after the migrations batch', async function () { | ||
let count; | ||
await knex.migrate.latest({ | ||
directory: 'test/integration2/migrate/test', | ||
afterAll: async (knexOrTrx) => { | ||
const data = await knexOrTrx('knex_migrations').select('*'); | ||
count = data.length; | ||
}, | ||
}); | ||
|
||
const data = await knex('knex_migrations').select('*'); | ||
expect(data.length).to.equal(count); | ||
}); | ||
|
||
it('is not called if the migration fails', async function () { | ||
const afterAll = sinon.stub(); | ||
|
||
await knex.migrate | ||
.latest({ | ||
directory: 'test/integration2/migrate/test_with_invalid', | ||
afterAll, | ||
}) | ||
.catch(() => {}); | ||
|
||
expect(afterAll.called).to.be.false; | ||
}); | ||
}); | ||
|
||
describe('beforeEach', function () { | ||
it('runs before each migration', async function () { | ||
const tableExistenceChecks = []; | ||
const beforeEach = sinon.stub().callsFake(async (trx) => { | ||
const hasFirstTestTable = await trx.schema.hasTable( | ||
'migration_test_1' | ||
); | ||
const hasSecondTestTable = await trx.schema.hasTable( | ||
'migration_test_2' | ||
); | ||
tableExistenceChecks.push({ | ||
hasFirstTestTable, | ||
hasSecondTestTable, | ||
}); | ||
}); | ||
|
||
await knex.migrate.latest({ | ||
directory: 'test/integration2/migrate/test', | ||
beforeEach, | ||
}); | ||
|
||
expect(beforeEach.callCount).to.equal(2); | ||
expect(tableExistenceChecks).to.deep.equal([ | ||
{ | ||
hasFirstTestTable: false, | ||
hasSecondTestTable: false, | ||
}, | ||
{ | ||
hasFirstTestTable: true, | ||
hasSecondTestTable: false, | ||
}, | ||
]); | ||
}); | ||
|
||
it('does not run the migration or the afterEach/afterAll hooks if the hook fails', async function () { | ||
timorthi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const beforeEach = sinon | ||
.stub() | ||
.throws(new Error('force beforeEach hook failure')); | ||
const afterEach = sinon.stub(); | ||
const afterAll = sinon.stub(); | ||
|
||
await knex.migrate | ||
.latest({ | ||
directory: 'test/integration2/migrate/test', | ||
beforeEach, | ||
afterEach, | ||
afterAll, | ||
}) | ||
.catch((error) => { | ||
expect(error.message).to.equal('force beforeEach hook failure'); | ||
timorthi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
// Should not have run the migration | ||
const hasTableCreatedByMigration = await knex.schema.hasTable( | ||
'migration_test_1' | ||
); | ||
expect(hasTableCreatedByMigration).to.be.false; | ||
|
||
// Should not have called the after hooks | ||
expect(afterEach.called).to.be.false; | ||
expect(afterAll.called).to.be.false; | ||
}); | ||
}); | ||
|
||
describe('afterEach', function () { | ||
it('runs after each migration', async function () { | ||
const tableExistenceChecks = []; | ||
const afterEach = sinon.stub().callsFake(async (trx) => { | ||
const hasFirstTestTable = await trx.schema.hasTable( | ||
'migration_test_1' | ||
); | ||
const hasSecondTestTable = await trx.schema.hasTable( | ||
'migration_test_2' | ||
); | ||
tableExistenceChecks.push({ | ||
hasFirstTestTable, | ||
hasSecondTestTable, | ||
}); | ||
}); | ||
|
||
await knex.migrate.latest({ | ||
directory: 'test/integration2/migrate/test', | ||
afterEach, | ||
}); | ||
|
||
expect(afterEach.callCount).to.equal(2); | ||
expect(tableExistenceChecks).to.deep.equal([ | ||
{ | ||
hasFirstTestTable: true, | ||
hasSecondTestTable: false, | ||
}, | ||
{ | ||
hasFirstTestTable: true, | ||
hasSecondTestTable: true, | ||
}, | ||
]); | ||
}); | ||
|
||
it('is not called after a migration fails', async function () { | ||
const afterEach = sinon.stub(); | ||
|
||
await knex.migrate | ||
.latest({ | ||
directory: 'test/integration2/migrate/test_with_invalid', | ||
afterEach, | ||
}) | ||
.catch(() => {}); | ||
|
||
// The afterEach hook should have run for the first two successful migrations, but | ||
// not after failed third migration | ||
expect(afterEach.callCount).to.equal(2); | ||
expect( | ||
afterEach.args.map(([_knex, args]) => { | ||
return args.file; | ||
}) | ||
).to.deep.equal([ | ||
'20131019235242_migration_1.js', | ||
'20131019235306_migration_2.js', | ||
]); | ||
}); | ||
|
||
it('does not run the afterAll hook if the hook fails', async function () { | ||
const afterEach = sinon | ||
.stub() | ||
.throws(new Error('force afterEach hook failure')); | ||
const afterAll = sinon.stub(); | ||
|
||
await knex.migrate | ||
.latest({ | ||
directory: 'test/integration2/migrate/test', | ||
afterEach, | ||
afterAll, | ||
}) | ||
.catch((error) => { | ||
expect(error.message).to.equal('force afterEach hook failure'); | ||
}); | ||
|
||
expect(afterAll.called).to.be.false; | ||
}); | ||
}); | ||
|
||
describe('execution order', function () { | ||
it('runs in the expected order of beforeAll -> beforeEach -> afterEach -> afterAll', async function () { | ||
const order = []; | ||
|
||
await knex.migrate.latest({ | ||
directory: 'test/integration2/migrate/test', | ||
beforeAll: () => order.push('beforeAll'), | ||
beforeEach: (_knex, { file }) => order.push(`beforeEach-${file}`), | ||
afterEach: (_knex, { file }) => order.push(`afterEach-${file}`), | ||
afterAll: () => order.push('afterAll'), | ||
}); | ||
|
||
expect(order).to.deep.equal([ | ||
'beforeAll', | ||
'beforeEach-20131019235242_migration_1.js', | ||
'afterEach-20131019235242_migration_1.js', | ||
'beforeEach-20131019235306_migration_2.js', | ||
'afterEach-20131019235306_migration_2.js', | ||
'afterAll', | ||
]); | ||
}); | ||
}); | ||
|
||
describe('when there are no pending migrations', function () { | ||
it('does not run any of the hooks', async function () { | ||
// Fire the migrations once to get the DB up to date | ||
await knex.migrate.latest({ | ||
directory: 'test/integration2/migrate/test', | ||
}); | ||
|
||
// Now there should not be any pending migrations | ||
const beforeAll = sinon.stub(); | ||
const beforeEach = sinon.stub(); | ||
const afterEach = sinon.stub(); | ||
const afterAll = sinon.stub(); | ||
|
||
await knex.migrate.latest({ | ||
directory: 'test/integration2/migrate/test', | ||
beforeAll, | ||
beforeEach, | ||
afterEach, | ||
afterAll, | ||
}); | ||
|
||
expect(beforeAll.called).to.be.false; | ||
expect(beforeEach.called).to.be.false; | ||
expect(afterEach.called).to.be.false; | ||
expect(afterAll.called).to.be.false; | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the explicit commit so that we can run the
afterEach
hook in the same transaction (line 527)