JSPM

  • Created
  • Published
  • Downloads 8
  • Score
    100M100P100Q47185F
  • License SEE LICENSE IN LICENSE.md

Universal config converter framework with exceptional developer experience

Package Exports

  • configforge
  • configforge/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (configforge) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

ConfigForge

A TypeScript library for converting, mapping, and transforming config data in JSON and YAML.

ConfigForge is a universal config converter library with a fluent API

ConfigForge makes it easy to convert configuration files between different formats and structures. Just define your mappings and let ConfigForge handle the rest.

🚀 Quick Start

Installation

npm install configforge

Basic Usage

const { forge } = require('configforge');

// Your source configuration
const sourceConfig = {
  name: 'MyApp',
  version: '1.0',
  author: {
    firstName: 'John',
    lastName: 'Doe',
  },
  items: ['apple', 'banana', 'cherry'],
};

// Create converter and define mappings
const converter = forge()
  .from('source')
  .to('target')
  .map('name', 'appName')
  .map('version', 'appVersion')
  .map('author.firstName', 'authorName')
  .map('items[0]', 'firstItem');

// Convert the data
const result = await converter.convert(sourceConfig);

console.log(result.data);
// Output:
// {
//   appName: 'MyApp',
//   appVersion: '1.0',
//   authorName: 'John',
//   firstItem: 'apple'
// }

📖 How It Works

1. Create a Converter

const converter = forge()
  .from('sourceSchema') // Define source
  .to('targetSchema'); // Define target

2. Map Fields

// Simple field mapping
.map('oldField', 'newField')

// Nested object mapping
.map('user.profile.name', 'displayName')

// Array element mapping
.map('items[0]', 'firstItem')
.map('items[1]', 'secondItem')

3. Add Transformations

// Transform values during mapping
.map('name', 'displayName', (value) => value.toUpperCase())

// Access source data in transforms
.map('firstName', 'fullName', (firstName, ctx) => {
  return `${firstName} ${ctx.source.lastName}`;
})

4. Add Conditional Logic

// Function conditions - only apply when condition returns true
.when(source => source.user?.type === 'admin')
  .map('user.permissions', 'adminPermissions')
  .map('user.role', 'adminRole')
  .end()

// String conditions - only apply when field exists and is truthy
.when('user.isActive')
  .map('user.lastLogin', 'lastActiveLogin')
  .end()

5. Merge Multiple Fields

// Combine multiple source fields into one target field
.merge(['firstName', 'lastName'], 'fullName', (first, last) => `${first} ${last}`)

// Merge with transformation
.merge(['basePrice', 'tax'], 'totalPrice',
  (price, tax) => price + tax,
  total => `$${total.toFixed(2)}`
)

// Merge complex data
.merge(['user.profile', 'user.settings'], 'userInfo', (profile, settings) => ({
  ...profile,
  ...settings
}))

6. Set Default Values

// Set static default values
.defaults({
  version: '1.0.0',
  enabled: true,
  environment: 'production'
})

// Set dynamic default values using functions
.defaults({
  timestamp: () => new Date().toISOString(),
  id: () => `user_${Date.now()}`,
  sessionId: () => Math.random().toString(36).substr(2, 9)
})

7. Add Lifecycle Hooks

// Before hooks - run before conversion starts
.before((sourceData) => {
  console.log('Starting conversion...');
  // Modify source data if needed
  return {
    ...sourceData,
    processed: true
  };
})

// After hooks - run after conversion completes
.after((targetData) => {
  console.log('Conversion completed!');
  // Add computed fields or modify target data
  return {
    ...targetData,
    processedAt: new Date().toISOString(),
    checksum: calculateChecksum(targetData)
  };
})

// Multiple hooks execute in order
.before(validateInput)
.before(preprocessData)
.after(addMetadata)
.after(logResults)

// Async hooks are supported
.before(async (data) => {
  const enrichedData = await fetchAdditionalData(data.userId);
  return { ...data, ...enrichedData };
})

8. Add Validation

const { validators } = require('configforge');

// Add field validation
.validate('email', validators.email)
.validate('age', validators.all(
  validators.required,
  validators.type('number'),
  validators.range(18, 120)
))
.validate('username', validators.all(
  validators.required,
  validators.minLength(3),
  validators.pattern(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores allowed')
))

// Custom validation with context
.validate('password', (value, context) => {
  if (context.source.confirmPassword !== value) {
    return 'Passwords do not match';
  }
  return validators.minLength(8)(value, context);
})

9. Convert Data

// Convert object data
const result = await converter.convert(sourceConfig);

// Convert from file
const result = await converter.convert('./config.yml');

// Synchronous conversion (objects only)
const result = converter.convertSync(sourceConfig);

🔧 Working with Results

const result = await converter.convert(data);

// Access converted data
console.log(result.data);

// Check conversion statistics
console.log(result.stats);
// {
//   fieldsProcessed: 10,
//   fieldsMapped: 5,
//   fieldsUnmapped: 5,
//   transformsApplied: 2,
//   duration: 3
// }

// See unmapped fields
console.log(result.unmapped);
// ['author.lastName', 'items[1]', 'items[2]']

// Pretty print report
result.print();

// Save to file
await result.save('./output.json'); // Saves as JSON
await result.save('./output.yml'); // Saves as YAML

// Get as string
const jsonString = result.toJSON();
const yamlString = result.toYAML();

📝 Real Examples

Example 1: Simple Config Transformation

const { forge } = require('configforge');

const oldConfig = {
  app_name: 'MyApp',
  app_version: '2.1.0',
  database: {
    host: 'localhost',
    port: 5432,
  },
};

const converter = forge()
  .from('old')
  .to('new')
  .map('app_name', 'name')
  .map('app_version', 'version')
  .map('database.host', 'db.hostname')
  .map('database.port', 'db.port')
  .defaults({
    environment: 'development',
  });

const result = await converter.convert(oldConfig);
console.log(result.data);
// {
//   name: 'MyApp',
//   version: '2.1.0',
//   db: {
//     hostname: 'localhost',
//     port: 5432
//   },
//   environment: 'development'
// }

Example 2: With Transformations

const userConfig = {
  user: {
    first_name: 'john',
    last_name: 'doe',
    email: 'JOHN.DOE@EXAMPLE.COM',
  },
  settings: {
    theme: 'dark',
    notifications: 'true',
  },
};

const converter = forge()
  .from('user')
  .to('profile')
  .map(
    'user.first_name',
    'name',
    name => name.charAt(0).toUpperCase() + name.slice(1)
  )
  .map('user.email', 'email', email => email.toLowerCase())
  .map('user.first_name', 'displayName', (firstName, ctx) => {
    const lastName = ctx.source.user.last_name;
    return `${firstName} ${lastName}`.replace(/\b\w/g, l => l.toUpperCase());
  })
  .map(
    'settings.notifications',
    'notificationsEnabled',
    value => value === 'true'
  );

const result = await converter.convert(userConfig);
console.log(result.data);
// {
//   name: 'John',
//   email: 'john.doe@example.com',
//   displayName: 'John Doe',
//   notificationsEnabled: true
// }

Example 3: Array Processing with forEach()

// Simple fruit inventory
const fruitData = {
  storeId: 'STORE-001',
  fruits: [
    {
      name: 'Apple',
      color: 'red',
      price: 1.5,
      quantity: 100,
    },
    {
      name: 'Banana',
      color: 'yellow',
      price: 0.75,
      quantity: 80,
    },
    {
      name: 'Orange',
      color: 'orange',
      price: 1.25,
      quantity: 60,
    },
  ],
};

const converter = forge()
  .from('inventory')
  .to('catalog')
  .map('storeId', 'storeId')
  .forEach('fruits', (fruit, index) => {
    return {
      id: index + 1,
      fruitName: fruit.name,
      displayColor: fruit.color,
      pricePerItem: fruit.price,
      inStock: fruit.quantity,
      totalValue: fruit.price * fruit.quantity,
      isExpensive: fruit.price > 1.0,
    };
  });

const result = await converter.convert(fruitData);
console.log(result.data);
// {
//   storeId: 'STORE-001',
//   fruits: [
//     {
//       id: 1,
//       fruitName: 'Apple',
//       displayColor: 'red',
//       pricePerItem: 1.50,
//       inStock: 100,
//       totalValue: 150,
//       isExpensive: true
//     },
//     {
//       id: 2,
//       fruitName: 'Banana',
//       displayColor: 'yellow',
//       pricePerItem: 0.75,
//       inStock: 80,
//       totalValue: 60,
//       isExpensive: false
//     },
//     // ... more fruits
//   ]
// }

Example 4: Conditional Mapping with when()

// Different types of pets need different mappings
const petData = {
  type: 'dog',
  name: 'Buddy',
  age: 3,
  dogInfo: {
    breed: 'Golden Retriever',
    tricks: ['sit', 'stay', 'fetch'],
  },
  catInfo: {
    breed: 'Persian',
    indoor: true,
  },
  birdInfo: {
    species: 'Parrot',
    canTalk: true,
  },
};

const converter = forge()
  .from('petData')
  .to('petProfile')
  .map('name', 'petName')
  .map('age', 'petAge')
  .map('type', 'animalType')

  // Only map dog-specific info for dogs
  .when(source => source.type === 'dog')
  .map('dogInfo.breed', 'breed')
  .map('dogInfo.tricks', 'knownTricks')
  .end()

  // Only map cat-specific info for cats
  .when(source => source.type === 'cat')
  .map('catInfo.breed', 'breed')
  .map('catInfo.indoor', 'isIndoorCat')
  .end()

  // Only map bird-specific info for birds
  .when(source => source.type === 'bird')
  .map('birdInfo.species', 'species')
  .map('birdInfo.canTalk', 'canSpeak')
  .end();

const result = await converter.convert(petData);
console.log(result.data);
// {
//   petName: 'Buddy',
//   petAge: 3,
//   animalType: 'dog',
//   breed: 'Golden Retriever',
//   knownTricks: ['sit', 'stay', 'fetch']
// }

Example 5: Merge Multiple Fields with merge()

// Simple student report card
const studentData = {
  student: {
    firstName: 'Alice',
    lastName: 'Smith',
  },
  grades: {
    math: 85,
    english: 92,
    science: 78,
  },
  activities: ['soccer', 'chess', 'art'],
  info: {
    grade: '5th',
    teacher: 'Ms. Johnson',
  },
};

const converter = forge()
  .from('report')
  .to('summary')

  // Merge student name fields
  .merge(
    ['student.firstName', 'student.lastName'],
    'studentName',
    (first, last) => `${first} ${last}`
  )

  // Calculate total with transformation
  .merge(
    ['order.basePrice', 'order.tax', 'order.shipping'],
    'subtotal',
    (base, tax, shipping) => base + tax + shipping
  )

  .merge(
    ['order.basePrice', 'order.tax', 'order.shipping', 'order.discount'],
    'total',
    (base, tax, shipping, discount) => base + tax + shipping - discount,
    total => `$${total.toFixed(2)}`
  ) // Transform to currency format

  // Merge arrays and objects
  .merge(['items', 'metadata'], 'orderSummary', (items, meta) => ({
    itemCount: items.length,
    items: items.join(', '),
    ...meta,
  }))

  // Simple field mappings
  .map('customer.email', 'billingEmail');

const result = await converter.convert(orderData);
console.log(result.data);
// {
//   customerName: 'John Doe',
//   subtotal: 121.49,
//   total: '$106.49',
//   orderSummary: {
//     itemCount: 3,
//     items: 'laptop, mouse, keyboard',
//     orderDate: '2023-12-01',
//     source: 'web'
//   },
//   billingEmail: 'john.doe@example.com'
// }

Example 6: Data Validation

const { forge, validators } = require('configforge');

// User registration form data
const formData = {
  personalInfo: {
    firstName: 'John',
    lastName: 'Doe',
    email: 'john.doe@example.com',
    age: 28,
  },
  accountInfo: {
    username: 'johndoe123',
    password: 'SecurePass123!',
    confirmPassword: 'SecurePass123!',
  },
  preferences: {
    newsletter: 'yes',
    theme: 'dark',
    language: 'en',
  },
};

const converter = forge()
  .from('form')
  .to('user')
  // Map fields
  .merge(
    ['personalInfo.firstName', 'personalInfo.lastName'],
    'fullName',
    (first, last) => `${first} ${last}`
  )
  .map('personalInfo.email', 'email')
  .map('personalInfo.age', 'age')
  .map('accountInfo.username', 'username')
  .map('accountInfo.password', 'password')
  .map(
    'preferences.newsletter',
    'subscribeToNewsletter',
    value => value === 'yes'
  )
  .map('preferences.theme', 'theme')
  .map('preferences.language', 'language')

  // Add validation rules
  .validate(
    'fullName',
    validators.all(
      validators.required,
      validators.minLength(2),
      validators.maxLength(100)
    )
  )
  .validate('email', validators.all(validators.required, validators.email))
  .validate(
    'age',
    validators.all(
      validators.required,
      validators.type('number'),
      validators.range(13, 120)
    )
  )
  .validate(
    'username',
    validators.all(
      validators.required,
      validators.minLength(3),
      validators.maxLength(20),
      validators.pattern(
        /^[a-zA-Z0-9_]+$/,
        'Username can only contain letters, numbers, and underscores'
      )
    )
  )
  .validate(
    'password',
    validators.all(
      validators.required,
      validators.minLength(8),
      validators.pattern(
        /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
        'Password must contain lowercase, uppercase, and number'
      )
    )
  )
  .validate('theme', validators.oneOf(['light', 'dark', 'auto']))
  .validate('language', validators.oneOf(['en', 'es', 'fr', 'de']))

  // Custom validation
  .validate('password', (value, context) => {
    if (context.source.accountInfo.confirmPassword !== value) {
      return 'Passwords do not match';
    }
    return true;
  });

const result = await converter.convert(formData);

if (result.errors.length > 0) {
  console.log('Validation errors:');
  result.errors.forEach(error => {
    console.log(`- ${error.path}: ${error.message}`);
  });
} else {
  console.log('User data is valid!');
  console.log(result.data);
  // {
  //   fullName: 'John Doe',
  //   email: 'john.doe@example.com',
  //   age: 28,
  //   username: 'johndoe123',
  //   password: 'SecurePass123!',
  //   subscribeToNewsletter: true,
  //   theme: 'dark',
  //   language: 'en'
  // }
}

Example 7: Defaults and Hooks

// Student data with missing fields
const studentData = {
  student: {
    firstName: 'Alice',
    lastName: 'Smith',
    // age is missing
  },
  grades: {
    math: 85,
    english: 92,
    // science grade is missing
  },
  // activities array is missing
  info: {
    grade: '5th',
    teacher: 'Ms. Johnson',
    // school year is missing
  },
};

const converter = forge()
  .from('studentReport')
  .to('completeProfile')

  // Basic mappings
  .map('student.firstName', 'firstName')
  .map('student.lastName', 'lastName')
  .map('student.age', 'age')
  .map('grades.math', 'mathGrade')
  .map('grades.english', 'englishGrade')
  .map('grades.science', 'scienceGrade')
  .map('activities', 'extracurriculars')
  .map('info.grade', 'gradeLevel')
  .map('info.teacher', 'teacher')
  .map('info.schoolYear', 'academicYear')

  // Set default values for missing fields
  .defaults({
    age: 10, // Default age for 5th graders
    scienceGrade: 80, // Default science grade
    extracurriculars: ['reading'], // Default activity
    academicYear: () => {
      // Dynamic default - current school year
      const now = new Date();
      const year = now.getFullYear();
      const month = now.getMonth();
      return month >= 8 ? `${year}-${year + 1}` : `${year - 1}-${year}`;
    },
    status: 'active',
    lastUpdated: () => new Date().toISOString(),
  })

  // Add before hook to log conversion start
  .before(data => {
    console.log('🔄 Starting conversion...');
    console.log(
      `Processing student: ${data.student?.firstName} ${data.student?.lastName}`
    );
    return data; // Return the data unchanged
  })

  // Add after hook to calculate grade average
  .after(data => {
    console.log('✅ Conversion completed!');

    // Create full name from first and last name
    if (data.firstName && data.lastName) {
      data.fullName = `${data.firstName} ${data.lastName}`;
    }

    // Calculate and add grade average
    const { mathGrade, englishGrade, scienceGrade } = data;
    if (mathGrade && englishGrade && scienceGrade) {
      data.gradeAverage = Math.round(
        (mathGrade + englishGrade + scienceGrade) / 3
      );
      console.log(`Calculated grade average: ${data.gradeAverage}`);
    }

    return data; // Return the modified data
  });

const result = await converter.convert(studentData);
console.log(result.data);
// {
//   firstName: 'Alice',
//   lastName: 'Smith',
//   mathGrade: 85,
//   englishGrade: 92,
//   gradeLevel: '5th',
//   teacher: 'Ms. Johnson',
//   age: 10,
//   scienceGrade: 80,
//   extracurriculars: ['reading'],
//   academicYear: '2024-2025',
//   status: 'active',
//   lastUpdated: '2024-12-23T06:22:05.491Z',
//   fullName: 'Alice Smith',
//   gradeAverage: 86
// }

Example 8: File Conversion

// Convert YAML file to JSON structure
const converter = forge()
  .from('yaml')
  .to('json')
  .map('server.host', 'hostname')
  .map('server.port', 'port')
  .map('database.url', 'dbUrl');

// Read and convert file
const result = await converter.convert('./config.yml');

// Save as JSON
await result.save('./config.json');

Example 9: CLI Generation System

const { forge, CLIGenerator } = require('configforge');

// Create your converter
const converter = forge()
  .from('legacy')
  .to('modern')
  .map('app_name', 'name')
  .map('app_version', 'version')
  .map('database.host', 'db.hostname')
  .map('database.port', 'db.port')
  .defaults({
    environment: 'production',
  });

// Generate CLI for your converter
const cli = CLIGenerator.forConverter(converter, {
  name: 'config-converter',
  version: '1.0.0',
  description: 'Convert legacy config to modern format',
});

// Add custom commands
cli.addCommand({
  name: 'migrate',
  description: 'Migrate all configs in a directory',
  options: [
    {
      flags: '-d, --directory <dir>',
      description: 'Directory containing config files',
    },
  ],
  action: async options => {
    // Custom migration logic
    console.log(`Migrating configs in ${options.directory}`);
  },
});

// Parse command line arguments
cli.parse();

// Now you can use your CLI:
// $ config-converter convert input.yml output.json
// $ config-converter validate config.yml
// $ config-converter profile save my-config
// $ config-converter profile list
// $ config-converter migrate -d ./configs

🎯 Key Features

  • Simple API: Just map fields and convert
  • Pipeline Architecture: Robust internal processing with configurable steps and error handling
  • No direct Mapper usage needed: The convert() method handles everything
  • Nested object support: Use dot notation like user.profile.name
  • Array access: Use bracket notation like items[0]
  • Transformations: Transform values during mapping
  • Conditional mapping: Use when() for conditional logic
  • Multi-field merging: Use merge() to combine multiple sources into one target
  • Default values: Set fallback values
  • Comprehensive Error Handling: Advanced error reporting with context and suggestions ⭐ NEW!
  • CLI Generation System: Create command-line interfaces for converters ⭐ NEW!
  • File support: Convert YAML, JSON files directly
  • Statistics: Get detailed conversion reports
  • TypeScript: Full type safety

🔄 Current Implementation Status

✅ Working Features:

  • Basic field mapping with map()
  • Nested object and array access
  • Value transformations
  • Default values with defaults() ⭐ ENHANCED!
  • Array/object processing with forEach()
  • Conditional mapping with when()
  • Multi-field merging with merge()
  • Field validation with validate()
  • Lifecycle hooks with before() and after() ⭐ NEW!
  • Advanced error handling and reporting system ⭐ NEW!
  • CLI generation system with command-line interfaces ⭐ ENHANCED!
  • File parsing (YAML, JSON)
  • Conversion statistics and reporting
  • Async and sync conversion support

🚧 Coming Soon:

  • Plugin system

💡 Tips

  1. You don't need to use the Mapper class directly - just use converter.convert()
  2. Use dot notation for nested objects: 'user.profile.name'
  3. Use bracket notation for arrays: 'items[0]', 'items[1]'
  4. Transform functions get the value and context: (value, ctx) => { ... }
  5. Use conditional mapping with when() for type-specific logic: when(source => source.accountType === 'premium')
  6. Use merge() to combine multiple fields: merge(['field1', 'field2'], 'result', (a, b) => a + b)
  7. Use validation to ensure data quality: validate('email', validators.email)
  8. Combine validators with validators.all() for multiple rules
  9. Always call .end() after conditional mappings to return to the main converter
  10. Check result.errors to see validation failures
  11. Check result.unmapped to see which fields weren't mapped
  12. Use result.print() for a nice conversion report
  13. Use defaults() to provide fallback values for missing fields: defaults({ status: 'active' })
  14. Use function defaults for dynamic values: defaults({ timestamp: () => new Date().toISOString() })
  15. Use before() hooks to preprocess source data before conversion
  16. Use after() hooks to postprocess target data after conversion
  17. Hooks can be async - just use async (data) => { ... } and they'll be awaited
  18. Multiple hooks execute in order - add as many as you need for complex workflows
  19. Error handling provides helpful suggestions - when field mapping fails, you'll get suggestions for similar field names
  20. Errors include rich context - see exactly where and why conversions failed with detailed error information
  21. Use error categories to filter and handle different types of errors (validation, mapping, parsing, etc.)
  22. Create CLIs for converters - use CLIGenerator.forConverter(converter) to generate command-line interfaces
  23. Use CLI profiles - save converter configurations as profiles for reuse: cli profile save my-converter
  24. CLI supports batch processing - convert multiple files at once with pattern matching and parallel processing

That's it! ConfigForge makes config conversion simple and straightforward. No complex setup, no direct class manipulation - just define your mappings and convert.

License

This package is licensed under the PolyForm Noncommercial License 1.0.0.
See the LICENSE file for full terms.