UNPKG

ts-migrate-mongoose

Version:

A migration framework for Mongoose, built with TypeScript.

225 lines (195 loc) 8.63 kB
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) }) })