-
-
Notifications
You must be signed in to change notification settings - Fork 190
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add persisted state migrations (#818)
* feat: Add persisted state migrations * test: Add tests for migrator * chore: add types to persisted migrations * docs: add migration docs * tests: make test cases a little more clear * docs(migrations): slight reordering * test: add one more test --------- Co-authored-by: Peter Weinberg <[email protected]>
- Loading branch information
1 parent
7e13adf
commit aa5dd51
Showing
7 changed files
with
433 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { produce, setAutoFreeze } from 'immer'; | ||
|
||
export const migrate = ( | ||
data, | ||
migrations, | ||
) => { | ||
setAutoFreeze(false); | ||
|
||
let version = data._migrationVersion ?? 0; | ||
const toVersion = migrations.migrationVersion | ||
|
||
if (typeof version !== "number" || typeof toVersion !== 'number') { | ||
throw new Error('No migration version found'); | ||
} | ||
|
||
while (version < toVersion) { | ||
const nextVersion = version + 1; | ||
const migrator = migrations[nextVersion]; | ||
|
||
if (!migrator) { | ||
throw new Error(`No migrator found for \`migrationVersion\` ${nextVersion}`); | ||
} | ||
|
||
data = produce(data, migrator); | ||
data._migrationVersion = nextVersion; | ||
version = data._migrationVersion; | ||
} | ||
|
||
setAutoFreeze(true); | ||
return data; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import { migrate } from '../src/migrations'; | ||
|
||
test('leaves an object untouched if there are no migrations pending', () => { | ||
// ARRANGE | ||
const result = migrate( | ||
{ | ||
_migrationVersion: 1, | ||
value: 'untouched', | ||
}, | ||
{ | ||
migrationVersion: 1, | ||
|
||
1: (state) => { | ||
state.value = 'modified'; | ||
}, | ||
}, | ||
); | ||
|
||
// ASSERT | ||
expect(result.value).toBe('untouched'); | ||
expect(result._migrationVersion).toBe(1); | ||
}); | ||
|
||
test('applies a migration if there is one pending', () => { | ||
// ARRANGE | ||
const result = migrate( | ||
{ | ||
value: 'untouched', | ||
}, | ||
{ | ||
migrationVersion: 1, | ||
|
||
1: (state) => { | ||
state.value = 'modified'; | ||
}, | ||
}, | ||
); | ||
|
||
expect(result.value).toBe('modified'); | ||
expect(result._migrationVersion).toBe(1); | ||
}); | ||
|
||
test('applies many migrations if there are many pending', () => { | ||
// ARRANGE | ||
const result = migrate( | ||
{ | ||
_migrationVersion: 0, | ||
}, | ||
{ | ||
migrationVersion: 4, | ||
|
||
0: (state) => { | ||
state.zero = true; | ||
}, | ||
1: (state) => { | ||
state.one = true; | ||
}, | ||
2: (state) => { | ||
state.two = true; | ||
}, | ||
3: (state) => { | ||
state.three = true; | ||
}, | ||
4: (state) => { | ||
state.four = true; | ||
}, | ||
5: (state) => { | ||
state.five = true; | ||
}, | ||
}, | ||
); | ||
|
||
// ASSERT | ||
expect(result.zero).toBe(undefined); | ||
expect(result.one).toBe(true); | ||
expect(result.two).toBe(true); | ||
expect(result.three).toBe(true); | ||
expect(result.four).toBe(true); | ||
expect(result.five).toBe(undefined); | ||
expect(result._migrationVersion).toBe(4); | ||
}); | ||
|
||
test('applies migrations and deletes old values', () => { | ||
// ARRANGE | ||
const result = migrate( | ||
{ | ||
_migrationVersion: 0, | ||
session: "session", | ||
}, | ||
{ | ||
migrationVersion: 2, | ||
|
||
1: (state) => { | ||
state.userSession = state.session; | ||
delete state.session; | ||
}, | ||
|
||
2: (state) => { | ||
state.domainSession = state.userSession; | ||
delete state.userSession; | ||
}, | ||
}, | ||
); | ||
|
||
// ASSERT | ||
expect(result.session).toBe(undefined); | ||
expect(result.userSession).toBe(undefined); | ||
expect(result.domainSession).toBe('session'); | ||
expect(result._migrationVersion).toBe(2); | ||
}); | ||
|
||
test('throws an error if there is no valid version', () => { | ||
// ARRANGE | ||
expect(() => { | ||
migrate( | ||
{ | ||
_migrationVersion: '0', | ||
}, | ||
{ | ||
migrationVersion: 1, | ||
|
||
1: (state) => { | ||
state.zero = true; | ||
}, | ||
}, | ||
); | ||
// ASSERT | ||
}).toThrowError(`No migration version found`); | ||
}); | ||
|
||
test('throws an error if there is no valid migration', () => { | ||
// ARRANGE | ||
expect(() => { | ||
migrate( | ||
{ | ||
_migrationVersion: 0, | ||
}, | ||
{ | ||
migrationVersion: 1, | ||
|
||
0: (state) => { | ||
state.zero = true; | ||
}, | ||
}, | ||
); | ||
// ASSERT | ||
}).toThrowError('No migrator found for `migrationVersion` 1'); | ||
}); | ||
|
||
test('guarantees that the version number ends up correct', () => { | ||
// ARRANGE | ||
const result = migrate( | ||
{ | ||
_migrationVersion: 0, | ||
value: 'untouched', | ||
}, | ||
{ | ||
migrationVersion: 1, | ||
1: (state) => { | ||
state.value = 'modified'; | ||
state._migrationVersion = 5; | ||
}, | ||
}, | ||
); | ||
|
||
// ASSERT | ||
expect(result.value).toBe('modified'); | ||
expect(result._migrationVersion).toBe(1); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
aa5dd51
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
easy-peasy – ./
easy-peasy-git-master-ctrlplusb.vercel.app
easy-peasy-ctrlplusb.vercel.app
easy-peasy.dev