@alexify/migronaut 2.1.0 → 2.2.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 CHANGED
@@ -3,6 +3,122 @@
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.2.0 — 2026-10-05
7
+
8
+ Atlas Search and Vector Search indexes in declared collections. Additive: a definition without
9
+ `searchIndexes` behaves exactly as in 2.1 — converge never even asks the server about Search for
10
+ it.
11
+
12
+ ### Added
13
+
14
+ - **Declared search indexes** — a collection definition takes `searchIndexes: [{ name?, type?,
15
+ definition }]`: Atlas Search (`type: 'search'`, the default) and Atlas Vector Search
16
+ (`'vectorSearch'`) indexes, with the definition exactly as Atlas documents it — automated
17
+ embedding (`autoEmbed`) fields included. `converge` keeps them in step with the same lock,
18
+ history, events, re-plan and fixed-point check as regular indexes. Experimental.
19
+ - **Rows**: a new target, `searchIndex`. A missing index is created (one `createSearchIndexes`
20
+ per collection), a changed definition is updated **in place** (`modify` — the old definition
21
+ serves until the new one is built), an undeclared one is kept, or dropped last under
22
+ `prune`. A definition that declares only `searchIndexes` is valid, and its collection is
23
+ created when missing.
24
+ - **Never a rebuild**: a `$search` against a missing index returns nothing rather than fail, so
25
+ converge never drops a search index to build it again. A change no update can make — the
26
+ type, or an `autoEmbed` field's path, model, `numDimensions`, quantization or modality — is a
27
+ `conflict` naming the new-name recipe, and so is a declared index the server is still
28
+ deleting.
29
+ - **Comparison**: definitions compare whole, key order ignored, with the defaults the server
30
+ writes into what it reports filled in on both sides — top-level (`analyzer`,
31
+ `searchAnalyzer`, `dynamic`, `storedSource`, `numPartitions`), per field mapping (`string`,
32
+ `number`, `autocomplete`, `token`, `geo`, `document`, nested fields and `multi` included) and
33
+ per vector field (`quantization`, `indexingMethod`, `hnswOptions`; `autoEmbed`
34
+ `numDimensions` and `quantization`). Vector `fields`, and a field indexed as several types,
35
+ compare as sets. A server that reports no `type` (a self-managed `mongot`) has it inferred
36
+ from the definition, and its `latestVersion` is read as the definition version.
37
+ - **Server-only options**: an option the server reports that the declaration does not set,
38
+ and whose default migronaut does not know (a newer `mongot`'s), is left out of the
39
+ comparison — named on the row (`ignored`) and in one warning — instead of making every
40
+ converge update, and the server rebuild, the index. Only option objects are trimmed: a
41
+ field, a mapping type or a vector field only the server has is still a difference. The
42
+ cost: removing such an option from a declaration goes unnoticed; declare the value wanted.
43
+ - **What differs**: a `modify` row names it down to the option
44
+ (`mappings.fields.title.norms`) — the first five paths and how many more. A search index
45
+ that still differs after an update is `unstable`, with a warning that every update builds it
46
+ again.
47
+ - **Raw commands** (`createSearchIndexes`, `updateSearchIndex`, `dropSearchIndex`,
48
+ `$listSearchIndexes`), so every driver in the peer range works — the driver's helpers start
49
+ at 5.6. A vector index update is retried once with its type when a self-managed `mongot`
50
+ asks for it; where the server refuses that too (an Atlas CLI local deployment on 8.0 and
51
+ 8.3), the step fails with the new-name recipe as its hint.
52
+ - **Build state**: rows carry `build` (`{ status, queryable, message?, updating? }`), and the
53
+ result `search` (`{ available, evidence, notReady, wait? }`) — how Search availability was
54
+ told, the declared indexes still building, updating, stale or failed, and how a wait ended.
55
+ A build under way does not count against `inSync`; a FAILED one fails `converge --check`
56
+ (exit 28). A STALE index (queryable, no longer replicating) is reported as such — table,
57
+ closing warning, `audit` — not as still building. A build message is kept to 500 characters.
58
+ - **`waitForSearchIndexes`** (config, `MIGRONAUT_WAIT_FOR_SEARCH_INDEXES`, `converge({
59
+ waitForSearchIndexes })`, `converge --wait-search` / `--no-wait-search`) — hold the converge
60
+ until every declared search index serves its declaration; a FAILED build of an index the run
61
+ created or changed, or `searchIndexWaitTimeoutMs` (`MIGRONAUT_SEARCH_INDEX_WAIT_TIMEOUT_MS`,
62
+ default 10 minutes), fails it with `ConvergeFailedError` `phase: 'wait'`. Off by default.
63
+ - **Without the lock**: the wait only reads, so the migration lock is released when it starts
64
+ (`lock:released` with `early: true`) — the next deploy or a queue's next job need not wait
65
+ out a build. The run itself (its id, span, history entry, `converge:end`) ends with the
66
+ wait.
67
+ - **Only what the run started holds it**: an index that failed or went STALE before the run,
68
+ its definition unchanged, is named and warned about instead — converge cannot fix it, and a
69
+ deploy is not held up by it. `--check` still fails on a FAILED build.
70
+ - **Resilient**: a list that fails with a network or failover blip is read again at the next
71
+ poll, up to three in a row (then `reason: 'unreadable'`); a stop cuts the pause between polls
72
+ short.
73
+ - **Observable**: `converge:wait` events (`started`, `progress` every 30 s, then `ready`,
74
+ `failed`, `timeout`, `unreadable` or `aborted`), `result.search.wait`, the history entry, a
75
+ queue job's log lines and `search-wait` progress phase, and — with `telemetry` — the
76
+ histogram `migronaut.converge.search.wait.duration` by `migronaut.converge.search.wait.outcome`.
77
+ - **`onSearchUnavailable`** (`MIGRONAUT_ON_SEARCH_UNAVAILABLE`) — declared search indexes on a
78
+ server without Atlas Search: `'fail'` (default) refuses the run before any write, with a hint;
79
+ `'skip'` converges everything else and reports `skip` rows. Detected once per run from the
80
+ server's own answer (checked against MongoDB 5.0, 6.0, 7.0, 8.0 and 8.2).
81
+ - **`migronaut audit`** — a `search` check, when the definitions declare search indexes: whether
82
+ the server has Atlas Search, and whether a declared index failed to build.
83
+ - **Types** — `SearchIndexDefinition`, `SearchDefinition`, `VectorSearchDefinition`,
84
+ `VectorSearchField`, `SearchIndexType`, `SearchIndexStatus`, `SearchIndexBuild`,
85
+ `SearchIndexNotReady`, `ConvergeSearchSummary`, `ConvergeWaitOutcome`, `ConvergeWaitEvent`;
86
+ `ConvergeTarget` gains `'searchIndex'`, `ConvergeActionKind` `'skip'`, `ConvergeAction`
87
+ `ignored`, `ConvergeHistoryEntry` `search`, `LockEvent` `early`, the event map
88
+ `'converge:wait'`; in `bullmq.d.ts` `ConvergeJobResult.search`, and `MigrationJobProgress`
89
+ gains the `'search-wait'` phase and `searchIndexes`. An exhaustive `switch` over one of these
90
+ unions needs a branch for the new member.
91
+ - **Queue** — a converge job's result carries `search`, and its log lines name search indexes.
92
+ Whether a job waits for builds is the worker kit's `waitForSearchIndexes`; the job payload is
93
+ unchanged.
94
+
95
+ ### Changed
96
+
97
+ - A definition with none of `indexes`, `searchIndexes` and `validator` is refused with "declares
98
+ no indexes, searchIndexes or validator — nothing to manage" (was "declares neither indexes nor
99
+ a validator").
100
+ - The converge table's drop/rebuild count includes dropped search indexes, and `converge --yes`
101
+ is required for them in `--json` mode, like an index drop.
102
+ - `ConvergeFailedError`'s documentation names every phase: `plan`, `replan` (already thrown by
103
+ 2.1, undocumented), `apply` and the new `wait`. A search index list that cannot be read is
104
+ reported in the phase that read it — `plan` only before the first write — and a run that stops
105
+ for any reason settles every row and carries the result so far in `context.converge`.
106
+ - `runWithLock` (internal) hands the work a `control` whose `release()` gives the lock up early;
107
+ `lock:released` may now come before `run:end`, with `early: true`.
108
+
109
+ ### Tooling
110
+
111
+ - `tests/integration/search-atlas.test.js` — an opt-in, manual suite against
112
+ `mongodb/mongodb-atlas-local` (`MIGRONAUT_TEST_ATLAS_URI`): every scenario ends at a fixed
113
+ point. Passes against `mongodb/mongodb-atlas-local` 8.0 (8.0.32) and `latest` (8.3.11). CI does
114
+ not run it; the coverage gate comes from the unit tier's fake, which answers the search
115
+ commands with lag, normalization and build progress on demand. The suite stays strict about
116
+ server-only options (production tolerates them): one there is a default the tables in
117
+ `search-index-spec.js` lack. 9/9 on 8.0.32 and 8.3.11, the lock released during the wait
118
+ included.
119
+ - `src/core/converge.js` is split: `converge-search-run.js` (the search half of a run) and
120
+ `server-info.js` (read options, read pace, server version, not-found codes).
121
+
6
122
  ## v2.1.0 — 2026-10-04
7
123
 
8
124
  Migrations as a queue, ids in your own format, OpenTelemetry, and declared collections. Additive:
package/README.md CHANGED
@@ -59,8 +59,9 @@ change before it touches your database.
59
59
  (warn by default, `onOutOfOrder: 'error'` to refuse) instead of silently applying.
60
60
  - **Lifecycle hooks** — `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, `onError`.
61
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)).
62
+ - **Indexes, search indexes and validators as an end state** — declare them (Atlas Search and
63
+ Vector Search indexes included), and `migronaut converge` brings the database to match; no
64
+ migration file per index change ([details](#declared-collections)).
64
65
  - **TypeScript, ESM & CommonJS** — all run with no `ts-node` plumbing.
65
66
  - **Zero config files required** — drive everything from env vars if you prefer.
66
67
  - **Pino-friendly logging** — the `logger` option is pino-compatible; pass a pino instance directly
@@ -90,6 +91,7 @@ change before it touches your database.
90
91
  | First-class TypeScript (built-in) | ❌ | ✅ |
91
92
  | History preserved on rollback (never deleted) | ❌ | ✅ |
92
93
  | Declared indexes & validators (`converge`) | ❌ | ✅ |
94
+ | Declared Atlas Search / Vector Search indexes | ❌ | ✅ |
93
95
  | Adopt an existing `migrate-mongo` changelog | — | ✅ `migronaut import` |
94
96
 
95
97
  <sub>Reflects `migrate-mongo`'s documented CLI as of mid-2026 (v14: optional
@@ -117,6 +119,7 @@ via a `client` argument; `migronaut` exposes the same plus a declarative per-fil
117
119
  | Pino-compatible logger | ❌ | ✅ |
118
120
  | BullMQ queue adapter (`/bullmq` entry point) | ❌ | ✅ |
119
121
  | Declared indexes & validators (`converge`) | ❌ | ✅ |
122
+ | Declared Atlas Search / Vector Search indexes | ❌ | ✅ |
120
123
  | Node floor | ≥ 18 | ≥ 22.18 |
121
124
 
122
125
  <sub>Compared against `mongo-migrate-kit` 1.2.2 — the version this project forked from. The Node
@@ -196,7 +199,7 @@ Every command accepts the global flags `--uri`, `--db`, `--dir`, `--config`, `--
196
199
  | `migronaut up [file]` | Run all pending migrations, one named file, or up to `--to <file>` |
197
200
  | `migronaut down [file]` | Roll back the last batch, a chosen batch, the last N steps, one file, or to `--to <file>` |
198
201
  | `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 |
202
+ | `migronaut converge` | Bring declared collections — indexes, search indexes and validators — to their declared state |
200
203
  | `migronaut status` | Print the full migration status table (`--check` to fail CI on pending) |
201
204
  | `migronaut list` | List migrations, filtered by status |
202
205
  | `migronaut dry-run <up\|down> [file]` | Preview a run without touching the database |
@@ -272,11 +275,12 @@ migronaut redo <file> # a specific file
272
275
  migronaut redo --no-lock # skip the lock (dev only)
273
276
  migronaut redo --json # machine-readable output (array of run results)
274
277
 
275
- # converge — declared indexes and validators → the database
278
+ # converge — declared indexes, search indexes and validators → the database
276
279
  migronaut converge # plan, ask before any drop/rebuild, then apply
277
280
  migronaut converge --dry-run # show the plan, change nothing
278
281
  migronaut converge --check # exit 28 if anything would change (CI gate)
279
282
  migronaut converge --prune # also drop indexes a definition does not declare
283
+ migronaut converge --wait-search # wait until every declared search index is queryable
280
284
  migronaut converge --yes # no confirmation (required for drops/rebuilds with --json)
281
285
  migronaut converge --no-lock # skip the concurrency lock (local dev only)
282
286
  migronaut converge --json # machine-readable output (the converge result)
@@ -630,12 +634,13 @@ All errors extend `MigronautError` and carry a typed `code` (`LOCK_ALREADY_HELD`
630
634
  </details>
631
635
 
632
636
  <details id="declared-collections">
633
- <summary><b>Declared collections</b> — indexes and validators as an end state, with no migration file per change</summary>
637
+ <summary><b>Declared collections</b> — indexes, search indexes and validators as an end state, with no migration file per change</summary>
634
638
 
635
639
  <br>
636
640
 
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:
641
+ When what matters is *the final shape* of a collection's indexes, Atlas Search indexes and
642
+ validator — not the history of how it got there — declare it and let `migronaut converge` make the
643
+ difference:
639
644
 
640
645
  ```js
641
646
  // migronaut.config.js
@@ -651,6 +656,19 @@ export default {
651
656
  ],
652
657
  validator: { $jsonSchema: { bsonType: 'object', required: ['email'] } },
653
658
  },
659
+ {
660
+ name: 'movies',
661
+ searchIndexes: [
662
+ { definition: { mappings: { dynamic: true } } }, // Atlas Search, named "default"
663
+ {
664
+ name: 'plot_vectors',
665
+ type: 'vectorSearch',
666
+ definition: {
667
+ fields: [{ type: 'vector', path: 'embedding', numDimensions: 1536, similarity: 'cosine' }],
668
+ },
669
+ },
670
+ ],
671
+ },
654
672
  ],
655
673
  collectionsDir: './collections', // …and/or one file per collection
656
674
  };
@@ -668,11 +686,15 @@ migronaut converge --check # exit 28 on drift: a CI gate
668
686
  a changed index is rebuilt (and put back if the rebuild fails). An index you did not declare is
669
687
  **kept and reported** — dropped only with `prune`. An identical index under another name is
670
688
  accepted as is, never silently rebuilt.
689
+ - **Search indexes, too** — on Atlas, an Atlas CLI local deployment or MongoDB 8.3+ with `mongot`.
690
+ A changed definition is updated in place (on Atlas the old one serves until the new one is
691
+ built), never dropped and rebuilt; `--wait-search` waits until the builds are queryable. On a server without
692
+ Search, converge refuses — or skips them with `onSearchUnavailable: 'skip'`.
671
693
  - **Locked like a migration**, and refused before the first write when the plan has a conflict.
672
694
  - **After every deploy** with `convergeAfterUp: true` — a bulk `up` then ends by converging,
673
695
  under the same lock — or as a [queue job](https://migronaut.vercel.app/guide/bullmq#converge-jobs).
674
696
 
675
- Experimental in 2.1. → **[Declared Collections](https://migronaut.vercel.app/guide/collections)**
697
+ Experimental since 2.1 (search indexes: 2.2). → **[Declared Collections](https://migronaut.vercel.app/guide/collections)**
676
698
 
677
699
  </details>
678
700
 
@@ -761,6 +783,8 @@ export default {
761
783
  // collections: [{ name: 'users', indexes: [{ key: { email: 1 }, unique: true }] }],
762
784
  // collectionsDir: './collections', // one definition file per collection
763
785
  // convergeAfterUp: false, // true → every bulk `up` ends by converging
786
+ // onSearchUnavailable: 'fail', // 'skip' → converge without search indexes where Search is absent
787
+ // waitForSearchIndexes: false, // true → converge waits until search indexes are queryable
764
788
 
765
789
  // ── Code-only options (omit in migronaut.config.json) ─────────────────────────
766
790
  // hooks: { beforeAll, afterAll, beforeEach, afterEach, onError },
@@ -896,6 +920,9 @@ optional rather than merely discouraged:
896
920
  | `MIGRONAUT_RELOAD_MIGRATIONS` | `reloadMigrations` | `false` |
897
921
  | `MIGRONAUT_COLLECTIONS_DIR` | `collectionsDir` | — *(none read)* |
898
922
  | `MIGRONAUT_CONVERGE_AFTER_UP` | `convergeAfterUp` | `false` |
923
+ | `MIGRONAUT_ON_SEARCH_UNAVAILABLE` | `onSearchUnavailable` | `fail` |
924
+ | `MIGRONAUT_WAIT_FOR_SEARCH_INDEXES` | `waitForSearchIndexes` | `false` |
925
+ | `MIGRONAUT_SEARCH_INDEX_WAIT_TIMEOUT_MS` | `searchIndexWaitTimeoutMs` | `600000` |
899
926
  | `MIGRONAUT_ENV_FILE` | `envFile` | `.env` |
900
927
 
901
928
  `fileExtensions`, `clientOptions`, `collections`, `client`, `mongoose`, `hooks`, `logger`,
package/bullmq.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AuditReport,
3
3
  CollectionConvergeResult,
4
+ ConvergeSearchSummary,
4
5
  ConvergeUnstable,
5
6
  LockInfo,
6
7
  MigratorKit,
@@ -237,6 +238,13 @@ export interface ConvergeJobResult {
237
238
  inSync: boolean;
238
239
  collections: CollectionConvergeResult[];
239
240
  unstable?: ConvergeUnstable[];
241
+ /**
242
+ * Atlas Search availability and the declared search indexes still building
243
+ * — when the worker's definitions declare search indexes. Whether the job
244
+ * waits for them is the worker kit's `waitForSearchIndexes`.
245
+ * @experimental New in 2.2
246
+ */
247
+ search?: ConvergeSearchSummary;
240
248
  runId?: string;
241
249
  /** Time (ms) spent waiting for the MongoDB migration lock */
242
250
  lockWaitMs: number;
@@ -247,7 +255,8 @@ export interface ConvergeJobResult {
247
255
  * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
248
256
  */
249
257
  export interface MigrationJobProgress {
250
- phase: 'lock-wait' | 'running' | 'completed' | 'failed';
258
+ /** `search-wait` (New in 2.2): a converge job waiting for search index builds */
259
+ phase: 'lock-wait' | 'running' | 'search-wait' | 'completed' | 'failed';
251
260
  migration?: string;
252
261
  direction?: 'up' | 'down';
253
262
  groupId?: string;
@@ -256,7 +265,13 @@ export interface MigrationJobProgress {
256
265
  kind?: 'sync' | 'converge';
257
266
  /** `lock-wait` only */
258
267
  attempts?: number;
268
+ /** `lock-wait` and `search-wait` */
259
269
  waitedMs?: number;
270
+ /**
271
+ * `search-wait` only — how many search indexes the wait is for
272
+ * @experimental New in 2.2
273
+ */
274
+ searchIndexes?: number;
260
275
  /**
261
276
  * `failed` only — the typed error code, so nobody has to parse
262
277
  * `failedReason`; `'UNKNOWN'` for an error that is not migronaut's.