feat: add deep merge and validation utility functions to ConfigManager
This commit adds utility functions to support partial settings save functionality: - isObject: Type guard for plain objects - deepMerge: Recursively merges objects, preserving unspecified fields - validateEditableFields: Validates settings against UI editable field whitelist - UI_EDITABLE_FIELDS: Whitelist of fields modifiable through UI A failing test is included to verify the deep merge behavior. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,73 @@ const DEFAULT_SETTINGS: SettingsData = {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if value is a plain object
|
||||
*/
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects, only updating fields present in target
|
||||
* Preserves all fields from source that are not in target
|
||||
*/
|
||||
function deepMerge<T>(source: T, target: Partial<T>): T {
|
||||
const result = { ...source }
|
||||
|
||||
for (const key in target) {
|
||||
if (key in target) {
|
||||
const targetValue = target[key]
|
||||
const sourceValue = result[key]
|
||||
|
||||
if (isObject(targetValue) && isObject(sourceValue)) {
|
||||
result[key] = deepMerge(sourceValue, targetValue)
|
||||
} else if (targetValue !== undefined) {
|
||||
result[key] = targetValue as T[Extract<keyof T, string>]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* UI editable field whitelist
|
||||
* Fields that can be modified through the settings UI
|
||||
*/
|
||||
const UI_EDITABLE_FIELDS: string[] = [
|
||||
'erp.url',
|
||||
'erp.username',
|
||||
'erp.password',
|
||||
// Add more fields as UI expands
|
||||
]
|
||||
|
||||
/**
|
||||
* Validate that settings only contain editable fields
|
||||
*/
|
||||
function validateEditableFields(settings: Partial<SettingsData>): {
|
||||
valid: boolean
|
||||
invalidFields: string[]
|
||||
} {
|
||||
const invalidFields: string[] = []
|
||||
|
||||
for (const [section, values] of Object.entries(settings)) {
|
||||
if (values && typeof values === 'object') {
|
||||
for (const field of Object.keys(values)) {
|
||||
const fieldPath = `${section}.${field}`
|
||||
if (!UI_EDITABLE_FIELDS.includes(fieldPath)) {
|
||||
invalidFields.push(fieldPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: invalidFields.length === 0,
|
||||
invalidFields
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration Manager Class
|
||||
*/
|
||||
|
||||
43
tests/main/services/config/config-manager.test.ts
Normal file
43
tests/main/services/config/config-manager.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { ConfigManager } from '@services/config/config-manager'
|
||||
import type { SettingsData } from '@types/settings.types'
|
||||
|
||||
describe('ConfigManager - deep merge utilities', () => {
|
||||
it('should deep merge objects, updating only specified fields', async () => {
|
||||
const manager = ConfigManager.getInstance()
|
||||
await manager.initialize()
|
||||
|
||||
// Setup initial state
|
||||
const initial: SettingsData = {
|
||||
erp: { url: 'http://old.com', username: 'user1', password: 'pass1', headless: true, ignoreHttpsErrors: true, autoCloseBrowser: true },
|
||||
database: { dbType: 'mysql', server: '', mysqlHost: 'localhost', mysqlPort: 3306, database: 'db', username: 'user', password: '' },
|
||||
paths: { dataDir: '/data', defaultOutput: 'out.xlsx', validationOutput: 'validation.xlsx' },
|
||||
extraction: { batchSize: 100, verbose: true, autoConvert: true, mergeBatches: true, enableDbPersistence: true },
|
||||
validation: { dataSource: 'database_full', batchSize: 2000, matchMode: 'substring', enableCrud: false, defaultManager: '' },
|
||||
ui: { fontFamily: 'Arial', fontSize: 12, productionIdInputWidth: 20 },
|
||||
execution: { dryRun: false }
|
||||
}
|
||||
|
||||
// Load initial settings
|
||||
await manager.saveAllSettings(initial)
|
||||
|
||||
// Partial update
|
||||
const partial = {
|
||||
erp: { url: 'http://new.com' }
|
||||
}
|
||||
|
||||
const result = await manager.savePartialSettings(partial)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const current = manager.getAllSettings()
|
||||
|
||||
// Updated field
|
||||
expect(current.erp.url).toBe('http://new.com')
|
||||
|
||||
// Preserved fields
|
||||
expect(current.erp.username).toBe('user1')
|
||||
expect(current.database.dbType).toBe('mysql')
|
||||
expect(current.paths.dataDir).toBe('/data')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user