docs/references/data/best-practice-layered-preset-pattern.md
The Layered Preset Configuration pattern is the recommended approach for handling scenarios where you have predefined configurations that users can partially customize.
Use this pattern when:
┌─────────────────────────────┐
│ Runtime Config │ ← Merged complete configuration
├─────────────────────────────┤
│ User Overrides Layer │ ← User modifications (delta only)
├─────────────────────────────┤
│ Default Presets Layer │ ← Predefined configurations
└─────────────────────────────┘
Key benefits:
| 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.
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.
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:
GET /catalog browsed
before creating a row — nothing to merge against).GET /:resource/:id:full-metadata
so the relationship to the parent stays explicit.When in doubt: merge.
All preset configurations should be placed in:
src/shared/data/presets/
Use .ts files (not JSON):
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.ts → PRESETS_PROVIDERSselectionActions.ts → PRESETS_SELECTION_ACTIONSaiModels.ts → PRESETS_AI_MODELSA preset file should contain both type definitions and preset data:
Note: The
Providerexample 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'],
},
// ...
]
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>
Add a preference key to store user overrides:
// src/shared/data/preference/preferenceSchemas.ts
export const DefaultPreferences = {
default: {
// ...existing preferences
'providers.overrides': {} as ProviderOverrides,
},
}
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,
}
}
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)}
/>
))}
</>
)
}
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>
)
}
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:
PRESETS_PROVIDERS containing OpenAI with apiHost: 'https://api.openai.com'apiHost to 'https://my-proxy.com'models arrayapiHost, but gets the new modelsFor 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: