@alexify/migronaut 2.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 +320 -0
- package/README.md +208 -6
- package/bullmq.d.ts +845 -0
- package/bullmq.js +1 -0
- package/index.d.ts +634 -18
- package/migronaut.schema.json +182 -1
- package/package.json +21 -5
- 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/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/up.js +14 -1
- package/src/cli/exit-codes.js +9 -2
- package/src/cli/index.js +2 -0
- package/src/cli/shared.js +14 -4
- package/src/cli/table.js +105 -0
- package/src/core/changelog.js +71 -6
- package/src/core/collections.js +372 -0
- package/src/core/config.js +100 -25
- 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/index-spec.js +496 -0
- package/src/core/lock-wait.js +260 -0
- package/src/core/lock.js +45 -16
- package/src/core/migrator.js +563 -283
- package/src/core/options.js +251 -0
- package/src/core/run-recorder.js +157 -0
- package/src/core/run.js +58 -90
- package/src/core/sequence.js +134 -0
- package/src/errors/index.js +56 -0
- package/src/index.js +8 -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/migration-name.js +32 -0
- package/src/utils/redact.js +21 -1
- package/src/utils/telemetry.js +393 -0
- package/src/utils/template.js +36 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,326 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
Release headings carry the publish date (`## vX.Y.Z — YYYY-MM-DD`).
|
|
5
5
|
|
|
6
|
+
## v2.1.0 — 2026-10-04
|
|
7
|
+
|
|
8
|
+
Migrations as a queue, ids in your own format, OpenTelemetry, and declared collections. Additive:
|
|
9
|
+
nothing changes for anyone who uses none of them, with the narrow exceptions listed under
|
|
10
|
+
**Changed**.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`@alexify/migronaut/bullmq`** — a new entry point that runs migrations as
|
|
15
|
+
[BullMQ](https://docs.bullmq.io/) jobs, **one migration per job**, so migronaut can be a
|
|
16
|
+
migration service: enqueue from an HTTP handler, a schedule or a deploy hook, and let a worker
|
|
17
|
+
apply them in order.
|
|
18
|
+
- `createMigrationQueue(options)` — the facade: `enqueueUp` / `enqueueDown` (returning a group
|
|
19
|
+
handle with `wait()`), `startWorker`, `status` / `pending` / `audit` / `lockInfo`, `getJob`,
|
|
20
|
+
`pause` / `resume`, `schedule` / `unschedule`, `close`.
|
|
21
|
+
- `createMigrationProcessor(options)` — the processor on its own, for a Worker you construct
|
|
22
|
+
(NestJS, BullMQ Pro), plus `enqueueUp` / `enqueueDown` / `planUpJobs` / `planDownJobs` /
|
|
23
|
+
`waitForGroup` for a Queue you own.
|
|
24
|
+
- **BullMQ is injected, never depended on** — `bullmq: { Queue, Worker, QueueEvents }` from
|
|
25
|
+
your own install. The package still has no `dependencies` and gains no peer; `src/` never
|
|
26
|
+
imports `bullmq` (a test enforces it).
|
|
27
|
+
- **Order comes from MongoDB, not from Redis.** Every job is a single-file run under the usual
|
|
28
|
+
lock; it refuses while an earlier migration is still pending, so a failed migration stops the
|
|
29
|
+
line (`MIGRATION_BLOCKED` for the jobs behind it). Jobs get one attempt on purpose — a BullMQ
|
|
30
|
+
retry re-queues behind the waiting jobs — and a held lock is waited out inside the job.
|
|
31
|
+
- **One batch per enqueue**, so `down` still rolls back a whole deploy; duplicate enqueues are
|
|
32
|
+
deduplicated, and a job whose migration is already applied completes as `skipped`.
|
|
33
|
+
- Job payloads are validated as untrusted input; messages, stacks and job logs are redacted. A
|
|
34
|
+
queue job's target must be a file of the migration sequence — a payload can never make the
|
|
35
|
+
worker import a dotfile, a declaration file or a helper module next to the migrations.
|
|
36
|
+
- **Shutdown puts unstarted work back.** A job that a closing worker stops before its migration
|
|
37
|
+
starts (waiting for the lock, or fetched during shutdown) is moved back to the head of the
|
|
38
|
+
queue instead of failing — so a rolling deploy no longer fails the rest of the enqueue it
|
|
39
|
+
interrupts as `MIGRATION_BLOCKED`. A migration already running always finishes.
|
|
40
|
+
- **A versioned, strict job contract.** A worker accepts every job data version from
|
|
41
|
+
`MIN_JOB_DATA_VERSION` (exported) up to its own and refuses newer ones and unknown fields
|
|
42
|
+
rather than ignoring what they mean — roll workers out before producers. Jobs always state
|
|
43
|
+
`ordered`, and may carry the plan-time `checksum` of their file.
|
|
44
|
+
- **Scheduler ticks are bounded**: they carry the queue's `jobOptions`, and keep the last 100
|
|
45
|
+
completed / 500 failed jobs when those set no retention.
|
|
46
|
+
- **Correlation**: a job's `runId` is on its progress (`completed` and `failed`), its failure log
|
|
47
|
+
row and its error's `context` (with `jobId` and `groupId`) — a failed job has no return value;
|
|
48
|
+
the worker's failure log line names the group, migration and run.
|
|
49
|
+
- An injected `Queue` (or `QueueEvents`) on another name or prefix than the facade's is rejected,
|
|
50
|
+
and the facade takes an injected queue's prefix by default; `startWorker()` can be retried
|
|
51
|
+
after a failed start; a closed queue refuses every further call.
|
|
52
|
+
- **A worker decides what a job may ask for** — the `allow` option (`{ down: true, force:
|
|
53
|
+
false, unordered: false }` by default) on `createMigrationProcessor` and
|
|
54
|
+
`createMigrationQueue`: a payload asking to re-run an applied migration or to skip the order
|
|
55
|
+
guard is refused (`QUEUE_JOB_INVALID`, `context.permission`) unless allowed. The facade's
|
|
56
|
+
`enqueue*` calls follow the same policy.
|
|
57
|
+
- **A job runs only the file it was planned with**: an `up` job carries the plan-time checksum,
|
|
58
|
+
and a worker with another version of the file fails it (`CHECKSUM_MISMATCH`,
|
|
59
|
+
`context.planned`) instead of applying it.
|
|
60
|
+
- **Several workers without global concurrency**: a job blocked only by earlier migrations that
|
|
61
|
+
have not failed (one may be in flight on another worker) waits for them within its lock-wait
|
|
62
|
+
budget instead of failing its group; `MigrationBlockedError` carries `context.failed`.
|
|
63
|
+
- `wait()` holds every await to its one `timeoutMs` budget, decides a timeout by the clock, and
|
|
64
|
+
its `QueueJobFailedError` carries the job's own typed `code`; an empty group can be waited for
|
|
65
|
+
without QueueEvents. A long lock wait reports progress every few seconds, not every poll.
|
|
66
|
+
- What the queue stores about a failure — `failedReason`, stack, job logs — masks the values a
|
|
67
|
+
duplicate-key error quotes, on top of credentials. `schedule({ every })` needs at least 1000 ms.
|
|
68
|
+
- Dedup ids encode file names reversibly (two names can no longer share one and absorb each
|
|
69
|
+
other's job), and a forced re-run has a dedup id of its own.
|
|
70
|
+
- **A schedule holds a failed migration**: a `sync` tick whose next migration failed, with its
|
|
71
|
+
file unchanged since, enqueues nothing (`returnvalue.held`, and a warning) instead of re-running
|
|
72
|
+
it every tick; a changed file or an explicit `enqueueUp(name)` resumes it.
|
|
73
|
+
- Jobs carry `requestedBy` / `reason` (`enqueueUp` / `enqueueDown` / `enqueueConverge` options).
|
|
74
|
+
- **`bullmq.d.ts`** — hand-written types for the entry point, with structural `BullMQ*Like`
|
|
75
|
+
interfaces instead of an import of `bullmq`, generic over the classes you inject.
|
|
76
|
+
- **`up(file, { batch })`** — stamp an explicit batch number instead of the next free one, and
|
|
77
|
+
**`MigratorKit.nextBatch()`** to peek at it: together they let several single-file runs form one
|
|
78
|
+
batch.
|
|
79
|
+
- **`up(file, { ordered: true })` / `down(file, { ordered: true })`** — refuse a single-file run
|
|
80
|
+
that would go out of sequence (`MigrationBlockedError`); an ordered `up` also applies the
|
|
81
|
+
`strict` drift check and the `onOutOfOrder` policy a bulk run would, and only targets a file of
|
|
82
|
+
the migration sequence.
|
|
83
|
+
- **`up(file, { checksum })`** — refuse (`CHECKSUM_MISMATCH`, `context.planned`) to apply any other
|
|
84
|
+
version of the file than the one with this SHA-256; `dryRun('up')` rows carry each file's
|
|
85
|
+
`checksum` for it.
|
|
86
|
+
- **`list(filter, { checksums: false })`** / **`status({ checksums: false })`** — skip hashing the
|
|
87
|
+
applied files, for a caller that needs names and dates only.
|
|
88
|
+
- **Who asked, and why** — `up`, `down`, `redo` and `converge` take `requestedBy` (≤ 128
|
|
89
|
+
characters) and `reason` (≤ 512), and `migronaut up` / `down` / `redo` / `converge` take
|
|
90
|
+
`--reason`. They are stamped on the changelog (`requestedBy` / `reason` on an apply,
|
|
91
|
+
`revertRequestedBy` / `revertReason` on a revert; a later apply that says nothing clears the old
|
|
92
|
+
ones) and on the converge history; `status()` rows show them. `executedBy` stays the OS user —
|
|
93
|
+
on a queue worker, the container's — which is why the requester has fields of its own. A failed
|
|
94
|
+
attempt's trace now also records the checksum of the file version that failed
|
|
95
|
+
(`StatusRow.failedChecksum`).
|
|
96
|
+
- **`runMigrations(config, { signal })`** — an `AbortSignal` (wired to SIGTERM) stops a wait for
|
|
97
|
+
the lock between polls, and a run that holds it between migrations.
|
|
98
|
+
- **`LockInfo.runId` and `LockInfo.ttlMs`** — which run holds the lock (also on `migronaut lock`),
|
|
99
|
+
and the holder's TTL, which the lock document now records.
|
|
100
|
+
- **Three error codes**: `MIGRATION_BLOCKED` (exit 24), `QUEUE_JOB_INVALID` (25),
|
|
101
|
+
`QUEUE_JOB_FAILED` (26), with `MigrationBlockedError`, `QueueJobInvalidError` and
|
|
102
|
+
`QueueJobFailedError` exported from the package root.
|
|
103
|
+
- **Runnable example** — `examples/migration-service`: a queue, a worker and a plain `node:http`
|
|
104
|
+
API (not published to npm).
|
|
105
|
+
- **`generateId` config option** — your own identifier format (ULID, CUID, nanoid, UUIDv7, …)
|
|
106
|
+
everywhere migronaut used to call `crypto.randomUUID()`: the `runId` of every run — on changelog
|
|
107
|
+
records, events, log lines and the lock's owner token — and the `groupId` of every queue
|
|
108
|
+
enqueue. One option covers the kit, the CLI (through `migronaut.config.js`/`.ts`),
|
|
109
|
+
`runMigrations` and the queue adapter.
|
|
110
|
+
- **Injected, like the logger** — migronaut ships no generator but the default. It is called
|
|
111
|
+
with no arguments, so third-party functions pass straight through: `generateId: ulid`.
|
|
112
|
+
- **Checked on every call** — it must synchronously return a non-empty string of at most 128
|
|
113
|
+
characters. A throw, a promise or anything else fails the run with `CONFIG_INVALID` before a
|
|
114
|
+
migration starts (and an enqueue before a job is added).
|
|
115
|
+
- Code-only: no environment variable and no place in a JSON config, like `logger` and `hooks`.
|
|
116
|
+
- **`MigratorKit.generateId()`** — a new id in the kit's configured format, for code that wants its
|
|
117
|
+
own ids to match (it is how the queue adapter mints group ids). Resolves the config; does not
|
|
118
|
+
connect.
|
|
119
|
+
- **`IdGenerator` type** — `() => string`, exported from the package root.
|
|
120
|
+
- **`telemetry` config option** — OpenTelemetry traces and metrics through a tracer and/or a meter
|
|
121
|
+
from your own `@opentelemetry/api`: `telemetry: { tracer, meter }`. One option covers the kit,
|
|
122
|
+
the CLI (through `migronaut.config.js`/`.ts`), `runMigrations` and the queue adapter.
|
|
123
|
+
- **Spans**: `migronaut.run` for every run that acquired the lock, and a child
|
|
124
|
+
`migronaut.migration` for every migration executed. The migration's span is the *active* one
|
|
125
|
+
while its hooks, its `up`/`down` and its changelog write run — so an instrumented MongoDB
|
|
126
|
+
driver nests its command spans under the migration that issued them. That is what lifecycle
|
|
127
|
+
events cannot do, and why this lives in the kit: at application startup there is no ambient
|
|
128
|
+
span, and the driver instrumentation records nothing without a parent.
|
|
129
|
+
- **Metrics**: `migronaut.run.duration`, `migronaut.migration.duration`,
|
|
130
|
+
`migronaut.lock.acquire.duration` and `migronaut.lock.wait.duration` (one point per wait for a
|
|
131
|
+
held lock, by `migronaut.lock.wait.outcome`: `acquired`, `timeout`, `aborted`) — histograms,
|
|
132
|
+
in seconds, with boundaries from 10ms to an hour — and the counters `migronaut.lock.refused`
|
|
133
|
+
and `migronaut.lock.lost`.
|
|
134
|
+
- **Dimensions**: every span and metric point carries `db.namespace` (the database name), plus
|
|
135
|
+
the caller's own static attributes from `telemetry.attributes` (at most 20 scalars).
|
|
136
|
+
- **Failures** set the span's status to `ERROR` with a redacted message (credentials and the
|
|
137
|
+
values a duplicate-key error quotes masked, at most 1 KB), and `error.type` — on
|
|
138
|
+
the span and on the metric point — to the typed error code. No exception event is recorded: it
|
|
139
|
+
would carry the unredacted message and stack.
|
|
140
|
+
- **A run that never got the lock emits no span.** A caller polling for a busy lock retries the
|
|
141
|
+
whole run every few hundred milliseconds; the refusals are counted
|
|
142
|
+
(`migronaut.lock.refused`) instead.
|
|
143
|
+
- **Injected, like the logger** — `@opentelemetry/api` is neither a dependency nor a peer, and
|
|
144
|
+
`src/` never imports it (a test enforces it). The types are structural: `MigronautTracer`,
|
|
145
|
+
`MigronautSpan`, `MigronautMeter`, `MigronautHistogram`, `MigronautCounter`,
|
|
146
|
+
`MigronautMetricOptions`, `MigronautAttributes` and `MigronautTelemetry`, exported from the
|
|
147
|
+
package root.
|
|
148
|
+
- **It can never fail a run.** Every call into the tracer, a span, the meter and an instrument
|
|
149
|
+
is guarded — a promise one of them returns included, so a rejecting SDK cannot surface as an
|
|
150
|
+
unhandled rejection — and a tracer that throws before or after running the work, or runs it
|
|
151
|
+
twice, still gets each migration executed exactly once.
|
|
152
|
+
- Code-only, like `logger` and `generateId`. The span, attribute and metric names are new and
|
|
153
|
+
should be treated as experimental.
|
|
154
|
+
- **`bullmq.telemetry`** — `createMigrationQueue({ bullmq: { Queue, Worker, telemetry } })` hands
|
|
155
|
+
BullMQ's own telemetry object (`new BullMQOtel({ tracerName })` from `bullmq-otel`) to the Queue
|
|
156
|
+
and the Worker it constructs. Together with the kit's `telemetry` it gives one trace from the
|
|
157
|
+
request that enqueued, across Redis, to the MongoDB commands in the worker. Until now the facade
|
|
158
|
+
built its Queue without it, so the enqueuing side of that trace could not be joined.
|
|
159
|
+
`workerOptions.telemetry` and `startWorker({ telemetry })` override it for the worker alone.
|
|
160
|
+
- **OpenTelemetry in the example** — `examples/migration-service` gains `tracing.js` and a Jaeger
|
|
161
|
+
in its `docker-compose.yml`: set `OTEL_EXPORTER_OTLP_ENDPOINT` and every enqueue is one trace.
|
|
162
|
+
- **Declared collections** — indexes and validators declared as an end state, and applied by
|
|
163
|
+
`migronaut converge`, with no migration file per change. For what only ever has a current value
|
|
164
|
+
(which indexes a collection has, which validator guards it); migrations stay the tool for
|
|
165
|
+
changes with an order and a history.
|
|
166
|
+
- **`collections` config option** — an array of definitions, `{ name, indexes?, validator?,
|
|
167
|
+
validationLevel?, validationAction?, prune? }`, each index in the driver's own flat
|
|
168
|
+
`createIndexes` shape. Works in `migronaut.config.{ts,js,json}` (and the JSON Schema) and in
|
|
169
|
+
`new MigratorKit({ collections })`.
|
|
170
|
+
- **`collectionsDir` config option** (`MIGRONAUT_COLLECTIONS_DIR`) — one definition file per
|
|
171
|
+
collection (`.ts`/`.js` default export, or `.json`; the name defaults to the file name).
|
|
172
|
+
Opt-in, combined with `collections`, and loaded only when a converge runs — a broken file never
|
|
173
|
+
blocks `status` or an emergency `down`. A collection declared twice is `CONFIG_INVALID`.
|
|
174
|
+
- **Validated strictly**: an unknown definition key or index option is an error with its path
|
|
175
|
+
(`collections[2].indexes[0].uniqe`) — the driver silently drops an option it does not know, so
|
|
176
|
+
a typo would otherwise build the wrong index and then look in sync forever.
|
|
177
|
+
- **`MigratorKit.converge({ dryRun?, prune?, noLock?, ordered? })`** and **`migronaut converge`**
|
|
178
|
+
(`--dry-run`, `--check`, `--prune`, `--yes`). Stateless: every run reads `listCollections` and
|
|
179
|
+
`listIndexes`, plans, and carries the plan out one step at a time under the migration lock —
|
|
180
|
+
create the collection, set the validator, create indexes, `collMod` a TTL or `hidden` in place,
|
|
181
|
+
rebuild what changed otherwise, drop last. Nothing is recorded.
|
|
182
|
+
- **Safe by default.** An index you did not declare is kept and reported (`keep`), and dropped
|
|
183
|
+
only with `prune` (per definition, or for the definitions that do not decide). An identical
|
|
184
|
+
index under another name is accepted as is rather than rebuilt; a different one covering the
|
|
185
|
+
same key is a conflict that refuses the run before the first write. A rebuild whose new index
|
|
186
|
+
fails to build puts the old one back — and says so, with the reason, when it cannot.
|
|
187
|
+
- **A unique index is never rebuilt unasked.** Dropping it opens a window with no constraint,
|
|
188
|
+
and a duplicate written in that window leaves neither index buildable. Such a rebuild is a
|
|
189
|
+
`conflict` unless `converge({ rebuildUnique: true })` / `--rebuild-unique`; a converge after
|
|
190
|
+
`up` and a queue job never pass it.
|
|
191
|
+
- **Comparisons follow what the server stores** — text indexes in their `_fts` form, collations
|
|
192
|
+
field by field against the expanded spec (`strength`, `caseLevel` and `numericOrdering` at
|
|
193
|
+
their universal defaults when left out), the collection's default collation,
|
|
194
|
+
`{ locale: 'simple' }`, a flag stored as `1`. A compound `Map` key may hold an integer-like
|
|
195
|
+
field only first — the driver reads keys back as plain objects.
|
|
196
|
+
Anything applied that still compares as changed is reported under `unstable` instead of being
|
|
197
|
+
rebuilt on every run.
|
|
198
|
+
- **The CLI plans first** and asks before any drop or rebuild, and before changing the validator
|
|
199
|
+
of a collection that holds data; `--json` refuses such a plan without `--yes` (and applies a
|
|
200
|
+
purely additive one); a plan with a conflict is refused without asking. `--check` exits `28` on
|
|
201
|
+
drift — a CI gate; `--ordered` refuses while a migration is pending.
|
|
202
|
+
- **Results say what changed**: every row that changes, drops or keeps something carries
|
|
203
|
+
`from` and `to` — the live and the declared index or validator, as plain JSON.
|
|
204
|
+
- **History** — every converge that changes something or fails appends an entry to
|
|
205
|
+
`_migronaut_converge` (`convergeLogCollection`, `MIGRONAUT_CONVERGE_LOG_COLLECTION`): when,
|
|
206
|
+
the trigger, the run id, who ran it and where, who asked and why, and every row it touched
|
|
207
|
+
with its `from` / `to`. Read it with `MigratorKit.convergeHistory({ limit })` or
|
|
208
|
+
`migronaut converge --history [--limit n] [--json]`. Best-effort, and a database that never
|
|
209
|
+
converges never gets the collection.
|
|
210
|
+
- **In place where the server can**: making an index unique (MongoDB 7.0+, `collMod`
|
|
211
|
+
`prepareUnique` then `unique` — duplicates leave the index as it was) and adding a TTL to a
|
|
212
|
+
single-field index (5.1+) are `modify`, not a rebuild. (6.0 accepts the unique conversion but
|
|
213
|
+
did not enforce it in our tests, so it rebuilds.)
|
|
214
|
+
- **Sharded clusters**: behind a `mongos`, prune never drops the index backing a shard key (read
|
|
215
|
+
from `config.collections`, or kept with a warning when the server refuses the drop).
|
|
216
|
+
- **Re-planned before each collection**: every collection after the first is read and planned
|
|
217
|
+
again right before its turn; one that changed meanwhile into a conflict or a new drop/rebuild
|
|
218
|
+
stops the run (`ConvergeFailedError`, `phase: 'replan'`) before it is touched.
|
|
219
|
+
- **Regular expressions** in a validator or partial filter must use flags the driver stores as
|
|
220
|
+
written (`i`, `m`; a `BSONRegExp` for server options) — `g` would become dotAll and `s`, `u`,
|
|
221
|
+
`y` vanish — and compare in their stored form. A `__proto__` key in a JSON definition stays a
|
|
222
|
+
key.
|
|
223
|
+
- **At scale**: all declared collections are read with one `listCollections` and a bounded
|
|
224
|
+
fan-out of `listIndexes`; the new indexes of a collection are built by one `createIndexes`
|
|
225
|
+
(one pass over the data); a connection that fails mid-rebuild is reported, never "repaired"
|
|
226
|
+
by restoring the old index next to a build the server may still be running.
|
|
227
|
+
- **`convergeAfterUp` config option** (`MIGRONAUT_CONVERGE_AFTER_UP`) — a bulk `up` (no file, no
|
|
228
|
+
`to`; also `runMigrations`) ends by converging under the same lock, even when nothing was
|
|
229
|
+
pending, so a failed converge is retried by the next deploy. `up(undefined, { converge })` and
|
|
230
|
+
`migronaut up --converge` / `--no-converge` decide per run. `up` still returns its migration
|
|
231
|
+
rows; `runMigrations` adds `summary.converge`.
|
|
232
|
+
- **`MigratorKit.convergesAfterUp()`** — whether a bulk `up` on the kit ends by converging.
|
|
233
|
+
- **Events**: `converge:start`, `converge:action` (per step: `'started'` before it runs — an index
|
|
234
|
+
build can take hours — then `'applied'` or `'failed'`) and `converge:end` (with the full
|
|
235
|
+
result), for real runs. An index build also logs which index it is starting on. The run itself is an ordinary `run:start`/`run:end` with
|
|
236
|
+
`command: 'converge'`, and an ordinary `migronaut.run` span.
|
|
237
|
+
- **Types**: `CollectionDefinition`, `CollectionDefinitionFile`, `IndexDefinition`,
|
|
238
|
+
`IndexKeyDirection`, `IndexCollation`, `ValidationLevel`, `ValidationAction`, `ConvergeOptions`,
|
|
239
|
+
`ConvergeResult`, `CollectionConvergeResult`, `ConvergeAction`, `ConvergeActionKind`,
|
|
240
|
+
`ConvergeActionStatus`, `ConvergeTarget`, `ConvergeUnstable`, `ConvergeTrigger` and the three
|
|
241
|
+
event payloads, exported from the package root.
|
|
242
|
+
- The definition shape, the result shape and the queue contract below are new and should be
|
|
243
|
+
treated as experimental.
|
|
244
|
+
- **Converge jobs in the queue adapter** — `JOB_NAMES.CONVERGE` (`'converge'`), `enqueueConverge()`
|
|
245
|
+
and `MigrationQueue.enqueueConverge()`, and `schedule({ job: 'converge' })` with its own default
|
|
246
|
+
id, `DEFAULT_CONVERGE_SCHEDULER_ID` (`'migronaut-converge'`). With `convergeAfterUp`,
|
|
247
|
+
`enqueueUp` ends a group that reaches the newest migration with a converge job (or adds a
|
|
248
|
+
converge-only job when nothing is pending but a dry run finds drift), and an idle `sync` tick does
|
|
249
|
+
the same. A converge job refuses as `MIGRATION_BLOCKED` while a migration is pending, is keyed for
|
|
250
|
+
deduplication on the migration it follows, and carries no `prune` — what may be dropped comes
|
|
251
|
+
from the worker's own definitions. New types: `ConvergeJobData`, `ConvergeJobResult`,
|
|
252
|
+
`ConvergeJobSpec`, `ConvergeHandle`, `EnqueueConvergeOptions`.
|
|
253
|
+
- **Two exit codes**: `CONVERGE_FAILED` (27, with `ConvergeFailedError` exported from the package
|
|
254
|
+
root) and the CLI-only `COLLECTIONS_DRIFT` (28, from `converge --check`).
|
|
255
|
+
|
|
256
|
+
### Changed
|
|
257
|
+
|
|
258
|
+
- **`MigronautErrorCode` gained four members** (above). TypeScript consumers with an exhaustive
|
|
259
|
+
`switch` over the code union need a `default` branch or the new cases.
|
|
260
|
+
- **`collections`, `collectionsDir` and `convergeAfterUp` config keys are now validated**
|
|
261
|
+
(`CONFIG_INVALID`). They were previously unknown and ignored, like any stray key.
|
|
262
|
+
- **The CLI arg parser makes a `--x` / `--no-x` pair tri-state**, as commander does: when a command
|
|
263
|
+
declares both, neither given leaves the option unset instead of defaulting to `true`. Only
|
|
264
|
+
`up --converge` / `--no-converge` uses this.
|
|
265
|
+
- **`dryRun('up')` now applies the out-of-order policy** of the run it previews: under
|
|
266
|
+
`onOutOfOrder: 'error'` a bulk preview refuses with `MIGRATION_OUT_OF_ORDER` instead of listing
|
|
267
|
+
rows the run would reject; under `'warn'` it logs the warning. A single-file preview is exempt,
|
|
268
|
+
as the single-file run is.
|
|
269
|
+
- **`connect()` is safe to call concurrently** — overlapping first calls on one `MigratorKit`
|
|
270
|
+
share a single connection instead of each opening (and all but one leaking) a client. Matters
|
|
271
|
+
for a long-lived kit serving several callers.
|
|
272
|
+
- The lock-wait loop of `runMigrations` moved to `src/core/lock-wait.js`, shared with the queue
|
|
273
|
+
processor. Two changes to how it waits: **polls back off**, doubling from `lockPollIntervalMs`
|
|
274
|
+
up to 5 s (and at most a quarter of the budget), so a fleet waiting out a long deploy no longer
|
|
275
|
+
hammers the lock document; and **the default `lockWaitTimeoutMs` follows the holder's TTL** —
|
|
276
|
+
`max(90 s, 1.5 × lockTTLSeconds)` — so a holder with a long TTL, whose heartbeat moves the lock
|
|
277
|
+
only every TTL/2, is no longer mistaken for a stalled one. An explicit `lockWaitTimeoutMs` is
|
|
278
|
+
used as given (with a warning when it is shorter than the holder's heartbeat). `waitedMs` is
|
|
279
|
+
now measured by the clock from the first refusal, and a wait that times out rethrows the
|
|
280
|
+
refusal with `context.timedOut`, `attempts` and `waitedMs`.
|
|
281
|
+
- **`runMigrations` validates `onLockHeld`** (`CONFIG_INVALID`): a value other than `'throw'` or
|
|
282
|
+
`'wait'` — `'Wait'`, say — used to behave as `'throw'` without a word. A poll interval above the
|
|
283
|
+
largest timer (2³¹−1 ms, which Node fires after 1 ms) is refused too.
|
|
284
|
+
- **The lock document gained a `nonce` field**, minted by migronaut on every acquire and matched
|
|
285
|
+
alongside `owner` when the lock is confirmed, renewed and released. The owner token is the run
|
|
286
|
+
id, whose format `generateId` now decides; the nonce keeps mutual exclusion independent of it,
|
|
287
|
+
so a generator that repeats an id can blur correlation but never let two runs hold the lock.
|
|
288
|
+
Older releases ignore the field and can share a database with this one. The nonce is never
|
|
289
|
+
exposed; the owner — the holder's run id — now is, as `LockInfo.runId` (`lock`, `lockInfo()`,
|
|
290
|
+
`LockAlreadyHeldError` context), next to the holder's `ttlMs`, also written since this release:
|
|
291
|
+
"which run holds the lock?" is the first question about a stuck one, and the nonce, not the
|
|
292
|
+
owner, is what proves ownership.
|
|
293
|
+
- **A `generateId` config key that is not a function is now rejected** (`CONFIG_INVALID`). The key
|
|
294
|
+
was previously unknown and ignored, like any stray key.
|
|
295
|
+
- **A `telemetry` config key that is not usable is now rejected** (`CONFIG_INVALID`): it must be an
|
|
296
|
+
object whose `tracer` has `startActiveSpan` and whose `meter` has `createHistogram` and
|
|
297
|
+
`createCounter`. Absent, `null` and `{}` all mean "off". The key was previously unknown and
|
|
298
|
+
ignored.
|
|
299
|
+
- **A scheduled `sync` job no longer joins the trace that registered its schedule.** Its template
|
|
300
|
+
now carries `telemetry: { omitContext: true }`: BullMQ builds each scheduler iteration from the
|
|
301
|
+
previous job's options, so with telemetry on, every tick would otherwise have been appended to
|
|
302
|
+
one ever-growing trace. Each tick is a trace of its own; no effect on a queue without telemetry.
|
|
303
|
+
|
|
304
|
+
### Fixed
|
|
305
|
+
|
|
306
|
+
- `docs/reference/cli.md` lists exit code `23` (`MIGRATION_OUT_OF_ORDER`), missing since v2.0.0.
|
|
307
|
+
- The events table in `docs/guide/hooks.md` had fallen behind the payloads: it now lists
|
|
308
|
+
`migration:skipped`, the `command` / `durationMs` / count fields of `run:start` and `run:end`,
|
|
309
|
+
and `ttlMs` / `acquireMs` on `lock:acquired`.
|
|
310
|
+
|
|
311
|
+
### Tooling
|
|
312
|
+
|
|
313
|
+
- A unit test pins `src/utils/id.js` as the only module that mints an identifier, so no id can
|
|
314
|
+
bypass `generateId`.
|
|
315
|
+
- A unit test pins that nothing under `src/` or `bin/`, and neither declaration file, imports an
|
|
316
|
+
`@opentelemetry/*` package or `bullmq-otel`; they are devDependencies only. The structural types
|
|
317
|
+
are checked part by part against the real `@opentelemetry/api`, and one integration test runs the
|
|
318
|
+
real `@opentelemetry/instrumentation-mongodb` to prove the driver's spans nest under a migration.
|
|
319
|
+
- Declared collections are pinned by a table-driven planner test, a fixed-point integration matrix
|
|
320
|
+
(every kind of index converges, then plans as unchanged on a real server), and the shared queue
|
|
321
|
+
scenarios on both the fake and the real BullMQ.
|
|
322
|
+
- An in-tree fake BullMQ carries the adapter's unit and integration tests; the same scenarios run
|
|
323
|
+
against the real `bullmq` package when `MIGRONAUT_TEST_REDIS_URL` is set, which CI now does
|
|
324
|
+
(a Redis service on the `test` job). `bullmq` and `ioredis` are devDependencies for that only.
|
|
325
|
+
|
|
6
326
|
## v2.0.0 — 2026-08-30
|
|
7
327
|
|
|
8
328
|
A major bump for three narrow contract changes (below); everything else is
|
package/README.md
CHANGED
|
@@ -43,7 +43,9 @@ change before it touches your database.
|
|
|
43
43
|
|
|
44
44
|
## Reasons to choose it
|
|
45
45
|
|
|
46
|
-
- **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).
|
|
47
49
|
Instant installs, nothing extra in your lockfile, no supply-chain surface.
|
|
48
50
|
- **Run a single migration** — `migronaut up <file>`, not just "all pending".
|
|
49
51
|
- **Roll back anything** — a batch (`--batch 3`), the last N (`--steps 2`), one file, or `redo`.
|
|
@@ -57,10 +59,21 @@ change before it touches your database.
|
|
|
57
59
|
(warn by default, `onOutOfOrder: 'error'` to refuse) instead of silently applying.
|
|
58
60
|
- **Lifecycle hooks** — `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, `onError`.
|
|
59
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)).
|
|
60
64
|
- **TypeScript, ESM & CommonJS** — all run with no `ts-node` plumbing.
|
|
61
65
|
- **Zero config files required** — drive everything from env vars if you prefer.
|
|
62
66
|
- **Pino-friendly logging** — the `logger` option is pino-compatible; pass a pino instance directly
|
|
63
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.
|
|
64
77
|
|
|
65
78
|
### How it compares to `migrate-mongo`
|
|
66
79
|
|
|
@@ -76,6 +89,7 @@ change before it touches your database.
|
|
|
76
89
|
| Lifecycle hooks | ❌ | ✅ |
|
|
77
90
|
| First-class TypeScript (built-in) | ❌ | ✅ |
|
|
78
91
|
| History preserved on rollback (never deleted) | ❌ | ✅ |
|
|
92
|
+
| Declared indexes & validators (`converge`) | ❌ | ✅ |
|
|
79
93
|
| Adopt an existing `migrate-mongo` changelog | — | ✅ `migronaut import` |
|
|
80
94
|
|
|
81
95
|
<sub>Reflects `migrate-mongo`'s documented CLI as of mid-2026 (v14: optional
|
|
@@ -101,6 +115,8 @@ via a `client` argument; `migronaut` exposes the same plus a declarative per-fil
|
|
|
101
115
|
| Changelog written inside the migration's transaction | ❌ | ✅ |
|
|
102
116
|
| Credentials masked in errors, logs and `--json` | ❌ | ✅ |
|
|
103
117
|
| Pino-compatible logger | ❌ | ✅ |
|
|
118
|
+
| BullMQ queue adapter (`/bullmq` entry point) | ❌ | ✅ |
|
|
119
|
+
| Declared indexes & validators (`converge`) | ❌ | ✅ |
|
|
104
120
|
| Node floor | ≥ 18 | ≥ 22.18 |
|
|
105
121
|
|
|
106
122
|
<sub>Compared against `mongo-migrate-kit` 1.2.2 — the version this project forked from. The Node
|
|
@@ -161,7 +177,7 @@ Full docs, guides, and the API reference live at
|
|
|
161
177
|
- [Core Concepts](https://migronaut.vercel.app/guide/concepts) — migrations, batches, the changelog, locking
|
|
162
178
|
- [Getting Started](https://migronaut.vercel.app/guide/getting-started) & [Tutorial](https://migronaut.vercel.app/guide/tutorial)
|
|
163
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)
|
|
164
|
-
- [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)
|
|
165
181
|
- Reference: [CLI Cheatsheet](https://migronaut.vercel.app/reference/cli) · [Error Codes](https://migronaut.vercel.app/reference/error-codes)
|
|
166
182
|
|
|
167
183
|
---
|
|
@@ -180,6 +196,7 @@ Every command accepts the global flags `--uri`, `--db`, `--dir`, `--config`, `--
|
|
|
180
196
|
| `migronaut up [file]` | Run all pending migrations, one named file, or up to `--to <file>` |
|
|
181
197
|
| `migronaut down [file]` | Roll back the last batch, a chosen batch, the last N steps, one file, or to `--to <file>` |
|
|
182
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 |
|
|
183
200
|
| `migronaut status` | Print the full migration status table (`--check` to fail CI on pending) |
|
|
184
201
|
| `migronaut list` | List migrations, filtered by status |
|
|
185
202
|
| `migronaut dry-run <up\|down> [file]` | Preview a run without touching the database |
|
|
@@ -187,8 +204,8 @@ Every command accepts the global flags `--uri`, `--db`, `--dir`, `--config`, `--
|
|
|
187
204
|
| `migronaut lock` | Show who currently holds the migration lock |
|
|
188
205
|
| `migronaut unlock` | Force-release a stuck lock left behind by a crashed run |
|
|
189
206
|
|
|
190
|
-
Most data commands (`up`, `down`, `redo`, `status`, `list`, `dry-run`, `import`,
|
|
191
|
-
`create`, `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
|
|
192
209
|
[CI & automation](#ci--automation).
|
|
193
210
|
|
|
194
211
|
<details>
|
|
@@ -236,6 +253,8 @@ migronaut up <file> --force # re-run an ALREADY-applied file (asks for co
|
|
|
236
253
|
migronaut up <file> --force --yes # confirm a re-run non-interactively (required with --json)
|
|
237
254
|
migronaut up --strict # abort on any checksum mismatch
|
|
238
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
|
|
239
258
|
migronaut up --json # machine-readable output (array of run results)
|
|
240
259
|
|
|
241
260
|
# down — roll back
|
|
@@ -253,6 +272,15 @@ migronaut redo <file> # a specific file
|
|
|
253
272
|
migronaut redo --no-lock # skip the lock (dev only)
|
|
254
273
|
migronaut redo --json # machine-readable output (array of run results)
|
|
255
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
|
+
|
|
256
284
|
# status — full status table
|
|
257
285
|
migronaut status # the full status table
|
|
258
286
|
migronaut status --check # exit 2 if any migration is pending (CI gate)
|
|
@@ -601,6 +629,95 @@ All errors extend `MigronautError` and carry a typed `code` (`LOCK_ALREADY_HELD`
|
|
|
601
629
|
|
|
602
630
|
</details>
|
|
603
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
|
+
|
|
719
|
+
</details>
|
|
720
|
+
|
|
604
721
|
---
|
|
605
722
|
|
|
606
723
|
## Configuration
|
|
@@ -633,19 +750,100 @@ export default {
|
|
|
633
750
|
// ── Bookkeeping collections ─────────────────────────────────────────────
|
|
634
751
|
migrationsCollection: '_migronaut_migrations', // the append-only audit trail
|
|
635
752
|
lockCollection: '_migronaut_locks', // the concurrency lock
|
|
753
|
+
convergeLogCollection: '_migronaut_converge', // what each converge changed
|
|
636
754
|
lockTTLSeconds: 60, // a lock older than this is reclaimable
|
|
637
755
|
|
|
638
756
|
// ── Safety ──────────────────────────────────────────────────────────────
|
|
639
757
|
strict: false, // true → abort on a checksum mismatch (instead of warn + skip)
|
|
640
758
|
useTransaction: false, // true → wrap every migration in a transaction (override per file)
|
|
641
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
|
+
|
|
642
765
|
// ── Code-only options (omit in migronaut.config.json) ─────────────────────────
|
|
643
766
|
// hooks: { beforeAll, afterAll, beforeEach, afterEach, onError },
|
|
644
767
|
// mongoose: myMongooseInstance, // pass if your migrations use Mongoose models
|
|
645
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
|
|
646
771
|
};
|
|
647
772
|
```
|
|
648
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
|
+
|
|
649
847
|
<details>
|
|
650
848
|
<summary><b>Structured logging with pino</b> — the logger option is pino-compatible</summary>
|
|
651
849
|
|
|
@@ -683,6 +881,7 @@ optional rather than merely discouraged:
|
|
|
683
881
|
| `MIGRONAUT_MIGRATIONS_DIR` | `migrationsDir` | `./migrations` |
|
|
684
882
|
| `MIGRONAUT_COLLECTION` | `migrationsCollection` | `_migronaut_migrations` |
|
|
685
883
|
| `MIGRONAUT_LOCK_COLLECTION` | `lockCollection` | `_migronaut_locks` |
|
|
884
|
+
| `MIGRONAUT_CONVERGE_LOG_COLLECTION` | `convergeLogCollection` | `_migronaut_converge` |
|
|
686
885
|
| `MIGRONAUT_LOCK_TTL` | `lockTTLSeconds` | `60` |
|
|
687
886
|
| `MIGRONAUT_STRICT` | `strict` | `false` |
|
|
688
887
|
| `MIGRONAUT_USE_TRANSACTION` | `useTransaction` | `false` |
|
|
@@ -695,10 +894,13 @@ optional rather than merely discouraged:
|
|
|
695
894
|
| `MIGRONAUT_ON_OUT_OF_ORDER` | `onOutOfOrder` | `warn` |
|
|
696
895
|
| `MIGRONAUT_ENSURE_INDEXES` | `ensureIndexes` | `true` |
|
|
697
896
|
| `MIGRONAUT_RELOAD_MIGRATIONS` | `reloadMigrations` | `false` |
|
|
897
|
+
| `MIGRONAUT_COLLECTIONS_DIR` | `collectionsDir` | — *(none read)* |
|
|
898
|
+
| `MIGRONAUT_CONVERGE_AFTER_UP` | `convergeAfterUp` | `false` |
|
|
698
899
|
| `MIGRONAUT_ENV_FILE` | `envFile` | `.env` |
|
|
699
900
|
|
|
700
|
-
`fileExtensions`, `clientOptions`, `client`, `mongoose`, `hooks
|
|
701
|
-
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.
|
|
702
904
|
|
|
703
905
|
A value that doesn't parse is **rejected, never coerced**: `MIGRONAUT_STRICT=on` or
|
|
704
906
|
`MIGRONAUT_LOCK_TTL=abc` fails with an error naming the variable, rather than quietly turning a
|