feat(migrations): add migration system for v3→v4 config format
- Create abstract BaseMigration class with order, shouldRun(), migrate() - Add MigrationRunner to execute migrations in order - Add Migration v1→v2 (snmp → upsDevices) - Add Migration v3→v4 (upsList → upsDevices) - Update INupstConfig with version field - Update loadConfig() to run migrations automatically - Update saveConfig() to ensure version field and remove legacy fields - Update Docker test scripts to use real UPS data from .nogit/env.json - Remove colors.bright (not available in @std/fmt/colors) Config migrations allow users to jump versions (e.g., v1→v4) with all intermediate migrations running automatically. Version field tracks config format for future migrations.
This commit is contained in:
54
ts/migrations/base-migration.ts
Normal file
54
ts/migrations/base-migration.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Abstract base class for configuration migrations
|
||||
*
|
||||
* Each migration represents an upgrade from one config version to another.
|
||||
* Migrations run in order based on the `order` field, allowing users to jump
|
||||
* multiple versions (e.g., v1 → v4 runs migrations 2, 3, and 4).
|
||||
*/
|
||||
export abstract class BaseMigration {
|
||||
/**
|
||||
* Migration order number
|
||||
* - Order 2: v1 → v2
|
||||
* - Order 3: v2 → v3
|
||||
* - Order 4: v3 → v4
|
||||
* etc.
|
||||
*/
|
||||
abstract readonly order: number;
|
||||
|
||||
/**
|
||||
* Source version this migration upgrades from
|
||||
* e.g., "1.x", "3.x"
|
||||
*/
|
||||
abstract readonly fromVersion: string;
|
||||
|
||||
/**
|
||||
* Target version this migration upgrades to
|
||||
* e.g., "2.0", "4.0"
|
||||
*/
|
||||
abstract readonly toVersion: string;
|
||||
|
||||
/**
|
||||
* Check if this migration should run on the given config
|
||||
*
|
||||
* @param config - Raw configuration object to check
|
||||
* @returns True if migration should run, false otherwise
|
||||
*/
|
||||
abstract shouldRun(config: any): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Perform the migration on the given config
|
||||
*
|
||||
* @param config - Raw configuration object to migrate
|
||||
* @returns Migrated configuration object
|
||||
*/
|
||||
abstract migrate(config: any): Promise<any>;
|
||||
|
||||
/**
|
||||
* Get human-readable name for this migration
|
||||
*
|
||||
* @returns Migration name
|
||||
*/
|
||||
getName(): string {
|
||||
return `Migration ${this.fromVersion} → ${this.toVersion}`;
|
||||
}
|
||||
}
|
10
ts/migrations/index.ts
Normal file
10
ts/migrations/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Configuration migrations module
|
||||
*
|
||||
* Exports the migration system for upgrading configs between versions.
|
||||
*/
|
||||
|
||||
export { BaseMigration } from './base-migration.ts';
|
||||
export { MigrationRunner } from './migration-runner.ts';
|
||||
export { MigrationV1ToV2 } from './migration-v1-to-v2.ts';
|
||||
export { MigrationV3ToV4 } from './migration-v3-to-v4.ts';
|
69
ts/migrations/migration-runner.ts
Normal file
69
ts/migrations/migration-runner.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import { MigrationV1ToV2 } from './migration-v1-to-v2.ts';
|
||||
import { MigrationV3ToV4 } from './migration-v3-to-v4.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Migration runner
|
||||
*
|
||||
* Discovers all available migrations, sorts them by order,
|
||||
* and runs applicable migrations in sequence.
|
||||
*/
|
||||
export class MigrationRunner {
|
||||
private migrations: BaseMigration[];
|
||||
|
||||
constructor() {
|
||||
// Register all migrations here
|
||||
this.migrations = [
|
||||
new MigrationV1ToV2(),
|
||||
new MigrationV3ToV4(),
|
||||
// Add future migrations here (v4→v5, v5→v6, etc.)
|
||||
];
|
||||
|
||||
// Sort by order to ensure they run in sequence
|
||||
this.migrations.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all applicable migrations on the config
|
||||
*
|
||||
* @param config - Raw configuration object to migrate
|
||||
* @returns Migrated configuration and whether migrations ran
|
||||
*/
|
||||
async run(config: any): Promise<{ config: any; migrated: boolean }> {
|
||||
let currentConfig = config;
|
||||
let anyMigrationsRan = false;
|
||||
|
||||
logger.dim('Checking for required config migrations...');
|
||||
|
||||
for (const migration of this.migrations) {
|
||||
const shouldRun = await migration.shouldRun(currentConfig);
|
||||
|
||||
if (shouldRun) {
|
||||
logger.info(`Running ${migration.getName()}...`);
|
||||
currentConfig = await migration.migrate(currentConfig);
|
||||
anyMigrationsRan = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyMigrationsRan) {
|
||||
logger.success('Configuration migrations complete');
|
||||
} else {
|
||||
logger.dim('No migrations needed');
|
||||
}
|
||||
|
||||
return {
|
||||
config: currentConfig,
|
||||
migrated: anyMigrationsRan,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered migrations
|
||||
*
|
||||
* @returns Array of all migrations sorted by order
|
||||
*/
|
||||
getMigrations(): BaseMigration[] {
|
||||
return [...this.migrations];
|
||||
}
|
||||
}
|
56
ts/migrations/migration-v1-to-v2.ts
Normal file
56
ts/migrations/migration-v1-to-v2.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Migration from v1 (single SNMP config) to v2 (upsDevices array)
|
||||
*
|
||||
* Detects old format:
|
||||
* {
|
||||
* snmp: { ... },
|
||||
* thresholds: { ... },
|
||||
* checkInterval: 30000
|
||||
* }
|
||||
*
|
||||
* Converts to:
|
||||
* {
|
||||
* version: "2.0",
|
||||
* upsDevices: [{ id: "default", name: "Default UPS", snmp: ..., thresholds: ... }],
|
||||
* groups: [],
|
||||
* checkInterval: 30000
|
||||
* }
|
||||
*/
|
||||
export class MigrationV1ToV2 extends BaseMigration {
|
||||
readonly order = 2;
|
||||
readonly fromVersion = '1.x';
|
||||
readonly toVersion = '2.0';
|
||||
|
||||
async shouldRun(config: any): Promise<boolean> {
|
||||
// V1 format has snmp field directly at root, no upsDevices or upsList
|
||||
return !!config.snmp && !config.upsDevices && !config.upsList;
|
||||
}
|
||||
|
||||
async migrate(config: any): Promise<any> {
|
||||
logger.info(`${this.getName()}: Converting single SNMP config to multi-UPS format...`);
|
||||
|
||||
const migrated = {
|
||||
version: this.toVersion,
|
||||
upsDevices: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
thresholds: config.thresholds || {
|
||||
battery: 60,
|
||||
runtime: 20,
|
||||
},
|
||||
groups: [],
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: config.checkInterval || 30000,
|
||||
};
|
||||
|
||||
logger.success(`${this.getName()}: Migration complete`);
|
||||
return migrated;
|
||||
}
|
||||
}
|
45
ts/migrations/migration-v3-to-v4.ts
Normal file
45
ts/migrations/migration-v3-to-v4.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Migration from v3 (upsList) to v4 (upsDevices)
|
||||
*
|
||||
* Detects v3 format:
|
||||
* {
|
||||
* upsList: [ ... ],
|
||||
* groups: [ ... ],
|
||||
* checkInterval: 30000
|
||||
* }
|
||||
*
|
||||
* Converts to:
|
||||
* {
|
||||
* version: "4.0",
|
||||
* upsDevices: [ ... ], // renamed from upsList
|
||||
* groups: [ ... ],
|
||||
* checkInterval: 30000
|
||||
* }
|
||||
*/
|
||||
export class MigrationV3ToV4 extends BaseMigration {
|
||||
readonly order = 4;
|
||||
readonly fromVersion = '3.x';
|
||||
readonly toVersion = '4.0';
|
||||
|
||||
async shouldRun(config: any): Promise<boolean> {
|
||||
// V3 format has upsList instead of upsDevices
|
||||
return !!config.upsList && !config.upsDevices;
|
||||
}
|
||||
|
||||
async migrate(config: any): Promise<any> {
|
||||
logger.info(`${this.getName()}: Renaming upsList to upsDevices...`);
|
||||
|
||||
const migrated = {
|
||||
version: this.toVersion,
|
||||
upsDevices: config.upsList, // Rename upsList → upsDevices
|
||||
groups: config.groups || [],
|
||||
checkInterval: config.checkInterval || 30000,
|
||||
};
|
||||
|
||||
logger.success(`${this.getName()}: Migration complete`);
|
||||
return migrated;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user