src — versioning

Module: src-versioning Cohesion: 0.80 Members: 0

src — versioning

The src/versioning module is a critical component responsible for managing the application's version lifecycle, including detecting current versions, migrating configuration files, and orchestrating complex data and schema migrations. It ensures that the application can gracefully evolve, handling changes in data structures, configuration schemas, and application logic across different releases.

This module provides robust mechanisms for:

Core Concepts

Before diving into the individual components, it's important to understand the foundational concepts that underpin this module:

Module Architecture

The versioning module is composed of three main classes, each with a distinct responsibility:

graph TD
    A[Application Startup] --> B(VersionDetector)
    B --> C{Needs Upgrade?}
    C -- Yes, Config --> D(ConfigMigrator)
    C -- Yes, Data/Schema --> E(MigrationManager)
    D -- Transforms Config Files --> F(Configuration Files)
    E -- Migrates Data/Schema --> G(Data Directory)
    E -- Manages Locks & Audit --> H(Shared Resources)
    D -- Creates Backups --> I(Backup Directory)
    E -- Tracks History --> J(Migration History)
    E -- Logs Operations --> K(Audit Log)

1. VersionDetector

The VersionDetector class is responsible for identifying the current version of the application from various sources and providing utilities for version comparison and manipulation.

Purpose: To provide a unified way to determine the application's current version, compare it against target versions, and understand if an upgrade is necessary.

Key Methods & Concepts:

Usage Pattern:

import { getVersionDetector } from './version-detector.js';

async function checkAppVersion() {
  const detector = getVersionDetector();
  await detector.initialize();

  const packageVersion = detector.getPackageVersion();
  const storedVersion = detector.getStoredVersion();
  const configVersion = detector.getConfigVersion();
  const currentAppVersion = detector.getCurrentVersion();

  console.log(`Package Version: ${packageVersion}`);
  console.log(`Stored Version: ${storedVersion}`);
  console.log(`Config Version: ${configVersion}`);
  console.log(`Current App Version: ${currentAppVersion}`);

  if (detector.needsUpgrade()) {
    console.log('Application upgrade is needed!');
    const upgradePath = detector.getUpgradePath(storedVersion || '0.0.0', packageVersion!);
    console.log('Upgrade path:', upgradePath);
  }
}

2. ConfigMigrator

The ConfigMigrator class specializes in applying versioned transformations to configuration files, typically JSON-based settings.

Purpose: To manage the evolution of configuration schemas, ensuring that user settings are automatically updated to match the latest application requirements. This includes adding new default fields, removing deprecated ones, renaming fields, and performing complex data transformations.

Key Interfaces & Concepts:

    export interface ConfigTransform {
      version: string; class="hl-cmt">// The target version this transform applies to
      name: string;
      description?: string;
      transform: (config: Record<string, unknown>) => Record<string, unknown>; class="hl-cmt">// The actual transformation logic
      validate?: (config: Record<string, unknown>) => boolean; class="hl-cmt">// Optional validation after transform
    }

Each ConfigTransform defines a single step in the migration process, associated with a specific target version.

Key Methods:

  1. Loads the current configuration.
  2. Determines the current version.
  3. Creates a backup if enabled.
  4. Identifies all ConfigTransforms between the current and targetVersion.
  5. Applies each transform sequentially, tracking changes and running optional validations.
  6. Updates the configuration's internal version.
  7. Saves the migrated configuration.
  8. If any transform fails, it attempts to restoreFromBackup().

Usage Pattern:

import { getConfigMigrator, ConfigTransform } from &#39;./config-migrator.js&#39;;
import * as path from &#39;path&#39;;
import * as os from &#39;os&#39;;

const migrator = getConfigMigrator({
  configDir: path.join(os.homedir(), &#39;.my-app&#39;, &#39;config&#39;),
  configFile: &#39;settings.json&#39;,
});

class="hl-cmt">// Define a transform
const transformV1_1_0: ConfigTransform = {
  version: &#39;1.1.0&#39;,
  name: &#39;Add new default setting&#39;,
  description: &#39;Adds a new `featureFlags.darkMode` setting with a default value.&#39;,
  transform: (config) => {
    class="hl-cmt">// Use helper methods for common tasks
    let newConfig = migrator.applyDefaults(config, {
      featureFlags: {
        darkMode: false,
      },
    });
    newConfig = migrator.removeDeprecatedFields(newConfig, [&#39;oldSetting&#39;]);
    return newConfig;
  },
  validate: (config) => &#39;featureFlags&#39; in config && typeof (config as any).featureFlags.darkMode === &#39;boolean&#39;,
};

const transformV1_2_0: ConfigTransform = {
  version: &#39;1.2.0&#39;,
  name: &#39;Rename user.name to user.fullName&#39;,
  transform: (config) => {
    if (config.user && typeof config.user === &#39;object&#39; && &#39;name&#39; in config.user) {
      const user = config.user as Record<string, unknown>;
      user.fullName = user.name;
      delete user.name;
    }
    return config;
  },
};

async function runConfigMigration() {
  await migrator.initialize();
  migrator.registerTransforms([transformV1_1_0, transformV1_2_0]);

  const targetVersion = &#39;1.2.0&#39;;
  console.log(`Attempting to migrate config to ${targetVersion}...`);

  const result = await migrator.migrate(targetVersion);

  if (result.success) {
    console.log(`Config migrated successfully from ${result.fromVersion} to ${result.toVersion}.`);
    console.log(`Transforms applied: ${result.transformsApplied}`);
    console.log(&#39;Changes:&#39;, result.changes);
  } else {
    console.error(&#39;Config migration failed:&#39;, result.errors);
    if (result.backup) {
      console.log(`Configuration restored from backup: ${result.backup}`);
    }
  }
}

3. MigrationManager

The MigrationManager is the most comprehensive component, designed for managing broader data and schema migrations with robust transactional guarantees and concurrency control.

Purpose: To provide a safe and auditable framework for evolving the application's persistent data structures. It's suitable for database schema changes, file system reorganizations, or any operation that modifies critical application data.

Key Interfaces & Concepts:

    export interface Migration {
      version: string;
      name: string;
      description?: string;
      up: (context: MigrationContext) => Promise<void>;   class="hl-cmt">// Logic to apply the migration
      down: (context: MigrationContext) => Promise<void>; class="hl-cmt">// Logic to revert the migration
      appliedAt?: Date;
    }

Each Migration defines a pair of up and down functions for a specific version. The up function applies the changes, and the down function reverts them.

Key Methods & Internal Mechanisms:

Usage Pattern:

import { getMigrationManager, Migration, MigrationContext } from &#39;./migration-manager.js&#39;;
import * as path from &#39;path&#39;;
import * as fs from &#39;fs-extra&#39;;
import * as os from &#39;os&#39;;

const manager = getMigrationManager({
  dataDir: path.join(os.homedir(), &#39;.my-app&#39;),
  configDir: path.join(os.homedir(), &#39;.my-app&#39;, &#39;config&#39;),
  verbose: true,
});

class="hl-cmt">// Define a migration
const migrationV1_0_0: Migration = {
  version: &#39;1.0.0&#39;,
  name: &#39;Initial database setup&#39;,
  description: &#39;Creates the initial user data file.&#39;,
  up: async (context: MigrationContext) => {
    const userFilePath = path.join(context.dataDir, &#39;users.json&#39;);
    context.logger.info(`Creating ${userFilePath}...`);
    await context.backupFile(userFilePath); class="hl-cmt">// Backup if it exists, or mark as new
    await fs.writeJson(userFilePath, [{ id: &#39;admin&#39;, name: &#39;Administrator&#39; }], { spaces: 2 });
  },
  down: async (context: MigrationContext) => {
    const userFilePath = path.join(context.dataDir, &#39;users.json&#39;);
    context.logger.info(`Removing ${userFilePath}...`);
    await context.backupFile(userFilePath); class="hl-cmt">// Backup before removing
    await fs.remove(userFilePath);
  },
};

const migrationV1_1_0: Migration = {
  version: &#39;1.1.0&#39;,
  name: &#39;Add settings file&#39;,
  description: &#39;Creates a default settings file.&#39;,
  up: async (context: MigrationContext) => {
    const settingsFilePath = path.join(context.configDir, &#39;app-settings.json&#39;);
    context.logger.info(`Creating ${settingsFilePath}...`);
    await context.backupFile(settingsFilePath);
    await fs.writeJson(settingsFilePath, { theme: &#39;dark&#39;, notifications: true }, { spaces: 2 });
  },
  down: async (context: MigrationContext) => {
    const settingsFilePath = path.join(context.configDir, &#39;app-settings.json&#39;);
    context.logger.info(`Removing ${settingsFilePath}...`);
    await context.backupFile(settingsFilePath);
    await fs.remove(settingsFilePath);
  },
};

async function runDataMigrations() {
  await manager.initialize();
  manager.registerMigrations([migrationV1_0_0, migrationV1_1_0]);

  console.log(&#39;Current migration status:&#39;, manager.getStatus());

  if (manager.hasPendingMigrations()) {
    console.log(&#39;Running pending migrations...&#39;);
    const result = await manager.migrate();

    if (result.success) {
      console.log(`Migrations completed successfully. Applied ${result.migrationsApplied} migrations.`);
    } else {
      console.error(&#39;Migrations failed:&#39;, result.errors);
    }
  } else {
    console.log(&#39;No pending migrations.&#39;);
  }

  console.log(&#39;Updated migration status:&#39;, manager.getStatus());

  class="hl-cmt">// Example of rollback
  class="hl-cmt">// console.log(&#39;Rolling back last migration...&#39;);
  class="hl-cmt">// const rollbackResult = await manager.rollback();
  class="hl-cmt">// if (rollbackResult.success) {
  class="hl-cmt">//   console.log(&#39;Last migration rolled back successfully.&#39;);
  class="hl-cmt">// } else {
  class="hl-cmt">//   console.error(&#39;Rollback failed:&#39;, rollbackResult.errors);
  class="hl-cmt">// }
}

Integration Points

The src/versioning module is designed to be a foundational service for the application. Based on the provided call graph, here's how it integrates with other parts of the codebase:

Design Considerations & Best Practices for Contributors