@pipelab/migration 1.0.0-beta.0 → 1.0.0-beta.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/.oxfmtrc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "ignorePatterns": ["dist", "out", "node_modules", "bin"]
3
+ }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @pipelab/migration
2
2
 
3
+ ## 1.0.0-beta.2
4
+
5
+ ### Patch Changes
6
+
7
+ - update
8
+
9
+ ## 1.0.0-beta.1
10
+
11
+ ### Patch Changes
12
+
13
+ - general improvements
14
+
3
15
  ## 1.0.0-beta.0
4
16
 
5
17
  ### Major Changes
package/dist/index.d.mts CHANGED
@@ -7,6 +7,7 @@ type MigrationFn<From, To> = (state: From, targetVersion: string) => Awaitable<T
7
7
  type MigrateOptions = {
8
8
  debug?: boolean;
9
9
  target?: SemVer;
10
+ onStep?: (state: any, version: SemVer) => Promise<void> | void;
10
11
  };
11
12
  declare const MigrationSchemaValidator: valibot.ObjectSchema<{
12
13
  readonly version: valibot.CustomSchema<`${number}.${number}.${number}`, undefined>;
@@ -15,24 +16,22 @@ declare const createVersionSchema: <OBJECTENTRIES extends ObjectEntries>(schema:
15
16
  type MigrationSchema = InferOutput<typeof MigrationSchemaValidator>;
16
17
  type OmitVersion<T> = Omit<T, keyof MigrationSchema>;
17
18
  type SemVer = `${number}.${number}.${number}`;
18
- interface MigrationClass<Down, Current, Up> {
19
+ interface MigrationClass<Current, Up> {
19
20
  version: SemVer;
20
21
  up: MigrationFn<Current, Up>;
21
- down: MigrationFn<Current, Down>;
22
22
  }
23
- interface MigrationObjInput<Down, Current, Up> {
23
+ interface MigrationObjInput<Current, Up> {
24
24
  version: SemVer;
25
25
  up: MigrationFn<OmitVersion<Current>, OmitVersion<Up>>;
26
- down: MigrationFn<OmitVersion<Current>, OmitVersion<Down>>;
27
26
  }
28
27
  interface MigratorConfig<InitialState, FinalState> {
29
- migrations: MigrationClass<any, any, any>[];
28
+ migrations: MigrationClass<any, any>[];
30
29
  coerce?: boolean;
31
30
  defaultValue?: FinalState;
32
31
  }
33
32
  declare class Migrator<InitialState extends MigrationSchema, OutputState extends MigrationSchema> {
34
33
  current: SemVer;
35
- migrations: Record<SemVer, MigrationClass<any, any, any>>;
34
+ migrations: Record<SemVer, MigrationClass<any, any>>;
36
35
  coerce: boolean;
37
36
  defaultValue: OutputState;
38
37
  constructor(config: MigratorConfig<InitialState, OutputState>);
@@ -43,17 +42,8 @@ declare class Migrator<InitialState extends MigrationSchema, OutputState extends
43
42
  }
44
43
  //#endregion
45
44
  //#region src/models/createMigration.d.ts
46
- declare function createMigration<Down extends MigrationSchema, Current extends MigrationSchema, Up extends MigrationSchema>(migration: MigrationObjInput<Down, Current, Up>): MigrationClass<Down, Current, Up>;
47
- declare const initialVersion: () => never;
45
+ declare function createMigration<Current extends MigrationSchema, Up extends MigrationSchema>(migration: MigrationObjInput<Current, Up>): MigrationClass<Current, Up>;
48
46
  declare const finalVersion: () => never;
49
- /**
50
- * @deprecated
51
- * @param version
52
- * @param migration
53
- * @returns
54
- */
55
- declare function initial<Current extends MigrationSchema, Up extends MigrationSchema>(version: SemVer, migration: MigrationFn<OmitVersion<Current>, OmitVersion<Up>>): MigrationClass<Current, Current, Up>;
56
- declare function final<Current extends MigrationSchema, Down extends MigrationSchema>(version: SemVer, migration: MigrationFn<OmitVersion<Current>, OmitVersion<Down>>): MigrationClass<Down, Current, Current>;
57
47
  //#endregion
58
48
  //#region src/models/createMigrator.d.ts
59
49
  declare const createMigrator: <InitialState extends MigrationSchema, FinalState extends MigrationSchema>() => {
@@ -62,5 +52,5 @@ declare const createMigrator: <InitialState extends MigrationSchema, FinalState
62
52
  };
63
53
  type MigratorFactory<InitialState extends MigrationSchema, FinalState extends MigrationSchema> = ReturnType<typeof createMigrator<InitialState, FinalState>>;
64
54
  //#endregion
65
- export { type MigrationFn, type MigrationSchema, type Migrator, type MigratorConfig, MigratorFactory, type OmitVersion, type SemVer, createMigration, createMigrator, createVersionSchema, final, finalVersion, initial, initialVersion };
55
+ export { type Awaitable, type MigrationFn, type MigrationSchema, type Migrator, type MigratorConfig, MigratorFactory, type OmitVersion, type SemVer, createMigration, createMigrator, createVersionSchema, finalVersion };
66
56
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/models/migration.ts","../src/models/createMigration.ts","../src/models/createMigrator.ts"],"mappings":";;;;KAMY,SAAA,MAAe,OAAA,CAAQ,CAAA,IAAK,CAAA;AAAA,KAE5B,WAAA,cAAyB,KAAA,EAAO,IAAA,EAAM,aAAA,aAA0B,SAAA,CAAU,EAAA;AAAA,KAE1E,cAAA;EACV,KAAA;EACA,MAAA,GAAS,MAAA;AAAA;AAAA,cAUE,wBAAA,EAEX,OAAA,CAFmC,YAAA;EAAA,kBAEnC,OAAA,CAAA,YAAA;AAAA;AAAA,cAEW,mBAAA,yBAA6C,aAAA,EAAe,MAAA,EAAQ,aAAA,KAAa,OAAA,CAAA,YAAA,CAAA,aAAA;AAAA,KAKlF,eAAA,GAAkB,WAAA,QAAmB,wBAAA;AAAA,KAErC,WAAA,MAAiB,IAAA,CAAK,CAAA,QAAS,eAAA;AAAA,KAE/B,MAAA;AAAA,UAEK,cAAA;EACf,OAAA,EAAS,MAAA;EACT,EAAA,EAAI,WAAA,CAAY,OAAA,EAAS,EAAA;EACzB,IAAA,EAAM,WAAA,CAAY,OAAA,EAAS,IAAA;AAAA;AAAA,UAGZ,iBAAA;EACf,OAAA,EAAS,MAAA;EACT,EAAA,EAAI,WAAA,CAAY,WAAA,CAAY,OAAA,GAAU,WAAA,CAAY,EAAA;EAClD,IAAA,EAAM,WAAA,CAAY,WAAA,CAAY,OAAA,GAAU,WAAA,CAAY,IAAA;AAAA;AAAA,UAGrC,cAAA;EACf,UAAA,EAAY,cAAA;EACZ,MAAA;EACA,YAAA,GAAe,UAAA;AAAA;AAAA,cAGJ,QAAA,sBAA8B,eAAA,sBAAqC,eAAA;EAC9E,OAAA,EAAS,MAAA;EAET,UAAA,EAAY,MAAA,CAAO,MAAA,EAAQ,cAAA;EAE3B,MAAA;EACA,YAAA,EAAc,WAAA;cAEF,MAAA,EAAQ,cAAA,CAAe,YAAA,EAAc,WAAA;EAYjD,WAAA,CAAA;EAKM,OAAA,CACJ,MAAA,EAAQ,eAAA,cACR,OAAA,GAAU,cAAA,GACT,OAAA,CAAQ,WAAA;EAiEX,SAAA,CAAU,OAAA,EAAS,MAAA,GAAS,MAAA;EAI5B,aAAA,CAAc,OAAA,EAAS,MAAA;AAAA;;;iBC/IT,eAAA,cACD,eAAA,kBACG,eAAA,aACL,eAAA,CAAA,CACX,SAAA,EAAW,iBAAA,CAAkB,IAAA,EAAM,OAAA,EAAS,EAAA,IAAM,cAAA,CAAe,IAAA,EAAM,OAAA,EAAS,EAAA;AAAA,cAgBrE,cAAA;AAAA,cAIA,YAAA;AD3Bb;;;;;;AAAA,iBCyCgB,OAAA,iBAAwB,eAAA,aAA4B,eAAA,CAAA,CAClE,OAAA,EAAS,MAAA,EACT,SAAA,EAAW,WAAA,CAAY,WAAA,CAAY,OAAA,GAAU,WAAA,CAAY,EAAA,KACxD,cAAA,CAAe,OAAA,EAAS,OAAA,EAAS,EAAA;AAAA,iBAUpB,KAAA,iBAAsB,eAAA,eAA8B,eAAA,CAAA,CAClE,OAAA,EAAS,MAAA,EACT,SAAA,EAAW,WAAA,CAAY,WAAA,CAAY,OAAA,GAAU,WAAA,CAAY,IAAA,KACxD,cAAA,CAAe,IAAA,EAAM,OAAA,EAAS,OAAA;;;cC7DpB,cAAA,wBACU,eAAA,qBACF,eAAA;8BAGW,UAAA,GAAa,UAAA;2BAI/B,cAAA,CAAe,YAAA,EAAc,UAAA,IACpC,QAAA,CAAS,YAAA,EAAc,UAAA;AAAA;AAAA,KAMlB,eAAA,sBACW,eAAA,qBACF,eAAA,IACjB,UAAA,QAAkB,cAAA,CAAe,YAAA,EAAc,UAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/models/migration.ts","../src/models/createMigration.ts","../src/models/createMigrator.ts"],"mappings":";;;;KAMY,SAAA,MAAe,OAAA,CAAQ,CAAA,IAAK,CAAA;AAAA,KAE5B,WAAA,cAAyB,KAAA,EAAO,IAAA,EAAM,aAAA,aAA0B,SAAA,CAAU,EAAA;AAAA,KAE1E,cAAA;EACV,KAAA;EACA,MAAA,GAAS,MAAA;EACT,MAAA,IAAU,KAAA,OAAY,OAAA,EAAS,MAAA,KAAW,OAAA;AAAA;AAAA,cAU/B,wBAAA,EAEX,OAAA,CAFmC,YAAA;EAAA,kBAEnC,OAAA,CAAA,YAAA;AAAA;AAAA,cAEW,mBAAA,yBAA6C,aAAA,EAAe,MAAA,EAAQ,aAAA,KAAa,OAAA,CAAA,YAAA,CAAA,aAAA;AAAA,KAKlF,eAAA,GAAkB,WAAA,QAAmB,wBAAA;AAAA,KAErC,WAAA,MAAiB,IAAA,CAAK,CAAA,QAAS,eAAA;AAAA,KAE/B,MAAA;AAAA,UAEK,cAAA;EACf,OAAA,EAAS,MAAA;EACT,EAAA,EAAI,WAAA,CAAY,OAAA,EAAS,EAAA;AAAA;AAAA,UAGV,iBAAA;EACf,OAAA,EAAS,MAAA;EACT,EAAA,EAAI,WAAA,CAAY,WAAA,CAAY,OAAA,GAAU,WAAA,CAAY,EAAA;AAAA;AAAA,UAGnC,cAAA;EACf,UAAA,EAAY,cAAA;EACZ,MAAA;EACA,YAAA,GAAe,UAAA;AAAA;AAAA,cAGJ,QAAA,sBAA8B,eAAA,sBAAqC,eAAA;EAC9E,OAAA,EAAS,MAAA;EAET,UAAA,EAAY,MAAA,CAAO,MAAA,EAAQ,cAAA;EAE3B,MAAA;EACA,YAAA,EAAc,WAAA;cAEF,MAAA,EAAQ,cAAA,CAAe,YAAA,EAAc,WAAA;EAYjD,WAAA,CAAA;EAKM,OAAA,CACJ,MAAA,EAAQ,eAAA,cACR,OAAA,GAAU,cAAA,GACT,OAAA,CAAQ,WAAA;EAqEX,SAAA,CAAU,OAAA,EAAS,MAAA,GAAS,MAAA;EAI5B,aAAA,CAAc,OAAA,EAAS,MAAA;AAAA;;;iBClJT,eAAA,iBACE,eAAA,aACL,eAAA,CAAA,CACX,SAAA,EAAW,iBAAA,CAAkB,OAAA,EAAS,EAAA,IAAM,cAAA,CAAe,OAAA,EAAS,EAAA;AAAA,cAazD,YAAA;;;cCvBA,cAAA,wBACU,eAAA,qBACF,eAAA;8BAGW,UAAA,GAAa,UAAA;2BAI/B,cAAA,CAAe,YAAA,EAAc,UAAA,IACpC,QAAA,CAAS,YAAA,EAAc,UAAA;AAAA;AAAA,KAMlB,eAAA,sBACW,eAAA,qBACF,eAAA,IACjB,UAAA,QAAkB,cAAA,CAAe,YAAA,EAAc,UAAA"}
package/dist/index.mjs CHANGED
@@ -7,47 +7,15 @@ function createMigration(migration) {
7
7
  return {
8
8
  version: migration.version,
9
9
  up: async (state, nextVersion) => {
10
- const newState = await migration.up(state, nextVersion);
11
- newState.version = nextVersion;
12
- return newState;
13
- },
14
- down: async (state, nextVersion) => {
15
- const newState = await migration.down(state, nextVersion);
10
+ const newState = migration.up ? await migration.up(state, nextVersion) : state;
16
11
  newState.version = nextVersion;
17
12
  return newState;
18
13
  }
19
14
  };
20
15
  }
21
- const initialVersion = () => {
22
- throw new Error("Unable to go down on the initial version!");
23
- };
24
16
  const finalVersion = () => {
25
17
  throw new Error("Unable to go up on the final version!");
26
18
  };
27
- /**
28
- * @deprecated
29
- * @param version
30
- * @param migration
31
- * @returns
32
- */
33
- function initial(version, migration) {
34
- return createMigration({
35
- version,
36
- up: migration,
37
- down() {
38
- throw new Error("Unable to go down on the initial version!");
39
- }
40
- });
41
- }
42
- function final(version, migration) {
43
- return createMigration({
44
- version,
45
- up: () => {
46
- throw new Error(`Unable to go up on the final version "${version}"!`);
47
- },
48
- down: migration
49
- });
50
- }
51
19
  //#endregion
52
20
  //#region src/utils/object-keys.ts
53
21
  const objectKeys = Object.keys;
@@ -83,20 +51,19 @@ var Migrator = class {
83
51
  if (currentIndex === -1) throw new Error(`Current version "${currentVersion}" not found in migrations`);
84
52
  if (targetIndex === -1) throw new Error(`Target version "${targetVersion}" not found in migrations`);
85
53
  if (currentIndex === targetIndex) return finalState;
86
- const isUpgrade = currentIndex < targetIndex;
87
- const increment = isUpgrade ? 1 : -1;
88
- const direction = isUpgrade ? "up" : "down";
89
- for (let i = currentIndex; isUpgrade ? i < targetIndex : i > targetIndex; i += increment) {
54
+ if (!(currentIndex < targetIndex)) return finalState;
55
+ for (let i = currentIndex; i < targetIndex; i++) {
90
56
  const currentVersion = versions[i];
91
- const nextVersion = versions[i + increment];
57
+ const nextVersion = versions[i + 1];
92
58
  if (options?.debug) console.log(" Migrating to version:", nextVersion);
93
59
  const migrationVersion = currentVersion;
94
60
  const migration = this.migrations[migrationVersion];
95
61
  const { version: _, ...stateWithoutVersion } = finalState;
96
62
  finalState = {
97
- ...await migration[direction](stateWithoutVersion, currentVersion),
63
+ ...await migration.up(stateWithoutVersion, currentVersion),
98
64
  version: nextVersion
99
65
  };
66
+ if (options?.onStep) await options.onStep(finalState, nextVersion);
100
67
  if (options?.debug) console.log(" Migrated state:", finalState);
101
68
  }
102
69
  return finalState;
@@ -122,6 +89,6 @@ const createMigrator = () => {
122
89
  };
123
90
  };
124
91
  //#endregion
125
- export { createMigration, createMigrator, createVersionSchema, final, finalVersion, initial, initialVersion };
92
+ export { createMigration, createMigrator, createVersionSchema, finalVersion };
126
93
 
127
94
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/models/createMigration.ts","../src/utils/object-keys.ts","../src/models/migration.ts","../src/models/createMigrator.ts"],"sourcesContent":["import {\n MigrationFn,\n MigrationObjInput,\n MigrationClass,\n MigrationSchema,\n OmitVersion,\n SemVer,\n} from \"./migration\";\n\nexport function createMigration<\n Down extends MigrationSchema,\n Current extends MigrationSchema,\n Up extends MigrationSchema,\n>(migration: MigrationObjInput<Down, Current, Up>): MigrationClass<Down, Current, Up> {\n return {\n version: migration.version,\n up: async (state, nextVersion) => {\n const newState = (await migration.up(state, nextVersion)) as unknown as Up;\n newState.version = nextVersion as SemVer;\n return newState;\n },\n down: async (state, nextVersion) => {\n const newState = (await migration.down(state, nextVersion)) as unknown as Down;\n newState.version = nextVersion as SemVer;\n return newState;\n },\n };\n}\n\nexport const initialVersion = () => {\n throw new Error(\"Unable to go down on the initial version!\");\n};\n\nexport const finalVersion = () => {\n throw new Error(\"Unable to go up on the final version!\");\n};\n\n// export const finalVersion = <T>(state: T) => {\n// return state\n// }\n\n/**\n * @deprecated\n * @param version\n * @param migration\n * @returns\n */\nexport function initial<Current extends MigrationSchema, Up extends MigrationSchema>(\n version: SemVer,\n migration: MigrationFn<OmitVersion<Current>, OmitVersion<Up>>,\n): MigrationClass<Current, Current, Up> {\n return createMigration({\n version,\n up: migration,\n down() {\n throw new Error(\"Unable to go down on the initial version!\");\n },\n });\n}\n\nexport function final<Current extends MigrationSchema, Down extends MigrationSchema>(\n version: SemVer,\n migration: MigrationFn<OmitVersion<Current>, OmitVersion<Down>>,\n): MigrationClass<Down, Current, Current> {\n return createMigration({\n version,\n up: () => {\n throw new Error(`Unable to go up on the final version \"${version}\"!`);\n },\n down: migration,\n });\n}\n","export type ObjectKeys<T extends object> = `${Exclude<keyof T, symbol>}`;\n\nexport const objectKeys = Object.keys as <Type extends object>(\n value: Type,\n) => Array<ObjectKeys<Type>>;\nexport const objectEntries = Object.entries as <Type extends Record<PropertyKey, unknown>>(\n value: Type,\n) => Array<[ObjectKeys<Type>, Type[ObjectKeys<Type>]]>;\n","import * as semver from \"semver\";\nimport { coerce } from \"semver\";\nimport { objectKeys } from \"../utils/object-keys\";\nimport { klona } from \"klona\";\nimport { custom, InferOutput, object, ObjectEntries } from \"valibot\";\n\nexport type Awaitable<T> = Promise<T> | T;\n\nexport type MigrationFn<From, To> = (state: From, targetVersion: string) => Awaitable<To>;\n\nexport type MigrateOptions = {\n debug?: boolean;\n target?: SemVer;\n};\n\nconst SemverValidator = custom<SemVer>((input) =>\n typeof input === \"string\" ? /^\\d+\\.\\d+\\.\\d+$/.test(input) : false,\n);\n\nexport const SemverVersionValidator = SemverValidator;\n// export const SemverVersionValidator = optional(SemverValidator, '1.0.0')\n\nexport const MigrationSchemaValidator = object({\n version: SemverVersionValidator,\n});\n\nexport const createVersionSchema = <OBJECTENTRIES extends ObjectEntries>(schema: OBJECTENTRIES) =>\n object({\n ...schema,\n });\n\nexport type MigrationSchema = InferOutput<typeof MigrationSchemaValidator>;\n\nexport type OmitVersion<T> = Omit<T, keyof MigrationSchema>;\n\nexport type SemVer = `${number}.${number}.${number}`;\n\nexport interface MigrationClass<Down, Current, Up> {\n version: SemVer;\n up: MigrationFn<Current, Up>;\n down: MigrationFn<Current, Down>;\n}\n\nexport interface MigrationObjInput<Down, Current, Up> {\n version: SemVer;\n up: MigrationFn<OmitVersion<Current>, OmitVersion<Up>>;\n down: MigrationFn<OmitVersion<Current>, OmitVersion<Down>>;\n}\n\nexport interface MigratorConfig<InitialState, FinalState> {\n migrations: MigrationClass<any, any, any>[];\n coerce?: boolean;\n defaultValue?: FinalState;\n}\n\nexport class Migrator<InitialState extends MigrationSchema, OutputState extends MigrationSchema> {\n current: SemVer;\n\n migrations: Record<SemVer, MigrationClass<any, any, any>> = {};\n\n coerce: boolean;\n defaultValue: OutputState;\n\n constructor(config: MigratorConfig<InitialState, OutputState>) {\n config.migrations.forEach((migration) => {\n this.migrations[migration.version] = migration;\n });\n\n this.defaultValue = config?.defaultValue ?? ({} as OutputState);\n\n const versions = this.getVersions();\n this.current = versions[versions.length - 1];\n this.coerce = config?.coerce ?? true;\n }\n\n getVersions() {\n const keys = objectKeys(this.migrations);\n return semver.sort(keys);\n }\n\n async migrate(\n _state: MigrationSchema | undefined,\n options?: MigrateOptions,\n ): Promise<OutputState> {\n const state = _state ?? this.defaultValue;\n const currentVersion = this.tryCoerce(state.version);\n const targetVersion = this.tryCoerce(options?.target ?? this.current);\n\n if (options?.debug) {\n console.log(\"Migrating from\", currentVersion, \"to\", targetVersion);\n }\n\n let finalState = klona(state) as any; // Using any here because we'll be transforming between versions\n const versions = this.getVersions();\n const currentIndex = versions.indexOf(currentVersion);\n const targetIndex = versions.indexOf(targetVersion);\n\n if (currentIndex === -1) {\n throw new Error(`Current version \"${currentVersion}\" not found in migrations`);\n }\n if (targetIndex === -1) {\n throw new Error(`Target version \"${targetVersion}\" not found in migrations`);\n }\n\n // If we're already at the target version, return the state\n if (currentIndex === targetIndex) {\n return finalState as OutputState;\n }\n\n // Get the migration path\n const isUpgrade = currentIndex < targetIndex;\n const increment = isUpgrade ? 1 : -1;\n const direction = isUpgrade ? \"up\" : \"down\";\n\n // Perform migrations\n for (let i = currentIndex; isUpgrade ? i < targetIndex : i > targetIndex; i += increment) {\n const currentVersion = versions[i];\n const nextVersion = versions[i + increment];\n\n if (options?.debug) {\n console.log(\"\\tMigrating to version:\", nextVersion);\n }\n\n // For upgrades, use current version's migration\n // For downgrades, use previous version's migration\n const migrationVersion = currentVersion;\n const migration = this.migrations[migrationVersion];\n\n // Remove version before migration\n const { version: _, ...stateWithoutVersion } = finalState;\n\n // Perform migration\n const migratedState = await migration[direction](stateWithoutVersion, currentVersion);\n\n // Add new version\n finalState = {\n ...migratedState,\n version: nextVersion,\n };\n\n if (options?.debug) {\n console.log(\"\\tMigrated state:\", finalState);\n }\n }\n\n return finalState as OutputState;\n }\n\n tryCoerce(version: SemVer): SemVer {\n return this.coerce ? ((coerce(version)?.version as SemVer | undefined) ?? version) : version;\n }\n\n needMigration(version: SemVer) {\n const newVersion = this.tryCoerce(version);\n return semver.lt(newVersion, this.current);\n }\n}\n","import { MigratorConfig, Migrator, MigrationSchema } from \"./migration\";\n\nexport const createMigrator = <\n InitialState extends MigrationSchema,\n FinalState extends MigrationSchema,\n>() => {\n return {\n createDefault(defaultState: FinalState): FinalState {\n return defaultState;\n },\n createMigrations(\n config: MigratorConfig<InitialState, FinalState>,\n ): Migrator<InitialState, FinalState> {\n return new Migrator<InitialState, FinalState>(config);\n },\n };\n};\n\nexport type MigratorFactory<\n InitialState extends MigrationSchema,\n FinalState extends MigrationSchema,\n> = ReturnType<typeof createMigrator<InitialState, FinalState>>;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/models/createMigration.ts","../src/utils/object-keys.ts","../src/models/migration.ts","../src/models/createMigrator.ts"],"sourcesContent":["import {\n MigrationFn,\n MigrationObjInput,\n MigrationClass,\n MigrationSchema,\n OmitVersion,\n SemVer,\n} from \"./migration\";\n\nexport function createMigration<\n Current extends MigrationSchema,\n Up extends MigrationSchema,\n>(migration: MigrationObjInput<Current, Up>): MigrationClass<Current, Up> {\n return {\n version: migration.version,\n up: async (state, nextVersion) => {\n const newState = migration.up\n ? ((await migration.up(state, nextVersion)) as unknown as Up)\n : (state as unknown as Up);\n newState.version = nextVersion as SemVer;\n return newState;\n },\n };\n}\n\nexport const finalVersion = () => {\n throw new Error(\"Unable to go up on the final version!\");\n};\n\n","export type ObjectKeys<T extends object> = `${Exclude<keyof T, symbol>}`;\n\nexport const objectKeys = Object.keys as <Type extends object>(\n value: Type,\n) => Array<ObjectKeys<Type>>;\nexport const objectEntries = Object.entries as <Type extends Record<PropertyKey, unknown>>(\n value: Type,\n) => Array<[ObjectKeys<Type>, Type[ObjectKeys<Type>]]>;\n","import * as semver from \"semver\";\nimport { coerce } from \"semver\";\nimport { objectKeys } from \"../utils/object-keys\";\nimport { klona } from \"klona\";\nimport { custom, InferOutput, object, ObjectEntries } from \"valibot\";\n\nexport type Awaitable<T> = Promise<T> | T;\n\nexport type MigrationFn<From, To> = (state: From, targetVersion: string) => Awaitable<To>;\n\nexport type MigrateOptions = {\n debug?: boolean;\n target?: SemVer;\n onStep?: (state: any, version: SemVer) => Promise<void> | void;\n};\n\nconst SemverValidator = custom<SemVer>((input) =>\n typeof input === \"string\" ? /^\\d+\\.\\d+\\.\\d+$/.test(input) : false,\n);\n\nexport const SemverVersionValidator = SemverValidator;\n// export const SemverVersionValidator = optional(SemverValidator, '1.0.0')\n\nexport const MigrationSchemaValidator = object({\n version: SemverVersionValidator,\n});\n\nexport const createVersionSchema = <OBJECTENTRIES extends ObjectEntries>(schema: OBJECTENTRIES) =>\n object({\n ...schema,\n });\n\nexport type MigrationSchema = InferOutput<typeof MigrationSchemaValidator>;\n\nexport type OmitVersion<T> = Omit<T, keyof MigrationSchema>;\n\nexport type SemVer = `${number}.${number}.${number}`;\n\nexport interface MigrationClass<Current, Up> {\n version: SemVer;\n up: MigrationFn<Current, Up>;\n}\n\nexport interface MigrationObjInput<Current, Up> {\n version: SemVer;\n up: MigrationFn<OmitVersion<Current>, OmitVersion<Up>>;\n}\n\nexport interface MigratorConfig<InitialState, FinalState> {\n migrations: MigrationClass<any, any>[];\n coerce?: boolean;\n defaultValue?: FinalState;\n}\n\nexport class Migrator<InitialState extends MigrationSchema, OutputState extends MigrationSchema> {\n current: SemVer;\n\n migrations: Record<SemVer, MigrationClass<any, any>> = {};\n\n coerce: boolean;\n defaultValue: OutputState;\n\n constructor(config: MigratorConfig<InitialState, OutputState>) {\n config.migrations.forEach((migration) => {\n this.migrations[migration.version] = migration;\n });\n\n this.defaultValue = config?.defaultValue ?? ({} as OutputState);\n\n const versions = this.getVersions();\n this.current = versions[versions.length - 1];\n this.coerce = config?.coerce ?? true;\n }\n\n getVersions() {\n const keys = objectKeys(this.migrations);\n return semver.sort(keys);\n }\n\n async migrate(\n _state: MigrationSchema | undefined,\n options?: MigrateOptions,\n ): Promise<OutputState> {\n const state = _state ?? this.defaultValue;\n const currentVersion = this.tryCoerce(state.version);\n const targetVersion = this.tryCoerce(options?.target ?? this.current);\n\n if (options?.debug) {\n console.log(\"Migrating from\", currentVersion, \"to\", targetVersion);\n }\n\n let finalState = klona(state) as any; // Using any here because we'll be transforming between versions\n const versions = this.getVersions();\n const currentIndex = versions.indexOf(currentVersion);\n const targetIndex = versions.indexOf(targetVersion);\n\n if (currentIndex === -1) {\n throw new Error(`Current version \"${currentVersion}\" not found in migrations`);\n }\n if (targetIndex === -1) {\n throw new Error(`Target version \"${targetVersion}\" not found in migrations`);\n }\n\n // If we're already at the target version, return the state\n if (currentIndex === targetIndex) {\n return finalState as OutputState;\n }\n\n // Get the migration path\n const isUpgrade = currentIndex < targetIndex;\n\n if (!isUpgrade) {\n return finalState as OutputState;\n }\n\n // Perform migrations\n for (let i = currentIndex; i < targetIndex; i++) {\n const currentVersion = versions[i];\n const nextVersion = versions[i + 1];\n\n if (options?.debug) {\n console.log(\"\\tMigrating to version:\", nextVersion);\n }\n\n const migrationVersion = currentVersion;\n const migration = this.migrations[migrationVersion];\n\n // Remove version before migration\n const { version: _, ...stateWithoutVersion } = finalState;\n\n // Perform migration\n const migratedState = await migration.up(stateWithoutVersion, currentVersion);\n\n // Add new version\n finalState = {\n ...migratedState,\n version: nextVersion,\n };\n\n if (options?.onStep) {\n await options.onStep(finalState, nextVersion);\n }\n\n if (options?.debug) {\n console.log(\"\\tMigrated state:\", finalState);\n }\n }\n\n return finalState as OutputState;\n }\n\n tryCoerce(version: SemVer): SemVer {\n return this.coerce ? ((coerce(version)?.version as SemVer | undefined) ?? version) : version;\n }\n\n needMigration(version: SemVer) {\n const newVersion = this.tryCoerce(version);\n return semver.lt(newVersion, this.current);\n }\n}\n","import { MigratorConfig, Migrator, MigrationSchema } from \"./migration\";\n\nexport const createMigrator = <\n InitialState extends MigrationSchema,\n FinalState extends MigrationSchema,\n>() => {\n return {\n createDefault(defaultState: FinalState): FinalState {\n return defaultState;\n },\n createMigrations(\n config: MigratorConfig<InitialState, FinalState>,\n ): Migrator<InitialState, FinalState> {\n return new Migrator<InitialState, FinalState>(config);\n },\n };\n};\n\nexport type MigratorFactory<\n InitialState extends MigrationSchema,\n FinalState extends MigrationSchema,\n> = ReturnType<typeof createMigrator<InitialState, FinalState>>;\n"],"mappings":";;;;;AASA,SAAgB,gBAGd,WAAwE;AACxE,QAAO;EACL,SAAS,UAAU;EACnB,IAAI,OAAO,OAAO,gBAAgB;GAChC,MAAM,WAAW,UAAU,KACrB,MAAM,UAAU,GAAG,OAAO,YAAY,GACvC;AACL,YAAS,UAAU;AACnB,UAAO;;EAEV;;AAGH,MAAa,qBAAqB;AAChC,OAAM,IAAI,MAAM,wCAAwC;;;;ACxB1D,MAAa,aAAa,OAAO;ACqBO,OAAO,EAC7C,SARsB,QAAgB,UACtC,OAAO,UAAU,WAAW,kBAAkB,KAAK,MAAM,GAAG,MAC7D,EAOA,CAAC;AAEF,MAAa,uBAA4D,WACvE,OAAO,EACL,GAAG,QACJ,CAAC;AAwBJ,IAAa,WAAb,MAAiG;CAC/F;CAEA,aAAuD,EAAE;CAEzD;CACA;CAEA,YAAY,QAAmD;AAC7D,SAAO,WAAW,SAAS,cAAc;AACvC,QAAK,WAAW,UAAU,WAAW;IACrC;AAEF,OAAK,eAAe,QAAQ,gBAAiB,EAAE;EAE/C,MAAM,WAAW,KAAK,aAAa;AACnC,OAAK,UAAU,SAAS,SAAS,SAAS;AAC1C,OAAK,SAAS,QAAQ,UAAU;;CAGlC,cAAc;EACZ,MAAM,OAAO,WAAW,KAAK,WAAW;AACxC,SAAO,OAAO,KAAK,KAAK;;CAG1B,MAAM,QACJ,QACA,SACsB;EACtB,MAAM,QAAQ,UAAU,KAAK;EAC7B,MAAM,iBAAiB,KAAK,UAAU,MAAM,QAAQ;EACpD,MAAM,gBAAgB,KAAK,UAAU,SAAS,UAAU,KAAK,QAAQ;AAErE,MAAI,SAAS,MACX,SAAQ,IAAI,kBAAkB,gBAAgB,MAAM,cAAc;EAGpE,IAAI,aAAa,MAAM,MAAM;EAC7B,MAAM,WAAW,KAAK,aAAa;EACnC,MAAM,eAAe,SAAS,QAAQ,eAAe;EACrD,MAAM,cAAc,SAAS,QAAQ,cAAc;AAEnD,MAAI,iBAAiB,GACnB,OAAM,IAAI,MAAM,oBAAoB,eAAe,2BAA2B;AAEhF,MAAI,gBAAgB,GAClB,OAAM,IAAI,MAAM,mBAAmB,cAAc,2BAA2B;AAI9E,MAAI,iBAAiB,YACnB,QAAO;AAMT,MAAI,EAFc,eAAe,aAG/B,QAAO;AAIT,OAAK,IAAI,IAAI,cAAc,IAAI,aAAa,KAAK;GAC/C,MAAM,iBAAiB,SAAS;GAChC,MAAM,cAAc,SAAS,IAAI;AAEjC,OAAI,SAAS,MACX,SAAQ,IAAI,0BAA2B,YAAY;GAGrD,MAAM,mBAAmB;GACzB,MAAM,YAAY,KAAK,WAAW;GAGlC,MAAM,EAAE,SAAS,GAAG,GAAG,wBAAwB;AAM/C,gBAAa;IACX,GAJoB,MAAM,UAAU,GAAG,qBAAqB,eAAe;IAK3E,SAAS;IACV;AAED,OAAI,SAAS,OACX,OAAM,QAAQ,OAAO,YAAY,YAAY;AAG/C,OAAI,SAAS,MACX,SAAQ,IAAI,oBAAqB,WAAW;;AAIhD,SAAO;;CAGT,UAAU,SAAyB;AACjC,SAAO,KAAK,SAAW,OAAO,QAAQ,EAAE,WAAkC,UAAW;;CAGvF,cAAc,SAAiB;EAC7B,MAAM,aAAa,KAAK,UAAU,QAAQ;AAC1C,SAAO,OAAO,GAAG,YAAY,KAAK,QAAQ;;;;;AC3J9C,MAAa,uBAGN;AACL,QAAO;EACL,cAAc,cAAsC;AAClD,UAAO;;EAET,iBACE,QACoC;AACpC,UAAO,IAAI,SAAmC,OAAO;;EAExD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipelab/migration",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.2",
4
4
  "private": false,
5
5
  "description": "Migration utilities for Pipelab data structures",
6
6
  "license": "FSL-1.1-MIT",
@@ -35,7 +35,7 @@
35
35
  "@types/semver": "^7.5.8",
36
36
  "tsdown": "0.21.2",
37
37
  "typescript": "5.9.3",
38
- "@pipelab/tsconfig": "1.0.0-beta.0"
38
+ "@pipelab/tsconfig": "1.0.0-beta.2"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsdown",
package/src/index.ts CHANGED
@@ -7,5 +7,6 @@ export type {
7
7
  MigrationFn,
8
8
  SemVer,
9
9
  OmitVersion,
10
+ Awaitable,
10
11
  } from "./models/migration";
11
12
  export { createVersionSchema } from "./models/migration";
@@ -8,65 +8,22 @@ import {
8
8
  } from "./migration";
9
9
 
10
10
  export function createMigration<
11
- Down extends MigrationSchema,
12
11
  Current extends MigrationSchema,
13
12
  Up extends MigrationSchema,
14
- >(migration: MigrationObjInput<Down, Current, Up>): MigrationClass<Down, Current, Up> {
13
+ >(migration: MigrationObjInput<Current, Up>): MigrationClass<Current, Up> {
15
14
  return {
16
15
  version: migration.version,
17
16
  up: async (state, nextVersion) => {
18
- const newState = (await migration.up(state, nextVersion)) as unknown as Up;
19
- newState.version = nextVersion as SemVer;
20
- return newState;
21
- },
22
- down: async (state, nextVersion) => {
23
- const newState = (await migration.down(state, nextVersion)) as unknown as Down;
17
+ const newState = migration.up
18
+ ? ((await migration.up(state, nextVersion)) as unknown as Up)
19
+ : (state as unknown as Up);
24
20
  newState.version = nextVersion as SemVer;
25
21
  return newState;
26
22
  },
27
23
  };
28
24
  }
29
25
 
30
- export const initialVersion = () => {
31
- throw new Error("Unable to go down on the initial version!");
32
- };
33
-
34
26
  export const finalVersion = () => {
35
27
  throw new Error("Unable to go up on the final version!");
36
28
  };
37
29
 
38
- // export const finalVersion = <T>(state: T) => {
39
- // return state
40
- // }
41
-
42
- /**
43
- * @deprecated
44
- * @param version
45
- * @param migration
46
- * @returns
47
- */
48
- export function initial<Current extends MigrationSchema, Up extends MigrationSchema>(
49
- version: SemVer,
50
- migration: MigrationFn<OmitVersion<Current>, OmitVersion<Up>>,
51
- ): MigrationClass<Current, Current, Up> {
52
- return createMigration({
53
- version,
54
- up: migration,
55
- down() {
56
- throw new Error("Unable to go down on the initial version!");
57
- },
58
- });
59
- }
60
-
61
- export function final<Current extends MigrationSchema, Down extends MigrationSchema>(
62
- version: SemVer,
63
- migration: MigrationFn<OmitVersion<Current>, OmitVersion<Down>>,
64
- ): MigrationClass<Down, Current, Current> {
65
- return createMigration({
66
- version,
67
- up: () => {
68
- throw new Error(`Unable to go up on the final version "${version}"!`);
69
- },
70
- down: migration,
71
- });
72
- }
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect, beforeAll, beforeEach, afterEach, vi } from "vitest";
2
- import { createMigration, final, finalVersion, initial, initialVersion } from "./createMigration";
2
+ import { createMigration, finalVersion } from "./createMigration";
3
3
  import { createMigrator, MigratorFactory } from "./createMigrator";
4
4
  import { Migrator, MigrationSchema } from "./migration";
5
5
  import { z } from "zod";
@@ -53,70 +53,31 @@ describe("migrator", () => {
53
53
  let migratorInstance: MigratorFactory<V1, V4>;
54
54
  let migratorMigrations: Migrator<V1, V4>;
55
55
  beforeAll(() => {
56
- // migratorInstance = createMigrator<V4>({
57
- // migrations: [
58
- // initial<V1, V2>('1.0.0', (state) => ({
59
- // dummyV2: state.dummyV1,
60
- // })),
61
- // createMigration<V1, V2, V3>({
62
- // version: '2.0.0',
63
- // up: (state) => ({
64
- // dummyV3: state.dummyV2,
65
- // }),
66
- // down: (state) => ({
67
- // dummyV1: state.dummyV2,
68
- // }),
69
- // }),
70
- // createMigration<V2, V3, V4>({
71
- // version: '3.0.0',
72
- // up: (state) => ({
73
- // dummy: state.dummyV3,
74
- // }),
75
- // down: (state) => ({
76
- // dummyV2: state.dummyV3,
77
- // }),
78
- // }),
79
- // final<V4, V3>('4.0.0', (state) => ({
80
- // dummyV3: state.dummy,
81
- // })),
82
- // ],
83
- // });
84
-
85
56
  migratorInstance = createMigrator<V1, V4>();
86
57
  migratorMigrations = migratorInstance.createMigrations({
87
- defaultValue: outputV1,
58
+ defaultValue: outputV4,
88
59
  migrations: [
89
- createMigration<never, V1, V2>({
60
+ createMigration<V1, V2>({
90
61
  version: "1.0.0",
91
62
  up: (state) => ({
92
63
  dummyV2: state.dummyV1,
93
64
  }),
94
- down: initialVersion,
95
65
  }),
96
- createMigration<V1, V2, V3>({
66
+ createMigration<V2, V3>({
97
67
  version: "2.0.0",
98
68
  up: (state) => ({
99
69
  dummyV3: state.dummyV2,
100
70
  }),
101
- down: (state) => ({
102
- dummyV1: state.dummyV2,
103
- }),
104
71
  }),
105
- createMigration<V2, V3, V4>({
72
+ createMigration<V3, V4>({
106
73
  version: "3.0.0",
107
74
  up: (state) => ({
108
75
  dummy: state.dummyV3,
109
76
  }),
110
- down: (state) => ({
111
- dummyV2: state.dummyV3,
112
- }),
113
77
  }),
114
- createMigration<V3, V4, never>({
78
+ createMigration<V4, never>({
115
79
  version: "4.0.0",
116
80
  up: finalVersion,
117
- down: (state) => ({
118
- dummyV3: state.dummy,
119
- }),
120
81
  }),
121
82
  ],
122
83
  });
@@ -165,48 +126,38 @@ describe("migrator", () => {
165
126
  return expect(migratorMigrations.migrate(input)).resolves.toStrictEqual(outputV4);
166
127
  });
167
128
 
168
- test("should downgrade", async () => {
129
+ test("should skip downgrades silently / not support them", async () => {
169
130
  const input: V4 = {
170
131
  dummy: "aaa",
171
132
  version: "4.0.0",
172
133
  };
173
- const result = await migratorMigrations.migrate(input);
174
- expect(result).toEqual(outputV4);
175
- const resultDown = await migratorMigrations.migrate(result, {
134
+ // Migrate should return the exact state since downgrades are bypassed
135
+ const result = await migratorMigrations.migrate(input, {
176
136
  target: "3.0.0",
177
137
  });
178
- expect(resultDown).toEqual(outputV3);
179
- const resultDown2 = await migratorMigrations.migrate(resultDown, {
180
- target: "2.0.0",
181
- });
182
- expect(resultDown2).toEqual(outputV2);
183
- const resultUp = await migratorMigrations.migrate(resultDown2, {
184
- target: "3.0.0",
185
- });
186
- expect(resultUp).toEqual(outputV3);
138
+ expect(result).toEqual(input);
187
139
  });
188
140
 
189
- test("should throw if target is not in migrations", async () => {
190
- const input: V4 = {
191
- dummy: "aaa",
192
- version: "1.2.0",
141
+ test("should support onStep callback during migration", async () => {
142
+ const input: V1 = {
143
+ dummyV1: "aaa",
144
+ version: "1.0.0",
193
145
  };
194
146
 
195
- const badMigratorInstance = createMigrator<V1, V4>().createMigrations({
196
- migrations: [
197
- initial<V1, V2>("1.0.0", (state) => ({
198
- dummyV2: state.dummyV1,
199
- })),
200
- final<V4, V3>("4.0.0", (state) => ({
201
- dummyV3: state.dummy,
202
- })),
203
- ],
147
+ const steps: any[] = [];
148
+ await migratorMigrations.migrate(input, {
149
+ onStep: async (state, version) => {
150
+ steps.push({ ...state, version });
151
+ },
204
152
  });
205
153
 
206
- return expect(badMigratorInstance.migrate(input)).rejects.toThrow();
154
+ expect(steps).toHaveLength(3); // 1 -> 2, 2 -> 3, 3 -> 4
155
+ expect(steps[0]).toEqual(expect.objectContaining({ dummyV2: "aaa", version: "2.0.0" }));
156
+ expect(steps[1]).toEqual(expect.objectContaining({ dummyV3: "aaa", version: "3.0.0" }));
157
+ expect(steps[2]).toEqual(expect.objectContaining({ dummy: "aaa", version: "4.0.0" }));
207
158
  });
208
159
 
209
- test("should throw on migration not found", async () => {
160
+ test("should throw if target is not in migrations", async () => {
210
161
  const input: V4 = {
211
162
  dummy: "aaa",
212
163
  version: "1.2.0",
@@ -214,16 +165,20 @@ describe("migrator", () => {
214
165
 
215
166
  const badMigratorInstance = createMigrator<V1, V4>().createMigrations({
216
167
  migrations: [
217
- initial<V1, V2>("1.0.0", (state) => ({
218
- dummyV2: state.dummyV1,
219
- })),
220
- final<V4, V3>("4.0.0", (state) => ({
221
- dummyV3: state.dummy,
222
- })),
168
+ createMigration<V1, V2>({
169
+ version: "1.0.0",
170
+ up: (state) => ({
171
+ dummyV2: state.dummyV1,
172
+ }),
173
+ }),
174
+ createMigration<V4, never>({
175
+ version: "4.0.0",
176
+ up: finalVersion,
177
+ }),
223
178
  ],
224
179
  });
225
180
 
226
- await expect(badMigratorInstance.migrate(input)).rejects.toThrow();
181
+ return expect(badMigratorInstance.migrate(input)).rejects.toThrow();
227
182
  });
228
183
 
229
184
  test("should migrate an incomplete version (loose)", async () => {
@@ -235,19 +190,15 @@ describe("migrator", () => {
235
190
 
236
191
  const migrator = createMigrator<V1, V4>().createMigrations({
237
192
  migrations: [
238
- createMigration<never, V1, V2>({
193
+ createMigration<V1, V2>({
239
194
  version: "1.0.0",
240
195
  up: (state) => ({
241
196
  dummyV2: state.dummyV1,
242
197
  }),
243
- down: initialVersion,
244
198
  }),
245
- createMigration<V1, V2, never>({
199
+ createMigration<V2, never>({
246
200
  version: "2.0.0",
247
201
  up: finalVersion,
248
- down: (state) => ({
249
- dummyV1: state.dummyV2,
250
- }),
251
202
  }),
252
203
  ],
253
204
  });
@@ -266,19 +217,15 @@ describe("migrator", () => {
266
217
 
267
218
  const migrator = createMigrator<V1, V2>().createMigrations({
268
219
  migrations: [
269
- createMigration<never, V1, V2>({
220
+ createMigration<V1, V2>({
270
221
  version: "1.0.0",
271
222
  up: (state) => ({
272
223
  dummyV2: state.dummyV1,
273
224
  }),
274
- down: initialVersion,
275
225
  }),
276
- createMigration<V1, V2, never>({
226
+ createMigration<V2, never>({
277
227
  version: "2.0.0",
278
228
  up: finalVersion,
279
- down: (state) => ({
280
- dummyV1: state.dummyV2,
281
- }),
282
229
  }),
283
230
  ],
284
231
  coerce: false,
@@ -306,7 +253,7 @@ describe("async", () => {
306
253
 
307
254
  const migrator = createMigrator<V1, V2>().createMigrations({
308
255
  migrations: [
309
- createMigration<never, V1, V2>({
256
+ createMigration<V1, V2>({
310
257
  version: "1.0.0",
311
258
  up: async (state) => {
312
259
  await sleep(1000);
@@ -314,17 +261,10 @@ describe("async", () => {
314
261
  dummyV2: state.dummyV1,
315
262
  };
316
263
  },
317
- down: initialVersion,
318
264
  }),
319
- createMigration<V1, V2, never>({
265
+ createMigration<V2, never>({
320
266
  version: "2.0.0",
321
267
  up: finalVersion,
322
- down: async (state) => {
323
- await sleep(1000);
324
- return {
325
- dummyV1: state.dummyV2,
326
- };
327
- },
328
268
  }),
329
269
  ],
330
270
  });
@@ -11,6 +11,7 @@ export type MigrationFn<From, To> = (state: From, targetVersion: string) => Awai
11
11
  export type MigrateOptions = {
12
12
  debug?: boolean;
13
13
  target?: SemVer;
14
+ onStep?: (state: any, version: SemVer) => Promise<void> | void;
14
15
  };
15
16
 
16
17
  const SemverValidator = custom<SemVer>((input) =>
@@ -35,20 +36,18 @@ export type OmitVersion<T> = Omit<T, keyof MigrationSchema>;
35
36
 
36
37
  export type SemVer = `${number}.${number}.${number}`;
37
38
 
38
- export interface MigrationClass<Down, Current, Up> {
39
+ export interface MigrationClass<Current, Up> {
39
40
  version: SemVer;
40
41
  up: MigrationFn<Current, Up>;
41
- down: MigrationFn<Current, Down>;
42
42
  }
43
43
 
44
- export interface MigrationObjInput<Down, Current, Up> {
44
+ export interface MigrationObjInput<Current, Up> {
45
45
  version: SemVer;
46
46
  up: MigrationFn<OmitVersion<Current>, OmitVersion<Up>>;
47
- down: MigrationFn<OmitVersion<Current>, OmitVersion<Down>>;
48
47
  }
49
48
 
50
49
  export interface MigratorConfig<InitialState, FinalState> {
51
- migrations: MigrationClass<any, any, any>[];
50
+ migrations: MigrationClass<any, any>[];
52
51
  coerce?: boolean;
53
52
  defaultValue?: FinalState;
54
53
  }
@@ -56,7 +55,7 @@ export interface MigratorConfig<InitialState, FinalState> {
56
55
  export class Migrator<InitialState extends MigrationSchema, OutputState extends MigrationSchema> {
57
56
  current: SemVer;
58
57
 
59
- migrations: Record<SemVer, MigrationClass<any, any, any>> = {};
58
+ migrations: Record<SemVer, MigrationClass<any, any>> = {};
60
59
 
61
60
  coerce: boolean;
62
61
  defaultValue: OutputState;
@@ -109,20 +108,20 @@ export class Migrator<InitialState extends MigrationSchema, OutputState extends
109
108
 
110
109
  // Get the migration path
111
110
  const isUpgrade = currentIndex < targetIndex;
112
- const increment = isUpgrade ? 1 : -1;
113
- const direction = isUpgrade ? "up" : "down";
111
+
112
+ if (!isUpgrade) {
113
+ return finalState as OutputState;
114
+ }
114
115
 
115
116
  // Perform migrations
116
- for (let i = currentIndex; isUpgrade ? i < targetIndex : i > targetIndex; i += increment) {
117
+ for (let i = currentIndex; i < targetIndex; i++) {
117
118
  const currentVersion = versions[i];
118
- const nextVersion = versions[i + increment];
119
+ const nextVersion = versions[i + 1];
119
120
 
120
121
  if (options?.debug) {
121
122
  console.log("\tMigrating to version:", nextVersion);
122
123
  }
123
124
 
124
- // For upgrades, use current version's migration
125
- // For downgrades, use previous version's migration
126
125
  const migrationVersion = currentVersion;
127
126
  const migration = this.migrations[migrationVersion];
128
127
 
@@ -130,7 +129,7 @@ export class Migrator<InitialState extends MigrationSchema, OutputState extends
130
129
  const { version: _, ...stateWithoutVersion } = finalState;
131
130
 
132
131
  // Perform migration
133
- const migratedState = await migration[direction](stateWithoutVersion, currentVersion);
132
+ const migratedState = await migration.up(stateWithoutVersion, currentVersion);
134
133
 
135
134
  // Add new version
136
135
  finalState = {
@@ -138,6 +137,10 @@ export class Migrator<InitialState extends MigrationSchema, OutputState extends
138
137
  version: nextVersion,
139
138
  };
140
139
 
140
+ if (options?.onStep) {
141
+ await options.onStep(finalState, nextVersion);
142
+ }
143
+
141
144
  if (options?.debug) {
142
145
  console.log("\tMigrated state:", finalState);
143
146
  }