purify-objects
Version:
A powerful TypeScript library for cleaning objects by removing empty values, with support for YAML and CSV formats
205 lines (175 loc) • 7.98 kB
text/typescript
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { cleanObject } from './index';
import { parseYAML, parseCSV, stringifyYAML, stringifyCSV } from './parsers';
import { FileFormat, ParserOptions, AnyObject } from './types';
interface CliOptions {
inputFile: string;
outputFile: string | null;
compareMode: boolean;
safeMode: boolean;
format: FileFormat;
convertTo: FileFormat | null;
delimiter: string;
headers: boolean;
removeZeroValues: boolean;
noClean: boolean;
}
const VALID_FORMATS = ['json', 'yaml', 'csv'] as const;
type FormatExtensions = Record<FileFormat, readonly string[]>;
const FORMAT_EXTENSIONS: FormatExtensions = {
json: ['json'],
yaml: ['yaml', 'yml'],
csv: ['csv']
} as const;
const detectFileFormat = (filename: string): FileFormat => {
const ext = path.extname(filename).toLowerCase().slice(1);
for (const [format, extensions] of Object.entries(FORMAT_EXTENSIONS)) {
if ((extensions as readonly string[]).includes(ext)) {
return format as FileFormat;
}
}
return 'json';
};
const getDefaultOutputFilename = (inputFile: string, format: FileFormat): string => {
const parsedPath = path.parse(inputFile);
const extension = format === 'yaml' ? '.yaml' : `.${format}`;
return path.join(parsedPath.dir, `${parsedPath.name}${extension}`);
};
const extractCliOptions = (args: string[]): CliOptions => {
const inputFile = args[0];
const formatIndex = args.indexOf('--format');
const formatValue = formatIndex !== -1 ? args[formatIndex + 1] : detectFileFormat(inputFile);
const convertToIndex = args.indexOf('--convert-to');
const convertToValue = convertToIndex !== -1 ? args[convertToIndex + 1] : null;
if (formatValue && !VALID_FORMATS.includes(formatValue as FileFormat)) {
console.error(chalk.red(`Error: Invalid format "${formatValue}". Valid formats are: ${VALID_FORMATS.join(', ')}`));
process.exit(1);
}
if (convertToValue && !VALID_FORMATS.includes(convertToValue as FileFormat)) {
console.error(chalk.red(`Error: Invalid conversion format "${convertToValue}". Valid formats are: ${VALID_FORMATS.join(', ')}`));
process.exit(1);
}
const outputIndex = args.indexOf('--output');
let outputFile = outputIndex !== -1 ? args[outputIndex + 1] : null;
if (convertToValue && !outputFile) {
outputFile = getDefaultOutputFilename(inputFile, convertToValue as FileFormat);
}
return {
inputFile,
outputFile,
compareMode: args.includes('--compare'),
safeMode: args.includes('--safe'),
format: formatValue as FileFormat,
convertTo: convertToValue as FileFormat | null,
delimiter: args.indexOf('--delimiter') !== -1 ? args[args.indexOf('--delimiter') + 1] : ',',
headers: !args.includes('--no-headers'),
removeZeroValues: args.includes('--remove-zero-values'),
noClean: args.includes('--noclean')
};
};
const parseContent = (content: string, format: FileFormat, options: ParserOptions): AnyObject | AnyObject[] => {
try {
switch (format) {
case 'yaml':
return parseYAML(content);
case 'csv':
return parseCSV(content, { delimiter: options.delimiter || ',', headers: options.headers !== false });
default:
return JSON.parse(content);
}
} catch (error: any) {
console.error(chalk.red(`Error parsing ${format.toUpperCase()} content: ${error.message}`));
process.exit(1);
}
};
const stringifyContent = (data: AnyObject | AnyObject[], format: FileFormat, options: ParserOptions): string => {
try {
switch (format) {
case 'yaml':
return stringifyYAML(data as AnyObject);
case 'csv':
return stringifyCSV(Array.isArray(data) ? data : [data], {
delimiter: options.delimiter || ',',
headers: options.headers !== false
});
default:
return JSON.stringify(data, null, 2);
}
} catch (error: any) {
console.error(chalk.red(`Error converting to ${format.toUpperCase()}: ${error.message}`));
process.exit(1);
}
};
const generateFieldMap = (original: any, cleaned: any, prefix = ''): string[] => (
Object.entries(original).reduce((acc: string[], [key, value]) => {
const currentPath = prefix ? `${prefix}.${key}` : key;
if (!(key in cleaned)) {
return [...acc, currentPath];
}
if (typeof value === 'object' && value !== null && typeof cleaned[key] === 'object' && cleaned[key] !== null) {
return [...acc, ...generateFieldMap(value, cleaned[key], currentPath)];
}
return acc;
}, [])
);
const executeCliOperation = (options: CliOptions): void => {
const { inputFile, outputFile, compareMode, safeMode, format, convertTo, delimiter, headers, removeZeroValues, noClean } = options;
if (!inputFile) {
console.error(chalk.red('Please provide an input file'));
console.log(chalk.yellow(
'Usage: npx purify-objects input.file [--output cleaned.file] [--compare] [--safe] ' +
`[--format ${VALID_FORMATS.join('|')}] [--convert-to ${VALID_FORMATS.join('|')}] ` +
'[--delimiter ","] [--no-headers] [--remove-zero-values] [--noclean]'
));
process.exit(1);
}
try {
const inputPath = path.resolve(process.cwd(), inputFile);
if (!fs.existsSync(inputPath)) {
console.error(chalk.red(`Error: File not found: ${inputFile}`));
process.exit(1);
}
const content = fs.readFileSync(inputPath, 'utf8');
const sourceData = parseContent(content, format, { delimiter, headers });
const customCleaner = removeZeroValues ? (key: string, value: any) => value === 0 : undefined;
const processedData = noClean ? sourceData : Array.isArray(sourceData)
? sourceData.map(item => cleanObject(item, customCleaner, [], { safe: safeMode }))
: cleanObject(sourceData, customCleaner, [], { safe: safeMode });
if (compareMode) {
safeMode && console.log(chalk.blue('\nSafe Mode: Original file will not be modified'));
console.log(chalk.yellow('\nOriginal data:'), stringifyContent(sourceData, format, { delimiter, headers }));
console.log(chalk.green('\nCleaned data (preview):'), stringifyContent(processedData, format, { delimiter, headers }));
if (!Array.isArray(sourceData)) {
const modifications = generateFieldMap(sourceData, processedData);
if (modifications.length) {
console.log(chalk.red('\nFields to be removed:'));
modifications.forEach(f => console.log(chalk.red(`- ${f}`)));
}
}
safeMode && console.log(chalk.blue('\nNo changes were made to the original file (Safe Mode)'));
process.exit(0);
}
if (outputFile) {
const outputPath = path.resolve(process.cwd(), outputFile);
const outputFormat = convertTo || format;
fs.writeFileSync(outputPath, stringifyContent(processedData, outputFormat, { delimiter, headers }));
if (convertTo) {
console.log(chalk.green(`\nFile successfully ${noClean ? 'converted' : 'converted and cleaned'}:`));
console.log(chalk.blue(`Input: ${inputFile} (${format.toUpperCase()})`));
console.log(chalk.blue(`Output: ${outputFile} (${convertTo.toUpperCase()})`));
} else {
safeMode && console.log(chalk.blue('Safe Mode: Created new file without modifying original'));
console.log(chalk.green(`${noClean ? 'Data' : 'Cleaned data'} saved to ${outputFile}`));
}
return;
}
console.log(stringifyContent(processedData, format, { delimiter, headers }));
} catch (error: any) {
console.error(chalk.red('Error:'), error?.message || 'Unknown error occurred');
process.exit(1);
}
};
executeCliOperation(extractCliOptions(process.argv.slice(2)));