@warlock.js/cascade 4.9.0 → 4.9.2

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 CHANGED
@@ -4,6 +4,19 @@ All notable changes to `@warlock.js/cascade` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 4.9.2
8
+
9
+ ### Fixed
10
+
11
+ - `migrate:rollback` and `migrate:rollback --all` ran `down()` migrations in **apply order** instead of reverse. `getMigrationsToRollback` reversed the executed list and then re-sorted it ascending, which put it straight back into forward order and made the reverse dead code — so a rollback would drop a table before dropping the column added to it, failing with `relation "…" does not exist`. Any batch containing more than one migration was affected; single-migration batches hid it because one item has no order to get wrong
12
+ - migration ordering now lives in `migration-order.ts` with an explicit `sortMigrationsForRollback`. The descending sort is required, not cosmetic: the executed list is read back ordered by `batch, name`, so it is alphabetical rather than chronological and simply *not* re-sorting after the reverse would have produced reverse-alphabetical order — a different wrong answer
13
+
14
+ ## 4.9.1
15
+
16
+ ### Fixed
17
+
18
+ - `save({ merge })` silently dropped a `Date` written over a column that already held a `Date` — the dirty tracker's merge treated anything `typeof "object"` as mergeable and recursed into the `Date`, which has no own enumerable properties, so nothing was copied and the old value survived. The column never went dirty and `save()` returned `{ success: true, modifiedCount: 0 }` without issuing an `UPDATE`. Only plain objects deep-merge now; `Date`, `Map`, `Set`, `RegExp` and every other class instance replace, matching what `model.data` already did. Writing into an empty column always worked, so only overwrites were affected
19
+
7
20
  ## 4.9.0 - 2026-08-06
8
21
 
9
22
  ### Fixed
package/cjs/index.cjs CHANGED
@@ -691,8 +691,17 @@ var DatabaseDirtyTracker = class {
691
691
  /**
692
692
  * Recursively merges source object into target object, performing a deep merge.
693
693
  *
694
- * For nested objects, the merge is recursive. For arrays and primitives, the source
695
- * value replaces the target value. All values are cloned to prevent reference sharing.
694
+ * Only **plain** objects are merged recursively. Everything else arrays,
695
+ * primitives, and class instances such as `Date` / `Map` / `Set` / `RegExp` —
696
+ * replaces the target value. All values are cloned to prevent reference sharing.
697
+ *
698
+ * The plain-object guard is load-bearing, not tidiness. A bare
699
+ * `typeof value === "object"` also matches a `Date`, and `Object.entries(date)`
700
+ * is `[]` — so merging a `Date` over a column that already held a `Date`
701
+ * recursed into it, copied nothing, and left the old value in the snapshot.
702
+ * The column then never went dirty and `save()` returned
703
+ * `{ success: true, modifiedCount: 0 }` without issuing an `UPDATE`. This is
704
+ * the same lesson `canBeFlatten` above already encodes.
696
705
  *
697
706
  * @param target - The object to merge into
698
707
  * @param source - The object to merge from
@@ -700,7 +709,7 @@ var DatabaseDirtyTracker = class {
700
709
  */
701
710
  mergeIntoRaw(target, source) {
702
711
  for (const [key, value] of Object.entries(source)) {
703
- if (value && typeof value === "object" && !Array.isArray(value) && target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) {
712
+ if ((0, _mongez_supportive_is.isPlainObject)(value) && (0, _mongez_supportive_is.isPlainObject)(target[key])) {
704
713
  this.mergeIntoRaw(target[key], value);
705
714
  continue;
706
715
  }
@@ -21630,6 +21639,37 @@ function compareCreatedAt(a, b) {
21630
21639
  if (dateB) return 1;
21631
21640
  }
21632
21641
 
21642
+ //#endregion
21643
+ //#region ../cascade/src/migration/migration-order.ts
21644
+ /**
21645
+ * Comparator for applying migrations — oldest first.
21646
+ *
21647
+ * Priority:
21648
+ * 1. `createdAt` timestamp (older = earlier)
21649
+ * 2. Alphabetical by migration name (last resort)
21650
+ */
21651
+ function sortMigrations(a, b) {
21652
+ const byCreatedAt = compareCreatedAt(a.createdAt, b.createdAt);
21653
+ if (byCreatedAt !== void 0) return byCreatedAt;
21654
+ return a.migrationName.localeCompare(b.migrationName);
21655
+ }
21656
+ /**
21657
+ * Comparator for rolling migrations back — newest first, the exact inverse of
21658
+ * {@link sortMigrations}.
21659
+ *
21660
+ * A rollback has to undo migrations in the reverse of the order they were
21661
+ * applied, or a `down()` will hit schema its predecessor already removed —
21662
+ * dropping a table before dropping the column that was added to it.
21663
+ *
21664
+ * This must be an explicit descending sort rather than a `.reverse()` of the
21665
+ * executed list: that list is read back ordered by `batch, name`, so it is
21666
+ * alphabetical rather than chronological, and reversing it merely produces
21667
+ * reverse-alphabetical order.
21668
+ */
21669
+ function sortMigrationsForRollback(a, b) {
21670
+ return sortMigrations(b, a);
21671
+ }
21672
+
21633
21673
  //#endregion
21634
21674
  //#region ../cascade/src/migration/sql-grammar.ts
21635
21675
  /**
@@ -21754,18 +21794,6 @@ var SQLGrammar = class {
21754
21794
  //#endregion
21755
21795
  //#region ../cascade/src/migration/migration-runner.ts
21756
21796
  /**
21757
- * Comparator for sorting migration classes.
21758
- *
21759
- * Priority:
21760
- * 1. `createdAt` timestamp (older = earlier)
21761
- * 2. Alphabetical by migration name (last resort)
21762
- */
21763
- function sortMigrations(a, b) {
21764
- const byCreatedAt = compareCreatedAt(a.createdAt, b.createdAt);
21765
- if (byCreatedAt !== void 0) return byCreatedAt;
21766
- return a.migrationName.localeCompare(b.migrationName);
21767
- }
21768
- /**
21769
21797
  * Migration runner that executes migrations.
21770
21798
  *
21771
21799
  * This is a pure executor - it doesn't discover migrations.
@@ -22339,13 +22367,24 @@ var MigrationRunner = class {
22339
22367
  return this.migrations.filter((m) => !executedNames.has(m.migrationName)).sort(sortMigrations);
22340
22368
  }
22341
22369
  /**
22342
- * Get migrations to rollback.
22370
+ * Get migrations to rollback, newest-first.
22371
+ *
22372
+ * A rollback must undo migrations in the exact inverse of the order `up`
22373
+ * applied them, or a `down()` will hit schema its predecessor already
22374
+ * removed — dropping a table before dropping the column added to it, say.
22375
+ *
22376
+ * The sort has to be explicitly **descending**. Reversing the executed list
22377
+ * is not enough: `getExecutedMigrations` orders by `batch, name`, so the
22378
+ * input is alphabetical rather than chronological, and reversing it only
22379
+ * yields reverse-alphabetical order. (Sorting *ascending* here — which is
22380
+ * what this method used to do after a `.reverse()` — silently restored the
22381
+ * forward `up` order and made the reverse dead code.)
22343
22382
  */
22344
22383
  async getMigrationsToRollback(batches) {
22345
22384
  const executed = await this.getExecutedMigrations();
22346
22385
  if (executed.length === 0) return [];
22347
22386
  const batchNumbers = [...new Set(executed.map((r) => r.batch))].sort((a, b) => b - a).slice(0, batches);
22348
- return executed.filter((r) => batchNumbers.includes(r.batch)).reverse().map((r) => this.migrations.find((m) => m.migrationName === r.name)).filter((m) => !!m).sort(sortMigrations);
22387
+ return executed.filter((r) => batchNumbers.includes(r.batch)).map((r) => this.migrations.find((m) => m.migrationName === r.name)).filter((m) => !!m).sort(sortMigrationsForRollback);
22349
22388
  }
22350
22389
  /**
22351
22390
  * Get executed migration records.