/** * Основной класс мигратора конфигураций виджета чата */ import { MigrationResult, MigrationContext, MigrationOptions, MigrationReport, MigrationLogger, ConfigVersion, CONFIG_VERSIONS } from './types' import { WidgetConfig } from '../config.types' import { getStrategiesForMigration } from './strategies' import { DefaultVersionDetector, CommandFactory } from './commands' /** Дефолтный логгер для миграции */ export class DefaultMigrationLogger implements MigrationLogger { constructor(private verbose: boolean = false) {} info(message: string, context?: any): void { if (this.verbose) { console.log(`[INFO] ${message}`, context ? JSON.stringify(context, null, 2) : '') } } warn(message: string, context?: any): void { console.warn(`[WARN] ${message}`, context ? JSON.stringify(context, null, 2) : '') } error(message: string, context?: any): void { console.error(`[ERROR] ${message}`, context ? JSON.stringify(context, null, 2) : '') } debug(message: string, context?: any): void { if (this.verbose) { console.debug(`[DEBUG] ${message}`, context ? JSON.stringify(context, null, 2) : '') } } } /** Основной класс мигратора */ export class ConfigMigrator { private logger: MigrationLogger private versionDetector: DefaultVersionDetector constructor(logger?: MigrationLogger) { this.logger = logger || new DefaultMigrationLogger() this.versionDetector = new DefaultVersionDetector() } private setSchemaVersion(config: any, version: ConfigVersion): void { if (!config || typeof config !== 'object') return const schema = (config as any).schema if (!schema || typeof schema !== 'object') { // Keep shape compatible with ConfigSchema from config.types.ts ;(config as any).schema = { version, required: [] } return } ;(schema as any).version = version if (!Array.isArray((schema as any).required)) { ;(schema as any).required = [] } } /** * Выполнить миграцию конфигурации */ async migrate( config: any, targetVersion: ConfigVersion, options: MigrationOptions = {} ): Promise> { const startTime = Date.now() this.logger.info(`Начинаем миграцию к версии ${targetVersion}`) try { // Детекция текущей версии const currentVersion = this.versionDetector.detect(config) if (!currentVersion) { return this.createErrorResult('Не удалось определить версию исходной конфигурации', startTime) } this.logger.info(`Определена исходная версия: ${currentVersion}`) // Проверяем, нужна ли миграция if (currentVersion === targetVersion) { this.logger.info('Конфигурация уже соответствует целевой версии') return this.createSuccessResult(config as T, currentVersion, targetVersion, [], startTime) } // Создаем план миграции const migrationPath = this.buildMigrationPath(currentVersion, targetVersion) if (migrationPath.length === 0) { return this.createErrorResult( `Не найден путь миграции с ${currentVersion} на ${targetVersion}`, startTime ) } this.logger.info(`План миграции: ${migrationPath.map(step => `${step.from} -> ${step.to}`).join(' -> ')}`) // Выполняем миграцию по шагам let currentConfig = config const appliedStrategies: string[] = [] const allWarnings: string[] = [] for (const step of migrationPath) { const stepResult = await this.executeStep(currentConfig, step.from, step.to, options) if (!stepResult.success) { return this.createErrorResult( `Ошибка на шаге ${step.from} -> ${step.to}: ${stepResult.errors.join(', ')}`, startTime, stepResult.errors ) } currentConfig = stepResult.data appliedStrategies.push(...stepResult.appliedStrategies) allWarnings.push(...stepResult.warnings) } // Ensure schema version matches the target after successful migration steps. this.setSchemaVersion(currentConfig, targetVersion) // Финальная валидация const validateCommand = CommandFactory.createCommand('ValidateConfig') if (validateCommand) { const validationResult = validateCommand.execute(currentConfig, { version: targetVersion }) if (!validationResult.success && options.strict) { return this.createErrorResult( `Валидация не прошла: ${validationResult.errors.join(', ')}`, startTime, validationResult.errors ) } allWarnings.push(...validationResult.warnings) } this.logger.info('Миграция успешно завершена') return this.createSuccessResult( currentConfig as T, currentVersion, targetVersion, appliedStrategies, startTime, allWarnings ) } catch (error) { this.logger.error('Критическая ошибка при миграции', error) return this.createErrorResult(`Критическая ошибка: ${error}`, startTime) } } /** * Получить отчет о возможной миграции без её выполнения */ async dryRun( config: any, targetVersion: ConfigVersion, _options: MigrationOptions = {} ): Promise { const startTime = Date.now() try { const currentVersion = this.versionDetector.detect(config) if (!currentVersion) { return this.createFailedReport(startTime, 'unknown', targetVersion, 'Не удалось определить версию') } const migrationPath = this.buildMigrationPath(currentVersion, targetVersion) const strategies = migrationPath.flatMap(step => getStrategiesForMigration(step.from, step.to) ) return { timestamp: new Date(), fromVersion: currentVersion, toVersion: targetVersion, success: true, duration: Date.now() - startTime, appliedStrategies: strategies.map(strategy => ({ name: strategy.name, success: true, errors: [], warnings: [] })), totalErrors: 0, totalWarnings: 0, summary: `Будет применено ${strategies.length} стратегий для миграции с ${currentVersion} на ${targetVersion}` } } catch (error) { return this.createFailedReport(startTime, 'unknown', targetVersion, `Ошибка: ${error}`) } } /** * Выполнить один шаг миграции */ private async executeStep( config: any, fromVersion: ConfigVersion, toVersion: ConfigVersion, options: MigrationOptions ): Promise> { const startTime = Date.now() this.logger.info(`Выполняем шаг: ${fromVersion} -> ${toVersion}`) // Получаем стратегии для этого шага let strategies = getStrategiesForMigration(fromVersion, toVersion) // Фильтруем стратегии согласно опциям if (options.onlyStrategies?.length) { strategies = strategies.filter(s => options.onlyStrategies!.includes(s.name)) } if (options.excludeStrategies?.length) { strategies = strategies.filter(s => !options.excludeStrategies!.includes(s.name)) } if (strategies.length === 0) { return this.createErrorResult( `Не найдено стратегий для миграции ${fromVersion} -> ${toVersion}`, startTime ) } const context: MigrationContext = { fromVersion, toVersion, config, options } let currentConfig = { ...config } const appliedStrategies: string[] = [] const allErrors: string[] = [] const allWarnings: string[] = [] // Применяем каждую стратегию for (const strategy of strategies) { this.logger.debug(`Применяем стратегию: ${strategy.name}`) if (!strategy.canApply({ ...context, config: currentConfig })) { this.logger.debug(`Стратегия ${strategy.name} не применима, пропускаем`) continue } const result = strategy.apply({ ...context, config: currentConfig }) if (!result.success) { this.logger.error(`Стратегия ${strategy.name} завершилась с ошибкой`, result.errors) allErrors.push(...result.errors) if (options.strict) { return this.createErrorResult( `Стратегия ${strategy.name} завершилась с ошибкой: ${result.errors.join(', ')}`, startTime, allErrors ) } } else { if (result.modified && result.data) { currentConfig = result.data } appliedStrategies.push(strategy.name) allWarnings.push(...result.warnings) this.logger.debug(`Стратегия ${strategy.name} успешно применена`) } } return this.createSuccessResult( currentConfig, fromVersion, toVersion, appliedStrategies, startTime, allWarnings, allErrors ) } /** * Построить путь миграции между версиями */ private buildMigrationPath(from: ConfigVersion, to: ConfigVersion): Array<{from: ConfigVersion, to: ConfigVersion}> { // Простая логика для последовательной миграции // В будущем можно расширить для более сложных сценариев const fromIndex = CONFIG_VERSIONS.indexOf(from) const toIndex = CONFIG_VERSIONS.indexOf(to) if (fromIndex === -1 || toIndex === -1) { return [] } const path: Array<{from: ConfigVersion, to: ConfigVersion}> = [] if (fromIndex < toIndex) { // Прямая миграция (вперед) for (let i = fromIndex; i < toIndex; i++) { path.push({ from: CONFIG_VERSIONS[i], to: CONFIG_VERSIONS[i + 1] }) } } else if (fromIndex > toIndex) { // Обратная миграция (назад) - пока не поддерживается // В будущем можно добавить стратегии downgrade return [] } return path } /** * Создать успешный результат миграции */ private createSuccessResult( data: T, fromVersion: ConfigVersion, toVersion: ConfigVersion, appliedStrategies: string[], _startTime: number, warnings: string[] = [], errors: string[] = [] ): MigrationResult { return { success: true, data, errors, warnings, fromVersion, toVersion, appliedStrategies } } /** * Создать результат с ошибкой */ private createErrorResult( message: string, _startTime: number, errors: string[] = [] ): MigrationResult { return { success: false, errors: [message, ...errors], warnings: [], fromVersion: 'unknown' as any, toVersion: 'unknown' as any, appliedStrategies: [] } } /** * Создать неудачный отчет */ private createFailedReport( _startTime: number, fromVersion: ConfigVersion | 'unknown', toVersion: ConfigVersion, message: string ): MigrationReport { return { timestamp: new Date(), fromVersion: fromVersion as ConfigVersion, toVersion, success: false, duration: Date.now() - _startTime, appliedStrategies: [], totalErrors: 1, totalWarnings: 0, summary: message } } }