@alexify/migronaut 1.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +409 -1
- package/README.md +248 -24
- package/bin/migronaut.js +11 -3
- package/bullmq.d.ts +845 -0
- package/bullmq.js +1 -0
- package/index.d.ts +757 -29
- package/migronaut.schema.json +191 -1
- package/package.json +27 -6
- package/src/bullmq/index.js +55 -0
- package/src/bullmq/jobs.js +454 -0
- package/src/bullmq/processor.js +608 -0
- package/src/bullmq/producer.js +424 -0
- package/src/bullmq/service.js +653 -0
- package/src/bullmq/wait.js +124 -0
- package/src/cli/args.js +12 -2
- package/src/cli/commands/baseline.js +45 -0
- package/src/cli/commands/converge.js +160 -0
- package/src/cli/commands/down.js +2 -0
- package/src/cli/commands/lock.js +2 -1
- package/src/cli/commands/redo.js +8 -1
- package/src/cli/commands/unlock.js +12 -2
- package/src/cli/commands/up.js +14 -1
- package/src/cli/exit-codes.js +10 -2
- package/src/cli/index.js +4 -0
- package/src/cli/shared.js +29 -7
- package/src/cli/table.js +105 -0
- package/src/core/audit.js +17 -3
- package/src/core/baseline.js +80 -0
- package/src/core/changelog.js +140 -24
- package/src/core/collections.js +372 -0
- package/src/core/config.js +125 -27
- package/src/core/converge-log.js +47 -0
- package/src/core/converge-plan.js +483 -0
- package/src/core/converge.js +867 -0
- package/src/core/import-runner.js +34 -6
- package/src/core/import.js +14 -7
- package/src/core/index-spec.js +496 -0
- package/src/core/lock-wait.js +260 -0
- package/src/core/lock.js +71 -20
- package/src/core/migrator.js +805 -304
- package/src/core/options.js +251 -0
- package/src/core/run-recorder.js +157 -0
- package/src/core/run.js +71 -71
- package/src/core/runner.js +70 -20
- package/src/core/sequence.js +134 -0
- package/src/errors/index.js +71 -1
- package/src/index.js +16 -0
- package/src/utils/actor.js +48 -0
- package/src/utils/canonical.js +179 -0
- package/src/utils/collection-name.js +21 -0
- package/src/utils/error.js +18 -1
- package/src/utils/id.js +77 -0
- package/src/utils/loader.js +39 -21
- package/src/utils/logger.js +30 -12
- package/src/utils/migration-name.js +32 -0
- package/src/utils/redact.js +57 -4
- package/src/utils/sanitize.js +8 -3
- package/src/utils/telemetry.js +393 -0
- package/src/utils/template.js +60 -12
package/index.d.ts
CHANGED
|
@@ -51,14 +51,15 @@ export interface MigrationModule {
|
|
|
51
51
|
|
|
52
52
|
// ─── Changelog ────────────────────────────────────────────────────────────────
|
|
53
53
|
|
|
54
|
-
export type MigrationStatus = 'applied' | 'reverted';
|
|
54
|
+
export type MigrationStatus = 'applied' | 'reverted' | 'failed';
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Where a changelog record originated. `'migrate-mongo'` marks a record adopted
|
|
58
|
-
* via `migronaut import
|
|
59
|
-
* Absent (or `'migronaut'`)
|
|
58
|
+
* via `migronaut import`, `'baseline'` one stamped by `migronaut baseline`; both are
|
|
59
|
+
* forward-only and cannot be reverted by migronaut. Absent (or `'migronaut'`)
|
|
60
|
+
* means a natively-applied, reversible migration.
|
|
60
61
|
*/
|
|
61
|
-
export type MigrationOrigin = 'migronaut' | 'migrate-mongo';
|
|
62
|
+
export type MigrationOrigin = 'migronaut' | 'migrate-mongo' | 'baseline';
|
|
62
63
|
|
|
63
64
|
/** A single record in the _migronaut_migrations changelog collection */
|
|
64
65
|
export interface MigrationRecord {
|
|
@@ -71,6 +72,10 @@ export interface MigrationRecord {
|
|
|
71
72
|
/** When this migration was applied the *first* time; survives a re-apply */
|
|
72
73
|
firstAppliedAt?: Date;
|
|
73
74
|
revertedAt?: Date;
|
|
75
|
+
/** When the last failed attempt was recorded (status `'failed'` only) */
|
|
76
|
+
failedAt?: Date;
|
|
77
|
+
/** Redacted message of the last failed attempt (status `'failed'` only) */
|
|
78
|
+
error?: string;
|
|
74
79
|
/** Execution time in milliseconds */
|
|
75
80
|
duration: number;
|
|
76
81
|
/** SHA-256 hash of the file at time of execution */
|
|
@@ -138,6 +143,16 @@ export interface MigrationHooks {
|
|
|
138
143
|
/** File type a created migration is written as */
|
|
139
144
|
export type MigrationExtension = 'ts' | 'js';
|
|
140
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
|
+
|
|
141
156
|
/**
|
|
142
157
|
* Every **scalar** option below is also settable from the environment as
|
|
143
158
|
* `MIGRONAUT_<SCREAMING_SNAKE>` (`migrationsDir` → `MIGRONAUT_MIGRATIONS_DIR`,
|
|
@@ -147,9 +162,9 @@ export type MigrationExtension = 'ts' | 'js';
|
|
|
147
162
|
* file and are outranked by CLI flags. A value that does not parse is rejected
|
|
148
163
|
* with a {@link ConfigInvalidError} naming the variable — never coerced.
|
|
149
164
|
*
|
|
150
|
-
* `fileExtensions`, `clientOptions` and the live
|
|
151
|
-
* `hooks`, `logger`) are
|
|
152
|
-
* 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.
|
|
153
168
|
*/
|
|
154
169
|
export interface MigronautConfig {
|
|
155
170
|
/** MongoDB connection URI. Not required when `client` is supplied */
|
|
@@ -174,6 +189,12 @@ export interface MigronautConfig {
|
|
|
174
189
|
migrationsCollection: string;
|
|
175
190
|
/** Collection name for distributed lock. Default: '_migronaut_locks' */
|
|
176
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;
|
|
177
198
|
/** How long (seconds) a lock is considered stale. Default: 60 */
|
|
178
199
|
lockTTLSeconds: number;
|
|
179
200
|
/**
|
|
@@ -237,11 +258,65 @@ export interface MigronautConfig {
|
|
|
237
258
|
* - `'warn'` — log and keep going.
|
|
238
259
|
*/
|
|
239
260
|
onLockLost?: 'abort' | 'warn';
|
|
261
|
+
/**
|
|
262
|
+
* What a bulk `up` does when a pending migration sorts before the newest
|
|
263
|
+
* applied one — a file merged late from a parallel branch, which would apply
|
|
264
|
+
* out of authoring order (environments migrated at different times then
|
|
265
|
+
* disagree on the effective order).
|
|
266
|
+
*
|
|
267
|
+
* - `'warn'` (default) — log the late arrivals and apply them.
|
|
268
|
+
* - `'error'` — refuse the run with an {@link OutOfOrderMigrationError}.
|
|
269
|
+
* - `'allow'` — apply silently.
|
|
270
|
+
*
|
|
271
|
+
* A single-file `up` (an explicit, deliberate target) is never checked.
|
|
272
|
+
*/
|
|
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;
|
|
240
295
|
/** Mongoose instance — required only if your migrations use Mongoose models */
|
|
241
296
|
mongoose?: MongooseLike;
|
|
242
297
|
hooks?: MigrationHooks;
|
|
243
298
|
/** Custom logger — set to null to silence all output (useful in tests) */
|
|
244
299
|
logger?: MigronautLogger | null;
|
|
300
|
+
/**
|
|
301
|
+
* Your own identifier format (ULID, CUID, UUIDv7, …) for every id migronaut
|
|
302
|
+
* mints: the run id — stamped on changelog records, events and log lines,
|
|
303
|
+
* and stored as the lock's owner token — and, through
|
|
304
|
+
* `@alexify/migronaut/bullmq`, the group id of an enqueue call.
|
|
305
|
+
* Default: `crypto.randomUUID()`.
|
|
306
|
+
*
|
|
307
|
+
* Ids are for correlation. The lock adds a token of its own, so a generator
|
|
308
|
+
* that repeats a value blurs which run wrote what but never lets two runs
|
|
309
|
+
* hold the lock at once.
|
|
310
|
+
*/
|
|
311
|
+
generateId?: IdGenerator;
|
|
312
|
+
/**
|
|
313
|
+
* OpenTelemetry, from your own `@opentelemetry/api`: a tracer, a meter, or
|
|
314
|
+
* both. Every run and every migration becomes a span — the migration's span
|
|
315
|
+
* is the active one while its `up`/`down` runs, so an instrumented MongoDB
|
|
316
|
+
* driver nests its command spans under it — and their durations are
|
|
317
|
+
* recorded as histograms. Absent, `null` or empty turns it off.
|
|
318
|
+
*/
|
|
319
|
+
telemetry?: MigronautTelemetry | null;
|
|
245
320
|
}
|
|
246
321
|
|
|
247
322
|
/**
|
|
@@ -257,6 +332,237 @@ export type MigronautConfigInput =
|
|
|
257
332
|
| Partial<MigronautConfig>
|
|
258
333
|
| (() => Partial<MigronautConfig> | Promise<Partial<MigronautConfig>>);
|
|
259
334
|
|
|
335
|
+
// ─── Collections (converge) ───────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
/** An index key direction: ascending, descending, or a special index type */
|
|
338
|
+
export type IndexKeyDirection = 1 | -1 | 'text' | 'hashed' | '2d' | '2dsphere';
|
|
339
|
+
|
|
340
|
+
/** Collation of an index — `locale` required, the rest as MongoDB defines them */
|
|
341
|
+
export interface IndexCollation {
|
|
342
|
+
locale: string;
|
|
343
|
+
caseLevel?: boolean;
|
|
344
|
+
caseFirst?: 'upper' | 'lower' | 'off';
|
|
345
|
+
strength?: 1 | 2 | 3 | 4 | 5;
|
|
346
|
+
numericOrdering?: boolean;
|
|
347
|
+
alternate?: 'non-ignorable' | 'shifted';
|
|
348
|
+
maxVariable?: 'punct' | 'space';
|
|
349
|
+
backwards?: boolean;
|
|
350
|
+
normalization?: boolean;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* One declared index, in the driver's own flat `createIndexes` shape. Every
|
|
355
|
+
* option is checked: an unknown one is a {@link ConfigInvalidError} rather
|
|
356
|
+
* than dropped, because the driver drops it silently and the index would be
|
|
357
|
+
* built without it.
|
|
358
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
359
|
+
*/
|
|
360
|
+
export interface IndexDefinition {
|
|
361
|
+
/**
|
|
362
|
+
* Field → direction, in index order. A compound key with an integer-like
|
|
363
|
+
* field name must be a `Map` (a plain object reorders such names), with that
|
|
364
|
+
* field first — the live key is read back as a plain object.
|
|
365
|
+
*/
|
|
366
|
+
key: Record<string, IndexKeyDirection> | Map<string, IndexKeyDirection>;
|
|
367
|
+
/** Defaults to the name MongoDB generates: `email_1`, `a_1_b_-1` */
|
|
368
|
+
name?: string;
|
|
369
|
+
unique?: boolean;
|
|
370
|
+
sparse?: boolean;
|
|
371
|
+
/** Changed in place (`collMod`) — no rebuild */
|
|
372
|
+
hidden?: boolean;
|
|
373
|
+
/** TTL in seconds. Changed in place when the live index already has one */
|
|
374
|
+
expireAfterSeconds?: number;
|
|
375
|
+
partialFilterExpression?: Record<string, unknown>;
|
|
376
|
+
collation?: IndexCollation;
|
|
377
|
+
/** For a wildcard (`$**`) index */
|
|
378
|
+
wildcardProjection?: Record<string, 0 | 1 | boolean>;
|
|
379
|
+
/** Text index field weights (default 1) */
|
|
380
|
+
weights?: Record<string, number>;
|
|
381
|
+
default_language?: string;
|
|
382
|
+
language_override?: string;
|
|
383
|
+
textIndexVersion?: number;
|
|
384
|
+
'2dsphereIndexVersion'?: number;
|
|
385
|
+
bits?: number;
|
|
386
|
+
min?: number;
|
|
387
|
+
max?: number;
|
|
388
|
+
storageEngine?: Record<string, unknown>;
|
|
389
|
+
/** Accepted and ignored — a no-op since MongoDB 4.2 */
|
|
390
|
+
background?: boolean;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export type ValidationLevel = 'off' | 'strict' | 'moderate';
|
|
394
|
+
export type ValidationAction = 'error' | 'warn' | 'errorAndLog';
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* A declared collection: the end state `converge()` keeps it in. Leave
|
|
398
|
+
* `indexes` or `validator` out to leave that part unmanaged.
|
|
399
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
400
|
+
*/
|
|
401
|
+
export interface CollectionDefinition {
|
|
402
|
+
name: string;
|
|
403
|
+
/**
|
|
404
|
+
* Every index besides `_id`. Undeclared live indexes are kept (and reported)
|
|
405
|
+
* unless `prune` is on.
|
|
406
|
+
*/
|
|
407
|
+
indexes?: IndexDefinition[];
|
|
408
|
+
/** A query or `{ $jsonSchema }` document; `null` (or `{}`) for no validator */
|
|
409
|
+
validator?: Record<string, unknown> | null;
|
|
410
|
+
/** Default: 'strict'. Only with a validator */
|
|
411
|
+
validationLevel?: ValidationLevel;
|
|
412
|
+
/** Default: 'error'. Only with a validator */
|
|
413
|
+
validationAction?: ValidationAction;
|
|
414
|
+
/** Drop live indexes this definition does not declare. Default: the call's `prune`, else false */
|
|
415
|
+
prune?: boolean;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** What a `collectionsDir` file exports: a definition whose name defaults to the file name */
|
|
419
|
+
export type CollectionDefinitionFile = Omit<CollectionDefinition, 'name'> & { name?: string };
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Options for {@link MigratorKit.converge}
|
|
423
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
424
|
+
*/
|
|
425
|
+
export interface ConvergeOptions {
|
|
426
|
+
/** Plan without writing: no lock, no events. The result's rows are `'planned'` */
|
|
427
|
+
dryRun?: boolean;
|
|
428
|
+
/** Drop undeclared indexes in collections whose definition does not set `prune` */
|
|
429
|
+
prune?: boolean;
|
|
430
|
+
/** Skip lock acquisition (dev only) */
|
|
431
|
+
noLock?: boolean;
|
|
432
|
+
/**
|
|
433
|
+
* Refuse ({@link MigrationBlockedError}) while any migration is still
|
|
434
|
+
* pending — checked under the lock. How the queue adapter runs a converge
|
|
435
|
+
* as the tail of a deploy.
|
|
436
|
+
*/
|
|
437
|
+
ordered?: boolean;
|
|
438
|
+
/**
|
|
439
|
+
* Allow a rebuild that drops a unique index and builds a unique one back.
|
|
440
|
+
* Without it such a rebuild plans as a `conflict`: the constraint is gone
|
|
441
|
+
* until the new index is built, and a duplicate written in between leaves
|
|
442
|
+
* neither index buildable. The after-up hook and queue jobs never set it.
|
|
443
|
+
* CLI: `--rebuild-unique`.
|
|
444
|
+
*/
|
|
445
|
+
rebuildUnique?: boolean;
|
|
446
|
+
/** Who asked for this converge — recorded in the converge history */
|
|
447
|
+
requestedBy?: string;
|
|
448
|
+
/** Why — recorded in the converge history */
|
|
449
|
+
reason?: string;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export type ConvergeTarget = 'collection' | 'validator' | 'index';
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* What converge does to one target. `keep` is an undeclared index left alone
|
|
456
|
+
* (prune off); `conflict` refuses the run — an undeclared index covers the
|
|
457
|
+
* declared one's key under another name, a unique index would be rebuilt
|
|
458
|
+
* without {@link ConvergeOptions.rebuildUnique}, or the collection is a view
|
|
459
|
+
* or a time-series collection.
|
|
460
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
461
|
+
*/
|
|
462
|
+
export type ConvergeActionKind =
|
|
463
|
+
| 'create'
|
|
464
|
+
| 'modify'
|
|
465
|
+
| 'recreate'
|
|
466
|
+
| 'drop'
|
|
467
|
+
| 'keep'
|
|
468
|
+
| 'unchanged'
|
|
469
|
+
| 'conflict';
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* `planned` in a dry run; otherwise `applied`, `failed`, or `skipped` — no
|
|
473
|
+
* change was needed, or the run stopped before reaching it.
|
|
474
|
+
*/
|
|
475
|
+
export type ConvergeActionStatus = 'planned' | 'applied' | 'failed' | 'skipped';
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* One row of a converge result
|
|
479
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
480
|
+
*/
|
|
481
|
+
export interface ConvergeAction {
|
|
482
|
+
target: ConvergeTarget;
|
|
483
|
+
/** The index name; the collection name for a `collection` or `validator` row */
|
|
484
|
+
name: string;
|
|
485
|
+
action: ConvergeActionKind;
|
|
486
|
+
status: ConvergeActionStatus;
|
|
487
|
+
/** What differs (`'unique, expireAfterSeconds'`), or why a row is what it is */
|
|
488
|
+
reason?: string;
|
|
489
|
+
/** The live index the row refers to when its name differs from the declared one */
|
|
490
|
+
liveName?: string;
|
|
491
|
+
durationMs?: number;
|
|
492
|
+
/**
|
|
493
|
+
* What is there now — the live index (`{ key, name, ...options }`) or
|
|
494
|
+
* validator (`{ validator, validationLevel, validationAction }`) — on rows
|
|
495
|
+
* that change or drop it, and on `keep` rows. Plain JSON.
|
|
496
|
+
*/
|
|
497
|
+
from?: Record<string, unknown>;
|
|
498
|
+
/** What the row puts there — the declared index or validator — on rows that create or change it */
|
|
499
|
+
to?: Record<string, unknown>;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
504
|
+
*/
|
|
505
|
+
export interface CollectionConvergeResult {
|
|
506
|
+
name: string;
|
|
507
|
+
actions: ConvergeAction[];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* One entry of the converge history (`convergeLogCollection`): a converge that
|
|
512
|
+
* changed something or failed.
|
|
513
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
514
|
+
*/
|
|
515
|
+
export interface ConvergeHistoryEntry {
|
|
516
|
+
runId?: string;
|
|
517
|
+
/** `'converge'`, or `'up'` for the converge that ended a bulk `up` */
|
|
518
|
+
trigger: ConvergeTrigger;
|
|
519
|
+
startedAt: Date;
|
|
520
|
+
finishedAt: Date;
|
|
521
|
+
durationMs: number;
|
|
522
|
+
success: boolean;
|
|
523
|
+
/** Redacted failure message (`success: false` only) */
|
|
524
|
+
error?: string;
|
|
525
|
+
executedBy: string;
|
|
526
|
+
host: string;
|
|
527
|
+
environment: string;
|
|
528
|
+
requestedBy?: string;
|
|
529
|
+
reason?: string;
|
|
530
|
+
/** Changes applied */
|
|
531
|
+
changed: number;
|
|
532
|
+
/** The rows that changed, failed or refused the run — each with its collection and `from` / `to` */
|
|
533
|
+
actions: Array<ConvergeAction & { collection: string }>;
|
|
534
|
+
unstable?: ConvergeUnstable[];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Something applied that still compares as changed — reported, never rebuilt in a loop
|
|
539
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
540
|
+
*/
|
|
541
|
+
export interface ConvergeUnstable {
|
|
542
|
+
collection: string;
|
|
543
|
+
target: ConvergeTarget;
|
|
544
|
+
name: string;
|
|
545
|
+
action: ConvergeActionKind;
|
|
546
|
+
reason?: string;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Outcome of {@link MigratorKit.converge}
|
|
551
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
552
|
+
*/
|
|
553
|
+
export interface ConvergeResult {
|
|
554
|
+
dryRun: boolean;
|
|
555
|
+
/** Changes applied — or, in a dry run, changes the run would make */
|
|
556
|
+
changed: number;
|
|
557
|
+
/**
|
|
558
|
+
* True when the database matches the declarations: nothing left to do and
|
|
559
|
+
* no conflict. Undeclared indexes kept with prune off do not count against it.
|
|
560
|
+
*/
|
|
561
|
+
inSync: boolean;
|
|
562
|
+
collections: CollectionConvergeResult[];
|
|
563
|
+
unstable?: ConvergeUnstable[];
|
|
564
|
+
}
|
|
565
|
+
|
|
260
566
|
// ─── Logger ───────────────────────────────────────────────────────────────────
|
|
261
567
|
|
|
262
568
|
/**
|
|
@@ -285,11 +591,97 @@ export interface MigronautLogger {
|
|
|
285
591
|
*/
|
|
286
592
|
export type LogMethod = (msg: string, fields?: Record<string, unknown>) => void;
|
|
287
593
|
|
|
594
|
+
// ─── Telemetry ────────────────────────────────────────────────────────────────
|
|
595
|
+
|
|
596
|
+
/** A span or metric attribute value — the scalar subset migronaut sets */
|
|
597
|
+
export type MigronautAttributes = Record<string, string | number | boolean>;
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* The slice of an OpenTelemetry `Span` migronaut calls. Declared structurally —
|
|
601
|
+
* `@opentelemetry/api` is deliberately not imported, so the package's types
|
|
602
|
+
* resolve for users who never installed it. A real `Span` satisfies it.
|
|
603
|
+
*/
|
|
604
|
+
export interface MigronautSpan {
|
|
605
|
+
setAttribute(key: string, value: string | number | boolean): unknown;
|
|
606
|
+
/** `code` is OpenTelemetry's `SpanStatusCode` — migronaut only ever sets ERROR (2) */
|
|
607
|
+
setStatus(status: { code: number; message?: string }): unknown;
|
|
608
|
+
end(): void;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* The slice of an OpenTelemetry `Tracer` migronaut calls — what
|
|
613
|
+
* `trace.getTracer('@alexify/migronaut')` returns. Only `startActiveSpan` is
|
|
614
|
+
* used: it is what makes a span the active context for the migration's own
|
|
615
|
+
* code, and so for any instrumentation running underneath it.
|
|
616
|
+
*/
|
|
617
|
+
export interface MigronautTracer {
|
|
618
|
+
startActiveSpan<T>(
|
|
619
|
+
name: string,
|
|
620
|
+
options: { attributes?: MigronautAttributes },
|
|
621
|
+
fn: (span: MigronautSpan) => T,
|
|
622
|
+
): T;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** An OpenTelemetry `Histogram`, as far as migronaut uses one */
|
|
626
|
+
export interface MigronautHistogram {
|
|
627
|
+
record(value: number, attributes?: MigronautAttributes): void;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** An OpenTelemetry `Counter`, as far as migronaut uses one */
|
|
631
|
+
export interface MigronautCounter {
|
|
632
|
+
add(value: number, attributes?: MigronautAttributes): void;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Options migronaut passes when it creates an instrument */
|
|
636
|
+
export interface MigronautMetricOptions {
|
|
637
|
+
description?: string;
|
|
638
|
+
unit?: string;
|
|
639
|
+
/** Histogram bucket boundaries, in the instrument's unit (seconds) */
|
|
640
|
+
advice?: { explicitBucketBoundaries?: number[] };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* The slice of an OpenTelemetry `Meter` migronaut calls — what
|
|
645
|
+
* `metrics.getMeter('@alexify/migronaut')` returns.
|
|
646
|
+
*/
|
|
647
|
+
export interface MigronautMeter {
|
|
648
|
+
createHistogram(name: string, options?: MigronautMetricOptions): MigronautHistogram;
|
|
649
|
+
createCounter(name: string, options?: MigronautMetricOptions): MigronautCounter;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* The `telemetry` config option. Both parts are optional and independent:
|
|
654
|
+
* a tracer alone gives spans, a meter alone gives metrics.
|
|
655
|
+
*
|
|
656
|
+
* Spans: `migronaut.run` (one per run that held the lock) and
|
|
657
|
+
* `migronaut.migration` (one per migration executed, a child of the run).
|
|
658
|
+
* Metrics: `migronaut.run.duration`, `migronaut.migration.duration` and
|
|
659
|
+
* `migronaut.lock.acquire.duration` (histograms, seconds), plus the counters
|
|
660
|
+
* `migronaut.lock.refused` and `migronaut.lock.lost`. A failure sets the span's
|
|
661
|
+
* status to ERROR with a redacted message, and `error.type` — on the span and
|
|
662
|
+
* the metric point — to the {@link MigronautErrorCode}, or for an error that is
|
|
663
|
+
* not migronaut's to its class name (`_OTHER` when it has none).
|
|
664
|
+
*
|
|
665
|
+
* A tracer or meter that throws never fails a run.
|
|
666
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
667
|
+
*/
|
|
668
|
+
export interface MigronautTelemetry {
|
|
669
|
+
tracer?: MigronautTracer | null;
|
|
670
|
+
meter?: MigronautMeter | null;
|
|
671
|
+
/**
|
|
672
|
+
* Static attributes added to every span and every metric point — your own
|
|
673
|
+
* low-cardinality dimensions (`{ tenant: 'acme' }`). At most 20. They cannot
|
|
674
|
+
* replace migronaut's own: `db.namespace` (the database name, always
|
|
675
|
+
* present) and the `migronaut.*` attributes win.
|
|
676
|
+
*/
|
|
677
|
+
attributes?: Record<string, string | number | boolean>;
|
|
678
|
+
}
|
|
679
|
+
|
|
288
680
|
// ─── Progress Reporter ─────────────────────────────────────────────────────────
|
|
289
681
|
|
|
290
682
|
/**
|
|
291
|
-
* Receives migration lifecycle callbacks so a presentation layer (e.g.
|
|
292
|
-
* spinner) can react. Deliberately separate from {@link MigrationHooks}: hooks
|
|
683
|
+
* Receives migration lifecycle callbacks so a presentation layer (e.g. a
|
|
684
|
+
* progress spinner) can react. Deliberately separate from {@link MigrationHooks}: hooks
|
|
293
685
|
* run user DB logic inside the migration; this only drives a UI indicator and
|
|
294
686
|
* never touches the database.
|
|
295
687
|
*/
|
|
@@ -318,19 +710,59 @@ export interface RunResult {
|
|
|
318
710
|
|
|
319
711
|
export interface StatusRow {
|
|
320
712
|
file: string;
|
|
321
|
-
|
|
713
|
+
/**
|
|
714
|
+
* `'failed'` marks a recorded failed attempt — the file still counts as
|
|
715
|
+
* pending for every run path (the next `up` retries it), but the failure is
|
|
716
|
+
* surfaced instead of rendering as a plain pending row. A reverted record
|
|
717
|
+
* reports as `'pending'`, with `revertedAt` carrying its history.
|
|
718
|
+
*/
|
|
719
|
+
status: 'applied' | 'pending' | 'failed';
|
|
322
720
|
batch: number | null;
|
|
323
721
|
appliedAt: Date | null;
|
|
324
722
|
duration: number | null;
|
|
325
723
|
/** null = never applied, true = match, false = mismatch */
|
|
326
724
|
checksumOk: boolean | null;
|
|
327
725
|
description?: string;
|
|
726
|
+
/** Who ran it — from the changelog's audit trail, when recorded */
|
|
727
|
+
executedBy?: string;
|
|
728
|
+
/** Environment stamped at apply time, when recorded */
|
|
729
|
+
environment?: string;
|
|
730
|
+
/** Correlation id of the run that wrote the record, when recorded */
|
|
731
|
+
runId?: string;
|
|
732
|
+
/** When the migration was reverted — present on reverted history rows */
|
|
733
|
+
revertedAt?: Date;
|
|
734
|
+
/** `'migrate-mongo'` / `'baseline'` mark forward-only adopted records */
|
|
735
|
+
origin?: MigrationOrigin;
|
|
736
|
+
/** Redacted message of the last failed attempt (status `'failed'` only) */
|
|
737
|
+
error?: string;
|
|
738
|
+
/** When the last failed attempt was recorded (status `'failed'` only) */
|
|
739
|
+
failedAt?: Date;
|
|
740
|
+
/**
|
|
741
|
+
* Present (true) on a not-yet-applied row that sorts before the newest
|
|
742
|
+
* applied migration — a file merged late from a parallel branch, which will
|
|
743
|
+
* apply out of authoring order. See `MigronautConfig.onOutOfOrder`.
|
|
744
|
+
*/
|
|
745
|
+
outOfOrder?: true;
|
|
328
746
|
/**
|
|
329
747
|
* Present (true) when the changelog record's name is not a plain filename —
|
|
330
748
|
* a legacy or tampered record. The row is reported as-is instead of failing
|
|
331
749
|
* the whole status/audit call.
|
|
332
750
|
*/
|
|
333
751
|
invalid?: true;
|
|
752
|
+
/**
|
|
753
|
+
* The file's current checksum (SHA-256 hex) — on `dryRun('up')` rows, so a
|
|
754
|
+
* caller that applies them later can insist on exactly this version
|
|
755
|
+
* (`up(file, { checksum })`).
|
|
756
|
+
*/
|
|
757
|
+
checksum?: string;
|
|
758
|
+
/** Who asked for the apply, and why — when the run said (`requestedBy` / `reason` options) */
|
|
759
|
+
requestedBy?: string;
|
|
760
|
+
reason?: string;
|
|
761
|
+
/** Who asked for the revert, and why — on reverted history rows */
|
|
762
|
+
revertRequestedBy?: string;
|
|
763
|
+
revertReason?: string;
|
|
764
|
+
/** The checksum of the file version that failed (status `'failed'` only) */
|
|
765
|
+
failedChecksum?: string;
|
|
334
766
|
}
|
|
335
767
|
|
|
336
768
|
// ─── Import (migrate-mongo adoption) ────────────────────────────────────────────
|
|
@@ -388,6 +820,13 @@ export interface LockInfo {
|
|
|
388
820
|
host: string;
|
|
389
821
|
/** Username of the holder */
|
|
390
822
|
executedBy: string;
|
|
823
|
+
/**
|
|
824
|
+
* The holder's run id — the `runId` of its events, log lines and changelog
|
|
825
|
+
* records. Absent for a document written by hand.
|
|
826
|
+
*/
|
|
827
|
+
runId?: string;
|
|
828
|
+
/** The holder's lock TTL (ms), which paces its heartbeat. Absent before 2.1 */
|
|
829
|
+
ttlMs?: number;
|
|
391
830
|
}
|
|
392
831
|
|
|
393
832
|
// ─── Error Codes ──────────────────────────────────────────────────────────────
|
|
@@ -411,7 +850,12 @@ export type MigronautErrorCode =
|
|
|
411
850
|
| 'CONNECTION_FAILED'
|
|
412
851
|
| 'NOT_APPLIED'
|
|
413
852
|
| 'IMPORT_TARGET_NOT_EMPTY'
|
|
414
|
-
| 'MIGRATION_IRREVERSIBLE'
|
|
853
|
+
| 'MIGRATION_IRREVERSIBLE'
|
|
854
|
+
| 'MIGRATION_OUT_OF_ORDER'
|
|
855
|
+
| 'MIGRATION_BLOCKED'
|
|
856
|
+
| 'QUEUE_JOB_INVALID'
|
|
857
|
+
| 'QUEUE_JOB_FAILED'
|
|
858
|
+
| 'CONVERGE_FAILED';
|
|
415
859
|
|
|
416
860
|
// ─── Config file format ─────────────────────────────────────────────────────────
|
|
417
861
|
|
|
@@ -443,6 +887,44 @@ export interface UpOptions {
|
|
|
443
887
|
* Mutually exclusive with a filename and `steps`.
|
|
444
888
|
*/
|
|
445
889
|
to?: string;
|
|
890
|
+
/**
|
|
891
|
+
* Stamp this batch number on what the run applies, instead of the next free
|
|
892
|
+
* one ({@link MigratorKit.nextBatch}). A label, not a reservation: it may
|
|
893
|
+
* equal a batch already in use, which is how several single-file runs become
|
|
894
|
+
* one rollback unit — the queue adapter gives every job of an enqueue group
|
|
895
|
+
* the same value. Positive integer; mutually exclusive with `step`.
|
|
896
|
+
*/
|
|
897
|
+
batch?: number;
|
|
898
|
+
/**
|
|
899
|
+
* Refuse ({@link MigrationBlockedError}) to apply the named file while an
|
|
900
|
+
* earlier file on disk is still pending — the invariant a bulk `up` gets by
|
|
901
|
+
* construction, enforced from the changelog rather than from the caller's
|
|
902
|
+
* memory. Also makes the single-file run honour `strict` drift checks and
|
|
903
|
+
* `onOutOfOrder` like a bulk run. Requires a filename.
|
|
904
|
+
*/
|
|
905
|
+
ordered?: boolean;
|
|
906
|
+
/**
|
|
907
|
+
* The SHA-256 (hex) the named file must have — refuse ({@link
|
|
908
|
+
* ChecksumMismatchError}, `context.planned: true`) to apply any other
|
|
909
|
+
* version of it. A queue job carries the checksum its plan saw, so a worker
|
|
910
|
+
* from another deploy never applies a different file under the same name.
|
|
911
|
+
* Requires a filename; an already-applied file is skipped as usual.
|
|
912
|
+
*/
|
|
913
|
+
checksum?: string;
|
|
914
|
+
/**
|
|
915
|
+
* Converge the declared collections after the migrations, under the same
|
|
916
|
+
* lock — overrides `convergeAfterUp` for this call. Bulk runs only: refused
|
|
917
|
+
* with a filename or `to`.
|
|
918
|
+
*/
|
|
919
|
+
converge?: boolean;
|
|
920
|
+
/**
|
|
921
|
+
* Who asked for this run (≤ 128 characters) — stamped on the changelog
|
|
922
|
+
* records it writes. `executedBy` is the OS user that ran it; on a queue
|
|
923
|
+
* worker that is the container's, which is why the requester is separate.
|
|
924
|
+
*/
|
|
925
|
+
requestedBy?: string;
|
|
926
|
+
/** Why (≤ 512 characters) — a ticket, a sentence; stamped like `requestedBy` */
|
|
927
|
+
reason?: string;
|
|
446
928
|
}
|
|
447
929
|
|
|
448
930
|
/** Options for {@link MigratorKit.down} */
|
|
@@ -463,6 +945,20 @@ export interface DownOptions {
|
|
|
463
945
|
* to the same state. Mutually exclusive with `batch`, `steps` and a filename.
|
|
464
946
|
*/
|
|
465
947
|
to?: string;
|
|
948
|
+
/**
|
|
949
|
+
* Refuse ({@link MigrationBlockedError}) to revert the named file while a
|
|
950
|
+
* migration applied *after* it is still applied — reverts must go newest
|
|
951
|
+
* first (by `appliedAt`, the order `steps` uses). Requires a filename.
|
|
952
|
+
*/
|
|
953
|
+
ordered?: boolean;
|
|
954
|
+
/**
|
|
955
|
+
* Who asked for this run (≤ 128 characters) — stamped on the records it
|
|
956
|
+
* reverts (`revertRequestedBy`). `executedBy` is the OS user that ran it; on a queue
|
|
957
|
+
* worker that is the container's, which is why the requester is separate.
|
|
958
|
+
*/
|
|
959
|
+
requestedBy?: string;
|
|
960
|
+
/** Why (≤ 512 characters) — a ticket, a sentence; stamped as `revertReason` */
|
|
961
|
+
reason?: string;
|
|
466
962
|
}
|
|
467
963
|
|
|
468
964
|
/** Payload common to every lifecycle event */
|
|
@@ -486,7 +982,7 @@ export interface MigrationEvent extends MigronautEventBase {
|
|
|
486
982
|
}
|
|
487
983
|
|
|
488
984
|
export interface RunStartEvent extends MigronautEventBase {
|
|
489
|
-
/** Which command started the run: 'up' | 'down' | 'redo' | 'import' */
|
|
985
|
+
/** Which command started the run: 'up' | 'down' | 'redo' | 'import' | 'baseline' | 'converge' */
|
|
490
986
|
command?: string;
|
|
491
987
|
direction?: 'up' | 'down';
|
|
492
988
|
}
|
|
@@ -520,10 +1016,62 @@ export interface LockEvent extends MigronautEventBase {
|
|
|
520
1016
|
acquireMs?: number;
|
|
521
1017
|
}
|
|
522
1018
|
|
|
1019
|
+
/** Who started a converge: the `converge` call itself, or a bulk `up` (`convergeAfterUp`) */
|
|
1020
|
+
export type ConvergeTrigger = 'converge' | 'up';
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
1024
|
+
*/
|
|
1025
|
+
export interface ConvergeStartEvent extends MigronautEventBase {
|
|
1026
|
+
trigger: ConvergeTrigger;
|
|
1027
|
+
/** Declared collections being converged */
|
|
1028
|
+
collections: number;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* One step a converge carried out (or failed)
|
|
1033
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
1034
|
+
*/
|
|
1035
|
+
export interface ConvergeActionEvent extends MigronautEventBase {
|
|
1036
|
+
collection: string;
|
|
1037
|
+
target: ConvergeTarget;
|
|
1038
|
+
name: string;
|
|
1039
|
+
action: ConvergeActionKind;
|
|
1040
|
+
/**
|
|
1041
|
+
* `'started'` fires before the step runs — an index build can take hours,
|
|
1042
|
+
* and this is how a subscriber sees which one is in progress; `'applied'`
|
|
1043
|
+
* or `'failed'` follows when it ends.
|
|
1044
|
+
*/
|
|
1045
|
+
status: 'started' | 'applied' | 'failed';
|
|
1046
|
+
/** `'applied'` only */
|
|
1047
|
+
durationMs?: number;
|
|
1048
|
+
reason?: string;
|
|
1049
|
+
/** Redacted failure message (status `'failed'` only) */
|
|
1050
|
+
error?: string;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
1055
|
+
*/
|
|
1056
|
+
export interface ConvergeEndEvent extends MigronautEventBase {
|
|
1057
|
+
trigger: ConvergeTrigger;
|
|
1058
|
+
success: boolean;
|
|
1059
|
+
durationMs: number;
|
|
1060
|
+
changed: number;
|
|
1061
|
+
inSync: boolean;
|
|
1062
|
+
/** Rows per action kind */
|
|
1063
|
+
counts: Partial<Record<ConvergeActionKind, number>>;
|
|
1064
|
+
/** The full result — partial on the failure path */
|
|
1065
|
+
result: ConvergeResult;
|
|
1066
|
+
/** Redacted failure message */
|
|
1067
|
+
error?: string;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
523
1070
|
/**
|
|
524
1071
|
* Lifecycle events emitted by {@link MigratorKit}. Subscribe to feed metrics or
|
|
525
1072
|
* alerting without parsing log lines; a listener that throws is contained and
|
|
526
|
-
* never fails the run.
|
|
1073
|
+
* never fails the run. The `converge:*` events fire for real converge runs
|
|
1074
|
+
* only, not for a dry run.
|
|
527
1075
|
*/
|
|
528
1076
|
export interface MigronautEvents {
|
|
529
1077
|
'run:start': (event: RunStartEvent) => void;
|
|
@@ -535,6 +1083,9 @@ export interface MigronautEvents {
|
|
|
535
1083
|
'lock:acquired': (event: LockEvent) => void;
|
|
536
1084
|
'lock:released': (event: LockEvent) => void;
|
|
537
1085
|
'lock:lost': (event: LockEvent) => void;
|
|
1086
|
+
'converge:start': (event: ConvergeStartEvent) => void;
|
|
1087
|
+
'converge:action': (event: ConvergeActionEvent) => void;
|
|
1088
|
+
'converge:end': (event: ConvergeEndEvent) => void;
|
|
538
1089
|
}
|
|
539
1090
|
|
|
540
1091
|
/** One check performed by {@link MigratorKit.audit} */
|
|
@@ -554,10 +1105,20 @@ export interface AuditReport {
|
|
|
554
1105
|
checks: AuditCheck[];
|
|
555
1106
|
}
|
|
556
1107
|
|
|
1108
|
+
/** Options for {@link MigratorKit.status} and {@link MigratorKit.list} */
|
|
1109
|
+
export interface StatusOptions {
|
|
1110
|
+
/** Hash applied files to fill `checksumOk`. Default true */
|
|
1111
|
+
checksums?: boolean;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
557
1114
|
/** Options for {@link MigratorKit.redo} */
|
|
558
1115
|
export interface RedoOptions {
|
|
559
1116
|
/** Skip lock acquisition (dev only) */
|
|
560
1117
|
noLock?: boolean;
|
|
1118
|
+
/** Who asked — stamped on the revert and on the re-apply */
|
|
1119
|
+
requestedBy?: string;
|
|
1120
|
+
/** Why — stamped like `requestedBy` */
|
|
1121
|
+
reason?: string;
|
|
561
1122
|
}
|
|
562
1123
|
|
|
563
1124
|
/** Options for {@link MigratorKit.create} */
|
|
@@ -585,6 +1146,24 @@ export interface InitOptions {
|
|
|
585
1146
|
secretProvider?: boolean;
|
|
586
1147
|
}
|
|
587
1148
|
|
|
1149
|
+
/** Options for {@link MigratorKit.baseline} */
|
|
1150
|
+
export interface BaselineOptions {
|
|
1151
|
+
/** Baseline pending files up to and including this one, instead of all */
|
|
1152
|
+
to?: string;
|
|
1153
|
+
/** Skip lock acquisition (dev only) */
|
|
1154
|
+
noLock?: boolean;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** Outcome of a {@link MigratorKit.baseline} call */
|
|
1158
|
+
export interface BaselineSummary {
|
|
1159
|
+
/** Files marked applied by this call, in name order */
|
|
1160
|
+
baselined: string[];
|
|
1161
|
+
/** Files on disk that were already applied (or beyond `--to`) and untouched */
|
|
1162
|
+
skipped: number;
|
|
1163
|
+
/** The shared batch number stamped on the baselined records; null when none */
|
|
1164
|
+
batch: number | null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
588
1167
|
/** Options for {@link MigratorKit.import} */
|
|
589
1168
|
export interface ImportOptions {
|
|
590
1169
|
/** Source collection to read. Default: `changelog` (migrate-mongo's default) */
|
|
@@ -605,9 +1184,16 @@ export interface ImportOptions {
|
|
|
605
1184
|
export interface MigratorKitOptions {
|
|
606
1185
|
/** Explicit config file path — overrides auto-discovery */
|
|
607
1186
|
configPath?: string;
|
|
1187
|
+
/**
|
|
1188
|
+
* Project root this instance resolves against: config-file discovery, the
|
|
1189
|
+
* `.env` file and a relative `migrationsDir`. Defaults to `process.cwd()`.
|
|
1190
|
+
* Set it when one process hosts kits for several projects, so their
|
|
1191
|
+
* relative paths stop sharing one global working directory.
|
|
1192
|
+
*/
|
|
1193
|
+
cwd?: string;
|
|
608
1194
|
/**
|
|
609
1195
|
* Optional lifecycle reporter, invoked around each migration's execution so a
|
|
610
|
-
* UI (the CLI's
|
|
1196
|
+
* UI (the CLI's spinner) can show progress. Core never imports a spinner
|
|
611
1197
|
* library — it only calls these callbacks.
|
|
612
1198
|
*/
|
|
613
1199
|
progress?: ProgressReporter;
|
|
@@ -650,6 +1236,19 @@ export class MigratorKit extends EventEmitter {
|
|
|
650
1236
|
* removed, or null if no lock was held.
|
|
651
1237
|
*/
|
|
652
1238
|
forceUnlock(): Promise<LockInfo | null>;
|
|
1239
|
+
/**
|
|
1240
|
+
* The batch number the next `up` would use (highest recorded batch + 1,
|
|
1241
|
+
* reverted and failed records included). A peek, not a reservation — pair it
|
|
1242
|
+
* with `up(name, { batch })` to stamp several single-file runs as one batch.
|
|
1243
|
+
* Connects if needed.
|
|
1244
|
+
*/
|
|
1245
|
+
nextBatch(): Promise<number>;
|
|
1246
|
+
/**
|
|
1247
|
+
* A new id in this kit's configured format — the `generateId` option, else a
|
|
1248
|
+
* random UUID. The same source every run id comes from, for code that wants
|
|
1249
|
+
* its own ids to match. Resolves the config; does not connect.
|
|
1250
|
+
*/
|
|
1251
|
+
generateId(): Promise<string>;
|
|
653
1252
|
/** Run all pending migrations, or a specific named file */
|
|
654
1253
|
up(filename?: string, options?: UpOptions): Promise<RunResult[]>;
|
|
655
1254
|
/** Rollback the last batch, a specific batch, a specific file, or the last N steps */
|
|
@@ -679,16 +1278,22 @@ export class MigratorKit extends EventEmitter {
|
|
|
679
1278
|
filename?: string,
|
|
680
1279
|
options?: { steps?: number; batch?: number; to?: string },
|
|
681
1280
|
): Promise<StatusRow[]>;
|
|
682
|
-
/**
|
|
683
|
-
|
|
1281
|
+
/**
|
|
1282
|
+
* Full migration status for all known files and records. `checksums: false`
|
|
1283
|
+
* skips hashing the applied files (`checksumOk` stays null).
|
|
1284
|
+
*/
|
|
1285
|
+
status(options?: StatusOptions): Promise<StatusRow[]>;
|
|
684
1286
|
/**
|
|
685
1287
|
* Read-only health check: configuration, connectivity, transaction support,
|
|
686
1288
|
* changelog indexes, lock state, checksum drift and runtime. Reports
|
|
687
1289
|
* problems; fixes none of them.
|
|
688
1290
|
*/
|
|
689
1291
|
audit(): Promise<AuditReport>;
|
|
690
|
-
/**
|
|
691
|
-
|
|
1292
|
+
/**
|
|
1293
|
+
* Filtered list of migrations. Default: 'all'. `checksums: false` skips
|
|
1294
|
+
* hashing the applied files — for a caller that needs names and dates only.
|
|
1295
|
+
*/
|
|
1296
|
+
list(filter?: 'all' | 'pending' | 'applied', options?: StatusOptions): Promise<StatusRow[]>;
|
|
692
1297
|
/** Create a new migration file and return its absolute path */
|
|
693
1298
|
create(name: string, options?: CreateOptions): Promise<string>;
|
|
694
1299
|
/** Create a migronaut config file in the working directory and return its path */
|
|
@@ -698,6 +1303,33 @@ export class MigratorKit extends EventEmitter {
|
|
|
698
1303
|
* records into our schema and writing them to `migrationsCollection`.
|
|
699
1304
|
*/
|
|
700
1305
|
import(options?: ImportOptions): Promise<ImportResult>;
|
|
1306
|
+
/**
|
|
1307
|
+
* Adopt an existing database with no prior migration tool: mark migration
|
|
1308
|
+
* files on disk as applied — checksums from disk, one shared batch,
|
|
1309
|
+
* `origin: 'baseline'` — without executing anything. Forward-only:
|
|
1310
|
+
* `down`/`redo` refuse baselined records. Idempotent: already-applied names
|
|
1311
|
+
* are skipped, so a partial baseline can simply be re-run.
|
|
1312
|
+
*/
|
|
1313
|
+
baseline(options?: BaselineOptions): Promise<BaselineSummary>;
|
|
1314
|
+
/**
|
|
1315
|
+
* Bring the declared collections (`collections`, `collectionsDir`) to their
|
|
1316
|
+
* declared indexes and validators. Stateless: the live database is read and
|
|
1317
|
+
* compared on every call; what a run changed is appended to the converge
|
|
1318
|
+
* history ({@link MigratorKit.convergeHistory}), which no run reads back. A
|
|
1319
|
+
* real run holds the migration lock; a plan with a conflict is refused before
|
|
1320
|
+
* any write, and a failed step throws {@link ConvergeFailedError}. Experimental.
|
|
1321
|
+
*/
|
|
1322
|
+
converge(options?: ConvergeOptions): Promise<ConvergeResult>;
|
|
1323
|
+
/**
|
|
1324
|
+
* Whether a bulk `up` on this kit ends by converging: `convergeAfterUp` is
|
|
1325
|
+
* on and something is declared. Resolves the config; does not connect.
|
|
1326
|
+
*/
|
|
1327
|
+
convergesAfterUp(): Promise<boolean>;
|
|
1328
|
+
/**
|
|
1329
|
+
* The converge history, newest first (`limit` 1–1000, default 20): one entry
|
|
1330
|
+
* per converge that changed something or failed. Read-only.
|
|
1331
|
+
*/
|
|
1332
|
+
convergeHistory(options?: { limit?: number }): Promise<ConvergeHistoryEntry[]>;
|
|
701
1333
|
}
|
|
702
1334
|
|
|
703
1335
|
// ─── Programmatic entry points ─────────────────────────────────────────────────
|
|
@@ -717,13 +1349,36 @@ export interface RunMigrationsOptions extends MigratorKitOptions {
|
|
|
717
1349
|
*/
|
|
718
1350
|
onLockHeld?: OnLockHeld;
|
|
719
1351
|
/**
|
|
720
|
-
* Max time (ms) to wait when `onLockHeld: 'wait'
|
|
721
|
-
*
|
|
722
|
-
*
|
|
1352
|
+
* Max time (ms) to wait when `onLockHeld: 'wait'` **without observing holder
|
|
1353
|
+
* progress**. While the holder's heartbeat visibly advances its lock, the
|
|
1354
|
+
* deadline is re-armed — a healthy peer working through a long backlog never
|
|
1355
|
+
* times its waiting peers out; only a stalled holder runs this budget down.
|
|
1356
|
+
* Default: 90000, or 1.5× the holder's lock TTL when that is longer — its
|
|
1357
|
+
* heartbeat only moves the lock every TTL/2, and a crashed holder's lock is
|
|
1358
|
+
* reclaimable only after a full TTL. An explicit value is used as given.
|
|
723
1359
|
*/
|
|
724
1360
|
lockWaitTimeoutMs?: number;
|
|
725
|
-
/**
|
|
1361
|
+
/**
|
|
1362
|
+
* First poll interval (ms) while waiting for the lock. Polls back off from
|
|
1363
|
+
* it, doubling, up to 5 s (and never more than a quarter of the wait budget).
|
|
1364
|
+
* Default: 500
|
|
1365
|
+
*/
|
|
726
1366
|
lockPollIntervalMs?: number;
|
|
1367
|
+
/**
|
|
1368
|
+
* Abort the call: a wait for the lock stops between polls, and a run that
|
|
1369
|
+
* holds it stops between migrations (one already executing finishes), with
|
|
1370
|
+
* a {@link RunAbortedError}. Wire it to SIGTERM so a pod being shut down
|
|
1371
|
+
* does not take the lock just before it is killed.
|
|
1372
|
+
*/
|
|
1373
|
+
signal?: AbortSignal;
|
|
1374
|
+
/**
|
|
1375
|
+
* Receives the internally-constructed {@link MigratorKit} right after
|
|
1376
|
+
* construction (before connect), so an embedding application can subscribe
|
|
1377
|
+
* to its lifecycle events — `kit.on('migration:success', …)` for metrics,
|
|
1378
|
+
* lock telemetry, runId correlation — while keeping the managed
|
|
1379
|
+
* connect/run/disconnect lifecycle.
|
|
1380
|
+
*/
|
|
1381
|
+
onKit?: (kit: MigratorKit) => void;
|
|
727
1382
|
}
|
|
728
1383
|
|
|
729
1384
|
/** Outcome of a {@link runMigrations} call */
|
|
@@ -734,10 +1389,12 @@ export interface MigrationSummary {
|
|
|
734
1389
|
upToDate: boolean;
|
|
735
1390
|
/** True when this instance waited for a peer to release the lock before running */
|
|
736
1391
|
waited: boolean;
|
|
737
|
-
/**
|
|
1392
|
+
/** Time (ms) from the first refusal to the run, by the clock. 0 when the lock was free */
|
|
738
1393
|
waitedMs: number;
|
|
739
1394
|
/** Number of `up` attempts made — 1 when the lock was free on the first try */
|
|
740
1395
|
attempts: number;
|
|
1396
|
+
/** The converge that ended the run — present only when `convergeAfterUp` converged */
|
|
1397
|
+
converge?: ConvergeResult;
|
|
741
1398
|
}
|
|
742
1399
|
|
|
743
1400
|
/**
|
|
@@ -761,16 +1418,34 @@ export function pendingMigrations(
|
|
|
761
1418
|
): Promise<StatusRow[]>;
|
|
762
1419
|
|
|
763
1420
|
/**
|
|
764
|
-
* The CLI's exit-code map: one entry per {@link MigronautErrorCode}, plus
|
|
1421
|
+
* The CLI's exit-code map: one entry per {@link MigronautErrorCode}, plus three
|
|
765
1422
|
* CLI-condition codes with no error class — `PENDING_MIGRATIONS` (from
|
|
766
|
-
* `status --check`)
|
|
767
|
-
*
|
|
768
|
-
* success is 0.
|
|
1423
|
+
* `status --check`), `AUDIT_FAILED` and `COLLECTIONS_DRIFT` (from
|
|
1424
|
+
* `converge --check`). Lets a wrapper script mirror the CLI's exit semantics
|
|
1425
|
+
* without hardcoding numbers. Anything unmapped exits 1; success is 0.
|
|
769
1426
|
*/
|
|
770
1427
|
export const EXIT_CODES: Readonly<
|
|
771
|
-
Record<
|
|
1428
|
+
Record<
|
|
1429
|
+
MigronautErrorCode | 'PENDING_MIGRATIONS' | 'AUDIT_FAILED' | 'COLLECTIONS_DRIFT',
|
|
1430
|
+
number
|
|
1431
|
+
>
|
|
772
1432
|
>;
|
|
773
1433
|
|
|
1434
|
+
// ─── Logger factory ───────────────────────────────────────────────────────────
|
|
1435
|
+
|
|
1436
|
+
/** Threshold accepted by {@link createLogger} — drops anything less severe */
|
|
1437
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Create the default console logger (pino-compatible surface, terminal-escape
|
|
1441
|
+
* sanitization and colors included). `debug`/`info` write to `stream`
|
|
1442
|
+
* (stdout by default); `warn`/`error` always write to stderr. For programmatic
|
|
1443
|
+
* callers who want migronaut's own output at a chosen verbosity — e.g.
|
|
1444
|
+
* `logger: createLogger(process.stdout, 'debug')` — without hand-writing a
|
|
1445
|
+
* four-method logger.
|
|
1446
|
+
*/
|
|
1447
|
+
export function createLogger(stream?: NodeJS.WritableStream, level?: LogLevel): MigronautLogger;
|
|
1448
|
+
|
|
774
1449
|
// ─── Errors ───────────────────────────────────────────────────────────────────
|
|
775
1450
|
|
|
776
1451
|
/** Construction options shared by every migronaut error — `cause` keeps the wrapped Error */
|
|
@@ -897,7 +1572,60 @@ export class ImportTargetNotEmptyError extends MigronautError {
|
|
|
897
1572
|
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
898
1573
|
}
|
|
899
1574
|
|
|
900
|
-
/** Thrown when attempting to roll back a
|
|
1575
|
+
/** Thrown when attempting to roll back a forward-only (imported or baselined) migration */
|
|
901
1576
|
export class IrreversibleMigrationError extends MigronautError {
|
|
902
1577
|
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
903
1578
|
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* Thrown by a bulk `up` under `onOutOfOrder: 'error'` when a pending migration
|
|
1582
|
+
* sorts before the newest applied one — a file merged late from a parallel
|
|
1583
|
+
* branch. `context.names` lists the late arrivals.
|
|
1584
|
+
*/
|
|
1585
|
+
export class OutOfOrderMigrationError extends MigronautError {
|
|
1586
|
+
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Thrown by an `ordered` single-file `up`/`down` that would run out of
|
|
1591
|
+
* sequence: an earlier migration is still pending (`up`), or one applied later
|
|
1592
|
+
* is still applied (`down`). `context.name`, `context.direction` and
|
|
1593
|
+
* `context.blockedBy` (the migrations that must go first).
|
|
1594
|
+
*/
|
|
1595
|
+
export class MigrationBlockedError extends MigronautError {
|
|
1596
|
+
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Thrown by the queue adapter (`@alexify/migronaut/bullmq`) when a job's
|
|
1601
|
+
* payload fails the contract check — an unknown job name or data version, a
|
|
1602
|
+
* migration name that is not a bare filename, malformed group fields. Job data
|
|
1603
|
+
* is untrusted input. `context.jobId`, `context.issue`.
|
|
1604
|
+
*/
|
|
1605
|
+
export class QueueJobInvalidError extends MigronautError {
|
|
1606
|
+
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Thrown by a queue group's `wait()` when one of its jobs failed or the wait
|
|
1611
|
+
* timed out (one budget for the whole call). `context.failedReason` is the
|
|
1612
|
+
* worker's (redacted) message, `context.code` the job's own typed error code
|
|
1613
|
+
* when it reported one (`MIGRATION_BLOCKED`, `CHECKSUM_MISMATCH`, …),
|
|
1614
|
+
* `context.results` the jobs that finished before it, plus `groupId`, `jobId`,
|
|
1615
|
+
* `migration`, `direction` and `timedOut`.
|
|
1616
|
+
*/
|
|
1617
|
+
export class QueueJobFailedError extends MigronautError {
|
|
1618
|
+
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Thrown by {@link MigratorKit.converge} when the database cannot be brought
|
|
1623
|
+
* to the declared state. `context.phase` is `'plan'` for a refused plan
|
|
1624
|
+
* (`context.conflicts` lists why; nothing was written) or `'apply'` for a
|
|
1625
|
+
* failed step (`collection`, `target`, `name`, `action`, `cause`, and
|
|
1626
|
+
* `mongoCode`, `hint` and — after a failed rebuild — `restored` when they
|
|
1627
|
+
* apply). `context.converge` is the {@link ConvergeResult} so far.
|
|
1628
|
+
*/
|
|
1629
|
+
export class ConvergeFailedError extends MigronautError {
|
|
1630
|
+
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
|
1631
|
+
}
|