@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
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
const { ConfigInvalidError, MigrationBlockedError } = require('../errors/index.js');
|
|
2
|
+
const { actorIssue, pickActor } = require('../utils/actor.js');
|
|
3
|
+
const { mapLimit } = require('../utils/concurrency.js');
|
|
4
|
+
const { assertId, randomId } = require('../utils/id.js');
|
|
5
|
+
const { assertMigrationName } = require('../utils/migration-name.js');
|
|
6
|
+
const {
|
|
7
|
+
FORBIDDEN_JOB_OPTIONS,
|
|
8
|
+
JOB_NAMES,
|
|
9
|
+
buildConvergeJob,
|
|
10
|
+
buildMigrationJob,
|
|
11
|
+
isPlainObject,
|
|
12
|
+
migrationJobOptions,
|
|
13
|
+
} = require('./jobs.js');
|
|
14
|
+
const { waitForGroup } = require('./wait.js');
|
|
15
|
+
|
|
16
|
+
/** Job lookups in flight while checking a group for deduplicated adds */
|
|
17
|
+
const LOOKUP_CONCURRENCY = 16;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `jobOptions` is a passthrough for retention and logging knobs
|
|
21
|
+
* (`removeOnComplete`, `removeOnFail`, `keepLogs`, …). Anything that would
|
|
22
|
+
* reorder, delay or re-run a job is refused by name rather than silently
|
|
23
|
+
* overridden — the caller should learn the queue is FIFO on purpose.
|
|
24
|
+
*/
|
|
25
|
+
function assertJobOptions(jobOptions) {
|
|
26
|
+
if (jobOptions === undefined) return;
|
|
27
|
+
if (!isPlainObject(jobOptions)) {
|
|
28
|
+
throw new ConfigInvalidError('jobOptions must be an object', { jobOptions: typeof jobOptions });
|
|
29
|
+
}
|
|
30
|
+
for (const key of FORBIDDEN_JOB_OPTIONS) {
|
|
31
|
+
if (jobOptions[key] !== undefined) {
|
|
32
|
+
throw new ConfigInvalidError(
|
|
33
|
+
`jobOptions.${key} is not configurable — migration jobs run strictly first-in, ` +
|
|
34
|
+
'first-out with a single attempt',
|
|
35
|
+
{ key },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Validate the `requestedBy` / `reason` of an enqueue call, and return them */
|
|
42
|
+
function actorOf(options) {
|
|
43
|
+
for (const key of ['requestedBy', 'reason']) {
|
|
44
|
+
const issue = actorIssue(key, options[key]);
|
|
45
|
+
if (issue) throw new ConfigInvalidError(issue, { [key]: typeof options[key] });
|
|
46
|
+
}
|
|
47
|
+
return pickActor(options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertBoolean(value, name) {
|
|
51
|
+
if (typeof value !== 'boolean') {
|
|
52
|
+
throw new ConfigInvalidError(`${name} must be a boolean`, { [name]: value });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The id of one enqueue call, in the kit's configured format (`generateId`) so
|
|
58
|
+
* a deployment sees one id format across the changelog and the queue. A
|
|
59
|
+
* duck-typed kit without the method gets the default. Checked again here
|
|
60
|
+
* either way: a worker refuses a job whose group id is not a short string, and
|
|
61
|
+
* that has to be this call's error, not a job failing later in the queue.
|
|
62
|
+
*/
|
|
63
|
+
async function newGroupId(kit) {
|
|
64
|
+
return assertId(typeof kit.generateId === 'function' ? await kit.generateId() : randomId());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Newest applied first — `appliedAt`, name-desc tiebreak: the order rollbacks must follow */
|
|
68
|
+
function newestFirst(a, b) {
|
|
69
|
+
const delta = (b.appliedAt?.getTime?.() ?? 0) - (a.appliedAt?.getTime?.() ?? 0);
|
|
70
|
+
if (delta !== 0) return delta;
|
|
71
|
+
if (a.file === b.file) return 0;
|
|
72
|
+
return a.file < b.file ? 1 : -1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether an `up` group ends with a converge job. Explicit `converge` wins;
|
|
77
|
+
* otherwise it mirrors the kit's own after-up hook (`convergeAfterUp`), which
|
|
78
|
+
* never fires in a queue — every job there is a single-file run. Only a group
|
|
79
|
+
* that brings the database to the head converges, as in the kit.
|
|
80
|
+
*/
|
|
81
|
+
async function resolveConverge(kit, { converge, filename, to }) {
|
|
82
|
+
if (converge !== undefined) {
|
|
83
|
+
assertBoolean(converge, 'converge');
|
|
84
|
+
if (converge && (filename !== undefined || to !== undefined)) {
|
|
85
|
+
throw new ConfigInvalidError(
|
|
86
|
+
'converge needs a group that reaches the newest migration — not a filename or `to`',
|
|
87
|
+
{ converge },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return converge;
|
|
91
|
+
}
|
|
92
|
+
if (filename !== undefined || to !== undefined) return false;
|
|
93
|
+
return typeof kit.convergesAfterUp === 'function' && (await kit.convergesAfterUp()) === true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Plan an `up` group without enqueuing it: which files, in which order, under
|
|
98
|
+
* which batch. The selection is `kit.dryRun('up')` — the same one a real run
|
|
99
|
+
* makes, order policy included — so the plan can never name a file a run
|
|
100
|
+
* would not apply. One batch number is peeked for the whole group, which is
|
|
101
|
+
* what makes a later `down` revert it as a unit.
|
|
102
|
+
*
|
|
103
|
+
* A group that converges carries its converge job apart from the migration
|
|
104
|
+
* jobs, as `plan.converge`, so `plan.jobs` stays one-to-one with
|
|
105
|
+
* `plan.migrations`. With nothing pending the converge job is planned only if
|
|
106
|
+
* a dry run finds the database out of step.
|
|
107
|
+
*/
|
|
108
|
+
async function planUpJobs(kit, options = {}) {
|
|
109
|
+
const { filename, to, force = false, ordered = true, jobOptions } = options;
|
|
110
|
+
if (filename !== undefined) assertMigrationName(filename);
|
|
111
|
+
if (to !== undefined) assertMigrationName(to);
|
|
112
|
+
assertBoolean(force, 'force');
|
|
113
|
+
assertBoolean(ordered, 'ordered');
|
|
114
|
+
const actor = actorOf(options);
|
|
115
|
+
if (force && filename === undefined) {
|
|
116
|
+
// A bulk plan only ever holds pending files — there is no applied target
|
|
117
|
+
// for `force` to re-run.
|
|
118
|
+
throw new ConfigInvalidError('force requires a filename', { force });
|
|
119
|
+
}
|
|
120
|
+
assertJobOptions(jobOptions);
|
|
121
|
+
const converge = await resolveConverge(kit, { converge: options.converge, filename, to });
|
|
122
|
+
|
|
123
|
+
const rows = await kit.dryRun('up', filename, to !== undefined ? { to } : {});
|
|
124
|
+
const migrations = [];
|
|
125
|
+
/** The version of each file the plan was made from — a worker refuses any other */
|
|
126
|
+
const checksums = new Map();
|
|
127
|
+
for (const row of rows) {
|
|
128
|
+
if (row.status !== 'applied' || force) {
|
|
129
|
+
migrations.push(row.file);
|
|
130
|
+
if (typeof row.checksum === 'string') checksums.set(row.file, row.checksum);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const groupId = await newGroupId(kit);
|
|
134
|
+
if (migrations.length === 0) {
|
|
135
|
+
const plan = { groupId, direction: JOB_NAMES.UP, batch: null, migrations, jobs: [] };
|
|
136
|
+
if (converge && !(await kit.converge({ dryRun: true })).inSync) {
|
|
137
|
+
plan.converge = buildConvergeJob({ groupId, ordered, jobOptions, ...actor });
|
|
138
|
+
}
|
|
139
|
+
return plan;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const batch = await kit.nextBatch();
|
|
143
|
+
const jobs = [];
|
|
144
|
+
for (const [index, migration] of migrations.entries()) {
|
|
145
|
+
jobs.push({
|
|
146
|
+
...buildMigrationJob({
|
|
147
|
+
direction: JOB_NAMES.UP,
|
|
148
|
+
migration,
|
|
149
|
+
groupId,
|
|
150
|
+
index,
|
|
151
|
+
total: migrations.length,
|
|
152
|
+
batch,
|
|
153
|
+
force,
|
|
154
|
+
ordered,
|
|
155
|
+
checksum: checksums.get(migration),
|
|
156
|
+
...actor,
|
|
157
|
+
}),
|
|
158
|
+
opts: migrationJobOptions(jobOptions, JOB_NAMES.UP, migration, { force }),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const plan = { groupId, direction: JOB_NAMES.UP, batch, migrations, jobs };
|
|
162
|
+
if (converge) {
|
|
163
|
+
plan.converge = buildConvergeJob({
|
|
164
|
+
groupId,
|
|
165
|
+
ordered,
|
|
166
|
+
after: migrations.at(-1),
|
|
167
|
+
jobOptions,
|
|
168
|
+
...actor,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return plan;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* An ordered rollback must be the top of the applied stack: every job reverts
|
|
176
|
+
* only once nothing applied after it remains, so a plan that skips a newer
|
|
177
|
+
* migration can never finish. Refusing here turns that into one synchronous
|
|
178
|
+
* error instead of a group that fails halfway through.
|
|
179
|
+
*/
|
|
180
|
+
async function assertTopOfStack(kit, migrations) {
|
|
181
|
+
// Names and dates are all this needs — not a re-hash of every applied file.
|
|
182
|
+
const applied = await kit.list('applied', { checksums: false });
|
|
183
|
+
applied.sort(newestFirst);
|
|
184
|
+
const planned = new Set(migrations);
|
|
185
|
+
let deepest = -1;
|
|
186
|
+
for (const [index, row] of applied.entries()) {
|
|
187
|
+
if (planned.has(row.file)) deepest = index;
|
|
188
|
+
}
|
|
189
|
+
const blockedBy = [];
|
|
190
|
+
for (let index = 0; index < deepest; index++) {
|
|
191
|
+
if (!planned.has(applied[index].file)) blockedBy.push(applied[index].file);
|
|
192
|
+
}
|
|
193
|
+
if (blockedBy.length === 0) return;
|
|
194
|
+
throw new MigrationBlockedError(
|
|
195
|
+
`Rollback is blocked: ${blockedBy.length} later migration(s) still applied and not part of ` +
|
|
196
|
+
`it: ${blockedBy.join(', ')}`,
|
|
197
|
+
{ direction: JOB_NAMES.DOWN, names: [...migrations], blockedBy },
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Plan a `down` group: `kit.dryRun('down')` picks the records (so forward-only
|
|
203
|
+
* and not-applied refusals happen here, before anything is enqueued), then an
|
|
204
|
+
* ordered plan is put in revert order — newest applied first.
|
|
205
|
+
*/
|
|
206
|
+
async function planDownJobs(kit, options = {}) {
|
|
207
|
+
const { filename, steps, batch, to, ordered = true, jobOptions } = options;
|
|
208
|
+
if (filename !== undefined) assertMigrationName(filename);
|
|
209
|
+
if (to !== undefined) assertMigrationName(to);
|
|
210
|
+
assertBoolean(ordered, 'ordered');
|
|
211
|
+
assertJobOptions(jobOptions);
|
|
212
|
+
const actor = actorOf(options);
|
|
213
|
+
|
|
214
|
+
const selection = {};
|
|
215
|
+
if (steps !== undefined) selection.steps = steps;
|
|
216
|
+
if (batch !== undefined) selection.batch = batch;
|
|
217
|
+
if (to !== undefined) selection.to = to;
|
|
218
|
+
const rows = await kit.dryRun('down', filename, selection);
|
|
219
|
+
if (ordered) rows.sort(newestFirst);
|
|
220
|
+
|
|
221
|
+
const migrations = [];
|
|
222
|
+
for (const row of rows) migrations.push(row.file);
|
|
223
|
+
const groupId = await newGroupId(kit);
|
|
224
|
+
if (migrations.length === 0) {
|
|
225
|
+
return { groupId, direction: JOB_NAMES.DOWN, batch: null, migrations, jobs: [] };
|
|
226
|
+
}
|
|
227
|
+
if (ordered) await assertTopOfStack(kit, migrations);
|
|
228
|
+
|
|
229
|
+
const jobs = [];
|
|
230
|
+
for (const [index, row] of rows.entries()) {
|
|
231
|
+
jobs.push({
|
|
232
|
+
...buildMigrationJob({
|
|
233
|
+
direction: JOB_NAMES.DOWN,
|
|
234
|
+
migration: row.file,
|
|
235
|
+
groupId,
|
|
236
|
+
index,
|
|
237
|
+
total: rows.length,
|
|
238
|
+
batch: row.batch,
|
|
239
|
+
ordered,
|
|
240
|
+
...actor,
|
|
241
|
+
}),
|
|
242
|
+
opts: migrationJobOptions(jobOptions, JOB_NAMES.DOWN, row.file),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return { groupId, direction: JOB_NAMES.DOWN, batch: null, migrations, jobs };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* For each id, whether the job stored under it was queued by another enqueue
|
|
250
|
+
* call. A deduplicated add hands back the *existing* job's id, so the stored
|
|
251
|
+
* job's group differs from ours — and waiting on that id simply joins the job
|
|
252
|
+
* that will do the work. All false on a queue that cannot read jobs back.
|
|
253
|
+
*/
|
|
254
|
+
async function foreignJobs(queue, ids, groupId) {
|
|
255
|
+
if (typeof queue.getJob !== 'function') return ids.map(() => false);
|
|
256
|
+
// A first deploy can enqueue hundreds of jobs: read them back a few at a time.
|
|
257
|
+
const stored = await mapLimit(ids, LOOKUP_CONCURRENCY, (id) => queue.getJob(id));
|
|
258
|
+
return stored.map((job) => Boolean(job && job.data?.groupId !== groupId));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The `wait()` of an enqueue handle. It waits through the caller's
|
|
263
|
+
* QueueEvents, else the one the facade lends (`getQueueEvents`) — never
|
|
264
|
+
* opened for a group that has nothing to wait for.
|
|
265
|
+
*/
|
|
266
|
+
function makeWait(queue, group, { queueEvents, getQueueEvents } = {}) {
|
|
267
|
+
const waitsForSomething = group.jobs.length > 0 || group.converge !== undefined;
|
|
268
|
+
return (waitOptions = {}) =>
|
|
269
|
+
waitForGroup({
|
|
270
|
+
queue,
|
|
271
|
+
queueEvents:
|
|
272
|
+
waitOptions.queueEvents ??
|
|
273
|
+
queueEvents ??
|
|
274
|
+
(waitsForSomething ? getQueueEvents?.() : undefined),
|
|
275
|
+
...group,
|
|
276
|
+
timeoutMs: waitOptions.timeoutMs,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function assertQueue(queue) {
|
|
281
|
+
if (!queue || typeof queue.addBulk !== 'function') {
|
|
282
|
+
throw new ConfigInvalidError('queue must be a BullMQ Queue (it has no addBulk method)');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Add a planned group to the queue (atomically) and return its handle */
|
|
287
|
+
async function enqueueGroup(queue, kit, plan, { queueEvents, getQueueEvents } = {}) {
|
|
288
|
+
assertQueue(queue);
|
|
289
|
+
const { groupId, direction, batch } = plan;
|
|
290
|
+
let jobs = [];
|
|
291
|
+
const deduplicated = [];
|
|
292
|
+
let converge = null;
|
|
293
|
+
|
|
294
|
+
const specs = plan.converge ? [...plan.jobs, plan.converge] : plan.jobs;
|
|
295
|
+
if (specs.length > 0) {
|
|
296
|
+
const added = await queue.addBulk(specs);
|
|
297
|
+
if (!Array.isArray(added) || added.length !== specs.length) {
|
|
298
|
+
throw new ConfigInvalidError('queue.addBulk did not return one job per migration', {
|
|
299
|
+
expected: specs.length,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
jobs = plan.migrations.map((migration, index) => ({
|
|
303
|
+
id: String(added[index].id),
|
|
304
|
+
migration,
|
|
305
|
+
index,
|
|
306
|
+
}));
|
|
307
|
+
const foreign = await foreignJobs(
|
|
308
|
+
queue,
|
|
309
|
+
added.map((job) => String(job.id)),
|
|
310
|
+
groupId,
|
|
311
|
+
);
|
|
312
|
+
for (const job of jobs) {
|
|
313
|
+
if (foreign[job.index]) deduplicated.push(job.migration);
|
|
314
|
+
}
|
|
315
|
+
if (plan.converge) {
|
|
316
|
+
converge = { id: String(added[specs.length - 1].id), deduplicated: foreign.at(-1) };
|
|
317
|
+
}
|
|
318
|
+
const what =
|
|
319
|
+
jobs.length > 0
|
|
320
|
+
? `${jobs.length} migration(s)${converge ? ' + converge' : ''}`
|
|
321
|
+
: 'a converge job';
|
|
322
|
+
kit.logger.info(
|
|
323
|
+
`⇢ Enqueued ${what} [${direction}${batch !== null ? `, batch ${batch}` : ''}]`,
|
|
324
|
+
{
|
|
325
|
+
groupId,
|
|
326
|
+
direction,
|
|
327
|
+
...(batch !== null ? { batch } : {}),
|
|
328
|
+
count: jobs.length,
|
|
329
|
+
deduplicated: deduplicated.length,
|
|
330
|
+
...(converge ? { converge: true } : {}),
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
groupId,
|
|
337
|
+
direction,
|
|
338
|
+
batch,
|
|
339
|
+
// "No migration to run" — a converge-only group is still up to date.
|
|
340
|
+
upToDate: jobs.length === 0,
|
|
341
|
+
jobs,
|
|
342
|
+
deduplicated,
|
|
343
|
+
converge,
|
|
344
|
+
wait: makeWait(
|
|
345
|
+
queue,
|
|
346
|
+
{ groupId, direction, batch, jobs, ...(converge ? { converge } : {}) },
|
|
347
|
+
{ queueEvents, getQueueEvents },
|
|
348
|
+
),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Enqueue a converge job on its own, on a queue you own: it brings the
|
|
354
|
+
* declared collections to their declared state, under the MongoDB lock.
|
|
355
|
+
* `ordered` (default true) makes it refuse while a migration is still
|
|
356
|
+
* pending — the declared state describes the newest schema.
|
|
357
|
+
*/
|
|
358
|
+
async function enqueueConverge(queue, kit, options = {}, internals = {}) {
|
|
359
|
+
if (!isPlainObject(options)) {
|
|
360
|
+
throw new ConfigInvalidError('enqueueConverge options must be an object');
|
|
361
|
+
}
|
|
362
|
+
const { ordered, jobOptions, queueEvents } = options;
|
|
363
|
+
if (ordered !== undefined) assertBoolean(ordered, 'ordered');
|
|
364
|
+
assertJobOptions(jobOptions);
|
|
365
|
+
assertQueue(queue);
|
|
366
|
+
const actor = actorOf(options);
|
|
367
|
+
const groupId = await newGroupId(kit);
|
|
368
|
+
const [added] = await queue.addBulk([
|
|
369
|
+
buildConvergeJob({ groupId, ordered, jobOptions, ...actor }),
|
|
370
|
+
]);
|
|
371
|
+
if (!added) throw new ConfigInvalidError('queue.addBulk did not return the converge job');
|
|
372
|
+
const jobId = String(added.id);
|
|
373
|
+
const [deduplicated] = await foreignJobs(queue, [jobId], groupId);
|
|
374
|
+
const wait = makeWait(
|
|
375
|
+
queue,
|
|
376
|
+
{ groupId, direction: 'converge', batch: null, jobs: [], converge: { id: jobId } },
|
|
377
|
+
{ queueEvents, getQueueEvents: internals.getQueueEvents },
|
|
378
|
+
);
|
|
379
|
+
kit.logger.info(`⇢ Enqueued a converge job${deduplicated ? ' (already queued)' : ''}`, {
|
|
380
|
+
groupId,
|
|
381
|
+
jobId,
|
|
382
|
+
deduplicated,
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
groupId,
|
|
386
|
+
jobId,
|
|
387
|
+
deduplicated,
|
|
388
|
+
wait: async (waitOptions = {}) => {
|
|
389
|
+
const { converge } = await wait(waitOptions);
|
|
390
|
+
return converge;
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Enqueue pending migrations (all, up to `to`, or one `filename`) as one job
|
|
397
|
+
* each, on a queue you own. `internals` is how the facade lends its lazily
|
|
398
|
+
* built QueueEvents to `wait()`.
|
|
399
|
+
*/
|
|
400
|
+
async function enqueueUp(queue, kit, options = {}, internals = {}) {
|
|
401
|
+
const { queueEvents, ...planOptions } = options;
|
|
402
|
+
return enqueueGroup(queue, kit, await planUpJobs(kit, planOptions), {
|
|
403
|
+
queueEvents,
|
|
404
|
+
...internals,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Enqueue a rollback (last batch, a `batch`, `steps`, back `to`, or one `filename`) */
|
|
409
|
+
async function enqueueDown(queue, kit, options = {}, internals = {}) {
|
|
410
|
+
const { queueEvents, ...planOptions } = options;
|
|
411
|
+
return enqueueGroup(queue, kit, await planDownJobs(kit, planOptions), {
|
|
412
|
+
queueEvents,
|
|
413
|
+
...internals,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
assertJobOptions,
|
|
419
|
+
enqueueConverge,
|
|
420
|
+
enqueueDown,
|
|
421
|
+
enqueueUp,
|
|
422
|
+
planDownJobs,
|
|
423
|
+
planUpJobs,
|
|
424
|
+
};
|