@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +41 -3
  3. package/bullmq.d.ts +484 -8
  4. package/index.d.ts +1264 -9
  5. package/migronaut.schema.json +93 -1
  6. package/package.json +9 -2
  7. package/src/bullmq/background-processor.js +541 -0
  8. package/src/bullmq/index.js +12 -0
  9. package/src/bullmq/jobs.js +254 -7
  10. package/src/bullmq/processor.js +348 -21
  11. package/src/bullmq/producer.js +185 -13
  12. package/src/bullmq/service.js +484 -45
  13. package/src/cli/commands/background.js +500 -0
  14. package/src/cli/commands/create.js +6 -0
  15. package/src/cli/exit-codes.js +6 -0
  16. package/src/cli/index.js +2 -0
  17. package/src/core/audit.js +11 -1
  18. package/src/core/background-audit.js +139 -0
  19. package/src/core/background-drift.js +126 -0
  20. package/src/core/background-dry-run.js +375 -0
  21. package/src/core/background-engine.js +849 -0
  22. package/src/core/background-kit.js +432 -0
  23. package/src/core/background-partition.js +298 -0
  24. package/src/core/background-runner.js +305 -0
  25. package/src/core/background-sandbox.js +701 -0
  26. package/src/core/background-shard.js +542 -0
  27. package/src/core/background-spec.js +597 -0
  28. package/src/core/background-store.js +951 -0
  29. package/src/core/background-throttle.js +269 -0
  30. package/src/core/background-watch-plan.js +164 -0
  31. package/src/core/background-watch-store.js +78 -0
  32. package/src/core/background-watch.js +610 -0
  33. package/src/core/background.js +1127 -0
  34. package/src/core/bson-peer.js +23 -0
  35. package/src/core/changelog.js +32 -0
  36. package/src/core/collections.js +78 -8
  37. package/src/core/config.js +102 -12
  38. package/src/core/converge-plan.js +86 -7
  39. package/src/core/converge.js +88 -0
  40. package/src/core/lock.js +48 -21
  41. package/src/core/migration-logger.js +279 -0
  42. package/src/core/migrator.js +1027 -22
  43. package/src/core/options.js +36 -0
  44. package/src/core/run-recorder.js +6 -1
  45. package/src/core/run.js +26 -12
  46. package/src/core/runner.js +34 -8
  47. package/src/core/server-info.js +9 -2
  48. package/src/core/shard-info.js +76 -0
  49. package/src/core/versioning-spec.js +181 -0
  50. package/src/errors/index.js +88 -0
  51. package/src/index.js +16 -0
  52. package/src/utils/error.js +11 -2
  53. package/src/utils/job-ref.js +44 -0
  54. package/src/utils/loader.js +77 -9
  55. package/src/utils/migration-name.js +33 -1
  56. package/src/utils/redact.js +140 -3
  57. package/src/utils/telemetry.js +110 -0
  58. package/src/utils/template.js +62 -1
  59. package/src/versioning/config.js +155 -0
  60. package/src/versioning/document.js +326 -0
  61. package/src/versioning/index.js +50 -0
  62. package/src/versioning/internal.js +279 -0
  63. package/src/versioning/mongoose.js +151 -0
  64. package/src/versioning/occ.js +318 -0
  65. package/src/versioning/registry.js +187 -0
  66. package/src/versioning/upcaster.js +213 -0
  67. package/versioning.d.ts +666 -0
  68. package/versioning.js +1 -0
@@ -0,0 +1,155 @@
1
+ const { ConfigInvalidError } = require('../errors/index.js');
2
+ const { isPlainObject } = require('./internal.js');
3
+
4
+ /**
5
+ * The `versioning` block of a collection definition — one source of truth for
6
+ * the shape version a collection is at, read by converge (validator, index,
7
+ * the `min` guard), by the background-migration engine and by the
8
+ * application's repository layer through `defineShapes`.
9
+ *
10
+ * Validated strictly, like every other definition key: a typo (`currnet`)
11
+ * must not read as "not versioned".
12
+ */
13
+
14
+ const VERSIONING_KEYS = ['current', 'min', 'field', 'revision', 'revisionField', 'index'];
15
+ const VERSIONING_KEY_SET = new Set(VERSIONING_KEYS);
16
+
17
+ const VERSIONING_DEFAULTS = Object.freeze({
18
+ min: 1,
19
+ field: '__v',
20
+ revision: true,
21
+ revisionField: '__rev',
22
+ index: true,
23
+ });
24
+
25
+ /** Longest field name accepted for the version or revision field */
26
+ const MAX_FIELD_NAME = 64;
27
+
28
+ /**
29
+ * Why `name` cannot be a system field, or `null`. Top-level only: the engine
30
+ * writes it with `$set`/`$inc`, so a dot would address a nested path and a
31
+ * leading `$` an operator; `_id` is immutable.
32
+ */
33
+ function fieldNameIssue(name) {
34
+ if (typeof name !== 'string' || name.length === 0) return 'must be a non-empty string';
35
+ if (name.length > MAX_FIELD_NAME) return `must be at most ${MAX_FIELD_NAME} characters`;
36
+ if (name.startsWith('$')) return "must not start with '$'";
37
+ if (name.includes('.')) return "must be a top-level field (no '.')";
38
+ if (name.includes('\0')) return 'must not contain NUL';
39
+ if (name === '_id') return 'must not be _id';
40
+ // As a JavaScript key it names the prototype: every write of it would be lost.
41
+ if (name === '__proto__') return 'must not be __proto__';
42
+ return null;
43
+ }
44
+
45
+ const isCount = (value, floor) => Number.isSafeInteger(value) && value >= floor;
46
+
47
+ /**
48
+ * Validate a `versioning` value, returning `{ path, message }` issues (empty
49
+ * when valid). `path` is where it sits — `collections[2].versioning`,
50
+ * `orders.ts: versioning`.
51
+ */
52
+ function versioningIssues(value, path = 'versioning') {
53
+ if (!isPlainObject(value)) {
54
+ return [
55
+ {
56
+ path,
57
+ message: 'must be an object: { current, min?, field?, revision?, revisionField?, index? }',
58
+ },
59
+ ];
60
+ }
61
+ const issues = [];
62
+ const report = (key, message) => issues.push({ path: `${path}.${key}`, message });
63
+ for (const key of Object.keys(value)) {
64
+ if (!VERSIONING_KEY_SET.has(key)) {
65
+ report(key, `is not a versioning key (expected one of: ${VERSIONING_KEYS.join(', ')})`);
66
+ }
67
+ }
68
+ const { current, min, field, revision, revisionField, index } = value;
69
+ const currentValid = isCount(current, 1);
70
+ if (current === undefined) report('current', 'is required');
71
+ else if (!currentValid) report('current', 'must be an integer ≥ 1');
72
+ // The default min (1) never exceeds a valid current (≥ 1).
73
+ if (min !== undefined) {
74
+ if (!isCount(min, 0)) report('min', 'must be an integer ≥ 0');
75
+ else if (currentValid && min > current) {
76
+ report('min', `must not exceed current (${current})`);
77
+ }
78
+ }
79
+ if (field !== undefined) {
80
+ const issue = fieldNameIssue(field);
81
+ if (issue) report('field', issue);
82
+ }
83
+ if (revision !== undefined && typeof revision !== 'boolean') {
84
+ report('revision', 'must be a boolean');
85
+ }
86
+ if (revisionField !== undefined) {
87
+ const issue = fieldNameIssue(revisionField);
88
+ if (issue) report('revisionField', issue);
89
+ else if (revision === false) report('revisionField', 'has no effect with revision: false');
90
+ }
91
+ if (revision !== false) {
92
+ const versionName = field ?? VERSIONING_DEFAULTS.field;
93
+ const revisionName = revisionField ?? VERSIONING_DEFAULTS.revisionField;
94
+ if (versionName === revisionName) {
95
+ report('revisionField', `must differ from the version field ("${versionName}")`);
96
+ }
97
+ }
98
+ if (index !== undefined && typeof index !== 'boolean') report('index', 'must be a boolean');
99
+ return issues;
100
+ }
101
+
102
+ /**
103
+ * A validated `versioning` value with every default filled in, frozen.
104
+ * `revisionField` is `null` when revisions are off.
105
+ *
106
+ * @throws {ConfigInvalidError} with every issue in `context.issues`
107
+ */
108
+ function resolveVersioning(value, { path = 'versioning' } = {}) {
109
+ const issues = versioningIssues(value, path);
110
+ if (issues.length > 0) {
111
+ throw new ConfigInvalidError(`Invalid ${path}: ${issues[0].path} ${issues[0].message}`, {
112
+ issues,
113
+ });
114
+ }
115
+ const revision = value.revision ?? VERSIONING_DEFAULTS.revision;
116
+ return Object.freeze({
117
+ current: value.current,
118
+ min: value.min ?? VERSIONING_DEFAULTS.min,
119
+ field: value.field ?? VERSIONING_DEFAULTS.field,
120
+ revision,
121
+ revisionField: revision ? (value.revisionField ?? VERSIONING_DEFAULTS.revisionField) : null,
122
+ index: value.index ?? VERSIONING_DEFAULTS.index,
123
+ });
124
+ }
125
+
126
+ /**
127
+ * The resolved versioning of `collection` among `definitions` — an array of
128
+ * definitions (each with `name`) or a `{ name: definition }` map — or `null`
129
+ * when it is not declared or not versioned.
130
+ */
131
+ function versioningOf(definitions, collection) {
132
+ let definition;
133
+ if (Array.isArray(definitions)) {
134
+ for (const candidate of definitions) {
135
+ if (isPlainObject(candidate) && candidate.name === collection) {
136
+ definition = candidate;
137
+ break;
138
+ }
139
+ }
140
+ } else if (isPlainObject(definitions) && Object.hasOwn(definitions, collection)) {
141
+ definition = definitions[collection];
142
+ }
143
+ if (!isPlainObject(definition) || definition.versioning === undefined) return null;
144
+ return resolveVersioning(definition.versioning, { path: `${collection}.versioning` });
145
+ }
146
+
147
+ module.exports = {
148
+ MAX_FIELD_NAME,
149
+ VERSIONING_DEFAULTS,
150
+ VERSIONING_KEYS,
151
+ fieldNameIssue,
152
+ resolveVersioning,
153
+ versioningIssues,
154
+ versioningOf,
155
+ };
@@ -0,0 +1,326 @@
1
+ const { ConfigInvalidError, ShapeVersionError } = require('../errors/index.js');
2
+ const { VERSIONING_DEFAULTS, fieldNameIssue, versioningOf } = require('./config.js');
3
+ const {
4
+ cloneDocument,
5
+ isPlainObject,
6
+ sameValue,
7
+ setOwn,
8
+ toCount,
9
+ touchedFields,
10
+ } = require('./internal.js');
11
+
12
+ /**
13
+ * The document-level contract shared by everything that writes a versioned
14
+ * collection — the repository helpers, the Mongoose plugin and the
15
+ * background-migration engine. They must agree on one model, or a background
16
+ * update would silently overwrite an application write:
17
+ *
18
+ * - a missing (or `null`) version or revision field reads as **0**, so legacy
19
+ * documents need no backfill before the first guarded write;
20
+ * - every write to a collection with revisions bumps the revision by one
21
+ * (`$inc`), so an optimistic-concurrency filter `{ _id, rev }` notices any
22
+ * write that happened in between;
23
+ * - the version is set to an exact number, never incremented — two releases
24
+ * writing the same shape agree on it.
25
+ *
26
+ * `names` below is `{ field, revisionField }` from `resolveFieldNames` (or a
27
+ * resolved `versioning`); `revisionField` is `null` when revisions are off.
28
+ */
29
+
30
+ /**
31
+ * The system field names, validated: `{ field, revisionField }` with the
32
+ * defaults filled in and `revisionField: null` under `revision: false`.
33
+ *
34
+ * @throws {ConfigInvalidError} on an invalid name, or both naming one field
35
+ */
36
+ function resolveFieldNames({ field, revisionField, revision } = {}) {
37
+ const versionName = field ?? VERSIONING_DEFAULTS.field;
38
+ const revisionName =
39
+ revision === false ? null : (revisionField ?? VERSIONING_DEFAULTS.revisionField);
40
+ for (const [key, name] of [
41
+ ['field', versionName],
42
+ ['revisionField', revisionName],
43
+ ]) {
44
+ if (name === null) continue;
45
+ const issue = fieldNameIssue(name);
46
+ if (issue) throw new ConfigInvalidError(`${key} ${issue}`, { key });
47
+ }
48
+ if (versionName === revisionName) {
49
+ throw new ConfigInvalidError('field and revisionField must differ', { key: 'revisionField' });
50
+ }
51
+ return { field: versionName, revisionField: revisionName };
52
+ }
53
+
54
+ /** A document's count field: 0 when missing or `null`, `null` when not a non-negative integer */
55
+ function countOf(doc, field) {
56
+ if (!isPlainObject(doc)) return null;
57
+ const value = doc[field];
58
+ return value === undefined || value === null ? 0 : toCount(value);
59
+ }
60
+
61
+ /** The shape version of `doc` — 0 when the field is missing, `null` when it is not a count */
62
+ const versionOf = (doc, field = VERSIONING_DEFAULTS.field) => countOf(doc, field);
63
+
64
+ /** The revision of `doc` — 0 when the field is missing, `null` when it is not a count */
65
+ const revisionOf = (doc, revisionField = VERSIONING_DEFAULTS.revisionField) =>
66
+ countOf(doc, revisionField);
67
+
68
+ /**
69
+ * Whether `doc` is at exactly `version` — a missing or `null` field is
70
+ * version 0. A document that is not a plain object (a hydrated Mongoose
71
+ * document) is refused: its fields are not its own properties, and a silent
72
+ * `false` would read as "an old shape".
73
+ */
74
+ function isVersion(doc, version, { field = VERSIONING_DEFAULTS.field } = {}) {
75
+ if (doc === null || doc === undefined) return false;
76
+ assertPlainDocument(doc);
77
+ return versionOf(doc, field) === version;
78
+ }
79
+
80
+ /** A plain document, or a ConfigInvalidError that says how to get one */
81
+ function assertPlainDocument(doc) {
82
+ if (!isPlainObject(doc)) {
83
+ throw new ConfigInvalidError(
84
+ 'A plain document is expected — read it with .lean(), or call .toObject() on a Mongoose ' +
85
+ 'document',
86
+ );
87
+ }
88
+ }
89
+
90
+ /**
91
+ * The revision field alone, validated (`__rev` by default) — for a write
92
+ * that never sets the version, whose field name it then has no use for: a
93
+ * caller naming only its revision field (`__v`, as a Mongoose app may keep
94
+ * it) is not refused over a version field it never touches.
95
+ */
96
+ function resolveRevisionField(revisionField) {
97
+ const name = revisionField ?? VERSIONING_DEFAULTS.revisionField;
98
+ const issue = fieldNameIssue(name);
99
+ if (issue) throw new ConfigInvalidError(`revisionField ${issue}`, { key: 'revisionField' });
100
+ return name;
101
+ }
102
+
103
+ /** The revision after one more write */
104
+ const nextRevision = (revision) => (toCount(revision) ?? 0) + 1;
105
+
106
+ function assertCount(value, what) {
107
+ if (!Number.isSafeInteger(value) || value < 0) {
108
+ throw new ConfigInvalidError(`${what} must be an integer ≥ 0`, { [what]: value });
109
+ }
110
+ }
111
+
112
+ /** `{ [field]: n }`, or — for 0 — a match on the field being 0, `null` or missing */
113
+ function countFilter(field, value) {
114
+ return value === 0 ? { [field]: { $in: [null, 0] } } : { [field]: value };
115
+ }
116
+
117
+ /** Documents at exactly `version` (0 = no version field) */
118
+ function versionFilter(names, version) {
119
+ assertCount(version, 'version');
120
+ return countFilter(names.field, version);
121
+ }
122
+
123
+ /** Documents at exactly `revision` (0 = no revision field) */
124
+ function revisionFilter(names, revision) {
125
+ assertCount(revision, 'revision');
126
+ return countFilter(names.revisionField, revision);
127
+ }
128
+
129
+ /**
130
+ * Documents below `version`: missing or `null` fields, smaller numbers, and
131
+ * anything that is not a number at all (which no validator would accept
132
+ * either). One `$not` range — two intervals of one index scan — rather than
133
+ * an `$or`. `null` for version 0: nothing is below it.
134
+ */
135
+ function belowVersionFilter(names, version) {
136
+ assertCount(version, 'version');
137
+ return version === 0 ? null : { [names.field]: { $not: { $gte: version } } };
138
+ }
139
+
140
+ /** The key of the index every version-filtered scan uses */
141
+ const versionIndexKey = (names) => ({ [names.field]: 1, _id: 1 });
142
+
143
+ /**
144
+ * The optimistic-concurrency filter for rewriting `prev`: its `_id`, the
145
+ * exact version it was read at (`from`, by default its own) and — with
146
+ * revisions — the exact revision. A concurrent write moves the revision (or
147
+ * the version), so the rewrite then matches nothing instead of losing it.
148
+ */
149
+ function occFilter(prev, names, { from } = {}) {
150
+ const version = from ?? versionOf(prev, names.field);
151
+ const filter = { _id: prev._id };
152
+ // A value that is not a count is matched as it is stored — `$eq`, so a
153
+ // stored object with `$`-keys is never read as an operator.
154
+ if (version === null) filter[names.field] = { $eq: prev[names.field] };
155
+ else Object.assign(filter, countFilter(names.field, version));
156
+ if (names.revisionField !== null) {
157
+ const revision = revisionOf(prev, names.revisionField);
158
+ if (revision === null) filter[names.revisionField] = { $eq: prev[names.revisionField] };
159
+ else Object.assign(filter, countFilter(names.revisionField, revision));
160
+ }
161
+ return filter;
162
+ }
163
+
164
+ /** Why `key` cannot be written as a top-level field with `$set`, or `null` */
165
+ function topLevelKeyIssue(key) {
166
+ if (key.length === 0) return 'an empty field name';
167
+ if (key.startsWith('$')) return `a field name starting with '$' ("${key}")`;
168
+ if (key.includes('.')) return `a field name with a dot ("${key}")`;
169
+ if (key.includes('\0')) return 'a field name with NUL';
170
+ return null;
171
+ }
172
+
173
+ function invalidShape(detail, extra) {
174
+ return new ShapeVersionError(`The transformation returned ${detail}`, {
175
+ reason: 'invalid',
176
+ ...extra,
177
+ });
178
+ }
179
+
180
+ const hasKeys = (object) => {
181
+ for (const key in object) if (Object.hasOwn(object, key)) return true;
182
+ return false;
183
+ };
184
+
185
+ /**
186
+ * The operator update that turns `prev` into `next` and stamps it: `$set`
187
+ * for every top-level field whose BSON value changed, `$unset` for every one
188
+ * that disappeared (or became `undefined`), the version set to `to` (unset
189
+ * for `to: 0`), and the revision bumped.
190
+ *
191
+ * Fields that did not change are left out — so the stored BSON type of
192
+ * everything a transformation did not touch survives (a document read with
193
+ * promoted values and written back whole would turn a `Double 1.0` into an
194
+ * `Int32`, a `Long` into a number). A changed nested field rewrites its
195
+ * whole top-level subdocument. `next`'s own version and revision fields are
196
+ * ignored: the engine owns them.
197
+ *
198
+ * @throws {ShapeVersionError} (`reason: 'invalid'`) when `next` is not a
199
+ * document, changes `_id`, or names a field `$set` cannot write
200
+ */
201
+ function stampedDiff(prev, next, names, { to }) {
202
+ if (!isPlainObject(next)) throw invalidShape('something that is not a document');
203
+ assertCount(to, 'to');
204
+ const { field, revisionField } = names;
205
+ const $set = {};
206
+ const $unset = {};
207
+ for (const key of Object.keys(next)) {
208
+ if (key === '_id') {
209
+ if (!sameValue(prev._id, next._id)) throw invalidShape('a document with a different _id');
210
+ continue;
211
+ }
212
+ if (key === field || key === revisionField) continue;
213
+ const issue = topLevelKeyIssue(key);
214
+ if (issue) throw invalidShape(issue, { field: key });
215
+ const value = next[key];
216
+ if (value === undefined) {
217
+ if (Object.hasOwn(prev, key)) setOwn($unset, key, '');
218
+ } else if (!Object.hasOwn(prev, key) || !sameValue(prev[key], value)) {
219
+ setOwn($set, key, value);
220
+ }
221
+ }
222
+ for (const key of Object.keys(prev)) {
223
+ if (key === '_id' || key === field || key === revisionField) continue;
224
+ if (!Object.hasOwn(next, key)) setOwn($unset, key, '');
225
+ }
226
+ if (to === 0) $unset[field] = '';
227
+ else $set[field] = to;
228
+ const update = {};
229
+ if (hasKeys($set)) update.$set = $set;
230
+ if (hasKeys($unset)) update.$unset = $unset;
231
+ if (revisionField !== null) update.$inc = { [revisionField]: 1 };
232
+ return update;
233
+ }
234
+
235
+ function refuseTouch(fields, name, role) {
236
+ if (name !== null && fields.has(name)) {
237
+ throw new ConfigInvalidError(
238
+ `The update must not write the ${role} field "${name}" — migronaut sets it`,
239
+ { field: name },
240
+ );
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Stamp an update the caller wrote: the version set to `to` (left alone when
246
+ * `to` is `undefined`; unset for 0) and the revision bumped. Works on an
247
+ * operator update (merged into its `$set`/`$unset`/`$inc`) and on a pipeline
248
+ * (one stage appended). A replacement document — or a pipeline that projects
249
+ * or replaces the root — is refused: it would drop the revision.
250
+ *
251
+ * @throws {ConfigInvalidError} on a replacement, or an update that writes a
252
+ * system field itself
253
+ */
254
+ function stampedUpdate(names, { to, update }) {
255
+ const { field, revisionField } = names;
256
+ if (to !== undefined) assertCount(to, 'to');
257
+ if (!Array.isArray(update) && !isPlainObject(update)) {
258
+ throw new ConfigInvalidError('The update must be an update document or a pipeline');
259
+ }
260
+ const touched = touchedFields(update);
261
+ if (touched.whole) {
262
+ throw new ConfigInvalidError(
263
+ Array.isArray(update)
264
+ ? 'A pipeline that projects or replaces the document cannot keep its revision'
265
+ : 'A replacement document is not an update — use replaceWithRevision',
266
+ );
267
+ }
268
+ refuseTouch(touched.fields, revisionField, 'revision');
269
+ if (to !== undefined) refuseTouch(touched.fields, field, 'version');
270
+
271
+ if (Array.isArray(update)) {
272
+ const stage = {};
273
+ if (to !== undefined && to !== 0) stage[field] = { $literal: to };
274
+ if (revisionField !== null) {
275
+ stage[revisionField] = { $add: [{ $ifNull: [`$${revisionField}`, 0] }, 1] };
276
+ }
277
+ const out = [...update];
278
+ if (hasKeys(stage)) out.push({ $set: stage });
279
+ if (to === 0) out.push({ $unset: field });
280
+ return out;
281
+ }
282
+ const out = { ...update };
283
+ if (to === 0) out.$unset = { ...update.$unset, [field]: '' };
284
+ else if (to !== undefined) out.$set = { ...update.$set, [field]: to };
285
+ if (revisionField !== null) out.$inc = { ...update.$inc, [revisionField]: 1 };
286
+ return out;
287
+ }
288
+
289
+ /**
290
+ * `doc` with its version set to `version` and its revision to 0 — each only
291
+ * when the document does not carry it yet, so stamping is idempotent and a
292
+ * document someone already stamped keeps its fields.
293
+ */
294
+ function stampDocument(doc, names, version) {
295
+ if (!isPlainObject(doc)) throw new ConfigInvalidError('Only a plain document can be stamped');
296
+ assertCount(version, 'version');
297
+ const out = { ...doc };
298
+ if (out[names.field] === undefined) out[names.field] = version;
299
+ if (names.revisionField !== null && out[names.revisionField] === undefined) {
300
+ out[names.revisionField] = 0;
301
+ }
302
+ return out;
303
+ }
304
+
305
+ module.exports = {
306
+ assertPlainDocument,
307
+ belowVersionFilter,
308
+ cloneDocument,
309
+ fieldNameIssue,
310
+ isVersion,
311
+ nextRevision,
312
+ occFilter,
313
+ refuseTouch,
314
+ resolveFieldNames,
315
+ resolveRevisionField,
316
+ revisionFilter,
317
+ revisionOf,
318
+ sameValue,
319
+ stampDocument,
320
+ stampedDiff,
321
+ stampedUpdate,
322
+ versionFilter,
323
+ versionIndexKey,
324
+ versionOf,
325
+ versioningOf,
326
+ };
@@ -0,0 +1,50 @@
1
+ const {
2
+ ConfigInvalidError,
3
+ MigronautError,
4
+ RevisionConflictError,
5
+ ShapeVersionError,
6
+ } = require('../errors/index.js');
7
+ const {
8
+ bumpRevision,
9
+ findOneAndUpdateWithRevision,
10
+ replaceWithRevision,
11
+ retryOnConflict,
12
+ updateWithRevision,
13
+ } = require('./occ.js');
14
+ const { isVersion } = require('./document.js');
15
+ const { versioningPlugin } = require('./mongoose.js');
16
+ const { defineShapes } = require('./registry.js');
17
+ const { upcaster } = require('./upcaster.js');
18
+
19
+ /**
20
+ * `@alexify/migronaut/versioning` — document versioning and optimistic
21
+ * concurrency for an application's repository layer.
22
+ *
23
+ * Deliberately small: it requires nothing but its own modules and the error
24
+ * classes — not the migration engine, not the driver, not mongoose — so a
25
+ * service that only reads and writes documents pays for nothing else. The
26
+ * error classes are the package root's own, so `instanceof` agrees whichever
27
+ * entry point a caller imports them from.
28
+ */
29
+ module.exports = {
30
+ // The application's view of its versioned collections
31
+ defineShapes,
32
+ upcaster,
33
+ isVersion,
34
+
35
+ // Optimistic concurrency
36
+ updateWithRevision,
37
+ replaceWithRevision,
38
+ findOneAndUpdateWithRevision,
39
+ retryOnConflict,
40
+ bumpRevision,
41
+
42
+ // Mongoose
43
+ versioningPlugin,
44
+
45
+ // Errors — the same classes the package root exports
46
+ MigronautError,
47
+ ConfigInvalidError,
48
+ RevisionConflictError,
49
+ ShapeVersionError,
50
+ };