@rpcbase/migrations 0.7.0 → 0.8.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/dist/diffResources.d.ts.map +1 -1
- package/dist/index.js +66 -32
- package/dist/index.js.map +1 -1
- package/dist/planner.d.ts +1 -1
- package/dist/planner.d.ts.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/runner.d.ts +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/testHarness.d.ts.map +1 -1
- package/dist/types.d.ts +11 -19
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diffResources.d.ts","sourceRoot":"","sources":["../src/diffResources.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAKV,cAAc,EACd,kBAAkB,EAEnB,MAAM,SAAS,CAAA;AAyChB,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,cAAc,EACtB,OAAO,cAAc,KACpB,
|
|
1
|
+
{"version":3,"file":"diffResources.d.ts","sourceRoot":"","sources":["../src/diffResources.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAKV,cAAc,EACd,kBAAkB,EAEnB,MAAM,SAAS,CAAA;AAyChB,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,cAAc,EACtB,OAAO,cAAc,KACpB,kBAmBF,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ var sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
|
15
15
|
var canonicalChecksum = (value) => sha256(canonicalStringify(value));
|
|
16
16
|
//#endregion
|
|
17
17
|
//#region src/resources.ts
|
|
18
|
-
var scopes = /* @__PURE__ */ new Set([
|
|
18
|
+
var scopes$1 = /* @__PURE__ */ new Set([
|
|
19
19
|
"global",
|
|
20
20
|
"tenant",
|
|
21
21
|
"filesystem"
|
|
@@ -28,7 +28,7 @@ var assertName = (value, label) => {
|
|
|
28
28
|
return normalized;
|
|
29
29
|
};
|
|
30
30
|
var assertScope = (scope) => {
|
|
31
|
-
if (!scopes.has(scope)) throw new Error(`Invalid migration scope: ${String(scope)}`);
|
|
31
|
+
if (!scopes$1.has(scope)) throw new Error(`Invalid migration scope: ${String(scope)}`);
|
|
32
32
|
};
|
|
33
33
|
var copyDocument = (value) => {
|
|
34
34
|
if (!value) return void 0;
|
|
@@ -196,14 +196,13 @@ var diffMongoResources = (before, after) => {
|
|
|
196
196
|
...searchIndexes,
|
|
197
197
|
...collectionValidators
|
|
198
198
|
];
|
|
199
|
-
const offline = changes.some((change) => change.kind !== "added");
|
|
200
199
|
return {
|
|
201
200
|
collections,
|
|
202
201
|
indexes,
|
|
203
202
|
searchIndexes,
|
|
204
203
|
collectionValidators,
|
|
205
204
|
changed: changes.length > 0,
|
|
206
|
-
|
|
205
|
+
requiresContract: changes.some((change) => change.kind !== "added")
|
|
207
206
|
};
|
|
208
207
|
};
|
|
209
208
|
//#endregion
|
|
@@ -215,11 +214,49 @@ var scopeRank = {
|
|
|
215
214
|
tenant: 1,
|
|
216
215
|
filesystem: 2
|
|
217
216
|
};
|
|
217
|
+
var scopes = Object.keys(scopeRank);
|
|
218
|
+
var bootstrapIntegrity = canonicalChecksum({
|
|
219
|
+
operation: "bootstrap-mongo-resources",
|
|
220
|
+
protocolVersion: 2,
|
|
221
|
+
revision: 1
|
|
222
|
+
});
|
|
223
|
+
var hasResources = (resources) => resources.collections.length > 0 || resources.indexes.length > 0 || resources.searchIndexes.length > 0 || resources.collectionValidators.length > 0;
|
|
224
|
+
var bootstrapId = (scope) => `00000000-bootstrap-${scope}`;
|
|
225
|
+
var runResourceBootstrap = async ({ helpers, resources }) => {
|
|
226
|
+
if (!resources) throw new Error("Missing bootstrap resource snapshot");
|
|
227
|
+
for (const index of resources.after.indexes) {
|
|
228
|
+
if (index.options?.unique !== true) continue;
|
|
229
|
+
const sparseFilter = index.options.sparse === true ? { $or: Object.keys(index.key).map((field) => ({ [field]: { $exists: true } })) } : void 0;
|
|
230
|
+
const partialFilter = index.options.partialFilterExpression;
|
|
231
|
+
const filter = sparseFilter && partialFilter ? { $and: [partialFilter, sparseFilter] } : partialFilter ?? sparseFilter;
|
|
232
|
+
const duplicates = await helpers.findDuplicateKeys(index.collection, index.key, {
|
|
233
|
+
...filter ? { filter } : {},
|
|
234
|
+
...index.options.collation ? { collation: index.options.collation } : {}
|
|
235
|
+
});
|
|
236
|
+
if (duplicates.length > 0) throw new Error(`Cannot create unique index ${index.collection}.${index.name}: duplicate keys found: ${JSON.stringify(duplicates)}`);
|
|
237
|
+
}
|
|
238
|
+
await helpers.reconcileResources(resources.after);
|
|
239
|
+
};
|
|
240
|
+
var createResourceBootstraps = (source, baseline) => scopes.flatMap((scope) => {
|
|
241
|
+
if (!hasResources(filterMongoResources(baseline, scope))) return [];
|
|
242
|
+
const id = bootstrapId(scope);
|
|
243
|
+
if (source.migrations.some((migration) => migration.id === id)) throw new Error(`Migration id ${id} is reserved for the ${source.name} resource bootstrap`);
|
|
244
|
+
return [Object.freeze({
|
|
245
|
+
id,
|
|
246
|
+
scope,
|
|
247
|
+
phase: "expand",
|
|
248
|
+
resources: Object.freeze({
|
|
249
|
+
before: null,
|
|
250
|
+
after: baseline.checksum
|
|
251
|
+
}),
|
|
252
|
+
up: runResourceBootstrap,
|
|
253
|
+
__rpcbaseIntegrity: bootstrapIntegrity
|
|
254
|
+
})];
|
|
255
|
+
});
|
|
218
256
|
var assertMigration = (migration) => {
|
|
219
257
|
if (!migrationIdPattern.test(migration.id)) throw new Error(`Invalid migration id "${migration.id}"`);
|
|
220
258
|
if (!(migration.scope in scopeRank)) throw new Error(`Invalid migration scope for ${migration.id}`);
|
|
221
|
-
if (migration.
|
|
222
|
-
if (migration.rollback !== void 0 && migration.rollback !== "compatible" && migration.rollback !== "incompatible") throw new Error(`Invalid migration rollback compatibility for ${migration.id}`);
|
|
259
|
+
if (migration.phase !== "expand" && migration.phase !== "contract") throw new Error(`Invalid migration phase for ${migration.id}`);
|
|
223
260
|
if (typeof migration.up !== "function") throw new Error(`Migration ${migration.id} is missing up()`);
|
|
224
261
|
if (migration.resources && migration.resources.before === migration.resources.after) throw new Error(`Migration ${migration.id} has an unchanged resource transition`);
|
|
225
262
|
};
|
|
@@ -227,7 +264,6 @@ var defineMigration = (migration) => {
|
|
|
227
264
|
assertMigration(migration);
|
|
228
265
|
return Object.freeze({
|
|
229
266
|
...migration,
|
|
230
|
-
rollback: migration.rollback ?? (migration.mode === "online" ? "compatible" : "incompatible"),
|
|
231
267
|
dependsOn: Object.freeze([...migration.dependsOn ?? []]),
|
|
232
268
|
...migration.resources ? { resources: Object.freeze({ ...migration.resources }) } : {}
|
|
233
269
|
});
|
|
@@ -235,6 +271,7 @@ var defineMigration = (migration) => {
|
|
|
235
271
|
var defineMigrationSource = (source) => {
|
|
236
272
|
const name = source.name.trim();
|
|
237
273
|
if (!sourceNamePattern.test(name)) throw new Error(`Invalid migration source name "${source.name}"`);
|
|
274
|
+
if (source.baseline === void 0 !== (source.resources === void 0)) throw new Error(`Migration source ${name} must declare baseline and resources together`);
|
|
238
275
|
let previousId = null;
|
|
239
276
|
const ids = /* @__PURE__ */ new Set();
|
|
240
277
|
for (const migration of source.migrations) {
|
|
@@ -262,8 +299,7 @@ var migrationChecksum = (migration, source) => {
|
|
|
262
299
|
id: migration.id,
|
|
263
300
|
source: source.name,
|
|
264
301
|
scope: migration.scope,
|
|
265
|
-
|
|
266
|
-
rollback: migration.rollback ?? (migration.mode === "online" ? "compatible" : "incompatible"),
|
|
302
|
+
phase: migration.phase,
|
|
267
303
|
dependsOn: migration.dependsOn ?? [],
|
|
268
304
|
resources: migration.resources ?? null,
|
|
269
305
|
codeChecksum
|
|
@@ -309,12 +345,18 @@ var compileMigrationRegistry = (inputSources, options = {}) => {
|
|
|
309
345
|
if (sourceNames.has(source.name)) throw new Error(`Duplicate migration source: ${source.name}`);
|
|
310
346
|
sourceNames.add(source.name);
|
|
311
347
|
const snapshots = /* @__PURE__ */ new Map();
|
|
312
|
-
for (const snapshot of [
|
|
348
|
+
for (const snapshot of [
|
|
349
|
+
...source.baseline ? [source.baseline] : [],
|
|
350
|
+
...source.resourceSnapshots ?? [],
|
|
351
|
+
...source.resources ? [source.resources] : []
|
|
352
|
+
]) {
|
|
313
353
|
const normalized = defineMongoResources(snapshot);
|
|
314
354
|
if (normalized.checksum !== snapshot.checksum) throw new Error(`Invalid resource snapshot checksum in ${source.name}: ${snapshot.checksum}`);
|
|
315
355
|
snapshots.set(normalized.checksum, normalized);
|
|
316
356
|
}
|
|
317
|
-
const
|
|
357
|
+
const baseline = source.baseline ? snapshots.get(source.baseline.checksum) : void 0;
|
|
358
|
+
const currentResources = source.resources ? snapshots.get(source.resources.checksum) : void 0;
|
|
359
|
+
const sourceMigrations = [...baseline ? createResourceBootstraps(source, baseline) : [], ...source.migrations].map((migration, sourcePosition) => {
|
|
318
360
|
if (migration.resources) {
|
|
319
361
|
if (migration.resources.before && !snapshots.has(migration.resources.before)) throw new Error(`Missing before resource snapshot for ${source.name}:${migration.id}`);
|
|
320
362
|
if (!snapshots.has(migration.resources.after)) throw new Error(`Missing after resource snapshot for ${source.name}:${migration.id}`);
|
|
@@ -328,14 +370,13 @@ var compileMigrationRegistry = (inputSources, options = {}) => {
|
|
|
328
370
|
sourcePosition,
|
|
329
371
|
sourcePriority: priority,
|
|
330
372
|
dependsOn: Object.freeze((migration.dependsOn ?? []).map((id) => resolveDependencyId(source.name, id))),
|
|
331
|
-
rollback: migration.rollback ?? (migration.mode === "online" ? "compatible" : "incompatible"),
|
|
332
373
|
checksum: integrity.checksum,
|
|
333
374
|
sealed: integrity.sealed
|
|
334
375
|
});
|
|
335
376
|
migrations.push(compiled);
|
|
336
377
|
return compiled;
|
|
337
378
|
});
|
|
338
|
-
for (const scope of
|
|
379
|
+
for (const scope of scopes) {
|
|
339
380
|
let previousChecksum = null;
|
|
340
381
|
let previousResources = defineMongoResources();
|
|
341
382
|
for (const migration of sourceMigrations.filter((item) => item.scope === scope && item.resources)) {
|
|
@@ -346,8 +387,8 @@ var compileMigrationRegistry = (inputSources, options = {}) => {
|
|
|
346
387
|
previousChecksum = transition.after;
|
|
347
388
|
previousResources = snapshots.get(previousChecksum) ?? defineMongoResources();
|
|
348
389
|
}
|
|
349
|
-
const currentForScope =
|
|
350
|
-
if (currentForScope
|
|
390
|
+
const currentForScope = currentResources ? filterMongoResources(currentResources, scope) : defineMongoResources();
|
|
391
|
+
if (hasResources(currentForScope) || previousChecksum) {
|
|
351
392
|
if (!previousChecksum) throw new Error(`Source ${source.name} has unmanaged ${scope} resources without a migration`);
|
|
352
393
|
const previousSnapshot = snapshots.get(previousChecksum);
|
|
353
394
|
if (!previousSnapshot || filterMongoResources(previousSnapshot, scope).checksum !== currentForScope.checksum) throw new Error(`Latest ${scope} resource transition for ${source.name} does not match current resources`);
|
|
@@ -355,6 +396,8 @@ var compileMigrationRegistry = (inputSources, options = {}) => {
|
|
|
355
396
|
}
|
|
356
397
|
compiledSources.push(Object.freeze({
|
|
357
398
|
...source,
|
|
399
|
+
...baseline ? { baseline } : {},
|
|
400
|
+
...currentResources ? { resources: currentResources } : {},
|
|
358
401
|
priority,
|
|
359
402
|
migrations: Object.freeze(sourceMigrations),
|
|
360
403
|
resourceSnapshots: snapshots
|
|
@@ -368,7 +411,7 @@ var compileMigrationRegistry = (inputSources, options = {}) => {
|
|
|
368
411
|
}
|
|
369
412
|
mergeMongoResources(compiledSources.flatMap((source) => source.resources ? [source.resources] : []));
|
|
370
413
|
return Object.freeze({
|
|
371
|
-
protocolVersion:
|
|
414
|
+
protocolVersion: 2,
|
|
372
415
|
sources: Object.freeze(compiledSources),
|
|
373
416
|
migrations: ordered,
|
|
374
417
|
migrationsById,
|
|
@@ -918,8 +961,7 @@ var toPlanItem = (migration) => ({
|
|
|
918
961
|
checksum: migration.checksum,
|
|
919
962
|
source: migration.source,
|
|
920
963
|
scope: migration.scope,
|
|
921
|
-
|
|
922
|
-
rollback: migration.rollback,
|
|
964
|
+
phase: migration.phase,
|
|
923
965
|
dependsOn: migration.dependsOn
|
|
924
966
|
});
|
|
925
967
|
var historyById = (history) => new Map(history.map((record) => [record._id, record]));
|
|
@@ -930,8 +972,7 @@ var validateKnownRecord = (migration, record, targetScope) => {
|
|
|
930
972
|
if (record.sourcePosition !== migration.sourcePosition) errors.push(`${migration.qualifiedId}: source position differs`);
|
|
931
973
|
if (record.scope !== migration.scope) errors.push(`${migration.qualifiedId}: scope differs`);
|
|
932
974
|
if (migration.scope !== targetScope) errors.push(`${migration.qualifiedId}: migration history is stored in a ${targetScope} database`);
|
|
933
|
-
if (record.
|
|
934
|
-
if (record.rollback !== migration.rollback) errors.push(`${migration.qualifiedId}: rollback compatibility differs`);
|
|
975
|
+
if (record.phase !== migration.phase) errors.push(`${migration.qualifiedId}: phase differs`);
|
|
935
976
|
if (![
|
|
936
977
|
"running",
|
|
937
978
|
"applied",
|
|
@@ -986,7 +1027,7 @@ var planMigrationTarget = async (registry, target, options = {}) => {
|
|
|
986
1027
|
continue;
|
|
987
1028
|
}
|
|
988
1029
|
unknown.push(record._id);
|
|
989
|
-
if (!(options.
|
|
1030
|
+
if (!(options.allowNewerApplied && record.status === "applied" && record.scope === target.scope)) integrityErrors.push(`${record._id}: applied migration is absent from the registry`);
|
|
990
1031
|
}
|
|
991
1032
|
integrityErrors.push(...validateSourcePrefixes(registry, target.scope, records));
|
|
992
1033
|
const applied = relevant.filter((migration) => records.get(migration.qualifiedId)?.status === "applied").map((migration) => migration.qualifiedId);
|
|
@@ -994,7 +1035,7 @@ var planMigrationTarget = async (registry, target, options = {}) => {
|
|
|
994
1035
|
const running = relevant.filter((migration) => records.get(migration.qualifiedId)?.status === "running").map((migration) => migration.qualifiedId);
|
|
995
1036
|
const failed = relevant.filter((migration) => records.get(migration.qualifiedId)?.status === "failed").map((migration) => migration.qualifiedId);
|
|
996
1037
|
const expectedResources = getExpectedResources(registry, target.scope, history);
|
|
997
|
-
const resourceDivergences = await inspectMongoResources(target.db, expectedResources, {
|
|
1038
|
+
const resourceDivergences = options.allowNewerApplied && unknown.length > 0 ? [] : await inspectMongoResources(target.db, expectedResources, {
|
|
998
1039
|
signal,
|
|
999
1040
|
requireSearchReady: true
|
|
1000
1041
|
});
|
|
@@ -1041,13 +1082,10 @@ var planMigrations = async (registry, provider, options = {}) => {
|
|
|
1041
1082
|
const databases = [];
|
|
1042
1083
|
for (const target of targets) databases.push(await planMigrationTarget(registry, target, options));
|
|
1043
1084
|
return {
|
|
1044
|
-
protocolVersion:
|
|
1085
|
+
protocolVersion: 2,
|
|
1045
1086
|
registryChecksum: registry.checksum,
|
|
1046
1087
|
databases,
|
|
1047
1088
|
hasPending: databases.some((database) => database.pending.length > 0 || database.running.length > 0 || database.failed.length > 0),
|
|
1048
|
-
hasOffline: databases.some((database) => database.pending.some((migration) => migration.mode === "offline") || [...database.running, ...database.failed].some((id) => registry.migrationsById.get(id)?.mode === "offline")),
|
|
1049
|
-
hasRollbackIncompatible: databases.some((database) => database.pending.some((migration) => migration.rollback === "incompatible") || [...database.running, ...database.failed].some((id) => registry.migrationsById.get(id)?.rollback === "incompatible")),
|
|
1050
|
-
hasAppliedRollbackIncompatible: databases.some((database) => database.applied.some((id) => registry.migrationsById.get(id)?.rollback === "incompatible")),
|
|
1051
1089
|
hasErrors: databases.some((database) => database.integrityErrors.length > 0 || database.running.length === 0 && database.failed.length === 0 && database.resourceDivergences.some((divergence) => divergence.code !== "runtime_index_options_mismatch"))
|
|
1052
1090
|
};
|
|
1053
1091
|
};
|
|
@@ -1207,8 +1245,7 @@ var recordForMigration = (migration, lock, attempt, release) => ({
|
|
|
1207
1245
|
source: migration.source,
|
|
1208
1246
|
sourcePosition: migration.sourcePosition,
|
|
1209
1247
|
scope: migration.scope,
|
|
1210
|
-
|
|
1211
|
-
rollback: migration.rollback,
|
|
1248
|
+
phase: migration.phase,
|
|
1212
1249
|
status: "running",
|
|
1213
1250
|
attempt,
|
|
1214
1251
|
...migration.resources?.before ? { resourcesBeforeHash: migration.resources.before } : {},
|
|
@@ -1368,7 +1405,6 @@ var runTarget = async (registry, target, lock, signal, options, earlierScopeDepe
|
|
|
1368
1405
|
const applied = new Set(plan.applied);
|
|
1369
1406
|
const migrationsToRun = registry.migrations.filter((migration) => migration.scope === target.scope && !applied.has(migration.qualifiedId));
|
|
1370
1407
|
for (const migration of migrationsToRun) {
|
|
1371
|
-
if (migration.mode === "offline" && !options.allowOffline) continue;
|
|
1372
1408
|
if (!sourcePredecessorsApplied(registry, migration, applied)) continue;
|
|
1373
1409
|
let dependenciesReady = true;
|
|
1374
1410
|
for (const dependencyId of migration.dependsOn) {
|
|
@@ -1461,7 +1497,6 @@ var runMigrations = async (registry, provider, options = {}) => {
|
|
|
1461
1497
|
var initializeTenantMigrations = async (registry, provider, tenantId, options = {}) => await runMigrations(registry, provider, {
|
|
1462
1498
|
...options,
|
|
1463
1499
|
tenantId,
|
|
1464
|
-
allowOffline: true,
|
|
1465
1500
|
initializeTenant: true
|
|
1466
1501
|
});
|
|
1467
1502
|
//#endregion
|
|
@@ -1577,7 +1612,6 @@ var testMigrationRegistry = async (options) => {
|
|
|
1577
1612
|
provisioningStatus: "active"
|
|
1578
1613
|
});
|
|
1579
1614
|
await runMigrations(options.registry, provider, {
|
|
1580
|
-
allowOffline: true,
|
|
1581
1615
|
concurrency: 1,
|
|
1582
1616
|
signal: options.signal
|
|
1583
1617
|
});
|
|
@@ -1587,7 +1621,7 @@ var testMigrationRegistry = async (options) => {
|
|
|
1587
1621
|
migrations: [defineMigration({
|
|
1588
1622
|
id: "20000101000000-checkpoint-recovery",
|
|
1589
1623
|
scope: "global",
|
|
1590
|
-
|
|
1624
|
+
phase: "expand",
|
|
1591
1625
|
async up({ checkpoint }) {
|
|
1592
1626
|
if (checkpoint.value === 1) return;
|
|
1593
1627
|
await checkpoint.save(1);
|