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
|
* 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