Skip to main content
Glama
config-migration.tsβ€’10.8 kB
/** * Configuration migration utility for Attio MCP Server * Handles migration of user.json files to fix outdated mappings */ import * as fs from 'fs'; import * as path from 'path'; import { MappingConfig } from './config-loader.js'; import logger from './logger.js'; // Re-export MappingConfig for external use export type { MappingConfig }; export interface MigrationResult { success: boolean; message: string; backupPath?: string; changesApplied?: string[]; errors?: string[]; } export interface MigrationDetection { needsMigration: boolean; outdatedMappings: string[]; filePath: string; exists: boolean; } /** * Known problematic mappings that need to be fixed */ const MAPPINGS_TO_FIX = { ZIP: { from: 'zip', to: 'postal_code', reason: 'zip attribute does not exist in Attio schema', }, 'Postal Code': { from: 'zip', to: 'postal_code', reason: 'zip attribute does not exist in Attio schema', }, } as const; /** * Paths for configuration files */ const CONFIG_PATHS = { user: path.resolve(process.cwd(), 'configs/runtime/mappings/user.json'), backup: path.resolve(process.cwd(), 'configs/runtime/mappings/backup'), }; /** * Detects if user.json needs migration for postal code mappings * * Scans the user configuration file for outdated postal code mappings * that use "zip" instead of the correct "postal_code" attribute name. * * @returns MigrationDetection object containing: * - needsMigration: whether migration is required * - outdatedMappings: array of problematic mappings found * - filePath: path to the user config file * - exists: whether the config file exists * * @example * ```typescript * const detection = detectMigrationNeeds(); * if (detection.needsMigration) { * console.error('Found outdated mappings:', detection.outdatedMappings); * } * ``` */ export function detectMigrationNeeds(): MigrationDetection { const result: MigrationDetection = { needsMigration: false, outdatedMappings: [], filePath: CONFIG_PATHS.user, exists: false, }; // Check if user.json exists if (!fs.existsSync(CONFIG_PATHS.user)) { return result; } result.exists = true; try { const userConfig: MappingConfig = JSON.parse( fs.readFileSync(CONFIG_PATHS.user, 'utf8') ); // Check common mappings for problematic postal code mappings const commonMappings = userConfig.mappings?.attributes?.common || {}; for (const [displayName, expectedFix] of Object.entries(MAPPINGS_TO_FIX)) { const currentMapping = commonMappings[displayName]; if (currentMapping === expectedFix.from) { result.needsMigration = true; result.outdatedMappings.push( `"${displayName}": "${currentMapping}" β†’ should be "${expectedFix.to}"` ); } } } catch (error: unknown) { // If we can't parse the file, we can't migrate it logger.warn( 'config-migration', 'Could not parse user configuration file for migration detection', { filePath: CONFIG_PATHS.user, error: error instanceof Error ? error.message : String(error), } ); } return result; } /** * Creates a timestamped backup of the user.json file * * Creates a backup in config/mappings/backup/ directory with ISO timestamp. * Ensures the backup directory exists before creating the backup file. * * @returns Object containing: * - success: whether backup creation succeeded * - backupPath: path to the created backup file (if successful) * - error: error message (if failed) * * @example * ```typescript * const backup = createBackup(); * if (backup.success) { * console.error('Backup created at:', backup.backupPath); * } else { * console.error('Backup failed:', backup.error); * } * ``` */ export function createBackup(): { success: boolean; backupPath?: string; error?: string; } { try { // Ensure backup directory exists if (!fs.existsSync(CONFIG_PATHS.backup)) { fs.mkdirSync(CONFIG_PATHS.backup, { recursive: true }); } // Create timestamped backup filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupFileName = `user.json.backup.${timestamp}`; const backupPath = path.join(CONFIG_PATHS.backup, backupFileName); // Copy the current user.json to backup fs.copyFileSync(CONFIG_PATHS.user, backupPath); return { success: true, backupPath }; } catch (error: unknown) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } } /** * Applies the postal code mapping migration to user.json * * Performs the complete migration workflow: * 1. Detects if migration is needed * 2. Creates a backup of the current configuration * 3. Applies the postal code mapping fixes * 4. Updates metadata to track migration history * * @returns MigrationResult object containing: * - success: whether the migration succeeded * - message: descriptive message about the operation * - backupPath: path to the backup file (if created) * - changesApplied: array of changes that were made * - errors: array of error messages (if any failures occurred) * * @example * ```typescript * const result = applyMigration(); * if (result.success) { * console.error('Migration completed:', result.message); * console.error('Changes:', result.changesApplied); * } else { * console.error('Migration failed:', result.message); * } * ``` */ export function applyMigration(): MigrationResult { const detection = detectMigrationNeeds(); if (!detection.exists) { return { success: true, message: 'No user.json file found - no migration needed', }; } if (!detection.needsMigration) { return { success: true, message: 'User configuration is already up to date - no migration needed', }; } try { // Create backup first const backup = createBackup(); if (!backup.success) { return { success: false, message: `Failed to create backup: ${backup.error}`, errors: [backup.error || 'Unknown backup error'], }; } // Load and modify the user config const userConfig: MappingConfig = JSON.parse( fs.readFileSync(CONFIG_PATHS.user, 'utf8') ); const changesApplied: string[] = []; // Apply postal code mapping fixes if (userConfig.mappings?.attributes?.common) { for (const [displayName, fix] of Object.entries(MAPPINGS_TO_FIX)) { const currentMapping = userConfig.mappings.attributes.common[displayName]; if (currentMapping === fix.from) { userConfig.mappings.attributes.common[displayName] = fix.to; changesApplied.push( `Updated "${displayName}": "${fix.from}" β†’ "${fix.to}"` ); } } } // Update metadata to track migration if (!userConfig.metadata) { userConfig.metadata = {}; } userConfig.metadata.lastMigration = new Date().toISOString(); userConfig.metadata.migratedMappings = changesApplied; // Write the updated config back to file fs.writeFileSync( CONFIG_PATHS.user, JSON.stringify(userConfig, null, 2), 'utf8' ); return { success: true, message: `Migration completed successfully. Applied ${changesApplied.length} changes.`, backupPath: backup.backupPath, changesApplied, }; } catch (error: unknown) { return { success: false, message: `Migration failed: ${ error instanceof Error ? error.message : String(error) }`, errors: [error instanceof Error ? error.message : String(error)], }; } } /** * Validates that the migration was applied correctly * * Checks if the user configuration still contains any outdated postal * code mappings after migration has been applied. * * @returns Object containing: * - valid: whether the configuration passes validation * - issues: array of validation issues found (if any) * * @example * ```typescript * const validation = validateMigration(); * if (!validation.valid) { * console.error('Validation failed:', validation.issues); * } * ``` */ export function validateMigration(): { valid: boolean; issues: string[] } { const detection = detectMigrationNeeds(); if (!detection.exists) { return { valid: true, issues: [] }; } if (detection.needsMigration) { return { valid: false, issues: [ `Migration incomplete. Still found outdated mappings: ${detection.outdatedMappings.join( ', ' )}`, ], }; } return { valid: true, issues: [] }; } /** * Complete migration workflow with validation * * Main entry point for the migration process. Orchestrates detection, * migration, and validation with optional dry-run mode. * * @param options Configuration options for the migration: * - dryRun: if true, only shows what would be changed without applying * * @returns MigrationResult object with complete operation status * * @example * ```typescript * // Preview changes without applying * const preview = migrateUserConfig({ dryRun: true }); * console.error('Would apply:', preview.changesApplied); * * // Apply the migration * const result = migrateUserConfig(); * if (result.success) { * console.error('Migration completed successfully'); * } * ``` */ export function migrateUserConfig( options: { dryRun?: boolean } = {} ): MigrationResult { const detection = detectMigrationNeeds(); // Early return if no migration needed if (!detection.exists) { return { success: true, message: 'No user.json file found - no migration needed', }; } if (!detection.needsMigration) { return { success: true, message: 'User configuration is already up to date - no migration needed', }; } // Dry run mode - just report what would be changed if (options.dryRun) { return { success: true, message: `Migration needed. Would fix: ${detection.outdatedMappings.join( ', ' )}`, changesApplied: detection.outdatedMappings, }; } // Apply the migration const migrationResult = applyMigration(); if (!migrationResult.success) { return migrationResult; } // Validate the migration worked const validation = validateMigration(); if (!validation.valid) { return { success: false, message: `Migration applied but validation failed: ${validation.issues.join( ', ' )}`, errors: validation.issues, backupPath: migrationResult.backupPath, changesApplied: migrationResult.changesApplied, }; } return migrationResult; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server