@rpcbase/migrations 0.6.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.
@@ -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,kBAoBF,CAAA"}
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
- mode: offline ? "offline" : "online"
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.mode !== "online" && migration.mode !== "offline") throw new Error(`Invalid migration mode for ${migration.id}`);
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
- mode: migration.mode,
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 [...source.resourceSnapshots ?? [], ...source.resources ? [source.resources] : []]) {
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 sourceMigrations = source.migrations.map((migration, sourcePosition) => {
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 Object.keys(scopeRank)) {
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 = source.resources ? filterMongoResources(source.resources, scope) : defineMongoResources();
350
- if (currentForScope.collections.length > 0 || currentForScope.indexes.length > 0 || currentForScope.searchIndexes.length > 0 || currentForScope.collectionValidators.length > 0 || previousChecksum) {
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: 1,
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
- mode: migration.mode,
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.mode !== migration.mode) errors.push(`${migration.qualifiedId}: mode differs`);
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.rollback && record.status === "applied" && record.mode === "online" && record.rollback === "compatible")) integrityErrors.push(`${record._id}: applied migration is absent from the registry`);
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: 1,
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
- mode: migration.mode,
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
- mode: "online",
1624
+ phase: "expand",
1591
1625
  async up({ checkpoint }) {
1592
1626
  if (checkpoint.value === 1) return;
1593
1627
  await checkpoint.save(1);