@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +409 -1
  2. package/README.md +248 -24
  3. package/bin/migronaut.js +11 -3
  4. package/bullmq.d.ts +845 -0
  5. package/bullmq.js +1 -0
  6. package/index.d.ts +757 -29
  7. package/migronaut.schema.json +191 -1
  8. package/package.json +27 -6
  9. package/src/bullmq/index.js +55 -0
  10. package/src/bullmq/jobs.js +454 -0
  11. package/src/bullmq/processor.js +608 -0
  12. package/src/bullmq/producer.js +424 -0
  13. package/src/bullmq/service.js +653 -0
  14. package/src/bullmq/wait.js +124 -0
  15. package/src/cli/args.js +12 -2
  16. package/src/cli/commands/baseline.js +45 -0
  17. package/src/cli/commands/converge.js +160 -0
  18. package/src/cli/commands/down.js +2 -0
  19. package/src/cli/commands/lock.js +2 -1
  20. package/src/cli/commands/redo.js +8 -1
  21. package/src/cli/commands/unlock.js +12 -2
  22. package/src/cli/commands/up.js +14 -1
  23. package/src/cli/exit-codes.js +10 -2
  24. package/src/cli/index.js +4 -0
  25. package/src/cli/shared.js +29 -7
  26. package/src/cli/table.js +105 -0
  27. package/src/core/audit.js +17 -3
  28. package/src/core/baseline.js +80 -0
  29. package/src/core/changelog.js +140 -24
  30. package/src/core/collections.js +372 -0
  31. package/src/core/config.js +125 -27
  32. package/src/core/converge-log.js +47 -0
  33. package/src/core/converge-plan.js +483 -0
  34. package/src/core/converge.js +867 -0
  35. package/src/core/import-runner.js +34 -6
  36. package/src/core/import.js +14 -7
  37. package/src/core/index-spec.js +496 -0
  38. package/src/core/lock-wait.js +260 -0
  39. package/src/core/lock.js +71 -20
  40. package/src/core/migrator.js +805 -304
  41. package/src/core/options.js +251 -0
  42. package/src/core/run-recorder.js +157 -0
  43. package/src/core/run.js +71 -71
  44. package/src/core/runner.js +70 -20
  45. package/src/core/sequence.js +134 -0
  46. package/src/errors/index.js +71 -1
  47. package/src/index.js +16 -0
  48. package/src/utils/actor.js +48 -0
  49. package/src/utils/canonical.js +179 -0
  50. package/src/utils/collection-name.js +21 -0
  51. package/src/utils/error.js +18 -1
  52. package/src/utils/id.js +77 -0
  53. package/src/utils/loader.js +39 -21
  54. package/src/utils/logger.js +30 -12
  55. package/src/utils/migration-name.js +32 -0
  56. package/src/utils/redact.js +57 -4
  57. package/src/utils/sanitize.js +8 -3
  58. package/src/utils/telemetry.js +393 -0
  59. package/src/utils/template.js +60 -12
package/bullmq.d.ts ADDED
@@ -0,0 +1,845 @@
1
+ import type {
2
+ AuditReport,
3
+ CollectionConvergeResult,
4
+ ConvergeUnstable,
5
+ LockInfo,
6
+ MigratorKit,
7
+ MigratorKitOptions,
8
+ MigronautConfig,
9
+ MigronautErrorCode,
10
+ OnLockHeld,
11
+ StatusRow,
12
+ } from './index.js';
13
+
14
+ // ─── Structural BullMQ surface ─────────────────────────────────────────────────
15
+
16
+ /**
17
+ * Structural stand-ins for BullMQ's `Job`, `Queue`, `Worker` and `QueueEvents`
18
+ * — only the members the adapter actually calls.
19
+ *
20
+ * Deliberately not `import type { Queue } from 'bullmq'`: bullmq is not a
21
+ * dependency of migronaut of any kind — you inject its classes — so a hard
22
+ * import would make this declaration file fail to resolve for everyone who has
23
+ * not installed it. The real classes are assignable to these (pinned by the
24
+ * type tests against the real package), and the factory is generic over what
25
+ * you inject, so `mq.queue` and `mq.worker` are your own `Queue`/`Worker`.
26
+ * Injected classes are inferred with BullMQ's widest type arguments; name the
27
+ * instance types to pin them — `createMigrationQueue<Queue, Worker>(…)`.
28
+ *
29
+ * Methods use shorthand syntax on purpose: method parameters are compared
30
+ * bivariantly, which is what lets BullMQ's generic, overloaded signatures
31
+ * satisfy these without being imported.
32
+ */
33
+ export interface BullMQJobLike<Data = any, Result = any> {
34
+ id?: string;
35
+ name: string;
36
+ data: Data;
37
+ opts: { attempts?: number };
38
+ attemptsMade: number;
39
+ progress: unknown;
40
+ returnvalue: Result;
41
+ failedReason: string;
42
+ timestamp: number;
43
+ processedOn?: number;
44
+ finishedOn?: number;
45
+ updateProgress(progress: any): Promise<void>;
46
+ log(row: string): Promise<number>;
47
+ getState(): Promise<string>;
48
+ waitUntilFinished(queueEvents: any, ttl?: number): Promise<Result>;
49
+ }
50
+
51
+ /** See {@link BullMQJobLike} for why this is structural */
52
+ export interface BullMQQueueLike {
53
+ name: string;
54
+ addBulk(jobs: (MigrationJobSpec | ConvergeJobSpec)[]): Promise<BullMQJobLike[]>;
55
+ getJob(id: string): Promise<BullMQJobLike | undefined>;
56
+ pause(): Promise<void>;
57
+ resume(): Promise<void>;
58
+ close(): Promise<void>;
59
+ /** BullMQ ≥ 5.9 — used when present to cap the queue at one active job */
60
+ setGlobalConcurrency?(concurrency: number): Promise<unknown>;
61
+ /** BullMQ ≥ 5.16 — needed by `schedule()` */
62
+ upsertJobScheduler?(id: string, repeat: any, template?: any): Promise<unknown>;
63
+ /** BullMQ ≥ 5.16 — needed by `unschedule()` */
64
+ removeJobScheduler?(id: string): Promise<boolean>;
65
+ }
66
+
67
+ /** See {@link BullMQJobLike} for why this is structural */
68
+ export interface BullMQWorkerLike {
69
+ name: string;
70
+ close(force?: boolean): Promise<void>;
71
+ }
72
+
73
+ /** See {@link BullMQJobLike} for why this is structural */
74
+ export interface BullMQQueueEventsLike {
75
+ /** Present on every QueueEvents — how an injected instance is told apart from the class */
76
+ on(event: string, listener: (...args: any[]) => void): unknown;
77
+ close(): Promise<void>;
78
+ waitUntilReady?(): Promise<unknown>;
79
+ }
80
+
81
+ /** The `Queue` class: `new Queue(name, { connection, prefix })` */
82
+ export type BullMQQueueClass<Q extends BullMQQueueLike = BullMQQueueLike> = new (
83
+ name: string,
84
+ opts: any,
85
+ ) => Q;
86
+
87
+ /** The `Worker` class: `new Worker(name, processor, { connection, concurrency, … })` */
88
+ export type BullMQWorkerClass<W extends BullMQWorkerLike = BullMQWorkerLike> = new (
89
+ name: string,
90
+ processor: any,
91
+ opts: any,
92
+ ) => W;
93
+
94
+ /** The `QueueEvents` class: `new QueueEvents(name, { connection, prefix })` */
95
+ export type BullMQQueueEventsClass<E extends BullMQQueueEventsLike = BullMQQueueEventsLike> = new (
96
+ name: string,
97
+ opts: any,
98
+ ) => E;
99
+
100
+ // ─── Job contract ──────────────────────────────────────────────────────────────
101
+
102
+ export type MigrationJobName = 'up' | 'down' | 'sync' | 'converge';
103
+
104
+ /**
105
+ * Job names: `up`/`down` carry one migration each; `sync` plans and enqueues
106
+ * what is pending; `converge` brings the declared collections to their
107
+ * declared state.
108
+ */
109
+ export const JOB_NAMES: Readonly<{ UP: 'up'; DOWN: 'down'; SYNC: 'sync'; CONVERGE: 'converge' }>;
110
+ /**
111
+ * Version stamped on every job's data as `v`. A worker accepts every version
112
+ * from {@link MIN_JOB_DATA_VERSION} up to its own and refuses a newer one —
113
+ * roll workers out before the producers that write a new version.
114
+ */
115
+ export const JOB_DATA_VERSION: 1;
116
+ /** The oldest job data version a worker still accepts — moves only in a major release */
117
+ export const MIN_JOB_DATA_VERSION: 1;
118
+ export const DEFAULT_QUEUE_NAME: 'migronaut';
119
+ export const DEFAULT_SCHEDULER_ID: 'migronaut-sync';
120
+ /** Default id of a `schedule({ job: 'converge' })` schedule */
121
+ export const DEFAULT_CONVERGE_SCHEDULER_ID: 'migronaut-converge';
122
+
123
+ /**
124
+ * Data of an `up` or `down` job. Stored in Redis — re-validated by the worker as untrusted input
125
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
126
+ */
127
+ export interface MigrationJobData {
128
+ v: 1;
129
+ direction: 'up' | 'down';
130
+ /** Bare migration filename */
131
+ migration: string;
132
+ /** The enqueue call this job belongs to — minted by the kit's `generateId`, a UUID by default */
133
+ groupId: string;
134
+ /** Position within the group, and the group's size */
135
+ index: number;
136
+ total: number;
137
+ /**
138
+ * `up`: the batch every job of the group stamps (peeked once at enqueue
139
+ * time). `down`: the batch the record carried, for information only.
140
+ */
141
+ batch?: number;
142
+ /** Re-run an already-applied migration (`up` only) */
143
+ force?: true;
144
+ /**
145
+ * `false` skips the order guard for this job. Always written by the
146
+ * producer; a job without it (hand-added) gets the worker's default.
147
+ */
148
+ ordered?: boolean;
149
+ /** SHA-256 of the migration file when the job was planned */
150
+ checksum?: string;
151
+ /** Who asked (≤ 128 characters) — carried by the jobs, stamped on the changelog / converge history */
152
+ requestedBy?: string;
153
+ /** Why (≤ 512 characters) — carried and stamped like `requestedBy` */
154
+ reason?: string;
155
+ }
156
+
157
+ /**
158
+ * Data of a `sync` job — what a schedule tick enqueues
159
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
160
+ */
161
+ export interface SyncJobData {
162
+ v: 1;
163
+ kind: 'sync';
164
+ /** Enqueue pending migrations only up to and including this file */
165
+ to?: string;
166
+ }
167
+
168
+ /**
169
+ * Data of a `converge` job. There is deliberately no `prune`: what may be
170
+ * dropped is decided by the definitions the worker loads, never by a payload.
171
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
172
+ */
173
+ export interface ConvergeJobData {
174
+ v: 1;
175
+ kind: 'converge';
176
+ /** The enqueue call it belongs to — the `up` group it ends, or its own */
177
+ groupId?: string;
178
+ /**
179
+ * `false` lets it run while migrations are pending. Always written by the
180
+ * producer; a job without it gets the worker's default (refuse).
181
+ */
182
+ ordered?: boolean;
183
+ /** Who asked (≤ 128 characters) — carried by the jobs, stamped on the changelog / converge history */
184
+ requestedBy?: string;
185
+ /** Why (≤ 512 characters) — carried and stamped like `requestedBy` */
186
+ reason?: string;
187
+ }
188
+
189
+ /**
190
+ * What a completed `up`/`down` job returns
191
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
192
+ */
193
+ export interface MigrationJobResult {
194
+ migration: string;
195
+ direction: 'up' | 'down';
196
+ /** `'skipped'`: already applied (up) or already reverted (down) — a duplicate job, not an error */
197
+ status: 'applied' | 'reverted' | 'skipped';
198
+ duration?: number;
199
+ batch?: number;
200
+ /** Correlation id of the run, matching the changelog record and the kit's events */
201
+ runId?: string;
202
+ reason?: string;
203
+ /** Time (ms) spent waiting for the MongoDB migration lock */
204
+ lockWaitMs: number;
205
+ }
206
+
207
+ /**
208
+ * What a completed `sync` job returns
209
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
210
+ */
211
+ export interface SyncJobResult {
212
+ kind: 'sync';
213
+ groupId: string | null;
214
+ batch: number | null;
215
+ /** Number of migration jobs this tick added */
216
+ enqueued: number;
217
+ upToDate: boolean;
218
+ migrations: string[];
219
+ /** The converge job this tick added (`convergeAfterUp`), if any */
220
+ converge?: { jobId: string; deduplicated: boolean };
221
+ /**
222
+ * Present when the tick enqueued nothing because the next migration failed
223
+ * and its file has not changed since — a schedule's circuit breaker. A fix
224
+ * (a changed file) or an explicit `enqueueUp(name)` resumes the line.
225
+ */
226
+ held?: { migration: string; reason: string; failedAt?: Date };
227
+ }
228
+
229
+ /**
230
+ * What a completed `converge` job returns — the kit's result, minus `dryRun`
231
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
232
+ */
233
+ export interface ConvergeJobResult {
234
+ kind: 'converge';
235
+ groupId?: string;
236
+ changed: number;
237
+ inSync: boolean;
238
+ collections: CollectionConvergeResult[];
239
+ unstable?: ConvergeUnstable[];
240
+ runId?: string;
241
+ /** Time (ms) spent waiting for the MongoDB migration lock */
242
+ lockWaitMs: number;
243
+ }
244
+
245
+ /**
246
+ * What a job reports through `job.updateProgress`
247
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
248
+ */
249
+ export interface MigrationJobProgress {
250
+ phase: 'lock-wait' | 'running' | 'completed' | 'failed';
251
+ migration?: string;
252
+ direction?: 'up' | 'down';
253
+ groupId?: string;
254
+ index?: number;
255
+ total?: number;
256
+ kind?: 'sync' | 'converge';
257
+ /** `lock-wait` only */
258
+ attempts?: number;
259
+ waitedMs?: number;
260
+ /**
261
+ * `failed` only — the typed error code, so nobody has to parse
262
+ * `failedReason`; `'UNKNOWN'` for an error that is not migronaut's.
263
+ */
264
+ code?: MigronautErrorCode | 'UNKNOWN';
265
+ /**
266
+ * `completed` and `failed` — the run's correlation id, matching the changelog
267
+ * record and the kit's events. A failed job has no return value, so this is
268
+ * where its run id is found.
269
+ */
270
+ runId?: string;
271
+ }
272
+
273
+ /**
274
+ * Per-job BullMQ options passed through to every migration job — retention and
275
+ * logging knobs. Anything that would reorder, delay or re-run a job is refused:
276
+ * the queue is first-in, first-out with a single attempt, on purpose.
277
+ */
278
+ export interface MigrationJobOptions {
279
+ removeOnComplete?: boolean | number | { age?: number; count?: number };
280
+ removeOnFail?: boolean | number | { age?: number; count?: number };
281
+ keepLogs?: number;
282
+ stackTraceLimit?: number;
283
+ sizeLimit?: number;
284
+ attempts?: never;
285
+ backoff?: never;
286
+ delay?: never;
287
+ priority?: never;
288
+ lifo?: never;
289
+ jobId?: never;
290
+ deduplication?: never;
291
+ repeat?: never;
292
+ parent?: never;
293
+ [option: string]: unknown;
294
+ }
295
+
296
+ /** One job as handed to `queue.addBulk` */
297
+ export interface MigrationJobSpec {
298
+ name: 'up' | 'down';
299
+ data: MigrationJobData;
300
+ opts: { attempts: 1; deduplication: { id: string }; [option: string]: unknown };
301
+ }
302
+
303
+ /** A converge job as handed to `queue.addBulk` */
304
+ export interface ConvergeJobSpec {
305
+ name: 'converge';
306
+ data: ConvergeJobData;
307
+ opts: { attempts: 1; deduplication: { id: string }; [option: string]: unknown };
308
+ }
309
+
310
+ /** A planned, not yet enqueued, group */
311
+ export interface MigrationPlan {
312
+ /** Id of this enqueue call, in the kit's `generateId` format (a UUID by default) */
313
+ groupId: string;
314
+ direction: 'up' | 'down';
315
+ /** Shared batch of an `up` group; null for `down` and for an empty plan */
316
+ batch: number | null;
317
+ /** The files, in execution order */
318
+ migrations: string[];
319
+ jobs: MigrationJobSpec[];
320
+ /** The converge job that ends an `up` group — see `EnqueueUpOptions.converge` */
321
+ converge?: ConvergeJobSpec;
322
+ }
323
+
324
+ /** {@link parseJobData}'s normalized result */
325
+ export type ParsedJobData =
326
+ | {
327
+ kind: 'migration';
328
+ direction: 'up' | 'down';
329
+ migration: string;
330
+ groupId: string;
331
+ index: number;
332
+ total: number;
333
+ batch?: number;
334
+ force?: true;
335
+ ordered?: boolean;
336
+ }
337
+ | { kind: 'sync'; to?: string }
338
+ | { kind: 'converge'; groupId?: string; ordered?: boolean };
339
+
340
+ /**
341
+ * Validate a job read back from the queue and return a normalized copy.
342
+ * Throws `QueueJobInvalidError` for anything outside the contract.
343
+ */
344
+ export function parseJobData(job: { id?: string; name: string; data: unknown }): ParsedJobData;
345
+
346
+ /** The deduplication id a migration's job carries — never contains `:` */
347
+ export function dedupId(direction: 'up' | 'down', migration: string): string;
348
+
349
+ /**
350
+ * Error codes a later attempt can get past with nothing fixed (lock busy or
351
+ * lost, database unreachable, run stopped). Every other `MigronautError` is
352
+ * failed without retry.
353
+ */
354
+ export const RETRYABLE_CODES: readonly MigronautErrorCode[];
355
+
356
+ /** Whether the processor treats `error` as retryable — see {@link RETRYABLE_CODES} */
357
+ export function isRetryableError(error: unknown): boolean;
358
+
359
+ // ─── Options ───────────────────────────────────────────────────────────────────
360
+
361
+ /** How a job behaves when the MongoDB migration lock is held (a CLI run, a peer worker) */
362
+ export interface LockWaitOptions {
363
+ /** Default `'wait'` — unlike `runMigrations`, nothing is blocked on a job */
364
+ onLockHeld?: OnLockHeld;
365
+ /**
366
+ * Max time (ms) to wait without observing the holder make progress. Default
367
+ * 90000, or 1.5× the holder's lock TTL when that is longer
368
+ */
369
+ lockWaitTimeoutMs?: number;
370
+ /** First poll interval (ms); polls back off, doubling, up to 5 s. Default 500 */
371
+ lockPollIntervalMs?: number;
372
+ }
373
+
374
+ /** Options for `enqueueUp` */
375
+ export interface EnqueueUpOptions {
376
+ /** Enqueue pending migrations up to and including this file */
377
+ to?: string;
378
+ /** Re-run an already-applied migration. Needs a filename */
379
+ force?: boolean;
380
+ /**
381
+ * Default `true`: each job refuses (`MigrationBlockedError`) while an
382
+ * earlier migration is still pending. `false` gives the plain single-file
383
+ * `up` of the CLI.
384
+ */
385
+ ordered?: boolean;
386
+ /**
387
+ * End the group with a converge job, which runs once every migration of the
388
+ * group is applied (and refuses, as blocked, while one is not). Default: the
389
+ * kit's `convergeAfterUp` — a queue never fires the kit's own after-up hook,
390
+ * since each job is a single-file run. With nothing pending, a converge job
391
+ * is added only when a dry run finds the database out of step. Refused with
392
+ * a filename or `to`.
393
+ */
394
+ converge?: boolean;
395
+ /** Who asked (≤ 128 characters) — carried by the jobs, stamped on the changelog / converge history */
396
+ requestedBy?: string;
397
+ /** Why (≤ 512 characters) — carried and stamped like `requestedBy` */
398
+ reason?: string;
399
+ }
400
+
401
+ /** Options for `enqueueConverge` */
402
+ export interface EnqueueConvergeOptions {
403
+ /** Default `true`: refuse while a migration is still pending. `false` converges anyway */
404
+ ordered?: boolean;
405
+ /** Who asked (≤ 128 characters) — carried by the jobs, stamped on the changelog / converge history */
406
+ requestedBy?: string;
407
+ /** Why (≤ 512 characters) — carried and stamped like `requestedBy` */
408
+ reason?: string;
409
+ }
410
+
411
+ /** Options for `enqueueDown` */
412
+ export interface EnqueueDownOptions {
413
+ /** Revert this batch instead of the last one */
414
+ batch?: number;
415
+ /** Revert the last N applied migrations */
416
+ steps?: number;
417
+ /** Revert everything applied after this migration */
418
+ to?: string;
419
+ /**
420
+ * Default `true`: the rollback must be the top of the applied stack and each
421
+ * job refuses while a later-applied migration remains. `false` gives the
422
+ * CLI's unguarded `down`.
423
+ */
424
+ ordered?: boolean;
425
+ /** Who asked (≤ 128 characters) — carried by the jobs, stamped on the changelog / converge history */
426
+ requestedBy?: string;
427
+ /** Why (≤ 512 characters) — carried and stamped like `requestedBy` */
428
+ reason?: string;
429
+ }
430
+
431
+ /** Options for a group's `wait()` */
432
+ export interface WaitOptions {
433
+ /** One budget (ms) for the whole group. Default: no limit */
434
+ timeoutMs?: number;
435
+ /** A QueueEvents instance to listen on, when the facade was not given one */
436
+ queueEvents?: BullMQQueueEventsLike;
437
+ }
438
+
439
+ /** What a group's `wait()` resolves with */
440
+ export interface GroupWaitResult {
441
+ groupId: string;
442
+ direction: 'up' | 'down';
443
+ batch: number | null;
444
+ /** One result per job, in group order */
445
+ results: MigrationJobResult[];
446
+ /** The group's converge job, when it had one */
447
+ converge?: ConvergeJobResult;
448
+ }
449
+
450
+ /** Handle returned by `enqueueUp`/`enqueueDown` */
451
+ export interface MigrationGroup {
452
+ /** Id of this enqueue call, in the kit's `generateId` format (a UUID by default) */
453
+ groupId: string;
454
+ direction: 'up' | 'down';
455
+ /** The batch every job of an `up` group will stamp; null for `down` or when nothing was enqueued */
456
+ batch: number | null;
457
+ /** True when there was nothing to do — no job was added */
458
+ upToDate: boolean;
459
+ jobs: { id: string; migration: string; index: number }[];
460
+ /**
461
+ * Files whose job already existed in the queue (enqueued by a peer). Their
462
+ * `jobs[].id` is that existing job, so `wait()` simply joins it.
463
+ */
464
+ deduplicated: string[];
465
+ /** The converge job ending the group, or null. `deduplicated`: a peer's identical job */
466
+ converge: { id: string; deduplicated: boolean } | null;
467
+ /**
468
+ * Resolve when every job has finished — the converge job last; reject with
469
+ * `QueueJobFailedError` at the first one that fails or outlives `timeoutMs`.
470
+ * Needs QueueEvents.
471
+ */
472
+ wait(options?: WaitOptions): Promise<GroupWaitResult>;
473
+ }
474
+
475
+ /** Handle returned by `enqueueConverge` */
476
+ export interface ConvergeHandle {
477
+ groupId: string;
478
+ jobId: string;
479
+ /** True when an identical converge job was already waiting — `jobId` is that one */
480
+ deduplicated: boolean;
481
+ /** Resolve with the job's result; reject with `QueueJobFailedError`. Needs QueueEvents */
482
+ wait(options?: WaitOptions): Promise<ConvergeJobResult>;
483
+ }
484
+
485
+ /**
486
+ * Options for {@link MigrationQueue.schedule} — exactly one of `every` /
487
+ * `pattern`, for a `sync` schedule (the default) or a `converge` one.
488
+ */
489
+ export type ScheduleOptions = (
490
+ | { every: number; pattern?: never }
491
+ | { pattern: string; every?: never }
492
+ ) & {
493
+ /** Time zone for `pattern` */
494
+ tz?: string;
495
+ } & (
496
+ | {
497
+ /** Each tick plans and enqueues what is pending. The default */
498
+ job?: 'sync';
499
+ /** Scheduler id. Default `'migronaut-sync'` */
500
+ id?: string;
501
+ /** Each tick enqueues pending migrations only up to and including this file */
502
+ to?: string;
503
+ }
504
+ | {
505
+ /** Each tick enqueues a converge job */
506
+ job: 'converge';
507
+ /** Scheduler id. Default `'migronaut-converge'` */
508
+ id?: string;
509
+ to?: never;
510
+ }
511
+ );
512
+
513
+ /** Options for {@link MigrationQueue.startWorker} — passed to the Worker constructor */
514
+ export interface StartWorkerOptions {
515
+ /** Always 1; anything else is rejected */
516
+ concurrency?: 1;
517
+ /** BullMQ job lock (ms). Default 60000 */
518
+ lockDuration?: number;
519
+ stalledInterval?: number;
520
+ /** Default 1 */
521
+ maxStalledCount?: number;
522
+ autorun?: boolean;
523
+ [option: string]: unknown;
524
+ }
525
+
526
+ /** A job as plain, redacted data — what {@link MigrationQueue.getJob} returns */
527
+ export interface MigrationJobView {
528
+ id: string;
529
+ name: string;
530
+ /**
531
+ * The job's data as stored in Redis — redacted, but not validated: anything
532
+ * with write access to Redis can have put it there. A job the adapter
533
+ * enqueued has one of the contract's shapes; check before relying on it.
534
+ */
535
+ data: unknown;
536
+ state: string;
537
+ /** As stored — see {@link MigrationJobProgress} for what the adapter writes */
538
+ progress: MigrationJobProgress | number;
539
+ returnvalue?: MigrationJobResult | SyncJobResult | ConvergeJobResult;
540
+ failedReason?: string;
541
+ attemptsMade: number;
542
+ timestamp?: number;
543
+ processedOn?: number;
544
+ finishedOn?: number;
545
+ }
546
+
547
+ // ─── Processor ─────────────────────────────────────────────────────────────────
548
+
549
+ /**
550
+ * What a worker accepts from a job's payload beyond "apply what is pending, in
551
+ * order". Anything that can write to Redis can enqueue, so the requests that
552
+ * go further are opt-in; a job asking for one that is off fails as
553
+ * `QUEUE_JOB_INVALID` (`context.permission`) before anything runs.
554
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
555
+ */
556
+ export interface MigrationJobPermissions {
557
+ /** Roll back (`down` jobs). Default `true` */
558
+ down?: boolean;
559
+ /** Re-run an applied migration (`force: true`). Default `false` */
560
+ force?: boolean;
561
+ /** Skip the order guard (`ordered: false`, on any job). Default `false` */
562
+ unordered?: boolean;
563
+ }
564
+
565
+ /** Options for {@link createMigrationProcessor} */
566
+ export interface CreateMigrationProcessorOptions {
567
+ /** Config for a MigratorKit the processor creates (and disconnects on `close()`) */
568
+ config?: Partial<MigronautConfig>;
569
+ kitOptions?: MigratorKitOptions;
570
+ /** A kit you own instead of `config` — never disconnected by the processor */
571
+ kit?: MigratorKit;
572
+ /** The queue the jobs arrive on. Needed only to process `sync` jobs, which enqueue into it */
573
+ queue?: BullMQQueueLike;
574
+ lockWait?: LockWaitOptions;
575
+ /** Order guard for jobs that do not say. Default `true` */
576
+ ordered?: boolean;
577
+ /** Options for the jobs a `sync` job enqueues */
578
+ jobOptions?: MigrationJobOptions;
579
+ /** What a job may ask for beyond the ordinary — see {@link MigrationJobPermissions} */
580
+ allow?: MigrationJobPermissions;
581
+ }
582
+
583
+ /**
584
+ * The function a BullMQ Worker runs — pass it as the Worker's processor. It
585
+ * declares exactly three parameters, which is what makes BullMQ hand it the
586
+ * cancellation signal. Jobs are processed one at a time even when the Worker
587
+ * is configured for more.
588
+ */
589
+ export interface MigrationProcessor {
590
+ (
591
+ job: BullMQJobLike<MigrationJobData | SyncJobData | ConvergeJobData>,
592
+ token?: string,
593
+ signal?: AbortSignal,
594
+ ): Promise<MigrationJobResult | SyncJobResult | ConvergeJobResult>;
595
+ /** The kit running the jobs — subscribe to its events for metrics */
596
+ readonly kit: MigratorKit;
597
+ /**
598
+ * Stop taking the lock. Irreversible. A job that has not started its
599
+ * migration (or converge) yet — waiting for the lock, or fetched after the
600
+ * shutdown — is moved back to the head of the queue (`job.moveToWait`) and
601
+ * rejects with an error named `WaitingError`, which BullMQ records as
602
+ * neither failed nor completed; without a token (outside a Worker) it fails
603
+ * with `RunAbortedError`. Close your Worker first, or together with this: a
604
+ * job put back must not be fetched again by the same worker.
605
+ */
606
+ shutdown(reason?: string): void;
607
+ /** `shutdown()`, let the job in flight settle, disconnect a kit the processor created */
608
+ close(): Promise<void>;
609
+ }
610
+
611
+ /**
612
+ * Build the processor for a Worker you construct yourself (a NestJS
613
+ * `@Processor`, BullMQ Pro, a shared worker process). Run it with
614
+ * `concurrency: 1`.
615
+ */
616
+ export function createMigrationProcessor(
617
+ options?: CreateMigrationProcessorOptions,
618
+ ): MigrationProcessor;
619
+
620
+ // ─── Producer building blocks ──────────────────────────────────────────────────
621
+
622
+ /** Plan an `up` group without enqueuing it */
623
+ export function planUpJobs(
624
+ kit: MigratorKit,
625
+ options?: EnqueueUpOptions & { filename?: string; jobOptions?: MigrationJobOptions },
626
+ ): Promise<MigrationPlan>;
627
+
628
+ /** Plan a `down` group without enqueuing it */
629
+ export function planDownJobs(
630
+ kit: MigratorKit,
631
+ options?: EnqueueDownOptions & { filename?: string; jobOptions?: MigrationJobOptions },
632
+ ): Promise<MigrationPlan>;
633
+
634
+ /** Enqueue pending migrations on a queue you own */
635
+ export function enqueueUp(
636
+ queue: BullMQQueueLike,
637
+ kit: MigratorKit,
638
+ options?: EnqueueUpOptions & {
639
+ filename?: string;
640
+ jobOptions?: MigrationJobOptions;
641
+ /** Lets the returned group's `wait()` work without further arguments */
642
+ queueEvents?: BullMQQueueEventsLike;
643
+ },
644
+ ): Promise<MigrationGroup>;
645
+
646
+ /** Enqueue a rollback on a queue you own */
647
+ export function enqueueDown(
648
+ queue: BullMQQueueLike,
649
+ kit: MigratorKit,
650
+ options?: EnqueueDownOptions & {
651
+ filename?: string;
652
+ jobOptions?: MigrationJobOptions;
653
+ queueEvents?: BullMQQueueEventsLike;
654
+ },
655
+ ): Promise<MigrationGroup>;
656
+
657
+ /** Enqueue a converge job on its own, on a queue you own */
658
+ export function enqueueConverge(
659
+ queue: BullMQQueueLike,
660
+ kit: MigratorKit,
661
+ options?: EnqueueConvergeOptions & {
662
+ jobOptions?: MigrationJobOptions;
663
+ /** Lets the returned handle's `wait()` work without further arguments */
664
+ queueEvents?: BullMQQueueEventsLike;
665
+ },
666
+ ): Promise<ConvergeHandle>;
667
+
668
+ /** Wait for a group's jobs — what `MigrationGroup.wait()` calls */
669
+ export function waitForGroup(options: {
670
+ queue: BullMQQueueLike;
671
+ queueEvents: BullMQQueueEventsLike;
672
+ groupId: string;
673
+ direction: 'up' | 'down';
674
+ batch: number | null;
675
+ jobs: { id: string; migration: string }[];
676
+ /** The group's converge job, waited for last */
677
+ converge?: { id: string };
678
+ timeoutMs?: number;
679
+ }): Promise<GroupWaitResult>;
680
+
681
+ // ─── Facade ────────────────────────────────────────────────────────────────────
682
+
683
+ /** Options for {@link createMigrationQueue} */
684
+ export interface CreateMigrationQueueOptions<
685
+ Q extends BullMQQueueLike = BullMQQueueLike,
686
+ W extends BullMQWorkerLike = BullMQWorkerLike,
687
+ E extends BullMQQueueEventsLike = BullMQQueueEventsLike,
688
+ > {
689
+ /**
690
+ * BullMQ, from your own install — migronaut never imports it. Pass classes,
691
+ * or instances you already have (an injected instance is never closed by
692
+ * `close()`).
693
+ */
694
+ bullmq: {
695
+ /** The `Queue` class, or a Queue instance */
696
+ Queue: BullMQQueueClass<Q> | Q;
697
+ /** The `Worker` class. Needed by `startWorker()`; a process that only enqueues can omit it */
698
+ Worker?: BullMQWorkerClass<W>;
699
+ /** The `QueueEvents` class, or an instance. Needed by `wait()` */
700
+ QueueEvents?: BullMQQueueEventsClass<E> | E;
701
+ /**
702
+ * BullMQ's own telemetry object — `new BullMQOtel({ tracerName })` from
703
+ * `bullmq-otel` — passed untouched to the Queue and the Worker this object
704
+ * constructs. It is what joins the trace of the process that enqueues to
705
+ * the one that applies. An injected Queue *instance* keeps whatever
706
+ * telemetry it was built with; `workerOptions.telemetry` and
707
+ * `startWorker({ telemetry })` override it for the worker.
708
+ *
709
+ * Not to be confused with the kit's own `config.telemetry` (a tracer and a
710
+ * meter for migronaut's spans and metrics).
711
+ */
712
+ telemetry?: object;
713
+ };
714
+ /**
715
+ * BullMQ `connection` — connection options or your Redis client, passed
716
+ * through untouched and never closed. Required when anything is constructed
717
+ * from a class.
718
+ */
719
+ connection?: unknown;
720
+ /** Config for the MigratorKit the queue creates (and disconnects on `close()`) */
721
+ config?: Partial<MigronautConfig>;
722
+ kitOptions?: MigratorKitOptions;
723
+ /** A kit you own instead of `config` — never disconnected by `close()` */
724
+ kit?: MigratorKit;
725
+ /**
726
+ * One queue per database. Default `'migronaut'` (or the injected queue's
727
+ * name — a different one is rejected)
728
+ */
729
+ queueName?: string;
730
+ /** BullMQ key prefix. Default: the injected queue's own (a different one is rejected) */
731
+ prefix?: string;
732
+ jobOptions?: MigrationJobOptions;
733
+ /** Defaults for `startWorker()` */
734
+ workerOptions?: StartWorkerOptions;
735
+ /**
736
+ * Set the queue's global concurrency to 1 when the worker starts (BullMQ
737
+ * ≥ 5.9), so several pods take turns. Default `true`. The order never
738
+ * depends on it — the MongoDB lock and the order guard keep it; without it,
739
+ * a job that reaches the lock before an earlier one still in flight on
740
+ * another worker waits for that one (within its lock-wait budget).
741
+ */
742
+ globalConcurrency?: boolean;
743
+ lockWait?: LockWaitOptions;
744
+ /**
745
+ * What the worker accepts from a job — and what `enqueueUp` / `enqueueDown` /
746
+ * `enqueueConverge` accept on this object, so a request its own worker would
747
+ * refuse fails at the call. Give every producer and worker the same policy.
748
+ */
749
+ allow?: MigrationJobPermissions;
750
+ }
751
+
752
+ /**
753
+ * Migrations as a queue: one database's migrations, enqueued as one BullMQ job
754
+ * each and applied in order by a single-concurrency worker. Status reads go
755
+ * straight to MongoDB — the changelog, not the queue, is the source of truth.
756
+ */
757
+ export class MigrationQueue<
758
+ Q extends BullMQQueueLike = BullMQQueueLike,
759
+ W extends BullMQWorkerLike = BullMQWorkerLike,
760
+ E extends BullMQQueueEventsLike = BullMQQueueEventsLike,
761
+ > {
762
+ constructor(options: CreateMigrationQueueOptions<Q, W, E>);
763
+
764
+ /** The kit behind the queue — `kit.on('migration:success', …)` for metrics */
765
+ readonly kit: MigratorKit;
766
+ /** Your Queue, with its own type */
767
+ readonly queue: Q;
768
+ /** The worker started by {@link startWorker}, if any */
769
+ readonly worker: W | undefined;
770
+ /** The QueueEvents in use — injected, or built on the first `wait()` */
771
+ readonly queueEvents: E | undefined;
772
+ readonly queueName: string;
773
+ /** The processor, for attaching to a Worker you construct yourself */
774
+ readonly processor: MigrationProcessor;
775
+
776
+ /**
777
+ * Enqueue pending migrations — all, up to `options.to`, or the one
778
+ * `filename` — as one job each, under a single shared batch.
779
+ */
780
+ enqueueUp(filename?: string, options?: EnqueueUpOptions): Promise<MigrationGroup>;
781
+ /**
782
+ * Enqueue a rollback — the last batch, `options.batch`, the last
783
+ * `options.steps`, everything after `options.to`, or the one `filename` —
784
+ * newest applied first.
785
+ */
786
+ enqueueDown(filename?: string, options?: EnqueueDownOptions): Promise<MigrationGroup>;
787
+ /**
788
+ * Enqueue a converge job: the declared collections brought to their declared
789
+ * state by the worker, under the MongoDB lock — refused while a migration is
790
+ * pending unless `ordered: false`. Experimental.
791
+ */
792
+ enqueueConverge(options?: EnqueueConvergeOptions): Promise<ConvergeHandle>;
793
+
794
+ /** Full migration status, read from MongoDB */
795
+ status(): Promise<StatusRow[]>;
796
+ /** Migrations not applied yet */
797
+ pending(): Promise<StatusRow[]>;
798
+ audit(): Promise<AuditReport>;
799
+ /** Current holder of the MongoDB migration lock, or null */
800
+ lockInfo(): Promise<LockInfo | null>;
801
+
802
+ /**
803
+ * Start the worker (concurrency 1). Needs `bullmq.Worker`. Connects to
804
+ * MongoDB first, so an unreachable database fails here rather than on the
805
+ * first job. Calling it again resolves the same worker — or, after a start
806
+ * that failed, tries again.
807
+ */
808
+ startWorker(options?: StartWorkerOptions): Promise<W>;
809
+ /** Stop workers from picking up new jobs; the job in flight finishes */
810
+ pause(): Promise<void>;
811
+ resume(): Promise<void>;
812
+ /** A job as plain, redacted data — or null */
813
+ getJob(id: string): Promise<MigrationJobView | null>;
814
+
815
+ /**
816
+ * Keep the database migrated on a schedule: each tick enqueues a `sync` job
817
+ * that plans and enqueues whatever is pending — or, with `job: 'converge'`,
818
+ * a converge job on a cadence of its own. Idempotent. BullMQ ≥ 5.16.
819
+ */
820
+ schedule(options: ScheduleOptions): Promise<void>;
821
+ /**
822
+ * Remove a schedule — the sync one by default; pass
823
+ * {@link DEFAULT_CONVERGE_SCHEDULER_ID} (or your own id) for another.
824
+ * Resolves whether one existed.
825
+ */
826
+ unschedule(id?: string): Promise<boolean>;
827
+
828
+ /**
829
+ * Stop fetching, stop taking the lock, let the worker finish its job, then
830
+ * close everything this object created. A job that had not started its
831
+ * migration is put back at the head of the queue for the next worker.
832
+ * `force` skips waiting for the job in flight — whose migration body, if
833
+ * running, keeps its connection: a kit this object created is disconnected
834
+ * once it settles. Injected instances, the Redis connection and an injected
835
+ * kit are left open. Idempotent.
836
+ */
837
+ close(options?: { force?: boolean }): Promise<void>;
838
+ }
839
+
840
+ /** Create a {@link MigrationQueue} — `new MigrationQueue(options)` with inference */
841
+ export function createMigrationQueue<
842
+ Q extends BullMQQueueLike = BullMQQueueLike,
843
+ W extends BullMQWorkerLike = BullMQWorkerLike,
844
+ E extends BullMQQueueEventsLike = BullMQQueueEventsLike,
845
+ >(options: CreateMigrationQueueOptions<Q, W, E>): MigrationQueue<Q, W, E>;