mongoshift
v0.1.0
Published
MongoDB migration tool with dry-run, file-hash detection, and stored logs
Maintainers
Readme
mongoshift
A MongoDB migration tool with dry-run, file-hash drift detection, stored logs, and first-class TypeScript support.
Highlights
- Dry-run via
up({ dryRun: true })and--dry-runCLI flag - Stored logs: every migration's
ctx.loggeroutput is persisted inline in the changelog entry (level, message, timestamp, duration) - Optional file-hash drift detection (
useFileHash: true) — refuses to run if an applied migration's file has changed - Configurable changelog collection name, timestamp
dateFormat, custom migration templates (--templateorsample-migration.ts) - TS-native single package, compiled to ESM +
.d.ts - Node 24+, ESM only
Install
npm install mongoshift mongodb
pnpm add mongoshift mongodb
yarn add mongoshift mongodb
bun add mongoshift mongodbQuick start
npx mongoshift init # creates mongoshift.config.ts + migrations/
npx mongoshift create "add users" # creates 20260405090703-add_users.ts
npx mongoshift up # applies all pending
npx mongoshift status # shows PENDING / APPLIED / CHANGED
npx mongoshift down # rolls back the last oneA migration file looks like this:
import type { Db, MongoClient } from "mongodb";
import type { MigrationContext } from "mongoshift";
export const up = async (db: Db, client: MongoClient, ctx: MigrationContext) => {
ctx.logger.log("creating users collection");
if (ctx.dryRun) return; // optional: skip side-effects when dry-running
await db.createCollection("users");
};
export const down = async (db: Db, client: MongoClient, ctx: MigrationContext) => {
await db.collection("users").drop();
};Programmatic API
import { loadConfig, connect, up, down, status } from "mongoshift";
const config = await loadConfig();
const { db, client, close } = await connect(config);
try {
const result = await up(db, client, config, { dryRun: true });
// result.migrations: [{ fileName, logs, durationMs, applied }]
} finally {
await close();
}Config reference (short)
// mongoshift.config.ts
import type { Config } from "mongoshift";
const config: Config = {
mongodb: { url: "mongodb://localhost:27017", databaseName: "my_db" },
migrationsDir: "migrations",
migrationFileExtension: ".ts",
dateFormat: "YYYYMMDDHHmmss", // dayjs tokens (wrap literals in [brackets])
changelogCollectionName: "changelog",
useFileHash: false,
};
export default config;Migrating from migrate-mongo
mongoshift is API-compatible in spirit but has a few deliberate breaks.
1. Config file
| migrate-mongo | mongoshift |
| ------------------------------- | --------------------------------------------- |
| migrate-mongo-config.js | mongoshift.config.ts (or .js) |
| CommonJS by default | ESM only |
| moduleSystem: "commonjs" | removed (ESM only) |
| lockCollectionName, lockTtl | removed (for now) |
| no dateFormat | dateFormat: "YYYYMMDDHHmmss" (configurable) |
Everything else maps 1:1: mongodb, migrationsDir, migrationFileExtension,
changelogCollectionName, useFileHash.
2. Migration signature — breaking change
migrate-mongo:
export const up = async (db, client) => {
/* ... */
};mongoshift adds a third ctx argument:
export const up = async (db, client, ctx) => {
ctx.logger.log("..."); // persisted in changelog
if (ctx.dryRun) return; // respect dry-run
};Old migrations that ignore the 3rd parameter keep working unchanged — the logger and dryRun flag are simply unused. You only need to touch your files if you want to use those features.
3. CLI command mapping
| migrate-mongo | mongoshift |
| ----------------------------- | ------------------------------------------ |
| migrate-mongo init | mongoshift init |
| migrate-mongo create <name> | mongoshift create <name> [-t template] |
| migrate-mongo up | mongoshift up [--dry-run] [--force-hash] |
| migrate-mongo down | mongoshift down [--dry-run] [--block] |
| migrate-mongo status | mongoshift status |
4. Keeping existing changelog entries
The changelog schema is a superset: existing migrate-mongo documents
({ fileName, appliedAt, migrationBlock, fileHash? }) are read correctly.
New fields (logs: [], durationMs) are added as new migrations run. You
can switch without dropping the collection.
If you want to backfill the new fields on old entries for consistency, run once:
await db
.collection("changelog")
.updateMany({ logs: { $exists: false } }, { $set: { logs: [], durationMs: 0 } });5. Dropped features (for now)
- Locks (
lockCollectionName/lockTtl) — planned; use an external lock for now if you run concurrent deploys. - CJS support — ESM only.
Coming from other tools
- umzug (generic, covers MongoDB):
umzug's storage is storage-agnostic; here the storage is always the
configured MongoDB
changelogCollectionName. Replace yourup/downsignature with(db, client, ctx). - node-migrate: state is stored in a
.jsonfile on disk; switch to a MongoDB collection viachangelogCollectionName. Migration files become async ESM modules. - Custom scripts in
scripts/migrate.ts: port them 1-to-1, they just become the bodies ofup()/down().
Roadmap
- Distributed lock (TTL-based, configurable)
- Website with full API docs, guides, recipes
- Transactions helper for multi-statement migrations
License
MIT
