@alexify/migronaut 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +128 -0
- package/LICENSE +22 -0
- package/README.md +773 -0
- package/bin/migronaut.js +36 -0
- package/index.d.ts +903 -0
- package/index.js +1 -0
- package/migronaut.schema.json +135 -0
- package/package.json +105 -0
- package/src/cli/args.js +314 -0
- package/src/cli/commands/audit.js +40 -0
- package/src/cli/commands/create.js +29 -0
- package/src/cli/commands/down.js +30 -0
- package/src/cli/commands/dry-run.js +36 -0
- package/src/cli/commands/import.js +33 -0
- package/src/cli/commands/init.js +64 -0
- package/src/cli/commands/list.js +19 -0
- package/src/cli/commands/lock.js +35 -0
- package/src/cli/commands/redo.js +16 -0
- package/src/cli/commands/status.js +52 -0
- package/src/cli/commands/unlock.js +43 -0
- package/src/cli/commands/up.js +53 -0
- package/src/cli/exit-codes.js +38 -0
- package/src/cli/index.js +91 -0
- package/src/cli/shared.js +343 -0
- package/src/cli/spinner.js +59 -0
- package/src/cli/table.js +259 -0
- package/src/core/audit.js +152 -0
- package/src/core/changelog.js +231 -0
- package/src/core/config.js +479 -0
- package/src/core/context.js +16 -0
- package/src/core/import-runner.js +164 -0
- package/src/core/import.js +60 -0
- package/src/core/lock.js +348 -0
- package/src/core/migrator.js +1475 -0
- package/src/core/run.js +144 -0
- package/src/core/runner.js +150 -0
- package/src/errors/index.js +215 -0
- package/src/index.js +59 -0
- package/src/utils/checksum.js +23 -0
- package/src/utils/colors.js +68 -0
- package/src/utils/concurrency.js +32 -0
- package/src/utils/date.js +28 -0
- package/src/utils/env.js +51 -0
- package/src/utils/error.js +11 -0
- package/src/utils/loader.js +131 -0
- package/src/utils/logger.js +109 -0
- package/src/utils/redact.js +39 -0
- package/src/utils/sanitize.js +43 -0
- package/src/utils/template.js +615 -0
- package/src/utils/user.js +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Alexis-Technologies/migronaut/main/assets/logo-icon-b-alt2.png" alt="@alexify/migronaut" width="420" />
|
|
4
|
+
|
|
5
|
+
# migronaut
|
|
6
|
+
|
|
7
|
+
**Elegant, fast, fully-typed, zero-dependency MongoDB migrations for Node.js.**
|
|
8
|
+
|
|
9
|
+
_Adopt an existing `migrate-mongo` changelog in one command — then get the controls it never had._
|
|
10
|
+
|
|
11
|
+
[](https://www.npmjs.com/package/@alexify/migronaut)
|
|
12
|
+
[](https://github.com/Alexis-Technologies/migronaut/actions/workflows/ci.yml)
|
|
13
|
+
[](#quick-start)
|
|
14
|
+
[](#reasons-to-choose-it)
|
|
15
|
+
[](https://migronaut.vercel.app/)
|
|
16
|
+
[](./LICENSE)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Precise, safe migrations for MongoDB. Run a single file, roll back anything, and preview every
|
|
20
|
+
change before it touches your database.
|
|
21
|
+
|
|
22
|
+
### 📖 [Read the documentation →](https://migronaut.vercel.app/)
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Reasons to choose it
|
|
29
|
+
|
|
30
|
+
- **Zero dependencies** — no runtime dependencies at all; only the `mongodb` driver as a peer.
|
|
31
|
+
Instant installs, nothing extra in your lockfile, no supply-chain surface.
|
|
32
|
+
- **Run a single migration** — `migronaut up <file>`, not just "all pending".
|
|
33
|
+
- **Roll back anything** — a batch (`--batch 3`), the last N (`--steps 2`), one file, or `redo`.
|
|
34
|
+
- **Preview before you run** — `migronaut dry-run up` prints the exact plan without touching the database.
|
|
35
|
+
- **No race conditions** — an atomic MongoDB lock stops two deploys running migrations at once.
|
|
36
|
+
- **Tamper detection** — SHA-256 checksums catch a migration edited after it was applied.
|
|
37
|
+
- **Audit trail kept** — a rollback updates the record, it never deletes it.
|
|
38
|
+
- **Lifecycle hooks** — `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, `onError`.
|
|
39
|
+
- **Opt-in transactions** — wrap a migration so it fully commits or fully aborts.
|
|
40
|
+
- **TypeScript, ESM & CommonJS** — all run with no `ts-node` plumbing.
|
|
41
|
+
- **Zero config files required** — drive everything from env vars if you prefer.
|
|
42
|
+
- **Pino-friendly logging** — the `logger` option is pino-compatible; pass a pino instance directly
|
|
43
|
+
and migronaut logs through it (with a `component: 'migronaut'` child binding).
|
|
44
|
+
|
|
45
|
+
### How it compares to `migrate-mongo`
|
|
46
|
+
|
|
47
|
+
| Capability | `migrate-mongo` | `migronaut` |
|
|
48
|
+
| ----------------------------------------------- | :-------------: | :-----------------: |
|
|
49
|
+
| `up` / `down` / `create` / `status` | ✅ | ✅ |
|
|
50
|
+
| Run a single migration file | ❌ | ✅ |
|
|
51
|
+
| Roll back a specific batch (not just the last) | ❌ | ✅ |
|
|
52
|
+
| Dry-run preview | ❌ | ✅ |
|
|
53
|
+
| `redo` (down + up) | ❌ | ✅ |
|
|
54
|
+
| SHA-256 checksum / tamper detection | ❌ | ✅ |
|
|
55
|
+
| Lifecycle hooks | ❌ | ✅ |
|
|
56
|
+
| First-class TypeScript (built-in) | ❌ | ✅ |
|
|
57
|
+
| History preserved on rollback (never deleted) | ❌ | ✅ |
|
|
58
|
+
| Adopt an existing `migrate-mongo` changelog | — | ✅ `migronaut import` |
|
|
59
|
+
|
|
60
|
+
<sub>Reflects `migrate-mongo`'s documented CLI as of mid-2026. It has since added transaction access
|
|
61
|
+
via a `client` argument; `migronaut` exposes the same plus a declarative per-file `useTransaction` flag.</sub>
|
|
62
|
+
|
|
63
|
+
### How it compares to `mongo-migrate-kit`
|
|
64
|
+
|
|
65
|
+
`migronaut` is a fork of [`mongo-migrate-kit`](https://www.npmjs.com/package/mongo-migrate-kit)
|
|
66
|
+
(CLI `mmk`) by Santosh Gupta. The fork was not a rename — everything below landed after it:
|
|
67
|
+
|
|
68
|
+
| Capability | `mongo-migrate-kit` | `migronaut` |
|
|
69
|
+
| ----------------------------------------------- | :-------------: | :-----------------: |
|
|
70
|
+
| Runtime dependencies | 6 | **0** |
|
|
71
|
+
| Ships as | bundled `dist/` | source, no build step |
|
|
72
|
+
| `--json` on every command | ❌ | ✅ |
|
|
73
|
+
| Typed exit code per error (`EXIT_CODES`) | ❌ | ✅ |
|
|
74
|
+
| `status --check` deploy gate | ❌ | ✅ |
|
|
75
|
+
| `audit` / `lock` commands | ❌ | ✅ |
|
|
76
|
+
| `up --to` / `down --to` targeting | ❌ | ✅ |
|
|
77
|
+
| Lifecycle events (`EventEmitter`) | ❌ | ✅ |
|
|
78
|
+
| Reuse an already-connected `MongoClient` | ❌ | ✅ |
|
|
79
|
+
| Changelog written inside the migration's transaction | ❌ | ✅ |
|
|
80
|
+
| Credentials masked in errors, logs and `--json` | ❌ | ✅ |
|
|
81
|
+
| Pino-compatible logger | ❌ | ✅ |
|
|
82
|
+
| Node floor | ≥ 18 | ≥ 22.18 |
|
|
83
|
+
|
|
84
|
+
<sub>Compared against `mongo-migrate-kit` 1.2.2 — the version this project forked from. The Node
|
|
85
|
+
floor is a trade-off, not a win: it is what buys `.ts` migrations with no loader and `.env` parsing
|
|
86
|
+
with no dependency.</sub>
|
|
87
|
+
|
|
88
|
+
→ **[Full comparison, and what stayed the same](https://migronaut.vercel.app/guide/vs-mongo-migrate-kit)**
|
|
89
|
+
|
|
90
|
+
> [!TIP]
|
|
91
|
+
> ### 🔄 Already using `migrate-mongo`? Switch in under a minute.
|
|
92
|
+
>
|
|
93
|
+
> `migronaut` adopts your existing `changelog` **as-is** — no re-running migrations, no data loss, no rewriting
|
|
94
|
+
> files. Point it at the same database and bring your whole history over in one command:
|
|
95
|
+
>
|
|
96
|
+
> ```bash
|
|
97
|
+
> migronaut import # one-time: adopt your migrate-mongo changelog (it's never modified)
|
|
98
|
+
> migronaut up # applies only what's new — your past migrations are recognized as already applied
|
|
99
|
+
> ```
|
|
100
|
+
>
|
|
101
|
+
> Your applied history is preserved and new migrations run normally. Your `up`/`down`/`create`/`status`
|
|
102
|
+
> mental model carries over 1:1 — you just gain dry-runs, single-file control, real rollbacks, hooks,
|
|
103
|
+
> and locking. → **[See how it works](#advanced-features)**
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Quick start
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install @alexify/migronaut
|
|
111
|
+
npm install mongodb # required peer dependency
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# 1 · create a configuration file (migronaut.config.js; pass --ts for TypeScript)
|
|
116
|
+
npx migronaut init
|
|
117
|
+
|
|
118
|
+
# 2 · create your first migration
|
|
119
|
+
npx migronaut create "add users email index"
|
|
120
|
+
|
|
121
|
+
# 3 · run everything pending
|
|
122
|
+
npx migronaut up
|
|
123
|
+
|
|
124
|
+
# 4 · see where you stand
|
|
125
|
+
npx migronaut status
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A migration is just an `up` and a `down`:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import type { MigrationContext } from '@alexify/migronaut';
|
|
132
|
+
|
|
133
|
+
export const description = 'Add unique index on users.email';
|
|
134
|
+
|
|
135
|
+
export async function up({ db }: MigrationContext): Promise<void> {
|
|
136
|
+
await db.collection('users').createIndex({ email: 1 }, { unique: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function down({ db }: MigrationContext): Promise<void> {
|
|
140
|
+
await db.collection('users').dropIndex('email_1');
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
> Prefer no files at all? Skip `migronaut init` and export `MIGRONAUT_URI` and `MIGRONAUT_DB` — that is enough to run.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Documentation
|
|
149
|
+
|
|
150
|
+
Full docs, guides, and the API reference live at
|
|
151
|
+
**[migronaut.vercel.app](https://migronaut.vercel.app/)**.
|
|
152
|
+
|
|
153
|
+
- [Why migronaut?](https://migronaut.vercel.app/guide/why) — how it compares to `migrate-mongo`
|
|
154
|
+
- [Core Concepts](https://migronaut.vercel.app/guide/concepts) — migrations, batches, the changelog, locking
|
|
155
|
+
- [Getting Started](https://migronaut.vercel.app/guide/getting-started) & [Tutorial](https://migronaut.vercel.app/guide/tutorial)
|
|
156
|
+
- [Configuration](https://migronaut.vercel.app/guide/configuration) · [Writing Migrations](https://migronaut.vercel.app/guide/writing-migrations) · [Transactions](https://migronaut.vercel.app/guide/transactions) · [Hooks](https://migronaut.vercel.app/guide/hooks)
|
|
157
|
+
- [Programmatic API](https://migronaut.vercel.app/guide/api) · [CI/CD](https://migronaut.vercel.app/guide/ci-cd) · [Troubleshooting](https://migronaut.vercel.app/guide/troubleshooting)
|
|
158
|
+
- Reference: [CLI Cheatsheet](https://migronaut.vercel.app/reference/cli) · [Error Codes](https://migronaut.vercel.app/reference/error-codes)
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Commands
|
|
163
|
+
|
|
164
|
+
Every command accepts the global flags `--uri`, `--db`, `--dir`, `--config`, `--env-file`,
|
|
165
|
+
`--no-env`, `--verbose`, `--quiet`, `--no-color`, and `--json` (except `init` — see `init --format`).
|
|
166
|
+
|
|
167
|
+
| Command | What it does |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `migronaut init` | Create a documented `migronaut.config.*` in the current directory |
|
|
170
|
+
| `migronaut import` | Adopt an existing `migrate-mongo` changelog (one-time, forward-only) |
|
|
171
|
+
| `migronaut create <name>` | Generate a timestamped migration file |
|
|
172
|
+
| `migronaut up [file]` | Run all pending migrations, one named file, or up to `--to <file>` |
|
|
173
|
+
| `migronaut down [file]` | Roll back the last batch, a chosen batch, the last N steps, one file, or to `--to <file>` |
|
|
174
|
+
| `migronaut redo [file]` | Roll back then re-apply (the last migration, or one file) |
|
|
175
|
+
| `migronaut status` | Print the full migration status table (`--check` to fail CI on pending) |
|
|
176
|
+
| `migronaut list` | List migrations, filtered by status |
|
|
177
|
+
| `migronaut dry-run <up\|down> [file]` | Preview a run without touching the database |
|
|
178
|
+
| `migronaut audit` | Read-only health check: config, connection, transactions, indexes, lock, drift |
|
|
179
|
+
| `migronaut lock` | Show who currently holds the migration lock |
|
|
180
|
+
| `migronaut unlock` | Force-release a stuck lock left behind by a crashed run |
|
|
181
|
+
|
|
182
|
+
Most data commands (`up`, `down`, `redo`, `status`, `list`, `dry-run`, `import`, `create`,
|
|
183
|
+
`audit`, `lock`, `unlock`) accept **`--json`** for machine-readable output — see
|
|
184
|
+
[CI & automation](#ci--automation).
|
|
185
|
+
|
|
186
|
+
<details>
|
|
187
|
+
<summary><b>Options for every command</b></summary>
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# init — generate a config file
|
|
191
|
+
migronaut init # migronaut.config.js (default)
|
|
192
|
+
migronaut init --js # migronaut.config.js (explicit default)
|
|
193
|
+
migronaut init --ts # migronaut.config.ts
|
|
194
|
+
migronaut init --format json # migronaut.config.json
|
|
195
|
+
migronaut init --secret-provider # async config that loads the URI from a secret manager (js/ts only)
|
|
196
|
+
migronaut init --force # overwrite an existing config file
|
|
197
|
+
migronaut init --uri mongodb://localhost:27017 --db my_app # prefill the generated config
|
|
198
|
+
|
|
199
|
+
# import — adopt an existing migrate-mongo changelog
|
|
200
|
+
migronaut import # read `changelog`, write the migronaut changelog
|
|
201
|
+
migronaut import --from <name> # read a differently-named source collection
|
|
202
|
+
migronaut import --to <name> # write to a specific collection (default: config migrationsCollection)
|
|
203
|
+
migronaut import --dry-run # preview the mapping, write nothing
|
|
204
|
+
migronaut import --trust-hash # reuse migrate-mongo's fileHash instead of recomputing
|
|
205
|
+
migronaut import --force # proceed even if the migronaut changelog already has records
|
|
206
|
+
migronaut import --no-lock # skip the concurrency lock (local dev only)
|
|
207
|
+
migronaut import --json # machine-readable output
|
|
208
|
+
|
|
209
|
+
# create — generate a migration file
|
|
210
|
+
migronaut create <name> # file type follows config `createExtension` (default .js)
|
|
211
|
+
migronaut create <name> --ts # force a .ts file
|
|
212
|
+
migronaut create <name> --js # force a .js file
|
|
213
|
+
migronaut create <name> --template <path> # use a custom template
|
|
214
|
+
migronaut create <name> --json # machine-readable output ({ "path": "..." })
|
|
215
|
+
|
|
216
|
+
# up — apply migrations
|
|
217
|
+
migronaut up # all pending (one shared batch for the run)
|
|
218
|
+
migronaut up <file> # one specific file
|
|
219
|
+
migronaut up --to <file> # pending files up to and including <file>, then stop
|
|
220
|
+
migronaut up --step # apply each file as its own batch (revert individually later)
|
|
221
|
+
migronaut up <file> --force # re-run an ALREADY-applied file (asks for confirmation)
|
|
222
|
+
migronaut up <file> --force --yes # confirm a re-run non-interactively (required with --json)
|
|
223
|
+
migronaut up --strict # abort on any checksum mismatch
|
|
224
|
+
migronaut up --no-lock # skip the concurrency lock (local dev only)
|
|
225
|
+
migronaut up --json # machine-readable output (array of run results)
|
|
226
|
+
|
|
227
|
+
# down — roll back
|
|
228
|
+
migronaut down # the last batch (may be several files)
|
|
229
|
+
migronaut down <file> # one specific file
|
|
230
|
+
migronaut down --batch <n> # a specific batch number
|
|
231
|
+
migronaut down --steps <n> # the last N migrations, newest first, ignoring batches
|
|
232
|
+
migronaut down --to <file> # everything applied after <file>; <file> itself stays applied
|
|
233
|
+
migronaut down --no-lock # skip the concurrency lock (local dev only)
|
|
234
|
+
migronaut down --json # machine-readable output (array of run results)
|
|
235
|
+
|
|
236
|
+
# redo — down then up
|
|
237
|
+
migronaut redo # the most recently applied migration
|
|
238
|
+
migronaut redo <file> # a specific file
|
|
239
|
+
migronaut redo --no-lock # skip the lock (dev only)
|
|
240
|
+
migronaut redo --json # machine-readable output (array of run results)
|
|
241
|
+
|
|
242
|
+
# status — full status table
|
|
243
|
+
migronaut status # the full status table
|
|
244
|
+
migronaut status --check # exit 2 if any migration is pending (CI gate)
|
|
245
|
+
migronaut status --pending # only the pending rows
|
|
246
|
+
migronaut status --limit <n> # only the last N rows (not combinable with --check)
|
|
247
|
+
migronaut status --json # machine-readable output (array of status rows)
|
|
248
|
+
|
|
249
|
+
# list — filtered status
|
|
250
|
+
migronaut list # all migrations
|
|
251
|
+
migronaut list --pending # only pending
|
|
252
|
+
migronaut list --applied # only applied
|
|
253
|
+
migronaut list --json # machine-readable output (array of status rows)
|
|
254
|
+
|
|
255
|
+
# dry-run — preview, never writes
|
|
256
|
+
migronaut dry-run up [file]
|
|
257
|
+
migronaut dry-run down [file]
|
|
258
|
+
migronaut dry-run down --steps <n> # preview a step rollback (the last N migrations)
|
|
259
|
+
migronaut dry-run down --batch <n> # preview reverting a specific batch
|
|
260
|
+
migronaut dry-run up --to <file> # preview a staged rollout up to <file>
|
|
261
|
+
migronaut dry-run down --to <file> # preview reverting everything applied after <file>
|
|
262
|
+
migronaut dry-run up --json # machine-readable output (array of status rows)
|
|
263
|
+
|
|
264
|
+
# audit — read-only health check
|
|
265
|
+
migronaut audit # pass/warn/fail per check; exit 22 on any fail
|
|
266
|
+
migronaut audit --json # machine-readable report
|
|
267
|
+
|
|
268
|
+
# lock — inspect the current lock
|
|
269
|
+
migronaut lock # shows the holder (pid / host / user / since), or "no lock"
|
|
270
|
+
migronaut lock --json # machine-readable output
|
|
271
|
+
|
|
272
|
+
# unlock — clear a stuck lock after a crash
|
|
273
|
+
migronaut unlock # shows the holder, prompts y/N
|
|
274
|
+
migronaut unlock --yes # skip the prompt (short: -y)
|
|
275
|
+
migronaut unlock --json # machine-readable output ({ "released": ..., "holder": ... })
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Global flags** (available on all commands): `--uri <uri>` (override `MIGRONAUT_URI`),
|
|
279
|
+
`--db <name>` (override `MIGRONAUT_DB`), `--dir <path>` (override `MIGRONAUT_MIGRATIONS_DIR`),
|
|
280
|
+
`--config <path>` (explicit config file, overrides auto-discovery), `--env-file <path>` /
|
|
281
|
+
`--no-env` (control `.env` loading), `--verbose` / `--quiet` (log level), `--no-color`,
|
|
282
|
+
`-V, --version`, `-h, --help`. Combined short flags are not supported — write `-f -y`, not `-fy`.
|
|
283
|
+
|
|
284
|
+
**`--json`** is a global flag (`migronaut --json status` and `migronaut status --json` both
|
|
285
|
+
work) and prints one JSON document to stdout — see [CI & automation](#ci--automation). The one
|
|
286
|
+
command without JSON output is `migronaut init`, whose deliverable is the config file itself:
|
|
287
|
+
use `init --format <js|ts|json>` to pick the file format.
|
|
288
|
+
|
|
289
|
+
</details>
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Advanced features
|
|
294
|
+
|
|
295
|
+
<details id="migrating-from-migrate-mongo">
|
|
296
|
+
<summary><b>Migrating from <code>migrate-mongo</code></b> — adopt an existing changelog with <code>migronaut import</code></summary>
|
|
297
|
+
|
|
298
|
+
<br>
|
|
299
|
+
|
|
300
|
+
`migronaut import` reads your existing `migrate-mongo` changelog and records that history in the `migronaut`
|
|
301
|
+
changelog, so `migronaut up` knows what is already applied and runs only what is new. It is a **one-time,
|
|
302
|
+
forward-only** step.
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# point migronaut at the same database, then:
|
|
306
|
+
migronaut import --dry-run # preview the mapping first (writes nothing)
|
|
307
|
+
migronaut import # adopt the history
|
|
308
|
+
migronaut up # apply only the migrations added since
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**What it does**
|
|
312
|
+
|
|
313
|
+
- Reads the source collection (`changelog` by default; `--from` to override) and **never modifies it** —
|
|
314
|
+
the mapped records are written to the `migronaut` changelog (your config's `migrationsCollection`,
|
|
315
|
+
`_migronaut_migrations` by default; `--to` to write to a different collection).
|
|
316
|
+
- Maps `fileName → name`, `appliedAt → appliedAt`, and resolves a checksum: it reuses `migrate-mongo`'s
|
|
317
|
+
`fileHash` when it matches the file on disk, otherwise recomputes a SHA-256 from disk (`--trust-hash`
|
|
318
|
+
reuses the stored hash as-is). Records whose files are missing are still imported.
|
|
319
|
+
- Assigns each migration a **unique, sequential batch number** in apply order. If the `migronaut` changelog
|
|
320
|
+
already has records, imported batches **continue after** the existing maximum (use `--force` to import
|
|
321
|
+
into a non-empty changelog).
|
|
322
|
+
- Leaves migration files that exist on disk but are **not** in the source changelog **pending** — they
|
|
323
|
+
run on the next `migronaut up`, exactly as expected for newly added migrations.
|
|
324
|
+
|
|
325
|
+
**Options**
|
|
326
|
+
|
|
327
|
+
| Flag | Default | What it does |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| `--from <collection>` | `changelog` | Source collection to read (never modified). |
|
|
330
|
+
| `--to <collection>` | config `migrationsCollection` (`_migronaut_migrations`) | Target collection to write the adopted history to. |
|
|
331
|
+
| `--dry-run` | off | Preview the mapping and print the table; writes nothing. |
|
|
332
|
+
| `--trust-hash` | off | Reuse `migrate-mongo`'s stored `fileHash` as-is instead of recomputing the checksum from disk. |
|
|
333
|
+
| `--force` | off | Import into a changelog that already has records (imported batches continue after the existing max). |
|
|
334
|
+
| `--no-lock` | off | Skip the MongoDB concurrency lock (local dev only). |
|
|
335
|
+
|
|
336
|
+
Plus the global flags `--uri`, `--db`, `--dir`, and `--config`.
|
|
337
|
+
|
|
338
|
+
**Forward-only — imported migrations cannot be rolled back**
|
|
339
|
+
|
|
340
|
+
Adopted records are tagged `origin: 'migrate-mongo'`. `migrate-mongo` files use a positional
|
|
341
|
+
`up(db, client)` signature, which `migronaut` does not execute (it passes a single context object). To avoid
|
|
342
|
+
ever corrupting your data, `migronaut down` / `migronaut redo` **refuse** an imported migration up front, before
|
|
343
|
+
running or writing anything, and tell you why:
|
|
344
|
+
|
|
345
|
+
```text
|
|
346
|
+
✖ Cannot roll back 1 migrate-mongo-imported migration(s): 20260101-add-index.js
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
If you need an old migration to be reversible under `migronaut`, re-author its file in the native format
|
|
350
|
+
(named exports, single context argument — see [Migration file formats](#migration-file-formats)).
|
|
351
|
+
|
|
352
|
+
</details>
|
|
353
|
+
|
|
354
|
+
<details>
|
|
355
|
+
<summary><b>Transactions</b> — wrap a migration in an all-or-nothing MongoDB transaction</summary>
|
|
356
|
+
|
|
357
|
+
<br>
|
|
358
|
+
|
|
359
|
+
Opt in per file with `export const useTransaction = true` (or globally via config). The runner opens a
|
|
360
|
+
session, passes it through the context, and commits on success or aborts on any error. Pass the
|
|
361
|
+
`session` to every operation so it joins the transaction:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
export const useTransaction = true;
|
|
365
|
+
|
|
366
|
+
export async function up({ db, session }: MigrationContext): Promise<void> {
|
|
367
|
+
await db.collection('accounts').insertOne({ balance: 100 }, { session });
|
|
368
|
+
await db.collection('ledger').insertOne({ delta: 100 }, { session });
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
> Transactions require a replica set or sharded cluster — MongoDB's own requirement, not a library limit.
|
|
373
|
+
|
|
374
|
+
</details>
|
|
375
|
+
|
|
376
|
+
<details>
|
|
377
|
+
<summary><b>Lifecycle hooks</b> — run code around the batch and each migration</summary>
|
|
378
|
+
|
|
379
|
+
<br>
|
|
380
|
+
|
|
381
|
+
Define hooks in your config file. Use them to seed data, emit metrics, or alert on failure:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
hooks: {
|
|
385
|
+
beforeAll: async (ctx) => { /* once, before the batch */ },
|
|
386
|
+
afterAll: async (ctx) => { /* once, after the batch */ },
|
|
387
|
+
beforeEach: async (name, ctx) => { /* before each file */ },
|
|
388
|
+
afterEach: async (name, durationMs, ctx) => { /* after each file */ },
|
|
389
|
+
onError: async (name, error, ctx) => { /* a file threw — alert, then it re-throws */ },
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
</details>
|
|
394
|
+
|
|
395
|
+
<details>
|
|
396
|
+
<summary><b>Loading secrets at runtime</b> — AWS, Google, Vault, Azure, anything</summary>
|
|
397
|
+
|
|
398
|
+
<br>
|
|
399
|
+
|
|
400
|
+
A `.ts`/`.js` config may export a **function** (sync or async) instead of an object.
|
|
401
|
+
`migronaut` calls it once per command, so you can fetch the connection from a secret manager at run time.
|
|
402
|
+
The secret is **never written to disk**, and a rotated value is picked up automatically on the next run.
|
|
403
|
+
|
|
404
|
+
The library ships **no** cloud SDKs — you bring the one you already use, so any provider works:
|
|
405
|
+
|
|
406
|
+
```js
|
|
407
|
+
// migronaut.config.js — AWS Secrets Manager
|
|
408
|
+
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
|
|
409
|
+
|
|
410
|
+
export default async () => {
|
|
411
|
+
const sm = new SecretsManagerClient({ region: 'us-east-1' });
|
|
412
|
+
const res = await sm.send(new GetSecretValueCommand({ SecretId: 'prod/mongo' }));
|
|
413
|
+
const { uri, dbName } = JSON.parse(res.SecretString ?? '{}');
|
|
414
|
+
return { uri, dbName }; // merged at the config-file tier — env vars / flags still override
|
|
415
|
+
};
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Run `migronaut init --secret-provider` to scaffold this form with an AWS example you can swap for any provider.
|
|
419
|
+
If the function throws, it surfaces as a `ConfigInvalidError` with the cause attached.
|
|
420
|
+
|
|
421
|
+
</details>
|
|
422
|
+
|
|
423
|
+
<details>
|
|
424
|
+
<summary><b>Batches & step rollback</b> — group a deploy, or revert file-by-file</summary>
|
|
425
|
+
|
|
426
|
+
<br>
|
|
427
|
+
|
|
428
|
+
A **batch** is one `migronaut up` run. By default every migration applied in a single run shares one batch
|
|
429
|
+
number, so `migronaut down` rolls back that whole run as a unit — the same model used by **Laravel** and
|
|
430
|
+
**Knex**. That keeps a deploy atomic: one command applied it, one command reverts it.
|
|
431
|
+
|
|
432
|
+
When you want finer control, two flags mirror Laravel's `migrate --step` / `migrate:rollback --step`:
|
|
433
|
+
|
|
434
|
+
- **`migronaut up --step`** — apply each file in the run as its **own** sequential batch instead of one shared
|
|
435
|
+
batch. A later `migronaut down` then peels them off one at a time.
|
|
436
|
+
- **`migronaut down --steps <n>`** — revert the **last N applied migrations**, newest first, counted as
|
|
437
|
+
individual files **regardless of batch**. `migronaut down --steps 1` reverts just the single most-recently
|
|
438
|
+
applied migration; a larger N can cross batch boundaries, so preview it first with
|
|
439
|
+
`migronaut dry-run down --steps <n>`.
|
|
440
|
+
|
|
441
|
+
`--steps` is mutually exclusive with `--batch` and a filename. Migrations are always reverted
|
|
442
|
+
newest-first, so `up` followed by `down --steps <same n>` returns you to the starting state.
|
|
443
|
+
|
|
444
|
+
</details>
|
|
445
|
+
|
|
446
|
+
<details>
|
|
447
|
+
<summary><b>Concurrency lock & checksums</b> — safe concurrent deploys, tamper detection</summary>
|
|
448
|
+
|
|
449
|
+
<br>
|
|
450
|
+
|
|
451
|
+
**Lock.** Each run acquires an atomic lock document in `_migronaut_locks`, so two deploys can never migrate
|
|
452
|
+
at once. A lock older than `lockTTLSeconds` is treated as stale and reclaimed; while a migration runs,
|
|
453
|
+
a heartbeat renews the lock at half the TTL so a long migration can't have its lock stolen mid-run.
|
|
454
|
+
The lock is always released in a `finally` block. `--no-lock` bypasses it for local development (and
|
|
455
|
+
warns loudly). If a process crashes hard and leaves a lock behind, clear it with **`migronaut unlock`** (it
|
|
456
|
+
shows you who held it and asks for confirmation).
|
|
457
|
+
|
|
458
|
+
**Checksums.** Every applied migration stores a SHA-256 of its file. On later runs `migronaut` compares the
|
|
459
|
+
two and surfaces drift in `status`. With `strict: true` (or `--strict`) a mismatch aborts the run;
|
|
460
|
+
otherwise it warns and skips. To intentionally re-run an edited, already-applied file, use
|
|
461
|
+
`migronaut up <file> --force`.
|
|
462
|
+
|
|
463
|
+
</details>
|
|
464
|
+
|
|
465
|
+
<details id="ci--automation">
|
|
466
|
+
<summary><b>CI & automation</b> — JSON output, deploy gates, scripting</summary>
|
|
467
|
+
|
|
468
|
+
<br>
|
|
469
|
+
|
|
470
|
+
**Machine-readable output.** Add `--json` to any data command (`up`, `down`, `redo`, `status`,
|
|
471
|
+
`list`, `dry-run`, `import`, `create`, `audit`, `lock`, `unlock`) to get a single JSON document on **stdout** — all
|
|
472
|
+
human logs and the spinner are redirected to stderr, so the stream is safe to pipe into `jq` or parse
|
|
473
|
+
in a script. On failure the command prints `{ "error": { "code": "...", "message": "..." } }` to
|
|
474
|
+
stdout and exits `1`.
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
# Apply pending migrations and capture the result in CI
|
|
478
|
+
migronaut up --json | jq '.[] | select(.status == "applied") | .file'
|
|
479
|
+
|
|
480
|
+
# Fail a deploy step if the database isn't fully migrated
|
|
481
|
+
migronaut status --check # exits 2 when anything is pending, 0 otherwise
|
|
482
|
+
|
|
483
|
+
# Inspect status as data
|
|
484
|
+
migronaut status --json | jq 'map(select(.status == "pending")) | length'
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
A typical pipeline gate:
|
|
488
|
+
|
|
489
|
+
```yaml
|
|
490
|
+
# .github/workflows/deploy.yml (excerpt)
|
|
491
|
+
- name: Fail if migrations are pending
|
|
492
|
+
run: npx migronaut status --check --uri "$MIGRONAUT_URI" --db "$MIGRONAUT_DB"
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
> Note: `migronaut init` is the one command without JSON output — its deliverable is the
|
|
496
|
+
> config file itself. `init --format json` writes `migronaut.config.json`.
|
|
497
|
+
|
|
498
|
+
</details>
|
|
499
|
+
|
|
500
|
+
<details>
|
|
501
|
+
<summary><b>Audit trail</b> — a complete, append-only history</summary>
|
|
502
|
+
|
|
503
|
+
<br>
|
|
504
|
+
|
|
505
|
+
Every record in `_migronaut_migrations` stores `batch`, `status`, `appliedAt`, `revertedAt`, `duration`,
|
|
506
|
+
`checksum`, `environment`, and `executedBy`. Rolling back **updates** a record's status to `reverted`
|
|
507
|
+
and stamps `revertedAt` — it is **never deleted**, so the full history stays intact for compliance.
|
|
508
|
+
|
|
509
|
+
</details>
|
|
510
|
+
|
|
511
|
+
<details>
|
|
512
|
+
<summary><b>Programmatic API</b> — run migrations from your own code</summary>
|
|
513
|
+
|
|
514
|
+
<br>
|
|
515
|
+
|
|
516
|
+
#### Run pending migrations on app start
|
|
517
|
+
|
|
518
|
+
`runMigrations()` is the blessed one-call entry point. It opens its own connection, applies every
|
|
519
|
+
pending migration, and **always disconnects** — even if a migration throws, so a failed boot never
|
|
520
|
+
leaks a MongoDB connection. A failing migration aborts startup instead of letting your app serve
|
|
521
|
+
traffic against a half-migrated database:
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
import { runMigrations } from '@alexify/migronaut';
|
|
525
|
+
|
|
526
|
+
// Call this before your server starts listening.
|
|
527
|
+
const { applied, upToDate } = await runMigrations({
|
|
528
|
+
uri: process.env.MIGRONAUT_URI!,
|
|
529
|
+
dbName: 'my_app',
|
|
530
|
+
migrationsDir: './migrations',
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
if (!upToDate) console.log(`Applied ${applied.length} migration(s)`);
|
|
534
|
+
// then: app.listen(...)
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
#### Multiple instances booting together
|
|
538
|
+
|
|
539
|
+
When several instances start at once, only one wins the lock. Set `onLockHeld: 'wait'` so the others
|
|
540
|
+
block until the migrating peer finishes, then confirm there's nothing left to apply before returning:
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
await runMigrations(
|
|
544
|
+
{ uri: process.env.MIGRONAUT_URI!, dbName: 'my_app' },
|
|
545
|
+
{ onLockHeld: 'wait', lockWaitTimeoutMs: 90_000 }, // default 'throw', waits up to 90s
|
|
546
|
+
);
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
#### Serverless / cold start
|
|
550
|
+
|
|
551
|
+
The same call works in a Lambda/Cloud Function bootstrap. Keep `onLockHeld: 'wait'` so concurrent
|
|
552
|
+
cold starts don't fail, and rely on the auto-disconnect so each invocation cleans up after itself.
|
|
553
|
+
|
|
554
|
+
#### Fail a deploy/health check when the DB is behind (no writes)
|
|
555
|
+
|
|
556
|
+
`pendingMigrations()` is a connection-managed, read-only readiness probe:
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
import { pendingMigrations } from '@alexify/migronaut';
|
|
560
|
+
|
|
561
|
+
const pending = await pendingMigrations({ uri, dbName: 'my_app' });
|
|
562
|
+
if (pending.length > 0) {
|
|
563
|
+
throw new Error(`Database is behind by ${pending.length} migration(s)`);
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
#### Full control
|
|
568
|
+
|
|
569
|
+
For everything else, every CLI command is a method on `MigratorKit` (you manage the lifecycle):
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
import { MigratorKit } from '@alexify/migronaut';
|
|
573
|
+
|
|
574
|
+
const migrator = new MigratorKit({ uri, dbName: 'my_app', migrationsDir: './migrations' });
|
|
575
|
+
await migrator.connect();
|
|
576
|
+
const rows = await migrator.status(); // StatusRow[]
|
|
577
|
+
await migrator.disconnect();
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
All errors extend `MigronautError` and carry a typed `code` (`LOCK_ALREADY_HELD`, `CHECKSUM_MISMATCH`,
|
|
581
|
+
`NOT_APPLIED`, …), so `catch` blocks stay type-safe.
|
|
582
|
+
|
|
583
|
+
</details>
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Configuration
|
|
588
|
+
|
|
589
|
+
`migronaut` resolves settings in this order (**highest wins**):
|
|
590
|
+
|
|
591
|
+
> **CLI flags → environment variables → config file → built-in defaults**
|
|
592
|
+
|
|
593
|
+
A config file is optional and auto-discovered in the working directory as `migronaut.config.ts`,
|
|
594
|
+
`migronaut.config.js`, or `migronaut.config.json`. Run `migronaut init` to generate one — it ships fully commented,
|
|
595
|
+
so every setting lives in one documented place:
|
|
596
|
+
|
|
597
|
+
```js
|
|
598
|
+
// migronaut.config.js — generated by `migronaut init`, every option explained
|
|
599
|
+
/** @type {import('@alexify/migronaut').MigronautConfig} */
|
|
600
|
+
export default {
|
|
601
|
+
// ── Connection (required) ───────────────────────────────────────────────
|
|
602
|
+
uri: 'mongodb://localhost:27017', // MongoDB connection string
|
|
603
|
+
dbName: 'my_app', // database to run migrations against
|
|
604
|
+
|
|
605
|
+
// ── Files ───────────────────────────────────────────────────────────────
|
|
606
|
+
migrationsDir: './migrations', // where migration files live
|
|
607
|
+
fileExtensions: ['.ts', '.js'], // which files count as migrations
|
|
608
|
+
createExtension: 'js', // default type for `migronaut create` ('js' | 'ts'); --js/--ts override
|
|
609
|
+
sequential: false, // true → 0001-style numbering instead of timestamps
|
|
610
|
+
// templatePath: './migration.template.ts', // custom template for `migronaut create`
|
|
611
|
+
|
|
612
|
+
// ── Bookkeeping collections ─────────────────────────────────────────────
|
|
613
|
+
migrationsCollection: '_migronaut_migrations', // the append-only audit trail
|
|
614
|
+
lockCollection: '_migronaut_locks', // the concurrency lock
|
|
615
|
+
lockTTLSeconds: 60, // a lock older than this is reclaimable
|
|
616
|
+
|
|
617
|
+
// ── Safety ──────────────────────────────────────────────────────────────
|
|
618
|
+
strict: false, // true → abort on a checksum mismatch (instead of warn + skip)
|
|
619
|
+
useTransaction: false, // true → wrap every migration in a transaction (override per file)
|
|
620
|
+
|
|
621
|
+
// ── Code-only options (omit in migronaut.config.json) ─────────────────────────
|
|
622
|
+
// hooks: { beforeAll, afterAll, beforeEach, afterEach, onError },
|
|
623
|
+
// mongoose: myMongooseInstance, // pass if your migrations use Mongoose models
|
|
624
|
+
// logger: null, // null silences all output; a pino instance works directly
|
|
625
|
+
};
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
<details>
|
|
629
|
+
<summary><b>Structured logging with pino</b> — the logger option is pino-compatible</summary>
|
|
630
|
+
|
|
631
|
+
<br>
|
|
632
|
+
|
|
633
|
+
Anything with `{ debug, info, warn, error }` methods works as `logger` — including a real pino
|
|
634
|
+
instance. When the logger has a pino-style `child()`, migronaut binds `component: 'migronaut'`
|
|
635
|
+
once, and a throwing logger can never break a migration run:
|
|
636
|
+
|
|
637
|
+
```js
|
|
638
|
+
const pino = require('pino');
|
|
639
|
+
const { runMigrations } = require('@alexify/migronaut');
|
|
640
|
+
|
|
641
|
+
await runMigrations({
|
|
642
|
+
uri: process.env.MIGRONAUT_URI,
|
|
643
|
+
dbName: 'my_app',
|
|
644
|
+
logger: pino({ level: 'info' }),
|
|
645
|
+
});
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
</details>
|
|
649
|
+
|
|
650
|
+
<details>
|
|
651
|
+
<summary><b>Environment variables</b> — the zero-file way to configure everything</summary>
|
|
652
|
+
|
|
653
|
+
<br>
|
|
654
|
+
|
|
655
|
+
Every **scalar** config option has an environment variable, which is what makes a config file
|
|
656
|
+
optional rather than merely discouraged:
|
|
657
|
+
|
|
658
|
+
| Env var | Config key | Default |
|
|
659
|
+
|---|---|---|
|
|
660
|
+
| `MIGRONAUT_URI` | `uri` | — *(required)* |
|
|
661
|
+
| `MIGRONAUT_DB` | `dbName` | — *(required)* |
|
|
662
|
+
| `MIGRONAUT_MIGRATIONS_DIR` | `migrationsDir` | `./migrations` |
|
|
663
|
+
| `MIGRONAUT_COLLECTION` | `migrationsCollection` | `_migronaut_migrations` |
|
|
664
|
+
| `MIGRONAUT_LOCK_COLLECTION` | `lockCollection` | `_migronaut_locks` |
|
|
665
|
+
| `MIGRONAUT_LOCK_TTL` | `lockTTLSeconds` | `60` |
|
|
666
|
+
| `MIGRONAUT_STRICT` | `strict` | `false` |
|
|
667
|
+
| `MIGRONAUT_USE_TRANSACTION` | `useTransaction` | `false` |
|
|
668
|
+
| `MIGRONAUT_SEQUENTIAL` | `sequential` | `false` |
|
|
669
|
+
| `MIGRONAUT_CREATE_EXTENSION` | `createExtension` | `js` |
|
|
670
|
+
| `MIGRONAUT_ENVIRONMENT` | `environment` | `NODE_ENV`, then `production` |
|
|
671
|
+
| `MIGRONAUT_TEMPLATE_PATH` | `templatePath` | — *(built-in template)* |
|
|
672
|
+
| `MIGRONAUT_TIMEOUT_MS` | `timeoutMs` | — *(no timeout)* |
|
|
673
|
+
| `MIGRONAUT_ON_LOCK_LOST` | `onLockLost` | `abort` |
|
|
674
|
+
| `MIGRONAUT_ENSURE_INDEXES` | `ensureIndexes` | `true` |
|
|
675
|
+
| `MIGRONAUT_RELOAD_MIGRATIONS` | `reloadMigrations` | `false` |
|
|
676
|
+
| `MIGRONAUT_ENV_FILE` | `envFile` | `.env` |
|
|
677
|
+
|
|
678
|
+
`fileExtensions`, `clientOptions`, `client`, `mongoose`, `hooks` and `logger` are config-file/API
|
|
679
|
+
only — they aren't scalars, so no environment variable can express them.
|
|
680
|
+
|
|
681
|
+
A value that doesn't parse is **rejected, never coerced**: `MIGRONAUT_STRICT=on` or
|
|
682
|
+
`MIGRONAUT_LOCK_TTL=abc` fails with an error naming the variable, rather than quietly turning a
|
|
683
|
+
safety setting off.
|
|
684
|
+
|
|
685
|
+
Three more variables shape the CLI rather than the config:
|
|
686
|
+
|
|
687
|
+
| Env var | Effect |
|
|
688
|
+
|---|---|
|
|
689
|
+
| `MIGRONAUT_NO_COLOR` | Disable ANSI color for migronaut only; outranks `NO_COLOR`/`FORCE_COLOR` |
|
|
690
|
+
| `MIGRONAUT_FORCE_COLOR` | Force color on (`0` forces it off); the highest-priority color signal |
|
|
691
|
+
| `MIGRONAUT_USER` | Who to record in `executedBy`; overrides the OS user (useful in CI) |
|
|
692
|
+
|
|
693
|
+
The unprefixed `NO_COLOR`, `FORCE_COLOR` and `TERM=dumb` are still honored underneath the prefixed
|
|
694
|
+
pair — they're ecosystem-wide conventions, and a CLI is expected to obey them.
|
|
695
|
+
|
|
696
|
+
`.env` files are loaded automatically, parsed by Node's built-in `util.parseEnv`. Real environment
|
|
697
|
+
variables always win over `.env` values. Supported syntax: one `KEY=VALUE` per line, optional
|
|
698
|
+
`export ` prefix, matching single/double/back quotes, full-line and inline `#` comments, and
|
|
699
|
+
multiline values inside double quotes. Not supported: `${VAR}` interpolation. Files over 1 MB are
|
|
700
|
+
rejected, as is anything that isn't a regular file.
|
|
701
|
+
|
|
702
|
+
</details>
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
## Migration file formats
|
|
707
|
+
|
|
708
|
+
`migronaut` loads TypeScript and both JavaScript module systems with no extra setup:
|
|
709
|
+
|
|
710
|
+
```ts
|
|
711
|
+
// TypeScript / ESM — named exports (native on Node 22.18+, or under a loader like tsx)
|
|
712
|
+
export async function up({ db }) { /* ... */ }
|
|
713
|
+
export async function down({ db }) { /* ... */ }
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
```js
|
|
717
|
+
// CommonJS — default export
|
|
718
|
+
module.exports = {
|
|
719
|
+
async up({ db }) { /* ... */ },
|
|
720
|
+
async down({ db }) { /* ... */ },
|
|
721
|
+
};
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
Optional per-file exports: `description` (shown in `status`) and `useTransaction`. Note that `up`/`down`
|
|
725
|
+
receive a **single context object** (`{ db, client, mongoose?, session? }`) — not `migrate-mongo`'s
|
|
726
|
+
positional `(db, client)`.
|
|
727
|
+
|
|
728
|
+
> **ESM vs CommonJS:** Node decides a file's module system from its extension and the nearest
|
|
729
|
+
> `package.json` `"type"`. In a project with `"type": "module"`, a `.js` file is an ES module, so
|
|
730
|
+
> `module.exports = …` throws *"module is not defined in ES module scope."* Use named `export`s (above),
|
|
731
|
+
> or name the file `.cjs` and add `'.cjs'` to `fileExtensions` in your config.
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## Benchmarks
|
|
736
|
+
|
|
737
|
+
Measured with the zero-dependency harness in [`bench/bench.js`](./bench/bench.js) (in-process
|
|
738
|
+
scenarios: 1s timed run after 2k warmup iterations; DB-bound scenarios against an in-memory
|
|
739
|
+
MongoDB replica set: 500ms timed run after 100 warmup iterations). Reproduce with:
|
|
740
|
+
|
|
741
|
+
```bash
|
|
742
|
+
pnpm bench
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Apple M3 Max, Node v24.14.1:
|
|
746
|
+
|
|
747
|
+
| Scenario | ops/sec |
|
|
748
|
+
|---|--------:|
|
|
749
|
+
| `computeChecksum` — small migration file (~1 KB) | ~16,000 |
|
|
750
|
+
| `computeChecksum` — large migration file (~100 KB) | ~8,000 |
|
|
751
|
+
| `loadMigrationFile` — CommonJS default export | ~60,000 |
|
|
752
|
+
| `loadMigrationFile` — ESM named exports | ~60,000 |
|
|
753
|
+
| `loadMigrationFile` — TypeScript (native type-stripping) | ~60,000 |
|
|
754
|
+
| `Changelog.getAll` — 1,000 records | ~20 |
|
|
755
|
+
| `Changelog.getAppliedNames` — 1,000 records | ~30 |
|
|
756
|
+
| `Changelog.getLastBatch` — 1,000 records | ~200 |
|
|
757
|
+
| `Changelog.markApplied` — idempotent upsert | ~150 |
|
|
758
|
+
| `Changelog.markReverted` — update existing applied record | ~150 |
|
|
759
|
+
| `MigrationLock.acquire + release` — uncontended round trip | ~80 |
|
|
760
|
+
| `MigrationLock.renew` — heartbeat update | ~150 |
|
|
761
|
+
|
|
762
|
+
Numbers vary by hardware and Node version — treat them as relative guidance, not absolutes. The
|
|
763
|
+
harness exists primarily to catch performance regressions between releases.
|
|
764
|
+
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
## License
|
|
768
|
+
|
|
769
|
+
[MIT](./LICENSE) © Alexis Technologies
|
|
770
|
+
|
|
771
|
+
Originally forked from `mongo-migrate-kit` by Santosh Gupta; MIT, attribution
|
|
772
|
+
retained in [LICENSE](./LICENSE). See
|
|
773
|
+
[what changed since the fork](https://migronaut.vercel.app/guide/vs-mongo-migrate-kit).
|