/** * Helper utilities for handling settings with pending changes that require restart. * * Backend returns settings in this format: * { * "enableLogin": false, // Current active value * "csrfDisabled": true, * "_pending": { // Optional - only present if there are pending changes * "enableLogin": true // Value that will be active after restart * } * } */ export interface SettingsWithPending { _pending?: Partial; [key: string]: any; } /** * Merge pending changes into the settings object. * Returns a new object with pending values overlaid on top of current values. * * @param settings Settings object from backend (may contain _pending block) * @returns Merged settings with pending values applied */ export function mergePendingSettings(settings: T): Omit { if (!settings || !settings._pending) { // No pending changes, return as-is (without _pending property) const { _pending, ...rest } = settings || {}; return rest as Omit; } // Deep merge pending changes const merged = deepMerge(settings, settings._pending); // Remove _pending from result const { _pending, ...result } = merged; return result as Omit; } /** * Check if a specific field has a pending change awaiting restart. * * @param settings Settings object from backend * @param fieldPath Dot-notation path to the field (e.g., "oauth2.clientSecret") * @returns True if field has pending changes */ export function isFieldPending( settings: T | null | undefined, fieldPath: string ): boolean { if (!settings?._pending) { console.log(`[isFieldPending] No _pending block found for field: ${fieldPath}`); return false; } // Navigate the pending object using dot notation const value = getNestedValue(settings._pending, fieldPath); const isPending = value !== undefined; if (isPending) { console.log(`[isFieldPending] Field ${fieldPath} IS pending with value:`, value); } return isPending; } /** * Check if there are any pending changes in the settings. * * @param settings Settings object from backend * @returns True if there are any pending changes */ export function hasPendingChanges( settings: T | null | undefined ): boolean { return settings?._pending !== undefined && Object.keys(settings._pending).length > 0; } /** * Get the pending value for a specific field, or undefined if no pending change. * * @param settings Settings object from backend * @param fieldPath Dot-notation path to the field * @returns Pending value or undefined */ export function getPendingValue( settings: T | null | undefined, fieldPath: string ): any { if (!settings?._pending) { return undefined; } return getNestedValue(settings._pending, fieldPath); } /** * Get the current active value for a field (ignoring pending changes). * * @param settings Settings object from backend * @param fieldPath Dot-notation path to the field * @returns Current active value */ export function getCurrentValue( settings: T | null | undefined, fieldPath: string ): any { if (!settings) { return undefined; } // Get from settings, ignoring _pending const { _pending, ...activeSettings } = settings; return getNestedValue(activeSettings, fieldPath); } // ========== Helper Functions ========== /** * Deep merge two objects. Second object takes priority. */ function deepMerge(target: any, source: any): any { if (!source) return target; if (!target) return source; const result = { ...target }; for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { const sourceValue = source[key]; const targetValue = result[key]; if (isPlainObject(sourceValue) && isPlainObject(targetValue)) { result[key] = deepMerge(targetValue, sourceValue); } else { result[key] = sourceValue; } } } return result; } /** * Get nested value using dot notation. */ function getNestedValue(obj: any, path: string): any { if (!obj || !path) return undefined; const parts = path.split('.'); let current = obj; for (const part of parts) { if (current === null || current === undefined) { return undefined; } current = current[part]; } return current; } /** * Check if value is a plain object (not array, not null, not Date, etc.) */ function isPlainObject(value: any): boolean { return ( value !== null && typeof value === 'object' && value.constructor === Object ); }