UNPKG

ts-migrate-mongoose

Version:

A migration framework for Mongoose, built with TypeScript.

647 lines (541 loc) 22.5 kB
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import fs from 'node:fs' import { checkbox } from '@inquirer/prompts' import mongoose, { type Connection, Types } from 'mongoose' import { getConfig } from '../src/commander' import { Migrator } from '../src/index' import { template } from '../src/template' import { create } from './mongo/server' import { clearDirectory } from './utils/filesystem' vi.mock('@inquirer/prompts', () => ({ checkbox: vi.fn().mockResolvedValue(['1']), })) describe('Tests for Migrator class - Programmatic approach', async () => { const { uri, destroy } = await create('migrator') let connection: Connection afterAll(async () => { await clearDirectory('migrations') await destroy() }) beforeEach(async () => { await clearDirectory('migrations') connection = await mongoose.createConnection(uri).asPromise() await connection.collection('migrations').deleteMany({}) }) it('should return [] if "There are no pending migrations"', async () => { const migrator = await Migrator.connect({ uri }) expect(migrator).toBeInstanceOf(Migrator) expect(migrator.connection.readyState).toBe(1) const migrations = await migrator.run('up') expect(migrations).toEqual([]) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should throw "There is already a migration with name \'create-users\' in the database"', async () => { const migrationName = 'create-users' const migrator = await Migrator.connect({ uri }) expect(migrator).toBeInstanceOf(Migrator) expect(migrator.connection.readyState).toBe(1) const migration = await migrator.create(migrationName) expect(migration.filename).toContain(migrationName) await expect(migrator.create(migrationName)).rejects.toThrow(`There is already a migration with name '${migrationName}' in the database`) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should throw "Could not find migration with name \'create-unicorns\' in the database"', async () => { const migrationName = 'create-unicorns' const migrator = await Migrator.connect({ uri, cli: true }) expect(migrator).toBeInstanceOf(Migrator) expect(migrator.connection.readyState).toBe(1) await expect(migrator.run('down', migrationName)).rejects.toThrow(`Could not find migration with name '${migrationName}' in the database`) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should create migrator with mongoose connection', async () => { const migrator = await Migrator.connect({ uri }) expect(migrator).toBeInstanceOf(Migrator) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should create migrator with uri', async () => { const migrator = await Migrator.connect({ uri }) expect(migrator).toBeInstanceOf(Migrator) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should insert a doc into collection with migrator', async () => { const migrator = await Migrator.connect({ uri, autosync: true }) await migrator.prune() const migrationName = `test-migration-creation-${new Types.ObjectId().toHexString()}` const migration = await migrator.create(migrationName) expect(migration.filename).toContain(migrationName) expect(migration.name).toBe(migrationName) // Migrate Up await migrator.run('up', migrationName) const foundUp = await migrator.migrationModel.findById(migration._id) expect(foundUp?.state).toBe('up') expect(foundUp?.name).toBe(migrationName) // Migrate Down await migrator.run('down', migrationName) const foundDown = await migrator.migrationModel.findById(migration._id) expect(foundDown?.state).toBe('down') expect(foundDown?.name).toBe(migrationName) // List Migrations const migrationList = await migrator.list() expect(migrationList).toEqual( expect.arrayContaining([ expect.objectContaining({ name: migrationName, }), ]), ) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should prune all migrations', async () => { const migrator = await Migrator.connect({ uri, autosync: true }) const migrationName = 'migration-creation' const migration = await migrator.create(migrationName) expect(migration.filename).toContain(migrationName) expect(migration.name).toBe(migrationName) // Migrate Up await migrator.run('up', migrationName) const foundUp = await migrator.migrationModel.findById(migration._id) expect(foundUp?.state).toBe('up') expect(foundUp?.name).toBe(migrationName) await clearDirectory('migrations') // Prune const migrations = await migrator.prune() expect(migrations[0]?.state).toBe('up') expect(migrations[0]?.name).toBe(migrationName) const migrationsInDB = await migrator.migrationModel.find({}) expect(migrationsInDB).toHaveLength(0) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should throw "No mongoose connection or mongo uri provided to migrator"', async () => { await expect(Migrator.connect({ uri: '' })).rejects.toThrow('No mongoose connection or mongo uri provided to migrator') }) it('should ensure migrations path', async () => { fs.rmSync('migrations', { recursive: true }) const migrator = await Migrator.connect({ uri }) expect(migrator).toBeInstanceOf(Migrator) expect(fs.existsSync('migrations')).toBe(true) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should throw "Failed to run migration"', async () => { const migrator = await Migrator.connect({ uri }) const migration = await migrator.migrationModel.create({ name: 'test-migration', createdAt: new Date(), }) // @ts-expect-error - private method await expect(migrator.runMigrations([migration], 'up')).rejects.toThrow() expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should log "Adding migration"', async () => { const migrator = await Migrator.connect({ uri }) const migration = await migrator.create('test-migration') await clearDirectory('migrations') // @ts-expect-error - private method const migrations = await migrator.syncMigrations([migration.filename]) expect(migrations).toHaveLength(1) expect(migrations[0]?.name).toBe('test-migration') expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should choose first', async () => { const migrator = await Migrator.connect({ uri }) // @ts-expect-error - private method const answers = await migrator.choseMigrations(['1', '2', '3'], 'Message') expect(checkbox).toHaveBeenCalled() expect(answers).toEqual(['1']) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should choose all', async () => { const migrator = await Migrator.connect({ uri, autosync: true }) // @ts-expect-error - private method const answers = await migrator.choseMigrations(['1', '2', '3'], 'Message') expect(checkbox).toHaveBeenCalled() expect(answers).toEqual(['1', '2', '3']) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should throw on sync', async () => { const migrator = await Migrator.connect({ uri, autosync: true }) // @ts-expect-error - private method vi.spyOn(migrator, 'getMigrations').mockImplementation(() => { throw new Error('Sync error') }) await expect(migrator.sync()).rejects.toThrow('Sync error') expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should run sync and find 3 migrations', async () => { const migrator = await Migrator.connect({ uri, autosync: true }) await migrator.create('test-migration1') await migrator.create('test-migration2') await migrator.create('test-migration3') await migrator.migrationModel.deleteMany({}) const migrations = await migrator.sync() expect(migrations).toBeInstanceOf(Array) expect(migrations).toHaveLength(3) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should run sync and find 0 migrations', async () => { const migrator = await Migrator.connect({ uri, autosync: true }) await migrator.migrationModel.deleteMany({}) await clearDirectory('migrations') const migrations = await migrator.sync() expect(migrations).toBeInstanceOf(Array) expect(migrations).toHaveLength(0) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should run prune and find 2 migrations in db that no longer exits in file system', async () => { const migrator = await Migrator.connect({ uri, autosync: true }) await migrator.create('test-migration1') await migrator.create('test-migration2') await migrator.run('up', 'test-migration1') await migrator.run('up', 'test-migration2') await clearDirectory('migrations') const migrations = await migrator.prune() console.log(migrations) expect(migrations).toBeInstanceOf(Array) expect(migrations).toHaveLength(2) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should run prune and find 0 migrations in db that no longer exits in file system', async () => { const migrator = await Migrator.connect({ uri, autosync: true }) await migrator.create('test-migration1') await migrator.create('test-migration2') await migrator.run('up', 'test-migration1') await migrator.run('up', 'test-migration2') const migrations = await migrator.prune() expect(migrations).toBeInstanceOf(Array) expect(migrations).toHaveLength(0) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should throw on prune', async () => { const migrator = await Migrator.connect({ uri, autosync: true }) // @ts-expect-error - private method vi.spyOn(migrator, 'getMigrations').mockImplementation(() => { throw new Error('Sync error') }) await expect(migrator.prune()).rejects.toThrow('Sync error') expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should get migrations', async () => { const migrator = await Migrator.connect({ uri }) await migrator.create('test-migration1') await migrator.create('test-migration2') await migrator.create('test-migration3') await migrator.run('up', 'test-migration1') // @ts-expect-error - private method const { migrationsInDb, migrationsInFs } = await migrator.getMigrations() expect(migrationsInDb).toHaveLength(3) expect(migrationsInDb[0]?.name).toBe('test-migration1') expect(migrationsInDb[0]?.state).toBe('up') expect(migrationsInDb[0]?.filename).toMatch(/^\d{13,}-test-migration1/) expect(migrationsInDb[1]?.name).toBe('test-migration2') expect(migrationsInDb[1]?.state).toBe('down') expect(migrationsInDb[1]?.filename).toMatch(/^\d{13,}-test-migration2/) expect(migrationsInDb[2]?.name).toBe('test-migration3') expect(migrationsInDb[2]?.state).toBe('down') expect(migrationsInDb[2]?.filename).toMatch(/^\d{13,}-test-migration3/) expect(migrationsInFs).toHaveLength(3) expect(migrationsInFs[0]?.filename).toMatch(/^\d{13,}-test-migration1/) expect(migrationsInFs[0]?.existsInDatabase).toBe(true) expect(migrationsInFs[1]?.filename).toMatch(/^\d{13,}-test-migration2/) expect(migrationsInFs[1]?.existsInDatabase).toBe(true) expect(migrationsInFs[2]?.filename).toMatch(/^\d{13,}-test-migration3/) expect(migrationsInFs[2]?.existsInDatabase).toBe(true) expect(migrationsInDb[0]?.filename).toBe(migrationsInFs[0]?.filename) expect(migrationsInDb[1]?.filename).toBe(migrationsInFs[1]?.filename) expect(migrationsInDb[2]?.filename).toBe(migrationsInFs[2]?.filename) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should create migrator instance with wrong template path and fallback to default template', async () => { const migrator = await Migrator.connect({ uri, templatePath: 'wrong/path' }) const migration = await migrator.create('test-migration') expect(migration.filename).toMatch(/^\d{13,}-test-migration/) const migrationContent = fs.readFileSync(`migrations/${migration.filename}.ts`, 'utf8') expect(template).toMatch(migrationContent) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should run up and run down', async () => { const migrator = await Migrator.connect({ uri }) const migration = await migrator.create('test-migration') expect(migration.filename).toMatch(/^\d{13,}-test-migration/) await migrator.run('up', 'test-migration') await migrator.run('down') expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should run up all 3 at oce and run down one by one using migrate down', async () => { const migrator = await Migrator.connect({ uri, cli: true }) const migration1 = await migrator.create('test-migration1') const migration2 = await migrator.create('test-migration2') const migration3 = await migrator.create('test-migration3') expect(migration1.filename).toMatch(/^\d{13,}-test-migration1/) expect(migration2.filename).toMatch(/^\d{13,}-test-migration2/) expect(migration3.filename).toMatch(/^\d{13,}-test-migration3/) await migrator.run('up') const migrationListUp = await migrator.list() expect(migrationListUp).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'test-migration1', state: 'up', }), expect.objectContaining({ name: 'test-migration2', state: 'up', }), expect.objectContaining({ name: 'test-migration3', state: 'up', }), ]), ) await migrator.run('down', undefined, true) const migrationListDown1 = await migrator.list() expect(migrationListDown1).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'test-migration1', state: 'up', }), expect.objectContaining({ name: 'test-migration2', state: 'up', }), expect.objectContaining({ name: 'test-migration3', state: 'down', }), ]), ) await migrator.run('down', undefined, true) const migrationListDown2 = await migrator.list() expect(migrationListDown2).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'test-migration1', state: 'up', }), expect.objectContaining({ name: 'test-migration2', state: 'down', }), expect.objectContaining({ name: 'test-migration3', state: 'down', }), ]), ) await migrator.run('down', undefined, true) const migrationListDown3 = await migrator.list() expect(migrationListDown3).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'test-migration1', state: 'down', }), expect.objectContaining({ name: 'test-migration2', state: 'down', }), expect.objectContaining({ name: 'test-migration3', state: 'down', }), ]), ) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should get migration .ts files', async () => { const config = await getConfig('./examples/config-file-usage/src/migrate.ts') const migrator = await Migrator.connect({ ...config, uri, migrationsPath: './examples/config-file-usage/src/migrations' }) // @ts-expect-error - private method const { migrationsInFs } = await migrator.getMigrations() expect(migrationsInFs).toHaveLength(1) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should get migration .js files', async () => { const config = await getConfig('./examples/config-file-usage/dist/migrate.js') const migrator = await Migrator.connect({ ...config, uri, migrationsPath: './examples/config-file-usage/dist/migrations' }) // @ts-expect-error - private method const { migrationsInFs } = await migrator.getMigrations() expect(migrationsInFs).toHaveLength(1) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should same filename when migrate using .ts files', async () => { const config = await getConfig('./examples/config-file-usage/src/migrate.ts') const migrator = await Migrator.connect({ ...config, uri, migrationsPath: './examples/config-file-usage/src/migrations', autosync: true, }) // @ts-expect-error - private method const { migrationsInFs } = await migrator.getMigrations() const migrations = await migrator.sync() expect(migrationsInFs[0]?.filename).toBe(migrations[0]?.filename) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should same filename when migrate using .js files', async () => { const config = await getConfig('./examples/config-file-usage/dist/migrate.js') const migrator = await Migrator.connect({ ...config, uri, migrationsPath: './examples/config-file-usage/dist/migrations', autosync: true, }) // @ts-expect-error - private method const { migrationsInFs } = await migrator.getMigrations() const migrations = await migrator.sync() expect(migrationsInFs[0]?.filename).toBe(migrations[0]?.filename) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should run up/down when migrate using .ts files', async () => { const config = await getConfig('./examples/config-file-usage/src/migrate.ts') const migrator = await Migrator.connect({ ...config, uri, migrationsPath: './examples/config-file-usage/src/migrations', autosync: true, }) await migrator.run('up') const migrationListUp = await migrator.list() expect(migrationListUp).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'first-migration-demo', state: 'up', }), ]), ) const newUser = await migrator.connection.collection('users').find({}).toArray() expect(newUser).toEqual( expect.arrayContaining([ expect.objectContaining({ firstName: 'John', lastName: 'Doe', }), expect.objectContaining({ firstName: 'Jane', lastName: 'Doe', }), ]), ) await migrator.run('down', undefined, true) const migrationListDown = await migrator.list() expect(migrationListDown).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'first-migration-demo', state: 'down', }), ]), ) const deletedUsers = await migrator.connection .collection('users') .find({ firstName: { $in: ['Jane', 'John'] } }) .toArray() expect(deletedUsers).toHaveLength(0) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) it('should run up/down when migrate using .js files', async () => { const config = await getConfig('./examples/config-file-usage/dist/migrate.js') const migrator = await Migrator.connect({ ...config, uri, migrationsPath: './examples/config-file-usage/dist/migrations', autosync: true, }) await migrator.run('up') const migrationListUp = await migrator.list() expect(migrationListUp).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'first-migration-demo', state: 'up', }), ]), ) const newUser = await migrator.connection.collection('users').find({}).toArray() expect(newUser).toEqual( expect.arrayContaining([ expect.objectContaining({ firstName: 'John', lastName: 'Doe', }), expect.objectContaining({ firstName: 'Jane', lastName: 'Doe', }), ]), ) await migrator.run('down', undefined, true) const migrationListDown = await migrator.list() expect(migrationListDown).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'first-migration-demo', state: 'down', }), ]), ) const deletedUsers = await migrator.connection .collection('users') .find({ firstName: { $in: ['Jane', 'John'] } }) .toArray() expect(deletedUsers).toHaveLength(0) expect(migrator.connection.readyState).toBe(1) await migrator.close() expect(migrator.connection.readyState).toBe(0) }) })