Skip to content

Commit

Permalink
Add migration helper.
Browse files Browse the repository at this point in the history
  • Loading branch information
jameswilddev committed Aug 7, 2024
1 parent dce8cf9 commit c060939
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 1 deletion.
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { createImageBackgroundComponent } from './react-native/components/create
export { createInputComponent } from './react-native/components/createInputComponent'
export { createLimitedHeightComponent } from './react-native/components/createLimitedHeightComponent'
export { createLimitedWidthComponent } from './react-native/components/createLimitedWidthComponent'
export { createMigratorManagerComponent } from './react-native/components/createMigratorManagerComponent'
export { createMinimumHeightComponent } from './react-native/components/createMinimumHeightComponent'
export { createMinimumWidthComponent } from './react-native/components/createMinimumWidthComponent'
export { createNullableEmailInputComponent } from './react-native/components/createNullableEmailInputComponent'
Expand Down
49 changes: 49 additions & 0 deletions react-native/components/createMigratorManagerComponent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as React from 'react'
import type { Json } from '../../types/Json'
import type { MigratorInterface } from '../../types/MigratorInterface'
import type { MigratableState } from '../../types/MigratableState'

/**
* Creates a React component which automatically manages a migrator, displaying
* a loading screen and executing it if appropriate.
* @template T The type of state to migrate.
* @param migrator The migrator.
* @returns A React component which automatically manages the migrator,
* displaying a loading screen and executing it if appropriate.
*/
export const createMigratorManagerComponent = <T extends Readonly<Record<string | number, Json>>>(
migrator: MigratorInterface<T>
): React.FunctionComponent<{
/**
* The state to migrate.
*/
readonly state: MigratableState<T>

/**
* Called once migration completes.
* @param to The resulting state..
*/
readonly setState: (to: MigratableState<T>) => void

/**
* The JSX to display while the state is migrated.
*/
readonly migrating: JSX.Element

/**
* The JSX to display once the state is migrated.
*/
readonly ready: JSX.Element
}> => {
return ({ state, setState, migrating, ready }) => {
const executionRequired = migrator.executionRequired(state)

React.useEffect(() => {
if (executionRequired) {
setState(migrator.execute(state))
}
}, [executionRequired])

return executionRequired ? migrating : ready
}
}
49 changes: 49 additions & 0 deletions react-native/components/createMigratorManagerComponent/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# `react-native-app-helpers/createMigratorManagerComponent`

Creates a React component which automatically manages a migrator, displaying a
loading screen and executing it if appropriate.

## Usage

```tsx
import {
Migrator,
MigratableState,
createMigratorManagerComponent,
} from "react-native-app-helpers";

type State = { readonly items: ReadonlyArray<number> }

const migrator = new Migrator<State>([
['b4dac8cd-af18-4e7d-a723-27f61d368228', (previous) => ({
...previous,
items: [...previous.items, 1],
})],
['1b69c28f-454e-4511-aa05-596fe5ae23a8', (previous) => ({
...previous,
items: [...previous.items, 2],
})],
['b07bc75d-1ba2-4bf7-b510-51a93d554a56', (previous) => ({
...previous,
items: [...previous.items, 3],
})],
]);

const MigratorManager = createMigratorManagerComponent(migrator);

export default () => {
const [state, setState] = React.useState<MigratableState<State>>({
executedMigrationUuids: ['1b69c28f-454e-4511-aa05-596fe5ae23a8'],
items: [],
});

return (
<MigratorManager
state={state}
setState={setState}
migrating={<Text>Migrations are in progress...</Text>}
ready={<Text>All migrations have completed.</Text>}
/>
);
};
```
123 changes: 123 additions & 0 deletions react-native/components/createMigratorManagerComponent/unit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import * as React from 'react'
import { Text } from 'react-native'
import * as TestRenderer from 'react-test-renderer'
import { createMigratorManagerComponent, type MigratorInterface } from '../../..'

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type State = { readonly items: readonly number[] }

test('displays the migrating screen when no migrations are required', async () => {
const migrator: MigratorInterface<State> = {
executionRequired: jest.fn().mockReturnValue(true),
execute: jest.fn()
}
const MigratorManager = createMigratorManagerComponent(migrator)
const setState = jest.fn()

const renderer = TestRenderer.create(
<MigratorManager
state={{ items: [1, 2, 3] }}
setState={setState}
migrating={<Text>Migrating</Text>}
ready={<Text>Ready</Text>}
/>
)

expect(renderer.toTree()?.rendered).toEqual(
expect.objectContaining({
props: expect.objectContaining({
children: 'Migrating'
})
})
)

expect(setState).not.toHaveBeenCalled()
expect(migrator.executionRequired).toHaveBeenCalledTimes(1)
expect(migrator.executionRequired).toHaveBeenCalledWith({ items: [1, 2, 3] })
expect(migrator.execute).not.toHaveBeenCalled()

renderer.unmount()

await TestRenderer.act(async () => {
await new Promise((resolve) => setTimeout(resolve, 250))
})
})

test('executes migrations when required', async () => {
const migrator: MigratorInterface<State> = {
executionRequired: jest.fn().mockReturnValue(true),
execute: jest.fn().mockReturnValue({ items: [4, 5, 6] })
}
const MigratorManager = createMigratorManagerComponent(migrator)
const setState = jest.fn()

const renderer = TestRenderer.create(
<MigratorManager
state={{ items: [1, 2, 3] }}
setState={setState}
migrating={<Text>Migrating</Text>}
ready={<Text>Ready</Text>}
/>
)

expect(renderer.toTree()?.rendered).toEqual(
expect.objectContaining({
props: expect.objectContaining({
children: 'Migrating'
})
})
)

await TestRenderer.act(async () => {
await new Promise((resolve) => setTimeout(resolve, 250))
})

expect(migrator.executionRequired).toHaveBeenCalledTimes(1)
expect(migrator.execute).toHaveBeenCalledTimes(1)
expect(migrator.execute).toHaveBeenCalledWith({ items: [1, 2, 3] })
expect(setState).toHaveBeenCalledTimes(1)
expect(setState).toHaveBeenCalledWith({ items: [4, 5, 6] })

renderer.unmount()

await TestRenderer.act(async () => {
await new Promise((resolve) => setTimeout(resolve, 250))
})
})

test('displays the ready screen when no migrations are required', async () => {
const migrator: MigratorInterface<State> = {
executionRequired: jest.fn().mockReturnValue(false),
execute: jest.fn()
}
const MigratorManager = createMigratorManagerComponent(migrator)
const setState = jest.fn()

const renderer = TestRenderer.create(
<MigratorManager
state={{ items: [1, 2, 3] }}
setState={setState}
migrating={<Text>Migrating</Text>}
ready={<Text>Ready</Text>}
/>
)

expect(renderer.toTree()?.rendered).toEqual(
expect.objectContaining({
props: expect.objectContaining({
children: 'Ready'
})
})
)

renderer.unmount()

await TestRenderer.act(async () => {
await new Promise((resolve) => setTimeout(resolve, 250))
})

expect(setState).not.toHaveBeenCalled()
expect(migrator.executionRequired).toHaveBeenCalledTimes(1)
expect(migrator.executionRequired).toHaveBeenCalledWith({ items: [1, 2, 3] })
expect(migrator.execute).not.toHaveBeenCalled()
})
3 changes: 2 additions & 1 deletion react-native/services/Migrator/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { Migrator, MigratableState } from "react-native-app-helpers";
type State = { readonly items: ReadonlyArray<number> }

const state: MigratableState<State> = {
executedMigrationUuids: ['1b69c28f-454e-4511-aa05-596fe5ae23a8']
executedMigrationUuids: ['1b69c28f-454e-4511-aa05-596fe5ae23a8'],
items: [],
}

const migrator = new Migrator<State>([
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { createTextComponent } from "react-native-app-helpers";
- [createInputComponent](./react-native/components/createInputComponent/readme.md)
- [createLimitedHeightComponent](./react-native/components/createLimitedHeightComponent/readme.md)
- [createLimitedWidthComponent](./react-native/components/createLimitedWidthComponent/readme.md)
- [createMigratorManagerComponent](./react-native/components/createMigratorManagerComponent/readme.md)
- [createMinimumHeightComponent](./react-native/components/createMinimumHeightComponent/readme.md)
- [createMinimumWidthComponent](./react-native/components/createMinimumWidthComponent/readme.md)
- [createNullableEmailInputComponent](./react-native/components/createNullableEmailInputComponent/readme.md)
Expand Down

0 comments on commit c060939

Please sign in to comment.