@alexify/migronaut 2.2.0 → 2.4.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 +190 -0
- package/README.md +41 -3
- package/bullmq.d.ts +484 -8
- package/index.d.ts +1264 -9
- package/migronaut.schema.json +93 -1
- package/package.json +9 -2
- package/src/bullmq/background-processor.js +541 -0
- package/src/bullmq/index.js +12 -0
- package/src/bullmq/jobs.js +254 -7
- package/src/bullmq/processor.js +348 -21
- package/src/bullmq/producer.js +185 -13
- package/src/bullmq/service.js +484 -45
- package/src/cli/commands/background.js +500 -0
- 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/core/audit.js +11 -1
- package/src/core/background-audit.js +139 -0
- package/src/core/background-drift.js +126 -0
- package/src/core/background-dry-run.js +375 -0
- package/src/core/background-engine.js +849 -0
- package/src/core/background-kit.js +432 -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 +610 -0
- package/src/core/background.js +1127 -0
- package/src/core/bson-peer.js +23 -0
- package/src/core/changelog.js +32 -0
- package/src/core/collections.js +78 -8
- package/src/core/config.js +102 -12
- package/src/core/converge-plan.js +86 -7
- package/src/core/converge.js +88 -0
- package/src/core/lock.js +48 -21
- package/src/core/migration-logger.js +279 -0
- package/src/core/migrator.js +1027 -22
- package/src/core/options.js +36 -0
- package/src/core/run-recorder.js +6 -1
- package/src/core/run.js +26 -12
- package/src/core/runner.js +34 -8
- package/src/core/server-info.js +9 -2
- package/src/core/shard-info.js +76 -0
- package/src/core/versioning-spec.js +181 -0
- package/src/errors/index.js +88 -0
- package/src/index.js +16 -0
- package/src/utils/error.js +11 -2
- package/src/utils/job-ref.js +44 -0
- package/src/utils/loader.js +77 -9
- package/src/utils/migration-name.js +33 -1
- package/src/utils/redact.js +140 -3
- package/src/utils/telemetry.js +110 -0
- package/src/utils/template.js +62 -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,213 @@
|
|
|
1
|
+
const { ConfigInvalidError, ShapeVersionError } = require('../errors/index.js');
|
|
2
|
+
const { resolveVersioning } = require('./config.js');
|
|
3
|
+
const { cloneDocument, versionOf } = require('./document.js');
|
|
4
|
+
const { isPlainObject, unwrapDefinition } = require('./internal.js');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An upcaster: the shape changes of one collection as plain functions, one
|
|
8
|
+
* per version step — `{ 1: v1 => v2, 2: v2 => v3 }` — usable two ways:
|
|
9
|
+
*
|
|
10
|
+
* - `step(1)` is the `migrate` of the background migration that rewrites the
|
|
11
|
+
* stored documents (the norm: data is upgraded in the database);
|
|
12
|
+
* - `upcast(doc)` lifts a document read from the database to the current
|
|
13
|
+
* shape in memory, without writing it (the exception: a read path that
|
|
14
|
+
* cannot wait for the background migration to finish).
|
|
15
|
+
*
|
|
16
|
+
* So the knowledge of what changed between two shapes lives in one place.
|
|
17
|
+
* Steps are synchronous and pure; the helper sets the version field, so a
|
|
18
|
+
* step only reshapes the document.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const NEWER = new Set(['throw', 'keep']);
|
|
22
|
+
|
|
23
|
+
function shapeError(message, context) {
|
|
24
|
+
return new ShapeVersionError(message, context);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The steps, validated against the versioning: `Map<from, fn>` */
|
|
28
|
+
function readSteps(steps, versioning, label) {
|
|
29
|
+
if (!isPlainObject(steps)) {
|
|
30
|
+
throw new ConfigInvalidError(`${label}: steps must be an object of { [fromVersion]: fn }`);
|
|
31
|
+
}
|
|
32
|
+
const map = new Map();
|
|
33
|
+
for (const [key, step] of Object.entries(steps)) {
|
|
34
|
+
const from = Number(key);
|
|
35
|
+
if (!Number.isSafeInteger(from) || from < 0 || String(from) !== key) {
|
|
36
|
+
throw new ConfigInvalidError(`${label}: step key "${key}" is not a version`);
|
|
37
|
+
}
|
|
38
|
+
if (from >= versioning.current) {
|
|
39
|
+
throw new ConfigInvalidError(
|
|
40
|
+
`${label}: a step from ${from} goes past the current version ${versioning.current}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (typeof step !== 'function') {
|
|
44
|
+
throw new ConfigInvalidError(`${label}: the step from ${from} must be a function`);
|
|
45
|
+
}
|
|
46
|
+
if (step.constructor?.name === 'AsyncFunction') {
|
|
47
|
+
throw new ConfigInvalidError(
|
|
48
|
+
`${label}: the step from ${from} is async — upcasting is synchronous and pure`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
map.set(from, step);
|
|
52
|
+
}
|
|
53
|
+
for (let from = versioning.min; from < versioning.current; from++) {
|
|
54
|
+
if (!map.has(from)) {
|
|
55
|
+
throw new ConfigInvalidError(
|
|
56
|
+
`${label}: no step from version ${from} — every version from min (${versioning.min}) ` +
|
|
57
|
+
`to current (${versioning.current}) needs one`,
|
|
58
|
+
{ missing: from },
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Run one step on `doc` (already a copy) and stamp the version it reaches */
|
|
66
|
+
function runStep(step, doc, from, versioning, context) {
|
|
67
|
+
const next = step(doc);
|
|
68
|
+
if (next !== null && typeof next === 'object' && typeof next.then === 'function') {
|
|
69
|
+
throw shapeError(`The step from version ${from} returned a promise — steps are synchronous`, {
|
|
70
|
+
...context,
|
|
71
|
+
reason: 'invalid',
|
|
72
|
+
version: from,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (!isPlainObject(next)) {
|
|
76
|
+
throw shapeError(`The step from version ${from} did not return a document`, {
|
|
77
|
+
...context,
|
|
78
|
+
reason: 'invalid',
|
|
79
|
+
version: from,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// A copy, not an assignment: a step may return a frozen object.
|
|
83
|
+
return { ...next, [versioning.field]: from + 1 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* An upcaster over already-resolved versioning (`defineShapes` passes its
|
|
88
|
+
* own). `collection` names it in errors.
|
|
89
|
+
*/
|
|
90
|
+
function createUpcaster(versioning, steps, { newer = 'throw', collection } = {}) {
|
|
91
|
+
const label = collection ? `upcaster(${collection})` : 'upcaster';
|
|
92
|
+
if (!NEWER.has(newer)) {
|
|
93
|
+
throw new ConfigInvalidError(`${label}: newer must be 'throw' or 'keep'`, { newer });
|
|
94
|
+
}
|
|
95
|
+
const chain = readSteps(steps, versioning, label);
|
|
96
|
+
const { current, min, field } = versioning;
|
|
97
|
+
const base = collection ? { collection, current } : { current };
|
|
98
|
+
|
|
99
|
+
const versionChecked = (doc) => {
|
|
100
|
+
const version = versionOf(doc, field);
|
|
101
|
+
if (version === null) {
|
|
102
|
+
throw shapeError(`The document's ${field} is not a non-negative integer`, {
|
|
103
|
+
...base,
|
|
104
|
+
reason: 'invalid',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return version;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** `doc` lifted from `from` to `to` — every step on one private copy (`copy: false`: it is one) */
|
|
111
|
+
const lift = (doc, from, to, { copy = true } = {}) => {
|
|
112
|
+
let next = copy ? cloneDocument(doc) : doc;
|
|
113
|
+
for (let version = from; version < to; version++) {
|
|
114
|
+
const step = chain.get(version);
|
|
115
|
+
if (step === undefined) {
|
|
116
|
+
throw shapeError(
|
|
117
|
+
`No step from version ${version} — the document is older than the oldest shape ` +
|
|
118
|
+
`still supported (${min})`,
|
|
119
|
+
{ ...base, reason: 'below-min', version: from },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
next = runStep(step, next, version, versioning, base);
|
|
123
|
+
}
|
|
124
|
+
return next;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The document in the current shape. One already current is returned as
|
|
129
|
+
* is; an older one is copied first, so the caller's object never changes.
|
|
130
|
+
* A newer one (written by a newer release) throws — or, with
|
|
131
|
+
* `newer: 'keep'`, is returned as is.
|
|
132
|
+
*/
|
|
133
|
+
const upcast = (doc) => {
|
|
134
|
+
if (!isPlainObject(doc)) {
|
|
135
|
+
throw shapeError(
|
|
136
|
+
'Only a plain document can be upcast — read it with .lean(), or call .toObject()',
|
|
137
|
+
{ ...base, reason: 'invalid' },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const version = versionChecked(doc);
|
|
141
|
+
if (version === current) return doc;
|
|
142
|
+
if (version > current) {
|
|
143
|
+
if (newer === 'keep') return doc;
|
|
144
|
+
throw shapeError(
|
|
145
|
+
`The document is at version ${version}, newer than ${current} — written by a newer ` +
|
|
146
|
+
'release',
|
|
147
|
+
{ ...base, reason: 'newer', version },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return lift(doc, version, current);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Whether `upcast` would change the document. Not one for a missing
|
|
155
|
+
* document; a document that is not a plain object (a hydrated Mongoose
|
|
156
|
+
* one) is refused — a silent `false` would leave it unread in its old shape.
|
|
157
|
+
*/
|
|
158
|
+
const needsUpcast = (doc) => {
|
|
159
|
+
if (doc === null || doc === undefined) return false;
|
|
160
|
+
if (!isPlainObject(doc)) {
|
|
161
|
+
throw shapeError(
|
|
162
|
+
'Only a plain document can be upcast — read it with .lean(), or call .toObject()',
|
|
163
|
+
{ ...base, reason: 'invalid' },
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return versionChecked(doc) < current;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The transformation from `from` to `to` (default `from + 1`) as a
|
|
171
|
+
* background migration's `migrate`: it takes the stored document and
|
|
172
|
+
* returns the new one; the engine writes the version.
|
|
173
|
+
*/
|
|
174
|
+
const step = (from, to = from + 1) => {
|
|
175
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 0 || to <= from) {
|
|
176
|
+
throw new ConfigInvalidError(`${label}: step(${from}, ${to}) is not a forward range`);
|
|
177
|
+
}
|
|
178
|
+
if (to > current) {
|
|
179
|
+
throw new ConfigInvalidError(`${label}: step(${from}, ${to}) goes past ${current}`);
|
|
180
|
+
}
|
|
181
|
+
for (let version = from; version < to; version++) {
|
|
182
|
+
if (!chain.has(version)) {
|
|
183
|
+
throw new ConfigInvalidError(`${label}: no step from version ${version}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// As a background migration's `migrate`, it is handed a private copy
|
|
187
|
+
// already (the engine's context says so): copying it again would clone
|
|
188
|
+
// every document of the collection twice. Called on its own, it copies.
|
|
189
|
+
return (doc, ctx) => lift(doc, from, to, { copy: ctx?.background === undefined });
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return Object.freeze({ current, min, field, upcast, needsUpcast, step });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* An upcaster for a collection definition (`{ versioning }`, as a
|
|
197
|
+
* `collections/*.js` file exports it) or a bare versioning block.
|
|
198
|
+
*
|
|
199
|
+
* @throws {ConfigInvalidError} when a step is missing between `min` and
|
|
200
|
+
* `current`, goes past `current`, is async, or is not a function
|
|
201
|
+
*/
|
|
202
|
+
function upcaster(definitionOrModule, steps, options = {}) {
|
|
203
|
+
const definition = unwrapDefinition(definitionOrModule);
|
|
204
|
+
if (!isPlainObject(definition)) {
|
|
205
|
+
throw new ConfigInvalidError('upcaster takes a collection definition or its versioning');
|
|
206
|
+
}
|
|
207
|
+
const source = isPlainObject(definition.versioning) ? definition.versioning : definition;
|
|
208
|
+
const versioning = resolveVersioning(source);
|
|
209
|
+
const collection = typeof definition.name === 'string' ? definition.name : undefined;
|
|
210
|
+
return createUpcaster(versioning, steps, { collection, ...options });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = { createUpcaster, upcaster };
|