Skip to content

Instantly share code, notes, and snippets.

@bitflower
Last active April 30, 2021 00:00
Show Gist options
  • Save bitflower/aa06462e6d70a9192949e6f601a38948 to your computer and use it in GitHub Desktop.
Save bitflower/aa06462e6d70a9192949e6f601a38948 to your computer and use it in GitHub Desktop.
MongoDB migrations with umzug in a FeathersJS application
import { Service } from '@feathersjs/feathers';
import { App } from '../src/app.interface';
import { Transformation } from '../src/services/transformations/transformations.interface';
export default {
up: async (app: App): Promise<boolean> => {
// Get service
const s: Service<Transformation> = app.service('transformations');
// Read all content atoms that have properties
const newTransformations: Transformation[] = [
{
category: 'conditionals',
functionCode: 'compareSwitch',
name: 'compareSwitch',
returnType: 'Mixed',
args: [
{
name: 'value1',
propertyType: 'Mixed',
required: true
},
{
name: 'value2',
propertyType: 'Mixed',
required: true
},
{
name: 'eqValue',
propertyType: 'Mixed',
required: true
},
{
name: 'ltValue',
propertyType: 'Mixed',
required: true
},
{
name: 'gtValue',
propertyType: 'Mixed',
required: true
}
]
} as any // remove for type checking while developing
];
for (const transformation of newTransformations) {
console.log(`Creating transformation ${transformation.name}`);
await s.create(transformation);
}
return true;
},
down: (app: App) => {
// a
}
};
const Bluebird = require('bluebird');
Bluebird.config({ longStackTraces: true, warnings: true });
global.Promise = Bluebird;
import App from '../index'; // '../app';
import program from 'commander';
import Umzug, { Migration } from 'umzug';
import { Mongoose, Connection } from 'mongoose';
import path from 'path';
import { Application } from '@feathersjs/express';
import { inspect } from 'util';
// import leakedHandles from "leaked-handles";
// leakedHandles.set({
// fullStack: true, // use full stack traces
// timeout: 30000, // run every 30 seconds instead of 5.
// debugSockets: true // pretty print tcp thrown exceptions.
// });
function getMigrationName(migration: Migration) {
return path.basename(migration.path, '.ts');
}
async function getStatus(umzug: Umzug.Umzug) {
const executed = (await umzug.executed()).map(getMigrationName);
const pending = (await umzug.pending()).map(getMigrationName);
const current = executed.length > 0 ? executed[0] : '<NO_MIGRATIONS>';
return { current, executed, pending };
}
async function init() {
const app = App();
const mongoose = app.get('mongooseClient') as Mongoose;
await waitConnected(mongoose.connection);
const umzug = createUmzug(app, mongoose);
return { app, mongoose, umzug };
}
function createUmzug(app: Application<any>, mongoose: Mongoose) {
const umzug = new Umzug({
migrations: {
params: [app],
pattern: /\.ts$/
},
storage: 'mongodb',
storageOptions: {
connection: mongoose.connection,
collectionName: '_migrations'
},
logging(...args: any[]) {
console.log(...args);
}
});
function logUmzugEvent(eventName: string) {
return function(name: string, migration: Umzug.Migration) {
console.log(`${name} ${eventName}`);
};
}
umzug.on('migrating', logUmzugEvent('migrating'));
umzug.on('migrated', logUmzugEvent('migrated'));
umzug.on('reverting', logUmzugEvent('reverting'));
umzug.on('reverted', logUmzugEvent('reverted'));
return umzug;
}
function handleError(reason: any, p: any) {
// tslint:disable-next-line:no-console
console.error(
'Unhandled Rejection at:',
p,
'reason:',
inspect(reason, { showHidden: false, depth: 6 })
);
process.exit(1);
}
function waitConnected(connection: Connection) {
return new Promise((resolve, reject) => {
setTimeout(
() =>
reject(
new Error('Mongo connection took too long, please check for errors.')
),
5000
);
connection.on('connected', () => resolve());
});
}
const errorEvents: any[] = [
'unhandledRejection',
'uncaughtException',
'SIGINT'
];
errorEvents.map(eventName => process.on(eventName, handleError));
program
.command('up')
.description('Migrate the database to the last version.')
.action(async () => {
const { mongoose, umzug } = await init();
const appliedMigrations = await umzug.up();
if (appliedMigrations.length === 0) {
console.log('No migrations were applied.');
}
await mongoose.disconnect();
});
try {
program
.command('reset')
.description('Reverts the database to the initial state')
.action(async () => {
const { umzug, mongoose } = await init();
await umzug.down({ to: 0 });
await mongoose.disconnect();
});
} catch (e) {
console.error(e);
process.exit(1);
}
program
.command('status')
.description('Shows which migrations were applied and which not')
.action(async () => {
const { umzug, mongoose } = await init();
const status = await getStatus(umzug);
console.log(JSON.stringify(status, null, 2));
await mongoose.disconnect();
});
program
.command('prev')
.description('Goes back to the previous version of the database')
.action(async () => {
const { umzug, mongoose } = await init();
const executed = await umzug.executed();
if (executed.length === 0) {
throw new Error('Already at initial state');
}
const prev = getMigrationName(executed[executed.length - 1]);
console.log('prev >>>', prev);
await umzug.down({ to: prev });
await mongoose.disconnect();
});
// error on unknown commands
program.on('command:*', function() {
console.error(
'Invalid command: %s\nSee --help for a list of available commands.',
program.args.join(' ')
);
process.exit(1);
});
program.parse(process.argv);
if (program.args.length === 0) {
program.help();
}
@dallinbjohnson
Copy link

Thank you so much for your example. I am working on this very problem. I do have a small question ...

Your example refers to a service:

app.service('transformations')

However, your code does not include the code for this service. Can you provide the code for this? Or do you have more information that might aid me in following after your approach. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment