Files
CherryHQ-cherry-studio/docs/references/data/best-practice-layered-preset-pattern.md
fullex d06302db48 refactor(shared): rename preset & codeLanguages files to camelCase per naming-conventions §3.2
`.ts` files under `src/shared` must use camelCase (naming-conventions §3.2);
kebab-case is only sanctioned under `packages/ui/` and `src/renderer/routes/`.
The `presets/` kebab naming came from best-practice-layered-preset-pattern.md,
which predated and conflicted with the authoritative spec.

- Rename presets/{code-cli,default-assistant,file-processing,mini-apps,
  translate-languages,web-search-providers}.ts and utils/code-languages.ts
  (plus the two matching __tests__ files) to camelCase, and update all importers
- Fix the upstream generator scripts/update-languages.ts to emit
  codeLanguages.ts; otherwise `pnpm update:languages` would recreate the
  kebab-named file
- Correct best-practice-layered-preset-pattern.md (kebab -> camelCase) and link
  it to naming-conventions §3.2 so it cannot drift again
- Fix two stale `types/file` path references in file/architecture.md
2026-06-19 22:08:49 -07:00

11 KiB

Layered Preset Configuration Pattern

The Layered Preset Configuration pattern is the recommended approach for handling scenarios where you have predefined configurations that users can partially customize.

When to Use This Pattern

Use this pattern when:

  • You have a list of predefined configurations (e.g., providers, templates, presets)
  • Presets can be updated through app updates (add/remove/modify)
  • Users can customize some fields of individual presets
  • User customizations should persist while still receiving preset updates

Architecture

┌─────────────────────────────┐
│     Runtime Config          │  ← Merged complete configuration
├─────────────────────────────┤
│   User Overrides Layer      │  ← User modifications (delta only)
├─────────────────────────────┤
│   Default Presets Layer     │  ← Predefined configurations
└─────────────────────────────┘

Key benefits:

  • Presets can be updated independently
  • User modifications are preserved
  • Only differences are stored, minimizing storage
  • Runtime merging provides the complete picture

Storage Strategy

Scenario Recommended Storage
Large item count (dozens+) + critical data Dedicated SQLite table
Small item count (< 20) Preference storage

For large-scale scenarios, the layered pattern still applies, but implementation details vary based on data characteristics. Refer to DataApi documentation for guidance.

Large-Scale Scenario: SQLite + Registry Service

For large item counts (dozens+) backed by SQLite, the layered merge is handled by a Registry Service in src/main/data/services/. The Registry Service reads preset data from a package or shared constants, obtains user overrides from the owning Entity Service, and returns merged results. It does not access the database directly.

This document focuses on small-scale scenarios using Preference storage.

Where preset-only fields merge

SQLite-backed entities have three field classes:

Class Owns Runtime location
User-editable DB row Written via PATCH /:resource/:id
Runtime default Code constants Merged in rowToEntity
Preset-only static Registry package Merged in rowToEntity

Preset-only static fields — websites, description, iconUrl, vendor links — have no DB column. The Registry Service looks them up by preset key during rowToEntity and folds them into the runtime entity. A single GET /:resource/:id returns the complete object.

Do not split preset-only fields into a parallel endpoint. A separate GET /:resource/:id/preset-metadata forces every consumer to issue two requests for one logical entity, fragments the type (Entity + EntityPresetMetadata), and duplicates the merge contract. Merge at the rowToEntity seam — that is where the entity becomes runtime-shaped.

Acceptable exceptions:

  1. Preset payload is not 1:1 with the entity (e.g. GET /catalog browsed before creating a row — nothing to merge against).
  2. Field set is large and consumed by only one specialised surface — pay the second request there, named as GET /:resource/:id:full-metadata so the relationship to the parent stays explicit.

When in doubt: merge.

Preset File Standards

Location

All preset configurations should be placed in:

src/shared/data/presets/

File Format

Use .ts files (not JSON):

  • Small configurations typically don't need online update services
  • TypeScript provides type safety
  • Types and data can be co-located

Naming Convention

File names follow the repo-wide naming-conventions.md §3.2 camelCase rule for .ts source — presets/ is not one of the kebab-case exceptions (those are limited to packages/ui/ and src/renderer/routes/).

Element Convention Example
File name camelCase selectionActions.ts
Constant name SCREAMING_SNAKE_CASE with PRESETS_ prefix PRESETS_SELECTION_ACTIONS

Naming correspondence:

  • providers.tsPRESETS_PROVIDERS
  • selectionActions.tsPRESETS_SELECTION_ACTIONS
  • aiModels.tsPRESETS_AI_MODELS

File Structure

A preset file should contain both type definitions and preset data:

Note: The Provider example below is for illustration purposes only and does not represent the actual provider implementation in Cherry Studio. Your actual data structure will vary based on your specific requirements.

// src/shared/data/presets/providers.ts

// Type definitions
export interface Provider {
  id: string
  name: string
  apiHost: string
  models: string[]
}

// Preset data
export const PRESETS_PROVIDERS: Provider[] = [
  {
    id: 'openai',
    name: 'OpenAI',
    apiHost: 'https://api.openai.com',
    models: ['gpt-4', 'gpt-3.5-turbo'],
  },
  {
    id: 'anthropic',
    name: 'Anthropic',
    apiHost: 'https://api.anthropic.com',
    models: ['claude-3-opus', 'claude-3-sonnet'],
  },
  // ...
]

Implementation with Preference

Step 1: Define Override Type

In your preset file, define the override type that represents user-customizable fields:

// src/shared/data/presets/providers.ts

// User-overridable fields (exclude id since it's the identifier)
export type ProviderOverride = Partial<Omit<Provider, 'id'>>

// Map of provider id to its overrides
export type ProviderOverrides = Record<string, ProviderOverride>

Step 2: Register Preference Key

Add a preference key to store user overrides:

// src/shared/data/preference/preferenceSchemas.ts

export const DefaultPreferences = {
  default: {
    // ...existing preferences
    'providers.overrides': {} as ProviderOverrides,
  },
}

Step 3: Create Merge Hook

Create a custom hook that merges presets with user overrides:

Note: The hook below is a basic example. Your actual implementation should be tailored to your specific data structure and usage patterns. Consider factors like: which fields are user-editable, how merging should work for nested objects, whether you need filtering/sorting, etc.

// src/renderer/hooks/useProviders.ts

import { useCallback, useMemo } from 'react'

import { usePreference } from '@data/hooks/usePreference'
import { PRESETS_PROVIDERS, ProviderOverride } from '@shared/data/presets/providers'

export function useProviders() {
  const [overrides, setOverrides] = usePreference('providers.overrides')

  // Merge: presets + user overrides
  const providers = useMemo(() => {
    return PRESETS_PROVIDERS.map((preset) => ({
      ...preset,
      ...overrides[preset.id],
    }))
  }, [overrides])

  // Update specific fields of a provider
  const updateProvider = useCallback(
    (id: string, updates: ProviderOverride) => {
      setOverrides({
        ...overrides,
        [id]: { ...overrides[id], ...updates },
      })
    },
    [overrides, setOverrides]
  )

  // Reset a provider to default values
  const resetProvider = useCallback(
    (id: string) => {
      const { [id]: _, ...rest } = overrides
      setOverrides(rest)
    },
    [overrides, setOverrides]
  )

  // Check if a provider has been customized
  const isCustomized = useCallback((id: string) => id in overrides, [overrides])

  return {
    providers,
    updateProvider,
    resetProvider,
    isCustomized,
  }
}

Usage Example

function ProviderSettings() {
  const { providers, updateProvider, resetProvider, isCustomized } = useProviders()

  return (
    <>
      {providers.map((provider) => (
        <ProviderCard
          key={provider.id}
          provider={provider}
          isCustomized={isCustomized(provider.id)}
          onApiHostChange={(host) => updateProvider(provider.id, { apiHost: host })}
          onReset={() => resetProvider(provider.id)}
        />
      ))}
    </>
  )
}

Pure Presets (No User Override)

For presets that don't require user customization, simply place them in the presets directory and import directly:

// src/shared/data/presets/languages.ts

export interface Language {
  code: string
  name: string
  nativeName: string
}

export const PRESETS_LANGUAGES: Language[] = [
  { code: 'en', name: 'English', nativeName: 'English' },
  { code: 'zh', name: 'Chinese', nativeName: '中文' },
  { code: 'ja', name: 'Japanese', nativeName: '日本語' },
  // ...
]

Usage:

import { PRESETS_LANGUAGES } from '@shared/data/presets/languages'

function LanguageSelector() {
  return (
    <select>
      {PRESETS_LANGUAGES.map((lang) => (
        <option key={lang.code} value={lang.code}>
          {lang.nativeName}
        </option>
      ))}
    </select>
  )
}

Update Compatibility

The layered pattern ensures smooth updates:

Operation Behavior
Add new preset Automatically appears in the list (no override)
Remove preset Disappears from list; override data retained (harmless)
Modify preset field User-overridden fields keep user values; others update

Example scenario:

  1. App ships with PRESETS_PROVIDERS containing OpenAI with apiHost: 'https://api.openai.com'
  2. User changes OpenAI's apiHost to 'https://my-proxy.com'
  3. App update changes OpenAI's models array
  4. Result: User keeps their custom apiHost, but gets the new models

Versioning for Complex Presets

For presets with frequent configuration changes, consider adding a version field to facilitate migration management:

// src/shared/data/presets/complexConfig.ts

export const PRESETS_COMPLEX_CONFIG_VERSION = 2

export interface ComplexConfig {
  id: string
  // ... other fields
}

export const PRESETS_COMPLEX_CONFIG: ComplexConfig[] = [
  // ...
]

Store the version alongside user overrides in a single object:

// preferenceSchemas.ts
'complex_config.overrides': {
  version: 0,
  data: {}
} as { version: number; data: ComplexConfigOverrides },

When the preset version changes, implement migration logic to transform the stored overrides to the new format.

This approach is recommended when:

  • Configuration structure changes frequently
  • Field names or types may be renamed/changed
  • You need to clean up deprecated override data