All files / main/services settingsService.ts

87.8% Statements 36/41
78.26% Branches 18/23
100% Functions 7/7
87.5% Lines 35/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97          1x                   1x 2x 2x         1x 1x     1x     1x   107x       7x 7x       1x 1x       11x       11x 12x         11x 11x   11x 75x 2x       9x 25x 9x     3x 1x     2x 2x   2x       2x       3x 3x 1x     2x 2x            
import { numericSettingsFields, sanitizeNumericSettings, validateNumericSettingValue, isPlainObject } from '@shared/settingsValidation'
import type { AppSettings } from '@shared/types'
import type { AutoLaunchPort } from '@main/ports/desktop'
import type { SettingsRepository } from '@main/ports/repositories'
 
const booleanFields = [
  'autoStartBreaks',
  'autoStartFocus',
  'notificationsEnabled',
  'soundEnabled',
  'openAtLogin',
  'startToTray',
  'closeToTray'
] as const
 
const validateBooleanField = (value: unknown, field: (typeof booleanFields)[number]): boolean => {
  Eif (typeof value !== 'boolean') {
    throw new Error(`${field} must be a boolean`)
  }
  return value
}
 
const validateThemePreference = (value: unknown): AppSettings['themePreference'] => {
  Iif (value === 'system' || value === 'light' || value === 'dark') {
    return value
  }
  throw new Error('themePreference must be one of system, light, dark')
}
 
const knownSettingsFields = [...booleanFields, ...numericSettingsFields, 'themePreference'] as const
 
const hasOwn = (value: Record<string, unknown>, key: string): boolean => Object.prototype.hasOwnProperty.call(value, key)
 
export class SettingsService {
  constructor(
    private readonly settings: SettingsRepository,
    private readonly autoLaunch: AutoLaunchPort
  ) {}
 
  async get(): Promise<AppSettings> {
    const current = await this.settings.get()
    return this.sanitizeStoredSettings(current)
  }
 
  async update(patch: Partial<AppSettings>): Promise<AppSettings> {
    Iif (!isPlainObject(patch)) {
      throw new Error('settings patch must be a plain object')
    }
 
    for (const field of Object.keys(patch)) {
      Iif (!(knownSettingsFields as readonly string[]).includes(field)) {
        throw new Error(`Unknown settings field: ${field}`)
      }
    }
 
    const nextPatch: Partial<AppSettings> = {}
    const patchRecord = patch as Record<string, unknown>
 
    for (const field of booleanFields) {
      if (hasOwn(patchRecord, field)) {
        nextPatch[field] = validateBooleanField(patchRecord[field], field)
      }
    }
 
    for (const field of numericSettingsFields) {
      if (hasOwn(patchRecord, field)) {
        nextPatch[field] = validateNumericSettingValue(patchRecord[field], field)
      }
    }
    if (hasOwn(patchRecord, 'themePreference')) {
      nextPatch.themePreference = validateThemePreference(patchRecord.themePreference)
    }
 
    const updated = await this.settings.update(nextPatch)
    const sanitizedUpdated = await this.sanitizeStoredSettings(updated)
 
    Iif (hasOwn(patchRecord, 'openAtLogin') || hasOwn(patchRecord, 'startToTray')) {
      this.autoLaunch.setOpenAtLogin(sanitizedUpdated.openAtLogin, sanitizedUpdated.startToTray)
    }
 
    return sanitizedUpdated
  }
 
  private async sanitizeStoredSettings(settings: AppSettings): Promise<AppSettings> {
    const { normalized, patch } = sanitizeNumericSettings(settings as Record<(typeof numericSettingsFields)[number], unknown>)
    if (Object.keys(patch).length === 0) {
      return settings
    }
 
    await this.settings.update(patch)
    return {
      ...settings,
      ...normalized
    }
  }
}