Skip to content

Commit dce8cf9

Browse files
committed
Add migrator service.
1 parent 0471621 commit dce8cf9

File tree

12 files changed

+251
-20
lines changed

12 files changed

+251
-20
lines changed

index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export { HorizontallySymmetricalSafeAreaView } from './react-native/components/H
6464
export { intercalateRendered } from './react-native/utilities/intercalateRendered'
6565
export { isRenderedByReact } from './react-native/utilities/isRenderedByReact'
6666
export { logger } from './react-native/services/logger'
67+
export { Migrator } from './react-native/services/Migrator'
6768
export { nop } from './react-native/utilities/nop'
6869
export { PermissionHelper } from './react-native/services/PermissionHelper'
6970
export { PictureHelper } from './react-native/services/PictureHelper'
@@ -132,6 +133,9 @@ export type { KeyableTableCell } from './react-native/types/KeyableTableCell'
132133
export type { LimitedHeightProps } from './react-native/types/LimitedHeightProps'
133134
export type { LimitedWidthProps } from './react-native/types/LimitedWidthProps'
134135
export type { LoggerInterface } from './react-native/types/LoggerInterface'
136+
export type { MigratableState } from './react-native/types/MigratableState'
137+
export type { MigrationList } from './react-native/types/MigrationList'
138+
export type { MigratorInterface } from './react-native/types/MigratorInterface'
135139
export type { MinimumHeightProps } from './react-native/types/MinimumHeightProps'
136140
export type { MinimumWidthProps } from './react-native/types/MinimumWidthProps'
137141
export type { NonKeyableTableCell } from './react-native/types/NonKeyableTableCell'

package-lock.json

Lines changed: 19 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"sideEffects": false,
6060
"dependencies": {
6161
"events": "3.3.0",
62-
"expo": "~51.0.22",
62+
"expo": "~51.0.24",
6363
"expo-constants": "~16.0.2",
6464
"expo-crypto": "~13.0.2",
6565
"expo-file-system": "~17.0.1",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { MigratableState } from '../../types/MigratableState'
2+
import type { MigrationList } from '../../types/MigrationList'
3+
import type { MigratorInterface } from '../../types/MigratorInterface'
4+
import type { Json } from '../../types/Json'
5+
6+
/**
7+
* Executes migrations.
8+
* @template T The data to be migrated.
9+
*/
10+
export class Migrator<T> implements MigratorInterface<T> {
11+
/**
12+
* @param migrationList The migrations to execute.
13+
*/
14+
constructor (private readonly migrationList: MigrationList) {}
15+
16+
executionRequired (state: MigratableState<T>): boolean {
17+
return !Object.prototype.hasOwnProperty.call(state, 'executedMigrationUuids') ||
18+
state.executedMigrationUuids === undefined ||
19+
this.migrationList.some(migration => !(state.executedMigrationUuids as readonly string[]).includes(migration[0]))
20+
}
21+
22+
execute (state: MigratableState<T>): MigratableState<T> {
23+
let initial = state
24+
25+
if (!Object.prototype.hasOwnProperty.call(initial, 'executedMigrationUuids') || initial.executedMigrationUuids === undefined) {
26+
initial = { ...initial, executedMigrationUuids: [] }
27+
}
28+
29+
let next = initial as { readonly executedMigrationUuids: readonly string[] } & Readonly<Record<string | number, Json>>
30+
31+
for (const migration of this.migrationList) {
32+
if (!next.executedMigrationUuids.includes(migration[0])) {
33+
next = {
34+
...migration[1](next),
35+
executedMigrationUuids: [
36+
...next.executedMigrationUuids,
37+
migration[0]
38+
]
39+
}
40+
}
41+
}
42+
43+
return next as MigratableState<T>
44+
}
45+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# `react-native-app-helpers/Migrator`
2+
3+
Executes migrations.
4+
5+
## Usage
6+
7+
```tsx
8+
import { Migrator, MigratableState } from "react-native-app-helpers";
9+
10+
type State = { readonly items: ReadonlyArray<number> }
11+
12+
const state: MigratableState<State> = {
13+
executedMigrationUuids: ['1b69c28f-454e-4511-aa05-596fe5ae23a8']
14+
}
15+
16+
const migrator = new Migrator<State>([
17+
['b4dac8cd-af18-4e7d-a723-27f61d368228', (previous) => ({
18+
...previous,
19+
items: [...previous.items, 1],
20+
})],
21+
['1b69c28f-454e-4511-aa05-596fe5ae23a8', (previous) => ({
22+
...previous,
23+
items: [...previous.items, 2],
24+
})],
25+
['b07bc75d-1ba2-4bf7-b510-51a93d554a56', (previous) => ({
26+
...previous,
27+
items: [...previous.items, 3],
28+
})],
29+
]);
30+
31+
console.log(migrator.executionRequired(state));
32+
console.log(migrator.execute(state));
33+
```
34+
35+
## Interface
36+
37+
This package also exports a `MigratorInterface` type which can be used to
38+
substitute other types in place of this class (for unit tests, for example).
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Migrator } from '../../..'
2+
3+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
4+
type State = { readonly items: readonly number[] }
5+
6+
const migrator = new Migrator<State>([
7+
['b4dac8cd-af18-4e7d-a723-27f61d368228', (previous) => ({
8+
...previous,
9+
items: [...(previous['items'] as readonly number[]), 1]
10+
})],
11+
['1b69c28f-454e-4511-aa05-596fe5ae23a8', (previous) => ({
12+
...previous,
13+
items: [...(previous['items'] as readonly number[]), 2]
14+
})],
15+
['b07bc75d-1ba2-4bf7-b510-51a93d554a56', (previous) => ({
16+
...previous,
17+
items: [...(previous['items'] as readonly number[]), 3],
18+
executedMigrationUuids: 'overwritten'
19+
})]
20+
])
21+
22+
test('execution is required when migrations do not exist', () => {
23+
expect(migrator.executionRequired({
24+
items: []
25+
})).toBeTruthy()
26+
})
27+
28+
test('execution is not required when all migrations have executed', () => {
29+
expect(migrator.executionRequired({
30+
executedMigrationUuids: ['1b69c28f-454e-4511-aa05-596fe5ae23a8', 'b4dac8cd-af18-4e7d-a723-27f61d368228', 'b07bc75d-1ba2-4bf7-b510-51a93d554a56'],
31+
items: []
32+
})).toBeFalsy()
33+
})
34+
35+
test('execution is required when at least one migration has not been executed', () => {
36+
expect(migrator.executionRequired({
37+
executedMigrationUuids: ['1b69c28f-454e-4511-aa05-596fe5ae23a8'],
38+
items: []
39+
})).toBeTruthy()
40+
})
41+
42+
test('all unexecuted migrations are ran in order', () => {
43+
expect(migrator.execute({
44+
executedMigrationUuids: ['1b69c28f-454e-4511-aa05-596fe5ae23a8'],
45+
items: []
46+
})).toEqual({
47+
executedMigrationUuids: ['1b69c28f-454e-4511-aa05-596fe5ae23a8', 'b4dac8cd-af18-4e7d-a723-27f61d368228', 'b07bc75d-1ba2-4bf7-b510-51a93d554a56'],
48+
items: [1, 3]
49+
})
50+
})
51+
52+
test('all migrations are ran in order', () => {
53+
expect(migrator.execute({
54+
items: []
55+
})).toEqual({
56+
executedMigrationUuids: ['b4dac8cd-af18-4e7d-a723-27f61d368228', '1b69c28f-454e-4511-aa05-596fe5ae23a8', 'b07bc75d-1ba2-4bf7-b510-51a93d554a56'],
57+
items: [1, 2, 3]
58+
})
59+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Represents data which can have migrations ran against it.
3+
* @template T The data which can have migrations ran against it.
4+
*/
5+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
6+
export type MigratableState<T> = {
7+
/**
8+
* The UUIDs of all executed migrations.
9+
*/
10+
readonly executedMigrationUuids?: readonly string[]
11+
} & T
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# `react-native-app-helpers/MigratableState`
2+
3+
Represents data which can have migrations ran against it.
4+
5+
## Usage
6+
7+
```tsx
8+
import type { MigratableState } from "react-native-app-helpers";
9+
10+
type ExampleData = {
11+
readonly exampleKey: 'exampleValue';
12+
};
13+
14+
const example: MigratableState<ExampleData> = {
15+
exampleKey: 'exampleValue',
16+
executedMigrationUuids: [],
17+
};
18+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Json } from '../Json'
2+
3+
/**
4+
* A list of migrations to be ran in order.
5+
*/
6+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
7+
export type MigrationList = ReadonlyArray<readonly [string, (previous: Readonly<Record<string | number, Json>>) => Readonly<Record<string | number, Json>>]>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# `react-native-app-helpers/MigrationList`
2+
3+
A list of migrations to be ran in order.
4+
5+
## Usage
6+
7+
```tsx
8+
import type { MigrationList } from "react-native-app-helpers";
9+
10+
const migrationList: MigrationList = [
11+
['b4dac8cd-af18-4e7d-a723-27f61d368228', (previous) => ({
12+
...previous,
13+
exampleChangeAKey: 'exampleChangeAValue',
14+
})],
15+
['1b69c28f-454e-4511-aa05-596fe5ae23a8', (previous) => ({
16+
...previous,
17+
exampleChangeBKey: 'exampleChangeBValue',
18+
})],
19+
['b07bc75d-1ba2-4bf7-b510-51a93d554a56', (previous) => ({
20+
...previous,
21+
exampleChangeCKey: 'exampleChangeCValue',
22+
})],
23+
]
24+
```

0 commit comments

Comments
 (0)