@alexify/migronaut 2.0.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 +436 -0
- package/README.md +235 -6
- package/bullmq.d.ts +860 -0
- package/bullmq.js +1 -0
- package/index.d.ts +888 -19
- package/migronaut.schema.json +238 -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 +632 -0
- package/src/bullmq/producer.js +427 -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 +188 -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 +164 -0
- package/src/core/audit.js +88 -3
- package/src/core/changelog.js +71 -6
- package/src/core/collections.js +396 -0
- package/src/core/config.js +130 -25
- package/src/core/converge-log.js +47 -0
- package/src/core/converge-plan.js +686 -0
- package/src/core/converge-search-run.js +440 -0
- package/src/core/converge-search.js +404 -0
- package/src/core/converge.js +1024 -0
- package/src/core/index-spec.js +507 -0
- package/src/core/lock-wait.js +260 -0
- package/src/core/lock.js +95 -28
- package/src/core/migrator.js +600 -287
- package/src/core/options.js +266 -0
- package/src/core/run-recorder.js +157 -0
- package/src/core/run.js +58 -90
- package/src/core/search-index-spec.js +758 -0
- package/src/core/sequence.js +134 -0
- package/src/core/server-info.js +63 -0
- package/src/errors/index.js +60 -0
- package/src/index.js +8 -0
- package/src/utils/actor.js +48 -0
- package/src/utils/canonical.js +212 -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 +410 -0
- package/src/utils/template.js +43 -2
package/index.d.ts
CHANGED
|
@@ -143,6 +143,16 @@ export interface MigrationHooks {
|
|
|
143
143
|
/** File type a created migration is written as */
|
|
144
144
|
export type MigrationExtension = 'ts' | 'js';
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Mints one identifier. Called with **no arguments** and no `this`, so a
|
|
148
|
+
* third-party generator passes straight through (`generateId: ulid`,
|
|
149
|
+
* `generateId: createId`, `generateId: nanoid`). It must be **synchronous** and
|
|
150
|
+
* return a non-empty string of at most 128 characters, different on every
|
|
151
|
+
* call; anything else — a throw and a returned promise included — fails the
|
|
152
|
+
* run with a {@link ConfigInvalidError}.
|
|
153
|
+
*/
|
|
154
|
+
export type IdGenerator = () => string;
|
|
155
|
+
|
|
146
156
|
/**
|
|
147
157
|
* Every **scalar** option below is also settable from the environment as
|
|
148
158
|
* `MIGRONAUT_<SCREAMING_SNAKE>` (`migrationsDir` → `MIGRONAUT_MIGRATIONS_DIR`,
|
|
@@ -152,9 +162,9 @@ export type MigrationExtension = 'ts' | 'js';
|
|
|
152
162
|
* file and are outranked by CLI flags. A value that does not parse is rejected
|
|
153
163
|
* with a {@link ConfigInvalidError} naming the variable — never coerced.
|
|
154
164
|
*
|
|
155
|
-
* `fileExtensions`, `clientOptions` and the live
|
|
156
|
-
* `hooks`, `logger`) are
|
|
157
|
-
* cannot express them.
|
|
165
|
+
* `fileExtensions`, `clientOptions`, `collections`, `generateId` and the live
|
|
166
|
+
* handles (`client`, `mongoose`, `hooks`, `logger`, `telemetry`) are
|
|
167
|
+
* config-file/API only: a single environment string cannot express them.
|
|
158
168
|
*/
|
|
159
169
|
export interface MigronautConfig {
|
|
160
170
|
/** MongoDB connection URI. Not required when `client` is supplied */
|
|
@@ -179,6 +189,12 @@ export interface MigronautConfig {
|
|
|
179
189
|
migrationsCollection: string;
|
|
180
190
|
/** Collection name for distributed lock. Default: '_migronaut_locks' */
|
|
181
191
|
lockCollection: string;
|
|
192
|
+
/**
|
|
193
|
+
* Collection holding the converge history — one entry per converge that
|
|
194
|
+
* changed something or failed. Created by the first such converge, never
|
|
195
|
+
* before. Default: '_migronaut_converge'
|
|
196
|
+
*/
|
|
197
|
+
convergeLogCollection: string;
|
|
182
198
|
/** How long (seconds) a lock is considered stale. Default: 60 */
|
|
183
199
|
lockTTLSeconds: number;
|
|
184
200
|
/**
|
|
@@ -255,11 +271,76 @@ export interface MigronautConfig {
|
|
|
255
271
|
* A single-file `up` (an explicit, deliberate target) is never checked.
|
|
256
272
|
*/
|
|
257
273
|
onOutOfOrder?: 'warn' | 'error' | 'allow';
|
|
274
|
+
/**
|
|
275
|
+
* Declared collections: their indexes and validator, as the end state you
|
|
276
|
+
* want. `converge()` (`migronaut converge`) compares them with the live
|
|
277
|
+
* database and makes the difference — no migration file per change, and
|
|
278
|
+
* nothing recorded. Combined with the files in `collectionsDir`; a
|
|
279
|
+
* collection declared twice is a {@link ConfigInvalidError}. Experimental.
|
|
280
|
+
*/
|
|
281
|
+
collections?: CollectionDefinition[];
|
|
282
|
+
/**
|
|
283
|
+
* Directory of collection definition files, one collection per file (a
|
|
284
|
+
* `.ts`/`.js` default export or a `.json` document; the collection name
|
|
285
|
+
* defaults to the file name). Opt-in — nothing is read unless this is set.
|
|
286
|
+
* Files are loaded when a converge runs, not at config resolution.
|
|
287
|
+
*/
|
|
288
|
+
collectionsDir?: string;
|
|
289
|
+
/**
|
|
290
|
+
* End every bulk `up` — no file, no `to` — by converging the declared
|
|
291
|
+
* collections, under the same lock and even when no migration was pending.
|
|
292
|
+
* Default: false
|
|
293
|
+
*/
|
|
294
|
+
convergeAfterUp?: boolean;
|
|
295
|
+
/**
|
|
296
|
+
* What converge does with declared search indexes on a server without
|
|
297
|
+
* Atlas Search: `'fail'` refuses the run before anything is written,
|
|
298
|
+
* `'skip'` converges everything else and reports them as `skip` rows.
|
|
299
|
+
* Default: 'fail'
|
|
300
|
+
* @experimental New in 2.2
|
|
301
|
+
*/
|
|
302
|
+
onSearchUnavailable?: 'fail' | 'skip';
|
|
303
|
+
/**
|
|
304
|
+
* Hold every converge — the after-up one included — until each declared
|
|
305
|
+
* search index is queryable with its declared definition. Search indexes
|
|
306
|
+
* build in the background, so without it a new one is not queryable yet when
|
|
307
|
+
* converge returns. An index that FAILED or went STALE before the run, its
|
|
308
|
+
* definition unchanged, does not hold it — it is warned about instead. The
|
|
309
|
+
* migration lock is released while it waits. Default: false
|
|
310
|
+
* @experimental New in 2.2
|
|
311
|
+
*/
|
|
312
|
+
waitForSearchIndexes?: boolean;
|
|
313
|
+
/**
|
|
314
|
+
* How long `waitForSearchIndexes` waits before the converge fails with
|
|
315
|
+
* `phase: 'wait'` (the server goes on building). Default: 600000 (10 minutes)
|
|
316
|
+
* @experimental New in 2.2
|
|
317
|
+
*/
|
|
318
|
+
searchIndexWaitTimeoutMs?: number;
|
|
258
319
|
/** Mongoose instance — required only if your migrations use Mongoose models */
|
|
259
320
|
mongoose?: MongooseLike;
|
|
260
321
|
hooks?: MigrationHooks;
|
|
261
322
|
/** Custom logger — set to null to silence all output (useful in tests) */
|
|
262
323
|
logger?: MigronautLogger | null;
|
|
324
|
+
/**
|
|
325
|
+
* Your own identifier format (ULID, CUID, UUIDv7, …) for every id migronaut
|
|
326
|
+
* mints: the run id — stamped on changelog records, events and log lines,
|
|
327
|
+
* and stored as the lock's owner token — and, through
|
|
328
|
+
* `@alexify/migronaut/bullmq`, the group id of an enqueue call.
|
|
329
|
+
* Default: `crypto.randomUUID()`.
|
|
330
|
+
*
|
|
331
|
+
* Ids are for correlation. The lock adds a token of its own, so a generator
|
|
332
|
+
* that repeats a value blurs which run wrote what but never lets two runs
|
|
333
|
+
* hold the lock at once.
|
|
334
|
+
*/
|
|
335
|
+
generateId?: IdGenerator;
|
|
336
|
+
/**
|
|
337
|
+
* OpenTelemetry, from your own `@opentelemetry/api`: a tracer, a meter, or
|
|
338
|
+
* both. Every run and every migration becomes a span — the migration's span
|
|
339
|
+
* is the active one while its `up`/`down` runs, so an instrumented MongoDB
|
|
340
|
+
* driver nests its command spans under it — and their durations are
|
|
341
|
+
* recorded as histograms. Absent, `null` or empty turns it off.
|
|
342
|
+
*/
|
|
343
|
+
telemetry?: MigronautTelemetry | null;
|
|
263
344
|
}
|
|
264
345
|
|
|
265
346
|
/**
|
|
@@ -275,6 +356,419 @@ export type MigronautConfigInput =
|
|
|
275
356
|
| Partial<MigronautConfig>
|
|
276
357
|
| (() => Partial<MigronautConfig> | Promise<Partial<MigronautConfig>>);
|
|
277
358
|
|
|
359
|
+
// ─── Collections (converge) ───────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
/** An index key direction: ascending, descending, or a special index type */
|
|
362
|
+
export type IndexKeyDirection = 1 | -1 | 'text' | 'hashed' | '2d' | '2dsphere';
|
|
363
|
+
|
|
364
|
+
/** Collation of an index — `locale` required, the rest as MongoDB defines them */
|
|
365
|
+
export interface IndexCollation {
|
|
366
|
+
locale: string;
|
|
367
|
+
caseLevel?: boolean;
|
|
368
|
+
caseFirst?: 'upper' | 'lower' | 'off';
|
|
369
|
+
strength?: 1 | 2 | 3 | 4 | 5;
|
|
370
|
+
numericOrdering?: boolean;
|
|
371
|
+
alternate?: 'non-ignorable' | 'shifted';
|
|
372
|
+
maxVariable?: 'punct' | 'space';
|
|
373
|
+
backwards?: boolean;
|
|
374
|
+
normalization?: boolean;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* One declared index, in the driver's own flat `createIndexes` shape. Every
|
|
379
|
+
* option is checked: an unknown one is a {@link ConfigInvalidError} rather
|
|
380
|
+
* than dropped, because the driver drops it silently and the index would be
|
|
381
|
+
* built without it.
|
|
382
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
383
|
+
*/
|
|
384
|
+
export interface IndexDefinition {
|
|
385
|
+
/**
|
|
386
|
+
* Field → direction, in index order. A compound key with an integer-like
|
|
387
|
+
* field name must be a `Map` (a plain object reorders such names), with that
|
|
388
|
+
* field first — the live key is read back as a plain object.
|
|
389
|
+
*/
|
|
390
|
+
key: Record<string, IndexKeyDirection> | Map<string, IndexKeyDirection>;
|
|
391
|
+
/** Defaults to the name MongoDB generates: `email_1`, `a_1_b_-1` */
|
|
392
|
+
name?: string;
|
|
393
|
+
unique?: boolean;
|
|
394
|
+
sparse?: boolean;
|
|
395
|
+
/** Changed in place (`collMod`) — no rebuild */
|
|
396
|
+
hidden?: boolean;
|
|
397
|
+
/** TTL in seconds. Changed in place when the live index already has one */
|
|
398
|
+
expireAfterSeconds?: number;
|
|
399
|
+
partialFilterExpression?: Record<string, unknown>;
|
|
400
|
+
collation?: IndexCollation;
|
|
401
|
+
/** For a wildcard (`$**`) index */
|
|
402
|
+
wildcardProjection?: Record<string, 0 | 1 | boolean>;
|
|
403
|
+
/** Text index field weights (default 1) */
|
|
404
|
+
weights?: Record<string, number>;
|
|
405
|
+
default_language?: string;
|
|
406
|
+
language_override?: string;
|
|
407
|
+
textIndexVersion?: number;
|
|
408
|
+
'2dsphereIndexVersion'?: number;
|
|
409
|
+
bits?: number;
|
|
410
|
+
min?: number;
|
|
411
|
+
max?: number;
|
|
412
|
+
storageEngine?: Record<string, unknown>;
|
|
413
|
+
/** Accepted and ignored — a no-op since MongoDB 4.2 */
|
|
414
|
+
background?: boolean;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export type ValidationLevel = 'off' | 'strict' | 'moderate';
|
|
418
|
+
export type ValidationAction = 'error' | 'warn' | 'errorAndLog';
|
|
419
|
+
|
|
420
|
+
/** The two kinds of Atlas search index */
|
|
421
|
+
export type SearchIndexType = 'search' | 'vectorSearch';
|
|
422
|
+
|
|
423
|
+
/** `mappings` of an Atlas Search definition */
|
|
424
|
+
export interface SearchIndexMappings {
|
|
425
|
+
/** Default: false */
|
|
426
|
+
dynamic?: boolean | { typeSet: string };
|
|
427
|
+
fields?: Record<string, unknown>;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* An Atlas Search index definition, as Atlas defines it. Compared whole, with
|
|
432
|
+
* the documented defaults filled in; anything Atlas adds can be declared too.
|
|
433
|
+
*/
|
|
434
|
+
export interface SearchDefinition {
|
|
435
|
+
mappings: SearchIndexMappings;
|
|
436
|
+
/** Default: 'lucene.standard' */
|
|
437
|
+
analyzer?: string;
|
|
438
|
+
/** Default: the analyzer */
|
|
439
|
+
searchAnalyzer?: string;
|
|
440
|
+
analyzers?: Array<Record<string, unknown>>;
|
|
441
|
+
synonyms?: Array<Record<string, unknown>>;
|
|
442
|
+
/** Default: false */
|
|
443
|
+
storedSource?: boolean | { include?: string[]; exclude?: string[] };
|
|
444
|
+
/** Default: 1 */
|
|
445
|
+
numPartitions?: number;
|
|
446
|
+
[option: string]: unknown;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** One field of a Vector Search definition */
|
|
450
|
+
export interface VectorSearchField {
|
|
451
|
+
/** `'autoEmbed'` is Atlas's automated embedding (in preview); one index holds vector or autoEmbed fields, not both */
|
|
452
|
+
type: 'vector' | 'filter' | 'autoEmbed' | (string & {});
|
|
453
|
+
path: string;
|
|
454
|
+
numDimensions?: number;
|
|
455
|
+
similarity?: 'euclidean' | 'cosine' | 'dotProduct';
|
|
456
|
+
/** Default: 'none' ('scalar' for autoEmbed) */
|
|
457
|
+
quantization?: string;
|
|
458
|
+
/** Default: 'hnsw' */
|
|
459
|
+
indexingMethod?: 'hnsw' | 'flat';
|
|
460
|
+
/** Default: { maxEdges: 16, numEdgeCandidates: 100 } */
|
|
461
|
+
hnswOptions?: { maxEdges?: number; numEdgeCandidates?: number };
|
|
462
|
+
/** autoEmbed: the embedding model */
|
|
463
|
+
model?: string;
|
|
464
|
+
/** autoEmbed: 'text' */
|
|
465
|
+
modality?: string;
|
|
466
|
+
[option: string]: unknown;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** A Vector Search index definition */
|
|
470
|
+
export interface VectorSearchDefinition {
|
|
471
|
+
fields: VectorSearchField[];
|
|
472
|
+
[option: string]: unknown;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* One declared Atlas Search or Vector Search index. The name defaults to
|
|
477
|
+
* `'default'` and the type to `'search'`, as on the server. A change of type
|
|
478
|
+
* — or of an autoEmbed field's path, model, size, quantization or modality —
|
|
479
|
+
* cannot be made in place: converge refuses it, and the way is a new index
|
|
480
|
+
* under a new name (converge, then remove the old declaration and converge
|
|
481
|
+
* with prune).
|
|
482
|
+
* @experimental New in 2.2 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
483
|
+
*/
|
|
484
|
+
export type SearchIndexDefinition =
|
|
485
|
+
| { name?: string; type?: 'search'; definition: SearchDefinition }
|
|
486
|
+
| { name?: string; type: 'vectorSearch'; definition: VectorSearchDefinition };
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* A declared collection: the end state `converge()` keeps it in. Leave
|
|
490
|
+
* `indexes`, `searchIndexes` or `validator` out to leave that part unmanaged.
|
|
491
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
492
|
+
*/
|
|
493
|
+
export interface CollectionDefinition {
|
|
494
|
+
name: string;
|
|
495
|
+
/**
|
|
496
|
+
* Every index besides `_id`. Undeclared live indexes are kept (and reported)
|
|
497
|
+
* unless `prune` is on.
|
|
498
|
+
*/
|
|
499
|
+
indexes?: IndexDefinition[];
|
|
500
|
+
/**
|
|
501
|
+
* Atlas Search and Vector Search indexes (Atlas, an Atlas CLI local
|
|
502
|
+
* deployment, or MongoDB 8.3+ with mongot). Undeclared live ones are kept
|
|
503
|
+
* unless `prune` is on; leave the key out and they are not managed at all.
|
|
504
|
+
* @experimental New in 2.2
|
|
505
|
+
*/
|
|
506
|
+
searchIndexes?: SearchIndexDefinition[];
|
|
507
|
+
/** A query or `{ $jsonSchema }` document; `null` (or `{}`) for no validator */
|
|
508
|
+
validator?: Record<string, unknown> | null;
|
|
509
|
+
/** Default: 'strict'. Only with a validator */
|
|
510
|
+
validationLevel?: ValidationLevel;
|
|
511
|
+
/** Default: 'error'. Only with a validator */
|
|
512
|
+
validationAction?: ValidationAction;
|
|
513
|
+
/**
|
|
514
|
+
* Drop live indexes (and search indexes, when `searchIndexes` is declared)
|
|
515
|
+
* this definition does not declare. Default: the call's `prune`, else false
|
|
516
|
+
*/
|
|
517
|
+
prune?: boolean;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** What a `collectionsDir` file exports: a definition whose name defaults to the file name */
|
|
521
|
+
export type CollectionDefinitionFile = Omit<CollectionDefinition, 'name'> & { name?: string };
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Options for {@link MigratorKit.converge}
|
|
525
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
526
|
+
*/
|
|
527
|
+
export interface ConvergeOptions {
|
|
528
|
+
/** Plan without writing: no lock, no events. The result's rows are `'planned'` */
|
|
529
|
+
dryRun?: boolean;
|
|
530
|
+
/** Drop undeclared indexes in collections whose definition does not set `prune` */
|
|
531
|
+
prune?: boolean;
|
|
532
|
+
/** Skip lock acquisition (dev only) */
|
|
533
|
+
noLock?: boolean;
|
|
534
|
+
/**
|
|
535
|
+
* Refuse ({@link MigrationBlockedError}) while any migration is still
|
|
536
|
+
* pending — checked under the lock. How the queue adapter runs a converge
|
|
537
|
+
* as the tail of a deploy.
|
|
538
|
+
*/
|
|
539
|
+
ordered?: boolean;
|
|
540
|
+
/**
|
|
541
|
+
* Allow a rebuild that drops a unique index and builds a unique one back.
|
|
542
|
+
* Without it such a rebuild plans as a `conflict`: the constraint is gone
|
|
543
|
+
* until the new index is built, and a duplicate written in between leaves
|
|
544
|
+
* neither index buildable. The after-up hook and queue jobs never set it.
|
|
545
|
+
* CLI: `--rebuild-unique`.
|
|
546
|
+
*/
|
|
547
|
+
rebuildUnique?: boolean;
|
|
548
|
+
/**
|
|
549
|
+
* Hold the run until every declared search index serves its
|
|
550
|
+
* declaration — failing on a FAILED build of an index this run created or
|
|
551
|
+
* changed, or after `searchIndexWaitTimeoutMs`. The migration lock is released
|
|
552
|
+
* when the wait starts — it only reads. Overrides the config's `waitForSearchIndexes`;
|
|
553
|
+
* not with `dryRun`. CLI: `--wait-search` / `--no-wait-search`.
|
|
554
|
+
* @experimental New in 2.2
|
|
555
|
+
*/
|
|
556
|
+
waitForSearchIndexes?: boolean;
|
|
557
|
+
/** Who asked for this converge — recorded in the converge history */
|
|
558
|
+
requestedBy?: string;
|
|
559
|
+
/** Why — recorded in the converge history */
|
|
560
|
+
reason?: string;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export type ConvergeTarget = 'collection' | 'validator' | 'index' | 'searchIndex';
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* What converge does to one target. `keep` is an undeclared index left alone
|
|
567
|
+
* (prune off); `conflict` refuses the run — an undeclared index covers the
|
|
568
|
+
* declared one's key under another name, a unique index would be rebuilt
|
|
569
|
+
* without {@link ConvergeOptions.rebuildUnique}, the collection is a view or a
|
|
570
|
+
* time-series collection, a search index would need a change no update can
|
|
571
|
+
* make (its type, an autoEmbed field's model or size), or the server has no
|
|
572
|
+
* Atlas Search; `skip` is a declared search index left alone on a server
|
|
573
|
+
* without Search (`onSearchUnavailable: 'skip'`). A search index is never
|
|
574
|
+
* `recreate`d: `modify` updates it in place.
|
|
575
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
576
|
+
*/
|
|
577
|
+
export type ConvergeActionKind =
|
|
578
|
+
| 'create'
|
|
579
|
+
| 'modify'
|
|
580
|
+
| 'recreate'
|
|
581
|
+
| 'drop'
|
|
582
|
+
| 'keep'
|
|
583
|
+
| 'unchanged'
|
|
584
|
+
| 'conflict'
|
|
585
|
+
| 'skip';
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The status `$listSearchIndexes` reports for a search index — `'UNKNOWN'`
|
|
589
|
+
* when the server reports none
|
|
590
|
+
*/
|
|
591
|
+
export type SearchIndexStatus =
|
|
592
|
+
| 'PENDING'
|
|
593
|
+
| 'BUILDING'
|
|
594
|
+
| 'READY'
|
|
595
|
+
| 'FAILED'
|
|
596
|
+
| 'STALE'
|
|
597
|
+
| 'DELETING'
|
|
598
|
+
| 'DOES_NOT_EXIST'
|
|
599
|
+
| 'UNKNOWN'
|
|
600
|
+
| (string & {});
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Where the server is with a search index: it builds in the background, so a
|
|
604
|
+
* created or updated one is not queryable (with its new definition) at once
|
|
605
|
+
* @experimental New in 2.2
|
|
606
|
+
*/
|
|
607
|
+
export interface SearchIndexBuild {
|
|
608
|
+
status: SearchIndexStatus;
|
|
609
|
+
queryable: boolean;
|
|
610
|
+
/** The server's message — why a build FAILED, typically */
|
|
611
|
+
message?: string;
|
|
612
|
+
/** A newer definition is being built next to the one served */
|
|
613
|
+
updating?: true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* `planned` in a dry run; otherwise `applied`, `failed`, or `skipped` — no
|
|
618
|
+
* change was needed, or the run stopped before reaching it.
|
|
619
|
+
*/
|
|
620
|
+
export type ConvergeActionStatus = 'planned' | 'applied' | 'failed' | 'skipped';
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* One row of a converge result
|
|
624
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
625
|
+
*/
|
|
626
|
+
export interface ConvergeAction {
|
|
627
|
+
target: ConvergeTarget;
|
|
628
|
+
/** The index name; the collection name for a `collection` or `validator` row */
|
|
629
|
+
name: string;
|
|
630
|
+
action: ConvergeActionKind;
|
|
631
|
+
status: ConvergeActionStatus;
|
|
632
|
+
/** What differs (`'unique, expireAfterSeconds'`), or why a row is what it is */
|
|
633
|
+
reason?: string;
|
|
634
|
+
/** The live index the row refers to when its name differs from the declared one */
|
|
635
|
+
liveName?: string;
|
|
636
|
+
durationMs?: number;
|
|
637
|
+
/**
|
|
638
|
+
* What is there now — the live index (`{ key, name, ...options }`) or
|
|
639
|
+
* validator (`{ validator, validationLevel, validationAction }`) — on rows
|
|
640
|
+
* that change or drop it, and on `keep` rows. Plain JSON.
|
|
641
|
+
*/
|
|
642
|
+
from?: Record<string, unknown>;
|
|
643
|
+
/** What the row puts there — the declared index or validator — on rows that create or change it */
|
|
644
|
+
to?: Record<string, unknown>;
|
|
645
|
+
/**
|
|
646
|
+
* A search index row's build state on the server — as read before the run,
|
|
647
|
+
* and after it for a row the run applied
|
|
648
|
+
* @experimental New in 2.2
|
|
649
|
+
*/
|
|
650
|
+
build?: SearchIndexBuild;
|
|
651
|
+
/**
|
|
652
|
+
* On a search index row: the options the server reports that the
|
|
653
|
+
* declaration does not set and migronaut knows no default for
|
|
654
|
+
* (`mappings.fields.title.similarity`) — left out of the comparison, so a
|
|
655
|
+
* new server default does not make every converge update the index.
|
|
656
|
+
* Declare one to manage it.
|
|
657
|
+
* @experimental New in 2.2
|
|
658
|
+
*/
|
|
659
|
+
ignored?: string[];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
664
|
+
*/
|
|
665
|
+
export interface CollectionConvergeResult {
|
|
666
|
+
name: string;
|
|
667
|
+
actions: ConvergeAction[];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* One entry of the converge history (`convergeLogCollection`): a converge that
|
|
672
|
+
* changed something or failed.
|
|
673
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
674
|
+
*/
|
|
675
|
+
export interface ConvergeHistoryEntry {
|
|
676
|
+
runId?: string;
|
|
677
|
+
/** `'converge'`, or `'up'` for the converge that ended a bulk `up` */
|
|
678
|
+
trigger: ConvergeTrigger;
|
|
679
|
+
startedAt: Date;
|
|
680
|
+
finishedAt: Date;
|
|
681
|
+
durationMs: number;
|
|
682
|
+
success: boolean;
|
|
683
|
+
/** Redacted failure message (`success: false` only) */
|
|
684
|
+
error?: string;
|
|
685
|
+
executedBy: string;
|
|
686
|
+
host: string;
|
|
687
|
+
environment: string;
|
|
688
|
+
requestedBy?: string;
|
|
689
|
+
reason?: string;
|
|
690
|
+
/** Changes applied */
|
|
691
|
+
changed: number;
|
|
692
|
+
/** The rows that changed, failed or refused the run — each with its collection and `from` / `to` */
|
|
693
|
+
actions: Array<ConvergeAction & { collection: string }>;
|
|
694
|
+
unstable?: ConvergeUnstable[];
|
|
695
|
+
/**
|
|
696
|
+
* What the run saw of Atlas Search, and how a wait for its builds ended —
|
|
697
|
+
* when a definition declares `searchIndexes`
|
|
698
|
+
* @experimental New in 2.2
|
|
699
|
+
*/
|
|
700
|
+
search?: ConvergeSearchSummary;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Something applied that still compares as changed — reported, never rebuilt in a loop
|
|
705
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
706
|
+
*/
|
|
707
|
+
export interface ConvergeUnstable {
|
|
708
|
+
collection: string;
|
|
709
|
+
target: ConvergeTarget;
|
|
710
|
+
name: string;
|
|
711
|
+
action: ConvergeActionKind;
|
|
712
|
+
reason?: string;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* A declared search index that exists but does not serve its declaration yet
|
|
717
|
+
* @experimental New in 2.2
|
|
718
|
+
*/
|
|
719
|
+
export interface SearchIndexNotReady extends SearchIndexBuild {
|
|
720
|
+
collection: string;
|
|
721
|
+
name: string;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* What a converge saw of Atlas Search — present when a definition declares
|
|
726
|
+
* `searchIndexes`
|
|
727
|
+
* @experimental New in 2.2
|
|
728
|
+
*/
|
|
729
|
+
export interface ConvergeSearchSummary {
|
|
730
|
+
/** Whether the server has Atlas Search */
|
|
731
|
+
available: boolean;
|
|
732
|
+
/**
|
|
733
|
+
* How that was told: `'listed'` (the server listed search indexes),
|
|
734
|
+
* `'parameter'` (its search index manager setting), `'error'` (it refused
|
|
735
|
+
* a search command), `'version'` (older than 6.0, not asked) or
|
|
736
|
+
* `'assumed'` (it would not say — a refusal at apply time reports it)
|
|
737
|
+
*/
|
|
738
|
+
evidence?: 'listed' | 'parameter' | 'error' | 'version' | 'assumed';
|
|
739
|
+
/**
|
|
740
|
+
* Declared search indexes still building, updating, stale or failed. Does
|
|
741
|
+
* not count against `inSync`: a build is the server's work, not a difference.
|
|
742
|
+
*/
|
|
743
|
+
notReady: SearchIndexNotReady[];
|
|
744
|
+
/** How a wait for the builds (`waitForSearchIndexes`) ended, when there was one */
|
|
745
|
+
wait?: { outcome: ConvergeWaitOutcome; waitedMs: number };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** How a wait for search index builds ended */
|
|
749
|
+
export type ConvergeWaitOutcome = 'ready' | 'failed' | 'timeout' | 'unreadable' | 'aborted';
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Outcome of {@link MigratorKit.converge}
|
|
753
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
754
|
+
*/
|
|
755
|
+
export interface ConvergeResult {
|
|
756
|
+
dryRun: boolean;
|
|
757
|
+
/** Changes applied — or, in a dry run, changes the run would make */
|
|
758
|
+
changed: number;
|
|
759
|
+
/**
|
|
760
|
+
* True when the database matches the declarations: nothing left to do and
|
|
761
|
+
* no conflict. Undeclared indexes kept with prune off, search indexes
|
|
762
|
+
* skipped on a server without Search, and search index builds still under
|
|
763
|
+
* way do not count against it.
|
|
764
|
+
*/
|
|
765
|
+
inSync: boolean;
|
|
766
|
+
collections: CollectionConvergeResult[];
|
|
767
|
+
unstable?: ConvergeUnstable[];
|
|
768
|
+
/** @experimental New in 2.2 */
|
|
769
|
+
search?: ConvergeSearchSummary;
|
|
770
|
+
}
|
|
771
|
+
|
|
278
772
|
// ─── Logger ───────────────────────────────────────────────────────────────────
|
|
279
773
|
|
|
280
774
|
/**
|
|
@@ -303,6 +797,92 @@ export interface MigronautLogger {
|
|
|
303
797
|
*/
|
|
304
798
|
export type LogMethod = (msg: string, fields?: Record<string, unknown>) => void;
|
|
305
799
|
|
|
800
|
+
// ─── Telemetry ────────────────────────────────────────────────────────────────
|
|
801
|
+
|
|
802
|
+
/** A span or metric attribute value — the scalar subset migronaut sets */
|
|
803
|
+
export type MigronautAttributes = Record<string, string | number | boolean>;
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* The slice of an OpenTelemetry `Span` migronaut calls. Declared structurally —
|
|
807
|
+
* `@opentelemetry/api` is deliberately not imported, so the package's types
|
|
808
|
+
* resolve for users who never installed it. A real `Span` satisfies it.
|
|
809
|
+
*/
|
|
810
|
+
export interface MigronautSpan {
|
|
811
|
+
setAttribute(key: string, value: string | number | boolean): unknown;
|
|
812
|
+
/** `code` is OpenTelemetry's `SpanStatusCode` — migronaut only ever sets ERROR (2) */
|
|
813
|
+
setStatus(status: { code: number; message?: string }): unknown;
|
|
814
|
+
end(): void;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* The slice of an OpenTelemetry `Tracer` migronaut calls — what
|
|
819
|
+
* `trace.getTracer('@alexify/migronaut')` returns. Only `startActiveSpan` is
|
|
820
|
+
* used: it is what makes a span the active context for the migration's own
|
|
821
|
+
* code, and so for any instrumentation running underneath it.
|
|
822
|
+
*/
|
|
823
|
+
export interface MigronautTracer {
|
|
824
|
+
startActiveSpan<T>(
|
|
825
|
+
name: string,
|
|
826
|
+
options: { attributes?: MigronautAttributes },
|
|
827
|
+
fn: (span: MigronautSpan) => T,
|
|
828
|
+
): T;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** An OpenTelemetry `Histogram`, as far as migronaut uses one */
|
|
832
|
+
export interface MigronautHistogram {
|
|
833
|
+
record(value: number, attributes?: MigronautAttributes): void;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** An OpenTelemetry `Counter`, as far as migronaut uses one */
|
|
837
|
+
export interface MigronautCounter {
|
|
838
|
+
add(value: number, attributes?: MigronautAttributes): void;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/** Options migronaut passes when it creates an instrument */
|
|
842
|
+
export interface MigronautMetricOptions {
|
|
843
|
+
description?: string;
|
|
844
|
+
unit?: string;
|
|
845
|
+
/** Histogram bucket boundaries, in the instrument's unit (seconds) */
|
|
846
|
+
advice?: { explicitBucketBoundaries?: number[] };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* The slice of an OpenTelemetry `Meter` migronaut calls — what
|
|
851
|
+
* `metrics.getMeter('@alexify/migronaut')` returns.
|
|
852
|
+
*/
|
|
853
|
+
export interface MigronautMeter {
|
|
854
|
+
createHistogram(name: string, options?: MigronautMetricOptions): MigronautHistogram;
|
|
855
|
+
createCounter(name: string, options?: MigronautMetricOptions): MigronautCounter;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* The `telemetry` config option. Both parts are optional and independent:
|
|
860
|
+
* a tracer alone gives spans, a meter alone gives metrics.
|
|
861
|
+
*
|
|
862
|
+
* Spans: `migronaut.run` (one per run that held the lock) and
|
|
863
|
+
* `migronaut.migration` (one per migration executed, a child of the run).
|
|
864
|
+
* Metrics: `migronaut.run.duration`, `migronaut.migration.duration` and
|
|
865
|
+
* `migronaut.lock.acquire.duration` (histograms, seconds), plus the counters
|
|
866
|
+
* `migronaut.lock.refused` and `migronaut.lock.lost`. A failure sets the span's
|
|
867
|
+
* status to ERROR with a redacted message, and `error.type` — on the span and
|
|
868
|
+
* the metric point — to the {@link MigronautErrorCode}, or for an error that is
|
|
869
|
+
* not migronaut's to its class name (`_OTHER` when it has none).
|
|
870
|
+
*
|
|
871
|
+
* A tracer or meter that throws never fails a run.
|
|
872
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
873
|
+
*/
|
|
874
|
+
export interface MigronautTelemetry {
|
|
875
|
+
tracer?: MigronautTracer | null;
|
|
876
|
+
meter?: MigronautMeter | null;
|
|
877
|
+
/**
|
|
878
|
+
* Static attributes added to every span and every metric point — your own
|
|
879
|
+
* low-cardinality dimensions (`{ tenant: 'acme' }`). At most 20. They cannot
|
|
880
|
+
* replace migronaut's own: `db.namespace` (the database name, always
|
|
881
|
+
* present) and the `migronaut.*` attributes win.
|
|
882
|
+
*/
|
|
883
|
+
attributes?: Record<string, string | number | boolean>;
|
|
884
|
+
}
|
|
885
|
+
|
|
306
886
|
// ─── Progress Reporter ─────────────────────────────────────────────────────────
|
|
307
887
|
|
|
308
888
|
/**
|
|
@@ -375,6 +955,20 @@ export interface StatusRow {
|
|
|
375
955
|
* the whole status/audit call.
|
|
376
956
|
*/
|
|
377
957
|
invalid?: true;
|
|
958
|
+
/**
|
|
959
|
+
* The file's current checksum (SHA-256 hex) — on `dryRun('up')` rows, so a
|
|
960
|
+
* caller that applies them later can insist on exactly this version
|
|
961
|
+
* (`up(file, { checksum })`).
|
|
962
|
+
*/
|
|
963
|
+
checksum?: string;
|
|
964
|
+
/** Who asked for the apply, and why — when the run said (`requestedBy` / `reason` options) */
|
|
965
|
+
requestedBy?: string;
|
|
966
|
+
reason?: string;
|
|
967
|
+
/** Who asked for the revert, and why — on reverted history rows */
|
|
968
|
+
revertRequestedBy?: string;
|
|
969
|
+
revertReason?: string;
|
|
970
|
+
/** The checksum of the file version that failed (status `'failed'` only) */
|
|
971
|
+
failedChecksum?: string;
|
|
378
972
|
}
|
|
379
973
|
|
|
380
974
|
// ─── Import (migrate-mongo adoption) ────────────────────────────────────────────
|
|
@@ -432,6 +1026,13 @@ export interface LockInfo {
|
|
|
432
1026
|
host: string;
|
|
433
1027
|
/** Username of the holder */
|
|
434
1028
|
executedBy: string;
|
|
1029
|
+
/**
|
|
1030
|
+
* The holder's run id — the `runId` of its events, log lines and changelog
|
|
1031
|
+
* records. Absent for a document written by hand.
|
|
1032
|
+
*/
|
|
1033
|
+
runId?: string;
|
|
1034
|
+
/** The holder's lock TTL (ms), which paces its heartbeat. Absent before 2.1 */
|
|
1035
|
+
ttlMs?: number;
|
|
435
1036
|
}
|
|
436
1037
|
|
|
437
1038
|
// ─── Error Codes ──────────────────────────────────────────────────────────────
|
|
@@ -456,7 +1057,11 @@ export type MigronautErrorCode =
|
|
|
456
1057
|
| 'NOT_APPLIED'
|
|
457
1058
|
| 'IMPORT_TARGET_NOT_EMPTY'
|
|
458
1059
|
| 'MIGRATION_IRREVERSIBLE'
|
|
459
|
-
| 'MIGRATION_OUT_OF_ORDER'
|
|
1060
|
+
| 'MIGRATION_OUT_OF_ORDER'
|
|
1061
|
+
| 'MIGRATION_BLOCKED'
|
|
1062
|
+
| 'QUEUE_JOB_INVALID'
|
|
1063
|
+
| 'QUEUE_JOB_FAILED'
|
|
1064
|
+
| 'CONVERGE_FAILED';
|
|
460
1065
|
|
|
461
1066
|
// ─── Config file format ─────────────────────────────────────────────────────────
|
|
462
1067
|
|
|
@@ -488,6 +1093,44 @@ export interface UpOptions {
|
|
|
488
1093
|
* Mutually exclusive with a filename and `steps`.
|
|
489
1094
|
*/
|
|
490
1095
|
to?: string;
|
|
1096
|
+
/**
|
|
1097
|
+
* Stamp this batch number on what the run applies, instead of the next free
|
|
1098
|
+
* one ({@link MigratorKit.nextBatch}). A label, not a reservation: it may
|
|
1099
|
+
* equal a batch already in use, which is how several single-file runs become
|
|
1100
|
+
* one rollback unit — the queue adapter gives every job of an enqueue group
|
|
1101
|
+
* the same value. Positive integer; mutually exclusive with `step`.
|
|
1102
|
+
*/
|
|
1103
|
+
batch?: number;
|
|
1104
|
+
/**
|
|
1105
|
+
* Refuse ({@link MigrationBlockedError}) to apply the named file while an
|
|
1106
|
+
* earlier file on disk is still pending — the invariant a bulk `up` gets by
|
|
1107
|
+
* construction, enforced from the changelog rather than from the caller's
|
|
1108
|
+
* memory. Also makes the single-file run honour `strict` drift checks and
|
|
1109
|
+
* `onOutOfOrder` like a bulk run. Requires a filename.
|
|
1110
|
+
*/
|
|
1111
|
+
ordered?: boolean;
|
|
1112
|
+
/**
|
|
1113
|
+
* The SHA-256 (hex) the named file must have — refuse ({@link
|
|
1114
|
+
* ChecksumMismatchError}, `context.planned: true`) to apply any other
|
|
1115
|
+
* version of it. A queue job carries the checksum its plan saw, so a worker
|
|
1116
|
+
* from another deploy never applies a different file under the same name.
|
|
1117
|
+
* Requires a filename; an already-applied file is skipped as usual.
|
|
1118
|
+
*/
|
|
1119
|
+
checksum?: string;
|
|
1120
|
+
/**
|
|
1121
|
+
* Converge the declared collections after the migrations, under the same
|
|
1122
|
+
* lock — overrides `convergeAfterUp` for this call. Bulk runs only: refused
|
|
1123
|
+
* with a filename or `to`.
|
|
1124
|
+
*/
|
|
1125
|
+
converge?: boolean;
|
|
1126
|
+
/**
|
|
1127
|
+
* Who asked for this run (≤ 128 characters) — stamped on the changelog
|
|
1128
|
+
* records it writes. `executedBy` is the OS user that ran it; on a queue
|
|
1129
|
+
* worker that is the container's, which is why the requester is separate.
|
|
1130
|
+
*/
|
|
1131
|
+
requestedBy?: string;
|
|
1132
|
+
/** Why (≤ 512 characters) — a ticket, a sentence; stamped like `requestedBy` */
|
|
1133
|
+
reason?: string;
|
|
491
1134
|
}
|
|
492
1135
|
|
|
493
1136
|
/** Options for {@link MigratorKit.down} */
|
|
@@ -508,6 +1151,20 @@ export interface DownOptions {
|
|
|
508
1151
|
* to the same state. Mutually exclusive with `batch`, `steps` and a filename.
|
|
509
1152
|
*/
|
|
510
1153
|
to?: string;
|
|
1154
|
+
/**
|
|
1155
|
+
* Refuse ({@link MigrationBlockedError}) to revert the named file while a
|
|
1156
|
+
* migration applied *after* it is still applied — reverts must go newest
|
|
1157
|
+
* first (by `appliedAt`, the order `steps` uses). Requires a filename.
|
|
1158
|
+
*/
|
|
1159
|
+
ordered?: boolean;
|
|
1160
|
+
/**
|
|
1161
|
+
* Who asked for this run (≤ 128 characters) — stamped on the records it
|
|
1162
|
+
* reverts (`revertRequestedBy`). `executedBy` is the OS user that ran it; on a queue
|
|
1163
|
+
* worker that is the container's, which is why the requester is separate.
|
|
1164
|
+
*/
|
|
1165
|
+
requestedBy?: string;
|
|
1166
|
+
/** Why (≤ 512 characters) — a ticket, a sentence; stamped as `revertReason` */
|
|
1167
|
+
reason?: string;
|
|
511
1168
|
}
|
|
512
1169
|
|
|
513
1170
|
/** Payload common to every lifecycle event */
|
|
@@ -531,7 +1188,7 @@ export interface MigrationEvent extends MigronautEventBase {
|
|
|
531
1188
|
}
|
|
532
1189
|
|
|
533
1190
|
export interface RunStartEvent extends MigronautEventBase {
|
|
534
|
-
/** Which command started the run: 'up' | 'down' | 'redo' | 'import' | 'baseline' */
|
|
1191
|
+
/** Which command started the run: 'up' | 'down' | 'redo' | 'import' | 'baseline' | 'converge' */
|
|
535
1192
|
command?: string;
|
|
536
1193
|
direction?: 'up' | 'down';
|
|
537
1194
|
}
|
|
@@ -563,12 +1220,90 @@ export interface LockEvent extends MigronautEventBase {
|
|
|
563
1220
|
ttlMs?: number;
|
|
564
1221
|
/** How long acquisition took in ms (on `lock:acquired`) */
|
|
565
1222
|
acquireMs?: number;
|
|
1223
|
+
/**
|
|
1224
|
+
* True on a `lock:released` that came before the run ended: a converge gave
|
|
1225
|
+
* the lock up to wait for search index builds, which only reads.
|
|
1226
|
+
* @experimental New in 2.2
|
|
1227
|
+
*/
|
|
1228
|
+
early?: true;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/** Who started a converge: the `converge` call itself, or a bulk `up` (`convergeAfterUp`) */
|
|
1232
|
+
export type ConvergeTrigger = 'converge' | 'up';
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
1236
|
+
*/
|
|
1237
|
+
export interface ConvergeStartEvent extends MigronautEventBase {
|
|
1238
|
+
trigger: ConvergeTrigger;
|
|
1239
|
+
/** Declared collections being converged */
|
|
1240
|
+
collections: number;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* One step a converge carried out (or failed)
|
|
1245
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
1246
|
+
*/
|
|
1247
|
+
export interface ConvergeActionEvent extends MigronautEventBase {
|
|
1248
|
+
collection: string;
|
|
1249
|
+
target: ConvergeTarget;
|
|
1250
|
+
name: string;
|
|
1251
|
+
action: ConvergeActionKind;
|
|
1252
|
+
/**
|
|
1253
|
+
* `'started'` fires before the step runs — an index build can take hours,
|
|
1254
|
+
* and this is how a subscriber sees which one is in progress; `'applied'`
|
|
1255
|
+
* or `'failed'` follows when it ends.
|
|
1256
|
+
*/
|
|
1257
|
+
status: 'started' | 'applied' | 'failed';
|
|
1258
|
+
/** `'applied'` only */
|
|
1259
|
+
durationMs?: number;
|
|
1260
|
+
reason?: string;
|
|
1261
|
+
/** Redacted failure message (status `'failed'` only) */
|
|
1262
|
+
error?: string;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
1267
|
+
*/
|
|
1268
|
+
/**
|
|
1269
|
+
* A converge's wait for search index builds (`waitForSearchIndexes`):
|
|
1270
|
+
* `started` once, `progress` every 30 seconds, then how it ended — one of
|
|
1271
|
+
* {@link ConvergeWaitOutcome}.
|
|
1272
|
+
* @experimental New in 2.2
|
|
1273
|
+
*/
|
|
1274
|
+
export interface ConvergeWaitEvent extends MigronautEventBase {
|
|
1275
|
+
status: 'started' | 'progress' | ConvergeWaitOutcome;
|
|
1276
|
+
/** How many search indexes the wait is for */
|
|
1277
|
+
searchIndexes: number;
|
|
1278
|
+
/** `started` only — whether the migration lock was released for the wait */
|
|
1279
|
+
lockReleased?: boolean;
|
|
1280
|
+
/** `started` only — the budget (`searchIndexWaitTimeoutMs`) */
|
|
1281
|
+
timeoutMs?: number;
|
|
1282
|
+
/** Every status but `started` */
|
|
1283
|
+
waitedMs?: number;
|
|
1284
|
+
/** `failed` and `timeout` — the indexes that did not get there */
|
|
1285
|
+
notReady?: SearchIndexNotReady[];
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
export interface ConvergeEndEvent extends MigronautEventBase {
|
|
1289
|
+
trigger: ConvergeTrigger;
|
|
1290
|
+
success: boolean;
|
|
1291
|
+
durationMs: number;
|
|
1292
|
+
changed: number;
|
|
1293
|
+
inSync: boolean;
|
|
1294
|
+
/** Rows per action kind */
|
|
1295
|
+
counts: Partial<Record<ConvergeActionKind, number>>;
|
|
1296
|
+
/** The full result — partial on the failure path */
|
|
1297
|
+
result: ConvergeResult;
|
|
1298
|
+
/** Redacted failure message */
|
|
1299
|
+
error?: string;
|
|
566
1300
|
}
|
|
567
1301
|
|
|
568
1302
|
/**
|
|
569
1303
|
* Lifecycle events emitted by {@link MigratorKit}. Subscribe to feed metrics or
|
|
570
1304
|
* alerting without parsing log lines; a listener that throws is contained and
|
|
571
|
-
* never fails the run.
|
|
1305
|
+
* never fails the run. The `converge:*` events fire for real converge runs
|
|
1306
|
+
* only, not for a dry run.
|
|
572
1307
|
*/
|
|
573
1308
|
export interface MigronautEvents {
|
|
574
1309
|
'run:start': (event: RunStartEvent) => void;
|
|
@@ -580,11 +1315,19 @@ export interface MigronautEvents {
|
|
|
580
1315
|
'lock:acquired': (event: LockEvent) => void;
|
|
581
1316
|
'lock:released': (event: LockEvent) => void;
|
|
582
1317
|
'lock:lost': (event: LockEvent) => void;
|
|
1318
|
+
'converge:start': (event: ConvergeStartEvent) => void;
|
|
1319
|
+
'converge:action': (event: ConvergeActionEvent) => void;
|
|
1320
|
+
'converge:wait': (event: ConvergeWaitEvent) => void;
|
|
1321
|
+
'converge:end': (event: ConvergeEndEvent) => void;
|
|
583
1322
|
}
|
|
584
1323
|
|
|
585
1324
|
/** One check performed by {@link MigratorKit.audit} */
|
|
586
1325
|
export interface AuditCheck {
|
|
587
|
-
/**
|
|
1326
|
+
/**
|
|
1327
|
+
* e.g. 'config', 'connection', 'transactions', 'indexes', 'lock', 'checksums',
|
|
1328
|
+
* 'pending', 'ordering', 'runtime' — and 'search' when declared collections
|
|
1329
|
+
* hold search indexes
|
|
1330
|
+
*/
|
|
588
1331
|
name: string;
|
|
589
1332
|
status: 'pass' | 'warn' | 'fail';
|
|
590
1333
|
detail: string;
|
|
@@ -599,10 +1342,20 @@ export interface AuditReport {
|
|
|
599
1342
|
checks: AuditCheck[];
|
|
600
1343
|
}
|
|
601
1344
|
|
|
1345
|
+
/** Options for {@link MigratorKit.status} and {@link MigratorKit.list} */
|
|
1346
|
+
export interface StatusOptions {
|
|
1347
|
+
/** Hash applied files to fill `checksumOk`. Default true */
|
|
1348
|
+
checksums?: boolean;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
602
1351
|
/** Options for {@link MigratorKit.redo} */
|
|
603
1352
|
export interface RedoOptions {
|
|
604
1353
|
/** Skip lock acquisition (dev only) */
|
|
605
1354
|
noLock?: boolean;
|
|
1355
|
+
/** Who asked — stamped on the revert and on the re-apply */
|
|
1356
|
+
requestedBy?: string;
|
|
1357
|
+
/** Why — stamped like `requestedBy` */
|
|
1358
|
+
reason?: string;
|
|
606
1359
|
}
|
|
607
1360
|
|
|
608
1361
|
/** Options for {@link MigratorKit.create} */
|
|
@@ -720,6 +1473,19 @@ export class MigratorKit extends EventEmitter {
|
|
|
720
1473
|
* removed, or null if no lock was held.
|
|
721
1474
|
*/
|
|
722
1475
|
forceUnlock(): Promise<LockInfo | null>;
|
|
1476
|
+
/**
|
|
1477
|
+
* The batch number the next `up` would use (highest recorded batch + 1,
|
|
1478
|
+
* reverted and failed records included). A peek, not a reservation — pair it
|
|
1479
|
+
* with `up(name, { batch })` to stamp several single-file runs as one batch.
|
|
1480
|
+
* Connects if needed.
|
|
1481
|
+
*/
|
|
1482
|
+
nextBatch(): Promise<number>;
|
|
1483
|
+
/**
|
|
1484
|
+
* A new id in this kit's configured format — the `generateId` option, else a
|
|
1485
|
+
* random UUID. The same source every run id comes from, for code that wants
|
|
1486
|
+
* its own ids to match. Resolves the config; does not connect.
|
|
1487
|
+
*/
|
|
1488
|
+
generateId(): Promise<string>;
|
|
723
1489
|
/** Run all pending migrations, or a specific named file */
|
|
724
1490
|
up(filename?: string, options?: UpOptions): Promise<RunResult[]>;
|
|
725
1491
|
/** Rollback the last batch, a specific batch, a specific file, or the last N steps */
|
|
@@ -749,16 +1515,22 @@ export class MigratorKit extends EventEmitter {
|
|
|
749
1515
|
filename?: string,
|
|
750
1516
|
options?: { steps?: number; batch?: number; to?: string },
|
|
751
1517
|
): Promise<StatusRow[]>;
|
|
752
|
-
/**
|
|
753
|
-
|
|
1518
|
+
/**
|
|
1519
|
+
* Full migration status for all known files and records. `checksums: false`
|
|
1520
|
+
* skips hashing the applied files (`checksumOk` stays null).
|
|
1521
|
+
*/
|
|
1522
|
+
status(options?: StatusOptions): Promise<StatusRow[]>;
|
|
754
1523
|
/**
|
|
755
1524
|
* Read-only health check: configuration, connectivity, transaction support,
|
|
756
1525
|
* changelog indexes, lock state, checksum drift and runtime. Reports
|
|
757
1526
|
* problems; fixes none of them.
|
|
758
1527
|
*/
|
|
759
1528
|
audit(): Promise<AuditReport>;
|
|
760
|
-
/**
|
|
761
|
-
|
|
1529
|
+
/**
|
|
1530
|
+
* Filtered list of migrations. Default: 'all'. `checksums: false` skips
|
|
1531
|
+
* hashing the applied files — for a caller that needs names and dates only.
|
|
1532
|
+
*/
|
|
1533
|
+
list(filter?: 'all' | 'pending' | 'applied', options?: StatusOptions): Promise<StatusRow[]>;
|
|
762
1534
|
/** Create a new migration file and return its absolute path */
|
|
763
1535
|
create(name: string, options?: CreateOptions): Promise<string>;
|
|
764
1536
|
/** Create a migronaut config file in the working directory and return its path */
|
|
@@ -776,6 +1548,25 @@ export class MigratorKit extends EventEmitter {
|
|
|
776
1548
|
* are skipped, so a partial baseline can simply be re-run.
|
|
777
1549
|
*/
|
|
778
1550
|
baseline(options?: BaselineOptions): Promise<BaselineSummary>;
|
|
1551
|
+
/**
|
|
1552
|
+
* Bring the declared collections (`collections`, `collectionsDir`) to their
|
|
1553
|
+
* declared indexes and validators. Stateless: the live database is read and
|
|
1554
|
+
* compared on every call; what a run changed is appended to the converge
|
|
1555
|
+
* history ({@link MigratorKit.convergeHistory}), which no run reads back. A
|
|
1556
|
+
* real run holds the migration lock; a plan with a conflict is refused before
|
|
1557
|
+
* any write, and a failed step throws {@link ConvergeFailedError}. Experimental.
|
|
1558
|
+
*/
|
|
1559
|
+
converge(options?: ConvergeOptions): Promise<ConvergeResult>;
|
|
1560
|
+
/**
|
|
1561
|
+
* Whether a bulk `up` on this kit ends by converging: `convergeAfterUp` is
|
|
1562
|
+
* on and something is declared. Resolves the config; does not connect.
|
|
1563
|
+
*/
|
|
1564
|
+
convergesAfterUp(): Promise<boolean>;
|
|
1565
|
+
/**
|
|
1566
|
+
* The converge history, newest first (`limit` 1–1000, default 20): one entry
|
|
1567
|
+
* per converge that changed something or failed. Read-only.
|
|
1568
|
+
*/
|
|
1569
|
+
convergeHistory(options?: { limit?: number }): Promise<ConvergeHistoryEntry[]>;
|
|
779
1570
|
}
|
|
780
1571
|
|
|
781
1572
|
// ─── Programmatic entry points ─────────────────────────────────────────────────
|
|
@@ -799,11 +1590,24 @@ export interface RunMigrationsOptions extends MigratorKitOptions {
|
|
|
799
1590
|
* progress**. While the holder's heartbeat visibly advances its lock, the
|
|
800
1591
|
* deadline is re-armed — a healthy peer working through a long backlog never
|
|
801
1592
|
* times its waiting peers out; only a stalled holder runs this budget down.
|
|
802
|
-
* Default: 90000.
|
|
1593
|
+
* Default: 90000, or 1.5× the holder's lock TTL when that is longer — its
|
|
1594
|
+
* heartbeat only moves the lock every TTL/2, and a crashed holder's lock is
|
|
1595
|
+
* reclaimable only after a full TTL. An explicit value is used as given.
|
|
803
1596
|
*/
|
|
804
1597
|
lockWaitTimeoutMs?: number;
|
|
805
|
-
/**
|
|
1598
|
+
/**
|
|
1599
|
+
* First poll interval (ms) while waiting for the lock. Polls back off from
|
|
1600
|
+
* it, doubling, up to 5 s (and never more than a quarter of the wait budget).
|
|
1601
|
+
* Default: 500
|
|
1602
|
+
*/
|
|
806
1603
|
lockPollIntervalMs?: number;
|
|
1604
|
+
/**
|
|
1605
|
+
* Abort the call: a wait for the lock stops between polls, and a run that
|
|
1606
|
+
* holds it stops between migrations (one already executing finishes), with
|
|
1607
|
+
* a {@link RunAbortedError}. Wire it to SIGTERM so a pod being shut down
|
|
1608
|
+
* does not take the lock just before it is killed.
|
|
1609
|
+
*/
|
|
1610
|
+
signal?: AbortSignal;
|
|
807
1611
|
/**
|
|
808
1612
|
* Receives the internally-constructed {@link MigratorKit} right after
|
|
809
1613
|
* construction (before connect), so an embedding application can subscribe
|
|
@@ -822,10 +1626,12 @@ export interface MigrationSummary {
|
|
|
822
1626
|
upToDate: boolean;
|
|
823
1627
|
/** True when this instance waited for a peer to release the lock before running */
|
|
824
1628
|
waited: boolean;
|
|
825
|
-
/**
|
|
1629
|
+
/** Time (ms) from the first refusal to the run, by the clock. 0 when the lock was free */
|
|
826
1630
|
waitedMs: number;
|
|
827
1631
|
/** Number of `up` attempts made — 1 when the lock was free on the first try */
|
|
828
1632
|
attempts: number;
|
|
1633
|
+
/** The converge that ended the run — present only when `convergeAfterUp` converged */
|
|
1634
|
+
converge?: ConvergeResult;
|
|
829
1635
|
}
|
|
830
1636
|
|
|
831
1637
|
/**
|
|
@@ -849,14 +1655,17 @@ export function pendingMigrations(
|
|
|
849
1655
|
): Promise<StatusRow[]>;
|
|
850
1656
|
|
|
851
1657
|
/**
|
|
852
|
-
* The CLI's exit-code map: one entry per {@link MigronautErrorCode}, plus
|
|
1658
|
+
* The CLI's exit-code map: one entry per {@link MigronautErrorCode}, plus three
|
|
853
1659
|
* CLI-condition codes with no error class — `PENDING_MIGRATIONS` (from
|
|
854
|
-
* `status --check`)
|
|
855
|
-
*
|
|
856
|
-
* success is 0.
|
|
1660
|
+
* `status --check`), `AUDIT_FAILED` and `COLLECTIONS_DRIFT` (from
|
|
1661
|
+
* `converge --check`). Lets a wrapper script mirror the CLI's exit semantics
|
|
1662
|
+
* without hardcoding numbers. Anything unmapped exits 1; success is 0.
|
|
857
1663
|
*/
|
|
858
1664
|
export const EXIT_CODES: Readonly<
|
|
859
|
-
Record<
|
|
1665
|
+
Record<
|
|
1666
|
+
MigronautErrorCode | 'PENDING_MIGRATIONS' | 'AUDIT_FAILED' | 'COLLECTIONS_DRIFT',
|
|
1667
|
+
number
|
|
1668
|
+
>
|
|
860
1669
|
>;
|
|
861
1670
|
|
|
862
1671
|
// ─── Logger factory ───────────────────────────────────────────────────────────
|
|
@@ -1013,3 +1822,63 @@ export class IrreversibleMigrationError extends MigronautError {
|
|
|
1013
1822
|
export class OutOfOrderMigrationError extends MigronautError {
|
|
1014
1823
|
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
1015
1824
|
}
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Thrown by an `ordered` single-file `up`/`down` that would run out of
|
|
1828
|
+
* sequence: an earlier migration is still pending (`up`), or one applied later
|
|
1829
|
+
* is still applied (`down`). `context.name`, `context.direction` and
|
|
1830
|
+
* `context.blockedBy` (the migrations that must go first).
|
|
1831
|
+
*/
|
|
1832
|
+
export class MigrationBlockedError extends MigronautError {
|
|
1833
|
+
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* Thrown by the queue adapter (`@alexify/migronaut/bullmq`) when a job's
|
|
1838
|
+
* payload fails the contract check — an unknown job name or data version, a
|
|
1839
|
+
* migration name that is not a bare filename, malformed group fields. Job data
|
|
1840
|
+
* is untrusted input. `context.jobId`, `context.issue`.
|
|
1841
|
+
*/
|
|
1842
|
+
export class QueueJobInvalidError extends MigronautError {
|
|
1843
|
+
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Thrown by a queue group's `wait()` when one of its jobs failed or the wait
|
|
1848
|
+
* timed out (one budget for the whole call). `context.failedReason` is the
|
|
1849
|
+
* worker's (redacted) message, `context.code` the job's own typed error code
|
|
1850
|
+
* when it reported one (`MIGRATION_BLOCKED`, `CHECKSUM_MISMATCH`, …),
|
|
1851
|
+
* `context.results` the jobs that finished before it, plus `groupId`, `jobId`,
|
|
1852
|
+
* `migration`, `direction` and `timedOut`.
|
|
1853
|
+
*/
|
|
1854
|
+
export class QueueJobFailedError extends MigronautError {
|
|
1855
|
+
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* Thrown by {@link MigratorKit.converge} when the database cannot be brought
|
|
1860
|
+
* to the declared state. `context.phase` is:
|
|
1861
|
+
* - `'plan'` for a refused plan (`context.conflicts` lists why, with a `hint`
|
|
1862
|
+
* when Atlas Search is missing; nothing was written) — or a search index
|
|
1863
|
+
* list that could not be read before the first write (`collection`,
|
|
1864
|
+
* `target: 'searchIndex'`, `cause`, `mongoCode`, `hint`);
|
|
1865
|
+
* - `'replan'` when a collection changed while the run was under way
|
|
1866
|
+
* (`collection`, `introduced`: the new conflicts or drops; nothing of that
|
|
1867
|
+
* collection was written) — or its search index list could not be read
|
|
1868
|
+
* again (as for `'plan'`);
|
|
1869
|
+
* - `'apply'` for a failed step (`collection`, `target`, `name`, `action`,
|
|
1870
|
+
* `cause`, and `mongoCode`, `hint` and — after a failed rebuild — `restored`
|
|
1871
|
+
* when they apply) — or a search index list that could not be read to check
|
|
1872
|
+
* the steps just applied (as for `'plan'`);
|
|
1873
|
+
* - `'wait'` when `waitForSearchIndexes` gave up: `reason` is `'failed'` (the
|
|
1874
|
+
* build of a search index this run created or changed FAILED) or `'timeout'`,
|
|
1875
|
+
* with `notReady` the indexes not serving their declaration, `waitedMs`,
|
|
1876
|
+
* `timeoutMs` — or `'unreadable'`: a search index list that could not be
|
|
1877
|
+
* read (as for `'plan'`), after up to three network or failover blips in a
|
|
1878
|
+
* row. Everything was applied — only the builds were not finished.
|
|
1879
|
+
*
|
|
1880
|
+
* `context.converge` is the {@link ConvergeResult} so far.
|
|
1881
|
+
*/
|
|
1882
|
+
export class ConvergeFailedError extends MigronautError {
|
|
1883
|
+
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
1884
|
+
}
|