@alexify/migronaut 1.0.0 → 2.1.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 +409 -1
- package/README.md +248 -24
- package/bin/migronaut.js +11 -3
- package/bullmq.d.ts +845 -0
- package/bullmq.js +1 -0
- package/index.d.ts +757 -29
- package/migronaut.schema.json +191 -1
- package/package.json +27 -6
- package/src/bullmq/index.js +55 -0
- package/src/bullmq/jobs.js +454 -0
- package/src/bullmq/processor.js +608 -0
- package/src/bullmq/producer.js +424 -0
- package/src/bullmq/service.js +653 -0
- package/src/bullmq/wait.js +124 -0
- package/src/cli/args.js +12 -2
- package/src/cli/commands/baseline.js +45 -0
- package/src/cli/commands/converge.js +160 -0
- package/src/cli/commands/down.js +2 -0
- package/src/cli/commands/lock.js +2 -1
- package/src/cli/commands/redo.js +8 -1
- package/src/cli/commands/unlock.js +12 -2
- package/src/cli/commands/up.js +14 -1
- package/src/cli/exit-codes.js +10 -2
- package/src/cli/index.js +4 -0
- package/src/cli/shared.js +29 -7
- package/src/cli/table.js +105 -0
- package/src/core/audit.js +17 -3
- package/src/core/baseline.js +80 -0
- package/src/core/changelog.js +140 -24
- package/src/core/collections.js +372 -0
- package/src/core/config.js +125 -27
- package/src/core/converge-log.js +47 -0
- package/src/core/converge-plan.js +483 -0
- package/src/core/converge.js +867 -0
- package/src/core/import-runner.js +34 -6
- package/src/core/import.js +14 -7
- package/src/core/index-spec.js +496 -0
- package/src/core/lock-wait.js +260 -0
- package/src/core/lock.js +71 -20
- package/src/core/migrator.js +805 -304
- package/src/core/options.js +251 -0
- package/src/core/run-recorder.js +157 -0
- package/src/core/run.js +71 -71
- package/src/core/runner.js +70 -20
- package/src/core/sequence.js +134 -0
- package/src/errors/index.js +71 -1
- package/src/index.js +16 -0
- package/src/utils/actor.js +48 -0
- package/src/utils/canonical.js +179 -0
- package/src/utils/collection-name.js +21 -0
- package/src/utils/error.js +18 -1
- package/src/utils/id.js +77 -0
- package/src/utils/loader.js +39 -21
- package/src/utils/logger.js +30 -12
- package/src/utils/migration-name.js +32 -0
- package/src/utils/redact.js +57 -4
- package/src/utils/sanitize.js +8 -3
- package/src/utils/telemetry.js +393 -0
- package/src/utils/template.js +60 -12
package/README.md
CHANGED
|
@@ -23,11 +23,29 @@ change before it touches your database.
|
|
|
23
23
|
|
|
24
24
|
</div>
|
|
25
25
|
|
|
26
|
+
> [!TIP]
|
|
27
|
+
> ### 🔄 Already using `migrate-mongo`? Switch in under a minute.
|
|
28
|
+
>
|
|
29
|
+
> `migronaut` adopts your existing `changelog` **as-is** — no re-running migrations, no data loss, no rewriting
|
|
30
|
+
> files. Point it at the same database and bring your whole history over in one command:
|
|
31
|
+
>
|
|
32
|
+
> ```bash
|
|
33
|
+
> migronaut import # one-time: adopt your migrate-mongo changelog (it's never modified)
|
|
34
|
+
> migronaut up # applies only what's new — your past migrations are recognized as already applied
|
|
35
|
+
> ```
|
|
36
|
+
>
|
|
37
|
+
> Your applied history is preserved and new migrations run normally. Your `up`/`down`/`create`/`status`
|
|
38
|
+
> mental model carries over 1:1 — you just gain dry-runs, single-file control, real rollbacks, hooks,
|
|
39
|
+
> and locking. No `migrate-mongo` history at all? `migronaut baseline` adopts an existing database
|
|
40
|
+
> with no prior tool. → **[See how it works](#advanced-features)**
|
|
41
|
+
|
|
26
42
|
---
|
|
27
43
|
|
|
28
44
|
## Reasons to choose it
|
|
29
45
|
|
|
30
|
-
- **Zero dependencies** — no runtime dependencies at all; only the `mongodb` driver as a peer
|
|
46
|
+
- **Zero dependencies** — no runtime dependencies at all; only the `mongodb` driver as a peer
|
|
47
|
+
(Mongoose, BullMQ and OpenTelemetry are optional integrations you inject — never installed for
|
|
48
|
+
you).
|
|
31
49
|
Instant installs, nothing extra in your lockfile, no supply-chain surface.
|
|
32
50
|
- **Run a single migration** — `migronaut up <file>`, not just "all pending".
|
|
33
51
|
- **Roll back anything** — a batch (`--batch 3`), the last N (`--steps 2`), one file, or `redo`.
|
|
@@ -35,12 +53,27 @@ change before it touches your database.
|
|
|
35
53
|
- **No race conditions** — an atomic MongoDB lock stops two deploys running migrations at once.
|
|
36
54
|
- **Tamper detection** — SHA-256 checksums catch a migration edited after it was applied.
|
|
37
55
|
- **Audit trail kept** — a rollback updates the record, it never deletes it.
|
|
56
|
+
- **Adopt any existing database** — `migronaut import` for a migrate-mongo history,
|
|
57
|
+
`migronaut baseline` when there was no migration tool at all.
|
|
58
|
+
- **Out-of-order detection** — a migration merged late from a parallel branch is flagged
|
|
59
|
+
(warn by default, `onOutOfOrder: 'error'` to refuse) instead of silently applying.
|
|
38
60
|
- **Lifecycle hooks** — `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, `onError`.
|
|
39
61
|
- **Opt-in transactions** — wrap a migration so it fully commits or fully aborts.
|
|
62
|
+
- **Indexes and validators as an end state** — declare them, and `migronaut converge` brings the
|
|
63
|
+
database to match; no migration file per index change ([details](#declared-collections)).
|
|
40
64
|
- **TypeScript, ESM & CommonJS** — all run with no `ts-node` plumbing.
|
|
41
65
|
- **Zero config files required** — drive everything from env vars if you prefer.
|
|
42
66
|
- **Pino-friendly logging** — the `logger` option is pino-compatible; pass a pino instance directly
|
|
43
67
|
and migronaut logs through it (with a `component: 'migronaut'` child binding).
|
|
68
|
+
- **Your id format** — run ids and queue group ids are random UUIDs by default; pass
|
|
69
|
+
`generateId: ulid` (or cuid2, nanoid, UUIDv7 — any `() => string`) and every id migronaut mints
|
|
70
|
+
comes from your generator.
|
|
71
|
+
- **OpenTelemetry (optional)** — pass a tracer and a meter from your own `@opentelemetry/api`: a
|
|
72
|
+
span per run and per migration, active while the migration runs, so an instrumented MongoDB
|
|
73
|
+
driver nests its command spans under the migration that issued them — plus duration metrics.
|
|
74
|
+
- **Migrations as a queue (optional)** — `@alexify/migronaut/bullmq` runs each migration as its own
|
|
75
|
+
BullMQ job, in order, so migronaut can be a migration service: trigger it over HTTP, on a
|
|
76
|
+
schedule, or from a deploy hook that waits for the result.
|
|
44
77
|
|
|
45
78
|
### How it compares to `migrate-mongo`
|
|
46
79
|
|
|
@@ -51,13 +84,16 @@ change before it touches your database.
|
|
|
51
84
|
| Roll back a specific batch (not just the last) | ❌ | ✅ |
|
|
52
85
|
| Dry-run preview | ❌ | ✅ |
|
|
53
86
|
| `redo` (down + up) | ❌ | ✅ |
|
|
54
|
-
|
|
|
87
|
+
| Concurrency lock | opt-in, TTL-only | ✅ always on: heartbeat, owner token, abort-on-loss, `lock`/`unlock` CLI |
|
|
88
|
+
| Checksum / tamper detection | opt-in `useFileHash` (re-runs changed files) | ✅ enforced drift detection: per-row `checksumOk`, `--strict`, `audit` |
|
|
55
89
|
| Lifecycle hooks | ❌ | ✅ |
|
|
56
90
|
| First-class TypeScript (built-in) | ❌ | ✅ |
|
|
57
91
|
| History preserved on rollback (never deleted) | ❌ | ✅ |
|
|
92
|
+
| Declared indexes & validators (`converge`) | ❌ | ✅ |
|
|
58
93
|
| Adopt an existing `migrate-mongo` changelog | — | ✅ `migronaut import` |
|
|
59
94
|
|
|
60
|
-
<sub>Reflects `migrate-mongo`'s documented CLI as of mid-2026
|
|
95
|
+
<sub>Reflects `migrate-mongo`'s documented CLI as of mid-2026 (v14: optional
|
|
96
|
+
`lockCollectionName`/`lockTtl` lock, optional `useFileHash`). It has since added transaction access
|
|
61
97
|
via a `client` argument; `migronaut` exposes the same plus a declarative per-file `useTransaction` flag.</sub>
|
|
62
98
|
|
|
63
99
|
### How it compares to `mongo-migrate-kit`
|
|
@@ -79,6 +115,8 @@ via a `client` argument; `migronaut` exposes the same plus a declarative per-fil
|
|
|
79
115
|
| Changelog written inside the migration's transaction | ❌ | ✅ |
|
|
80
116
|
| Credentials masked in errors, logs and `--json` | ❌ | ✅ |
|
|
81
117
|
| Pino-compatible logger | ❌ | ✅ |
|
|
118
|
+
| BullMQ queue adapter (`/bullmq` entry point) | ❌ | ✅ |
|
|
119
|
+
| Declared indexes & validators (`converge`) | ❌ | ✅ |
|
|
82
120
|
| Node floor | ≥ 18 | ≥ 22.18 |
|
|
83
121
|
|
|
84
122
|
<sub>Compared against `mongo-migrate-kit` 1.2.2 — the version this project forked from. The Node
|
|
@@ -87,21 +125,6 @@ with no dependency.</sub>
|
|
|
87
125
|
|
|
88
126
|
→ **[Full comparison, and what stayed the same](https://migronaut.vercel.app/guide/vs-mongo-migrate-kit)**
|
|
89
127
|
|
|
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
128
|
---
|
|
106
129
|
|
|
107
130
|
## Quick start
|
|
@@ -154,7 +177,7 @@ Full docs, guides, and the API reference live at
|
|
|
154
177
|
- [Core Concepts](https://migronaut.vercel.app/guide/concepts) — migrations, batches, the changelog, locking
|
|
155
178
|
- [Getting Started](https://migronaut.vercel.app/guide/getting-started) & [Tutorial](https://migronaut.vercel.app/guide/tutorial)
|
|
156
179
|
- [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)
|
|
180
|
+
- [Programmatic API](https://migronaut.vercel.app/guide/api) · [Migrations as a Queue (BullMQ)](https://migronaut.vercel.app/guide/bullmq) · [OpenTelemetry](https://migronaut.vercel.app/guide/opentelemetry) · [CI/CD](https://migronaut.vercel.app/guide/ci-cd) · [Troubleshooting](https://migronaut.vercel.app/guide/troubleshooting)
|
|
158
181
|
- Reference: [CLI Cheatsheet](https://migronaut.vercel.app/reference/cli) · [Error Codes](https://migronaut.vercel.app/reference/error-codes)
|
|
159
182
|
|
|
160
183
|
---
|
|
@@ -168,10 +191,12 @@ Every command accepts the global flags `--uri`, `--db`, `--dir`, `--config`, `--
|
|
|
168
191
|
|---|---|
|
|
169
192
|
| `migronaut init` | Create a documented `migronaut.config.*` in the current directory |
|
|
170
193
|
| `migronaut import` | Adopt an existing `migrate-mongo` changelog (one-time, forward-only) |
|
|
194
|
+
| `migronaut baseline` | Mark existing migration files as applied without running them (adopt an existing DB) |
|
|
171
195
|
| `migronaut create <name>` | Generate a timestamped migration file |
|
|
172
196
|
| `migronaut up [file]` | Run all pending migrations, one named file, or up to `--to <file>` |
|
|
173
197
|
| `migronaut down [file]` | Roll back the last batch, a chosen batch, the last N steps, one file, or to `--to <file>` |
|
|
174
198
|
| `migronaut redo [file]` | Roll back then re-apply (the last migration, or one file) |
|
|
199
|
+
| `migronaut converge` | Bring declared collections — indexes and validators — to their declared state |
|
|
175
200
|
| `migronaut status` | Print the full migration status table (`--check` to fail CI on pending) |
|
|
176
201
|
| `migronaut list` | List migrations, filtered by status |
|
|
177
202
|
| `migronaut dry-run <up\|down> [file]` | Preview a run without touching the database |
|
|
@@ -179,8 +204,8 @@ Every command accepts the global flags `--uri`, `--db`, `--dir`, `--config`, `--
|
|
|
179
204
|
| `migronaut lock` | Show who currently holds the migration lock |
|
|
180
205
|
| `migronaut unlock` | Force-release a stuck lock left behind by a crashed run |
|
|
181
206
|
|
|
182
|
-
Most data commands (`up`, `down`, `redo`, `status`, `list`, `dry-run`, `import`,
|
|
183
|
-
`audit`, `lock`, `unlock`) accept **`--json`** for machine-readable output — see
|
|
207
|
+
Most data commands (`up`, `down`, `redo`, `converge`, `status`, `list`, `dry-run`, `import`,
|
|
208
|
+
`baseline`, `create`, `audit`, `lock`, `unlock`) accept **`--json`** for machine-readable output — see
|
|
184
209
|
[CI & automation](#ci--automation).
|
|
185
210
|
|
|
186
211
|
<details>
|
|
@@ -206,6 +231,12 @@ migronaut import --force # proceed even if the migronaut changelog alr
|
|
|
206
231
|
migronaut import --no-lock # skip the concurrency lock (local dev only)
|
|
207
232
|
migronaut import --json # machine-readable output
|
|
208
233
|
|
|
234
|
+
# baseline — adopt an existing database (no prior migration tool)
|
|
235
|
+
migronaut baseline # mark ALL pending files as applied, without running them
|
|
236
|
+
migronaut baseline --to <file> # only files up to and including <file>
|
|
237
|
+
migronaut baseline --yes # skip the confirmation prompt (required with --json)
|
|
238
|
+
migronaut baseline --json # machine-readable output ({ "baselined": [...], ... })
|
|
239
|
+
|
|
209
240
|
# create — generate a migration file
|
|
210
241
|
migronaut create <name> # file type follows config `createExtension` (default .js)
|
|
211
242
|
migronaut create <name> --ts # force a .ts file
|
|
@@ -222,6 +253,8 @@ migronaut up <file> --force # re-run an ALREADY-applied file (asks for co
|
|
|
222
253
|
migronaut up <file> --force --yes # confirm a re-run non-interactively (required with --json)
|
|
223
254
|
migronaut up --strict # abort on any checksum mismatch
|
|
224
255
|
migronaut up --no-lock # skip the concurrency lock (local dev only)
|
|
256
|
+
migronaut up --converge # converge the declared collections afterwards (bulk runs only)
|
|
257
|
+
migronaut up --no-converge # don't, even with convergeAfterUp on
|
|
225
258
|
migronaut up --json # machine-readable output (array of run results)
|
|
226
259
|
|
|
227
260
|
# down — roll back
|
|
@@ -239,6 +272,15 @@ migronaut redo <file> # a specific file
|
|
|
239
272
|
migronaut redo --no-lock # skip the lock (dev only)
|
|
240
273
|
migronaut redo --json # machine-readable output (array of run results)
|
|
241
274
|
|
|
275
|
+
# converge — declared indexes and validators → the database
|
|
276
|
+
migronaut converge # plan, ask before any drop/rebuild, then apply
|
|
277
|
+
migronaut converge --dry-run # show the plan, change nothing
|
|
278
|
+
migronaut converge --check # exit 28 if anything would change (CI gate)
|
|
279
|
+
migronaut converge --prune # also drop indexes a definition does not declare
|
|
280
|
+
migronaut converge --yes # no confirmation (required for drops/rebuilds with --json)
|
|
281
|
+
migronaut converge --no-lock # skip the concurrency lock (local dev only)
|
|
282
|
+
migronaut converge --json # machine-readable output (the converge result)
|
|
283
|
+
|
|
242
284
|
# status — full status table
|
|
243
285
|
migronaut status # the full status table
|
|
244
286
|
migronaut status --check # exit 2 if any migration is pending (CI gate)
|
|
@@ -580,6 +622,100 @@ await migrator.disconnect();
|
|
|
580
622
|
All errors extend `MigronautError` and carry a typed `code` (`LOCK_ALREADY_HELD`, `CHECKSUM_MISMATCH`,
|
|
581
623
|
`NOT_APPLIED`, …), so `catch` blocks stay type-safe.
|
|
582
624
|
|
|
625
|
+
> [!NOTE]
|
|
626
|
+
> Running several kits against several databases in **one process**? Pass `envFile: false` and
|
|
627
|
+
> explicit `uri`/`dbName` to each — `.env` loading mutates the shared `process.env` (dotenv
|
|
628
|
+
> semantics), so different env files could otherwise leak `MIGRONAUT_*` values between kits.
|
|
629
|
+
|
|
630
|
+
</details>
|
|
631
|
+
|
|
632
|
+
<details id="declared-collections">
|
|
633
|
+
<summary><b>Declared collections</b> — indexes and validators as an end state, with no migration file per change</summary>
|
|
634
|
+
|
|
635
|
+
<br>
|
|
636
|
+
|
|
637
|
+
When what matters is *the final shape* of a collection's indexes and validator — not the history
|
|
638
|
+
of how it got there — declare it and let `migronaut converge` make the difference:
|
|
639
|
+
|
|
640
|
+
```js
|
|
641
|
+
// migronaut.config.js
|
|
642
|
+
export default {
|
|
643
|
+
uri: process.env.MIGRONAUT_URI,
|
|
644
|
+
dbName: 'my_app',
|
|
645
|
+
collections: [
|
|
646
|
+
{
|
|
647
|
+
name: 'users',
|
|
648
|
+
indexes: [
|
|
649
|
+
{ key: { email: 1 }, unique: true },
|
|
650
|
+
{ key: { createdAt: 1 }, expireAfterSeconds: 60 * 60 * 24 * 30 },
|
|
651
|
+
],
|
|
652
|
+
validator: { $jsonSchema: { bsonType: 'object', required: ['email'] } },
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
collectionsDir: './collections', // …and/or one file per collection
|
|
656
|
+
};
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
migronaut converge --dry-run # what would change
|
|
661
|
+
migronaut converge # apply — asks before dropping or rebuilding an index
|
|
662
|
+
migronaut converge --check # exit 28 on drift: a CI gate
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
- **Stateless.** Every run reads `listIndexes` / `listCollections` and compares; nothing is
|
|
666
|
+
recorded. Edit the declaration, converge again.
|
|
667
|
+
- **Safe by default.** Missing indexes are created, a TTL or `hidden` change is applied in place,
|
|
668
|
+
a changed index is rebuilt (and put back if the rebuild fails). An index you did not declare is
|
|
669
|
+
**kept and reported** — dropped only with `prune`. An identical index under another name is
|
|
670
|
+
accepted as is, never silently rebuilt.
|
|
671
|
+
- **Locked like a migration**, and refused before the first write when the plan has a conflict.
|
|
672
|
+
- **After every deploy** with `convergeAfterUp: true` — a bulk `up` then ends by converging,
|
|
673
|
+
under the same lock — or as a [queue job](https://migronaut.vercel.app/guide/bullmq#converge-jobs).
|
|
674
|
+
|
|
675
|
+
Experimental in 2.1. → **[Declared Collections](https://migronaut.vercel.app/guide/collections)**
|
|
676
|
+
|
|
677
|
+
</details>
|
|
678
|
+
|
|
679
|
+
<details>
|
|
680
|
+
<summary><b>Migrations as a queue</b> — a migration service on BullMQ, one migration per job</summary>
|
|
681
|
+
|
|
682
|
+
<br>
|
|
683
|
+
|
|
684
|
+
`@alexify/migronaut/bullmq` enqueues each pending migration as its own BullMQ job and applies them
|
|
685
|
+
in order with a single-concurrency worker. BullMQ is **injected** — it is your dependency, never
|
|
686
|
+
migronaut's:
|
|
687
|
+
|
|
688
|
+
```js
|
|
689
|
+
const { Queue, Worker, QueueEvents } = require('bullmq');
|
|
690
|
+
const { createMigrationQueue } = require('@alexify/migronaut/bullmq');
|
|
691
|
+
|
|
692
|
+
const mq = createMigrationQueue({
|
|
693
|
+
config: { uri: process.env.MIGRONAUT_URI, dbName: 'my_app' },
|
|
694
|
+
bullmq: { Queue, Worker, QueueEvents },
|
|
695
|
+
connection: { host: 'redis', port: 6379 },
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
await mq.startWorker(); // the process that applies migrations
|
|
699
|
+
|
|
700
|
+
const group = await mq.enqueueUp(); // one job per pending migration, one shared batch
|
|
701
|
+
const { results } = await group.wait(); // optional: block until they all finished
|
|
702
|
+
|
|
703
|
+
await mq.enqueueDown(); // roll the last batch back, newest first
|
|
704
|
+
await mq.schedule({ every: 300_000 }); // or keep the database migrated on a schedule
|
|
705
|
+
await mq.enqueueConverge(); // declared indexes and validators, as a job
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
- **Order comes from MongoDB, not from Redis.** Every job is a normal single-file run under the
|
|
709
|
+
usual lock, and refuses while an earlier migration is still pending — so a failed migration
|
|
710
|
+
stops the line (`MIGRATION_BLOCKED`), and a CLI `migronaut up` at the same moment is safe.
|
|
711
|
+
- **One attempt per job, on purpose** — a queue retry would let later migrations overtake the
|
|
712
|
+
failed one. Duplicate enqueues are deduplicated; an already-applied migration completes as
|
|
713
|
+
`skipped`.
|
|
714
|
+
- **Bring your own Worker** with `createMigrationProcessor()` (NestJS, BullMQ Pro).
|
|
715
|
+
|
|
716
|
+
→ **[Migrations as a Queue](https://migronaut.vercel.app/guide/bullmq)** ·
|
|
717
|
+
[runnable example service](examples/migration-service)
|
|
718
|
+
|
|
583
719
|
</details>
|
|
584
720
|
|
|
585
721
|
---
|
|
@@ -592,7 +728,9 @@ All errors extend `MigronautError` and carry a typed `code` (`LOCK_ALREADY_HELD`
|
|
|
592
728
|
|
|
593
729
|
A config file is optional and auto-discovered in the working directory as `migronaut.config.ts`,
|
|
594
730
|
`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
|
|
731
|
+
so every setting lives in one documented place. A generated `migronaut.config.json` points its
|
|
732
|
+
`$schema` at the hosted copy for editor completion; offline or air-gapped setups can point it at
|
|
733
|
+
the copy every install already ships: `./node_modules/@alexify/migronaut/migronaut.schema.json`.
|
|
596
734
|
|
|
597
735
|
```js
|
|
598
736
|
// migronaut.config.js — generated by `migronaut init`, every option explained
|
|
@@ -612,19 +750,100 @@ export default {
|
|
|
612
750
|
// ── Bookkeeping collections ─────────────────────────────────────────────
|
|
613
751
|
migrationsCollection: '_migronaut_migrations', // the append-only audit trail
|
|
614
752
|
lockCollection: '_migronaut_locks', // the concurrency lock
|
|
753
|
+
convergeLogCollection: '_migronaut_converge', // what each converge changed
|
|
615
754
|
lockTTLSeconds: 60, // a lock older than this is reclaimable
|
|
616
755
|
|
|
617
756
|
// ── Safety ──────────────────────────────────────────────────────────────
|
|
618
757
|
strict: false, // true → abort on a checksum mismatch (instead of warn + skip)
|
|
619
758
|
useTransaction: false, // true → wrap every migration in a transaction (override per file)
|
|
620
759
|
|
|
760
|
+
// ── Declared collections (experimental) — see `migronaut converge` ──────
|
|
761
|
+
// collections: [{ name: 'users', indexes: [{ key: { email: 1 }, unique: true }] }],
|
|
762
|
+
// collectionsDir: './collections', // one definition file per collection
|
|
763
|
+
// convergeAfterUp: false, // true → every bulk `up` ends by converging
|
|
764
|
+
|
|
621
765
|
// ── Code-only options (omit in migronaut.config.json) ─────────────────────────
|
|
622
766
|
// hooks: { beforeAll, afterAll, beforeEach, afterEach, onError },
|
|
623
767
|
// mongoose: myMongooseInstance, // pass if your migrations use Mongoose models
|
|
624
768
|
// logger: null, // null silences all output; a pino instance works directly
|
|
769
|
+
// generateId: ulid, // your id format for run ids — any sync `() => string`
|
|
770
|
+
// telemetry: { tracer, meter }, // OpenTelemetry, from your own @opentelemetry/api
|
|
625
771
|
};
|
|
626
772
|
```
|
|
627
773
|
|
|
774
|
+
<details>
|
|
775
|
+
<summary><b>Custom id format</b> — ULID, CUID, UUIDv7 or anything else instead of UUIDs</summary>
|
|
776
|
+
|
|
777
|
+
<br>
|
|
778
|
+
|
|
779
|
+
Every run gets a `runId` — stamped on its changelog records, events and log lines, and stored as
|
|
780
|
+
the lock's owner token — and every queue enqueue gets a `groupId`. Both are `crypto.randomUUID()`
|
|
781
|
+
by default. Pass `generateId` to mint them with the generator the rest of your system uses:
|
|
782
|
+
|
|
783
|
+
```js
|
|
784
|
+
const { ulid } = require('ulid');
|
|
785
|
+
const { runMigrations } = require('@alexify/migronaut');
|
|
786
|
+
|
|
787
|
+
await runMigrations({
|
|
788
|
+
uri: process.env.MIGRONAUT_URI,
|
|
789
|
+
dbName: 'my_app',
|
|
790
|
+
generateId: ulid, // createId (cuid2), nanoid, uuidv7 … pass straight through
|
|
791
|
+
});
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
It is called with no arguments and must synchronously return a non-empty string of at most 128
|
|
795
|
+
characters; anything else fails the run with `CONFIG_INVALID` before a migration starts. Ids are
|
|
796
|
+
for correlation only — the lock carries a token of its own, so a generator that repeats a value
|
|
797
|
+
can never let two runs hold the lock at once.
|
|
798
|
+
|
|
799
|
+
</details>
|
|
800
|
+
|
|
801
|
+
<details>
|
|
802
|
+
<summary><b>OpenTelemetry</b> — a span per run and per migration, and their durations as metrics</summary>
|
|
803
|
+
|
|
804
|
+
<br>
|
|
805
|
+
|
|
806
|
+
Pass a tracer and/or a meter from your own `@opentelemetry/api` — migronaut never imports it:
|
|
807
|
+
|
|
808
|
+
```js
|
|
809
|
+
const { metrics, trace } = require('@opentelemetry/api');
|
|
810
|
+
const { runMigrations } = require('@alexify/migronaut');
|
|
811
|
+
|
|
812
|
+
await runMigrations({
|
|
813
|
+
uri: process.env.MIGRONAUT_URI,
|
|
814
|
+
dbName: 'my_app',
|
|
815
|
+
telemetry: {
|
|
816
|
+
tracer: trace.getTracer('@alexify/migronaut'),
|
|
817
|
+
meter: metrics.getMeter('@alexify/migronaut'),
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
```
|
|
823
|
+
migronaut.run one per run, once it holds the lock
|
|
824
|
+
└─ migronaut.migration one per migration — the ACTIVE span while it runs
|
|
825
|
+
├─ insert users ← your instrumented MongoDB driver nests here
|
|
826
|
+
└─ update _migronaut_migrations
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
That nesting is the point. An instrumented driver only records a command that has a parent span,
|
|
830
|
+
and a migration run at application startup has none — so without this, the driver's spans for your
|
|
831
|
+
migrations are simply missing. A [lifecycle event](#advanced-features) can tell you a migration
|
|
832
|
+
started; only a span opened inside the kit can be the parent of what it does next.
|
|
833
|
+
|
|
834
|
+
The meter gets `migronaut.run.duration`, `migronaut.migration.duration` and
|
|
835
|
+
`migronaut.lock.acquire.duration` (histograms, seconds), plus the counters `migronaut.lock.refused`
|
|
836
|
+
and `migronaut.lock.lost`. A failure sets the span's status to `ERROR` — with the message redacted
|
|
837
|
+
like every log line — and `error.type` to the typed error code. A tracer or meter that throws never
|
|
838
|
+
fails a run.
|
|
839
|
+
|
|
840
|
+
Through the [BullMQ adapter](https://migronaut.vercel.app/guide/bullmq), add
|
|
841
|
+
`bullmq: { Queue, Worker, telemetry: new BullMQOtel({ tracerName }) }` and one trace runs from the
|
|
842
|
+
request that enqueued to the MongoDB commands in the worker. Full guide:
|
|
843
|
+
[OpenTelemetry](https://migronaut.vercel.app/guide/opentelemetry).
|
|
844
|
+
|
|
845
|
+
</details>
|
|
846
|
+
|
|
628
847
|
<details>
|
|
629
848
|
<summary><b>Structured logging with pino</b> — the logger option is pino-compatible</summary>
|
|
630
849
|
|
|
@@ -662,6 +881,7 @@ optional rather than merely discouraged:
|
|
|
662
881
|
| `MIGRONAUT_MIGRATIONS_DIR` | `migrationsDir` | `./migrations` |
|
|
663
882
|
| `MIGRONAUT_COLLECTION` | `migrationsCollection` | `_migronaut_migrations` |
|
|
664
883
|
| `MIGRONAUT_LOCK_COLLECTION` | `lockCollection` | `_migronaut_locks` |
|
|
884
|
+
| `MIGRONAUT_CONVERGE_LOG_COLLECTION` | `convergeLogCollection` | `_migronaut_converge` |
|
|
665
885
|
| `MIGRONAUT_LOCK_TTL` | `lockTTLSeconds` | `60` |
|
|
666
886
|
| `MIGRONAUT_STRICT` | `strict` | `false` |
|
|
667
887
|
| `MIGRONAUT_USE_TRANSACTION` | `useTransaction` | `false` |
|
|
@@ -671,12 +891,16 @@ optional rather than merely discouraged:
|
|
|
671
891
|
| `MIGRONAUT_TEMPLATE_PATH` | `templatePath` | — *(built-in template)* |
|
|
672
892
|
| `MIGRONAUT_TIMEOUT_MS` | `timeoutMs` | — *(no timeout)* |
|
|
673
893
|
| `MIGRONAUT_ON_LOCK_LOST` | `onLockLost` | `abort` |
|
|
894
|
+
| `MIGRONAUT_ON_OUT_OF_ORDER` | `onOutOfOrder` | `warn` |
|
|
674
895
|
| `MIGRONAUT_ENSURE_INDEXES` | `ensureIndexes` | `true` |
|
|
675
896
|
| `MIGRONAUT_RELOAD_MIGRATIONS` | `reloadMigrations` | `false` |
|
|
897
|
+
| `MIGRONAUT_COLLECTIONS_DIR` | `collectionsDir` | — *(none read)* |
|
|
898
|
+
| `MIGRONAUT_CONVERGE_AFTER_UP` | `convergeAfterUp` | `false` |
|
|
676
899
|
| `MIGRONAUT_ENV_FILE` | `envFile` | `.env` |
|
|
677
900
|
|
|
678
|
-
`fileExtensions`, `clientOptions`, `client`, `mongoose`, `hooks
|
|
679
|
-
only — they aren't scalars, so no environment
|
|
901
|
+
`fileExtensions`, `clientOptions`, `collections`, `client`, `mongoose`, `hooks`, `logger`,
|
|
902
|
+
`generateId` and `telemetry` are config-file/API only — they aren't scalars, so no environment
|
|
903
|
+
variable can express them.
|
|
680
904
|
|
|
681
905
|
A value that doesn't parse is **rejected, never coerced**: `MIGRONAUT_STRICT=on` or
|
|
682
906
|
`MIGRONAUT_LOCK_TTL=abc` fails with an error naming the variable, rather than quietly turning a
|
package/bin/migronaut.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const { errorText } = require('../src/utils/error.js');
|
|
3
|
+
const { sanitizeTerminal } = require('../src/utils/sanitize.js');
|
|
4
|
+
|
|
5
|
+
// Same hygiene as the default logger: these last-resort handlers are the only
|
|
6
|
+
// paths that write to the TTY directly, and an escaped error message can embed
|
|
7
|
+
// DB-derived text — sanitized by construction, like every other output path.
|
|
8
|
+
const writeError = (error) => {
|
|
9
|
+
process.stderr.write(`✖ ${sanitizeTerminal(errorText(error))}\n`);
|
|
10
|
+
};
|
|
3
11
|
|
|
4
12
|
// `status --json | head` closes the pipe early; the resulting EPIPE arrives as
|
|
5
13
|
// a stream 'error' event, not a promise rejection, and would otherwise crash
|
|
@@ -13,7 +21,7 @@ for (const stream of [process.stdout, process.stderr]) {
|
|
|
13
21
|
// A rejection escaping the CLI's own handling still gets the project's error
|
|
14
22
|
// format instead of Node's default trace.
|
|
15
23
|
process.on('unhandledRejection', (error) => {
|
|
16
|
-
|
|
24
|
+
writeError(error);
|
|
17
25
|
process.exitCode = 1;
|
|
18
26
|
});
|
|
19
27
|
|
|
@@ -22,14 +30,14 @@ process.on('unhandledRejection', (error) => {
|
|
|
22
30
|
// skips errorText's URI redaction. exit(1), not exitCode: after an uncaught
|
|
23
31
|
// throw the process state is not trustworthy enough to keep draining.
|
|
24
32
|
process.on('uncaughtException', (error) => {
|
|
25
|
-
|
|
33
|
+
writeError(error);
|
|
26
34
|
process.exit(1);
|
|
27
35
|
});
|
|
28
36
|
|
|
29
37
|
const { run } = require('../src/cli/index.js');
|
|
30
38
|
|
|
31
39
|
run(process.argv).catch((error) => {
|
|
32
|
-
|
|
40
|
+
writeError(error);
|
|
33
41
|
// exitCode, not exit(): letting the event loop drain flushes buffered
|
|
34
42
|
// stdout/stderr writes that process.exit() would truncate on a pipe.
|
|
35
43
|
process.exitCode = 1;
|