@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 +13 -0
- package/cjs/index.cjs +56 -17
- package/cjs/index.cjs.map +1 -1
- package/esm/database-dirty-tracker.d.mts +11 -2
- package/esm/database-dirty-tracker.d.mts.map +1 -1
- package/esm/database-dirty-tracker.mjs +12 -3
- package/esm/database-dirty-tracker.mjs.map +1 -1
- package/esm/migration/migration-order.mjs +35 -0
- package/esm/migration/migration-order.mjs.map +1 -0
- package/esm/migration/migration-runner.d.mts +12 -1
- package/esm/migration/migration-runner.d.mts.map +1 -1
- package/esm/migration/migration-runner.mjs +15 -15
- package/esm/migration/migration-runner.mjs.map +1 -1
- package/llms-full.txt +18 -0
- package/package.json +4 -4
- package/skills/track-changes/SKILL.md +18 -0
|
@@ -211,8 +211,17 @@ declare class DatabaseDirtyTracker {
|
|
|
211
211
|
/**
|
|
212
212
|
* Recursively merges source object into target object, performing a deep merge.
|
|
213
213
|
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
214
|
+
* Only **plain** objects are merged recursively. Everything else — arrays,
|
|
215
|
+
* primitives, and class instances such as `Date` / `Map` / `Set` / `RegExp` —
|
|
216
|
+
* replaces the target value. All values are cloned to prevent reference sharing.
|
|
217
|
+
*
|
|
218
|
+
* The plain-object guard is load-bearing, not tidiness. A bare
|
|
219
|
+
* `typeof value === "object"` also matches a `Date`, and `Object.entries(date)`
|
|
220
|
+
* is `[]` — so merging a `Date` over a column that already held a `Date`
|
|
221
|
+
* recursed into it, copied nothing, and left the old value in the snapshot.
|
|
222
|
+
* The column then never went dirty and `save()` returned
|
|
223
|
+
* `{ success: true, modifiedCount: 0 }` without issuing an `UPDATE`. This is
|
|
224
|
+
* the same lesson `canBeFlatten` above already encodes.
|
|
216
225
|
*
|
|
217
226
|
* @param target - The object to merge into
|
|
218
227
|
* @param source - The object to merge from
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-dirty-tracker.d.mts","names":[],"sources":["../../../../../../cascade/src/database-dirty-tracker.ts"],"mappings":";;;;KA+CK,UAAA,GAAa,MAAM;;AAAA;AAAA;KAKnB,iBAAA;EAAsB,QAAA;EAAmB,QAAQ;AAAA;AAkBtD;;;;;;;;;;;;;;;;AAAA,cAAa,oBAAA;
|
|
1
|
+
{"version":3,"file":"database-dirty-tracker.d.mts","names":[],"sources":["../../../../../../cascade/src/database-dirty-tracker.ts"],"mappings":";;;;KA+CK,UAAA,GAAa,MAAM;;AAAA;AAAA;KAKnB,iBAAA;EAAsB,QAAA;EAAmB,QAAQ;AAAA;AAkBtD;;;;;;;;;;;;;;;;AAAA,cAAa,oBAAA;EA8SoB;;;;EAAA,UAzSrB,UAAA,EAAY,MAAA;EA6XY;;;EAAA,UAxXxB,UAAA,EAAY,MAAA;EAAA;;;;EAAA,UAMZ,gBAAA,EAAkB,UAAA;EAUT;;;EAAA,UALT,gBAAA,EAAkB,UAAA;;;;qBAKT,YAAA,EAAY,GAAA;EAmDxB;;;EAAA,mBA9CY,cAAA,EAAc,GAAA;cAEd,IAAA,EAAM,MAAA;EA0FW;;;;;;;;;;;;;EAnE7B,eAAA;EA4LqB;;;;;;;;;;;;;;;;;EAvKrB,UAAA;EAqT0B;;AAAC;EA9S3B,OAAA,CAAQ,MAAA;;;;;;;;;;;;;;;;EAmBR,iBAAA;;;;;;;;;;;;;;;;;EAoBA,yBAAA,IAA6B,MAAA,SAAe,iBAAA;;;;;;;;;;;;;;;;EA+B5C,kBAAA,CAAmB,IAAA,EAAM,MAAA;;;;;;;;;;;;;;;;;EAsBzB,YAAA,CAAa,OAAA,EAAS,MAAA;;;;;;;;;;;;;;;;EAqBtB,KAAA,CAAM,OAAA;;;;;;;;;;;;;;;;;;;;;EA+BN,KAAA,CAAM,IAAA,GAAO,MAAA;;;;;YAgBV,WAAA,CAAY,IAAA,EAAM,MAAA,oBAA0B,UAAA;;;;;;;;;;YAa5C,gBAAA;;;;;;;;;;;;;;;;;;;;YA6CA,YAAA,CAAa,MAAA,EAAQ,MAAA,mBAAyB,MAAA,EAAQ,MAAA;;;;;;;;;;YAoBtD,aAAA,CAAc,IAAA;;;;;;;;;;;YAwCd,cAAA,CAAe,SAAA,WAAoB,OAAA;;;;;;;;YAwBnC,SAAA,IAAa,IAAA,EAAM,CAAA,GAAI,CAAA;AAAA"}
|
|
@@ -280,8 +280,17 @@ var DatabaseDirtyTracker = class {
|
|
|
280
280
|
/**
|
|
281
281
|
* Recursively merges source object into target object, performing a deep merge.
|
|
282
282
|
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
283
|
+
* Only **plain** objects are merged recursively. Everything else — arrays,
|
|
284
|
+
* primitives, and class instances such as `Date` / `Map` / `Set` / `RegExp` —
|
|
285
|
+
* replaces the target value. All values are cloned to prevent reference sharing.
|
|
286
|
+
*
|
|
287
|
+
* The plain-object guard is load-bearing, not tidiness. A bare
|
|
288
|
+
* `typeof value === "object"` also matches a `Date`, and `Object.entries(date)`
|
|
289
|
+
* is `[]` — so merging a `Date` over a column that already held a `Date`
|
|
290
|
+
* recursed into it, copied nothing, and left the old value in the snapshot.
|
|
291
|
+
* The column then never went dirty and `save()` returned
|
|
292
|
+
* `{ success: true, modifiedCount: 0 }` without issuing an `UPDATE`. This is
|
|
293
|
+
* the same lesson `canBeFlatten` above already encodes.
|
|
285
294
|
*
|
|
286
295
|
* @param target - The object to merge into
|
|
287
296
|
* @param source - The object to merge from
|
|
@@ -289,7 +298,7 @@ var DatabaseDirtyTracker = class {
|
|
|
289
298
|
*/
|
|
290
299
|
mergeIntoRaw(target, source) {
|
|
291
300
|
for (const [key, value] of Object.entries(source)) {
|
|
292
|
-
if (
|
|
301
|
+
if (isPlainObject(value) && isPlainObject(target[key])) {
|
|
293
302
|
this.mergeIntoRaw(target[key], value);
|
|
294
303
|
continue;
|
|
295
304
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-dirty-tracker.mjs","names":[],"sources":["../../../../../../cascade/src/database-dirty-tracker.ts"],"sourcesContent":["import { areEqual, clone } from \"@mongez/reinforcements\";\nimport { isPlainObject } from \"@mongez/supportive-is\";\n\nfunction canBeFlatten(object: unknown): boolean {\n return isPlainObject(object);\n}\n\n/**\n * A fix for flatten as non-plain object is being flatten as well which it should not be\n */\nfunction flatten(\n object: Record<string, unknown>,\n separator = \".\",\n keepNestedOriginalObject = false,\n parent?: string,\n root: Record<string, unknown> = {},\n) {\n if (canBeFlatten(object) === false) {\n return object;\n }\n // object = toPlainObject(object);\n for (const key of Object.keys(object)) {\n const value = object[key];\n const keyChain = parent ? parent + separator + key : key;\n if ((Array.isArray(value) && value.length === 0) || typeof value === \"function\") {\n root[keyChain] = value;\n } else if (canBeFlatten(value)) {\n if (keepNestedOriginalObject) {\n root[keyChain] = value;\n }\n flatten(\n value as Record<string, unknown>,\n separator,\n keepNestedOriginalObject,\n keyChain,\n root,\n );\n } else {\n root[keyChain] = value;\n }\n }\n return root;\n}\n\n/**\n * Flattened record type representing dot-notation paths mapped to their values.\n */\ntype FlatRecord = Record<string, unknown>;\n\n/**\n * Represents the old and new values of a dirty column.\n */\ntype DirtyColumnValues = { oldValue: unknown; newValue: unknown };\n\n/**\n * Tracks changes to model data by maintaining snapshots of initial and current state.\n *\n * The tracker stores both raw (nested) and flattened (dot-notation) versions of the data\n * to accurately detect modifications, additions, and removals at any nesting level.\n *\n * @example\n * ```typescript\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\", age: 30 });\n * tracker.mergeChanges({ age: 31 });\n * console.log(tracker.hasChanges()); // true\n * console.log(tracker.getDirtyColumns()); // [\"age\"]\n * console.log(tracker.getDirtyColumnsWithValues());\n * // { age: { oldValue: 30, newValue: 31 } }\n * ```\n */\nexport class DatabaseDirtyTracker {\n /**\n * The initial raw data snapshot taken at construction or last reset.\n * Used as the baseline for comparison.\n */\n protected initialRaw: Record<string, unknown>;\n\n /**\n * The current raw data snapshot reflecting all changes made via merge/unset.\n */\n protected currentRaw: Record<string, unknown>;\n\n /**\n * Flattened version of the initial data using dot-notation keys.\n * Example: { \"address.city\": \"NYC\" }\n */\n protected initialFlattened: FlatRecord;\n\n /**\n * Flattened version of the current data using dot-notation keys.\n */\n protected currentFlattened: FlatRecord;\n\n /**\n * Set of column names (dot-notation paths) that have been modified.\n */\n protected readonly dirtyColumns = new Set<string>();\n\n /**\n * Set of column names (dot-notation paths) that existed initially but have been removed.\n */\n protected readonly removedColumns = new Set<string>();\n\n public constructor(data: Record<string, unknown>) {\n this.initialRaw = this.cloneData(data);\n this.currentRaw = this.cloneData(data);\n\n this.initialFlattened = this.flattenData(this.initialRaw);\n this.currentFlattened = { ...this.initialFlattened };\n\n this.updateDirtyState();\n }\n\n /**\n * Returns the list of dirty columns using dot-notation.\n *\n * A column is considered dirty if its value has changed compared to the initial snapshot.\n *\n * @returns An array of column names (dot-notation paths) that have been modified\n *\n * @example\n * ```typescript\n * tracker.mergeChanges({ name: \"Bob\", \"address.city\": \"LA\" });\n * tracker.getDirtyColumns(); // [\"name\", \"address.city\"]\n * ```\n */\n public getDirtyColumns(): string[] {\n return Array.from(this.dirtyColumns);\n }\n\n /**\n * Determines whether there are any tracked changes.\n *\n * Returns `true` if any columns have been modified or removed since the initial snapshot.\n *\n * @returns `true` if there are changes, `false` otherwise\n *\n * @example\n * ```typescript\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\" });\n * tracker.hasChanges(); // false\n * tracker.mergeChanges({ name: \"Bob\" });\n * tracker.hasChanges(); // true\n * tracker.unset(\"name\");\n * tracker.hasChanges(); // true (removed column counts as a change)\n * ```\n */\n public hasChanges(): boolean {\n return this.dirtyColumns.size > 0 || this.removedColumns.size > 0;\n }\n\n /**\n * Check if the given column is dirty (changed)\n */\n public isDirty(column: string): boolean {\n return this.dirtyColumns.has(column);\n }\n\n /**\n * Returns the set of columns that have been removed compared to the baseline.\n *\n * A column is considered removed if it existed in the initial snapshot but has been\n * explicitly unset or deleted from the current data.\n *\n * @returns An array of column names (dot-notation paths) that have been removed\n *\n * @example\n * ```typescript\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\", temp: \"value\" });\n * tracker.unset(\"temp\");\n * tracker.getRemovedColumns(); // [\"temp\"]\n * ```\n */\n public getRemovedColumns(): string[] {\n return Array.from(this.removedColumns);\n }\n\n /**\n * Provides a mapping of dirty columns to their previous and current values.\n *\n * This is useful for generating audit logs, building partial update payloads,\n * or displaying change summaries to users.\n *\n * @returns A record mapping each dirty column to an object containing oldValue and newValue\n *\n * @example\n * ```typescript\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\", age: 30 });\n * tracker.mergeChanges({ age: 31 });\n * tracker.getDirtyColumnsWithValues();\n * // { age: { oldValue: 30, newValue: 31 } }\n * ```\n */\n public getDirtyColumnsWithValues(): Record<string, DirtyColumnValues> {\n const result: Record<string, DirtyColumnValues> = {};\n\n for (const column of this.dirtyColumns) {\n const hasCurrent =\n this.currentFlattened[column] !== undefined || column in this.currentFlattened;\n\n result[column] = {\n oldValue: this.initialFlattened[column],\n newValue: hasCurrent ? this.currentFlattened[column] : undefined,\n };\n }\n\n return result;\n }\n\n /**\n * Replaces the current data snapshot entirely and recomputes the diff.\n *\n * This is useful when you want to replace all current data with a new set,\n * while keeping the initial baseline for comparison.\n *\n * @param data - The new data to set as the current snapshot\n *\n * @example\n * ```typescript\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\" });\n * tracker.replaceCurrentData({ name: \"Bob\", email: \"bob@example.com\" });\n * tracker.getDirtyColumns(); // [\"name\", \"email\"]\n * ```\n */\n public replaceCurrentData(data: Record<string, unknown>): void {\n this.currentRaw = this.cloneData(data);\n this.currentFlattened = this.flattenData(this.currentRaw);\n this.updateDirtyState();\n }\n\n /**\n * Merges a partial payload into the current snapshot and recomputes the diff.\n *\n * This performs a deep merge, preserving existing nested structures while\n * updating only the specified fields.\n *\n * @param partial - Partial data to merge into the current snapshot\n *\n * @example\n * ```typescript\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\", address: { city: \"NYC\" } });\n * tracker.mergeChanges({ address: { zip: \"10001\" } });\n * // Current data: { name: \"Alice\", address: { city: \"NYC\", zip: \"10001\" } }\n * tracker.getDirtyColumns(); // [\"address.zip\"]\n * ```\n */\n public mergeChanges(partial: Record<string, unknown>): void {\n this.mergeIntoRaw(this.currentRaw, partial);\n this.currentFlattened = this.flattenData(this.currentRaw);\n this.updateDirtyState();\n }\n\n /**\n * Explicitly removes one or more columns from the current data.\n *\n * Supports both single column names and arrays of column names.\n * Columns can be specified using dot-notation for nested paths.\n *\n * @param columns - A single column name or an array of column names to remove\n *\n * @example\n * ```typescript\n * tracker.unset(\"tempField\");\n * tracker.unset([\"field1\", \"field2\", \"nested.field\"]);\n * tracker.getRemovedColumns(); // [\"tempField\", \"field1\", \"field2\", \"nested.field\"]\n * ```\n */\n public unset(columns: string | string[]): void {\n const targets = Array.isArray(columns) ? columns : [columns];\n\n for (const path of targets) {\n this.deleteFromRaw(path);\n }\n\n this.currentFlattened = this.flattenData(this.currentRaw);\n this.updateDirtyState();\n }\n\n /**\n * Resets both the initial and current snapshots to the provided data.\n *\n * If no data is provided, the current snapshot becomes the new baseline.\n * This clears all tracked changes and removed columns.\n *\n * @param data - Optional new data to use as the baseline. If omitted, uses current data.\n *\n * @example\n * ```typescript\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\" });\n * tracker.mergeChanges({ name: \"Bob\" });\n * tracker.hasChanges(); // true\n * tracker.reset(); // Make current state the new baseline\n * tracker.hasChanges(); // false\n *\n * // Or reset to entirely new data:\n * tracker.reset({ name: \"Charlie\", age: 25 });\n * ```\n */\n public reset(data?: Record<string, unknown>): void {\n const source = data ?? this.currentRaw;\n this.initialRaw = this.cloneData(source);\n this.currentRaw = this.cloneData(source);\n\n this.initialFlattened = this.flattenData(this.initialRaw);\n this.currentFlattened = this.flattenData(this.currentRaw);\n\n this.dirtyColumns.clear();\n this.removedColumns.clear();\n }\n\n /**\n * Flattens the given data object.\n * Can be overridden by subclasses to change flattening behavior.\n */\n protected flattenData(data: Record<string, unknown>): FlatRecord {\n return flatten(data);\n }\n\n /**\n * Recomputes the dirty and removed column sets by comparing initial and current snapshots.\n *\n * This method is called internally after any operation that modifies the current data.\n * It iterates through all keys in both flattened snapshots and determines which columns\n * have been modified or removed.\n *\n * @protected\n */\n protected updateDirtyState(): void {\n this.dirtyColumns.clear();\n this.removedColumns.clear();\n\n const keys = new Set([\n ...Object.keys(this.initialFlattened),\n ...Object.keys(this.currentFlattened),\n ]);\n\n for (const key of keys) {\n const hasCurrent = this.currentFlattened[key] !== undefined || key in this.currentFlattened;\n const hasInitial = this.initialFlattened[key] !== undefined || key in this.initialFlattened;\n\n if (!hasCurrent && hasInitial) {\n this.removedColumns.add(key);\n }\n\n const initialValue = this.initialFlattened[key];\n const currentValue = hasCurrent ? this.currentFlattened[key] : undefined;\n\n if (!areEqual(initialValue, currentValue)) {\n this.dirtyColumns.add(key);\n }\n }\n }\n\n /**\n * Recursively merges source object into target object, performing a deep merge.\n *\n * For nested objects, the merge is recursive. For arrays and primitives, the source\n * value replaces the target value. All values are cloned to prevent reference sharing.\n *\n * @param target - The object to merge into\n * @param source - The object to merge from\n * @private\n */\n protected mergeIntoRaw(target: Record<string, unknown>, source: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(source)) {\n if (\n value &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n target[key] &&\n typeof target[key] === \"object\" &&\n !Array.isArray(target[key])\n ) {\n this.mergeIntoRaw(target[key] as Record<string, unknown>, value as Record<string, unknown>);\n continue;\n }\n\n target[key] = this.cloneData(value);\n }\n }\n\n /**\n * Deletes a field from the current raw data using a dot-notation path.\n *\n * Supports nested paths (e.g., \"address.city\") and array indices (e.g., \"items.0\").\n * If any segment in the path doesn't exist, the operation is a no-op.\n *\n * @param path - The dot-notation path to the field to delete\n * @private\n */\n protected deleteFromRaw(path: string): void {\n const segments = path.split(\".\");\n let container: unknown = this.currentRaw;\n\n for (let index = 0; index < segments.length - 1; index += 1) {\n if (container === undefined || container === null) {\n return;\n }\n\n container = this.resolveSegment(container, segments[index]);\n }\n\n if (container === undefined || container === null) {\n return;\n }\n\n const lastSegment = segments[segments.length - 1];\n if (Array.isArray(container)) {\n const numericIndex = Number(lastSegment);\n if (!Number.isNaN(numericIndex)) {\n container.splice(numericIndex, 1);\n }\n return;\n }\n\n if (typeof container === \"object\") {\n delete (container as Record<string, unknown>)[lastSegment];\n }\n }\n\n /**\n * Resolves a single segment of a dot-notation path within a container.\n *\n * Handles both object property access and array index access.\n *\n * @param container - The object or array to access\n * @param segment - The property name or array index as a string\n * @returns The value at the specified segment, or undefined if not found\n * @private\n */\n protected resolveSegment(container: unknown, segment: string): unknown {\n if (Array.isArray(container)) {\n const numericIndex = Number(segment);\n if (Number.isNaN(numericIndex)) {\n return undefined;\n }\n\n return container[numericIndex];\n }\n\n if (container && typeof container === \"object\") {\n return (container as Record<string, unknown>)[segment];\n }\n\n return undefined;\n }\n\n /**\n * Creates a deep clone of the provided data.\n *\n * @param data - The data to clone\n * @returns A deep clone of the data\n * @private\n */\n protected cloneData<T>(data: T): T {\n return clone(data);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database-dirty-tracker.mjs","names":[],"sources":["../../../../../../cascade/src/database-dirty-tracker.ts"],"sourcesContent":["import { areEqual, clone } from \"@mongez/reinforcements\";\r\nimport { isPlainObject } from \"@mongez/supportive-is\";\r\n\r\nfunction canBeFlatten(object: unknown): boolean {\r\n return isPlainObject(object);\r\n}\r\n\r\n/**\r\n * A fix for flatten as non-plain object is being flatten as well which it should not be\r\n */\r\nfunction flatten(\r\n object: Record<string, unknown>,\r\n separator = \".\",\r\n keepNestedOriginalObject = false,\r\n parent?: string,\r\n root: Record<string, unknown> = {},\r\n) {\r\n if (canBeFlatten(object) === false) {\r\n return object;\r\n }\r\n // object = toPlainObject(object);\r\n for (const key of Object.keys(object)) {\r\n const value = object[key];\r\n const keyChain = parent ? parent + separator + key : key;\r\n if ((Array.isArray(value) && value.length === 0) || typeof value === \"function\") {\r\n root[keyChain] = value;\r\n } else if (canBeFlatten(value)) {\r\n if (keepNestedOriginalObject) {\r\n root[keyChain] = value;\r\n }\r\n flatten(\r\n value as Record<string, unknown>,\r\n separator,\r\n keepNestedOriginalObject,\r\n keyChain,\r\n root,\r\n );\r\n } else {\r\n root[keyChain] = value;\r\n }\r\n }\r\n return root;\r\n}\r\n\r\n/**\r\n * Flattened record type representing dot-notation paths mapped to their values.\r\n */\r\ntype FlatRecord = Record<string, unknown>;\r\n\r\n/**\r\n * Represents the old and new values of a dirty column.\r\n */\r\ntype DirtyColumnValues = { oldValue: unknown; newValue: unknown };\r\n\r\n/**\r\n * Tracks changes to model data by maintaining snapshots of initial and current state.\r\n *\r\n * The tracker stores both raw (nested) and flattened (dot-notation) versions of the data\r\n * to accurately detect modifications, additions, and removals at any nesting level.\r\n *\r\n * @example\r\n * ```typescript\r\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\", age: 30 });\r\n * tracker.mergeChanges({ age: 31 });\r\n * console.log(tracker.hasChanges()); // true\r\n * console.log(tracker.getDirtyColumns()); // [\"age\"]\r\n * console.log(tracker.getDirtyColumnsWithValues());\r\n * // { age: { oldValue: 30, newValue: 31 } }\r\n * ```\r\n */\r\nexport class DatabaseDirtyTracker {\r\n /**\r\n * The initial raw data snapshot taken at construction or last reset.\r\n * Used as the baseline for comparison.\r\n */\r\n protected initialRaw: Record<string, unknown>;\r\n\r\n /**\r\n * The current raw data snapshot reflecting all changes made via merge/unset.\r\n */\r\n protected currentRaw: Record<string, unknown>;\r\n\r\n /**\r\n * Flattened version of the initial data using dot-notation keys.\r\n * Example: { \"address.city\": \"NYC\" }\r\n */\r\n protected initialFlattened: FlatRecord;\r\n\r\n /**\r\n * Flattened version of the current data using dot-notation keys.\r\n */\r\n protected currentFlattened: FlatRecord;\r\n\r\n /**\r\n * Set of column names (dot-notation paths) that have been modified.\r\n */\r\n protected readonly dirtyColumns = new Set<string>();\r\n\r\n /**\r\n * Set of column names (dot-notation paths) that existed initially but have been removed.\r\n */\r\n protected readonly removedColumns = new Set<string>();\r\n\r\n public constructor(data: Record<string, unknown>) {\r\n this.initialRaw = this.cloneData(data);\r\n this.currentRaw = this.cloneData(data);\r\n\r\n this.initialFlattened = this.flattenData(this.initialRaw);\r\n this.currentFlattened = { ...this.initialFlattened };\r\n\r\n this.updateDirtyState();\r\n }\r\n\r\n /**\r\n * Returns the list of dirty columns using dot-notation.\r\n *\r\n * A column is considered dirty if its value has changed compared to the initial snapshot.\r\n *\r\n * @returns An array of column names (dot-notation paths) that have been modified\r\n *\r\n * @example\r\n * ```typescript\r\n * tracker.mergeChanges({ name: \"Bob\", \"address.city\": \"LA\" });\r\n * tracker.getDirtyColumns(); // [\"name\", \"address.city\"]\r\n * ```\r\n */\r\n public getDirtyColumns(): string[] {\r\n return Array.from(this.dirtyColumns);\r\n }\r\n\r\n /**\r\n * Determines whether there are any tracked changes.\r\n *\r\n * Returns `true` if any columns have been modified or removed since the initial snapshot.\r\n *\r\n * @returns `true` if there are changes, `false` otherwise\r\n *\r\n * @example\r\n * ```typescript\r\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\" });\r\n * tracker.hasChanges(); // false\r\n * tracker.mergeChanges({ name: \"Bob\" });\r\n * tracker.hasChanges(); // true\r\n * tracker.unset(\"name\");\r\n * tracker.hasChanges(); // true (removed column counts as a change)\r\n * ```\r\n */\r\n public hasChanges(): boolean {\r\n return this.dirtyColumns.size > 0 || this.removedColumns.size > 0;\r\n }\r\n\r\n /**\r\n * Check if the given column is dirty (changed)\r\n */\r\n public isDirty(column: string): boolean {\r\n return this.dirtyColumns.has(column);\r\n }\r\n\r\n /**\r\n * Returns the set of columns that have been removed compared to the baseline.\r\n *\r\n * A column is considered removed if it existed in the initial snapshot but has been\r\n * explicitly unset or deleted from the current data.\r\n *\r\n * @returns An array of column names (dot-notation paths) that have been removed\r\n *\r\n * @example\r\n * ```typescript\r\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\", temp: \"value\" });\r\n * tracker.unset(\"temp\");\r\n * tracker.getRemovedColumns(); // [\"temp\"]\r\n * ```\r\n */\r\n public getRemovedColumns(): string[] {\r\n return Array.from(this.removedColumns);\r\n }\r\n\r\n /**\r\n * Provides a mapping of dirty columns to their previous and current values.\r\n *\r\n * This is useful for generating audit logs, building partial update payloads,\r\n * or displaying change summaries to users.\r\n *\r\n * @returns A record mapping each dirty column to an object containing oldValue and newValue\r\n *\r\n * @example\r\n * ```typescript\r\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\", age: 30 });\r\n * tracker.mergeChanges({ age: 31 });\r\n * tracker.getDirtyColumnsWithValues();\r\n * // { age: { oldValue: 30, newValue: 31 } }\r\n * ```\r\n */\r\n public getDirtyColumnsWithValues(): Record<string, DirtyColumnValues> {\r\n const result: Record<string, DirtyColumnValues> = {};\r\n\r\n for (const column of this.dirtyColumns) {\r\n const hasCurrent =\r\n this.currentFlattened[column] !== undefined || column in this.currentFlattened;\r\n\r\n result[column] = {\r\n oldValue: this.initialFlattened[column],\r\n newValue: hasCurrent ? this.currentFlattened[column] : undefined,\r\n };\r\n }\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Replaces the current data snapshot entirely and recomputes the diff.\r\n *\r\n * This is useful when you want to replace all current data with a new set,\r\n * while keeping the initial baseline for comparison.\r\n *\r\n * @param data - The new data to set as the current snapshot\r\n *\r\n * @example\r\n * ```typescript\r\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\" });\r\n * tracker.replaceCurrentData({ name: \"Bob\", email: \"bob@example.com\" });\r\n * tracker.getDirtyColumns(); // [\"name\", \"email\"]\r\n * ```\r\n */\r\n public replaceCurrentData(data: Record<string, unknown>): void {\r\n this.currentRaw = this.cloneData(data);\r\n this.currentFlattened = this.flattenData(this.currentRaw);\r\n this.updateDirtyState();\r\n }\r\n\r\n /**\r\n * Merges a partial payload into the current snapshot and recomputes the diff.\r\n *\r\n * This performs a deep merge, preserving existing nested structures while\r\n * updating only the specified fields.\r\n *\r\n * @param partial - Partial data to merge into the current snapshot\r\n *\r\n * @example\r\n * ```typescript\r\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\", address: { city: \"NYC\" } });\r\n * tracker.mergeChanges({ address: { zip: \"10001\" } });\r\n * // Current data: { name: \"Alice\", address: { city: \"NYC\", zip: \"10001\" } }\r\n * tracker.getDirtyColumns(); // [\"address.zip\"]\r\n * ```\r\n */\r\n public mergeChanges(partial: Record<string, unknown>): void {\r\n this.mergeIntoRaw(this.currentRaw, partial);\r\n this.currentFlattened = this.flattenData(this.currentRaw);\r\n this.updateDirtyState();\r\n }\r\n\r\n /**\r\n * Explicitly removes one or more columns from the current data.\r\n *\r\n * Supports both single column names and arrays of column names.\r\n * Columns can be specified using dot-notation for nested paths.\r\n *\r\n * @param columns - A single column name or an array of column names to remove\r\n *\r\n * @example\r\n * ```typescript\r\n * tracker.unset(\"tempField\");\r\n * tracker.unset([\"field1\", \"field2\", \"nested.field\"]);\r\n * tracker.getRemovedColumns(); // [\"tempField\", \"field1\", \"field2\", \"nested.field\"]\r\n * ```\r\n */\r\n public unset(columns: string | string[]): void {\r\n const targets = Array.isArray(columns) ? columns : [columns];\r\n\r\n for (const path of targets) {\r\n this.deleteFromRaw(path);\r\n }\r\n\r\n this.currentFlattened = this.flattenData(this.currentRaw);\r\n this.updateDirtyState();\r\n }\r\n\r\n /**\r\n * Resets both the initial and current snapshots to the provided data.\r\n *\r\n * If no data is provided, the current snapshot becomes the new baseline.\r\n * This clears all tracked changes and removed columns.\r\n *\r\n * @param data - Optional new data to use as the baseline. If omitted, uses current data.\r\n *\r\n * @example\r\n * ```typescript\r\n * const tracker = new DatabaseDirtyTracker({ name: \"Alice\" });\r\n * tracker.mergeChanges({ name: \"Bob\" });\r\n * tracker.hasChanges(); // true\r\n * tracker.reset(); // Make current state the new baseline\r\n * tracker.hasChanges(); // false\r\n *\r\n * // Or reset to entirely new data:\r\n * tracker.reset({ name: \"Charlie\", age: 25 });\r\n * ```\r\n */\r\n public reset(data?: Record<string, unknown>): void {\r\n const source = data ?? this.currentRaw;\r\n this.initialRaw = this.cloneData(source);\r\n this.currentRaw = this.cloneData(source);\r\n\r\n this.initialFlattened = this.flattenData(this.initialRaw);\r\n this.currentFlattened = this.flattenData(this.currentRaw);\r\n\r\n this.dirtyColumns.clear();\r\n this.removedColumns.clear();\r\n }\r\n\r\n /**\r\n * Flattens the given data object.\r\n * Can be overridden by subclasses to change flattening behavior.\r\n */\r\n protected flattenData(data: Record<string, unknown>): FlatRecord {\r\n return flatten(data);\r\n }\r\n\r\n /**\r\n * Recomputes the dirty and removed column sets by comparing initial and current snapshots.\r\n *\r\n * This method is called internally after any operation that modifies the current data.\r\n * It iterates through all keys in both flattened snapshots and determines which columns\r\n * have been modified or removed.\r\n *\r\n * @protected\r\n */\r\n protected updateDirtyState(): void {\r\n this.dirtyColumns.clear();\r\n this.removedColumns.clear();\r\n\r\n const keys = new Set([\r\n ...Object.keys(this.initialFlattened),\r\n ...Object.keys(this.currentFlattened),\r\n ]);\r\n\r\n for (const key of keys) {\r\n const hasCurrent = this.currentFlattened[key] !== undefined || key in this.currentFlattened;\r\n const hasInitial = this.initialFlattened[key] !== undefined || key in this.initialFlattened;\r\n\r\n if (!hasCurrent && hasInitial) {\r\n this.removedColumns.add(key);\r\n }\r\n\r\n const initialValue = this.initialFlattened[key];\r\n const currentValue = hasCurrent ? this.currentFlattened[key] : undefined;\r\n\r\n if (!areEqual(initialValue, currentValue)) {\r\n this.dirtyColumns.add(key);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Recursively merges source object into target object, performing a deep merge.\r\n *\r\n * Only **plain** objects are merged recursively. Everything else — arrays,\r\n * primitives, and class instances such as `Date` / `Map` / `Set` / `RegExp` —\r\n * replaces the target value. All values are cloned to prevent reference sharing.\r\n *\r\n * The plain-object guard is load-bearing, not tidiness. A bare\r\n * `typeof value === \"object\"` also matches a `Date`, and `Object.entries(date)`\r\n * is `[]` — so merging a `Date` over a column that already held a `Date`\r\n * recursed into it, copied nothing, and left the old value in the snapshot.\r\n * The column then never went dirty and `save()` returned\r\n * `{ success: true, modifiedCount: 0 }` without issuing an `UPDATE`. This is\r\n * the same lesson `canBeFlatten` above already encodes.\r\n *\r\n * @param target - The object to merge into\r\n * @param source - The object to merge from\r\n * @private\r\n */\r\n protected mergeIntoRaw(target: Record<string, unknown>, source: Record<string, unknown>): void {\r\n for (const [key, value] of Object.entries(source)) {\r\n if (isPlainObject(value) && isPlainObject(target[key])) {\r\n this.mergeIntoRaw(target[key] as Record<string, unknown>, value as Record<string, unknown>);\r\n continue;\r\n }\r\n\r\n target[key] = this.cloneData(value);\r\n }\r\n }\r\n\r\n /**\r\n * Deletes a field from the current raw data using a dot-notation path.\r\n *\r\n * Supports nested paths (e.g., \"address.city\") and array indices (e.g., \"items.0\").\r\n * If any segment in the path doesn't exist, the operation is a no-op.\r\n *\r\n * @param path - The dot-notation path to the field to delete\r\n * @private\r\n */\r\n protected deleteFromRaw(path: string): void {\r\n const segments = path.split(\".\");\r\n let container: unknown = this.currentRaw;\r\n\r\n for (let index = 0; index < segments.length - 1; index += 1) {\r\n if (container === undefined || container === null) {\r\n return;\r\n }\r\n\r\n container = this.resolveSegment(container, segments[index]);\r\n }\r\n\r\n if (container === undefined || container === null) {\r\n return;\r\n }\r\n\r\n const lastSegment = segments[segments.length - 1];\r\n if (Array.isArray(container)) {\r\n const numericIndex = Number(lastSegment);\r\n if (!Number.isNaN(numericIndex)) {\r\n container.splice(numericIndex, 1);\r\n }\r\n return;\r\n }\r\n\r\n if (typeof container === \"object\") {\r\n delete (container as Record<string, unknown>)[lastSegment];\r\n }\r\n }\r\n\r\n /**\r\n * Resolves a single segment of a dot-notation path within a container.\r\n *\r\n * Handles both object property access and array index access.\r\n *\r\n * @param container - The object or array to access\r\n * @param segment - The property name or array index as a string\r\n * @returns The value at the specified segment, or undefined if not found\r\n * @private\r\n */\r\n protected resolveSegment(container: unknown, segment: string): unknown {\r\n if (Array.isArray(container)) {\r\n const numericIndex = Number(segment);\r\n if (Number.isNaN(numericIndex)) {\r\n return undefined;\r\n }\r\n\r\n return container[numericIndex];\r\n }\r\n\r\n if (container && typeof container === \"object\") {\r\n return (container as Record<string, unknown>)[segment];\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n /**\r\n * Creates a deep clone of the provided data.\r\n *\r\n * @param data - The data to clone\r\n * @returns A deep clone of the data\r\n * @private\r\n */\r\n protected cloneData<T>(data: T): T {\r\n return clone(data);\r\n }\r\n}\r\n"],"mappings":";;;;AAGA,SAAS,aAAa,QAA0B;CAC9C,OAAO,cAAc,MAAM;AAC7B;;;;AAKA,SAAS,QACP,QACA,YAAY,KACZ,2BAA2B,OAC3B,QACA,OAAgC,CAAC,GACjC;CACA,IAAI,aAAa,MAAM,MAAM,OAC3B,OAAO;CAGT,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG;EACrC,MAAM,QAAQ,OAAO;EACrB,MAAM,WAAW,SAAS,SAAS,YAAY,MAAM;EACrD,IAAK,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,KAAM,OAAO,UAAU,YACnE,KAAK,YAAY;OACZ,IAAI,aAAa,KAAK,GAAG;GAC9B,IAAI,0BACF,KAAK,YAAY;GAEnB,QACE,OACA,WACA,0BACA,UACA,IACF;EACF,OACE,KAAK,YAAY;CAErB;CACA,OAAO;AACT;;;;;;;;;;;;;;;;;AA4BA,IAAa,uBAAb,MAAkC;;;;;CAKhC,AAAU;;;;CAKV,AAAU;;;;;CAMV,AAAU;;;;CAKV,AAAU;;;;CAKV,AAAmB,+BAAe,IAAI,IAAY;;;;CAKlD,AAAmB,iCAAiB,IAAI,IAAY;CAEpD,AAAO,YAAY,MAA+B;EAChD,KAAK,aAAa,KAAK,UAAU,IAAI;EACrC,KAAK,aAAa,KAAK,UAAU,IAAI;EAErC,KAAK,mBAAmB,KAAK,YAAY,KAAK,UAAU;EACxD,KAAK,mBAAmB,EAAE,GAAG,KAAK,iBAAiB;EAEnD,KAAK,iBAAiB;CACxB;;;;;;;;;;;;;;CAeA,AAAO,kBAA4B;EACjC,OAAO,MAAM,KAAK,KAAK,YAAY;CACrC;;;;;;;;;;;;;;;;;;CAmBA,AAAO,aAAsB;EAC3B,OAAO,KAAK,aAAa,OAAO,KAAK,KAAK,eAAe,OAAO;CAClE;;;;CAKA,AAAO,QAAQ,QAAyB;EACtC,OAAO,KAAK,aAAa,IAAI,MAAM;CACrC;;;;;;;;;;;;;;;;CAiBA,AAAO,oBAA8B;EACnC,OAAO,MAAM,KAAK,KAAK,cAAc;CACvC;;;;;;;;;;;;;;;;;CAkBA,AAAO,4BAA+D;EACpE,MAAM,SAA4C,CAAC;EAEnD,KAAK,MAAM,UAAU,KAAK,cAAc;GACtC,MAAM,aACJ,KAAK,iBAAiB,YAAY,UAAa,UAAU,KAAK;GAEhE,OAAO,UAAU;IACf,UAAU,KAAK,iBAAiB;IAChC,UAAU,aAAa,KAAK,iBAAiB,UAAU;GACzD;EACF;EAEA,OAAO;CACT;;;;;;;;;;;;;;;;CAiBA,AAAO,mBAAmB,MAAqC;EAC7D,KAAK,aAAa,KAAK,UAAU,IAAI;EACrC,KAAK,mBAAmB,KAAK,YAAY,KAAK,UAAU;EACxD,KAAK,iBAAiB;CACxB;;;;;;;;;;;;;;;;;CAkBA,AAAO,aAAa,SAAwC;EAC1D,KAAK,aAAa,KAAK,YAAY,OAAO;EAC1C,KAAK,mBAAmB,KAAK,YAAY,KAAK,UAAU;EACxD,KAAK,iBAAiB;CACxB;;;;;;;;;;;;;;;;CAiBA,AAAO,MAAM,SAAkC;EAC7C,MAAM,UAAU,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO;EAE3D,KAAK,MAAM,QAAQ,SACjB,KAAK,cAAc,IAAI;EAGzB,KAAK,mBAAmB,KAAK,YAAY,KAAK,UAAU;EACxD,KAAK,iBAAiB;CACxB;;;;;;;;;;;;;;;;;;;;;CAsBA,AAAO,MAAM,MAAsC;EACjD,MAAM,SAAS,QAAQ,KAAK;EAC5B,KAAK,aAAa,KAAK,UAAU,MAAM;EACvC,KAAK,aAAa,KAAK,UAAU,MAAM;EAEvC,KAAK,mBAAmB,KAAK,YAAY,KAAK,UAAU;EACxD,KAAK,mBAAmB,KAAK,YAAY,KAAK,UAAU;EAExD,KAAK,aAAa,MAAM;EACxB,KAAK,eAAe,MAAM;CAC5B;;;;;CAMA,AAAU,YAAY,MAA2C;EAC/D,OAAO,QAAQ,IAAI;CACrB;;;;;;;;;;CAWA,AAAU,mBAAyB;EACjC,KAAK,aAAa,MAAM;EACxB,KAAK,eAAe,MAAM;EAE1B,MAAM,OAAO,IAAI,IAAI,CACnB,GAAG,OAAO,KAAK,KAAK,gBAAgB,GACpC,GAAG,OAAO,KAAK,KAAK,gBAAgB,CACtC,CAAC;EAED,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,aAAa,KAAK,iBAAiB,SAAS,UAAa,OAAO,KAAK;GAC3E,MAAM,aAAa,KAAK,iBAAiB,SAAS,UAAa,OAAO,KAAK;GAE3E,IAAI,CAAC,cAAc,YACjB,KAAK,eAAe,IAAI,GAAG;GAG7B,MAAM,eAAe,KAAK,iBAAiB;GAG3C,IAAI,CAAC,SAAS,cAFO,aAAa,KAAK,iBAAiB,OAAO,MAEvB,GACtC,KAAK,aAAa,IAAI,GAAG;EAE7B;CACF;;;;;;;;;;;;;;;;;;;;CAqBA,AAAU,aAAa,QAAiC,QAAuC;EAC7F,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;GACjD,IAAI,cAAc,KAAK,KAAK,cAAc,OAAO,IAAI,GAAG;IACtD,KAAK,aAAa,OAAO,MAAiC,KAAgC;IAC1F;GACF;GAEA,OAAO,OAAO,KAAK,UAAU,KAAK;EACpC;CACF;;;;;;;;;;CAWA,AAAU,cAAc,MAAoB;EAC1C,MAAM,WAAW,KAAK,MAAM,GAAG;EAC/B,IAAI,YAAqB,KAAK;EAE9B,KAAK,IAAI,QAAQ,GAAG,QAAQ,SAAS,SAAS,GAAG,SAAS,GAAG;GAC3D,IAAI,cAAc,UAAa,cAAc,MAC3C;GAGF,YAAY,KAAK,eAAe,WAAW,SAAS,MAAM;EAC5D;EAEA,IAAI,cAAc,UAAa,cAAc,MAC3C;EAGF,MAAM,cAAc,SAAS,SAAS,SAAS;EAC/C,IAAI,MAAM,QAAQ,SAAS,GAAG;GAC5B,MAAM,eAAe,OAAO,WAAW;GACvC,IAAI,CAAC,OAAO,MAAM,YAAY,GAC5B,UAAU,OAAO,cAAc,CAAC;GAElC;EACF;EAEA,IAAI,OAAO,cAAc,UACvB,OAAQ,UAAsC;CAElD;;;;;;;;;;;CAYA,AAAU,eAAe,WAAoB,SAA0B;EACrE,IAAI,MAAM,QAAQ,SAAS,GAAG;GAC5B,MAAM,eAAe,OAAO,OAAO;GACnC,IAAI,OAAO,MAAM,YAAY,GAC3B;GAGF,OAAO,UAAU;EACnB;EAEA,IAAI,aAAa,OAAO,cAAc,UACpC,OAAQ,UAAsC;CAIlD;;;;;;;;CASA,AAAU,UAAa,MAAY;EACjC,OAAO,MAAM,IAAI;CACnB;AACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { compareCreatedAt } from "./parse-created-at.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../cascade/src/migration/migration-order.ts
|
|
4
|
+
/**
|
|
5
|
+
* Comparator for applying migrations — oldest first.
|
|
6
|
+
*
|
|
7
|
+
* Priority:
|
|
8
|
+
* 1. `createdAt` timestamp (older = earlier)
|
|
9
|
+
* 2. Alphabetical by migration name (last resort)
|
|
10
|
+
*/
|
|
11
|
+
function sortMigrations(a, b) {
|
|
12
|
+
const byCreatedAt = compareCreatedAt(a.createdAt, b.createdAt);
|
|
13
|
+
if (byCreatedAt !== void 0) return byCreatedAt;
|
|
14
|
+
return a.migrationName.localeCompare(b.migrationName);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Comparator for rolling migrations back — newest first, the exact inverse of
|
|
18
|
+
* {@link sortMigrations}.
|
|
19
|
+
*
|
|
20
|
+
* A rollback has to undo migrations in the reverse of the order they were
|
|
21
|
+
* applied, or a `down()` will hit schema its predecessor already removed —
|
|
22
|
+
* dropping a table before dropping the column that was added to it.
|
|
23
|
+
*
|
|
24
|
+
* This must be an explicit descending sort rather than a `.reverse()` of the
|
|
25
|
+
* executed list: that list is read back ordered by `batch, name`, so it is
|
|
26
|
+
* alphabetical rather than chronological, and reversing it merely produces
|
|
27
|
+
* reverse-alphabetical order.
|
|
28
|
+
*/
|
|
29
|
+
function sortMigrationsForRollback(a, b) {
|
|
30
|
+
return sortMigrations(b, a);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { sortMigrations, sortMigrationsForRollback };
|
|
35
|
+
//# sourceMappingURL=migration-order.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-order.mjs","names":[],"sources":["../../../../../../../cascade/src/migration/migration-order.ts"],"sourcesContent":["import { compareCreatedAt } from \"./parse-created-at\";\n\n/** The minimum shape ordering needs — the runner passes full migration classes. */\ntype Orderable = {\n createdAt?: string;\n migrationName: string;\n};\n\n/**\n * Comparator for applying migrations — oldest first.\n *\n * Priority:\n * 1. `createdAt` timestamp (older = earlier)\n * 2. Alphabetical by migration name (last resort)\n */\nexport function sortMigrations(a: Orderable, b: Orderable): number {\n const byCreatedAt = compareCreatedAt(a.createdAt, b.createdAt);\n\n if (byCreatedAt !== undefined) {\n return byCreatedAt;\n }\n\n // Last resort: alphabetical\n return a.migrationName.localeCompare(b.migrationName);\n}\n\n/**\n * Comparator for rolling migrations back — newest first, the exact inverse of\n * {@link sortMigrations}.\n *\n * A rollback has to undo migrations in the reverse of the order they were\n * applied, or a `down()` will hit schema its predecessor already removed —\n * dropping a table before dropping the column that was added to it.\n *\n * This must be an explicit descending sort rather than a `.reverse()` of the\n * executed list: that list is read back ordered by `batch, name`, so it is\n * alphabetical rather than chronological, and reversing it merely produces\n * reverse-alphabetical order.\n */\nexport function sortMigrationsForRollback(a: Orderable, b: Orderable): number {\n return sortMigrations(b, a);\n}\n"],"mappings":";;;;;;;;;;AAeA,SAAgB,eAAe,GAAc,GAAsB;CACjE,MAAM,cAAc,iBAAiB,EAAE,WAAW,EAAE,SAAS;CAE7D,IAAI,gBAAgB,QAClB,OAAO;CAIT,OAAO,EAAE,cAAc,cAAc,EAAE,aAAa;AACtD;;;;;;;;;;;;;;AAeA,SAAgB,0BAA0B,GAAc,GAAsB;CAC5E,OAAO,eAAe,GAAG,CAAC;AAC5B"}
|
|
@@ -253,7 +253,18 @@ declare class MigrationRunner {
|
|
|
253
253
|
*/
|
|
254
254
|
private getPendingMigrations;
|
|
255
255
|
/**
|
|
256
|
-
* Get migrations to rollback.
|
|
256
|
+
* Get migrations to rollback, newest-first.
|
|
257
|
+
*
|
|
258
|
+
* A rollback must undo migrations in the exact inverse of the order `up`
|
|
259
|
+
* applied them, or a `down()` will hit schema its predecessor already
|
|
260
|
+
* removed — dropping a table before dropping the column added to it, say.
|
|
261
|
+
*
|
|
262
|
+
* The sort has to be explicitly **descending**. Reversing the executed list
|
|
263
|
+
* is not enough: `getExecutedMigrations` orders by `batch, name`, so the
|
|
264
|
+
* input is alphabetical rather than chronological, and reversing it only
|
|
265
|
+
* yields reverse-alphabetical order. (Sorting *ascending* here — which is
|
|
266
|
+
* what this method used to do after a `.reverse()` — silently restored the
|
|
267
|
+
* forward `up` order and made the reverse dead code.)
|
|
257
268
|
*/
|
|
258
269
|
private getMigrationsToRollback;
|
|
259
270
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-runner.d.mts","names":[],"sources":["../../../../../../../cascade/src/migration/migration-runner.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"migration-runner.d.mts","names":[],"sources":["../../../../../../../cascade/src/migration/migration-runner.ts"],"mappings":";;;;;;;AAW2E;KAKtE,cAAA,cAA4B,SAAS;EACxC,aAAA;EACA,SAAA;AAAA;;;;KAgBG,cAAA;EAAA,uDAEM,MAAA;WAEA,MAAM;AAAA;AAqCjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,cAAa,eAAA;EA63B2B;EAAA,SA33BtB,UAAA,EAAY,cAAA;EA23BiB;EAAA,QAx3BrC,UAAA;EAHoB;EAAA,QAMpB,qBAAA;EAAA;EAAA,iBAGS,eAAA;EAGA;EAAA,iBAAA,OAAA;EASb;;;;;cADF,OAAA;IACE,UAAA,GAAa,UAAA;IACb,eAAA;IACA,OAAA;EAAA;EA4DG;;;EA7CA,aAAA,CAAc,UAAA,EAAY,UAAA;EAmED;;;EAAA,QA1DxB,aAAA;EAiGK;;;EAAA,QAvFL,kBAAA;EAyFN;;;;;;;;;;;;;;;EA/DK,QAAA,CAAS,cAAA,EAAgB,cAAA;EAuTE;;;;;;EAjS3B,YAAA,CAAa,UAAA,EAAY,cAAA;EAsXyB;;;EA5WlD,KAAA;EAyXI;;;EAjXJ,kBAAA;EA4ZM;;;;;;;;;;;;;EAvYA,GAAA,CACX,cAAA,EAAgB,cAAA,EAChB,OAAA,GAAS,cAAA,GACR,OAAA,CAAQ,eAAA;EAqaP;;;;;;;;;;;;EAlZS,QAAA,CACX,cAAA,EAAgB,cAAA,EAChB,OAAA,GAAS,cAAA,GACR,OAAA,CAAQ,eAAA;EAuvBG;;;AAQkB;AAOlC;;;;AAAoD;;;;;;;EA5uBrC,MAAA,CAAO,OAAA,GAAS,cAAA,GAAsB,OAAA,CAAQ,eAAA;;;;;EAuM9C,SAAA,CAAU,OAAA;IAAW,WAAA;IAAuB,OAAA;EAAA,IAA2B,OAAA;;;;;;;EAqFvE,YAAA,CAAa,OAAA,GAAS,cAAA,GAAsB,OAAA,CAAQ,eAAA;;;;;;;;EAWpD,eAAA,CACX,OAAA,UACA,OAAA,GAAS,cAAA,GACR,OAAA,CAAQ,eAAA;;;;;;;EA0CE,WAAA,CAAY,OAAA,GAAS,cAAA,GAAsB,OAAA,CAAQ,eAAA;;;;;;;EAiBnD,KAAA,CAAM,OAAA,GAAS,cAAA,GAAsB,OAAA,CAAQ,eAAA;;;;EAa7C,MAAA,IAAU,OAAA,CACrB,KAAA;IACE,IAAA;IACA,KAAA;IACA,QAAA;IACA,KAAA;EAAA;;;;;;;;;;;;UAkCU,wBAAA;;;;UAwCA,YAAA;;;;;;;;;UAgJN,uBAAA;;;;;;;;;;;;;;;;;;;UAyBA,kBAAA;;;;UAqCM,oBAAA;;;;;;;;;;;;;;;UAsBA,uBAAA;;;;EAmBD,qBAAA,IAAyB,OAAA,CAAQ,eAAA;;;;UAmBhC,eAAA;;;;UAkBA,qBAAA;;;;UAQA,kBAAA;AAAA;AAAA,cAOH,eAAA,EAAe,eAAwB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dataSourceRegistry } from "../data-source/data-source-registry.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { parseCreatedAt } from "./parse-created-at.mjs";
|
|
3
|
+
import { sortMigrations, sortMigrationsForRollback } from "./migration-order.mjs";
|
|
3
4
|
import { SQLGrammar } from "./sql-grammar.mjs";
|
|
4
5
|
import { colors } from "@mongez/copper";
|
|
5
6
|
import { log } from "@warlock.js/logger";
|
|
@@ -8,18 +9,6 @@ import path from "path";
|
|
|
8
9
|
|
|
9
10
|
//#region ../cascade/src/migration/migration-runner.ts
|
|
10
11
|
/**
|
|
11
|
-
* Comparator for sorting migration classes.
|
|
12
|
-
*
|
|
13
|
-
* Priority:
|
|
14
|
-
* 1. `createdAt` timestamp (older = earlier)
|
|
15
|
-
* 2. Alphabetical by migration name (last resort)
|
|
16
|
-
*/
|
|
17
|
-
function sortMigrations(a, b) {
|
|
18
|
-
const byCreatedAt = compareCreatedAt(a.createdAt, b.createdAt);
|
|
19
|
-
if (byCreatedAt !== void 0) return byCreatedAt;
|
|
20
|
-
return a.migrationName.localeCompare(b.migrationName);
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
12
|
* Migration runner that executes migrations.
|
|
24
13
|
*
|
|
25
14
|
* This is a pure executor - it doesn't discover migrations.
|
|
@@ -593,13 +582,24 @@ var MigrationRunner = class {
|
|
|
593
582
|
return this.migrations.filter((m) => !executedNames.has(m.migrationName)).sort(sortMigrations);
|
|
594
583
|
}
|
|
595
584
|
/**
|
|
596
|
-
* Get migrations to rollback.
|
|
585
|
+
* Get migrations to rollback, newest-first.
|
|
586
|
+
*
|
|
587
|
+
* A rollback must undo migrations in the exact inverse of the order `up`
|
|
588
|
+
* applied them, or a `down()` will hit schema its predecessor already
|
|
589
|
+
* removed — dropping a table before dropping the column added to it, say.
|
|
590
|
+
*
|
|
591
|
+
* The sort has to be explicitly **descending**. Reversing the executed list
|
|
592
|
+
* is not enough: `getExecutedMigrations` orders by `batch, name`, so the
|
|
593
|
+
* input is alphabetical rather than chronological, and reversing it only
|
|
594
|
+
* yields reverse-alphabetical order. (Sorting *ascending* here — which is
|
|
595
|
+
* what this method used to do after a `.reverse()` — silently restored the
|
|
596
|
+
* forward `up` order and made the reverse dead code.)
|
|
597
597
|
*/
|
|
598
598
|
async getMigrationsToRollback(batches) {
|
|
599
599
|
const executed = await this.getExecutedMigrations();
|
|
600
600
|
if (executed.length === 0) return [];
|
|
601
601
|
const batchNumbers = [...new Set(executed.map((r) => r.batch))].sort((a, b) => b - a).slice(0, batches);
|
|
602
|
-
return executed.filter((r) => batchNumbers.includes(r.batch)).
|
|
602
|
+
return executed.filter((r) => batchNumbers.includes(r.batch)).map((r) => this.migrations.find((m) => m.migrationName === r.name)).filter((m) => !!m).sort(sortMigrationsForRollback);
|
|
603
603
|
}
|
|
604
604
|
/**
|
|
605
605
|
* Get executed migration records.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-runner.mjs","names":[],"sources":["../../../../../../../cascade/src/migration/migration-runner.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { log } from \"@warlock.js/logger\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { MigrationDriverContract } from \"../contracts/migration-driver.contract\";\nimport type { DataSource } from \"../data-source/data-source\";\nimport { dataSourceRegistry } from \"../data-source/data-source-registry\";\nimport { type Migration } from \"./migration\";\nimport { compareCreatedAt, parseCreatedAt } from \"./parse-created-at\";\nimport { SQLGrammar } from \"./sql-grammar\";\nimport type { MigrationRecord, MigrationResult, TaggedSQL } from \"./types\";\n\n/**\n * Migration class type with static name property.\n */\ntype MigrationClass = (new () => Migration) & {\n migrationName: string;\n createdAt?: string;\n};\n\n/**\n * Resolved instance data for a single pending migration.\n * @internal\n */\ntype MigrationData = {\n MigrationClass: MigrationClass;\n migration: Migration;\n name: string;\n};\n\n/**\n * Options for migration execution.\n */\ntype ExecuteOptions = {\n /** Run in dry-run mode (no actual changes) */\n readonly dryRun?: boolean;\n /** Record to migrations table (default: true for batch, false for single) */\n readonly record?: boolean;\n};\n\n/**\n * Comparator for sorting migration classes.\n *\n * Priority:\n * 1. `createdAt` timestamp (older = earlier)\n * 2. Alphabetical by migration name (last resort)\n */\nfunction sortMigrations(\n a: { createdAt?: string; migrationName: string },\n b: { createdAt?: string; migrationName: string },\n): number {\n const byCreatedAt = compareCreatedAt(a.createdAt, b.createdAt);\n\n if (byCreatedAt !== undefined) {\n return byCreatedAt;\n }\n\n // Last resort: alphabetical\n return a.migrationName.localeCompare(b.migrationName);\n}\n\n/**\n * Migration runner that executes migrations.\n *\n * This is a pure executor - it doesn't discover migrations.\n * Discovery is handled by the framework (e.g., @warlock.js/core CLI).\n *\n * The migration name is read from the static `name` property on the class,\n * which should be set by the CLI after importing:\n *\n * @example\n * ```typescript\n * // In CLI after importing:\n * const { default: MigrationClass } = await import(\"./create-users.migration.ts\");\n * MigrationClass.migrationName ??= \"create-users\";\n *\n * // Then register or execute:\n * runner.register(MigrationClass);\n * // or\n * await runner.execute(MigrationClass);\n * ```\n *\n * @example\n * ```typescript\n * // Simple direct execution\n * await runner.execute(CreateUsersTable);\n * await runner.rollback(CreateUsersTable);\n *\n * // Registry pattern for batch operations\n * runner.register(CreateUsersTable);\n * runner.register(AddEmailIndex);\n * await runner.runAll();\n * await runner.rollbackAll();\n * ```\n */\nexport class MigrationRunner {\n /** Registered migrations */\n public readonly migrations: MigrationClass[] = [];\n\n /** Data source to use */\n private dataSource?: DataSource;\n\n /** Cached migration driver */\n private cachedMigrationDriver?: MigrationDriverContract;\n\n /** Table name for tracking migrations */\n private readonly migrationsTable: string;\n\n /** Whether to log operations */\n private readonly verbose: boolean;\n\n /**\n * Create a new migration runner.\n *\n * @param options - Runner options\n */\n public constructor(\n options: {\n dataSource?: DataSource;\n migrationsTable?: string;\n verbose?: boolean;\n } = {},\n ) {\n this.dataSource = options.dataSource;\n this.migrationsTable = options.migrationsTable ?? \"_migrations\";\n this.verbose = options.verbose ?? true;\n }\n\n // ============================================================================\n // DATA SOURCE\n // ============================================================================\n\n /**\n * Set the data source.\n */\n public setDataSource(dataSource: DataSource): this {\n this.dataSource = dataSource;\n this.cachedMigrationDriver = undefined;\n return this;\n }\n\n /**\n * Get the data source.\n */\n private getDataSource(): DataSource {\n if (!this.dataSource) {\n this.dataSource = dataSourceRegistry.get();\n }\n return this.dataSource;\n }\n\n /**\n * Get the migration driver.\n */\n private getMigrationDriver(): MigrationDriverContract {\n if (!this.cachedMigrationDriver) {\n this.cachedMigrationDriver = this.getDataSource().driver.migrationDriver();\n }\n return this.cachedMigrationDriver;\n }\n\n // ============================================================================\n // REGISTRATION\n // ============================================================================\n\n /**\n * Register a migration.\n *\n * The migration name is read from `MigrationClass.migrationName`.\n *\n * @param MigrationClass - Migration class (must have static `name` set)\n * @param createdAt - Optional timestamp for ordering\n * @returns This runner for chaining\n *\n * @example\n * ```typescript\n * CreateUsersTable.migrationName = \"2024-01-15_create-users\";\n * runner.register(CreateUsersTable);\n * ```\n */\n public register(MigrationClass: MigrationClass): this {\n const name = MigrationClass.migrationName;\n if (!name) {\n throw new Error(\n `Migration class must have a static 'migrationName' property set. ` +\n `Set it in CLI after importing: MigrationClass.migrationName = \"filename\";`,\n );\n }\n // Avoid duplicates\n if (!this.migrations.some((m) => m.migrationName === name)) {\n this.migrations.push(MigrationClass);\n }\n\n return this;\n }\n\n /**\n * Register multiple migrations.\n *\n * @param migrations - Array of migration classes\n * @returns This runner for chaining\n */\n public registerMany(migrations: MigrationClass[]): this {\n for (const MigrationClass of migrations) {\n this.register(MigrationClass);\n }\n return this;\n }\n\n /**\n * Clear all registered migrations.\n */\n public clear(): this {\n this.migrations.length = 0;\n return this;\n }\n\n /**\n * Get all registered migration names.\n */\n public getRegisteredNames(): string[] {\n return this.migrations.map((m) => m.migrationName);\n }\n\n // ============================================================================\n // SINGLE EXECUTION\n // ============================================================================\n\n /**\n * Execute a single migration's up() method.\n *\n * @param MigrationClass - Migration class to execute\n * @param options - Execution options\n * @returns Migration result\n *\n * @example\n * ```typescript\n * await runner.execute(CreateUsersTable);\n * await runner.execute(AddEmailIndex, { dryRun: true });\n * ```\n */\n public async run(\n MigrationClass: MigrationClass,\n options: ExecuteOptions = {},\n ): Promise<MigrationResult> {\n return this.runMigration(MigrationClass, \"up\", {\n dryRun: options.dryRun,\n record: options.record ?? false,\n });\n }\n\n /**\n * Execute a single migration's down() method.\n *\n * @param MigrationClass - Migration class to rollback\n * @param options - Execution options\n * @returns Migration result\n *\n * @example\n * ```typescript\n * await runner.rollback(CreateUsersTable);\n * ```\n */\n public async rollback(\n MigrationClass: MigrationClass,\n options: ExecuteOptions = {},\n ): Promise<MigrationResult> {\n return this.runMigration(MigrationClass, \"down\", {\n dryRun: options.dryRun,\n record: options.record ?? false,\n });\n }\n\n // ============================================================================\n // BATCH EXECUTION (REGISTERED MIGRATIONS)\n // ============================================================================\n\n /**\n * Run all pending registered migrations.\n *\n * Only runs migrations not already in the migrations table.\n *\n * @param options - Execution options\n * @returns Results for each migration\n *\n * @example\n * ```typescript\n * runner.register(CreateUsersTable);\n * runner.register(AddEmailIndex);\n * const results = await runner.runAll();\n * ```\n */\n public async runAll(options: ExecuteOptions = {}): Promise<MigrationResult[]> {\n const { dryRun = false, record = true } = options;\n\n const results: MigrationResult[] = [];\n\n // Get pending migrations\n const pending = await this.getPendingMigrations();\n\n if (pending.length === 0) {\n log.warn(\"database\", \"migration\", \"Nothing to migrate.\");\n return results;\n }\n\n // Drivers without SQL serialization (MongoDB) execute each migration's\n // pending operations directly through the migration driver — there is no\n // SQL pool to collect or phase-sort.\n if (this.getDataSource().driver.supportsSqlSerialization === false) {\n const batch = await this.getNextBatchNumber();\n for (const MigrationClass of pending) {\n const result = await this.runMigration(MigrationClass, \"up\", { dryRun, record, batch });\n results.push(result);\n\n if (!result.success) {\n break;\n }\n }\n return results;\n }\n\n log.info(\n \"database\",\n \"migration\",\n `Found ${pending.length} pending migration(s). Generating SQL pool...`,\n );\n const nextBatch = await this.getNextBatchNumber();\n\n const taggedStatements: TaggedSQL[] = [];\n const migrationsData: MigrationData[] = [];\n\n // 1. Collect SQL from each pending migration.\n // Fire extension checks concurrently as we encounter CREATE EXTENSION\n // statements — they resolve before execution begins.\n const extensionChecks: Promise<void>[] = [];\n\n for (const MigrationClass of pending) {\n const migration = this.createMigrationInstance(MigrationClass);\n const name = MigrationClass.migrationName;\n\n await migration.up();\n const upStatements = migration.toSQL();\n\n migrationsData.push({ MigrationClass, migration, name });\n\n for (const sql of upStatements) {\n const statementType = SQLGrammar.classify(sql);\n\n if (statementType === \"CREATE_EXTENSION\") {\n const ext = SQLGrammar.extractExtensionName(sql);\n if (ext) extensionChecks.push(this.informIfExtensionMissing(ext));\n }\n\n taggedStatements.push({\n sql,\n phase: SQLGrammar.phase(sql),\n statementType,\n createdAt: MigrationClass.createdAt,\n migrationName: name,\n });\n }\n }\n\n // 2. Resolve all extension checks before any SQL is executed.\n // Each check displays a rich message if the extension is missing\n // but does not throw — execution continues and Postgres will\n // surface its own error with full context already shown.\n await Promise.all(extensionChecks);\n\n // 3. Sort all SQL statements globally across all pending migrations\n const sortedStatements = SQLGrammar.sort(taggedStatements);\n\n // 4. Execute in a single batch\n if (dryRun) {\n log.info(\"database\", \"migration\", \"Dry run enabled. Would execute the following statements:\");\n for (const statement of sortedStatements) {\n console.log(\n `-- [${statement.statementType}] Phase ${statement.phase} [${statement.migrationName}]`,\n );\n console.log(statement.sql + \";\\n\");\n }\n return [];\n }\n\n const driver = this.getDataSource().driver;\n\n let transactionFailed = false;\n let errorMessage = \"\";\n /** The migration name that owns the SQL statement that threw. */\n let failingMigrationName: string | undefined;\n\n const startTime = Date.now();\n\n /**\n * Execute all sorted statements, capturing which migration owns the\n * statement that throws — so we report a precise culprit instead of\n * blaming every migration in the batch.\n */\n const executeStatements = async (): Promise<void> => {\n for (const statement of sortedStatements) {\n try {\n await driver.query(statement.sql);\n } catch (err) {\n failingMigrationName = statement.migrationName;\n throw err;\n }\n }\n\n if (record) {\n for (const data of migrationsData) {\n await this.recordMigration(\n data.name,\n nextBatch,\n data.MigrationClass.createdAt\n ? parseCreatedAt(data.MigrationClass.createdAt)\n : new Date(),\n );\n }\n }\n };\n\n try {\n if (driver.transaction) {\n await driver.transaction(executeStatements);\n } else {\n await executeStatements();\n }\n } catch (err) {\n transactionFailed = true;\n errorMessage = err instanceof Error ? err.message : String(err);\n }\n\n const durationMs = Date.now() - startTime;\n\n // Report results per-migration.\n // Only the migration that owns the failing statement is marked as failed;\n // all others are reported as rolled back / not reached.\n for (const data of migrationsData) {\n const isCulprit = transactionFailed && data.name === failingMigrationName;\n const wasSkipped = transactionFailed && !isCulprit;\n\n results.push({\n name: data.name,\n table: data.migration.table,\n direction: \"up\",\n success: !transactionFailed,\n error: isCulprit ? errorMessage : undefined,\n durationMs: Math.round(durationMs / migrationsData.length),\n executedAt: new Date(),\n });\n\n if (isCulprit) {\n log.error(\n \"database\",\n \"migration\",\n `${colors.magenta(data.name)}: ✗ Failed: ${errorMessage}`,\n );\n } else if (wasSkipped) {\n log.warn(\n \"database\",\n \"migration\",\n `${colors.magenta(data.name)}: rolled back (batch transaction failed)`,\n );\n } else {\n log.success(\"database\", \"migration\", `Migrated: ${colors.magenta(data.name)} successfully`);\n }\n }\n\n if (transactionFailed) {\n log.error(\n \"database\",\n \"migration\",\n `Batch execution failed. Rollback performed if transactional.`,\n );\n throw new Error(\"Migration batch failed: \" + errorMessage);\n }\n\n const successCount = results.filter((r) => r.success).length;\n log.success(\n \"database\",\n \"migration\",\n `Migration bulk phase execution complete: ${successCount}/${pending.length} migrations processed successfully.`,\n );\n\n return results;\n }\n\n /**\n * Export migrations as phase-ordered SQL files in database/sql/ directory.\n * By default, it exports all registered migrations. Use `pendingOnly: true` to export only pending ones.\n */\n public async exportSQL(options: { pendingOnly?: boolean; compact?: boolean } = {}): Promise<void> {\n if (this.getDataSource().driver.supportsSqlSerialization === false) {\n throw new Error(\n \"SQL export is not supported on this data source — its driver has no SQL dialect. \" +\n \"Migrations on this driver execute native commands through the migration driver instead.\",\n );\n }\n\n const migrationsToExport = options.pendingOnly\n ? await this.getPendingMigrations()\n : this.migrations;\n\n if (migrationsToExport.length === 0) {\n log.warn(\"database\", \"migration\", \"No migrations to export.\");\n return;\n }\n\n log.info(\n \"database\",\n \"migration\",\n `Exporting ${migrationsToExport.length} ${options.pendingOnly ? \"pending \" : \"\"}migration(s) to SQL files...`,\n );\n\n const upStatements: TaggedSQL[] = [];\n const downStatements: TaggedSQL[] = [];\n\n for (const MigrationClass of migrationsToExport) {\n const migration = this.createMigrationInstance(MigrationClass);\n const name = MigrationClass.migrationName;\n\n // Collect up SQL\n await migration.up();\n for (const sql of migration.toSQL()) {\n upStatements.push({\n sql,\n phase: SQLGrammar.phase(sql),\n statementType: SQLGrammar.classify(sql),\n createdAt: MigrationClass.createdAt,\n migrationName: name,\n });\n }\n\n // Collect down SQL (reuse same instance — toSQL() cleared pendingOps)\n await migration.down();\n for (const sql of migration.toSQL()) {\n downStatements.push({\n sql,\n phase: SQLGrammar.phase(sql),\n statementType: SQLGrammar.classify(sql),\n createdAt: MigrationClass.createdAt,\n migrationName: name,\n });\n }\n }\n\n const sortedUp = SQLGrammar.sort(upStatements);\n // Down SQL: reverse order (undo in reverse dependency order)\n const sortedDown = downStatements.reverse();\n\n const upSQLString = this.formatSQLForExport(sortedUp, options.compact);\n const downSQLString = this.formatSQLForExport(sortedDown, options.compact);\n\n const rootPath = process.cwd();\n const sqlDir = path.join(rootPath, \"database\", \"sql\");\n\n if (!fs.existsSync(sqlDir)) {\n fs.mkdirSync(sqlDir, { recursive: true });\n }\n\n const timestamp = new Date().toISOString().replace(/T/, \"_\").replace(/:/g, \"-\").split(\".\")[0];\n const upPath = path.join(sqlDir, `migration_${timestamp}.up.sql`);\n const downPath = path.join(sqlDir, `migration_${timestamp}.down.sql`);\n\n fs.writeFileSync(upPath, upSQLString);\n fs.writeFileSync(downPath, downSQLString);\n\n log.success(\"database\", \"migration\", `Exported to:\\n- ${upPath}\\n- ${downPath}`);\n }\n\n /**\n * Rollback the last batch of migrations.\n *\n * @param options - Execution options\n * @returns Results for each migration\n */\n public async rollbackLast(options: ExecuteOptions = {}): Promise<MigrationResult[]> {\n return this.rollbackBatches(1, options);\n }\n\n /**\n * Rollback N batches of migrations.\n *\n * @param batches - Number of batches to rollback\n * @param options - Execution options\n * @returns Results for each migration\n */\n public async rollbackBatches(\n batches: number,\n options: ExecuteOptions = {},\n ): Promise<MigrationResult[]> {\n const dryRun = options.dryRun ?? false;\n const record = options.record ?? true;\n const results: MigrationResult[] = [];\n\n const toRollback = await this.getMigrationsToRollback(batches);\n\n if (toRollback.length === 0) {\n log.warn(\"database\", \"migration\", \"Nothing to rollback.\");\n return results;\n }\n\n log.info(\"database\", \"migration\", `Rolling back ${toRollback.length} migration(s).`);\n\n for (const MigrationClass of toRollback) {\n const result = await this.runMigration(MigrationClass, \"down\", {\n dryRun,\n record,\n });\n results.push(result);\n\n if (!result.success) {\n break;\n }\n }\n\n const successCount = results.filter((r) => r.success).length;\n log.success(\n \"database\",\n \"migration\",\n `Rollback complete: ${successCount}/${toRollback.length} successful.`,\n );\n\n return results;\n }\n\n /**\n * Rollback all executed migrations.\n *\n * @param options - Execution options\n * @returns Results for each migration\n */\n public async rollbackAll(options: ExecuteOptions = {}): Promise<MigrationResult[]> {\n const executed = await this.getExecutedMigrations();\n if (executed.length === 0) {\n log.warn(\"database\", \"migration\", \"Nothing to rollback.\");\n return [];\n }\n\n const maxBatch = Math.max(...executed.map((r) => r.batch));\n return this.rollbackBatches(maxBatch, options);\n }\n\n /**\n * Reset and re-run: rollback all then run all.\n *\n * @param options - Execution options\n * @returns Combined results\n */\n public async fresh(options: ExecuteOptions = {}): Promise<MigrationResult[]> {\n const rollbackResults = await this.rollbackAll(options);\n const runResults = await this.runAll(options);\n return [...rollbackResults, ...runResults];\n }\n\n // ============================================================================\n // STATUS\n // ============================================================================\n\n /**\n * Get status of all registered migrations.\n */\n public async status(): Promise<\n Array<{\n name: string;\n table: string;\n executed: boolean;\n batch: number | null;\n }>\n > {\n const executed = await this.getExecutedMigrations();\n const executedMap = new Map(executed.map((r) => [r.name, r]));\n\n return this.migrations.map((MigrationClass) => {\n const instance = new MigrationClass();\n const name = MigrationClass.migrationName;\n const record = executedMap.get(name);\n return {\n name,\n table: instance.table,\n executed: !!record,\n batch: record?.batch ?? null,\n };\n });\n }\n\n // ============================================================================\n // EXTENSION PRE-FLIGHT\n // ============================================================================\n\n /**\n * Check whether a database extension is available and inform the developer\n * if it is not installed.\n *\n * Does NOT throw — execution proceeds normally. If the extension is truly\n * missing, the database will surface its own error with full context already\n * displayed to the developer.\n *\n * @example\n * await this.informIfExtensionMissing(\"vector\");\n */\n private async informIfExtensionMissing(extension: string): Promise<void> {\n try {\n const migrationDriver = this.getMigrationDriver();\n const isAvailable = await migrationDriver.isExtensionAvailable(extension);\n\n if (!isAvailable) {\n const hr = \"─\".repeat(60);\n console.log(`\\n${colors.yellow(hr)}`);\n console.log(colors.yellow(` ⚠ Missing Database Extension: ${colors.bold(extension)}`));\n console.log(colors.yellow(hr));\n console.log();\n console.log(` A pending migration requires the ${colors.cyan(extension)} extension,`);\n console.log(` which is not installed on your database server.`);\n console.log();\n console.log(\n ` ${colors.bold(\"This means the physical database server is missing the extension package.\")}`,\n );\n console.log(` You cannot simply run CREATE EXTENSION until the package is installed`);\n console.log(` on the host machine or Docker container.`);\n console.log();\n\n const docsUrl = migrationDriver.getExtensionDocsUrl(extension);\n if (docsUrl) {\n console.log(` ${colors.bold(\"Or follow the installation guide:\")}`);\n console.log(` ${colors.cyan(docsUrl)}`);\n }\n console.log(`\\n${colors.yellow(hr)}\\n`);\n }\n } catch {\n // If the check itself fails, silently skip — don't break the migration.\n }\n }\n\n // ============================================================================\n // PRIVATE HELPERS\n // ============================================================================\n\n /**\n * Run a single migration.\n */\n private async runMigration(\n MigrationClass: MigrationClass,\n direction: \"up\" | \"down\",\n options: {\n dryRun?: boolean;\n record?: boolean;\n batch?: number;\n } = {},\n ): Promise<MigrationResult> {\n const { dryRun = false, record = true } = options;\n const startTime = Date.now();\n let success = true;\n let error: string | undefined;\n\n const migration = new MigrationClass();\n const name = MigrationClass.migrationName;\n\n log.info(\n \"database\",\n \"migration\",\n `${direction === \"up\" ? \"Migrating\" : \"Rolling back\"}: ${colors.magenta(name)}...`,\n );\n\n try {\n if (!dryRun) {\n const driver = this.getMigrationDriver();\n migration.setDriver(driver);\n migration.setMigrationDefaults(this.getDataSource().migrationDefaults);\n\n // ============================================================================\n // TRANSACTION RESOLUTION (3-tier hierarchy)\n // ============================================================================\n // 1. Migration-level explicit override\n // 2. Config-level global override\n // 3. Driver default (PostgreSQL: true, MongoDB: false)\n const shouldUseTransaction =\n migration.transactional ??\n this.getDataSource().migrations?.transactional ??\n driver.getDefaultTransactional();\n\n // ============================================================================\n // EXECUTE WITH OR WITHOUT TRANSACTION\n // ============================================================================\n\n // Collect the requested direction's operations\n if (direction === \"up\") {\n await migration.up();\n } else {\n await migration.down();\n }\n\n const databaseDriver = this.getDataSource().driver;\n\n // SQL-capable drivers serialize the queued operations to SQL strings;\n // drivers without SQL serialization (MongoDB) keep them queued and\n // execute them directly through the migration driver.\n const directExecution = databaseDriver.supportsSqlSerialization === false;\n const sqlStatements = directExecution ? [] : migration.toSQL();\n\n const applyMigration = async (): Promise<void> => {\n if (directExecution) {\n await migration.execute();\n return;\n }\n\n // Execute generated SQL statements sequentially (no phase-sorting here since it's single execution)\n for (const sql of sqlStatements) {\n await databaseDriver.query(sql);\n }\n };\n\n if (shouldUseTransaction && databaseDriver.transaction) {\n // Transactional execution\n await databaseDriver.transaction(async () => {\n await applyMigration();\n\n // Record migration tracking\n if (record) {\n if (direction === \"up\") {\n const batch = options.batch ?? (await this.getNextBatchNumber());\n await this.recordMigration(\n name,\n batch,\n MigrationClass.createdAt ? parseCreatedAt(MigrationClass.createdAt) : new Date(),\n );\n } else {\n await this.removeMigrationRecord(name);\n }\n }\n });\n } else {\n // Non-transactional execution\n await applyMigration();\n\n if (record) {\n if (direction === \"up\") {\n const batch = options.batch ?? (await this.getNextBatchNumber());\n await this.recordMigration(\n name,\n batch,\n MigrationClass.createdAt ? parseCreatedAt(MigrationClass.createdAt) : new Date(),\n );\n } else {\n await this.removeMigrationRecord(name);\n }\n }\n }\n }\n } catch (err) {\n success = false;\n error = err instanceof Error ? err.message : String(err);\n log.error(\"database\", \"migration\", `${colors.magenta(name)}: ✗ Failed: ${error}`);\n throw err;\n }\n\n const durationMs = Date.now() - startTime;\n\n if (success) {\n log.success(\n \"database\",\n \"migration\",\n `${direction == \"up\" ? \"Migrated\" : \"Rolled back\"}: ${colors.magenta(name)} successfully (${durationMs}ms)`,\n );\n }\n\n return {\n name,\n table: migration.table,\n direction,\n success,\n error,\n durationMs,\n executedAt: new Date(),\n };\n }\n\n /**\n * Create, configure, and return a ready-to-use migration instance.\n *\n * Centralises the repeated \"new + setDriver + setMigrationDefaults\" boilerplate\n * that all batch/single execution paths need.\n *\n * @internal\n */\n private createMigrationInstance(MigrationClass: MigrationClass): Migration {\n const migration = new MigrationClass();\n migration.setDriver(this.getMigrationDriver());\n migration.setMigrationDefaults(this.getDataSource().migrationDefaults);\n return migration;\n }\n\n /**\n * Format an ordered array of TaggedSQL into a human-readable SQL file string.\n *\n * Consecutive statements that belong to the same (phase, migration) group share\n * a single block comment at the top, avoiding the noisy per-statement repetition.\n *\n * Example output:\n * ```sql\n * /* Phase 3 [create-users] *\\/\n * ALTER TABLE \"users\" ADD COLUMN \"name\" TEXT NOT NULL;\n * ALTER TABLE \"users\" ADD COLUMN \"email\" TEXT NOT NULL;\n *\n * /* Phase 4 [create-users] *\\/\n * CREATE UNIQUE INDEX ...;\n * ```\n *\n * @internal\n */\n private formatSQLForExport(statements: TaggedSQL[], compact: boolean = false): string {\n const lines: string[] = [];\n\n if (compact) {\n // Just output raw statements, no grouping, no blank lines\n for (const stmt of statements) {\n lines.push(`${stmt.sql};`);\n }\n return lines.join(\"\\n\");\n }\n\n // Group statements by their phase and migration name\n const grouped = new Map<string, string[]>();\n\n for (const stmt of statements) {\n const groupKey = `Phase ${stmt.phase} [${stmt.migrationName}]`;\n if (!grouped.has(groupKey)) {\n grouped.set(groupKey, []);\n }\n grouped.get(groupKey)!.push(stmt.sql);\n }\n\n // Format each group\n for (const [groupKey, sqls] of grouped.entries()) {\n if (lines.length > 0) lines.push(\"\"); // blank line between groups\n lines.push(`/* ${groupKey} */`);\n for (const sql of sqls) {\n lines.push(`${sql};`);\n }\n }\n\n return lines.join(\"\\n\");\n }\n\n /**\n * Get pending (not executed) registered migrations.\n */\n private async getPendingMigrations(): Promise<MigrationClass[]> {\n const executed = await this.getExecutedMigrations();\n const executedNames = new Set(executed.map((r) => r.name));\n const migrations = this.migrations.filter((m) => !executedNames.has(m.migrationName));\n\n return migrations.sort(sortMigrations);\n }\n\n /**\n * Get migrations to rollback.\n */\n private async getMigrationsToRollback(batches: number): Promise<MigrationClass[]> {\n const executed = await this.getExecutedMigrations();\n if (executed.length === 0) return [];\n\n const batchNumbers = [...new Set(executed.map((r) => r.batch))]\n .sort((a, b) => b - a)\n .slice(0, batches);\n\n const toRollback = executed.filter((r) => batchNumbers.includes(r.batch)).reverse();\n\n const migrations = toRollback\n .map((r) => this.migrations.find((m) => m.migrationName === r.name))\n .filter((m): m is MigrationClass => !!m);\n\n return migrations.sort(sortMigrations);\n }\n\n /**\n * Get executed migration records.\n */\n public async getExecutedMigrations(): Promise<MigrationRecord[]> {\n const driver = this.getDataSource().driver;\n\n try {\n const migrationDriver = this.getMigrationDriver();\n\n // Ensure migrations table exists\n await migrationDriver.ensureMigrationsTable(this.migrationsTable);\n\n const queryBuilder = driver.queryBuilder<MigrationRecord>(this.migrationsTable);\n return await queryBuilder.orderBy(\"batch\", \"asc\").orderBy(\"name\", \"asc\").get();\n } catch {\n return [];\n }\n }\n\n /**\n * Record a migration.\n */\n private async recordMigration(name: string, batch: number, createdAt?: Date): Promise<void> {\n const driver = this.getDataSource().driver;\n const migrationDriver = this.getMigrationDriver();\n\n // Ensure migrations table exists\n await migrationDriver.ensureMigrationsTable(this.migrationsTable);\n\n await driver.insert(this.migrationsTable, {\n name,\n batch,\n executedAt: new Date(),\n createdAt,\n });\n }\n\n /**\n * Remove a migration record.\n */\n private async removeMigrationRecord(name: string): Promise<void> {\n const driver = this.getDataSource().driver;\n await driver.delete(this.migrationsTable, { name });\n }\n\n /**\n * Get next batch number.\n */\n private async getNextBatchNumber(): Promise<number> {\n const executed = await this.getExecutedMigrations();\n if (executed.length === 0) return 1;\n return Math.max(...executed.map((r) => r.batch)) + 1;\n }\n}\n\nexport const migrationRunner = new MigrationRunner();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"migration-runner.mjs","names":[],"sources":["../../../../../../../cascade/src/migration/migration-runner.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { log } from \"@warlock.js/logger\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { MigrationDriverContract } from \"../contracts/migration-driver.contract\";\nimport type { DataSource } from \"../data-source/data-source\";\nimport { dataSourceRegistry } from \"../data-source/data-source-registry\";\nimport { type Migration } from \"./migration\";\nimport { sortMigrations, sortMigrationsForRollback } from \"./migration-order\";\nimport { parseCreatedAt } from \"./parse-created-at\";\nimport { SQLGrammar } from \"./sql-grammar\";\nimport type { MigrationRecord, MigrationResult, TaggedSQL } from \"./types\";\n\n/**\n * Migration class type with static name property.\n */\ntype MigrationClass = (new () => Migration) & {\n migrationName: string;\n createdAt?: string;\n};\n\n/**\n * Resolved instance data for a single pending migration.\n * @internal\n */\ntype MigrationData = {\n MigrationClass: MigrationClass;\n migration: Migration;\n name: string;\n};\n\n/**\n * Options for migration execution.\n */\ntype ExecuteOptions = {\n /** Run in dry-run mode (no actual changes) */\n readonly dryRun?: boolean;\n /** Record to migrations table (default: true for batch, false for single) */\n readonly record?: boolean;\n};\n\n/**\n * Migration runner that executes migrations.\n *\n * This is a pure executor - it doesn't discover migrations.\n * Discovery is handled by the framework (e.g., @warlock.js/core CLI).\n *\n * The migration name is read from the static `name` property on the class,\n * which should be set by the CLI after importing:\n *\n * @example\n * ```typescript\n * // In CLI after importing:\n * const { default: MigrationClass } = await import(\"./create-users.migration.ts\");\n * MigrationClass.migrationName ??= \"create-users\";\n *\n * // Then register or execute:\n * runner.register(MigrationClass);\n * // or\n * await runner.execute(MigrationClass);\n * ```\n *\n * @example\n * ```typescript\n * // Simple direct execution\n * await runner.execute(CreateUsersTable);\n * await runner.rollback(CreateUsersTable);\n *\n * // Registry pattern for batch operations\n * runner.register(CreateUsersTable);\n * runner.register(AddEmailIndex);\n * await runner.runAll();\n * await runner.rollbackAll();\n * ```\n */\nexport class MigrationRunner {\n /** Registered migrations */\n public readonly migrations: MigrationClass[] = [];\n\n /** Data source to use */\n private dataSource?: DataSource;\n\n /** Cached migration driver */\n private cachedMigrationDriver?: MigrationDriverContract;\n\n /** Table name for tracking migrations */\n private readonly migrationsTable: string;\n\n /** Whether to log operations */\n private readonly verbose: boolean;\n\n /**\n * Create a new migration runner.\n *\n * @param options - Runner options\n */\n public constructor(\n options: {\n dataSource?: DataSource;\n migrationsTable?: string;\n verbose?: boolean;\n } = {},\n ) {\n this.dataSource = options.dataSource;\n this.migrationsTable = options.migrationsTable ?? \"_migrations\";\n this.verbose = options.verbose ?? true;\n }\n\n // ============================================================================\n // DATA SOURCE\n // ============================================================================\n\n /**\n * Set the data source.\n */\n public setDataSource(dataSource: DataSource): this {\n this.dataSource = dataSource;\n this.cachedMigrationDriver = undefined;\n return this;\n }\n\n /**\n * Get the data source.\n */\n private getDataSource(): DataSource {\n if (!this.dataSource) {\n this.dataSource = dataSourceRegistry.get();\n }\n return this.dataSource;\n }\n\n /**\n * Get the migration driver.\n */\n private getMigrationDriver(): MigrationDriverContract {\n if (!this.cachedMigrationDriver) {\n this.cachedMigrationDriver = this.getDataSource().driver.migrationDriver();\n }\n return this.cachedMigrationDriver;\n }\n\n // ============================================================================\n // REGISTRATION\n // ============================================================================\n\n /**\n * Register a migration.\n *\n * The migration name is read from `MigrationClass.migrationName`.\n *\n * @param MigrationClass - Migration class (must have static `name` set)\n * @param createdAt - Optional timestamp for ordering\n * @returns This runner for chaining\n *\n * @example\n * ```typescript\n * CreateUsersTable.migrationName = \"2024-01-15_create-users\";\n * runner.register(CreateUsersTable);\n * ```\n */\n public register(MigrationClass: MigrationClass): this {\n const name = MigrationClass.migrationName;\n if (!name) {\n throw new Error(\n `Migration class must have a static 'migrationName' property set. ` +\n `Set it in CLI after importing: MigrationClass.migrationName = \"filename\";`,\n );\n }\n // Avoid duplicates\n if (!this.migrations.some((m) => m.migrationName === name)) {\n this.migrations.push(MigrationClass);\n }\n\n return this;\n }\n\n /**\n * Register multiple migrations.\n *\n * @param migrations - Array of migration classes\n * @returns This runner for chaining\n */\n public registerMany(migrations: MigrationClass[]): this {\n for (const MigrationClass of migrations) {\n this.register(MigrationClass);\n }\n return this;\n }\n\n /**\n * Clear all registered migrations.\n */\n public clear(): this {\n this.migrations.length = 0;\n return this;\n }\n\n /**\n * Get all registered migration names.\n */\n public getRegisteredNames(): string[] {\n return this.migrations.map((m) => m.migrationName);\n }\n\n // ============================================================================\n // SINGLE EXECUTION\n // ============================================================================\n\n /**\n * Execute a single migration's up() method.\n *\n * @param MigrationClass - Migration class to execute\n * @param options - Execution options\n * @returns Migration result\n *\n * @example\n * ```typescript\n * await runner.execute(CreateUsersTable);\n * await runner.execute(AddEmailIndex, { dryRun: true });\n * ```\n */\n public async run(\n MigrationClass: MigrationClass,\n options: ExecuteOptions = {},\n ): Promise<MigrationResult> {\n return this.runMigration(MigrationClass, \"up\", {\n dryRun: options.dryRun,\n record: options.record ?? false,\n });\n }\n\n /**\n * Execute a single migration's down() method.\n *\n * @param MigrationClass - Migration class to rollback\n * @param options - Execution options\n * @returns Migration result\n *\n * @example\n * ```typescript\n * await runner.rollback(CreateUsersTable);\n * ```\n */\n public async rollback(\n MigrationClass: MigrationClass,\n options: ExecuteOptions = {},\n ): Promise<MigrationResult> {\n return this.runMigration(MigrationClass, \"down\", {\n dryRun: options.dryRun,\n record: options.record ?? false,\n });\n }\n\n // ============================================================================\n // BATCH EXECUTION (REGISTERED MIGRATIONS)\n // ============================================================================\n\n /**\n * Run all pending registered migrations.\n *\n * Only runs migrations not already in the migrations table.\n *\n * @param options - Execution options\n * @returns Results for each migration\n *\n * @example\n * ```typescript\n * runner.register(CreateUsersTable);\n * runner.register(AddEmailIndex);\n * const results = await runner.runAll();\n * ```\n */\n public async runAll(options: ExecuteOptions = {}): Promise<MigrationResult[]> {\n const { dryRun = false, record = true } = options;\n\n const results: MigrationResult[] = [];\n\n // Get pending migrations\n const pending = await this.getPendingMigrations();\n\n if (pending.length === 0) {\n log.warn(\"database\", \"migration\", \"Nothing to migrate.\");\n return results;\n }\n\n // Drivers without SQL serialization (MongoDB) execute each migration's\n // pending operations directly through the migration driver — there is no\n // SQL pool to collect or phase-sort.\n if (this.getDataSource().driver.supportsSqlSerialization === false) {\n const batch = await this.getNextBatchNumber();\n for (const MigrationClass of pending) {\n const result = await this.runMigration(MigrationClass, \"up\", { dryRun, record, batch });\n results.push(result);\n\n if (!result.success) {\n break;\n }\n }\n return results;\n }\n\n log.info(\n \"database\",\n \"migration\",\n `Found ${pending.length} pending migration(s). Generating SQL pool...`,\n );\n const nextBatch = await this.getNextBatchNumber();\n\n const taggedStatements: TaggedSQL[] = [];\n const migrationsData: MigrationData[] = [];\n\n // 1. Collect SQL from each pending migration.\n // Fire extension checks concurrently as we encounter CREATE EXTENSION\n // statements — they resolve before execution begins.\n const extensionChecks: Promise<void>[] = [];\n\n for (const MigrationClass of pending) {\n const migration = this.createMigrationInstance(MigrationClass);\n const name = MigrationClass.migrationName;\n\n await migration.up();\n const upStatements = migration.toSQL();\n\n migrationsData.push({ MigrationClass, migration, name });\n\n for (const sql of upStatements) {\n const statementType = SQLGrammar.classify(sql);\n\n if (statementType === \"CREATE_EXTENSION\") {\n const ext = SQLGrammar.extractExtensionName(sql);\n if (ext) extensionChecks.push(this.informIfExtensionMissing(ext));\n }\n\n taggedStatements.push({\n sql,\n phase: SQLGrammar.phase(sql),\n statementType,\n createdAt: MigrationClass.createdAt,\n migrationName: name,\n });\n }\n }\n\n // 2. Resolve all extension checks before any SQL is executed.\n // Each check displays a rich message if the extension is missing\n // but does not throw — execution continues and Postgres will\n // surface its own error with full context already shown.\n await Promise.all(extensionChecks);\n\n // 3. Sort all SQL statements globally across all pending migrations\n const sortedStatements = SQLGrammar.sort(taggedStatements);\n\n // 4. Execute in a single batch\n if (dryRun) {\n log.info(\"database\", \"migration\", \"Dry run enabled. Would execute the following statements:\");\n for (const statement of sortedStatements) {\n console.log(\n `-- [${statement.statementType}] Phase ${statement.phase} [${statement.migrationName}]`,\n );\n console.log(statement.sql + \";\\n\");\n }\n return [];\n }\n\n const driver = this.getDataSource().driver;\n\n let transactionFailed = false;\n let errorMessage = \"\";\n /** The migration name that owns the SQL statement that threw. */\n let failingMigrationName: string | undefined;\n\n const startTime = Date.now();\n\n /**\n * Execute all sorted statements, capturing which migration owns the\n * statement that throws — so we report a precise culprit instead of\n * blaming every migration in the batch.\n */\n const executeStatements = async (): Promise<void> => {\n for (const statement of sortedStatements) {\n try {\n await driver.query(statement.sql);\n } catch (err) {\n failingMigrationName = statement.migrationName;\n throw err;\n }\n }\n\n if (record) {\n for (const data of migrationsData) {\n await this.recordMigration(\n data.name,\n nextBatch,\n data.MigrationClass.createdAt\n ? parseCreatedAt(data.MigrationClass.createdAt)\n : new Date(),\n );\n }\n }\n };\n\n try {\n if (driver.transaction) {\n await driver.transaction(executeStatements);\n } else {\n await executeStatements();\n }\n } catch (err) {\n transactionFailed = true;\n errorMessage = err instanceof Error ? err.message : String(err);\n }\n\n const durationMs = Date.now() - startTime;\n\n // Report results per-migration.\n // Only the migration that owns the failing statement is marked as failed;\n // all others are reported as rolled back / not reached.\n for (const data of migrationsData) {\n const isCulprit = transactionFailed && data.name === failingMigrationName;\n const wasSkipped = transactionFailed && !isCulprit;\n\n results.push({\n name: data.name,\n table: data.migration.table,\n direction: \"up\",\n success: !transactionFailed,\n error: isCulprit ? errorMessage : undefined,\n durationMs: Math.round(durationMs / migrationsData.length),\n executedAt: new Date(),\n });\n\n if (isCulprit) {\n log.error(\n \"database\",\n \"migration\",\n `${colors.magenta(data.name)}: ✗ Failed: ${errorMessage}`,\n );\n } else if (wasSkipped) {\n log.warn(\n \"database\",\n \"migration\",\n `${colors.magenta(data.name)}: rolled back (batch transaction failed)`,\n );\n } else {\n log.success(\"database\", \"migration\", `Migrated: ${colors.magenta(data.name)} successfully`);\n }\n }\n\n if (transactionFailed) {\n log.error(\n \"database\",\n \"migration\",\n `Batch execution failed. Rollback performed if transactional.`,\n );\n throw new Error(\"Migration batch failed: \" + errorMessage);\n }\n\n const successCount = results.filter((r) => r.success).length;\n log.success(\n \"database\",\n \"migration\",\n `Migration bulk phase execution complete: ${successCount}/${pending.length} migrations processed successfully.`,\n );\n\n return results;\n }\n\n /**\n * Export migrations as phase-ordered SQL files in database/sql/ directory.\n * By default, it exports all registered migrations. Use `pendingOnly: true` to export only pending ones.\n */\n public async exportSQL(options: { pendingOnly?: boolean; compact?: boolean } = {}): Promise<void> {\n if (this.getDataSource().driver.supportsSqlSerialization === false) {\n throw new Error(\n \"SQL export is not supported on this data source — its driver has no SQL dialect. \" +\n \"Migrations on this driver execute native commands through the migration driver instead.\",\n );\n }\n\n const migrationsToExport = options.pendingOnly\n ? await this.getPendingMigrations()\n : this.migrations;\n\n if (migrationsToExport.length === 0) {\n log.warn(\"database\", \"migration\", \"No migrations to export.\");\n return;\n }\n\n log.info(\n \"database\",\n \"migration\",\n `Exporting ${migrationsToExport.length} ${options.pendingOnly ? \"pending \" : \"\"}migration(s) to SQL files...`,\n );\n\n const upStatements: TaggedSQL[] = [];\n const downStatements: TaggedSQL[] = [];\n\n for (const MigrationClass of migrationsToExport) {\n const migration = this.createMigrationInstance(MigrationClass);\n const name = MigrationClass.migrationName;\n\n // Collect up SQL\n await migration.up();\n for (const sql of migration.toSQL()) {\n upStatements.push({\n sql,\n phase: SQLGrammar.phase(sql),\n statementType: SQLGrammar.classify(sql),\n createdAt: MigrationClass.createdAt,\n migrationName: name,\n });\n }\n\n // Collect down SQL (reuse same instance — toSQL() cleared pendingOps)\n await migration.down();\n for (const sql of migration.toSQL()) {\n downStatements.push({\n sql,\n phase: SQLGrammar.phase(sql),\n statementType: SQLGrammar.classify(sql),\n createdAt: MigrationClass.createdAt,\n migrationName: name,\n });\n }\n }\n\n const sortedUp = SQLGrammar.sort(upStatements);\n // Down SQL: reverse order (undo in reverse dependency order)\n const sortedDown = downStatements.reverse();\n\n const upSQLString = this.formatSQLForExport(sortedUp, options.compact);\n const downSQLString = this.formatSQLForExport(sortedDown, options.compact);\n\n const rootPath = process.cwd();\n const sqlDir = path.join(rootPath, \"database\", \"sql\");\n\n if (!fs.existsSync(sqlDir)) {\n fs.mkdirSync(sqlDir, { recursive: true });\n }\n\n const timestamp = new Date().toISOString().replace(/T/, \"_\").replace(/:/g, \"-\").split(\".\")[0];\n const upPath = path.join(sqlDir, `migration_${timestamp}.up.sql`);\n const downPath = path.join(sqlDir, `migration_${timestamp}.down.sql`);\n\n fs.writeFileSync(upPath, upSQLString);\n fs.writeFileSync(downPath, downSQLString);\n\n log.success(\"database\", \"migration\", `Exported to:\\n- ${upPath}\\n- ${downPath}`);\n }\n\n /**\n * Rollback the last batch of migrations.\n *\n * @param options - Execution options\n * @returns Results for each migration\n */\n public async rollbackLast(options: ExecuteOptions = {}): Promise<MigrationResult[]> {\n return this.rollbackBatches(1, options);\n }\n\n /**\n * Rollback N batches of migrations.\n *\n * @param batches - Number of batches to rollback\n * @param options - Execution options\n * @returns Results for each migration\n */\n public async rollbackBatches(\n batches: number,\n options: ExecuteOptions = {},\n ): Promise<MigrationResult[]> {\n const dryRun = options.dryRun ?? false;\n const record = options.record ?? true;\n const results: MigrationResult[] = [];\n\n const toRollback = await this.getMigrationsToRollback(batches);\n\n if (toRollback.length === 0) {\n log.warn(\"database\", \"migration\", \"Nothing to rollback.\");\n return results;\n }\n\n log.info(\"database\", \"migration\", `Rolling back ${toRollback.length} migration(s).`);\n\n for (const MigrationClass of toRollback) {\n const result = await this.runMigration(MigrationClass, \"down\", {\n dryRun,\n record,\n });\n results.push(result);\n\n if (!result.success) {\n break;\n }\n }\n\n const successCount = results.filter((r) => r.success).length;\n log.success(\n \"database\",\n \"migration\",\n `Rollback complete: ${successCount}/${toRollback.length} successful.`,\n );\n\n return results;\n }\n\n /**\n * Rollback all executed migrations.\n *\n * @param options - Execution options\n * @returns Results for each migration\n */\n public async rollbackAll(options: ExecuteOptions = {}): Promise<MigrationResult[]> {\n const executed = await this.getExecutedMigrations();\n if (executed.length === 0) {\n log.warn(\"database\", \"migration\", \"Nothing to rollback.\");\n return [];\n }\n\n const maxBatch = Math.max(...executed.map((r) => r.batch));\n return this.rollbackBatches(maxBatch, options);\n }\n\n /**\n * Reset and re-run: rollback all then run all.\n *\n * @param options - Execution options\n * @returns Combined results\n */\n public async fresh(options: ExecuteOptions = {}): Promise<MigrationResult[]> {\n const rollbackResults = await this.rollbackAll(options);\n const runResults = await this.runAll(options);\n return [...rollbackResults, ...runResults];\n }\n\n // ============================================================================\n // STATUS\n // ============================================================================\n\n /**\n * Get status of all registered migrations.\n */\n public async status(): Promise<\n Array<{\n name: string;\n table: string;\n executed: boolean;\n batch: number | null;\n }>\n > {\n const executed = await this.getExecutedMigrations();\n const executedMap = new Map(executed.map((r) => [r.name, r]));\n\n return this.migrations.map((MigrationClass) => {\n const instance = new MigrationClass();\n const name = MigrationClass.migrationName;\n const record = executedMap.get(name);\n return {\n name,\n table: instance.table,\n executed: !!record,\n batch: record?.batch ?? null,\n };\n });\n }\n\n // ============================================================================\n // EXTENSION PRE-FLIGHT\n // ============================================================================\n\n /**\n * Check whether a database extension is available and inform the developer\n * if it is not installed.\n *\n * Does NOT throw — execution proceeds normally. If the extension is truly\n * missing, the database will surface its own error with full context already\n * displayed to the developer.\n *\n * @example\n * await this.informIfExtensionMissing(\"vector\");\n */\n private async informIfExtensionMissing(extension: string): Promise<void> {\n try {\n const migrationDriver = this.getMigrationDriver();\n const isAvailable = await migrationDriver.isExtensionAvailable(extension);\n\n if (!isAvailable) {\n const hr = \"─\".repeat(60);\n console.log(`\\n${colors.yellow(hr)}`);\n console.log(colors.yellow(` ⚠ Missing Database Extension: ${colors.bold(extension)}`));\n console.log(colors.yellow(hr));\n console.log();\n console.log(` A pending migration requires the ${colors.cyan(extension)} extension,`);\n console.log(` which is not installed on your database server.`);\n console.log();\n console.log(\n ` ${colors.bold(\"This means the physical database server is missing the extension package.\")}`,\n );\n console.log(` You cannot simply run CREATE EXTENSION until the package is installed`);\n console.log(` on the host machine or Docker container.`);\n console.log();\n\n const docsUrl = migrationDriver.getExtensionDocsUrl(extension);\n if (docsUrl) {\n console.log(` ${colors.bold(\"Or follow the installation guide:\")}`);\n console.log(` ${colors.cyan(docsUrl)}`);\n }\n console.log(`\\n${colors.yellow(hr)}\\n`);\n }\n } catch {\n // If the check itself fails, silently skip — don't break the migration.\n }\n }\n\n // ============================================================================\n // PRIVATE HELPERS\n // ============================================================================\n\n /**\n * Run a single migration.\n */\n private async runMigration(\n MigrationClass: MigrationClass,\n direction: \"up\" | \"down\",\n options: {\n dryRun?: boolean;\n record?: boolean;\n batch?: number;\n } = {},\n ): Promise<MigrationResult> {\n const { dryRun = false, record = true } = options;\n const startTime = Date.now();\n let success = true;\n let error: string | undefined;\n\n const migration = new MigrationClass();\n const name = MigrationClass.migrationName;\n\n log.info(\n \"database\",\n \"migration\",\n `${direction === \"up\" ? \"Migrating\" : \"Rolling back\"}: ${colors.magenta(name)}...`,\n );\n\n try {\n if (!dryRun) {\n const driver = this.getMigrationDriver();\n migration.setDriver(driver);\n migration.setMigrationDefaults(this.getDataSource().migrationDefaults);\n\n // ============================================================================\n // TRANSACTION RESOLUTION (3-tier hierarchy)\n // ============================================================================\n // 1. Migration-level explicit override\n // 2. Config-level global override\n // 3. Driver default (PostgreSQL: true, MongoDB: false)\n const shouldUseTransaction =\n migration.transactional ??\n this.getDataSource().migrations?.transactional ??\n driver.getDefaultTransactional();\n\n // ============================================================================\n // EXECUTE WITH OR WITHOUT TRANSACTION\n // ============================================================================\n\n // Collect the requested direction's operations\n if (direction === \"up\") {\n await migration.up();\n } else {\n await migration.down();\n }\n\n const databaseDriver = this.getDataSource().driver;\n\n // SQL-capable drivers serialize the queued operations to SQL strings;\n // drivers without SQL serialization (MongoDB) keep them queued and\n // execute them directly through the migration driver.\n const directExecution = databaseDriver.supportsSqlSerialization === false;\n const sqlStatements = directExecution ? [] : migration.toSQL();\n\n const applyMigration = async (): Promise<void> => {\n if (directExecution) {\n await migration.execute();\n return;\n }\n\n // Execute generated SQL statements sequentially (no phase-sorting here since it's single execution)\n for (const sql of sqlStatements) {\n await databaseDriver.query(sql);\n }\n };\n\n if (shouldUseTransaction && databaseDriver.transaction) {\n // Transactional execution\n await databaseDriver.transaction(async () => {\n await applyMigration();\n\n // Record migration tracking\n if (record) {\n if (direction === \"up\") {\n const batch = options.batch ?? (await this.getNextBatchNumber());\n await this.recordMigration(\n name,\n batch,\n MigrationClass.createdAt ? parseCreatedAt(MigrationClass.createdAt) : new Date(),\n );\n } else {\n await this.removeMigrationRecord(name);\n }\n }\n });\n } else {\n // Non-transactional execution\n await applyMigration();\n\n if (record) {\n if (direction === \"up\") {\n const batch = options.batch ?? (await this.getNextBatchNumber());\n await this.recordMigration(\n name,\n batch,\n MigrationClass.createdAt ? parseCreatedAt(MigrationClass.createdAt) : new Date(),\n );\n } else {\n await this.removeMigrationRecord(name);\n }\n }\n }\n }\n } catch (err) {\n success = false;\n error = err instanceof Error ? err.message : String(err);\n log.error(\"database\", \"migration\", `${colors.magenta(name)}: ✗ Failed: ${error}`);\n throw err;\n }\n\n const durationMs = Date.now() - startTime;\n\n if (success) {\n log.success(\n \"database\",\n \"migration\",\n `${direction == \"up\" ? \"Migrated\" : \"Rolled back\"}: ${colors.magenta(name)} successfully (${durationMs}ms)`,\n );\n }\n\n return {\n name,\n table: migration.table,\n direction,\n success,\n error,\n durationMs,\n executedAt: new Date(),\n };\n }\n\n /**\n * Create, configure, and return a ready-to-use migration instance.\n *\n * Centralises the repeated \"new + setDriver + setMigrationDefaults\" boilerplate\n * that all batch/single execution paths need.\n *\n * @internal\n */\n private createMigrationInstance(MigrationClass: MigrationClass): Migration {\n const migration = new MigrationClass();\n migration.setDriver(this.getMigrationDriver());\n migration.setMigrationDefaults(this.getDataSource().migrationDefaults);\n return migration;\n }\n\n /**\n * Format an ordered array of TaggedSQL into a human-readable SQL file string.\n *\n * Consecutive statements that belong to the same (phase, migration) group share\n * a single block comment at the top, avoiding the noisy per-statement repetition.\n *\n * Example output:\n * ```sql\n * /* Phase 3 [create-users] *\\/\n * ALTER TABLE \"users\" ADD COLUMN \"name\" TEXT NOT NULL;\n * ALTER TABLE \"users\" ADD COLUMN \"email\" TEXT NOT NULL;\n *\n * /* Phase 4 [create-users] *\\/\n * CREATE UNIQUE INDEX ...;\n * ```\n *\n * @internal\n */\n private formatSQLForExport(statements: TaggedSQL[], compact: boolean = false): string {\n const lines: string[] = [];\n\n if (compact) {\n // Just output raw statements, no grouping, no blank lines\n for (const stmt of statements) {\n lines.push(`${stmt.sql};`);\n }\n return lines.join(\"\\n\");\n }\n\n // Group statements by their phase and migration name\n const grouped = new Map<string, string[]>();\n\n for (const stmt of statements) {\n const groupKey = `Phase ${stmt.phase} [${stmt.migrationName}]`;\n if (!grouped.has(groupKey)) {\n grouped.set(groupKey, []);\n }\n grouped.get(groupKey)!.push(stmt.sql);\n }\n\n // Format each group\n for (const [groupKey, sqls] of grouped.entries()) {\n if (lines.length > 0) lines.push(\"\"); // blank line between groups\n lines.push(`/* ${groupKey} */`);\n for (const sql of sqls) {\n lines.push(`${sql};`);\n }\n }\n\n return lines.join(\"\\n\");\n }\n\n /**\n * Get pending (not executed) registered migrations.\n */\n private async getPendingMigrations(): Promise<MigrationClass[]> {\n const executed = await this.getExecutedMigrations();\n const executedNames = new Set(executed.map((r) => r.name));\n const migrations = this.migrations.filter((m) => !executedNames.has(m.migrationName));\n\n return migrations.sort(sortMigrations);\n }\n\n /**\n * Get migrations to rollback, newest-first.\n *\n * A rollback must undo migrations in the exact inverse of the order `up`\n * applied them, or a `down()` will hit schema its predecessor already\n * removed — dropping a table before dropping the column added to it, say.\n *\n * The sort has to be explicitly **descending**. Reversing the executed list\n * is not enough: `getExecutedMigrations` orders by `batch, name`, so the\n * input is alphabetical rather than chronological, and reversing it only\n * yields reverse-alphabetical order. (Sorting *ascending* here — which is\n * what this method used to do after a `.reverse()` — silently restored the\n * forward `up` order and made the reverse dead code.)\n */\n private async getMigrationsToRollback(batches: number): Promise<MigrationClass[]> {\n const executed = await this.getExecutedMigrations();\n if (executed.length === 0) return [];\n\n const batchNumbers = [...new Set(executed.map((r) => r.batch))]\n .sort((a, b) => b - a)\n .slice(0, batches);\n\n const migrations = executed\n .filter((r) => batchNumbers.includes(r.batch))\n .map((r) => this.migrations.find((m) => m.migrationName === r.name))\n .filter((m): m is MigrationClass => !!m);\n\n return migrations.sort(sortMigrationsForRollback);\n }\n\n /**\n * Get executed migration records.\n */\n public async getExecutedMigrations(): Promise<MigrationRecord[]> {\n const driver = this.getDataSource().driver;\n\n try {\n const migrationDriver = this.getMigrationDriver();\n\n // Ensure migrations table exists\n await migrationDriver.ensureMigrationsTable(this.migrationsTable);\n\n const queryBuilder = driver.queryBuilder<MigrationRecord>(this.migrationsTable);\n return await queryBuilder.orderBy(\"batch\", \"asc\").orderBy(\"name\", \"asc\").get();\n } catch {\n return [];\n }\n }\n\n /**\n * Record a migration.\n */\n private async recordMigration(name: string, batch: number, createdAt?: Date): Promise<void> {\n const driver = this.getDataSource().driver;\n const migrationDriver = this.getMigrationDriver();\n\n // Ensure migrations table exists\n await migrationDriver.ensureMigrationsTable(this.migrationsTable);\n\n await driver.insert(this.migrationsTable, {\n name,\n batch,\n executedAt: new Date(),\n createdAt,\n });\n }\n\n /**\n * Remove a migration record.\n */\n private async removeMigrationRecord(name: string): Promise<void> {\n const driver = this.getDataSource().driver;\n await driver.delete(this.migrationsTable, { name });\n }\n\n /**\n * Get next batch number.\n */\n private async getNextBatchNumber(): Promise<number> {\n const executed = await this.getExecutedMigrations();\n if (executed.length === 0) return 1;\n return Math.max(...executed.map((r) => r.batch)) + 1;\n }\n}\n\nexport const migrationRunner = new MigrationRunner();\n"],"mappings":"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"}
|
package/llms-full.txt
CHANGED
|
@@ -2556,6 +2556,24 @@ for (const [field, { oldValue, newValue }] of Object.entries(dirty)) {
|
|
|
2556
2556
|
}
|
|
2557
2557
|
```
|
|
2558
2558
|
|
|
2559
|
+
## How a merge decides what changed
|
|
2560
|
+
|
|
2561
|
+
`.merge()` deep-merges **plain objects only**. Every other value — primitives, arrays, and class instances such as `Date`, `Map`, `Set`, `RegExp` — **replaces** the target outright.
|
|
2562
|
+
|
|
2563
|
+
```ts
|
|
2564
|
+
// plain object → merged key-by-key, siblings survive
|
|
2565
|
+
user.merge({ profile: { age: 26 } }); // profile.city untouched
|
|
2566
|
+
user.getDirtyColumns(); // ["profile.age"]
|
|
2567
|
+
|
|
2568
|
+
// Date → replaced, not merged
|
|
2569
|
+
conversation.merge({ lastOutboundAt: new Date() });
|
|
2570
|
+
conversation.getDirtyColumns(); // ["lastOutboundAt"]
|
|
2571
|
+
```
|
|
2572
|
+
|
|
2573
|
+
:::note[Fixed in 4.9.1]
|
|
2574
|
+
Before 4.9.1 a `Date` merged over a column that **already held a `Date`** was silently dropped: the merge treated it as a mergeable container and recursed into it, and a `Date` has no own enumerable properties, so nothing was copied. The column never went dirty and `save()` returned `{ success: true, modifiedCount: 0 }` with no `UPDATE` issued. Writing into an empty column always worked, so only overwrites were affected. If you are on 4.9.0 or earlier, upgrade.
|
|
2575
|
+
:::
|
|
2576
|
+
|
|
2559
2577
|
## Things NOT to do
|
|
2560
2578
|
|
|
2561
2579
|
- Don't use `hasChanges()` / `isDirty()` after `save()` to verify the save persisted. The tracker resets to clean on save — these become false regardless. Read back from the DB if you need verification.
|
package/package.json
CHANGED
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"@mongez/events": "^2.2.6",
|
|
28
28
|
"@mongez/reinforcements": "^3.3.0",
|
|
29
29
|
"@mongez/supportive-is": "^2.1.3",
|
|
30
|
-
"@warlock.js/context": "4.9.
|
|
31
|
-
"@warlock.js/logger": "4.9.
|
|
32
|
-
"@warlock.js/seal": "4.9.
|
|
30
|
+
"@warlock.js/context": "4.9.2",
|
|
31
|
+
"@warlock.js/logger": "4.9.2",
|
|
32
|
+
"@warlock.js/seal": "4.9.2",
|
|
33
33
|
"citty": "^0.2.2",
|
|
34
34
|
"fast-glob": "^3.3.3"
|
|
35
35
|
},
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"bin": {
|
|
41
41
|
"cascade": "bin/cascade.js"
|
|
42
42
|
},
|
|
43
|
-
"version": "4.9.
|
|
43
|
+
"version": "4.9.2",
|
|
44
44
|
"main": "./cjs/index.cjs",
|
|
45
45
|
"module": "./esm/index.mjs",
|
|
46
46
|
"types": "./esm/index.d.mts",
|
|
@@ -97,6 +97,24 @@ for (const [field, { oldValue, newValue }] of Object.entries(dirty)) {
|
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
## How a merge decides what changed
|
|
101
|
+
|
|
102
|
+
`.merge()` deep-merges **plain objects only**. Every other value — primitives, arrays, and class instances such as `Date`, `Map`, `Set`, `RegExp` — **replaces** the target outright.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// plain object → merged key-by-key, siblings survive
|
|
106
|
+
user.merge({ profile: { age: 26 } }); // profile.city untouched
|
|
107
|
+
user.getDirtyColumns(); // ["profile.age"]
|
|
108
|
+
|
|
109
|
+
// Date → replaced, not merged
|
|
110
|
+
conversation.merge({ lastOutboundAt: new Date() });
|
|
111
|
+
conversation.getDirtyColumns(); // ["lastOutboundAt"]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
:::note[Fixed in 4.9.1]
|
|
115
|
+
Before 4.9.1 a `Date` merged over a column that **already held a `Date`** was silently dropped: the merge treated it as a mergeable container and recursed into it, and a `Date` has no own enumerable properties, so nothing was copied. The column never went dirty and `save()` returned `{ success: true, modifiedCount: 0 }` with no `UPDATE` issued. Writing into an empty column always worked, so only overwrites were affected. If you are on 4.9.0 or earlier, upgrade.
|
|
116
|
+
:::
|
|
117
|
+
|
|
100
118
|
## Things NOT to do
|
|
101
119
|
|
|
102
120
|
- Don't use `hasChanges()` / `isDirty()` after `save()` to verify the save persisted. The tracker resets to clean on save — these become false regardless. Read back from the DB if you need verification.
|