Skip to content

Commit 04888e6

Browse files
committed
...
1 parent 14e9835 commit 04888e6

2 files changed

Lines changed: 176 additions & 2 deletions

File tree

src/migration/migrator.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,35 @@ export class Migrator {
189189
*
190190
* await migrator.migrateTo(NO_MIGRATIONS)
191191
* ```
192+
*
193+
* By default, this method migrates up or down depending on which side of the
194+
* current migration state the target migration is on. Pass
195+
* {@link MigrateToOptions.direction} to enforce a direction — if reaching the
196+
* target would require migrating the other way, nothing is executed and
197+
* {@link MigrationResultSet.error} holds an error:
198+
*
199+
* ```ts
200+
* import { promises as fs } from 'node:fs'
201+
* import path from 'node:path'
202+
* import { FileMigrationProvider, Migrator } from 'kysely/migration'
203+
*
204+
* const migrator = new Migrator({
205+
* db,
206+
* provider: new FileMigrationProvider({
207+
* fs,
208+
* // Path to the folder that contains all your migrations.
209+
* migrationFolder: 'some/path/to/migrations',
210+
* path,
211+
* })
212+
* })
213+
*
214+
* // Errors if `some_migration` is already executed.
215+
* await migrator.migrateTo('some_migration', { direction: 'Up' })
216+
* ```
192217
*/
193218
async migrateTo(
194219
targetMigrationName: string | NoMigrations,
195-
options?: MigrateOptions,
220+
options?: MigrateToOptions,
196221
): Promise<MigrationResultSet> {
197222
return this.#migrate(
198223
({
@@ -204,6 +229,10 @@ export class Migrator {
204229
isObject(targetMigrationName) &&
205230
targetMigrationName.__noMigrations__ === true
206231
) {
232+
if (options?.direction === 'Up') {
233+
throw new Error(`can't migrate up to NO_MIGRATIONS`)
234+
}
235+
207236
return { direction: 'Down', step: Infinity }
208237
}
209238

@@ -222,13 +251,25 @@ export class Migrator {
222251
)
223252

224253
if (executedIndex !== -1) {
254+
if (options?.direction === 'Up') {
255+
throw new Error(
256+
`migration "${targetMigrationName}" is already executed; can't migrate up to it`,
257+
)
258+
}
259+
225260
return {
226261
direction: 'Down',
227262
step: executedMigrations.length - executedIndex - 1,
228263
}
229264
}
230265

231266
if (pendingIndex !== -1) {
267+
if (options?.direction === 'Down') {
268+
throw new Error(
269+
`migration "${targetMigrationName}" isn't executed; can't migrate down to it`,
270+
)
271+
}
272+
232273
return { direction: 'Up', step: pendingIndex + 1 }
233274
}
234275

@@ -933,6 +974,22 @@ export interface MigrateOptions {
933974

934975
export type MigratorTransactionMode = 'per-run' | 'per-migration' | 'none'
935976

977+
export interface MigrateToOptions extends MigrateOptions {
978+
/**
979+
* Enforces a migration direction.
980+
*
981+
* When provided and reaching the target migration would require migrating
982+
* the other way, nothing is executed and {@link MigrationResultSet.error}
983+
* holds an error. For example, `direction: 'Up'` errors when the target
984+
* migration is already executed.
985+
*
986+
* By default, {@link Migrator.migrateTo | migrateTo} migrates up or down
987+
* depending on which side of the current migration state the target
988+
* migration is on.
989+
*/
990+
readonly direction?: MigrationDirection
991+
}
992+
936993
export interface MigratorProps extends MigrateOptions {
937994
readonly db: Kysely<any>
938995
readonly provider: MigrationProvider
@@ -1043,7 +1100,7 @@ export interface MigrationResultSet {
10431100
readonly results?: MigrationResult[]
10441101
}
10451102

1046-
type MigrationDirection = 'Up' | 'Down'
1103+
export type MigrationDirection = 'Up' | 'Down'
10471104

10481105
export interface MigrationResult {
10491106
readonly migrationName: string

test/node/src/migration.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,123 @@ for (const dialect of DIALECTS) {
488488
expect(executedDownMethods2).to.eql(['migration4', 'migration3'])
489489
})
490490

491+
it('should migrate up to a specific migration with direction enforced', async () => {
492+
const { migrator, executedUpMethods } = createMigrations([
493+
'migration1',
494+
'migration2',
495+
'migration3',
496+
])
497+
498+
const { results } = await migrator.migrateTo('migration2', {
499+
direction: 'Up',
500+
})
501+
502+
expect(results).to.eql([
503+
{ migrationName: 'migration1', direction: 'Up', status: 'Success' },
504+
{ migrationName: 'migration2', direction: 'Up', status: 'Success' },
505+
])
506+
507+
expect(executedUpMethods).to.eql(['migration1', 'migration2'])
508+
})
509+
510+
it('should return an error when migrating up to an already executed migration', async () => {
511+
const { migrator, executedUpMethods, executedDownMethods } =
512+
createMigrations(['migration1', 'migration2', 'migration3'])
513+
514+
await migrator.migrateToLatest()
515+
516+
const { error } = await migrator.migrateTo('migration2', {
517+
direction: 'Up',
518+
})
519+
520+
expect(error).to.be.an.instanceOf(Error)
521+
expect(getMessage(error)).to.eql(
522+
`migration "migration2" is already executed; can't migrate up to it`,
523+
)
524+
525+
expect(executedUpMethods).to.eql([
526+
'migration1',
527+
'migration2',
528+
'migration3',
529+
])
530+
expect(executedDownMethods).to.eql([])
531+
})
532+
533+
it('should migrate down to a specific migration with direction enforced', async () => {
534+
const { migrator, executedDownMethods } = createMigrations([
535+
'migration1',
536+
'migration2',
537+
'migration3',
538+
])
539+
540+
await migrator.migrateToLatest()
541+
542+
const { results } = await migrator.migrateTo('migration1', {
543+
direction: 'Down',
544+
})
545+
546+
expect(results).to.eql([
547+
{ migrationName: 'migration3', direction: 'Down', status: 'Success' },
548+
{ migrationName: 'migration2', direction: 'Down', status: 'Success' },
549+
])
550+
551+
expect(executedDownMethods).to.eql(['migration3', 'migration2'])
552+
})
553+
554+
it('should return an error when migrating down to a pending migration', async () => {
555+
const { migrator, executedUpMethods, executedDownMethods } =
556+
createMigrations(['migration1', 'migration2', 'migration3'])
557+
558+
const { error } = await migrator.migrateTo('migration2', {
559+
direction: 'Down',
560+
})
561+
562+
expect(error).to.be.an.instanceOf(Error)
563+
expect(getMessage(error)).to.eql(
564+
`migration "migration2" isn't executed; can't migrate down to it`,
565+
)
566+
567+
expect(executedUpMethods).to.eql([])
568+
expect(executedDownMethods).to.eql([])
569+
})
570+
571+
it('should migrate all the way down with direction enforced', async () => {
572+
const { migrator, executedDownMethods } = createMigrations([
573+
'migration1',
574+
'migration2',
575+
])
576+
577+
await migrator.migrateToLatest()
578+
579+
const { results } = await migrator.migrateTo(NO_MIGRATIONS, {
580+
direction: 'Down',
581+
})
582+
583+
expect(results).to.eql([
584+
{ migrationName: 'migration2', direction: 'Down', status: 'Success' },
585+
{ migrationName: 'migration1', direction: 'Down', status: 'Success' },
586+
])
587+
588+
expect(executedDownMethods).to.eql(['migration2', 'migration1'])
589+
})
590+
591+
it('should return an error when migrating up to NO_MIGRATIONS', async () => {
592+
const { migrator, executedUpMethods, executedDownMethods } =
593+
createMigrations(['migration1', 'migration2'])
594+
595+
await migrator.migrateToLatest()
596+
597+
const { error } = await migrator.migrateTo(NO_MIGRATIONS, {
598+
direction: 'Up',
599+
})
600+
601+
expect(error).to.be.an.instanceOf(Error)
602+
expect(getMessage(error)).to.eql(`can't migrate up to NO_MIGRATIONS`)
603+
604+
expect(executedUpMethods).to.eql(['migration1', 'migration2'])
605+
expect(executedDownMethods).to.eql([])
606+
})
607+
491608
describe('with allowUnorderedMigrations enabled', () => {
492609
it('should migrate up to a specific migration', async () => {
493610
const { migrator: migrator1, executedUpMethods: executedUpMethods1 } =

0 commit comments

Comments
 (0)