@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/README.md +68 -10
  3. package/bullmq.d.ts +465 -7
  4. package/index.d.ts +1272 -18
  5. package/migronaut.schema.json +150 -2
  6. package/package.json +8 -2
  7. package/src/bullmq/background-processor.js +469 -0
  8. package/src/bullmq/index.js +12 -0
  9. package/src/bullmq/jobs.js +254 -7
  10. package/src/bullmq/processor.js +153 -15
  11. package/src/bullmq/producer.js +202 -27
  12. package/src/bullmq/service.js +480 -45
  13. package/src/cli/commands/background.js +500 -0
  14. package/src/cli/commands/converge.js +38 -10
  15. package/src/cli/commands/create.js +6 -0
  16. package/src/cli/exit-codes.js +6 -0
  17. package/src/cli/index.js +2 -0
  18. package/src/cli/table.js +68 -9
  19. package/src/core/audit.js +98 -3
  20. package/src/core/background-audit.js +139 -0
  21. package/src/core/background-drift.js +126 -0
  22. package/src/core/background-dry-run.js +366 -0
  23. package/src/core/background-engine.js +818 -0
  24. package/src/core/background-kit.js +425 -0
  25. package/src/core/background-partition.js +298 -0
  26. package/src/core/background-runner.js +305 -0
  27. package/src/core/background-sandbox.js +701 -0
  28. package/src/core/background-shard.js +542 -0
  29. package/src/core/background-spec.js +597 -0
  30. package/src/core/background-store.js +951 -0
  31. package/src/core/background-throttle.js +269 -0
  32. package/src/core/background-watch-plan.js +164 -0
  33. package/src/core/background-watch-store.js +78 -0
  34. package/src/core/background-watch.js +605 -0
  35. package/src/core/background.js +1121 -0
  36. package/src/core/bson-peer.js +23 -0
  37. package/src/core/changelog.js +32 -0
  38. package/src/core/collections.js +125 -31
  39. package/src/core/config.js +133 -13
  40. package/src/core/converge-plan.js +343 -61
  41. package/src/core/converge-search-run.js +440 -0
  42. package/src/core/converge-search.js +404 -0
  43. package/src/core/converge.js +428 -183
  44. package/src/core/index-spec.js +27 -16
  45. package/src/core/lock.js +97 -32
  46. package/src/core/migrator.js +951 -26
  47. package/src/core/options.js +32 -1
  48. package/src/core/run.js +26 -12
  49. package/src/core/runner.js +1 -1
  50. package/src/core/search-index-spec.js +758 -0
  51. package/src/core/server-info.js +70 -0
  52. package/src/core/shard-info.js +76 -0
  53. package/src/core/versioning-spec.js +181 -0
  54. package/src/errors/index.js +97 -5
  55. package/src/index.js +16 -0
  56. package/src/utils/canonical.js +34 -1
  57. package/src/utils/error.js +11 -2
  58. package/src/utils/loader.js +77 -9
  59. package/src/utils/migration-name.js +33 -1
  60. package/src/utils/telemetry.js +125 -1
  61. package/src/utils/template.js +69 -1
  62. package/src/versioning/config.js +155 -0
  63. package/src/versioning/document.js +326 -0
  64. package/src/versioning/index.js +50 -0
  65. package/src/versioning/internal.js +279 -0
  66. package/src/versioning/mongoose.js +151 -0
  67. package/src/versioning/occ.js +318 -0
  68. package/src/versioning/registry.js +187 -0
  69. package/src/versioning/upcaster.js +213 -0
  70. package/versioning.d.ts +666 -0
  71. 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 };
@@ -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.
@@ -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, regExpIssue, toWire } = require('../utils/canonical.js');
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 (definition.indexes === undefined && definition.validator === undefined) {
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 neither indexes nor a validator — nothing to manage',
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
- const hasValidator = isPlainObject(validator) && !isEmptyObject(validator);
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), the validator cleaned for the
207
- * wire. `indexes`/`validator` stay `undefined` when not managed.
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: definition.indexes?.map((index) => normalizeDeclaredIndex(index)),
215
- validator: validator === undefined || validator === null ? validator : toWire(validator),
216
- ...(definition.validationLevel !== undefined
217
- ? { validationLevel: definition.validationLevel }
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
 
@@ -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
- // Three bookkeeping collections, three jobs: sharing one would mix records.
205
- const bookkeeping = ['migrationsCollection', 'lockCollection', 'convergeLogCollection'];
206
- for (const [position, key] of bookkeeping.entries()) {
207
- for (const other of bookkeeping.slice(0, position)) {
208
- if (config[key] !== undefined && config[key] === config[other]) {
209
- issues.push({ path: key, message: `must differ from ${other}` });
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
- config.migrationsCollection,
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 (!TELEMETRY_KEYS.includes(key)) (unknown ??= []).push(`telemetry.${key}`);
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,