@alexify/migronaut 2.1.0 → 2.3.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 +223 -0
- package/README.md +68 -10
- package/bullmq.d.ts +465 -7
- package/index.d.ts +1272 -18
- package/migronaut.schema.json +150 -2
- package/package.json +8 -2
- package/src/bullmq/background-processor.js +469 -0
- package/src/bullmq/index.js +12 -0
- package/src/bullmq/jobs.js +254 -7
- package/src/bullmq/processor.js +153 -15
- package/src/bullmq/producer.js +202 -27
- package/src/bullmq/service.js +480 -45
- package/src/cli/commands/background.js +500 -0
- package/src/cli/commands/converge.js +38 -10
- package/src/cli/commands/create.js +6 -0
- package/src/cli/exit-codes.js +6 -0
- package/src/cli/index.js +2 -0
- package/src/cli/table.js +68 -9
- package/src/core/audit.js +98 -3
- package/src/core/background-audit.js +139 -0
- package/src/core/background-drift.js +126 -0
- package/src/core/background-dry-run.js +366 -0
- package/src/core/background-engine.js +818 -0
- package/src/core/background-kit.js +425 -0
- package/src/core/background-partition.js +298 -0
- package/src/core/background-runner.js +305 -0
- package/src/core/background-sandbox.js +701 -0
- package/src/core/background-shard.js +542 -0
- package/src/core/background-spec.js +597 -0
- package/src/core/background-store.js +951 -0
- package/src/core/background-throttle.js +269 -0
- package/src/core/background-watch-plan.js +164 -0
- package/src/core/background-watch-store.js +78 -0
- package/src/core/background-watch.js +605 -0
- package/src/core/background.js +1121 -0
- package/src/core/bson-peer.js +23 -0
- package/src/core/changelog.js +32 -0
- package/src/core/collections.js +125 -31
- package/src/core/config.js +133 -13
- package/src/core/converge-plan.js +343 -61
- package/src/core/converge-search-run.js +440 -0
- package/src/core/converge-search.js +404 -0
- package/src/core/converge.js +428 -183
- package/src/core/index-spec.js +27 -16
- package/src/core/lock.js +97 -32
- package/src/core/migrator.js +951 -26
- package/src/core/options.js +32 -1
- package/src/core/run.js +26 -12
- package/src/core/runner.js +1 -1
- package/src/core/search-index-spec.js +758 -0
- package/src/core/server-info.js +70 -0
- package/src/core/shard-info.js +76 -0
- package/src/core/versioning-spec.js +181 -0
- package/src/errors/index.js +97 -5
- package/src/index.js +16 -0
- package/src/utils/canonical.js +34 -1
- package/src/utils/error.js +11 -2
- package/src/utils/loader.js +77 -9
- package/src/utils/migration-name.js +33 -1
- package/src/utils/telemetry.js +125 -1
- package/src/utils/template.js +69 -1
- package/src/versioning/config.js +155 -0
- package/src/versioning/document.js +326 -0
- package/src/versioning/index.js +50 -0
- package/src/versioning/internal.js +279 -0
- package/src/versioning/mongoose.js +151 -0
- package/src/versioning/occ.js +318 -0
- package/src/versioning/registry.js +187 -0
- package/src/versioning/upcaster.js +213 -0
- package/versioning.d.ts +666 -0
- package/versioning.js +1 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
const { deepEqual } = require('../utils/canonical.js');
|
|
2
|
-
const {
|
|
2
|
+
const { shardedVersionIndexKey, versionFloorConflict } = require('./versioning-spec.js');
|
|
3
|
+
const {
|
|
4
|
+
compareIndex,
|
|
5
|
+
normalizeDeclaredIndex,
|
|
6
|
+
normalizeLiveIndex,
|
|
7
|
+
restoreSpec,
|
|
8
|
+
sameSignature,
|
|
9
|
+
} = require('./index-spec.js');
|
|
10
|
+
const {
|
|
11
|
+
compareSearchIndex,
|
|
12
|
+
isBeingRemoved,
|
|
13
|
+
normalizeLiveSearchIndex,
|
|
14
|
+
searchBuild,
|
|
15
|
+
searchIndexSpec,
|
|
16
|
+
searchIndexValue,
|
|
17
|
+
} = require('./search-index-spec.js');
|
|
3
18
|
|
|
4
19
|
/**
|
|
5
20
|
* The converge planner: one declared collection against its live state, as
|
|
@@ -17,17 +32,35 @@ const { compareIndex, normalizeLiveIndex, restoreSpec, sameSignature } = require
|
|
|
17
32
|
* - undeclared indexes are kept (and reported) unless `prune` is on;
|
|
18
33
|
* - a rebuild that drops a unique index to build a unique one back is a
|
|
19
34
|
* conflict unless `rebuildUnique` is on — see UNIQUE_REBUILD_REASON;
|
|
20
|
-
* - `_id_` and a clustered index are the collection's own and never listed
|
|
35
|
+
* - `_id_` and a clustered index are the collection's own and never listed;
|
|
36
|
+
* - a search index is created or updated in place — never dropped to be built
|
|
37
|
+
* again: a search against a missing index returns nothing rather than fail,
|
|
38
|
+
* so a rebuild would be a silent outage. What no update can change (the
|
|
39
|
+
* type, an autoEmbed field's model or size) is a conflict instead.
|
|
21
40
|
*/
|
|
22
41
|
|
|
23
42
|
/** Actions that change the database — what a plan "would do" and a run "did" */
|
|
24
43
|
const CHANGE_ACTIONS = new Set(['create', 'modify', 'recreate', 'drop']);
|
|
25
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Actions of a declared search index that leaves it on the server — the
|
|
47
|
+
* ones whose build is worth reporting, or waiting for
|
|
48
|
+
*/
|
|
49
|
+
const SERVED_ACTIONS = new Set(['create', 'modify', 'unchanged']);
|
|
50
|
+
|
|
51
|
+
/** A row's target as people read it — where the code's name is not plain English */
|
|
52
|
+
const TARGET_LABELS = Object.freeze({ searchIndex: 'search index' });
|
|
53
|
+
|
|
26
54
|
/** Server defaults for a collection that has a validator but did not say how to apply it */
|
|
27
55
|
const VALIDATOR_DEFAULTS = { validationLevel: 'strict', validationAction: 'error' };
|
|
28
56
|
|
|
29
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Whether a row drops or rebuilds an index — or drops a search index (a
|
|
59
|
+
* search index is updated in place, never rebuilt: the old one serves until
|
|
60
|
+
* the new definition is built).
|
|
61
|
+
*/
|
|
30
62
|
function isDestructive(action) {
|
|
63
|
+
if (action.target === 'searchIndex') return action.action === 'drop';
|
|
31
64
|
return action.target === 'index' && (action.action === 'drop' || action.action === 'recreate');
|
|
32
65
|
}
|
|
33
66
|
|
|
@@ -140,12 +173,63 @@ function dropsUniqueConstraint(declared, drops) {
|
|
|
140
173
|
*/
|
|
141
174
|
function backsShardKey(index, shardKey) {
|
|
142
175
|
if (!shardKey) return false;
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
176
|
+
let position = 0;
|
|
177
|
+
for (const field of Object.keys(shardKey)) {
|
|
178
|
+
if (index.serverKey[position]?.[0] !== field) return false;
|
|
179
|
+
position += 1;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
146
182
|
}
|
|
147
183
|
|
|
148
|
-
|
|
184
|
+
/** `first; second`, or `second` alone when there is no first */
|
|
185
|
+
function joinReasons(first, second) {
|
|
186
|
+
return first ? `${first}; ${second}` : second;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Why the ordinary version index stays on a sharded collection whose version index replaced it */
|
|
190
|
+
const DISPLACED_REASON =
|
|
191
|
+
'replaced by the shard-key-prefixed version index — drop it once nothing hints it ' +
|
|
192
|
+
'(prune does, when the indexes are declared)';
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The declared indexes as they apply to this live collection: on a sharded
|
|
196
|
+
* one, the version index takes the shard key between the version field and
|
|
197
|
+
* `_id` (versioning-spec.js). `displaced` names the ordinary version index it
|
|
198
|
+
* replaces, when the two differ.
|
|
199
|
+
*/
|
|
200
|
+
function indexesFor(definition, live) {
|
|
201
|
+
const indexes = definition.indexes;
|
|
202
|
+
if (indexes === undefined || !definition.versioning || !live.shardKey) return { indexes };
|
|
203
|
+
const sharded = normalizeDeclaredIndex({
|
|
204
|
+
key: shardedVersionIndexKey(definition.versioning, live.shardKey),
|
|
205
|
+
});
|
|
206
|
+
const result = [];
|
|
207
|
+
let displaced;
|
|
208
|
+
for (const index of indexes) {
|
|
209
|
+
if (index.versionIndex === true && index.name !== sharded.name) {
|
|
210
|
+
displaced = index.name;
|
|
211
|
+
result.push({ ...sharded, versionIndex: true });
|
|
212
|
+
} else {
|
|
213
|
+
result.push(index);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { indexes: result, displaced };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Plan the declared indexes of one collection against its live ones. With
|
|
221
|
+
* `partial` (only the version index of `versioning` is declared, not the
|
|
222
|
+
* collection's own list) the other live indexes are not managed at all: never
|
|
223
|
+
* listed, never dropped — prune does not reach them.
|
|
224
|
+
*/
|
|
225
|
+
function planIndexes(
|
|
226
|
+
declaredIndexes,
|
|
227
|
+
live,
|
|
228
|
+
{ prune: pruneOption, rebuildUnique, capabilities, partial = false, displaced },
|
|
229
|
+
row,
|
|
230
|
+
steps,
|
|
231
|
+
) {
|
|
232
|
+
const prune = pruneOption && !partial;
|
|
149
233
|
const defaultCollation = live.options?.collation;
|
|
150
234
|
const liveIndexes = [];
|
|
151
235
|
for (const raw of live.indexes) {
|
|
@@ -267,9 +351,7 @@ function planIndexes(declaredIndexes, live, { prune, rebuildUnique, capabilities
|
|
|
267
351
|
}
|
|
268
352
|
drops.push(...blockers);
|
|
269
353
|
action.action = 'recreate';
|
|
270
|
-
action.reason = identical
|
|
271
|
-
? 'name'
|
|
272
|
-
: [item.reason, `replaces ${names}`].filter(Boolean).join('; ');
|
|
354
|
+
action.reason = identical ? 'name' : joinReasons(item.reason, `replaces ${names}`);
|
|
273
355
|
}
|
|
274
356
|
|
|
275
357
|
if (drops.length === 0) {
|
|
@@ -278,7 +360,7 @@ function planIndexes(declaredIndexes, live, { prune, rebuildUnique, capabilities
|
|
|
278
360
|
// Not done unasked: the CLI asks with --rebuild-unique, while the paths
|
|
279
361
|
// nobody watches (after up, a queue job) never get this far on their own.
|
|
280
362
|
action.action = 'conflict';
|
|
281
|
-
action.reason =
|
|
363
|
+
action.reason = joinReasons(action.reason, UNIQUE_REBUILD_REASON);
|
|
282
364
|
} else {
|
|
283
365
|
rebuilds.push({ declared, action, drops });
|
|
284
366
|
}
|
|
@@ -287,16 +369,16 @@ function planIndexes(declaredIndexes, live, { prune, rebuildUnique, capabilities
|
|
|
287
369
|
// A create that collides with an index another rebuild is about to drop has
|
|
288
370
|
// to wait for that drop — and two rebuilds that swap keys each wait for the
|
|
289
371
|
// other. Such entangled ones run as one group: every drop, then every create.
|
|
290
|
-
|
|
372
|
+
// Each doomed index keeps its rebuild and itself, so no lookup is needed below.
|
|
373
|
+
const doomed = [];
|
|
291
374
|
for (const rebuild of rebuilds) {
|
|
292
|
-
for (const index of rebuild.drops) doomed.
|
|
375
|
+
for (const index of rebuild.drops) doomed.push({ owner: rebuild, index });
|
|
293
376
|
}
|
|
294
377
|
const entangled = new Set();
|
|
295
|
-
for (const
|
|
296
|
-
for (const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (collides(item.declared, index)) {
|
|
378
|
+
for (const list of [creates, rebuilds]) {
|
|
379
|
+
for (const item of list) {
|
|
380
|
+
for (const { owner, index } of doomed) {
|
|
381
|
+
if (owner === item || !collides(item.declared, index)) continue;
|
|
300
382
|
entangled.add(item);
|
|
301
383
|
entangled.add(owner);
|
|
302
384
|
}
|
|
@@ -313,20 +395,37 @@ function planIndexes(declaredIndexes, live, { prune, rebuildUnique, capabilities
|
|
|
313
395
|
}
|
|
314
396
|
steps.push(...modifies);
|
|
315
397
|
const group = { op: 'rebuild', drops: [], creates: [], actions: [] };
|
|
316
|
-
for (const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
398
|
+
for (const list of [rebuilds, waiting]) {
|
|
399
|
+
for (const item of list) {
|
|
400
|
+
const step = entangled.has(item)
|
|
401
|
+
? group
|
|
402
|
+
: { op: 'rebuild', drops: [], creates: [], actions: [] };
|
|
403
|
+
for (const index of item.drops ?? []) {
|
|
404
|
+
step.drops.push({ name: index.name, restore: restoreSpec(index.raw) });
|
|
405
|
+
}
|
|
406
|
+
step.creates.push({ spec: item.declared.spec, action: item.action });
|
|
407
|
+
step.actions.push(item.action);
|
|
408
|
+
if (step !== group) steps.push(step);
|
|
322
409
|
}
|
|
323
|
-
step.creates.push({ spec: item.declared.spec, action: item.action });
|
|
324
|
-
step.actions.push(item.action);
|
|
325
|
-
if (step !== group) steps.push(step);
|
|
326
410
|
}
|
|
327
411
|
if (group.actions.length > 0) steps.push(group);
|
|
328
412
|
|
|
413
|
+
// The ordinary version index a sharded one replaced: kept, and said so —
|
|
414
|
+
// a background migration may still be hinting it.
|
|
415
|
+
const old = displaced !== undefined ? byName.get(displaced) : undefined;
|
|
416
|
+
if (old !== undefined && !declaredNames.has(displaced) && !prune) {
|
|
417
|
+
consumed.add(displaced);
|
|
418
|
+
row({
|
|
419
|
+
target: 'index',
|
|
420
|
+
name: displaced,
|
|
421
|
+
action: 'keep',
|
|
422
|
+
reason: DISPLACED_REASON,
|
|
423
|
+
from: indexValue(old.raw),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
329
427
|
// Last, so an index is only ever removed once everything declared exists.
|
|
428
|
+
if (partial) return;
|
|
330
429
|
for (const index of liveIndexes) {
|
|
331
430
|
if (consumed.has(index.name) || declaredNames.has(index.name)) continue;
|
|
332
431
|
if (prune && backsShardKey(index, live.shardKey)) {
|
|
@@ -358,12 +457,173 @@ function planIndexes(declaredIndexes, live, { prune, rebuildUnique, capabilities
|
|
|
358
457
|
}
|
|
359
458
|
}
|
|
360
459
|
|
|
460
|
+
/** The way out of a change a search index cannot make in place */
|
|
461
|
+
const NEW_NAME_RECIPE =
|
|
462
|
+
'declare it under a new name, converge, then remove the old declaration and converge with prune';
|
|
463
|
+
|
|
464
|
+
/** Why a declared search index is not planned on a server without Atlas Search */
|
|
465
|
+
const SEARCH_UNAVAILABLE_REASON = 'Atlas Search is not available on this server';
|
|
466
|
+
|
|
467
|
+
function typeChangeReason(from, to) {
|
|
468
|
+
return `the type cannot change in place (${from} → ${to}) — ${NEW_NAME_RECIPE}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function autoEmbedReason(changes) {
|
|
472
|
+
return `autoEmbed ${changes.join(', ')} cannot change in place — ${NEW_NAME_RECIPE}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** `mappings.fields.title.norms, storedSource (+2 more)` */
|
|
476
|
+
function diffReason(paths, more) {
|
|
477
|
+
return `${paths.join(', ')}${more > 0 ? ` (+${more} more)` : ''}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function deletingReason(status) {
|
|
481
|
+
return `is being deleted on the server (${status}) — converge again once it is gone`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Plan the search indexes of one collection. Submissions (creates, then
|
|
486
|
+
* updates) go to `submit`, drops to `drops` — converge.js sends submissions
|
|
487
|
+
* before the regular index builds (the server builds a search index in the
|
|
488
|
+
* background) and drops last of all.
|
|
489
|
+
*/
|
|
490
|
+
function planSearchIndexes(declaredList, live, { prune, search }, row, submit, drops) {
|
|
491
|
+
if (!search.available) {
|
|
492
|
+
// Nothing to compare with: every declaration is refused, or skipped when
|
|
493
|
+
// the configuration says a server without Search is expected.
|
|
494
|
+
for (const declared of declaredList) {
|
|
495
|
+
row({
|
|
496
|
+
target: 'searchIndex',
|
|
497
|
+
name: declared.name,
|
|
498
|
+
action: search.onUnavailable === 'skip' ? 'skip' : 'conflict',
|
|
499
|
+
reason: SEARCH_UNAVAILABLE_REASON,
|
|
500
|
+
to: searchIndexValue(declared),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// Normalized and indexed by name in one pass; the map's order is the server's.
|
|
506
|
+
const byName = new Map();
|
|
507
|
+
for (const raw of live.searchIndexes ?? []) {
|
|
508
|
+
const index = normalizeLiveSearchIndex(raw);
|
|
509
|
+
byName.set(index.name, index);
|
|
510
|
+
}
|
|
511
|
+
const declaredNames = new Set();
|
|
512
|
+
|
|
513
|
+
const specs = [];
|
|
514
|
+
const created = [];
|
|
515
|
+
const updates = [];
|
|
516
|
+
for (const declared of declaredList) {
|
|
517
|
+
declaredNames.add(declared.name);
|
|
518
|
+
const current = byName.get(declared.name);
|
|
519
|
+
const to = searchIndexValue(declared);
|
|
520
|
+
if (!current) {
|
|
521
|
+
specs.push(searchIndexSpec(declared));
|
|
522
|
+
created.push(row({ target: 'searchIndex', name: declared.name, action: 'create', to }));
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const from = searchIndexValue(current);
|
|
526
|
+
const build = searchBuild(current);
|
|
527
|
+
if (isBeingRemoved(current)) {
|
|
528
|
+
row({
|
|
529
|
+
target: 'searchIndex',
|
|
530
|
+
name: declared.name,
|
|
531
|
+
action: 'conflict',
|
|
532
|
+
reason: deletingReason(current.status),
|
|
533
|
+
build,
|
|
534
|
+
});
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const { diffs, paths, more, typeChange, immutable, ignored } = compareSearchIndex(
|
|
538
|
+
declared,
|
|
539
|
+
current,
|
|
540
|
+
);
|
|
541
|
+
// Options only the server reports, left out of the comparison: named on the row.
|
|
542
|
+
const tolerated = ignored.length > 0 ? { ignored } : {};
|
|
543
|
+
if (diffs.length === 0) {
|
|
544
|
+
row({ target: 'searchIndex', name: declared.name, action: 'unchanged', build, ...tolerated });
|
|
545
|
+
} else if (typeChange || immutable.length > 0) {
|
|
546
|
+
row({
|
|
547
|
+
target: 'searchIndex',
|
|
548
|
+
name: declared.name,
|
|
549
|
+
action: 'conflict',
|
|
550
|
+
reason: typeChange
|
|
551
|
+
? typeChangeReason(current.type, declared.type)
|
|
552
|
+
: autoEmbedReason(immutable),
|
|
553
|
+
from,
|
|
554
|
+
to,
|
|
555
|
+
build,
|
|
556
|
+
...tolerated,
|
|
557
|
+
});
|
|
558
|
+
} else {
|
|
559
|
+
const action = row({
|
|
560
|
+
target: 'searchIndex',
|
|
561
|
+
name: declared.name,
|
|
562
|
+
action: 'modify',
|
|
563
|
+
reason: diffReason(paths, more),
|
|
564
|
+
from,
|
|
565
|
+
to,
|
|
566
|
+
build,
|
|
567
|
+
...tolerated,
|
|
568
|
+
});
|
|
569
|
+
updates.push({
|
|
570
|
+
op: 'updateSearchIndex',
|
|
571
|
+
name: declared.name,
|
|
572
|
+
type: declared.type,
|
|
573
|
+
definition: declared.definition,
|
|
574
|
+
...(current.version !== undefined ? { sinceVersion: current.version } : {}),
|
|
575
|
+
actions: [action],
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (specs.length > 0) submit.push({ op: 'createSearchIndexes', specs, actions: created });
|
|
580
|
+
submit.push(...updates);
|
|
581
|
+
|
|
582
|
+
for (const index of byName.values()) {
|
|
583
|
+
if (declaredNames.has(index.name)) continue;
|
|
584
|
+
const from = searchIndexValue(index);
|
|
585
|
+
const build = searchBuild(index);
|
|
586
|
+
if (isBeingRemoved(index)) {
|
|
587
|
+
// On its way out already — dropping it again would only fail.
|
|
588
|
+
row({
|
|
589
|
+
target: 'searchIndex',
|
|
590
|
+
name: index.name,
|
|
591
|
+
action: 'keep',
|
|
592
|
+
reason: 'being deleted',
|
|
593
|
+
from,
|
|
594
|
+
build,
|
|
595
|
+
});
|
|
596
|
+
} else if (prune) {
|
|
597
|
+
const action = row({
|
|
598
|
+
target: 'searchIndex',
|
|
599
|
+
name: index.name,
|
|
600
|
+
action: 'drop',
|
|
601
|
+
reason: 'not declared',
|
|
602
|
+
from,
|
|
603
|
+
build,
|
|
604
|
+
});
|
|
605
|
+
drops.push({ op: 'dropSearchIndex', name: index.name, actions: [action] });
|
|
606
|
+
} else {
|
|
607
|
+
row({
|
|
608
|
+
target: 'searchIndex',
|
|
609
|
+
name: index.name,
|
|
610
|
+
action: 'keep',
|
|
611
|
+
reason: 'not declared',
|
|
612
|
+
from,
|
|
613
|
+
build,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
361
619
|
/**
|
|
362
620
|
* Plan one collection. `definition` is a normalized definition (see
|
|
363
|
-
* collections.js); `live` is `{ exists, type?, options?, indexes
|
|
364
|
-
* converge.js reads it.
|
|
365
|
-
* result rows (status `'planned'`), `steps` the operations
|
|
366
|
-
* out, in execution order, each pointing at the rows it
|
|
621
|
+
* collections.js); `live` is `{ exists, type?, options?, indexes,
|
|
622
|
+
* searchIndexes?, shardKey?, versionFloor? }` as converge.js reads it.
|
|
623
|
+
* Returns `{ name, actions, steps }`: `actions` are the result rows (status `'planned'`), `steps` the operations
|
|
624
|
+
* that carry them out, in execution order, each pointing at the rows it
|
|
625
|
+
* settles. `search` is `{ available, onUnavailable }` — whether the server has
|
|
626
|
+
* Atlas Search, and what a declared search index becomes when it does not.
|
|
367
627
|
*/
|
|
368
628
|
/**
|
|
369
629
|
* Fold every run of consecutive `createIndex` steps into one `createIndexes`:
|
|
@@ -393,10 +653,12 @@ function planCollection(definition, live, options = {}) {
|
|
|
393
653
|
return { ...plan, steps: batchCreates(plan.steps) };
|
|
394
654
|
}
|
|
395
655
|
|
|
656
|
+
const SEARCH_AVAILABLE = Object.freeze({ available: true, onUnavailable: 'fail' });
|
|
657
|
+
|
|
396
658
|
function planCollectionSteps(
|
|
397
659
|
definition,
|
|
398
660
|
live,
|
|
399
|
-
{ prune = false, rebuildUnique = false, capabilities = {} } = {},
|
|
661
|
+
{ prune = false, rebuildUnique = false, capabilities = {}, search = SEARCH_AVAILABLE } = {},
|
|
400
662
|
) {
|
|
401
663
|
const name = definition.name;
|
|
402
664
|
const actions = [];
|
|
@@ -418,17 +680,28 @@ function planCollectionSteps(
|
|
|
418
680
|
}
|
|
419
681
|
|
|
420
682
|
const desired = desiredValidator(definition);
|
|
421
|
-
const declaredIndexes = definition
|
|
683
|
+
const { indexes: declaredIndexes, displaced } = indexesFor(definition, live);
|
|
684
|
+
const declaredSearch = definition.searchIndexes;
|
|
685
|
+
const indexSteps = [];
|
|
686
|
+
const searchSubmit = [];
|
|
687
|
+
const searchDrops = [];
|
|
422
688
|
|
|
423
689
|
if (!live.exists) {
|
|
424
690
|
// Nothing worth creating an empty collection for — a definition that only
|
|
425
|
-
// says "no validator" is already true of a collection that does not exist
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
if (desired) {
|
|
429
|
-
linked
|
|
691
|
+
// says "no validator" is already true of a collection that does not exist,
|
|
692
|
+
// and so is "no search indexes" (or any, on a server without Search).
|
|
693
|
+
const wantsSearch = search.available && declaredSearch?.length > 0;
|
|
694
|
+
if (desired || declaredIndexes?.length > 0 || wantsSearch) {
|
|
695
|
+
const linked = [row({ target: 'collection', name, action: 'create' })];
|
|
696
|
+
if (desired) {
|
|
697
|
+
linked.push(row({ target: 'validator', name, action: 'create', to: desired }));
|
|
698
|
+
}
|
|
699
|
+
steps.push({
|
|
700
|
+
op: 'createCollection',
|
|
701
|
+
options: desired ? { ...desired } : {},
|
|
702
|
+
actions: linked,
|
|
703
|
+
});
|
|
430
704
|
}
|
|
431
|
-
steps.push({ op: 'createCollection', options: desired ? { ...desired } : {}, actions: linked });
|
|
432
705
|
for (const declared of declaredIndexes ?? []) {
|
|
433
706
|
const action = row({
|
|
434
707
|
target: 'index',
|
|
@@ -436,40 +709,50 @@ function planCollectionSteps(
|
|
|
436
709
|
action: 'create',
|
|
437
710
|
to: indexValue(declared.spec),
|
|
438
711
|
});
|
|
439
|
-
|
|
712
|
+
indexSteps.push({ op: 'createIndex', spec: declared.spec, actions: [action] });
|
|
440
713
|
}
|
|
714
|
+
if (declaredSearch !== undefined) {
|
|
715
|
+
const none = { searchIndexes: [] };
|
|
716
|
+
planSearchIndexes(declaredSearch, none, { prune, search }, row, searchSubmit, searchDrops);
|
|
717
|
+
}
|
|
718
|
+
steps.push(...searchSubmit, ...indexSteps);
|
|
441
719
|
return { name, actions, steps };
|
|
442
720
|
}
|
|
443
721
|
|
|
444
722
|
if (desired !== undefined) {
|
|
445
|
-
|
|
723
|
+
// Raising versioning.min over documents still below it: refused, so the
|
|
724
|
+
// whole run stops before its first write (see versionFloorToCheck).
|
|
725
|
+
const refused = versionFloorConflict(live.versionFloor);
|
|
726
|
+
if (refused) {
|
|
727
|
+
row({ target: 'validator', name, action: 'conflict', reason: refused, to: desired });
|
|
728
|
+
} else {
|
|
729
|
+
planValidator(name, desired, liveValidator(live.options), row, steps);
|
|
730
|
+
}
|
|
446
731
|
}
|
|
447
732
|
if (declaredIndexes !== undefined) {
|
|
448
|
-
|
|
733
|
+
const partial = definition.indexesPartial === true;
|
|
734
|
+
planIndexes(
|
|
735
|
+
declaredIndexes,
|
|
736
|
+
live,
|
|
737
|
+
{ prune, rebuildUnique, capabilities, partial, displaced },
|
|
738
|
+
row,
|
|
739
|
+
indexSteps,
|
|
740
|
+
);
|
|
449
741
|
}
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/** Counts over planned (or executed) collections */
|
|
454
|
-
function summarize(collections) {
|
|
455
|
-
let changes = 0;
|
|
456
|
-
let applied = 0;
|
|
457
|
-
let conflicts = 0;
|
|
458
|
-
let destructive = 0;
|
|
459
|
-
for (const collection of collections) {
|
|
460
|
-
for (const action of collection.actions) {
|
|
461
|
-
if (action.action === 'conflict') conflicts += 1;
|
|
462
|
-
if (!CHANGE_ACTIONS.has(action.action)) continue;
|
|
463
|
-
changes += 1;
|
|
464
|
-
if (action.status === 'applied') applied += 1;
|
|
465
|
-
if (isDestructive(action)) destructive += 1;
|
|
466
|
-
}
|
|
742
|
+
if (declaredSearch !== undefined) {
|
|
743
|
+
planSearchIndexes(declaredSearch, live, { prune, search }, row, searchSubmit, searchDrops);
|
|
467
744
|
}
|
|
468
|
-
return
|
|
745
|
+
// Search submissions return at once and build in the background, so they go
|
|
746
|
+
// before the regular index builds; every drop still comes last.
|
|
747
|
+
steps.push(...searchSubmit, ...indexSteps, ...searchDrops);
|
|
748
|
+
return { name, actions, steps };
|
|
469
749
|
}
|
|
470
750
|
|
|
471
751
|
module.exports = {
|
|
472
752
|
CHANGE_ACTIONS,
|
|
753
|
+
SEARCH_UNAVAILABLE_REASON,
|
|
754
|
+
SERVED_ACTIONS,
|
|
755
|
+
TARGET_LABELS,
|
|
473
756
|
UNIQUE_REBUILD_REASON,
|
|
474
757
|
VALIDATOR_DEFAULTS,
|
|
475
758
|
desiredValidator,
|
|
@@ -479,5 +762,4 @@ module.exports = {
|
|
|
479
762
|
needsConfirmation,
|
|
480
763
|
liveValidator,
|
|
481
764
|
planCollection,
|
|
482
|
-
summarize,
|
|
483
765
|
};
|