@pipelab/migration 1.0.0-beta.1 → 1.0.0-beta.10
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 +1 -1
- package/CHANGELOG.md +54 -0
- package/dist/index.d.mts +7 -17
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +7 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
- package/src/index.ts +1 -0
- package/src/models/createMigration.ts +6 -51
- package/src/models/migration.spec.ts +42 -102
- package/src/models/migration.ts +16 -13
package/.oxfmtrc.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# @pipelab/migration
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- s
|
|
8
|
+
|
|
9
|
+
## 1.0.0-beta.9
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- fg
|
|
14
|
+
|
|
15
|
+
## 1.0.0-beta.8
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- changes
|
|
20
|
+
|
|
21
|
+
## 1.0.0-beta.7
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- bump
|
|
26
|
+
|
|
27
|
+
## 1.0.0-beta.6
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- fixes
|
|
32
|
+
|
|
33
|
+
## 1.0.0-beta.5
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- bump
|
|
38
|
+
|
|
39
|
+
## 1.0.0-beta.4
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- beta tag
|
|
44
|
+
|
|
45
|
+
## 1.0.0-beta.3
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- bump
|
|
50
|
+
|
|
51
|
+
## 1.0.0-beta.2
|
|
52
|
+
|
|
53
|
+
### Patch Changes
|
|
54
|
+
|
|
55
|
+
- update
|
|
56
|
+
|
|
3
57
|
## 1.0.0-beta.1
|
|
4
58
|
|
|
5
59
|
### Patch 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<
|
|
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<
|
|
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
|
|
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
|
|
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<
|
|
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,
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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,
|
|
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,iBAAgC,eAAA,aAA4B,eAAA,CAAA,CAC1E,SAAA,EAAW,iBAAA,CAAkB,OAAA,EAAS,EAAA,IACrC,cAAA,CAAe,OAAA,EAAS,EAAA;AAAA,cAad,YAAA;;;cCtBA,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
|
-
|
|
87
|
-
|
|
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 +
|
|
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
|
|
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,
|
|
92
|
+
export { createMigration, createMigrator, createVersionSchema, finalVersion };
|
|
126
93
|
|
|
127
94
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -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<Current extends MigrationSchema, Up extends MigrationSchema>(\n migration: MigrationObjInput<Current, Up>,\n): 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","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,gBACd,WAC6B;AAC7B,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;;;;ACvB1D,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.
|
|
3
|
+
"version": "1.0.0-beta.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Migration utilities for Pipelab data structures",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|
|
@@ -35,11 +35,14 @@
|
|
|
35
35
|
"@types/semver": "^7.5.8",
|
|
36
36
|
"tsdown": "0.21.2",
|
|
37
37
|
"typescript": "5.9.3",
|
|
38
|
-
"
|
|
38
|
+
"vitest": "3.1.4",
|
|
39
|
+
"@pipelab/tsconfig": "1.0.0-beta.10"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"build": "tsdown",
|
|
42
43
|
"format": "oxfmt .",
|
|
43
|
-
"lint": "oxlint ."
|
|
44
|
+
"lint": "oxlint .",
|
|
45
|
+
"typecheck": "tsc -b",
|
|
46
|
+
"test": "vitest run"
|
|
44
47
|
}
|
|
45
48
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,66 +7,21 @@ import {
|
|
|
7
7
|
SemVer,
|
|
8
8
|
} from "./migration";
|
|
9
9
|
|
|
10
|
-
export function createMigration<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Up extends MigrationSchema,
|
|
14
|
-
>(migration: MigrationObjInput<Down, Current, Up>): MigrationClass<Down, Current, Up> {
|
|
10
|
+
export function createMigration<Current extends MigrationSchema, Up extends MigrationSchema>(
|
|
11
|
+
migration: MigrationObjInput<Current, Up>,
|
|
12
|
+
): MigrationClass<Current, Up> {
|
|
15
13
|
return {
|
|
16
14
|
version: migration.version,
|
|
17
15
|
up: async (state, nextVersion) => {
|
|
18
|
-
const newState =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
down: async (state, nextVersion) => {
|
|
23
|
-
const newState = (await migration.down(state, nextVersion)) as unknown as Down;
|
|
16
|
+
const newState = migration.up
|
|
17
|
+
? ((await migration.up(state, nextVersion)) as unknown as Up)
|
|
18
|
+
: (state as unknown as Up);
|
|
24
19
|
newState.version = nextVersion as SemVer;
|
|
25
20
|
return newState;
|
|
26
21
|
},
|
|
27
22
|
};
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
export const initialVersion = () => {
|
|
31
|
-
throw new Error("Unable to go down on the initial version!");
|
|
32
|
-
};
|
|
33
|
-
|
|
34
25
|
export const finalVersion = () => {
|
|
35
26
|
throw new Error("Unable to go up on the final version!");
|
|
36
27
|
};
|
|
37
|
-
|
|
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,10 +1,10 @@
|
|
|
1
1
|
import { describe, test, expect, beforeAll, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import { 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";
|
|
6
6
|
|
|
7
|
-
export const withMigration = (obj: z.
|
|
7
|
+
export const withMigration = (obj: z.ZodObject<any>) =>
|
|
8
8
|
z
|
|
9
9
|
.object({
|
|
10
10
|
version: z.string(),
|
|
@@ -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:
|
|
58
|
+
defaultValue: outputV4,
|
|
88
59
|
migrations: [
|
|
89
|
-
createMigration<
|
|
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<
|
|
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<
|
|
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<
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
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(
|
|
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
|
|
190
|
-
const input:
|
|
191
|
-
|
|
192
|
-
version: "1.
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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
|
});
|
package/src/models/migration.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
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
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
111
|
+
|
|
112
|
+
if (!isUpgrade) {
|
|
113
|
+
return finalState as OutputState;
|
|
114
|
+
}
|
|
114
115
|
|
|
115
116
|
// Perform migrations
|
|
116
|
-
for (let i = currentIndex;
|
|
117
|
+
for (let i = currentIndex; i < targetIndex; i++) {
|
|
117
118
|
const currentVersion = versions[i];
|
|
118
|
-
const nextVersion = versions[i +
|
|
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
|
|
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
|
}
|