@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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The driver's BSON, from the `mongodb` peer — loaded on first use, never at
|
|
3
|
+
* startup, and from this one module only (pinned by a test), so nothing else
|
|
4
|
+
* under src/ grows its own dependency on the driver's internals.
|
|
5
|
+
*
|
|
6
|
+
* What it is for: measuring a document the way the server will (a step's
|
|
7
|
+
* checkpoint must stay small) and writing one as relaxed EJSON for a dry
|
|
8
|
+
* run's report — both things only BSON itself can do exactly.
|
|
9
|
+
*/
|
|
10
|
+
let bson;
|
|
11
|
+
|
|
12
|
+
function loadBson() {
|
|
13
|
+
bson ??= require('mongodb').BSON;
|
|
14
|
+
return bson;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The size of `value` as a BSON document, in bytes */
|
|
18
|
+
const bsonSize = (value) => loadBson().calculateObjectSize(value);
|
|
19
|
+
|
|
20
|
+
/** `value` as relaxed EJSON — plain JSON a person can read, types tagged where JSON has none */
|
|
21
|
+
const toRelaxedEjson = (value) => loadBson().EJSON.serialize(value, { relaxed: true });
|
|
22
|
+
|
|
23
|
+
module.exports = { bsonSize, loadBson, toRelaxedEjson };
|
package/src/core/changelog.js
CHANGED
|
@@ -109,6 +109,26 @@ class Changelog {
|
|
|
109
109
|
return failed;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Which of `names` are applied from a history that predates migronaut — a
|
|
114
|
+
* `baseline` or an import from migrate-mongo: never run here, so a
|
|
115
|
+
* background migration among them has no state, and counts as done.
|
|
116
|
+
*/
|
|
117
|
+
async getAdoptedNames(db, names) {
|
|
118
|
+
if (names.length === 0) return [];
|
|
119
|
+
const docs = await this.#coll(db)
|
|
120
|
+
.find({
|
|
121
|
+
status: 'applied',
|
|
122
|
+
origin: { $in: ['baseline', 'migrate-mongo'] },
|
|
123
|
+
name: { $in: names },
|
|
124
|
+
})
|
|
125
|
+
.project({ name: 1, _id: 0 })
|
|
126
|
+
.toArray();
|
|
127
|
+
const adopted = [];
|
|
128
|
+
for (const doc of docs) adopted.push(doc.name);
|
|
129
|
+
return adopted;
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
/** Return a single record by migration name, or null */
|
|
113
133
|
async getByName(db, name) {
|
|
114
134
|
return this.#coll(db).findOne({ name });
|
|
@@ -292,6 +312,18 @@ class Changelog {
|
|
|
292
312
|
await this.#coll(db).bulkWrite(ops, { ordered: false });
|
|
293
313
|
}
|
|
294
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Re-pin an applied record to the file now on disk — `background repin`,
|
|
317
|
+
* so the strict drift check agrees with what is actually running.
|
|
318
|
+
*/
|
|
319
|
+
async setChecksum(db, name, checksum) {
|
|
320
|
+
const result = await this.#coll(db).updateOne(
|
|
321
|
+
{ name, status: 'applied' },
|
|
322
|
+
{ $set: { checksum } },
|
|
323
|
+
);
|
|
324
|
+
return result.matchedCount === 1;
|
|
325
|
+
}
|
|
326
|
+
|
|
295
327
|
/**
|
|
296
328
|
* Mark a migration as reverted. Sets `status='reverted'` and `revertedAt=now`.
|
|
297
329
|
* Never deletes the record — preserves the full audit history.
|
package/src/core/collections.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
const fs = require('node:fs/promises');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const { ConfigInvalidError } = require('../errors/index.js');
|
|
4
|
-
const { isPlainObject,
|
|
4
|
+
const { isPlainObject, toWire, unsendable } = require('../utils/canonical.js');
|
|
5
5
|
const { isCollectionName } = require('../utils/collection-name.js');
|
|
6
6
|
const { mapLimit } = require('../utils/concurrency.js');
|
|
7
7
|
const { errorText } = require('../utils/error.js');
|
|
8
8
|
const { importUserFile, tsLoadMessageOrNull } = require('../utils/loader.js');
|
|
9
9
|
const { indexIssues, normalizeDeclaredIndex, sameDeclaredSignature } = require('./index-spec.js');
|
|
10
|
+
const { normalizeDeclaredSearchIndex, searchIndexIssues } = require('./search-index-spec.js');
|
|
11
|
+
const { resolveVersioning, versioningIssues } = require('../versioning/config.js');
|
|
12
|
+
const {
|
|
13
|
+
isVersioningIndexKey,
|
|
14
|
+
mergeVersioningValidator,
|
|
15
|
+
validatorVersioningIssues,
|
|
16
|
+
versioningIndex,
|
|
17
|
+
} = require('./versioning-spec.js');
|
|
10
18
|
|
|
11
19
|
/**
|
|
12
20
|
* Declared collections: validating a definition, normalizing it for the
|
|
@@ -23,10 +31,12 @@ const { indexIssues, normalizeDeclaredIndex, sameDeclaredSignature } = require('
|
|
|
23
31
|
const DEFINITION_KEYS = [
|
|
24
32
|
'name',
|
|
25
33
|
'indexes',
|
|
34
|
+
'searchIndexes',
|
|
26
35
|
'validator',
|
|
27
36
|
'validationLevel',
|
|
28
37
|
'validationAction',
|
|
29
38
|
'prune',
|
|
39
|
+
'versioning',
|
|
30
40
|
];
|
|
31
41
|
const DEFINITION_KEY_SET = new Set(DEFINITION_KEYS);
|
|
32
42
|
const VALIDATION_LEVELS = ['off', 'strict', 'moderate'];
|
|
@@ -40,27 +50,6 @@ function join(base, key) {
|
|
|
40
50
|
return base.endsWith(':') ? `${base} ${key}` : `${base}.${key}`;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
/** What makes a validator unsendable — a function, a symbol, a cycle — or null */
|
|
44
|
-
function unsendable(value, seen = new Set()) {
|
|
45
|
-
if (seen.size === 0) {
|
|
46
|
-
const issue = regExpIssue(value);
|
|
47
|
-
if (issue) return issue;
|
|
48
|
-
}
|
|
49
|
-
const type = typeof value;
|
|
50
|
-
if (type === 'function') return 'must not contain functions';
|
|
51
|
-
if (type === 'symbol') return 'must not contain symbols';
|
|
52
|
-
if (value === null || type !== 'object') return null;
|
|
53
|
-
if (seen.has(value)) return 'must not contain circular references';
|
|
54
|
-
seen.add(value);
|
|
55
|
-
const items = Array.isArray(value) ? value : isPlainObject(value) ? Object.values(value) : [];
|
|
56
|
-
for (const item of items) {
|
|
57
|
-
const reason = unsendable(item, seen);
|
|
58
|
-
if (reason) return reason;
|
|
59
|
-
}
|
|
60
|
-
seen.delete(value);
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
53
|
const isEmptyObject = (value) => isPlainObject(value) && Object.keys(value).length === 0;
|
|
65
54
|
|
|
66
55
|
function indexListIssues(indexes, base, issues) {
|
|
@@ -99,6 +88,68 @@ function indexListIssues(indexes, base, issues) {
|
|
|
99
88
|
}
|
|
100
89
|
}
|
|
101
90
|
|
|
91
|
+
function searchIndexListIssues(searchIndexes, base, issues) {
|
|
92
|
+
const listPath = join(base, 'searchIndexes');
|
|
93
|
+
if (!Array.isArray(searchIndexes)) {
|
|
94
|
+
issues.push({ path: listPath, message: 'must be an array of search index definitions' });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const names = new Map();
|
|
98
|
+
for (const [position, index] of searchIndexes.entries()) {
|
|
99
|
+
const indexPath = `${listPath}[${position}]`;
|
|
100
|
+
const found = searchIndexIssues(index, indexPath);
|
|
101
|
+
issues.push(...found);
|
|
102
|
+
if (found.length > 0) continue;
|
|
103
|
+
const { name } = normalizeDeclaredSearchIndex(index);
|
|
104
|
+
if (names.has(name)) {
|
|
105
|
+
issues.push({
|
|
106
|
+
path: indexPath,
|
|
107
|
+
message:
|
|
108
|
+
`has the same name as searchIndexes[${names.get(name)}] ("${name}")` +
|
|
109
|
+
(index.name === undefined
|
|
110
|
+
? ' — a search index declared without a name is "default"'
|
|
111
|
+
: ''),
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
names.set(name, position);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate the `versioning` key and how it fits the rest of the definition:
|
|
121
|
+
* a declared validator must leave the managed fields to it, and a declared
|
|
122
|
+
* index must not duplicate the version index. Returns whether the definition
|
|
123
|
+
* is versioned (valid or not), so the validator checks below know a
|
|
124
|
+
* validator is coming.
|
|
125
|
+
*/
|
|
126
|
+
function versioningDefinitionIssues(definition, base, issues) {
|
|
127
|
+
if (definition.versioning === undefined) return false;
|
|
128
|
+
const path = join(base, 'versioning');
|
|
129
|
+
const found = versioningIssues(definition.versioning, path);
|
|
130
|
+
issues.push(...found);
|
|
131
|
+
if (found.length > 0) return true;
|
|
132
|
+
const versioning = resolveVersioning(definition.versioning);
|
|
133
|
+
if (isPlainObject(definition.validator) || definition.validator === null) {
|
|
134
|
+
for (const message of validatorVersioningIssues(definition.validator, versioning)) {
|
|
135
|
+
issues.push({ path: join(base, 'validator'), message });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (versioning.index && Array.isArray(definition.indexes)) {
|
|
139
|
+
for (const [position, index] of definition.indexes.entries()) {
|
|
140
|
+
if (isPlainObject(index) && isVersioningIndexKey(index.key, versioning)) {
|
|
141
|
+
issues.push({
|
|
142
|
+
path: `${join(base, 'indexes')}[${position}]`,
|
|
143
|
+
message:
|
|
144
|
+
'is the version index, which versioning declares itself — remove it, or set ' +
|
|
145
|
+
'versioning.index: false',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
102
153
|
/**
|
|
103
154
|
* Validate one collection definition, returning `{ path, message }` issues
|
|
104
155
|
* (empty when valid). `path` prefixes every issue (`collections[2]`, or
|
|
@@ -137,13 +188,22 @@ function definitionIssues(definition, { path: base, reserved = [], fallbackName
|
|
|
137
188
|
report('name', `"${name}" is one of migronaut's own collections`);
|
|
138
189
|
}
|
|
139
190
|
|
|
140
|
-
if (
|
|
191
|
+
if (
|
|
192
|
+
definition.indexes === undefined &&
|
|
193
|
+
definition.searchIndexes === undefined &&
|
|
194
|
+
definition.validator === undefined &&
|
|
195
|
+
definition.versioning === undefined
|
|
196
|
+
) {
|
|
141
197
|
issues.push({
|
|
142
198
|
path: self,
|
|
143
|
-
message: 'declares
|
|
199
|
+
message: 'declares no indexes, searchIndexes, validator or versioning — nothing to manage',
|
|
144
200
|
});
|
|
145
201
|
}
|
|
146
202
|
if (definition.indexes !== undefined) indexListIssues(definition.indexes, base, issues);
|
|
203
|
+
if (definition.searchIndexes !== undefined) {
|
|
204
|
+
searchIndexListIssues(definition.searchIndexes, base, issues);
|
|
205
|
+
}
|
|
206
|
+
const versioning = versioningDefinitionIssues(definition, base, issues);
|
|
147
207
|
|
|
148
208
|
const { validator } = definition;
|
|
149
209
|
if (validator !== undefined && validator !== null) {
|
|
@@ -154,7 +214,8 @@ function definitionIssues(definition, { path: base, reserved = [], fallbackName
|
|
|
154
214
|
if (reason) report('validator', reason);
|
|
155
215
|
}
|
|
156
216
|
}
|
|
157
|
-
|
|
217
|
+
// The versioning rules are a validator of their own.
|
|
218
|
+
const hasValidator = (isPlainObject(validator) && !isEmptyObject(validator)) || versioning;
|
|
158
219
|
for (const [key, allowed] of [
|
|
159
220
|
['validationLevel', VALIDATION_LEVELS],
|
|
160
221
|
['validationAction', VALIDATION_ACTIONS],
|
|
@@ -203,23 +264,56 @@ function collectionsIssues(list, { reserved = [] } = {}) {
|
|
|
203
264
|
|
|
204
265
|
/**
|
|
205
266
|
* A valid definition in the planner's shape: indexes normalized (effective
|
|
206
|
-
* names, server-form keys, the spec to send),
|
|
207
|
-
*
|
|
267
|
+
* names, server-form keys, the spec to send), search indexes with their
|
|
268
|
+
* default name and type, the validator cleaned for the wire.
|
|
269
|
+
* `indexes`/`searchIndexes`/`validator` stay `undefined` when not managed.
|
|
270
|
+
* `versioning` is resolved and folded in: its rules merged into the
|
|
271
|
+
* validator, its index appended to `indexes` — with `indexesPartial` when it
|
|
272
|
+
* is the only index declared, so the others stay unmanaged.
|
|
208
273
|
*/
|
|
209
274
|
function normalizeDefinition(definition, { name, source } = {}) {
|
|
210
275
|
const { validator } = definition;
|
|
276
|
+
let wireValidator = validator === undefined || validator === null ? validator : toWire(validator);
|
|
277
|
+
let indexes = definition.indexes?.map((index) => normalizeDeclaredIndex(index));
|
|
278
|
+
let validationLevel = definition.validationLevel;
|
|
279
|
+
let versioning;
|
|
280
|
+
let indexesPartial = false;
|
|
281
|
+
if (definition.versioning !== undefined) {
|
|
282
|
+
// Folded into an ordinary validator and an ordinary index, so the planner
|
|
283
|
+
// needs to know nothing about versioning.
|
|
284
|
+
versioning = resolveVersioning(definition.versioning);
|
|
285
|
+
// A validator synthesized for versioning alone applies `moderate`: an
|
|
286
|
+
// update to a legacy document that predates the rules stays possible.
|
|
287
|
+
if (wireValidator === undefined || isEmptyObject(wireValidator)) validationLevel ??= 'moderate';
|
|
288
|
+
wireValidator = mergeVersioningValidator(wireValidator, versioning);
|
|
289
|
+
if (versioning.index) {
|
|
290
|
+
// Marked: on a sharded collection the planner swaps in the shard-key-prefixed form.
|
|
291
|
+
const index = { ...normalizeDeclaredIndex(versioningIndex(versioning)), versionIndex: true };
|
|
292
|
+
// Declaring the version index alone must not make every other index of
|
|
293
|
+
// the collection "undeclared" — and so a candidate for prune.
|
|
294
|
+
indexesPartial = indexes === undefined;
|
|
295
|
+
indexes = [...(indexes ?? []), index];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
211
298
|
return {
|
|
212
299
|
name: definition.name ?? name,
|
|
213
300
|
source,
|
|
214
|
-
indexes
|
|
215
|
-
|
|
216
|
-
...(definition.
|
|
217
|
-
? {
|
|
301
|
+
indexes,
|
|
302
|
+
...(indexesPartial ? { indexesPartial } : {}),
|
|
303
|
+
...(definition.searchIndexes !== undefined
|
|
304
|
+
? {
|
|
305
|
+
searchIndexes: definition.searchIndexes.map((index) =>
|
|
306
|
+
normalizeDeclaredSearchIndex(index),
|
|
307
|
+
),
|
|
308
|
+
}
|
|
218
309
|
: {}),
|
|
310
|
+
validator: wireValidator,
|
|
311
|
+
...(validationLevel !== undefined ? { validationLevel } : {}),
|
|
219
312
|
...(definition.validationAction !== undefined
|
|
220
313
|
? { validationAction: definition.validationAction }
|
|
221
314
|
: {}),
|
|
222
315
|
...(definition.prune !== undefined ? { prune: definition.prune } : {}),
|
|
316
|
+
...(versioning !== undefined ? { versioning } : {}),
|
|
223
317
|
};
|
|
224
318
|
}
|
|
225
319
|
|
package/src/core/config.js
CHANGED
|
@@ -4,6 +4,9 @@ const { pathToFileURL } = require('node:url');
|
|
|
4
4
|
const { ConfigInvalidError } = require('../errors/index.js');
|
|
5
5
|
const { isCollectionName } = require('../utils/collection-name.js');
|
|
6
6
|
const { TELEMETRY_KEYS, telemetryIssues } = require('../utils/telemetry.js');
|
|
7
|
+
|
|
8
|
+
/** The keys a `telemetry` object may hold — a Set, read once per key */
|
|
9
|
+
const TELEMETRY_KEY_SET = new Set(TELEMETRY_KEYS);
|
|
7
10
|
const { applyEnvFile } = require('../utils/env.js');
|
|
8
11
|
const { errorText } = require('../utils/error.js');
|
|
9
12
|
const { resolveLogger } = require('../utils/logger.js');
|
|
@@ -32,6 +35,14 @@ const DEFAULT_CONFIG = {
|
|
|
32
35
|
onOutOfOrder: 'warn',
|
|
33
36
|
reloadMigrations: false,
|
|
34
37
|
convergeAfterUp: false,
|
|
38
|
+
onSearchUnavailable: 'fail',
|
|
39
|
+
waitForSearchIndexes: false,
|
|
40
|
+
searchIndexWaitTimeoutMs: 600_000,
|
|
41
|
+
backgroundCollection: '_migronaut_background',
|
|
42
|
+
backgroundInline: false,
|
|
43
|
+
backgroundOnDrift: 'reopen',
|
|
44
|
+
backgroundDrift: 'poll',
|
|
45
|
+
backgroundShardAware: 'auto',
|
|
35
46
|
};
|
|
36
47
|
|
|
37
48
|
/** Candidate config file names, checked in priority order within the cwd */
|
|
@@ -41,6 +52,26 @@ const isNonEmptyString = (value) => typeof value === 'string' && value.length >
|
|
|
41
52
|
const isBoolean = (value) => typeof value === 'boolean';
|
|
42
53
|
const isPositiveInteger = (value) => Number.isInteger(value) && value > 0;
|
|
43
54
|
const isExtension = (value) => value === 'ts' || value === 'js';
|
|
55
|
+
const oneOf = (values) => (value) => values.includes(value);
|
|
56
|
+
const quoted = (values) => values.map((value) => `'${value}'`).join(', ');
|
|
57
|
+
|
|
58
|
+
const ON_DRIFT = ['reopen', 'report'];
|
|
59
|
+
const DRIFT_MODES = ['poll', 'stream', 'both'];
|
|
60
|
+
const SHARD_AWARE = ['auto', 'off'];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The collections a background migration keeps its state in: the state
|
|
64
|
+
* documents themselves, their partitions and the drift watcher's resume
|
|
65
|
+
* tokens — the last two named after the first, so one setting moves all
|
|
66
|
+
* three (and a test of "must differ" covers them together).
|
|
67
|
+
*/
|
|
68
|
+
function backgroundCollectionNames(backgroundCollection) {
|
|
69
|
+
return {
|
|
70
|
+
state: backgroundCollection,
|
|
71
|
+
partitions: `${backgroundCollection}_partitions`,
|
|
72
|
+
watch: `${backgroundCollection}_watch`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
44
75
|
|
|
45
76
|
function isStringList(value) {
|
|
46
77
|
if (!Array.isArray(value) || value.length === 0) return false;
|
|
@@ -150,6 +181,44 @@ const CONFIG_KEYS = [
|
|
|
150
181
|
optional: true,
|
|
151
182
|
},
|
|
152
183
|
{ path: 'convergeAfterUp', check: isBoolean, message: 'must be a boolean', optional: true },
|
|
184
|
+
{
|
|
185
|
+
path: 'onSearchUnavailable',
|
|
186
|
+
check: (value) => value === 'fail' || value === 'skip',
|
|
187
|
+
message: "must be 'fail' or 'skip'",
|
|
188
|
+
optional: true,
|
|
189
|
+
},
|
|
190
|
+
{ path: 'waitForSearchIndexes', check: isBoolean, message: 'must be a boolean', optional: true },
|
|
191
|
+
{
|
|
192
|
+
path: 'searchIndexWaitTimeoutMs',
|
|
193
|
+
check: isPositiveInteger,
|
|
194
|
+
message: 'must be a positive integer',
|
|
195
|
+
optional: true,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
path: 'backgroundCollection',
|
|
199
|
+
check: isCollectionName,
|
|
200
|
+
message: "must be a valid collection name (no '$'/NUL, not system.*)",
|
|
201
|
+
optional: true,
|
|
202
|
+
},
|
|
203
|
+
{ path: 'backgroundInline', check: isBoolean, message: 'must be a boolean', optional: true },
|
|
204
|
+
{
|
|
205
|
+
path: 'backgroundOnDrift',
|
|
206
|
+
check: oneOf(ON_DRIFT),
|
|
207
|
+
message: `must be ${quoted(ON_DRIFT)}`,
|
|
208
|
+
optional: true,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
path: 'backgroundDrift',
|
|
212
|
+
check: oneOf(DRIFT_MODES),
|
|
213
|
+
message: `must be ${quoted(DRIFT_MODES)}`,
|
|
214
|
+
optional: true,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
path: 'backgroundShardAware',
|
|
218
|
+
check: oneOf(SHARD_AWARE),
|
|
219
|
+
message: `must be ${quoted(SHARD_AWARE)}`,
|
|
220
|
+
optional: true,
|
|
221
|
+
},
|
|
153
222
|
];
|
|
154
223
|
|
|
155
224
|
/**
|
|
@@ -201,26 +270,51 @@ function validateConfig(config, options = {}) {
|
|
|
201
270
|
// pure data, so this costs nothing. Definition *files* are loaded only when
|
|
202
271
|
// a converge runs: importing them here would make one broken file block
|
|
203
272
|
// every command, an emergency `down` included.
|
|
204
|
-
//
|
|
205
|
-
const bookkeeping =
|
|
206
|
-
for (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
273
|
+
// Every bookkeeping collection has one job: sharing one would mix records.
|
|
274
|
+
const bookkeeping = bookkeepingCollections(config);
|
|
275
|
+
for (let position = 1; position < bookkeeping.length; position++) {
|
|
276
|
+
const [key, name, what] = bookkeeping[position];
|
|
277
|
+
if (name === undefined) continue;
|
|
278
|
+
for (let earlier = 0; earlier < position; earlier++) {
|
|
279
|
+
const [otherKey, otherName, otherWhat] = bookkeeping[earlier];
|
|
280
|
+
if (name !== otherName) continue;
|
|
281
|
+
issues.push({
|
|
282
|
+
path: key,
|
|
283
|
+
message: `${what ? `${what} (${name}) ` : ''}must differ from ${otherWhat ? `${otherKey}'s ${otherWhat}` : otherKey}`,
|
|
284
|
+
});
|
|
285
|
+
break;
|
|
211
286
|
}
|
|
212
287
|
}
|
|
213
288
|
if (Array.isArray(config.collections)) {
|
|
214
|
-
const reserved = [
|
|
215
|
-
|
|
216
|
-
config.lockCollection,
|
|
217
|
-
config.convergeLogCollection,
|
|
218
|
-
];
|
|
289
|
+
const reserved = [];
|
|
290
|
+
for (const [, name] of bookkeeping) if (name !== undefined) reserved.push(name);
|
|
219
291
|
for (const issue of collectionsIssues(config.collections, { reserved })) issues.push(issue);
|
|
220
292
|
}
|
|
221
293
|
return issues;
|
|
222
294
|
}
|
|
223
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Every collection migronaut keeps records in, as `[configKey, name, what?]`
|
|
298
|
+
* — `what` names a collection derived from the key (the background
|
|
299
|
+
* partitions and watch collections).
|
|
300
|
+
*/
|
|
301
|
+
function bookkeepingCollections(config) {
|
|
302
|
+
const entries = [
|
|
303
|
+
['migrationsCollection', config.migrationsCollection],
|
|
304
|
+
['lockCollection', config.lockCollection],
|
|
305
|
+
['convergeLogCollection', config.convergeLogCollection],
|
|
306
|
+
];
|
|
307
|
+
if (typeof config.backgroundCollection === 'string') {
|
|
308
|
+
const names = backgroundCollectionNames(config.backgroundCollection);
|
|
309
|
+
entries.push(
|
|
310
|
+
['backgroundCollection', names.state],
|
|
311
|
+
['backgroundCollection', names.partitions, 'partitions collection'],
|
|
312
|
+
['backgroundCollection', names.watch, 'watch collection'],
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return entries;
|
|
316
|
+
}
|
|
317
|
+
|
|
224
318
|
const TRUE_BOOLEAN_STRINGS = new Set(['true', '1', 'yes']);
|
|
225
319
|
const FALSE_BOOLEAN_STRINGS = new Set(['false', '0', 'no']);
|
|
226
320
|
|
|
@@ -331,6 +425,30 @@ const ENV_KEYS = [
|
|
|
331
425
|
{ env: 'MIGRONAUT_RELOAD_MIGRATIONS', path: 'reloadMigrations', parse: parseBoolean },
|
|
332
426
|
{ env: 'MIGRONAUT_COLLECTIONS_DIR', path: 'collectionsDir', parse: parseString },
|
|
333
427
|
{ env: 'MIGRONAUT_CONVERGE_AFTER_UP', path: 'convergeAfterUp', parse: parseBoolean },
|
|
428
|
+
{
|
|
429
|
+
env: 'MIGRONAUT_ON_SEARCH_UNAVAILABLE',
|
|
430
|
+
path: 'onSearchUnavailable',
|
|
431
|
+
parse: parseEnum(['fail', 'skip']),
|
|
432
|
+
},
|
|
433
|
+
{ env: 'MIGRONAUT_WAIT_FOR_SEARCH_INDEXES', path: 'waitForSearchIndexes', parse: parseBoolean },
|
|
434
|
+
{
|
|
435
|
+
env: 'MIGRONAUT_SEARCH_INDEX_WAIT_TIMEOUT_MS',
|
|
436
|
+
path: 'searchIndexWaitTimeoutMs',
|
|
437
|
+
parse: parsePositiveInteger,
|
|
438
|
+
},
|
|
439
|
+
{ env: 'MIGRONAUT_BACKGROUND_COLLECTION', path: 'backgroundCollection', parse: parseString },
|
|
440
|
+
{ env: 'MIGRONAUT_BACKGROUND_INLINE', path: 'backgroundInline', parse: parseBoolean },
|
|
441
|
+
{
|
|
442
|
+
env: 'MIGRONAUT_BACKGROUND_ON_DRIFT',
|
|
443
|
+
path: 'backgroundOnDrift',
|
|
444
|
+
parse: parseEnum(ON_DRIFT),
|
|
445
|
+
},
|
|
446
|
+
{ env: 'MIGRONAUT_BACKGROUND_DRIFT', path: 'backgroundDrift', parse: parseEnum(DRIFT_MODES) },
|
|
447
|
+
{
|
|
448
|
+
env: 'MIGRONAUT_BACKGROUND_SHARD_AWARE',
|
|
449
|
+
path: 'backgroundShardAware',
|
|
450
|
+
parse: parseEnum(SHARD_AWARE),
|
|
451
|
+
},
|
|
334
452
|
];
|
|
335
453
|
|
|
336
454
|
/** Build a partial config from the MIGRONAUT_* environment variables */
|
|
@@ -523,7 +641,7 @@ async function loadConfig(options = {}) {
|
|
|
523
641
|
// (`trace`, `metrics`) instead of a tracer and a meter turns it off silently.
|
|
524
642
|
if (config.telemetry) {
|
|
525
643
|
for (const key in config.telemetry) {
|
|
526
|
-
if (!
|
|
644
|
+
if (!TELEMETRY_KEY_SET.has(key)) (unknown ??= []).push(`telemetry.${key}`);
|
|
527
645
|
}
|
|
528
646
|
}
|
|
529
647
|
if (unknown) {
|
|
@@ -569,6 +687,8 @@ module.exports = {
|
|
|
569
687
|
CONFIG_KEYS,
|
|
570
688
|
DEFAULT_CONFIG,
|
|
571
689
|
ENV_KEYS,
|
|
690
|
+
backgroundCollectionNames,
|
|
691
|
+
bookkeepingCollections,
|
|
572
692
|
// Re-exported from utils/collection-name.js, where it moved so that
|
|
573
693
|
// core/collections.js can use it without a require cycle through here.
|
|
574
694
|
isCollectionName,
|