ts-migrate-mongoose
Version:
A migration framework for Mongoose, built with TypeScript.
225 lines (195 loc) • 8.63 kB
text/typescript
import fs from 'node:fs'
import mongoose, { type Connection } from 'mongoose'
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { chalk } from '../src/chalk'
import { Migrate, getMigrator } from '../src/commander'
import { template } from '../src/template'
import { create } from './mongo/server'
import { clearDirectory, deleteDirectory } from './utils/filesystem'
const exec = (...args: string[]) => {
const migrate = new Migrate()
process.argv = ['node', 'migrate', ...args]
return migrate.run(false)
}
const execExit = (...args: string[]) => {
const migrate = new Migrate()
process.argv = ['node', 'migrate', ...args]
return migrate.run()
}
describe('cli', async () => {
const migrationsPath = 'cli'
const { uri, destroy } = await create('cli')
const commandLineOptions = ['-d', uri, '-m', migrationsPath]
let connection: Connection
afterAll(async () => {
await deleteDirectory(migrationsPath)
await destroy()
})
beforeEach(async () => {
await clearDirectory(migrationsPath)
connection = await mongoose.createConnection(uri).asPromise()
await connection.collection('migrations').deleteMany({})
})
afterEach(() => {
vi.restoreAllMocks()
})
it('should get migrator instance', async () => {
const migrator = await getMigrator({ uri, migrationsPath })
expect(migrator).toBeDefined()
expect(migrator.connection).toBeDefined()
expect(migrator.connection.readyState).toBe(1)
await migrator.close()
expect(migrator.connection.readyState).toBe(0)
})
describe('sequence', async () => {
it('should run list command', async () => {
const consoleSpy = vi.spyOn(console, 'log')
const opts = await exec('list', ...commandLineOptions)
expect(opts?.uri).toBe(uri)
expect(consoleSpy).toHaveBeenCalledWith(chalk.cyan('Listing migrations'))
expect(consoleSpy).toHaveBeenCalledWith(chalk.yellow('There are no migrations to list'))
})
it('should run list command with pending migrations', async () => {
await exec('create', 'migration-name-test', ...commandLineOptions)
const consoleSpy = vi.spyOn(console, 'log')
const opts = await exec('list', ...commandLineOptions)
expect(opts?.uri).toBe(uri)
expect(consoleSpy).toHaveBeenCalledWith(chalk.cyan('Listing migrations'))
expect(consoleSpy).toHaveBeenCalledWith(expect.stringMatching(/migration-name-test/))
})
it('should run create command', async () => {
const consoleSpy = vi.spyOn(console, 'log')
const opts = await exec('create', 'migration-name-test', ...commandLineOptions)
expect(opts?.uri).toBe(uri)
expect(consoleSpy).toHaveBeenCalledWith(expect.stringMatching(/^Created migration migration-name-test in/))
})
it('should run up command', async () => {
const migrationName = 'migration-name-test'
await exec('create', migrationName, ...commandLineOptions)
const consoleSpy = vi.spyOn(console, 'log')
const opts = await exec('up', ...commandLineOptions)
expect(opts?.uri).toBe(uri)
expect(consoleSpy).toHaveBeenCalledWith(expect.stringMatching(/up:/))
expect(consoleSpy).toHaveBeenCalledWith(expect.stringMatching(migrationName))
expect(consoleSpy).toHaveBeenCalledWith(chalk.green('All migrations finished successfully'))
})
it('should run down command', async () => {
const migrationName = 'migration-name-test'
await exec('create', migrationName, ...commandLineOptions)
await exec('up', migrationName, ...commandLineOptions)
const consoleSpy = vi.spyOn(console, 'log')
const opts = await exec('down', 'migration-name-test', ...commandLineOptions)
expect(opts?.uri).toBe(uri)
expect(consoleSpy).toHaveBeenCalledWith(expect.stringMatching(migrationName))
expect(consoleSpy).toHaveBeenCalledWith(chalk.green('All migrations finished successfully'))
})
})
it('should throw "You need to provide the MongoDB Connection URI to persist migration status.\nUse option --uri / -d to provide the URI."', async () => {
await expect(exec('up', 'invalid-migration-name')).rejects.toThrow(chalk.red('You need to provide the MongoDB Connection URI to persist migration status.\nUse option --uri / -d to provide the URI.'))
})
it('should prune command', async () => {
await exec('create', 'migration-name-prune', ...commandLineOptions)
await exec('up', 'migration-name-prune', '-a', 'true', ...commandLineOptions)
await clearDirectory(migrationsPath)
const consoleSpy = vi.spyOn(console, 'log')
const opts = await exec('prune', '-a', 'true', ...commandLineOptions)
expect(consoleSpy).toHaveBeenCalledWith(expect.stringMatching(/Removing migration\(s\) from database/))
expect(opts?.uri).toBe(uri)
expect(opts?.autosync).toBe('true')
})
it('should exit with code 1', async () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation((number) => {
throw new Error(`process.exit: ${number}`)
})
await expect(execExit('up', ...commandLineOptions)).rejects.toThrow()
expect(mockExit).toHaveBeenCalledWith(1)
mockExit.mockRestore()
})
it('should exit with code 0', async () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation((number) => {
throw new Error(`process.exit: ${number}`)
})
await expect(execExit('list', ...commandLineOptions)).rejects.toThrow()
expect(mockExit).toHaveBeenCalledWith(0)
mockExit.mockRestore()
})
it('should log no pending migrations', async () => {
await exec('create', 'test-migration', ...commandLineOptions)
await exec('up', ...commandLineOptions)
const consoleSpy = vi.spyOn(console, 'log')
await exec('up', ...commandLineOptions)
expect(consoleSpy).toHaveBeenCalledWith(chalk.yellow('There are no pending migrations'))
})
it('should throw "The \'up\' export is not defined in"', async () => {
await connection.collection('migrations').deleteMany({})
fs.appendFileSync(`${migrationsPath}/template.ts`, 'export function down () { /* do nothing */ }')
await exec('create', 'test-migration', '-t', `${migrationsPath}/template.ts`, ...commandLineOptions)
await expect(exec('up', ...commandLineOptions)).rejects.toThrow(/The 'up' export is not defined in/)
})
it('should throw "Failed to run migration"', async () => {
await connection.collection('migrations').deleteMany({})
fs.appendFileSync(`${migrationsPath}/template.ts`, 'export function up () { throw new Error("Failed to run migration") }')
await exec('create', 'test-migration', '-t', `${migrationsPath}/template.ts`, ...commandLineOptions)
await expect(exec('up', ...commandLineOptions)).rejects.toThrow(/Failed to run migration/)
})
it('should disable strict', async () => {
await connection.collection('migrations').deleteMany({})
const testModel = `import { Schema, model, models } from 'mongoose'
export interface IExample {
name: string
type: Date
}
export const ExampleSchema = new Schema({
name: {
type: Schema.Types.String,
required: true,
},
type: {
type: Schema.Types.Number,
required: true,
},
})
export default models.User ?? model<IExample>('Example', ExampleSchema)`
const testTemplate = template
.replace('// Import your schemas here', `import { ExampleSchema } from './Example.ts'`)
.replace(
'// Write migration here',
`const Example = connection.model('Example', ExampleSchema)
await Example.insertMany([
{
name: 'test',
type: 1,
},
{
name: 'test2',
type: 1,
},
{
name: 'test3',
type: 2,
},
{
name: 'test4',
type: 2,
}
])`,
)
.replace(
'// Write migration here',
`const Example = connection.model('Example', ExampleSchema)
await Example.deleteMany({ propertyIsNotDefinedInSchema: 'some-value' })
await Example.deleteMany({ type: 1 })`,
)
console.log(testModel)
console.log(testTemplate)
fs.appendFileSync(`${migrationsPath}/Example.ts`, testModel)
fs.appendFileSync(`${migrationsPath}/template.ts`, testTemplate)
await exec('create', 'test-migration', '-t', `${migrationsPath}/template.ts`, ...commandLineOptions)
await exec('up', 'test-migration', ...commandLineOptions)
const countUp = await connection.collection('examples').countDocuments()
expect(countUp).toBe(4)
await exec('down', 'test-migration', ...commandLineOptions)
const countDown = await connection.collection('examples').countDocuments()
expect(countDown).toBe(2)
})
})