@alexify/migronaut 2.2.0 → 2.3.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +33 -2
  3. package/bullmq.d.ts +449 -6
  4. package/index.d.ts +1010 -9
  5. package/migronaut.schema.json +93 -1
  6. package/package.json +8 -2
  7. package/src/bullmq/background-processor.js +469 -0
  8. package/src/bullmq/index.js +12 -0
  9. package/src/bullmq/jobs.js +254 -7
  10. package/src/bullmq/processor.js +128 -14
  11. package/src/bullmq/producer.js +185 -13
  12. package/src/bullmq/service.js +480 -45
  13. package/src/cli/commands/background.js +500 -0
  14. package/src/cli/commands/create.js +6 -0
  15. package/src/cli/exit-codes.js +6 -0
  16. package/src/cli/index.js +2 -0
  17. package/src/core/audit.js +11 -1
  18. package/src/core/background-audit.js +139 -0
  19. package/src/core/background-drift.js +126 -0
  20. package/src/core/background-dry-run.js +366 -0
  21. package/src/core/background-engine.js +818 -0
  22. package/src/core/background-kit.js +425 -0
  23. package/src/core/background-partition.js +298 -0
  24. package/src/core/background-runner.js +305 -0
  25. package/src/core/background-sandbox.js +701 -0
  26. package/src/core/background-shard.js +542 -0
  27. package/src/core/background-spec.js +597 -0
  28. package/src/core/background-store.js +951 -0
  29. package/src/core/background-throttle.js +269 -0
  30. package/src/core/background-watch-plan.js +164 -0
  31. package/src/core/background-watch-store.js +78 -0
  32. package/src/core/background-watch.js +605 -0
  33. package/src/core/background.js +1121 -0
  34. package/src/core/bson-peer.js +23 -0
  35. package/src/core/changelog.js +32 -0
  36. package/src/core/collections.js +78 -8
  37. package/src/core/config.js +102 -12
  38. package/src/core/converge-plan.js +86 -7
  39. package/src/core/converge.js +88 -0
  40. package/src/core/lock.js +48 -21
  41. package/src/core/migrator.js +904 -12
  42. package/src/core/options.js +16 -0
  43. package/src/core/run.js +26 -12
  44. package/src/core/runner.js +1 -1
  45. package/src/core/server-info.js +9 -2
  46. package/src/core/shard-info.js +76 -0
  47. package/src/core/versioning-spec.js +181 -0
  48. package/src/errors/index.js +88 -0
  49. package/src/index.js +16 -0
  50. package/src/utils/error.js +11 -2
  51. package/src/utils/loader.js +77 -9
  52. package/src/utils/migration-name.js +33 -1
  53. package/src/utils/telemetry.js +107 -0
  54. package/src/utils/template.js +62 -1
  55. package/src/versioning/config.js +155 -0
  56. package/src/versioning/document.js +326 -0
  57. package/src/versioning/index.js +50 -0
  58. package/src/versioning/internal.js +279 -0
  59. package/src/versioning/mongoose.js +151 -0
  60. package/src/versioning/occ.js +318 -0
  61. package/src/versioning/registry.js +187 -0
  62. package/src/versioning/upcaster.js +213 -0
  63. package/versioning.d.ts +666 -0
  64. package/versioning.js +1 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,113 @@
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.3.0 — 2026-10-06
7
+
8
+ Document versioning and background migrations. Additive: nothing changes for a project that
9
+ declares no `versioning` and writes no `background` file. Everything new is experimental — its
10
+ shape may still change in a minor release (named here).
11
+
12
+ ### Added
13
+
14
+ - **Document versioning** — a collection definition takes `versioning: { current, min?, field?,
15
+ revision?, revisionField?, index? }`: the shape version (`__v`) and the optimistic-concurrency
16
+ revision (`__rev`) as one contract. Converge folds it into the validator (the version an `int`
17
+ with a `minimum` of `min` and no `maximum`; the revision `int|long`; `moderate` when the
18
+ validator exists for versioning alone) and an index `{ __v: 1, _id: 1 }` — on a sharded
19
+ collection `{ __v: 1, …shard key, _id: 1 }`, the ordinary one kept with a row that says why.
20
+ Raising `min` over documents still below it is refused before any write (a `conflict` row, no
21
+ id named).
22
+ - **`@alexify/migronaut/versioning`** — a third entry point, with no engine behind it, for the
23
+ repository layer: `defineShapes` (`stamp`, `onInsert`, `stampUpsert`, `upcaster`, `plugin`),
24
+ `updateWithRevision` / `replaceWithRevision` / `findOneAndUpdateWithRevision`
25
+ (`RevisionConflictError` with `reason: 'conflict' | 'not-found' | 'unknown'`),
26
+ `retryOnConflict`, `bumpRevision`, the in-memory `upcaster` (`ShapeVersionError`),
27
+ `isVersion`, and `versioningPlugin` for Mongoose (its version key moved to `__rev`, optimistic
28
+ concurrency on). Per-version shape types without codegen: `AnyShape`, `CurrentShape`,
29
+ `ShapeAt`, `Stamped`, `Body`, `BackgroundMigrationFor`, a typed `defineShapes<Shapes>()(…)`
30
+ (`versioning.d.ts` needs TypeScript ≥ 5.0).
31
+ - **Background migrations** — a migration file with `export const background = { collection,
32
+ from, to, migrate | migrateBatch, revert?, … }` (or a free-form `step(ctx)`), which `up`
33
+ registers without running; the rewrite runs beside the migration line, never holding the
34
+ migration lock:
35
+ - **partitions and lanes**: the collection split into `_id` ranges from a sampled quantile per
36
+ BSON type — or, on a sharded collection, into runs of chunks per shard, with targeted reads
37
+ and writes, a shard-key guard (`shard-key-changed`) and `shardConcurrency`; leases are slots,
38
+ capped by a unique index at `maxParallel` across every process, every checkpoint fenced;
39
+ - **a coordinator** that plans passes, finalizes them and completes when nothing matches any
40
+ more (`maxPasses` against an old release that keeps writing); writes are `stampedDiff`s under
41
+ the optimistic filter, so an application write in between is never lost and untouched fields
42
+ keep their BSON types;
43
+ - **transactional batches** (`transaction: true`) with side writes through `ctx.session`;
44
+ - **pacing**: `pauseMs`, a `throttle` hook, replication lag and an adaptive (AIMD) controller;
45
+ - **`requires`** between background migrations (a DAG, unblocked as they complete) and from
46
+ ordinary migrations, which wait (`BackgroundPendingError`, or `onBackgroundPending: 'stop'`);
47
+ `backgroundInline` runs them inside `up`;
48
+ - **controls**: pause, resume, cancel, retry (`fromStart`), repin, unlock; **dry runs** on a
49
+ sample (`--validate` through the real write path in an always-aborted transaction) and of
50
+ `step` migrations in a sandbox (refusals exit 34);
51
+ - **drift after completion**: the drift watch (`verifyBackground`, every 10 minutes in each
52
+ runtime, `backgroundOnDrift: 'reopen' | 'report'`) and the **live drift watcher**
53
+ (`watchBackground`, change streams, one leader per collection, `backgroundDrift: 'poll' |
54
+ 'stream' | 'both'`);
55
+ - **three runtimes**: `migronaut background run`, `startBackgroundRunner()` in the application,
56
+ and the queue (below); `status()`, `background status --partitions`, an audit check, events
57
+ `background:*`, and OpenTelemetry spans `migronaut.background.slice` /
58
+ `migronaut.background.coordinate` with their metrics.
59
+ - **`migronaut background <action>`** — status, run, pause, resume, cancel, retry, repin,
60
+ dry-run, unlock, verify and watch; `create --background`.
61
+ - **Background migrations on the queue** — `createMigrationQueue({ background: true | {…} })`: a
62
+ queue of their own, a coordinator job per background migration with its lanes as children,
63
+ heals from MongoDB (worker start, every sync tick, every drift-watch tick) and a stall
64
+ takeover; `startBackgroundWorker()`, `enqueueBackground()`, the `background-verify` schedule,
65
+ `createBackgroundProcessor()`. An `up` plan stops before a migration that waits for a
66
+ background one.
67
+ - **Config**: `backgroundCollection`, `backgroundInline`, `backgroundOnDrift`, `backgroundDrift`,
68
+ `backgroundShardAware`, each with its `MIGRONAUT_*` variable.
69
+ - **Errors and exit codes**: `REVISION_CONFLICT` (29), `SHAPE_VERSION_UNSUPPORTED` (30),
70
+ `BACKGROUND_PENDING` (31), `BACKGROUND_FAILED` (32), `BACKGROUND_CONFLICT` (33),
71
+ `SANDBOX_REFUSED` (34).
72
+ - **`shapes.occ(name)`** — the revision guards and `bumpRevision` bound to a collection's field
73
+ names; `RevisionConflictContext` types an error's `context`.
74
+ - **`startBackgroundRunner().stop({ timeoutMs })`**, `RunnableBackground` (what
75
+ `runnableBackground()` now returns: live leases, last progress, coordinator), `previous` on a
76
+ background status (the registration a re-registration replaced), `plan.atLeast`.
77
+
78
+ ### Changed
79
+
80
+ For code written against 2.2 — the queue adapter's types and results, and two type-level details:
81
+
82
+ - **`SyncJobResult.held` means a failure again, only.** A tick whose next migration waits for a
83
+ background migration reports `waiting: { migration, waitsFor }` instead; while it still waits
84
+ for the same thing, later ticks say so without planning. A group that stops at such a migration
85
+ has `upToDate: false` (`MigrationGroup.waiting` says why).
86
+ - **`JOB_NAMES` has three more values** (`background`, `background-lane`, `background-verify`) and
87
+ `MigrationJobView.returnvalue` three more result types: an exhaustive `Record` over either needs
88
+ them.
89
+ - **`MigronautErrorCode` has six more members** (above): an exhaustive `switch` with a `never`
90
+ default needs a case for each — as with the codes 2.1 added.
91
+ - **`CollectionDefinition.indexes` and `searchIndexes` are `readonly` arrays**, so a definition
92
+ declared `as const` type-checks; code that pushes into a typed definition's array must copy it.
93
+ - **Background metric names** (experimental): `migronaut.background.throttled`,
94
+ `migronaut.background.drift.detected`, `migronaut.background.transaction.retried`.
95
+
96
+ ### Notes
97
+
98
+ - A typed `current` past the highest declared shape is a compile error. Written inline it reads
99
+ "Type 'number' is not assignable to type 'never'"; from an imported `as const` definition, it
100
+ names the two versions ("Type '3' is not assignable to type '2'").
101
+ - The shard-aware mode needs `clusterMonitor` (it reads `config.collections` and
102
+ `config.chunks`); without it, a sharded collection is partitioned by `_id`.
103
+ - Fail-closed limits: `maxDocumentErrors` is at most 1000 (the ids a state keeps); a revision
104
+ guard refuses an `_id` given as an operator; a background `filter` may not run server-side
105
+ JavaScript; `create --background`'s placeholder collection (`'TODO'`) is refused by `up`;
106
+ `watchBackground`, `enqueueBackground` and `createBackgroundProcessor` refuse options they do
107
+ not know.
108
+ - A lane stopped by its process (a shutdown, a deploy) is not a failed slice, and
109
+ `maxSliceFailures` counts failed slices in a row. `down` of a one-way background migration pauses
110
+ it and waits for its lanes before it decides; a `step` migration counts as having rewritten
111
+ documents once one step was checkpointed.
112
+
6
113
  ## v2.2.0 — 2026-10-05
7
114
 
8
115
  Atlas Search and Vector Search indexes in declared collections. Additive: a definition without
package/README.md CHANGED
@@ -75,6 +75,15 @@ change before it touches your database.
75
75
  - **Migrations as a queue (optional)** — `@alexify/migronaut/bullmq` runs each migration as its own
76
76
  BullMQ job, in order, so migronaut can be a migration service: trigger it over HTTP, on a
77
77
  schedule, or from a deploy hook that waits for the result.
78
+ - **Document versioning (experimental)** — declare a collection's shape version (`__v`) and
79
+ optimistic-concurrency revision (`__rev`) once; converge enforces them, and
80
+ `@alexify/migronaut/versioning` gives your repository layer `updateWithRevision`, typed
81
+ per-version shapes, an upcaster and a Mongoose plugin ([guide](https://migronaut.vercel.app/guide/versioning)).
82
+ - **Background migrations (experimental)** — long data rewrites (`export const background = {…}`)
83
+ that `up` only registers and that run beside the migration line: partitions and parallel lanes
84
+ across pods, checkpoints, pause/resume, transactional batches, shard-aware on sharded clusters,
85
+ dry runs, and a drift watcher that upgrades old-shape writes after completion — from the CLI,
86
+ inside your app, or on the queue ([guide](https://migronaut.vercel.app/guide/background-migrations)).
78
87
 
79
88
  ### How it compares to `migrate-mongo`
80
89
 
@@ -200,6 +209,7 @@ Every command accepts the global flags `--uri`, `--db`, `--dir`, `--config`, `--
200
209
  | `migronaut down [file]` | Roll back the last batch, a chosen batch, the last N steps, one file, or to `--to <file>` |
201
210
  | `migronaut redo [file]` | Roll back then re-apply (the last migration, or one file) |
202
211
  | `migronaut converge` | Bring declared collections — indexes, search indexes and validators — to their declared state |
212
+ | `migronaut background <action> [name]` | Background migrations: `status`, `run`, `pause`, `resume`, `cancel`, `retry`, `repin`, `dry-run`, `unlock`, `verify`, `watch` |
203
213
  | `migronaut status` | Print the full migration status table (`--check` to fail CI on pending) |
204
214
  | `migronaut list` | List migrations, filtered by status |
205
215
  | `migronaut dry-run <up\|down> [file]` | Preview a run without touching the database |
@@ -207,8 +217,8 @@ Every command accepts the global flags `--uri`, `--db`, `--dir`, `--config`, `--
207
217
  | `migronaut lock` | Show who currently holds the migration lock |
208
218
  | `migronaut unlock` | Force-release a stuck lock left behind by a crashed run |
209
219
 
210
- Most data commands (`up`, `down`, `redo`, `converge`, `status`, `list`, `dry-run`, `import`,
211
- `baseline`, `create`, `audit`, `lock`, `unlock`) accept **`--json`** for machine-readable output — see
220
+ Most data commands (`up`, `down`, `redo`, `converge`, `background`, `status`, `list`, `dry-run`,
221
+ `import`, `baseline`, `create`, `audit`, `lock`, `unlock`) accept **`--json`** for machine-readable output — see
212
222
  [CI & automation](#ci--automation).
213
223
 
214
224
  <details>
@@ -285,6 +295,15 @@ migronaut converge --yes # no confirmation (required for drops/rebuild
285
295
  migronaut converge --no-lock # skip the concurrency lock (local dev only)
286
296
  migronaut converge --json # machine-readable output (the converge result)
287
297
 
298
+ # background — background migrations (registered by up, rewritten in partitions)
299
+ migronaut background status # every background migration
300
+ migronaut background status --check # exit 32 if one failed, 31 if one is not completed
301
+ migronaut background run <name> # drive it from here (--concurrency N, --once, --all)
302
+ migronaut background pause <name> --wait # stop its lanes at the next batch
303
+ migronaut background dry-run <name> --validate # on a sample, in an always-aborted transaction
304
+ migronaut background verify # look for old-shape documents after completion
305
+ migronaut background watch # upgrade old-shape writes as they land (Ctrl-C stops)
306
+
288
307
  # status — full status table
289
308
  migronaut status # the full status table
290
309
  migronaut status --check # exit 2 if any migration is pending (CI gate)
@@ -786,6 +805,13 @@ export default {
786
805
  // onSearchUnavailable: 'fail', // 'skip' → converge without search indexes where Search is absent
787
806
  // waitForSearchIndexes: false, // true → converge waits until search indexes are queryable
788
807
 
808
+ // ── Background migrations (experimental) — see `migronaut background` ───
809
+ // backgroundCollection: '_migronaut_background', // state (+ _partitions, _watch)
810
+ // backgroundInline: false, // true → run it to the end inside the registering `up`
811
+ // backgroundOnDrift: 'reopen', // 'report' → only report old-shape documents after completion
812
+ // backgroundDrift: 'poll', // 'stream' | 'both' → watch drift with change streams
813
+ // backgroundShardAware: 'auto', // 'off' → no shard-key partitions on sharded collections
814
+
789
815
  // ── Code-only options (omit in migronaut.config.json) ─────────────────────────
790
816
  // hooks: { beforeAll, afterAll, beforeEach, afterEach, onError },
791
817
  // mongoose: myMongooseInstance, // pass if your migrations use Mongoose models
@@ -923,6 +949,11 @@ optional rather than merely discouraged:
923
949
  | `MIGRONAUT_ON_SEARCH_UNAVAILABLE` | `onSearchUnavailable` | `fail` |
924
950
  | `MIGRONAUT_WAIT_FOR_SEARCH_INDEXES` | `waitForSearchIndexes` | `false` |
925
951
  | `MIGRONAUT_SEARCH_INDEX_WAIT_TIMEOUT_MS` | `searchIndexWaitTimeoutMs` | `600000` |
952
+ | `MIGRONAUT_BACKGROUND_COLLECTION` | `backgroundCollection` | `_migronaut_background` |
953
+ | `MIGRONAUT_BACKGROUND_INLINE` | `backgroundInline` | `false` |
954
+ | `MIGRONAUT_BACKGROUND_ON_DRIFT` | `backgroundOnDrift` | `reopen` |
955
+ | `MIGRONAUT_BACKGROUND_DRIFT` | `backgroundDrift` | `poll` |
956
+ | `MIGRONAUT_BACKGROUND_SHARD_AWARE` | `backgroundShardAware` | `auto` |
926
957
  | `MIGRONAUT_ENV_FILE` | `envFile` | `.env` |
927
958
 
928
959
  `fileExtensions`, `clientOptions`, `collections`, `client`, `mongoose`, `hooks`, `logger`,