import ConfigurationService from './configuration_service.js' import FirmwareCatalogService from './firmware_catalog_service.js' import type { CreateConfigurationDto } from './configuration_service.js' import type { CreateFirmwareEntryDto } from './firmware_catalog_service.js' import { Db } from 'mongodb' interface MigrationReport { discoveryMigrated: number discoverySkipped: number provisioningMigrated: number provisioningSkipped: number firmwareEntriesCreated: number errors: string[] } interface RollbackReport { discoveryRolledBack: number provisioningRolledBack: number errors: string[] } export default class MigrationService { constructor( private db: Db, private configService = new ConfigurationService(), private firmwareCatalogService = new FirmwareCatalogService(), ) {} /** * Migrate all legacy Discovery and Provisioning configs to unified Configuration format */ async migrateAll(): Promise { const report: MigrationReport = { discoveryMigrated: 0, discoverySkipped: 0, provisioningMigrated: 0, provisioningSkipped: 0, firmwareEntriesCreated: 0, errors: [], } try { // Migrate Discovery configs (inventory + maintain policy) const discoveryCollection = this.db.collection('discovery_configs') const discoveryConfigs = await discoveryCollection.find({}).toArray() for (const legacy of discoveryConfigs) { try { const migrated = await this.migrateDiscoveryConfig(legacy) if (migrated) { report.discoveryMigrated++ } else { report.discoverySkipped++ } } catch (error) { report.discoverySkipped++ report.errors.push(`Discovery ${legacy.identifier}: ${(error as Error).message}`) } } // Migrate Provisioning configs (desired parameters + once/maintain + firmware) const provisioningCollection = this.db.collection('provisioning_configs') const provisioningConfigs = await provisioningCollection.find({}).toArray() for (const legacy of provisioningConfigs) { try { const migrated = await this.migrateProvisioningConfig(legacy) if (migrated) { report.provisioningMigrated++ } else { report.provisioningSkipped++ } // Extract firmware Downloads into FirmwareCatalog if (legacy.downloadActions && legacy.downloadActions.length > 0) { for (const action of legacy.downloadActions) { try { await this.extractFirmwareEntry(action, legacy) report.firmwareEntriesCreated++ } catch (error) { report.errors.push(`Firmware extraction for ${legacy.identifier}: ${(error as Error).message}`) } } } } catch (error) { report.provisioningSkipped++ report.errors.push(`Provisioning ${legacy.identifier}: ${(error as Error).message}`) } } } catch (error) { report.errors.push(`Migration error: ${(error as Error).message}`) } return report } /** * Migrate a single Discovery config to Configuration * Discovery = inventory paths + "maintain" policy */ private async migrateDiscoveryConfig(legacy: any): Promise { const criteria = this.buildCriteriaFromLegacy(legacy) const dto: CreateConfigurationDto = { identifier: `legacy-disc-${legacy.identifier}`, name: `[Migrated] ${legacy.name || legacy.identifier}`, version: '1.0.0', priority: 100, policy: 'maintain', enabled: true, tags: ['legacy', 'discovery'], criteria, inventory: (legacy.modelPaths || []).map((p: string) => ({ path: p })), desiredParameters: [], } // Check if already exists (skip if yes) const existing = await this.configService.findById(dto.identifier) if (existing) { return false } await this.configService.create(dto) return true } /** * Migrate a single Provisioning config to Configuration * Provisioning = desired parameters + (once OR maintain) policy + firmware migration */ private async migrateProvisioningConfig(legacy: any): Promise { const criteria = this.buildCriteriaFromLegacy(legacy) const policy = legacy.downloadActions?.length > 0 ? 'once' : 'maintain' const desiredParameters = this.buildDesiredParametersFromLegacy(legacy) // Build inventory paths from desired parameters (auto-suggest) const inventoryPaths = new Set() for (const param of desiredParameters) { const parts = param.name.split('.') parts.pop() inventoryPaths.add(parts.join('.') + '.') } const dto: CreateConfigurationDto = { identifier: `legacy-prov-${legacy.identifier}`, name: `[Migrated] ${legacy.name || legacy.identifier}`, version: '1.0.0', priority: 100, policy, enabled: true, tags: ['legacy', 'provisioning'], criteria, inventory: [...inventoryPaths].map((p) => ({ path: p })), desiredParameters, } const existing = await this.configService.findById(dto.identifier) if (existing) { return false } await this.configService.create(dto) return true } /** * Build criteria object from legacy config device filters */ private buildCriteriaFromLegacy(legacy: any): Record { const criteria: Record = {} if (legacy.manufacturer) criteria.manufacturer = legacy.manufacturer if (legacy.oui) criteria.oui = legacy.oui if (legacy.productClass) criteria.productClass = legacy.productClass if (legacy.serialNumber) criteria.serialNumber = legacy.serialNumber return criteria } /** * Build desired parameters from legacy Set Parameter actions */ private buildDesiredParametersFromLegacy(legacy: any): Array<{ name: string; value: string | number | boolean; type: 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime' }> { const params: Array<{ name: string; value: string | number | boolean; type: 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime' }> = [] if (!legacy.actions) return params for (const action of legacy.actions) { if (action.type === 'SET_PARAMETER' && action.parameter && action.value !== undefined) { params.push({ name: action.parameter, value: action.value, type: typeof action.value === 'boolean' ? 'boolean' : typeof action.value === 'number' ? 'int' : 'string', }) } } return params } /** * Extract firmware Download action into FirmwareCatalog entry */ private async extractFirmwareEntry(action: any, legacy: any): Promise { const entry: CreateFirmwareEntryDto = { identifier: `legacy-fw-${legacy.productClass || 'device'}-${action.version || '1.0.0'}`, productClass: legacy.productClass || 'Device', version: action.version || '1.0.0', downloadUrl: action.url || action.firmwareUrl || '', checksum: action.checksum || '', checksumType: 'md5', fileSize: action.fileSize || 0, } try { await this.firmwareCatalogService.createEntry(entry) } catch { // Entry may already exist — skip silently } } /** * Rollback migration: reconstruct legacy collections from prefixed configurations */ async rollback(): Promise { const report: RollbackReport = { discoveryRolledBack: 0, provisioningRolledBack: 0, errors: [], } try { const result = await this.configService.findAll({ perPage: 1000 }) const allConfigs = result.data const discoveryCollection = this.db.collection('discovery_configs') const provisioningCollection = this.db.collection('provisioning_configs') for (const config of allConfigs) { try { if (config.identifier.startsWith('legacy-disc-')) { const originalId = config.identifier.replace('legacy-disc-', '') const legacy = this.buildLegacyDiscoveryFromConfig(config, originalId) await discoveryCollection.insertOne(legacy) report.discoveryRolledBack++ } else if (config.identifier.startsWith('legacy-prov-')) { const originalId = config.identifier.replace('legacy-prov-', '') const legacy = this.buildLegacyProvisioningFromConfig(config, originalId) await provisioningCollection.insertOne(legacy) report.provisioningRolledBack++ } } catch (error) { report.errors.push(`Rollback ${config.identifier}: ${(error as Error).message}`) } } } catch (error) { report.errors.push(`Rollback error: ${(error as Error).message}`) } return report } private buildLegacyDiscoveryFromConfig(config: any, originalId: string): any { return { identifier: originalId, name: config.name.replace('[Migrated] ', ''), manufacturer: config.criteria?.manufacturer, oui: config.criteria?.oui, productClass: config.criteria?.productClass, serialNumber: config.criteria?.serialNumber, modelPaths: (config.inventory || []).map((inv: any) => inv.path), createdAt: config.createdAt, updatedAt: config.updatedAt, } } private buildLegacyProvisioningFromConfig(config: any, originalId: string): any { const actions = config.desiredParameters?.map((param: any) => ({ type: 'SET_PARAMETER', parameter: param.name, value: param.value, })) || [] return { identifier: originalId, name: config.name.replace('[Migrated] ', ''), manufacturer: config.criteria?.manufacturer, oui: config.criteria?.oui, productClass: config.criteria?.productClass, serialNumber: config.criteria?.serialNumber, actions, downloadActions: [], createdAt: config.createdAt, updatedAt: config.updatedAt, } } }