Skip to content

Commit

Permalink
feat: settings migration (#371)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristianoventura authored Dec 4, 2024
1 parent 7bb06aa commit 0415013
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/components/Settings/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react'
import { FormProvider, useForm } from 'react-hook-form'

import { ProxySettings } from './ProxySettings'
import { AppSettingsSchema } from '@/schemas/appSettings'
import { AppSettingsSchema } from '@/schemas/settings'
import { RecorderSettings } from './RecorderSettings'
import { AppSettings } from '@/types/settings'
import { UsageReportSettings } from './UsageReportSettings'
Expand Down
27 changes: 27 additions & 0 deletions src/schemas/settings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Settings migration

Migrations are needed when changing the structure of existing settings file such as renaming or deleting keys.

If you're simply adding new options to the settings file, a migration is not needed. In this case, extend the latest schema available.

## Creating a new migration

For when a new migration is needed:

1. Create a directory for the new version (e.g. `/v2`).
2. Declare the new schema with appropriate changes.
3. In `/v2/index.ts`, implement a `migrate` function that takes a `v2` schema and returns a `v3` schema.
4. Update `/schemas/settings/index.ts` to use the new version:

```ts
function migrate(settings: z.infer<typeof AnySettingSchema>) {

case '2.0':
return migrate(v2.migrate(settings))

}
```

5. Export types from the new version in `/schemas/settings/index.ts`.
6. Update the default settings in `src/settings.ts` according to the new schema.
7. Make changes to the remaining implementation to use the new schema.
34 changes: 34 additions & 0 deletions src/schemas/settings/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { migrate } from '.'
import * as v1 from './v1'

describe('Settings migration', () => {
it('should migrate from v1 to latest', () => {
const v1Settings: v1.AppSettings = {
version: '1.0',
proxy: {
mode: 'regular',
port: 6000,
automaticallyFindPort: true,
},
recorder: {
detectBrowserPath: true,
},
windowState: {
width: 1200,
height: 800,
x: 0,
y: 0,
isMaximized: true,
},
usageReport: {
enabled: true,
},
appearance: {
theme: 'system',
},
}

expect(migrate(v1Settings).version).toBe('2.0')
})
})
30 changes: 30 additions & 0 deletions src/schemas/settings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { z } from 'zod'
import * as v1 from './v1'
import * as v2 from './v2'
import { exhaustive } from '../../utils/typescript'

const AnySettingSchema = z.discriminatedUnion('version', [
v1.AppSettingsSchema,
v2.AppSettingsSchema,
])

export function migrate(settings: z.infer<typeof AnySettingSchema>) {
switch (settings.version) {
case '1.0':
return migrate(v1.migrate(settings))
case '2.0':
return settings
default:
return exhaustive(settings)
}
}

export const AppSettingsSchema = AnySettingSchema.transform(migrate)

export {
AppearanceSchema,
ProxySettingsSchema,
RecorderSettingsSchema,
UsageReportSettingsSchema,
WindowStateSchema,
} from './v2'
23 changes: 20 additions & 3 deletions src/schemas/appSettings.ts → src/schemas/settings/v1/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from 'zod'
import * as v2 from '../v2'

export const RegularProxySettingsSchema = z.object({
mode: z.literal('regular'),
Expand Down Expand Up @@ -86,23 +87,39 @@ export const RecorderSettingsSchema = z
}
})

const WindowStateSchema = z.object({
export const WindowStateSchema = z.object({
x: z.number().int(),
y: z.number().int(),
width: z.number().int(),
height: z.number().int(),
isMaximized: z.boolean(),
})

const AppearanceSchema = z.object({
export const AppearanceSchema = z.object({
theme: z.union([z.literal('light'), z.literal('dark'), z.literal('system')]),
})

export const AppSettingsSchema = z.object({
version: z.string(),
version: z.literal('1.0'),
proxy: ProxySettingsSchema,
recorder: RecorderSettingsSchema,
windowState: WindowStateSchema,
usageReport: UsageReportSettingsSchema,
appearance: AppearanceSchema,
})

// Migrate settings to the next version
export function migrate(
settings: z.infer<typeof AppSettingsSchema>
): v2.AppSettings {
return {
version: '2.0',
proxy: settings.proxy,
recorder: settings.recorder,
windowState: settings.windowState,
usageReport: settings.usageReport,
appearance: settings.appearance,
}
}

export type AppSettings = z.infer<typeof AppSettingsSchema>
32 changes: 32 additions & 0 deletions src/schemas/settings/v2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { z } from 'zod'
import {
AppearanceSchema,
ProxySettingsSchema,
RecorderSettingsSchema,
UsageReportSettingsSchema,
WindowStateSchema,
} from '../v1'

export {
AppearanceSchema,
ProxySettingsSchema,
RecorderSettingsSchema,
UsageReportSettingsSchema,
WindowStateSchema,
}

export const AppSettingsSchema = z.object({
version: z.literal('2.0'),
proxy: ProxySettingsSchema,
recorder: RecorderSettingsSchema,
windowState: WindowStateSchema,
usageReport: UsageReportSettingsSchema,
appearance: AppearanceSchema,
})

export type AppSettings = z.infer<typeof AppSettingsSchema>

// TODO: Migrate settings to the next version
export function migrate(settings: z.infer<typeof AppSettingsSchema>) {
return { ...settings }
}
4 changes: 2 additions & 2 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { app, dialog } from 'electron'
import { writeFile, open } from 'fs/promises'
import path from 'node:path'
import { AppSettings } from './types/settings'
import { AppSettingsSchema } from './schemas/appSettings'
import { AppSettingsSchema } from './schemas/settings'
import { existsSync, readFileSync } from 'fs'
import { safeJsonParse } from './utils/json'
import log from 'electron-log/main'

const defaultSettings: AppSettings = {
version: '1.0',
version: '2.0',
proxy: {
mode: 'regular',
port: 6000,
Expand Down
2 changes: 1 addition & 1 deletion src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
ProxySettingsSchema,
RecorderSettingsSchema,
UsageReportSettingsSchema,
} from '@/schemas/appSettings'
} from '@/schemas/settings'
import { z } from 'zod'

export type AppSettings = z.infer<typeof AppSettingsSchema>
Expand Down

0 comments on commit 0415013

Please sign in to comment.