@cerios/csv-nested-json
Advanced tools
+1014
-27
@@ -1,61 +0,1048 @@ | ||
| import { Readable } from 'node:stream'; | ||
| import { Readable, TransformOptions, Transform, TransformCallback } from 'node:stream'; | ||
| /** | ||
| * Validation mode for handling rows with more values than headers. | ||
| * - `'ignore'`: Silently ignore extra values | ||
| * - `'warn'`: Log a warning to console (default) | ||
| * - `'error'`: Throw a CsvValidationError | ||
| */ | ||
| type ValidationMode = "ignore" | "warn" | "error"; | ||
| /** | ||
| * Behavior for handling forced array fields that have no values. | ||
| * - `'empty-array'`: Create an empty array `[]` | ||
| * - `'omit'`: Omit the field entirely (default) | ||
| */ | ||
| type EmptyArrayBehavior = "empty-array" | "omit"; | ||
| /** | ||
| * Mode for converting arrays back to CSV format. | ||
| * - `'rows'`: Output arrays as continuation rows (matches parser format) | ||
| * - `'json'`: JSON-stringify arrays into a single cell | ||
| */ | ||
| type ArrayMode = "rows" | "json"; | ||
| /** | ||
| * A flat CSV record with string keys and string values. | ||
| * Represents a single row parsed from CSV before nesting conversion. | ||
| */ | ||
| type CsvRecord = Record<string, string>; | ||
| /** | ||
| * A nested object structure produced by the CSV parser. | ||
| * Values can be primitives, arrays, dates, or nested objects. | ||
| */ | ||
| type NestedValue = string | number | boolean | null | Date | NestedObject | NestedValue[]; | ||
| /** | ||
| * A nested JSON object with string keys and nested values. | ||
| */ | ||
| interface NestedObject { | ||
| [key: string]: NestedValue; | ||
| } | ||
| /** | ||
| * Function type for transforming individual cell values. | ||
| * Receives the current value (after auto-parsing if enabled) and the header name. | ||
| * | ||
| * @param value - The cell value (may be string, number, or boolean after auto-parsing) | ||
| * @param header - The column header name | ||
| * @returns The transformed value | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Convert specific columns to uppercase | ||
| * const transformer: ValueTransformer = (value, header) => { | ||
| * if (header === 'name' && typeof value === 'string') { | ||
| * return value.toUpperCase(); | ||
| * } | ||
| * return value; | ||
| * }; | ||
| * ``` | ||
| */ | ||
| type ValueTransformer = (value: string | number | boolean, header: string) => unknown; | ||
| /** | ||
| * Function type for transforming header names during parsing. | ||
| * Receives the original header name and returns the transformed name. | ||
| * | ||
| * @param header - The original header name from the CSV | ||
| * @returns The transformed header name | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Convert headers to camelCase | ||
| * const transformer: HeaderTransformer = (header) => { | ||
| * return header.replace(/[-_](.)/g, (_, c) => c.toUpperCase()); | ||
| * }; | ||
| * ``` | ||
| */ | ||
| type HeaderTransformer = (header: string) => string; | ||
| /** | ||
| * Function type for filtering rows during parsing. | ||
| * Receives the parsed record and returns true to include, false to exclude. | ||
| * | ||
| * @param record - The parsed record (flat, before nesting) | ||
| * @param rowIndex - The 0-based index of the data row (excludes header and skipped rows) | ||
| * @returns true to include the row, false to exclude it | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Only include rows where status is 'active' | ||
| * const filter: RowFilter = (record) => record.status === 'active'; | ||
| * ``` | ||
| */ | ||
| type RowFilter = (record: CsvRecord, rowIndex: number) => boolean; | ||
| /** | ||
| * Representation for null values in output. | ||
| * - `'null'`: Use JavaScript null | ||
| * - `'undefined'`: Use JavaScript undefined | ||
| * - `'empty-string'`: Use empty string '' | ||
| * - `'omit'`: Omit the field entirely (default) | ||
| */ | ||
| type NullRepresentation = "null" | "undefined" | "empty-string" | "omit"; | ||
| /** | ||
| * Configuration options for CSV parsing and conversion. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const options: CsvParserOptions = { | ||
| * delimiter: ';', | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true, | ||
| * skipRows: 2, | ||
| * stripBom: true | ||
| * }; | ||
| * | ||
| * const result = CsvParser.parseString(csvContent, options); | ||
| * ``` | ||
| */ | ||
| interface CsvParserOptions { | ||
| /** | ||
| * How to handle rows with more values than headers. | ||
| * - 'ignore': Silently ignore extra values | ||
| * - 'warn': Log a warning to console (default) | ||
| * - 'error': Throw an error | ||
| * - `'ignore'`: Silently ignore extra values | ||
| * - `'warn'`: Log a warning to console (default) | ||
| * - `'error'`: Throw a CsvValidationError | ||
| */ | ||
| validationMode?: ValidationMode; | ||
| /** | ||
| * Field delimiter character (default: ',') | ||
| * Field delimiter character. | ||
| * @default ',' | ||
| */ | ||
| delimiter?: string; | ||
| /** | ||
| * Quote character for escaping fields (default: '"') | ||
| * Quote character for escaping fields containing delimiters or newlines. | ||
| * @default '"' | ||
| */ | ||
| quote?: string; | ||
| /** | ||
| * File encoding when reading from file (default: 'utf-8') | ||
| * File encoding when reading from file. | ||
| * @default 'utf-8' | ||
| */ | ||
| encoding?: BufferEncoding; | ||
| /** | ||
| * Suffix indicator in column headers to force array type. | ||
| * Fields with this suffix will always be arrays, even with single values. | ||
| * @default '[]' | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Header: 'person.children[].name' | ||
| * // Result: { person: { children: [{ name: '...' }] } } | ||
| * ``` | ||
| */ | ||
| arraySuffixIndicator?: string; | ||
| /** | ||
| * How to handle forced array fields with no values. | ||
| * - `'empty-array'`: Create an empty array `[]` | ||
| * - `'omit'`: Omit the field entirely (default) | ||
| */ | ||
| emptyArrayBehavior?: EmptyArrayBehavior; | ||
| /** | ||
| * Number of rows to skip before the header row. | ||
| * Useful for files with metadata or comments at the top. | ||
| * @default 0 | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Skip 2 rows of metadata before the header | ||
| * CsvParser.parseString(csv, { skipRows: 2 }); | ||
| * ``` | ||
| */ | ||
| skipRows?: number; | ||
| /** | ||
| * Automatically strip BOM (Byte Order Mark) from the beginning of content. | ||
| * Handles UTF-8 BOM (\uFEFF) and UTF-16 BOMs. | ||
| * @default true | ||
| */ | ||
| stripBom?: boolean; | ||
| /** | ||
| * Automatically convert numeric strings to numbers. | ||
| * Applies to values that can be parsed as valid numbers. | ||
| * @default false | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // '42' becomes 42, '3.14' becomes 3.14 | ||
| * CsvParser.parseString(csv, { autoParseNumbers: true }); | ||
| * ``` | ||
| */ | ||
| autoParseNumbers?: boolean; | ||
| /** | ||
| * Automatically convert 'true'/'false' strings to booleans. | ||
| * Case-insensitive matching. | ||
| * @default false | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // 'true' becomes true, 'FALSE' becomes false | ||
| * CsvParser.parseString(csv, { autoParseBooleans: true }); | ||
| * ``` | ||
| */ | ||
| autoParseBooleans?: boolean; | ||
| /** | ||
| * Custom function to transform values after parsing. | ||
| * Called after autoParseNumbers and autoParseBooleans (if enabled). | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * CsvParser.parseString(csv, { | ||
| * valueTransformer: (value, header) => { | ||
| * if (header === 'date') return new Date(value as string); | ||
| * return value; | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| valueTransformer?: ValueTransformer; | ||
| /** | ||
| * Mode for converting arrays to CSV (used by JsonToCsv). | ||
| * - `'rows'`: Output arrays as continuation rows (default, matches parser) | ||
| * - `'json'`: JSON-stringify arrays into a single cell | ||
| * @default 'rows' | ||
| */ | ||
| arrayMode?: ArrayMode; | ||
| /** | ||
| * Transform header names before processing. | ||
| * Applied to each header after reading from CSV. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Convert headers to lowercase | ||
| * CsvParser.parseString(csv, { | ||
| * headerTransformer: (header) => header.toLowerCase() | ||
| * }); | ||
| * ``` | ||
| */ | ||
| headerTransformer?: HeaderTransformer; | ||
| /** | ||
| * Map column names to new names. | ||
| * Applied after headerTransformer (if specified). | ||
| * Keys are original names, values are new names. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * CsvParser.parseString(csv, { | ||
| * columnMapping: { | ||
| * 'First Name': 'firstName', | ||
| * 'Last Name': 'lastName' | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| columnMapping?: Record<string, string>; | ||
| /** | ||
| * Filter rows during parsing. | ||
| * Return true to include the row, false to exclude it. | ||
| * Applied after parsing but before nesting conversion. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * CsvParser.parseString(csv, { | ||
| * rowFilter: (record) => record.status !== 'deleted' | ||
| * }); | ||
| * ``` | ||
| */ | ||
| rowFilter?: RowFilter; | ||
| /** | ||
| * Default values for columns. | ||
| * Applied when a cell is empty. | ||
| * Keys are column names (after transformation/mapping). | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * CsvParser.parseString(csv, { | ||
| * defaultValues: { | ||
| * status: 'pending', | ||
| * count: '0' | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| defaultValues?: Record<string, string>; | ||
| /** | ||
| * Automatically parse date strings to Date objects. | ||
| * Uses JavaScript's Date.parse() for recognition. | ||
| * @default false | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // '2024-01-15' becomes Date object | ||
| * CsvParser.parseString(csv, { autoParseDates: true }); | ||
| * ``` | ||
| */ | ||
| autoParseDates?: boolean; | ||
| /** | ||
| * Values to treat as null. | ||
| * Case-insensitive matching. | ||
| * @default ['null', 'NULL', 'nil', 'NIL', ''] | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * CsvParser.parseString(csv, { | ||
| * nullValues: ['null', 'N/A', '-', ''] | ||
| * }); | ||
| * ``` | ||
| */ | ||
| nullValues?: string[]; | ||
| /** | ||
| * How to represent null values in the output. | ||
| * - `'null'`: Use JavaScript null | ||
| * - `'undefined'`: Use JavaScript undefined | ||
| * - `'empty-string'`: Use empty string '' | ||
| * - `'omit'`: Omit the field entirely (default) | ||
| * @default 'omit' | ||
| */ | ||
| nullRepresentation?: NullRepresentation; | ||
| } | ||
| /** | ||
| * High-level CSV to nested JSON parser | ||
| * Combines file I/O, CSV parsing, and nested JSON conversion | ||
| * File and stream I/O operations for CSV parsing. | ||
| * Handles reading CSV content from files and streams with configurable encoding. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Read file synchronously | ||
| * const content = CsvFileReader.readFileSync('data.csv'); | ||
| * | ||
| * // Read file asynchronously | ||
| * const content = await CsvFileReader.readFile('data.csv'); | ||
| * | ||
| * // Read from stream | ||
| * const stream = fs.createReadStream('data.csv'); | ||
| * const content = await CsvFileReader.readStream(stream); | ||
| * ``` | ||
| */ | ||
| declare class CsvFileReader { | ||
| /** | ||
| * Read CSV file synchronously. | ||
| * | ||
| * @param filePath - Path to the CSV file | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns The file content as a string | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const content = CsvFileReader.readFileSync('data.csv', { encoding: 'utf-8' }); | ||
| * ``` | ||
| */ | ||
| static readFileSync(filePath: string, options?: CsvParserOptions): string; | ||
| /** | ||
| * Read CSV file asynchronously. | ||
| * | ||
| * @param filePath - Path to the CSV file | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns Promise resolving to the file content as a string | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const content = await CsvFileReader.readFile('data.csv', { encoding: 'utf-16le' }); | ||
| * ``` | ||
| */ | ||
| static readFile(filePath: string, options?: CsvParserOptions): Promise<string>; | ||
| /** | ||
| * Read CSV from a readable stream. | ||
| * | ||
| * @param stream - Readable stream containing CSV data | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns Promise resolving to the stream content as a string | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const stream = fs.createReadStream('large-file.csv'); | ||
| * const content = await CsvFileReader.readStream(stream); | ||
| * ``` | ||
| */ | ||
| static readStream(stream: Readable, options?: CsvParserOptions): Promise<string>; | ||
| } | ||
| /** | ||
| * High-level CSV to nested JSON parser. | ||
| * Combines file I/O, CSV parsing, and nested JSON conversion into a single API. | ||
| * | ||
| * The parser supports: | ||
| * - Dot-notation headers for nested objects (`person.address.city`) | ||
| * - Automatic array detection when values collide | ||
| * - Forced array fields with `[]` suffix (`tags[]`) | ||
| * - Continuation rows (rows with empty first column extend previous record) | ||
| * - Value transformation (auto-parse numbers, booleans, custom transformers) | ||
| * - BOM stripping and row skipping | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Parse from file | ||
| * const result = CsvParser.parseFileSync('data.csv'); | ||
| * | ||
| * // Parse from string with options | ||
| * const result = CsvParser.parseString(csvContent, { | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true, | ||
| * skipRows: 1 | ||
| * }); | ||
| * | ||
| * // Parse with custom value transformer | ||
| * const result = CsvParser.parseString(csv, { | ||
| * valueTransformer: (value, header) => { | ||
| * if (header === 'date') return new Date(value as string); | ||
| * return value; | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| declare abstract class CsvParser { | ||
| /** | ||
| * Parse CSV file synchronously to nested JSON | ||
| * @param csvFilePath Path to CSV file | ||
| * @param options Parsing options | ||
| * Parse CSV file synchronously to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvFilePath - Path to the CSV file | ||
| * @param options - Parsing options | ||
| * @returns Array of nested JSON objects | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * interface Person { | ||
| * id: string; | ||
| * name: string; | ||
| * address: { city: string }; | ||
| * } | ||
| * | ||
| * const people = CsvParser.parseFileSync<Person>('people.csv'); | ||
| * console.log(people[0].address.city); | ||
| * ``` | ||
| */ | ||
| static parseFileSync(csvFilePath: string, options?: CsvParserOptions): any[]; | ||
| static parseFileSync<T = NestedObject>(csvFilePath: string, options?: CsvParserOptions): T[]; | ||
| /** | ||
| * Parse CSV file asynchronously to nested JSON | ||
| * @param csvFilePath Path to CSV file | ||
| * @param options Parsing options | ||
| * Parse CSV file asynchronously to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvFilePath - Path to the CSV file | ||
| * @param options - Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const people = await CsvParser.parseFile<Person>('people.csv'); | ||
| * ``` | ||
| */ | ||
| static parseFile(csvFilePath: string, options?: CsvParserOptions): Promise<any[]>; | ||
| static parseFile<T = NestedObject>(csvFilePath: string, options?: CsvParserOptions): Promise<T[]>; | ||
| /** | ||
| * Parse CSV string content to nested JSON | ||
| * @param csvContent CSV content as string | ||
| * @param options Parsing options | ||
| * Parse CSV from readable stream to nested JSON. | ||
| * Note: This method buffers the entire stream before parsing. | ||
| * For true streaming with large files, use {@link CsvStreamParser}. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param stream - Readable stream containing CSV data | ||
| * @param options - Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const stream = fs.createReadStream('large-file.csv'); | ||
| * const result = await CsvParser.parseStream(stream); | ||
| * ``` | ||
| */ | ||
| static parseStream<T = NestedObject>(stream: Readable, options?: CsvParserOptions): Promise<T[]>; | ||
| /** | ||
| * Parse CSV string content to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvContent - CSV content as string | ||
| * @param options - Parsing options | ||
| * @returns Array of nested JSON objects | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const csv = `id,name,address.city | ||
| * 1,John,NYC | ||
| * 2,Jane,LA`; | ||
| * | ||
| * const result = CsvParser.parseString(csv); | ||
| * // [ | ||
| * // { id: '1', name: 'John', address: { city: 'NYC' } }, | ||
| * // { id: '2', name: 'Jane', address: { city: 'LA' } } | ||
| * // ] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With continuation rows for arrays | ||
| * const csv = `id,tags | ||
| * 1,javascript | ||
| * ,typescript | ||
| * ,nodejs`; | ||
| * | ||
| * const result = CsvParser.parseString(csv); | ||
| * // [{ id: '1', tags: ['javascript', 'typescript', 'nodejs'] }] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With auto-parsing | ||
| * const csv = `id,active,score | ||
| * 1,true,95.5`; | ||
| * | ||
| * const result = CsvParser.parseString(csv, { | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true | ||
| * }); | ||
| * // [{ id: 1, active: true, score: 95.5 }] | ||
| * ``` | ||
| */ | ||
| static parseString(csvContent: string, options?: CsvParserOptions): any[]; | ||
| static parseString<T = NestedObject>(csvContent: string, options?: CsvParserOptions): T[]; | ||
| } | ||
| /** | ||
| * Low-level CSV parsing utilities. | ||
| * Handles parsing CSV content into flat record objects. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const records = CsvReader.parse('id,name\n1,Alice\n2,Bob'); | ||
| * // [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }] | ||
| * ``` | ||
| */ | ||
| declare class CsvReader { | ||
| /** | ||
| * Parse CSV from readable stream to nested JSON | ||
| * @param stream Readable stream containing CSV data | ||
| * @param options Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * Parse CSV content into an array of flat record objects. | ||
| * Each record maps header names to cell values. | ||
| * | ||
| * @param content - The CSV content as a string | ||
| * @param options - Parser options | ||
| * @returns Array of flat record objects with string values | ||
| * @throws {CsvValidationError} If validationMode is 'error' and row has too many columns | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Basic parsing | ||
| * const records = CsvReader.parse('id,name\n1,Alice'); | ||
| * // [{ id: '1', name: 'Alice' }] | ||
| * | ||
| * // With BOM stripping and skip rows | ||
| * const records = CsvReader.parse(content, { | ||
| * stripBom: true, | ||
| * skipRows: 2 | ||
| * }); | ||
| * ``` | ||
| */ | ||
| static parseStream(stream: Readable, options?: CsvParserOptions): Promise<any[]>; | ||
| static parse(content: string, options?: CsvParserOptions): CsvRecord[]; | ||
| /** | ||
| * Strip BOM (Byte Order Mark) from the beginning of content. | ||
| * Handles UTF-8 and UTF-16 BOMs. | ||
| * | ||
| * @param content - The content that may contain a BOM | ||
| * @returns Content with BOM removed | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const clean = CsvReader.stripBom('\uFEFFid,name'); | ||
| * // 'id,name' | ||
| * ``` | ||
| */ | ||
| static stripBom(content: string): string; | ||
| /** | ||
| * Split CSV content into lines, respecting quoted fields that may contain newlines. | ||
| * | ||
| * @param content - The CSV content | ||
| * @returns Array of lines (quoted newlines are preserved within lines) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const lines = CsvReader.splitLines('id,note\n1,"Line 1\nLine 2"'); | ||
| * // ['id,note', '1,"Line 1\nLine 2"'] | ||
| * ``` | ||
| */ | ||
| static splitLines(content: string): string[]; | ||
| /** | ||
| * Parse a single CSV line into an array of values, respecting quotes and delimiters. | ||
| * | ||
| * @param line - A single line of CSV data | ||
| * @param delimiter - The field delimiter (default: ',') | ||
| * @param quote - The quote character (default: '"') | ||
| * @returns Array of parsed values | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const values = CsvReader.parseLine('1,"Hello, World",test'); | ||
| * // ['1', 'Hello, World', 'test'] | ||
| * | ||
| * // Escaped quotes | ||
| * const values = CsvReader.parseLine('1,"Say ""Hello""",test'); | ||
| * // ['1', 'Say "Hello"', 'test'] | ||
| * ``` | ||
| */ | ||
| static parseLine(line: string, delimiter?: string, quote?: string): string[]; | ||
| } | ||
| export { CsvParser, type CsvParserOptions, type ValidationMode }; | ||
| /** | ||
| * Options for the streaming CSV parser. | ||
| * Extends standard TransformOptions with CSV-specific options. | ||
| */ | ||
| interface CsvStreamParserOptions extends CsvParserOptions, TransformOptions { | ||
| /** | ||
| * Whether to emit nested objects (true) or flat records (false). | ||
| * When true, records are converted using NestedJsonConverter logic. | ||
| * When false, raw flat records are emitted. | ||
| * @default true | ||
| */ | ||
| nested?: boolean; | ||
| } | ||
| /** | ||
| * A streaming CSV parser that processes CSV data chunk by chunk. | ||
| * Emits parsed records as they become available, without buffering the entire file. | ||
| * | ||
| * This is ideal for processing very large CSV files that don't fit in memory. | ||
| * The parser handles quoted fields that span multiple chunks correctly. | ||
| * | ||
| * Note: For true nested JSON conversion with continuation rows (rows that extend | ||
| * previous records), consider using the standard CsvParser methods, as continuation | ||
| * row handling requires buffering related rows together. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * import { createReadStream } from 'node:fs'; | ||
| * import { CsvStreamParser } from '@cerios/csv-nested-json'; | ||
| * | ||
| * const parser = new CsvStreamParser({ delimiter: ',' }); | ||
| * | ||
| * createReadStream('large-file.csv') | ||
| * .pipe(parser) | ||
| * .on('data', (record) => { | ||
| * console.log('Parsed record:', record); | ||
| * }) | ||
| * .on('end', () => { | ||
| * console.log('Done parsing'); | ||
| * }) | ||
| * .on('error', (err) => { | ||
| * console.error('Parse error:', err); | ||
| * }); | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Collect all records | ||
| * const records: NestedObject[] = []; | ||
| * const parser = new CsvStreamParser(); | ||
| * | ||
| * for await (const record of readStream.pipe(parser)) { | ||
| * records.push(record); | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare class CsvStreamParser extends Transform { | ||
| private buffer; | ||
| private headers; | ||
| private headersProcessed; | ||
| private rowsSkipped; | ||
| private dataRowIndex; | ||
| private options; | ||
| private delimiter; | ||
| private quote; | ||
| private skipRows; | ||
| private stripBom; | ||
| private bomStripped; | ||
| private nullSet; | ||
| /** | ||
| * Creates a new streaming CSV parser. | ||
| * | ||
| * @param options - Parser options | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const parser = new CsvStreamParser({ | ||
| * delimiter: ';', | ||
| * quote: '"', | ||
| * skipRows: 1, | ||
| * autoParseNumbers: true | ||
| * }); | ||
| * ``` | ||
| */ | ||
| constructor(options?: CsvStreamParserOptions); | ||
| /** | ||
| * Transform implementation - processes incoming chunks. | ||
| * @internal | ||
| */ | ||
| _transform(chunk: Buffer | string, _encoding: BufferEncoding, callback: TransformCallback): void; | ||
| /** | ||
| * Flush implementation - processes any remaining data. | ||
| * @internal | ||
| */ | ||
| _flush(callback: TransformCallback): void; | ||
| /** | ||
| * Strip BOM from the beginning of a string. | ||
| */ | ||
| private stripBomFromString; | ||
| /** | ||
| * Process the buffer and extract complete lines. | ||
| */ | ||
| private processBuffer; | ||
| /** | ||
| * Process a single line of CSV data. | ||
| */ | ||
| private processLine; | ||
| /** | ||
| * Parse a single line into values. | ||
| */ | ||
| private parseLine; | ||
| /** | ||
| * Create a record object from values array. | ||
| */ | ||
| private createRecord; | ||
| /** | ||
| * Apply value transformations to a record. | ||
| */ | ||
| private applyTransformations; | ||
| /** | ||
| * Apply null representation based on option. | ||
| */ | ||
| private applyNullRepresentation; | ||
| /** | ||
| * Try to parse a string as a number. | ||
| */ | ||
| private tryParseNumber; | ||
| /** | ||
| * Try to parse a string as a boolean. | ||
| */ | ||
| private tryParseBoolean; | ||
| /** | ||
| * Try to parse a string as a Date. | ||
| * Uses JavaScript's Date.parse() for recognition. | ||
| */ | ||
| private tryParseDate; | ||
| /** | ||
| * Unflatten a record with dot-notation keys into a nested object. | ||
| */ | ||
| private unflatten; | ||
| } | ||
| /** | ||
| * Base error class for CSV parsing errors. | ||
| * Provides context about where the error occurred. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * CsvParser.parseString(csvContent); | ||
| * } catch (error) { | ||
| * if (error instanceof CsvParseError) { | ||
| * console.log(`Error at row ${error.row}: ${error.message}`); | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare class CsvParseError extends Error { | ||
| readonly row?: number | undefined; | ||
| readonly column?: number | undefined; | ||
| /** | ||
| * Creates a new CSV parse error | ||
| * @param message - Human-readable error description | ||
| * @param row - The 1-based row number where the error occurred (optional) | ||
| * @param column - The 1-based column number where the error occurred (optional) | ||
| */ | ||
| constructor(message: string, row?: number | undefined, column?: number | undefined); | ||
| } | ||
| /** | ||
| * Error thrown when a CSV file cannot be found at the specified path. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * CsvParser.parseFileSync('/path/to/missing.csv'); | ||
| * } catch (error) { | ||
| * if (error instanceof CsvFileNotFoundError) { | ||
| * console.log(`File not found: ${error.filePath}`); | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare class CsvFileNotFoundError extends CsvParseError { | ||
| readonly filePath: string; | ||
| /** | ||
| * Creates a new file not found error | ||
| * @param filePath - The path to the file that was not found | ||
| */ | ||
| constructor(filePath: string); | ||
| } | ||
| /** | ||
| * Error thrown when CSV data fails validation rules. | ||
| * Contains details about the expected vs actual column counts. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * CsvParser.parseString(csvContent, { validationMode: 'error' }); | ||
| * } catch (error) { | ||
| * if (error instanceof CsvValidationError) { | ||
| * console.log(`Row ${error.row}: expected ${error.expectedColumns} columns, got ${error.actualColumns}`); | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare class CsvValidationError extends CsvParseError { | ||
| readonly expectedColumns: number; | ||
| readonly actualColumns: number; | ||
| /** | ||
| * Creates a new validation error | ||
| * @param message - Human-readable error description | ||
| * @param row - The 1-based row number where validation failed | ||
| * @param expectedColumns - The expected number of columns (from header) | ||
| * @param actualColumns - The actual number of columns found in the row | ||
| */ | ||
| constructor(message: string, row: number, expectedColumns: number, actualColumns: number); | ||
| } | ||
| /** | ||
| * Error thrown when there are encoding or BOM-related issues. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * CsvParser.parseFileSync('file.csv', { encoding: 'utf-16' }); | ||
| * } catch (error) { | ||
| * if (error instanceof CsvEncodingError) { | ||
| * console.log(`Encoding error: ${error.message}`); | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare class CsvEncodingError extends CsvParseError { | ||
| readonly encoding?: string | undefined; | ||
| /** | ||
| * Creates a new encoding error | ||
| * @param message - Human-readable error description | ||
| * @param encoding - The encoding that was being used | ||
| */ | ||
| constructor(message: string, encoding?: string | undefined); | ||
| } | ||
| /** | ||
| * Options for JSON to CSV conversion. | ||
| */ | ||
| interface JsonToCsvOptions extends Pick<CsvParserOptions, "delimiter" | "quote" | "encoding" | "arrayMode"> { | ||
| /** | ||
| * Line ending to use in output. | ||
| * @default '\n' | ||
| */ | ||
| lineEnding?: "\n" | "\r\n"; | ||
| /** | ||
| * Whether to include a header row. | ||
| * @default true | ||
| */ | ||
| includeHeader?: boolean; | ||
| } | ||
| /** | ||
| * Converts nested JSON objects back to CSV format. | ||
| * Supports the reverse operation of CsvParser, including: | ||
| * - Nested objects converted to dot-notation headers | ||
| * - Arrays output as continuation rows (default) or JSON-stringified | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const data = [ | ||
| * { id: '1', person: { name: 'John', city: 'NYC' } }, | ||
| * { id: '2', person: { name: 'Jane', city: 'LA' } } | ||
| * ]; | ||
| * | ||
| * const csv = JsonToCsv.stringify(data); | ||
| * // id,person.name,person.city | ||
| * // 1,John,NYC | ||
| * // 2,Jane,LA | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With arrays as continuation rows | ||
| * const data = [ | ||
| * { id: '1', tags: ['js', 'ts', 'node'] } | ||
| * ]; | ||
| * | ||
| * const csv = JsonToCsv.stringify(data, { arrayMode: 'rows' }); | ||
| * // id,tags | ||
| * // 1,js | ||
| * // ,ts | ||
| * // ,node | ||
| * ``` | ||
| */ | ||
| declare class JsonToCsv { | ||
| /** | ||
| * Convert array of nested objects to CSV string. | ||
| * | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * @returns CSV string | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const csv = JsonToCsv.stringify([ | ||
| * { id: 1, name: 'Alice', address: { city: 'NYC' } } | ||
| * ]); | ||
| * // "id,name,address.city\n1,Alice,NYC" | ||
| * ``` | ||
| */ | ||
| static stringify(data: NestedObject[], options?: JsonToCsvOptions): string; | ||
| /** | ||
| * Write nested objects to CSV file synchronously. | ||
| * | ||
| * @param filePath - Path to output file | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * JsonToCsv.writeFileSync('output.csv', data, { delimiter: ';' }); | ||
| * ``` | ||
| */ | ||
| static writeFileSync(filePath: string, data: NestedObject[], options?: JsonToCsvOptions): void; | ||
| /** | ||
| * Write nested objects to CSV file asynchronously. | ||
| * | ||
| * @param filePath - Path to output file | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * @returns Promise that resolves when file is written | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await JsonToCsv.writeFile('output.csv', data); | ||
| * ``` | ||
| */ | ||
| static writeFile(filePath: string, data: NestedObject[], options?: JsonToCsvOptions): Promise<void>; | ||
| /** | ||
| * Collect all unique headers from an array of nested objects. | ||
| * Headers are generated using dot-notation for nested properties. | ||
| */ | ||
| private static collectHeaders; | ||
| /** | ||
| * Recursively collect headers from a nested object. | ||
| */ | ||
| private static collectHeadersFromObject; | ||
| /** | ||
| * Flatten a nested object into one or more rows of flat key-value pairs. | ||
| * Arrays result in multiple rows (continuation rows). | ||
| */ | ||
| private static flattenObject; | ||
| /** | ||
| * Flatten a nested object to path-value pairs, separating arrays. | ||
| */ | ||
| private static flattenToPathValues; | ||
| /** | ||
| * Generate continuation rows for arrays. | ||
| * First row contains all values, subsequent rows only contain array continuations. | ||
| */ | ||
| private static generateContinuationRows; | ||
| /** | ||
| * Convert a value to string for CSV output. | ||
| */ | ||
| private static valueToString; | ||
| /** | ||
| * Escape a value for CSV output. | ||
| * Wraps in quotes if the value contains delimiter, quote, or newline. | ||
| */ | ||
| private static escapeValue; | ||
| } | ||
| /** | ||
| * Nested JSON conversion utilities. | ||
| * Converts flat CSV records into nested JSON structures with automatic array detection. | ||
| * | ||
| * Features: | ||
| * - Dot-notation paths in headers become nested objects | ||
| * - Rows with empty first column are continuation rows (extend previous record) | ||
| * - Automatic array detection when values collide during merge | ||
| * - Forced array fields via `[]` suffix in headers | ||
| * - Value transformation (auto-parse numbers, booleans, custom transformers) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const records = [ | ||
| * { 'id': '1', 'person.name': 'John', 'person.age': '30' } | ||
| * ]; | ||
| * | ||
| * const result = NestedJsonConverter.convert(records); | ||
| * // [{ id: '1', person: { name: 'John', age: '30' } }] | ||
| * ``` | ||
| */ | ||
| declare class NestedJsonConverter { | ||
| /** | ||
| * Convert flat CSV records into nested JSON structure with array detection. | ||
| * | ||
| * @param records - Array of flat CSV records with string values | ||
| * @param options - Parser options for customizing conversion | ||
| * @returns Array of nested JSON objects | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Basic conversion with dot notation | ||
| * const records = [{ 'person.name': 'John', 'person.city': 'NYC' }]; | ||
| * const result = NestedJsonConverter.convert(records); | ||
| * // [{ person: { name: 'John', city: 'NYC' } }] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With value transformation | ||
| * const records = [{ id: '1', active: 'true', score: '95.5' }]; | ||
| * const result = NestedJsonConverter.convert(records, { | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true | ||
| * }); | ||
| * // [{ id: 1, active: true, score: 95.5 }] | ||
| * ``` | ||
| */ | ||
| static convert(records: CsvRecord[], options?: CsvParserOptions): NestedObject[]; | ||
| /** | ||
| * Apply value transformations (null detection, auto-parse numbers, booleans, dates, custom transformer). | ||
| * Transformation order: nullValues → autoParseNumbers → autoParseBooleans → autoParseDates → valueTransformer | ||
| */ | ||
| private static applyValueTransformations; | ||
| /** | ||
| * Apply null representation based on option. | ||
| */ | ||
| private static applyNullRepresentation; | ||
| /** | ||
| * Try to parse a string as a number. | ||
| * Returns null if the string is not a valid number. | ||
| */ | ||
| private static tryParseNumber; | ||
| /** | ||
| * Try to parse a string as a boolean. | ||
| * Returns null if the string is not 'true' or 'false' (case-insensitive). | ||
| */ | ||
| private static tryParseBoolean; | ||
| /** | ||
| * Try to parse a string as a Date. | ||
| * Returns null if the string is not a valid date. | ||
| * Uses JavaScript's Date.parse() for recognition. | ||
| */ | ||
| private static tryParseDate; | ||
| private static detectForcedArrayFields; | ||
| private static normalizeHeaders; | ||
| private static processGroup; | ||
| private static unflatten; | ||
| private static deepMerge; | ||
| private static checkIfAllKeysCollide; | ||
| private static shouldCreateArrayOfObjects; | ||
| private static detectArrayFields; | ||
| private static normalizeArrays; | ||
| } | ||
| export { type ArrayMode, CsvEncodingError, CsvFileNotFoundError, CsvFileReader, CsvParseError, CsvParser, type CsvParserOptions, CsvReader, type CsvRecord, CsvStreamParser, type CsvStreamParserOptions, CsvValidationError, type EmptyArrayBehavior, type HeaderTransformer, JsonToCsv, type JsonToCsvOptions, NestedJsonConverter, type NestedObject, type NestedValue, type NullRepresentation, type RowFilter, type ValidationMode, type ValueTransformer }; |
+1014
-27
@@ -1,61 +0,1048 @@ | ||
| import { Readable } from 'node:stream'; | ||
| import { Readable, TransformOptions, Transform, TransformCallback } from 'node:stream'; | ||
| /** | ||
| * Validation mode for handling rows with more values than headers. | ||
| * - `'ignore'`: Silently ignore extra values | ||
| * - `'warn'`: Log a warning to console (default) | ||
| * - `'error'`: Throw a CsvValidationError | ||
| */ | ||
| type ValidationMode = "ignore" | "warn" | "error"; | ||
| /** | ||
| * Behavior for handling forced array fields that have no values. | ||
| * - `'empty-array'`: Create an empty array `[]` | ||
| * - `'omit'`: Omit the field entirely (default) | ||
| */ | ||
| type EmptyArrayBehavior = "empty-array" | "omit"; | ||
| /** | ||
| * Mode for converting arrays back to CSV format. | ||
| * - `'rows'`: Output arrays as continuation rows (matches parser format) | ||
| * - `'json'`: JSON-stringify arrays into a single cell | ||
| */ | ||
| type ArrayMode = "rows" | "json"; | ||
| /** | ||
| * A flat CSV record with string keys and string values. | ||
| * Represents a single row parsed from CSV before nesting conversion. | ||
| */ | ||
| type CsvRecord = Record<string, string>; | ||
| /** | ||
| * A nested object structure produced by the CSV parser. | ||
| * Values can be primitives, arrays, dates, or nested objects. | ||
| */ | ||
| type NestedValue = string | number | boolean | null | Date | NestedObject | NestedValue[]; | ||
| /** | ||
| * A nested JSON object with string keys and nested values. | ||
| */ | ||
| interface NestedObject { | ||
| [key: string]: NestedValue; | ||
| } | ||
| /** | ||
| * Function type for transforming individual cell values. | ||
| * Receives the current value (after auto-parsing if enabled) and the header name. | ||
| * | ||
| * @param value - The cell value (may be string, number, or boolean after auto-parsing) | ||
| * @param header - The column header name | ||
| * @returns The transformed value | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Convert specific columns to uppercase | ||
| * const transformer: ValueTransformer = (value, header) => { | ||
| * if (header === 'name' && typeof value === 'string') { | ||
| * return value.toUpperCase(); | ||
| * } | ||
| * return value; | ||
| * }; | ||
| * ``` | ||
| */ | ||
| type ValueTransformer = (value: string | number | boolean, header: string) => unknown; | ||
| /** | ||
| * Function type for transforming header names during parsing. | ||
| * Receives the original header name and returns the transformed name. | ||
| * | ||
| * @param header - The original header name from the CSV | ||
| * @returns The transformed header name | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Convert headers to camelCase | ||
| * const transformer: HeaderTransformer = (header) => { | ||
| * return header.replace(/[-_](.)/g, (_, c) => c.toUpperCase()); | ||
| * }; | ||
| * ``` | ||
| */ | ||
| type HeaderTransformer = (header: string) => string; | ||
| /** | ||
| * Function type for filtering rows during parsing. | ||
| * Receives the parsed record and returns true to include, false to exclude. | ||
| * | ||
| * @param record - The parsed record (flat, before nesting) | ||
| * @param rowIndex - The 0-based index of the data row (excludes header and skipped rows) | ||
| * @returns true to include the row, false to exclude it | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Only include rows where status is 'active' | ||
| * const filter: RowFilter = (record) => record.status === 'active'; | ||
| * ``` | ||
| */ | ||
| type RowFilter = (record: CsvRecord, rowIndex: number) => boolean; | ||
| /** | ||
| * Representation for null values in output. | ||
| * - `'null'`: Use JavaScript null | ||
| * - `'undefined'`: Use JavaScript undefined | ||
| * - `'empty-string'`: Use empty string '' | ||
| * - `'omit'`: Omit the field entirely (default) | ||
| */ | ||
| type NullRepresentation = "null" | "undefined" | "empty-string" | "omit"; | ||
| /** | ||
| * Configuration options for CSV parsing and conversion. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const options: CsvParserOptions = { | ||
| * delimiter: ';', | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true, | ||
| * skipRows: 2, | ||
| * stripBom: true | ||
| * }; | ||
| * | ||
| * const result = CsvParser.parseString(csvContent, options); | ||
| * ``` | ||
| */ | ||
| interface CsvParserOptions { | ||
| /** | ||
| * How to handle rows with more values than headers. | ||
| * - 'ignore': Silently ignore extra values | ||
| * - 'warn': Log a warning to console (default) | ||
| * - 'error': Throw an error | ||
| * - `'ignore'`: Silently ignore extra values | ||
| * - `'warn'`: Log a warning to console (default) | ||
| * - `'error'`: Throw a CsvValidationError | ||
| */ | ||
| validationMode?: ValidationMode; | ||
| /** | ||
| * Field delimiter character (default: ',') | ||
| * Field delimiter character. | ||
| * @default ',' | ||
| */ | ||
| delimiter?: string; | ||
| /** | ||
| * Quote character for escaping fields (default: '"') | ||
| * Quote character for escaping fields containing delimiters or newlines. | ||
| * @default '"' | ||
| */ | ||
| quote?: string; | ||
| /** | ||
| * File encoding when reading from file (default: 'utf-8') | ||
| * File encoding when reading from file. | ||
| * @default 'utf-8' | ||
| */ | ||
| encoding?: BufferEncoding; | ||
| /** | ||
| * Suffix indicator in column headers to force array type. | ||
| * Fields with this suffix will always be arrays, even with single values. | ||
| * @default '[]' | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Header: 'person.children[].name' | ||
| * // Result: { person: { children: [{ name: '...' }] } } | ||
| * ``` | ||
| */ | ||
| arraySuffixIndicator?: string; | ||
| /** | ||
| * How to handle forced array fields with no values. | ||
| * - `'empty-array'`: Create an empty array `[]` | ||
| * - `'omit'`: Omit the field entirely (default) | ||
| */ | ||
| emptyArrayBehavior?: EmptyArrayBehavior; | ||
| /** | ||
| * Number of rows to skip before the header row. | ||
| * Useful for files with metadata or comments at the top. | ||
| * @default 0 | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Skip 2 rows of metadata before the header | ||
| * CsvParser.parseString(csv, { skipRows: 2 }); | ||
| * ``` | ||
| */ | ||
| skipRows?: number; | ||
| /** | ||
| * Automatically strip BOM (Byte Order Mark) from the beginning of content. | ||
| * Handles UTF-8 BOM (\uFEFF) and UTF-16 BOMs. | ||
| * @default true | ||
| */ | ||
| stripBom?: boolean; | ||
| /** | ||
| * Automatically convert numeric strings to numbers. | ||
| * Applies to values that can be parsed as valid numbers. | ||
| * @default false | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // '42' becomes 42, '3.14' becomes 3.14 | ||
| * CsvParser.parseString(csv, { autoParseNumbers: true }); | ||
| * ``` | ||
| */ | ||
| autoParseNumbers?: boolean; | ||
| /** | ||
| * Automatically convert 'true'/'false' strings to booleans. | ||
| * Case-insensitive matching. | ||
| * @default false | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // 'true' becomes true, 'FALSE' becomes false | ||
| * CsvParser.parseString(csv, { autoParseBooleans: true }); | ||
| * ``` | ||
| */ | ||
| autoParseBooleans?: boolean; | ||
| /** | ||
| * Custom function to transform values after parsing. | ||
| * Called after autoParseNumbers and autoParseBooleans (if enabled). | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * CsvParser.parseString(csv, { | ||
| * valueTransformer: (value, header) => { | ||
| * if (header === 'date') return new Date(value as string); | ||
| * return value; | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| valueTransformer?: ValueTransformer; | ||
| /** | ||
| * Mode for converting arrays to CSV (used by JsonToCsv). | ||
| * - `'rows'`: Output arrays as continuation rows (default, matches parser) | ||
| * - `'json'`: JSON-stringify arrays into a single cell | ||
| * @default 'rows' | ||
| */ | ||
| arrayMode?: ArrayMode; | ||
| /** | ||
| * Transform header names before processing. | ||
| * Applied to each header after reading from CSV. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Convert headers to lowercase | ||
| * CsvParser.parseString(csv, { | ||
| * headerTransformer: (header) => header.toLowerCase() | ||
| * }); | ||
| * ``` | ||
| */ | ||
| headerTransformer?: HeaderTransformer; | ||
| /** | ||
| * Map column names to new names. | ||
| * Applied after headerTransformer (if specified). | ||
| * Keys are original names, values are new names. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * CsvParser.parseString(csv, { | ||
| * columnMapping: { | ||
| * 'First Name': 'firstName', | ||
| * 'Last Name': 'lastName' | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| columnMapping?: Record<string, string>; | ||
| /** | ||
| * Filter rows during parsing. | ||
| * Return true to include the row, false to exclude it. | ||
| * Applied after parsing but before nesting conversion. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * CsvParser.parseString(csv, { | ||
| * rowFilter: (record) => record.status !== 'deleted' | ||
| * }); | ||
| * ``` | ||
| */ | ||
| rowFilter?: RowFilter; | ||
| /** | ||
| * Default values for columns. | ||
| * Applied when a cell is empty. | ||
| * Keys are column names (after transformation/mapping). | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * CsvParser.parseString(csv, { | ||
| * defaultValues: { | ||
| * status: 'pending', | ||
| * count: '0' | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| defaultValues?: Record<string, string>; | ||
| /** | ||
| * Automatically parse date strings to Date objects. | ||
| * Uses JavaScript's Date.parse() for recognition. | ||
| * @default false | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // '2024-01-15' becomes Date object | ||
| * CsvParser.parseString(csv, { autoParseDates: true }); | ||
| * ``` | ||
| */ | ||
| autoParseDates?: boolean; | ||
| /** | ||
| * Values to treat as null. | ||
| * Case-insensitive matching. | ||
| * @default ['null', 'NULL', 'nil', 'NIL', ''] | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * CsvParser.parseString(csv, { | ||
| * nullValues: ['null', 'N/A', '-', ''] | ||
| * }); | ||
| * ``` | ||
| */ | ||
| nullValues?: string[]; | ||
| /** | ||
| * How to represent null values in the output. | ||
| * - `'null'`: Use JavaScript null | ||
| * - `'undefined'`: Use JavaScript undefined | ||
| * - `'empty-string'`: Use empty string '' | ||
| * - `'omit'`: Omit the field entirely (default) | ||
| * @default 'omit' | ||
| */ | ||
| nullRepresentation?: NullRepresentation; | ||
| } | ||
| /** | ||
| * High-level CSV to nested JSON parser | ||
| * Combines file I/O, CSV parsing, and nested JSON conversion | ||
| * File and stream I/O operations for CSV parsing. | ||
| * Handles reading CSV content from files and streams with configurable encoding. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Read file synchronously | ||
| * const content = CsvFileReader.readFileSync('data.csv'); | ||
| * | ||
| * // Read file asynchronously | ||
| * const content = await CsvFileReader.readFile('data.csv'); | ||
| * | ||
| * // Read from stream | ||
| * const stream = fs.createReadStream('data.csv'); | ||
| * const content = await CsvFileReader.readStream(stream); | ||
| * ``` | ||
| */ | ||
| declare class CsvFileReader { | ||
| /** | ||
| * Read CSV file synchronously. | ||
| * | ||
| * @param filePath - Path to the CSV file | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns The file content as a string | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const content = CsvFileReader.readFileSync('data.csv', { encoding: 'utf-8' }); | ||
| * ``` | ||
| */ | ||
| static readFileSync(filePath: string, options?: CsvParserOptions): string; | ||
| /** | ||
| * Read CSV file asynchronously. | ||
| * | ||
| * @param filePath - Path to the CSV file | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns Promise resolving to the file content as a string | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const content = await CsvFileReader.readFile('data.csv', { encoding: 'utf-16le' }); | ||
| * ``` | ||
| */ | ||
| static readFile(filePath: string, options?: CsvParserOptions): Promise<string>; | ||
| /** | ||
| * Read CSV from a readable stream. | ||
| * | ||
| * @param stream - Readable stream containing CSV data | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns Promise resolving to the stream content as a string | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const stream = fs.createReadStream('large-file.csv'); | ||
| * const content = await CsvFileReader.readStream(stream); | ||
| * ``` | ||
| */ | ||
| static readStream(stream: Readable, options?: CsvParserOptions): Promise<string>; | ||
| } | ||
| /** | ||
| * High-level CSV to nested JSON parser. | ||
| * Combines file I/O, CSV parsing, and nested JSON conversion into a single API. | ||
| * | ||
| * The parser supports: | ||
| * - Dot-notation headers for nested objects (`person.address.city`) | ||
| * - Automatic array detection when values collide | ||
| * - Forced array fields with `[]` suffix (`tags[]`) | ||
| * - Continuation rows (rows with empty first column extend previous record) | ||
| * - Value transformation (auto-parse numbers, booleans, custom transformers) | ||
| * - BOM stripping and row skipping | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Parse from file | ||
| * const result = CsvParser.parseFileSync('data.csv'); | ||
| * | ||
| * // Parse from string with options | ||
| * const result = CsvParser.parseString(csvContent, { | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true, | ||
| * skipRows: 1 | ||
| * }); | ||
| * | ||
| * // Parse with custom value transformer | ||
| * const result = CsvParser.parseString(csv, { | ||
| * valueTransformer: (value, header) => { | ||
| * if (header === 'date') return new Date(value as string); | ||
| * return value; | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| declare abstract class CsvParser { | ||
| /** | ||
| * Parse CSV file synchronously to nested JSON | ||
| * @param csvFilePath Path to CSV file | ||
| * @param options Parsing options | ||
| * Parse CSV file synchronously to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvFilePath - Path to the CSV file | ||
| * @param options - Parsing options | ||
| * @returns Array of nested JSON objects | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * interface Person { | ||
| * id: string; | ||
| * name: string; | ||
| * address: { city: string }; | ||
| * } | ||
| * | ||
| * const people = CsvParser.parseFileSync<Person>('people.csv'); | ||
| * console.log(people[0].address.city); | ||
| * ``` | ||
| */ | ||
| static parseFileSync(csvFilePath: string, options?: CsvParserOptions): any[]; | ||
| static parseFileSync<T = NestedObject>(csvFilePath: string, options?: CsvParserOptions): T[]; | ||
| /** | ||
| * Parse CSV file asynchronously to nested JSON | ||
| * @param csvFilePath Path to CSV file | ||
| * @param options Parsing options | ||
| * Parse CSV file asynchronously to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvFilePath - Path to the CSV file | ||
| * @param options - Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const people = await CsvParser.parseFile<Person>('people.csv'); | ||
| * ``` | ||
| */ | ||
| static parseFile(csvFilePath: string, options?: CsvParserOptions): Promise<any[]>; | ||
| static parseFile<T = NestedObject>(csvFilePath: string, options?: CsvParserOptions): Promise<T[]>; | ||
| /** | ||
| * Parse CSV string content to nested JSON | ||
| * @param csvContent CSV content as string | ||
| * @param options Parsing options | ||
| * Parse CSV from readable stream to nested JSON. | ||
| * Note: This method buffers the entire stream before parsing. | ||
| * For true streaming with large files, use {@link CsvStreamParser}. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param stream - Readable stream containing CSV data | ||
| * @param options - Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const stream = fs.createReadStream('large-file.csv'); | ||
| * const result = await CsvParser.parseStream(stream); | ||
| * ``` | ||
| */ | ||
| static parseStream<T = NestedObject>(stream: Readable, options?: CsvParserOptions): Promise<T[]>; | ||
| /** | ||
| * Parse CSV string content to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvContent - CSV content as string | ||
| * @param options - Parsing options | ||
| * @returns Array of nested JSON objects | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const csv = `id,name,address.city | ||
| * 1,John,NYC | ||
| * 2,Jane,LA`; | ||
| * | ||
| * const result = CsvParser.parseString(csv); | ||
| * // [ | ||
| * // { id: '1', name: 'John', address: { city: 'NYC' } }, | ||
| * // { id: '2', name: 'Jane', address: { city: 'LA' } } | ||
| * // ] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With continuation rows for arrays | ||
| * const csv = `id,tags | ||
| * 1,javascript | ||
| * ,typescript | ||
| * ,nodejs`; | ||
| * | ||
| * const result = CsvParser.parseString(csv); | ||
| * // [{ id: '1', tags: ['javascript', 'typescript', 'nodejs'] }] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With auto-parsing | ||
| * const csv = `id,active,score | ||
| * 1,true,95.5`; | ||
| * | ||
| * const result = CsvParser.parseString(csv, { | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true | ||
| * }); | ||
| * // [{ id: 1, active: true, score: 95.5 }] | ||
| * ``` | ||
| */ | ||
| static parseString(csvContent: string, options?: CsvParserOptions): any[]; | ||
| static parseString<T = NestedObject>(csvContent: string, options?: CsvParserOptions): T[]; | ||
| } | ||
| /** | ||
| * Low-level CSV parsing utilities. | ||
| * Handles parsing CSV content into flat record objects. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const records = CsvReader.parse('id,name\n1,Alice\n2,Bob'); | ||
| * // [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }] | ||
| * ``` | ||
| */ | ||
| declare class CsvReader { | ||
| /** | ||
| * Parse CSV from readable stream to nested JSON | ||
| * @param stream Readable stream containing CSV data | ||
| * @param options Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * Parse CSV content into an array of flat record objects. | ||
| * Each record maps header names to cell values. | ||
| * | ||
| * @param content - The CSV content as a string | ||
| * @param options - Parser options | ||
| * @returns Array of flat record objects with string values | ||
| * @throws {CsvValidationError} If validationMode is 'error' and row has too many columns | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Basic parsing | ||
| * const records = CsvReader.parse('id,name\n1,Alice'); | ||
| * // [{ id: '1', name: 'Alice' }] | ||
| * | ||
| * // With BOM stripping and skip rows | ||
| * const records = CsvReader.parse(content, { | ||
| * stripBom: true, | ||
| * skipRows: 2 | ||
| * }); | ||
| * ``` | ||
| */ | ||
| static parseStream(stream: Readable, options?: CsvParserOptions): Promise<any[]>; | ||
| static parse(content: string, options?: CsvParserOptions): CsvRecord[]; | ||
| /** | ||
| * Strip BOM (Byte Order Mark) from the beginning of content. | ||
| * Handles UTF-8 and UTF-16 BOMs. | ||
| * | ||
| * @param content - The content that may contain a BOM | ||
| * @returns Content with BOM removed | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const clean = CsvReader.stripBom('\uFEFFid,name'); | ||
| * // 'id,name' | ||
| * ``` | ||
| */ | ||
| static stripBom(content: string): string; | ||
| /** | ||
| * Split CSV content into lines, respecting quoted fields that may contain newlines. | ||
| * | ||
| * @param content - The CSV content | ||
| * @returns Array of lines (quoted newlines are preserved within lines) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const lines = CsvReader.splitLines('id,note\n1,"Line 1\nLine 2"'); | ||
| * // ['id,note', '1,"Line 1\nLine 2"'] | ||
| * ``` | ||
| */ | ||
| static splitLines(content: string): string[]; | ||
| /** | ||
| * Parse a single CSV line into an array of values, respecting quotes and delimiters. | ||
| * | ||
| * @param line - A single line of CSV data | ||
| * @param delimiter - The field delimiter (default: ',') | ||
| * @param quote - The quote character (default: '"') | ||
| * @returns Array of parsed values | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const values = CsvReader.parseLine('1,"Hello, World",test'); | ||
| * // ['1', 'Hello, World', 'test'] | ||
| * | ||
| * // Escaped quotes | ||
| * const values = CsvReader.parseLine('1,"Say ""Hello""",test'); | ||
| * // ['1', 'Say "Hello"', 'test'] | ||
| * ``` | ||
| */ | ||
| static parseLine(line: string, delimiter?: string, quote?: string): string[]; | ||
| } | ||
| export { CsvParser, type CsvParserOptions, type ValidationMode }; | ||
| /** | ||
| * Options for the streaming CSV parser. | ||
| * Extends standard TransformOptions with CSV-specific options. | ||
| */ | ||
| interface CsvStreamParserOptions extends CsvParserOptions, TransformOptions { | ||
| /** | ||
| * Whether to emit nested objects (true) or flat records (false). | ||
| * When true, records are converted using NestedJsonConverter logic. | ||
| * When false, raw flat records are emitted. | ||
| * @default true | ||
| */ | ||
| nested?: boolean; | ||
| } | ||
| /** | ||
| * A streaming CSV parser that processes CSV data chunk by chunk. | ||
| * Emits parsed records as they become available, without buffering the entire file. | ||
| * | ||
| * This is ideal for processing very large CSV files that don't fit in memory. | ||
| * The parser handles quoted fields that span multiple chunks correctly. | ||
| * | ||
| * Note: For true nested JSON conversion with continuation rows (rows that extend | ||
| * previous records), consider using the standard CsvParser methods, as continuation | ||
| * row handling requires buffering related rows together. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * import { createReadStream } from 'node:fs'; | ||
| * import { CsvStreamParser } from '@cerios/csv-nested-json'; | ||
| * | ||
| * const parser = new CsvStreamParser({ delimiter: ',' }); | ||
| * | ||
| * createReadStream('large-file.csv') | ||
| * .pipe(parser) | ||
| * .on('data', (record) => { | ||
| * console.log('Parsed record:', record); | ||
| * }) | ||
| * .on('end', () => { | ||
| * console.log('Done parsing'); | ||
| * }) | ||
| * .on('error', (err) => { | ||
| * console.error('Parse error:', err); | ||
| * }); | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Collect all records | ||
| * const records: NestedObject[] = []; | ||
| * const parser = new CsvStreamParser(); | ||
| * | ||
| * for await (const record of readStream.pipe(parser)) { | ||
| * records.push(record); | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare class CsvStreamParser extends Transform { | ||
| private buffer; | ||
| private headers; | ||
| private headersProcessed; | ||
| private rowsSkipped; | ||
| private dataRowIndex; | ||
| private options; | ||
| private delimiter; | ||
| private quote; | ||
| private skipRows; | ||
| private stripBom; | ||
| private bomStripped; | ||
| private nullSet; | ||
| /** | ||
| * Creates a new streaming CSV parser. | ||
| * | ||
| * @param options - Parser options | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const parser = new CsvStreamParser({ | ||
| * delimiter: ';', | ||
| * quote: '"', | ||
| * skipRows: 1, | ||
| * autoParseNumbers: true | ||
| * }); | ||
| * ``` | ||
| */ | ||
| constructor(options?: CsvStreamParserOptions); | ||
| /** | ||
| * Transform implementation - processes incoming chunks. | ||
| * @internal | ||
| */ | ||
| _transform(chunk: Buffer | string, _encoding: BufferEncoding, callback: TransformCallback): void; | ||
| /** | ||
| * Flush implementation - processes any remaining data. | ||
| * @internal | ||
| */ | ||
| _flush(callback: TransformCallback): void; | ||
| /** | ||
| * Strip BOM from the beginning of a string. | ||
| */ | ||
| private stripBomFromString; | ||
| /** | ||
| * Process the buffer and extract complete lines. | ||
| */ | ||
| private processBuffer; | ||
| /** | ||
| * Process a single line of CSV data. | ||
| */ | ||
| private processLine; | ||
| /** | ||
| * Parse a single line into values. | ||
| */ | ||
| private parseLine; | ||
| /** | ||
| * Create a record object from values array. | ||
| */ | ||
| private createRecord; | ||
| /** | ||
| * Apply value transformations to a record. | ||
| */ | ||
| private applyTransformations; | ||
| /** | ||
| * Apply null representation based on option. | ||
| */ | ||
| private applyNullRepresentation; | ||
| /** | ||
| * Try to parse a string as a number. | ||
| */ | ||
| private tryParseNumber; | ||
| /** | ||
| * Try to parse a string as a boolean. | ||
| */ | ||
| private tryParseBoolean; | ||
| /** | ||
| * Try to parse a string as a Date. | ||
| * Uses JavaScript's Date.parse() for recognition. | ||
| */ | ||
| private tryParseDate; | ||
| /** | ||
| * Unflatten a record with dot-notation keys into a nested object. | ||
| */ | ||
| private unflatten; | ||
| } | ||
| /** | ||
| * Base error class for CSV parsing errors. | ||
| * Provides context about where the error occurred. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * CsvParser.parseString(csvContent); | ||
| * } catch (error) { | ||
| * if (error instanceof CsvParseError) { | ||
| * console.log(`Error at row ${error.row}: ${error.message}`); | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare class CsvParseError extends Error { | ||
| readonly row?: number | undefined; | ||
| readonly column?: number | undefined; | ||
| /** | ||
| * Creates a new CSV parse error | ||
| * @param message - Human-readable error description | ||
| * @param row - The 1-based row number where the error occurred (optional) | ||
| * @param column - The 1-based column number where the error occurred (optional) | ||
| */ | ||
| constructor(message: string, row?: number | undefined, column?: number | undefined); | ||
| } | ||
| /** | ||
| * Error thrown when a CSV file cannot be found at the specified path. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * CsvParser.parseFileSync('/path/to/missing.csv'); | ||
| * } catch (error) { | ||
| * if (error instanceof CsvFileNotFoundError) { | ||
| * console.log(`File not found: ${error.filePath}`); | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare class CsvFileNotFoundError extends CsvParseError { | ||
| readonly filePath: string; | ||
| /** | ||
| * Creates a new file not found error | ||
| * @param filePath - The path to the file that was not found | ||
| */ | ||
| constructor(filePath: string); | ||
| } | ||
| /** | ||
| * Error thrown when CSV data fails validation rules. | ||
| * Contains details about the expected vs actual column counts. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * CsvParser.parseString(csvContent, { validationMode: 'error' }); | ||
| * } catch (error) { | ||
| * if (error instanceof CsvValidationError) { | ||
| * console.log(`Row ${error.row}: expected ${error.expectedColumns} columns, got ${error.actualColumns}`); | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare class CsvValidationError extends CsvParseError { | ||
| readonly expectedColumns: number; | ||
| readonly actualColumns: number; | ||
| /** | ||
| * Creates a new validation error | ||
| * @param message - Human-readable error description | ||
| * @param row - The 1-based row number where validation failed | ||
| * @param expectedColumns - The expected number of columns (from header) | ||
| * @param actualColumns - The actual number of columns found in the row | ||
| */ | ||
| constructor(message: string, row: number, expectedColumns: number, actualColumns: number); | ||
| } | ||
| /** | ||
| * Error thrown when there are encoding or BOM-related issues. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * CsvParser.parseFileSync('file.csv', { encoding: 'utf-16' }); | ||
| * } catch (error) { | ||
| * if (error instanceof CsvEncodingError) { | ||
| * console.log(`Encoding error: ${error.message}`); | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare class CsvEncodingError extends CsvParseError { | ||
| readonly encoding?: string | undefined; | ||
| /** | ||
| * Creates a new encoding error | ||
| * @param message - Human-readable error description | ||
| * @param encoding - The encoding that was being used | ||
| */ | ||
| constructor(message: string, encoding?: string | undefined); | ||
| } | ||
| /** | ||
| * Options for JSON to CSV conversion. | ||
| */ | ||
| interface JsonToCsvOptions extends Pick<CsvParserOptions, "delimiter" | "quote" | "encoding" | "arrayMode"> { | ||
| /** | ||
| * Line ending to use in output. | ||
| * @default '\n' | ||
| */ | ||
| lineEnding?: "\n" | "\r\n"; | ||
| /** | ||
| * Whether to include a header row. | ||
| * @default true | ||
| */ | ||
| includeHeader?: boolean; | ||
| } | ||
| /** | ||
| * Converts nested JSON objects back to CSV format. | ||
| * Supports the reverse operation of CsvParser, including: | ||
| * - Nested objects converted to dot-notation headers | ||
| * - Arrays output as continuation rows (default) or JSON-stringified | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const data = [ | ||
| * { id: '1', person: { name: 'John', city: 'NYC' } }, | ||
| * { id: '2', person: { name: 'Jane', city: 'LA' } } | ||
| * ]; | ||
| * | ||
| * const csv = JsonToCsv.stringify(data); | ||
| * // id,person.name,person.city | ||
| * // 1,John,NYC | ||
| * // 2,Jane,LA | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With arrays as continuation rows | ||
| * const data = [ | ||
| * { id: '1', tags: ['js', 'ts', 'node'] } | ||
| * ]; | ||
| * | ||
| * const csv = JsonToCsv.stringify(data, { arrayMode: 'rows' }); | ||
| * // id,tags | ||
| * // 1,js | ||
| * // ,ts | ||
| * // ,node | ||
| * ``` | ||
| */ | ||
| declare class JsonToCsv { | ||
| /** | ||
| * Convert array of nested objects to CSV string. | ||
| * | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * @returns CSV string | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const csv = JsonToCsv.stringify([ | ||
| * { id: 1, name: 'Alice', address: { city: 'NYC' } } | ||
| * ]); | ||
| * // "id,name,address.city\n1,Alice,NYC" | ||
| * ``` | ||
| */ | ||
| static stringify(data: NestedObject[], options?: JsonToCsvOptions): string; | ||
| /** | ||
| * Write nested objects to CSV file synchronously. | ||
| * | ||
| * @param filePath - Path to output file | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * JsonToCsv.writeFileSync('output.csv', data, { delimiter: ';' }); | ||
| * ``` | ||
| */ | ||
| static writeFileSync(filePath: string, data: NestedObject[], options?: JsonToCsvOptions): void; | ||
| /** | ||
| * Write nested objects to CSV file asynchronously. | ||
| * | ||
| * @param filePath - Path to output file | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * @returns Promise that resolves when file is written | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await JsonToCsv.writeFile('output.csv', data); | ||
| * ``` | ||
| */ | ||
| static writeFile(filePath: string, data: NestedObject[], options?: JsonToCsvOptions): Promise<void>; | ||
| /** | ||
| * Collect all unique headers from an array of nested objects. | ||
| * Headers are generated using dot-notation for nested properties. | ||
| */ | ||
| private static collectHeaders; | ||
| /** | ||
| * Recursively collect headers from a nested object. | ||
| */ | ||
| private static collectHeadersFromObject; | ||
| /** | ||
| * Flatten a nested object into one or more rows of flat key-value pairs. | ||
| * Arrays result in multiple rows (continuation rows). | ||
| */ | ||
| private static flattenObject; | ||
| /** | ||
| * Flatten a nested object to path-value pairs, separating arrays. | ||
| */ | ||
| private static flattenToPathValues; | ||
| /** | ||
| * Generate continuation rows for arrays. | ||
| * First row contains all values, subsequent rows only contain array continuations. | ||
| */ | ||
| private static generateContinuationRows; | ||
| /** | ||
| * Convert a value to string for CSV output. | ||
| */ | ||
| private static valueToString; | ||
| /** | ||
| * Escape a value for CSV output. | ||
| * Wraps in quotes if the value contains delimiter, quote, or newline. | ||
| */ | ||
| private static escapeValue; | ||
| } | ||
| /** | ||
| * Nested JSON conversion utilities. | ||
| * Converts flat CSV records into nested JSON structures with automatic array detection. | ||
| * | ||
| * Features: | ||
| * - Dot-notation paths in headers become nested objects | ||
| * - Rows with empty first column are continuation rows (extend previous record) | ||
| * - Automatic array detection when values collide during merge | ||
| * - Forced array fields via `[]` suffix in headers | ||
| * - Value transformation (auto-parse numbers, booleans, custom transformers) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const records = [ | ||
| * { 'id': '1', 'person.name': 'John', 'person.age': '30' } | ||
| * ]; | ||
| * | ||
| * const result = NestedJsonConverter.convert(records); | ||
| * // [{ id: '1', person: { name: 'John', age: '30' } }] | ||
| * ``` | ||
| */ | ||
| declare class NestedJsonConverter { | ||
| /** | ||
| * Convert flat CSV records into nested JSON structure with array detection. | ||
| * | ||
| * @param records - Array of flat CSV records with string values | ||
| * @param options - Parser options for customizing conversion | ||
| * @returns Array of nested JSON objects | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Basic conversion with dot notation | ||
| * const records = [{ 'person.name': 'John', 'person.city': 'NYC' }]; | ||
| * const result = NestedJsonConverter.convert(records); | ||
| * // [{ person: { name: 'John', city: 'NYC' } }] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With value transformation | ||
| * const records = [{ id: '1', active: 'true', score: '95.5' }]; | ||
| * const result = NestedJsonConverter.convert(records, { | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true | ||
| * }); | ||
| * // [{ id: 1, active: true, score: 95.5 }] | ||
| * ``` | ||
| */ | ||
| static convert(records: CsvRecord[], options?: CsvParserOptions): NestedObject[]; | ||
| /** | ||
| * Apply value transformations (null detection, auto-parse numbers, booleans, dates, custom transformer). | ||
| * Transformation order: nullValues → autoParseNumbers → autoParseBooleans → autoParseDates → valueTransformer | ||
| */ | ||
| private static applyValueTransformations; | ||
| /** | ||
| * Apply null representation based on option. | ||
| */ | ||
| private static applyNullRepresentation; | ||
| /** | ||
| * Try to parse a string as a number. | ||
| * Returns null if the string is not a valid number. | ||
| */ | ||
| private static tryParseNumber; | ||
| /** | ||
| * Try to parse a string as a boolean. | ||
| * Returns null if the string is not 'true' or 'false' (case-insensitive). | ||
| */ | ||
| private static tryParseBoolean; | ||
| /** | ||
| * Try to parse a string as a Date. | ||
| * Returns null if the string is not a valid date. | ||
| * Uses JavaScript's Date.parse() for recognition. | ||
| */ | ||
| private static tryParseDate; | ||
| private static detectForcedArrayFields; | ||
| private static normalizeHeaders; | ||
| private static processGroup; | ||
| private static unflatten; | ||
| private static deepMerge; | ||
| private static checkIfAllKeysCollide; | ||
| private static shouldCreateArrayOfObjects; | ||
| private static detectArrayFields; | ||
| private static normalizeArrays; | ||
| } | ||
| export { type ArrayMode, CsvEncodingError, CsvFileNotFoundError, CsvFileReader, CsvParseError, CsvParser, type CsvParserOptions, CsvReader, type CsvRecord, CsvStreamParser, type CsvStreamParserOptions, CsvValidationError, type EmptyArrayBehavior, type HeaderTransformer, JsonToCsv, type JsonToCsvOptions, NestedJsonConverter, type NestedObject, type NestedValue, type NullRepresentation, type RowFilter, type ValidationMode, type ValueTransformer }; |
+1095
-49
@@ -33,3 +33,12 @@ "use strict"; | ||
| __export(index_exports, { | ||
| CsvParser: () => CsvParser | ||
| CsvEncodingError: () => CsvEncodingError, | ||
| CsvFileNotFoundError: () => CsvFileNotFoundError, | ||
| CsvFileReader: () => CsvFileReader, | ||
| CsvParseError: () => CsvParseError, | ||
| CsvParser: () => CsvParser, | ||
| CsvReader: () => CsvReader, | ||
| CsvStreamParser: () => CsvStreamParser, | ||
| CsvValidationError: () => CsvValidationError, | ||
| JsonToCsv: () => JsonToCsv, | ||
| NestedJsonConverter: () => NestedJsonConverter | ||
| }); | ||
@@ -40,9 +49,87 @@ module.exports = __toCommonJS(index_exports); | ||
| var import_node_fs = __toESM(require("fs")); | ||
| // src/errors.ts | ||
| var CsvParseError = class _CsvParseError extends Error { | ||
| /** | ||
| * Creates a new CSV parse error | ||
| * @param message - Human-readable error description | ||
| * @param row - The 1-based row number where the error occurred (optional) | ||
| * @param column - The 1-based column number where the error occurred (optional) | ||
| */ | ||
| constructor(message, row, column) { | ||
| super(message); | ||
| this.row = row; | ||
| this.column = column; | ||
| this.name = "CsvParseError"; | ||
| if (Error.captureStackTrace) { | ||
| Error.captureStackTrace(this, _CsvParseError); | ||
| } | ||
| } | ||
| }; | ||
| var CsvFileNotFoundError = class _CsvFileNotFoundError extends CsvParseError { | ||
| /** | ||
| * Creates a new file not found error | ||
| * @param filePath - The path to the file that was not found | ||
| */ | ||
| constructor(filePath) { | ||
| super(`CSV file not found: ${filePath}`); | ||
| this.filePath = filePath; | ||
| this.name = "CsvFileNotFoundError"; | ||
| if (Error.captureStackTrace) { | ||
| Error.captureStackTrace(this, _CsvFileNotFoundError); | ||
| } | ||
| } | ||
| }; | ||
| var CsvValidationError = class _CsvValidationError extends CsvParseError { | ||
| /** | ||
| * Creates a new validation error | ||
| * @param message - Human-readable error description | ||
| * @param row - The 1-based row number where validation failed | ||
| * @param expectedColumns - The expected number of columns (from header) | ||
| * @param actualColumns - The actual number of columns found in the row | ||
| */ | ||
| constructor(message, row, expectedColumns, actualColumns) { | ||
| super(message, row); | ||
| this.expectedColumns = expectedColumns; | ||
| this.actualColumns = actualColumns; | ||
| this.name = "CsvValidationError"; | ||
| if (Error.captureStackTrace) { | ||
| Error.captureStackTrace(this, _CsvValidationError); | ||
| } | ||
| } | ||
| }; | ||
| var CsvEncodingError = class _CsvEncodingError extends CsvParseError { | ||
| /** | ||
| * Creates a new encoding error | ||
| * @param message - Human-readable error description | ||
| * @param encoding - The encoding that was being used | ||
| */ | ||
| constructor(message, encoding) { | ||
| super(message); | ||
| this.encoding = encoding; | ||
| this.name = "CsvEncodingError"; | ||
| if (Error.captureStackTrace) { | ||
| Error.captureStackTrace(this, _CsvEncodingError); | ||
| } | ||
| } | ||
| }; | ||
| // src/csv-file-reader.ts | ||
| var CsvFileReader = class { | ||
| /** | ||
| * Read CSV file synchronously | ||
| * Read CSV file synchronously. | ||
| * | ||
| * @param filePath - Path to the CSV file | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns The file content as a string | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const content = CsvFileReader.readFileSync('data.csv', { encoding: 'utf-8' }); | ||
| * ``` | ||
| */ | ||
| static readFileSync(filePath, options = {}) { | ||
| if (!import_node_fs.default.existsSync(filePath)) { | ||
| throw new Error(`CSV file ${filePath} not found.`); | ||
| throw new CsvFileNotFoundError(filePath); | ||
| } | ||
@@ -53,7 +140,17 @@ const encoding = options.encoding || "utf-8"; | ||
| /** | ||
| * Read CSV file asynchronously | ||
| * Read CSV file asynchronously. | ||
| * | ||
| * @param filePath - Path to the CSV file | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns Promise resolving to the file content as a string | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const content = await CsvFileReader.readFile('data.csv', { encoding: 'utf-16le' }); | ||
| * ``` | ||
| */ | ||
| static async readFile(filePath, options = {}) { | ||
| if (!import_node_fs.default.existsSync(filePath)) { | ||
| throw new Error(`CSV file ${filePath} not found.`); | ||
| throw new CsvFileNotFoundError(filePath); | ||
| } | ||
@@ -64,3 +161,13 @@ const encoding = options.encoding || "utf-8"; | ||
| /** | ||
| * Read CSV from readable stream | ||
| * Read CSV from a readable stream. | ||
| * | ||
| * @param stream - Readable stream containing CSV data | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns Promise resolving to the stream content as a string | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const stream = fs.createReadStream('large-file.csv'); | ||
| * const content = await CsvFileReader.readStream(stream); | ||
| * ``` | ||
| */ | ||
@@ -91,17 +198,55 @@ static async readStream(stream, options = {}) { | ||
| /** | ||
| * Parse CSV content into array of record objects | ||
| * Parse CSV content into an array of flat record objects. | ||
| * Each record maps header names to cell values. | ||
| * | ||
| * @param content - The CSV content as a string | ||
| * @param options - Parser options | ||
| * @returns Array of flat record objects with string values | ||
| * @throws {CsvValidationError} If validationMode is 'error' and row has too many columns | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Basic parsing | ||
| * const records = CsvReader.parse('id,name\n1,Alice'); | ||
| * // [{ id: '1', name: 'Alice' }] | ||
| * | ||
| * // With BOM stripping and skip rows | ||
| * const records = CsvReader.parse(content, { | ||
| * stripBom: true, | ||
| * skipRows: 2 | ||
| * }); | ||
| * ``` | ||
| */ | ||
| static parse(content, options = {}) { | ||
| var _a; | ||
| if (!content || content.trim() === "") { | ||
| return []; | ||
| } | ||
| const stripBom = options.stripBom !== false; | ||
| let processedContent = content; | ||
| if (stripBom) { | ||
| processedContent = this.stripBom(content); | ||
| } | ||
| const validationMode = options.validationMode || "warn"; | ||
| const delimiter = options.delimiter || ","; | ||
| const quote = options.quote || '"'; | ||
| const lines = this.splitLines(content); | ||
| const skipRows = options.skipRows || 0; | ||
| const lines = this.splitLines(processedContent); | ||
| if (lines.length === 0) return []; | ||
| const headers = this.parseLine(lines[0], delimiter, quote); | ||
| const dataStartIndex = skipRows; | ||
| if (dataStartIndex >= lines.length) return []; | ||
| let headers = this.parseLine(lines[dataStartIndex], delimiter, quote); | ||
| if (headers.length === 0) return []; | ||
| if (options.headerTransformer) { | ||
| headers = headers.map(options.headerTransformer); | ||
| } | ||
| if (options.columnMapping) { | ||
| headers = headers.map((h) => { | ||
| var _a2, _b; | ||
| return (_b = (_a2 = options.columnMapping) == null ? void 0 : _a2[h]) != null ? _b : h; | ||
| }); | ||
| } | ||
| const records = []; | ||
| for (let i = 1; i < lines.length; i++) { | ||
| let dataRowIndex = 0; | ||
| for (let i = dataStartIndex + 1; i < lines.length; i++) { | ||
| const line = lines[i].trim(); | ||
@@ -114,3 +259,3 @@ if (line === "") continue; | ||
| if (validationMode === "error") { | ||
| throw new Error(message); | ||
| throw new CsvValidationError(message, lineNumber, headers.length, values.length); | ||
| } | ||
@@ -123,5 +268,13 @@ if (validationMode === "warn") { | ||
| for (let j = 0; j < headers.length; j++) { | ||
| const value = j < values.length ? values[j] : ""; | ||
| let value = j < values.length ? values[j] : ""; | ||
| if (value === "" && ((_a = options.defaultValues) == null ? void 0 : _a[headers[j]]) !== void 0) { | ||
| value = options.defaultValues[headers[j]]; | ||
| } | ||
| record[headers[j]] = value; | ||
| } | ||
| if (options.rowFilter && !options.rowFilter(record, dataRowIndex)) { | ||
| dataRowIndex++; | ||
| continue; | ||
| } | ||
| dataRowIndex++; | ||
| records.push(record); | ||
@@ -132,4 +285,36 @@ } | ||
| /** | ||
| * Split CSV content into lines, respecting quoted fields with newlines | ||
| * Strip BOM (Byte Order Mark) from the beginning of content. | ||
| * Handles UTF-8 and UTF-16 BOMs. | ||
| * | ||
| * @param content - The content that may contain a BOM | ||
| * @returns Content with BOM removed | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const clean = CsvReader.stripBom('\uFEFFid,name'); | ||
| * // 'id,name' | ||
| * ``` | ||
| */ | ||
| static stripBom(content) { | ||
| if (content.length === 0) return content; | ||
| if (content.charCodeAt(0) === 65279) { | ||
| return content.slice(1); | ||
| } | ||
| if (content.charCodeAt(0) === 65534) { | ||
| return content.slice(1); | ||
| } | ||
| return content; | ||
| } | ||
| /** | ||
| * Split CSV content into lines, respecting quoted fields that may contain newlines. | ||
| * | ||
| * @param content - The CSV content | ||
| * @returns Array of lines (quoted newlines are preserved within lines) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const lines = CsvReader.splitLines('id,note\n1,"Line 1\nLine 2"'); | ||
| * // ['id,note', '1,"Line 1\nLine 2"'] | ||
| * ``` | ||
| */ | ||
| static splitLines(content) { | ||
@@ -162,3 +347,18 @@ const lines = []; | ||
| /** | ||
| * Parse a single CSV line into values, respecting quotes and delimiters | ||
| * Parse a single CSV line into an array of values, respecting quotes and delimiters. | ||
| * | ||
| * @param line - A single line of CSV data | ||
| * @param delimiter - The field delimiter (default: ',') | ||
| * @param quote - The quote character (default: '"') | ||
| * @returns Array of parsed values | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const values = CsvReader.parseLine('1,"Hello, World",test'); | ||
| * // ['1', 'Hello, World', 'test'] | ||
| * | ||
| * // Escaped quotes | ||
| * const values = CsvReader.parseLine('1,"Say ""Hello""",test'); | ||
| * // ['1', 'Say "Hello"', 'test'] | ||
| * ``` | ||
| */ | ||
@@ -194,11 +394,41 @@ static parseLine(line, delimiter = ",", quote = '"') { | ||
| /** | ||
| * Convert flat CSV records into nested JSON structure with array detection | ||
| * Convert flat CSV records into nested JSON structure with array detection. | ||
| * | ||
| * @param records - Array of flat CSV records with string values | ||
| * @param options - Parser options for customizing conversion | ||
| * @returns Array of nested JSON objects | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Basic conversion with dot notation | ||
| * const records = [{ 'person.name': 'John', 'person.city': 'NYC' }]; | ||
| * const result = NestedJsonConverter.convert(records); | ||
| * // [{ person: { name: 'John', city: 'NYC' } }] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With value transformation | ||
| * const records = [{ id: '1', active: 'true', score: '95.5' }]; | ||
| * const result = NestedJsonConverter.convert(records, { | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true | ||
| * }); | ||
| * // [{ id: 1, active: true, score: 95.5 }] | ||
| * ``` | ||
| */ | ||
| static convert(records) { | ||
| static convert(records, options = {}) { | ||
| var _a, _b; | ||
| if (records.length === 0) return []; | ||
| const firstKey = Object.keys(records[0])[0]; | ||
| const arraySuffix = (_a = options.arraySuffixIndicator) != null ? _a : "[]"; | ||
| const emptyArrayBehavior = (_b = options.emptyArrayBehavior) != null ? _b : "omit"; | ||
| const forcedArrayFields = this.detectForcedArrayFields(records, arraySuffix); | ||
| const normalizedRecords = this.normalizeHeaders(records, arraySuffix); | ||
| const transformedRecords = this.applyValueTransformations(normalizedRecords, options); | ||
| const firstKey = Object.keys(transformedRecords[0])[0]; | ||
| const groups = []; | ||
| let currentGroup = []; | ||
| for (const row of records) { | ||
| if (row[firstKey] && row[firstKey].trim() !== "") { | ||
| for (const row of transformedRecords) { | ||
| const firstValue = row[firstKey]; | ||
| if (firstValue && String(firstValue).trim() !== "") { | ||
| if (currentGroup.length > 0) { | ||
@@ -216,5 +446,149 @@ groups.push(currentGroup); | ||
| const processedGroups = groups.map((group) => this.processGroup(group)); | ||
| const arrayFields = this.detectArrayFields(processedGroups); | ||
| return processedGroups.map((group) => this.normalizeArrays(group, arrayFields)); | ||
| const autoArrayFields = this.detectArrayFields(processedGroups); | ||
| const allArrayFields = /* @__PURE__ */ new Set([...forcedArrayFields, ...autoArrayFields]); | ||
| return processedGroups.map( | ||
| (group) => this.normalizeArrays(group, allArrayFields, forcedArrayFields, emptyArrayBehavior) | ||
| ); | ||
| } | ||
| /** | ||
| * Apply value transformations (null detection, auto-parse numbers, booleans, dates, custom transformer). | ||
| * Transformation order: nullValues → autoParseNumbers → autoParseBooleans → autoParseDates → valueTransformer | ||
| */ | ||
| static applyValueTransformations(records, options) { | ||
| const { autoParseNumbers, autoParseBooleans, autoParseDates, valueTransformer, nullValues, nullRepresentation } = options; | ||
| const nullSet = new Set((nullValues != null ? nullValues : ["null", "NULL", "nil", "NIL"]).map((v) => v.toLowerCase())); | ||
| if (!autoParseNumbers && !autoParseBooleans && !autoParseDates && !valueTransformer && nullValues === void 0) { | ||
| return records; | ||
| } | ||
| return records.map((record) => { | ||
| const transformed = {}; | ||
| for (const [header, value] of Object.entries(record)) { | ||
| let transformedValue = value; | ||
| if (value === "") { | ||
| if (nullValues !== void 0 && nullSet.has("")) { | ||
| transformedValue = this.applyNullRepresentation(nullRepresentation); | ||
| if (nullRepresentation === "omit") { | ||
| continue; | ||
| } | ||
| } | ||
| transformed[header] = transformedValue; | ||
| continue; | ||
| } | ||
| if (nullValues !== void 0 && nullSet.has(value.toLowerCase())) { | ||
| const nullVal = this.applyNullRepresentation(nullRepresentation); | ||
| if (nullRepresentation === "omit") { | ||
| continue; | ||
| } | ||
| transformed[header] = nullVal; | ||
| continue; | ||
| } | ||
| if (autoParseNumbers) { | ||
| const parsed = this.tryParseNumber(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (autoParseBooleans && typeof transformedValue === "string") { | ||
| const parsed = this.tryParseBoolean(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (autoParseDates && typeof transformedValue === "string") { | ||
| const parsed = this.tryParseDate(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (valueTransformer) { | ||
| transformedValue = valueTransformer(transformedValue, header); | ||
| } | ||
| transformed[header] = transformedValue; | ||
| } | ||
| return transformed; | ||
| }); | ||
| } | ||
| /** | ||
| * Apply null representation based on option. | ||
| */ | ||
| static applyNullRepresentation(representation) { | ||
| switch (representation) { | ||
| case "null": | ||
| return null; | ||
| case "undefined": | ||
| return void 0; | ||
| case "empty-string": | ||
| return ""; | ||
| case "omit": | ||
| default: | ||
| return void 0; | ||
| } | ||
| } | ||
| /** | ||
| * Try to parse a string as a number. | ||
| * Returns null if the string is not a valid number. | ||
| */ | ||
| static tryParseNumber(value) { | ||
| if (value.trim() === "") return null; | ||
| if (/^0\d+$/.test(value)) return null; | ||
| const parsed = Number(value); | ||
| if (!Number.isNaN(parsed) && Number.isFinite(parsed)) { | ||
| return parsed; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Try to parse a string as a boolean. | ||
| * Returns null if the string is not 'true' or 'false' (case-insensitive). | ||
| */ | ||
| static tryParseBoolean(value) { | ||
| const lower = value.toLowerCase().trim(); | ||
| if (lower === "true") return true; | ||
| if (lower === "false") return false; | ||
| return null; | ||
| } | ||
| /** | ||
| * Try to parse a string as a Date. | ||
| * Returns null if the string is not a valid date. | ||
| * Uses JavaScript's Date.parse() for recognition. | ||
| */ | ||
| static tryParseDate(value) { | ||
| if (value.trim() === "" || /^-?\d+(\.\d+)?$/.test(value)) return null; | ||
| const timestamp = Date.parse(value); | ||
| if (!Number.isNaN(timestamp)) { | ||
| return new Date(timestamp); | ||
| } | ||
| return null; | ||
| } | ||
| static detectForcedArrayFields(records, arraySuffix) { | ||
| if (records.length === 0 || !arraySuffix) return /* @__PURE__ */ new Set(); | ||
| const forcedFields = /* @__PURE__ */ new Set(); | ||
| const headers = Object.keys(records[0]); | ||
| for (const header of headers) { | ||
| const parts = header.split("."); | ||
| let currentPath = ""; | ||
| for (let i = 0; i < parts.length; i++) { | ||
| let part = parts[i]; | ||
| if (part.endsWith(arraySuffix)) { | ||
| part = part.slice(0, -arraySuffix.length); | ||
| currentPath = currentPath ? `${currentPath}.${part}` : part; | ||
| forcedFields.add(currentPath); | ||
| } else { | ||
| currentPath = currentPath ? `${currentPath}.${part}` : part; | ||
| } | ||
| } | ||
| } | ||
| return forcedFields; | ||
| } | ||
| static normalizeHeaders(records, arraySuffix) { | ||
| if (!arraySuffix) return records; | ||
| return records.map((record) => { | ||
| const normalized = {}; | ||
| for (const [key, value] of Object.entries(record)) { | ||
| const normalizedKey = key.split(".").map((part) => part.endsWith(arraySuffix) ? part.slice(0, -arraySuffix.length) : part).join("."); | ||
| normalized[normalizedKey] = value; | ||
| } | ||
| return normalized; | ||
| }); | ||
| } | ||
| static processGroup(rows) { | ||
@@ -231,3 +605,3 @@ const result = {}; | ||
| for (const [key, value] of Object.entries(row)) { | ||
| if (value === "" || value === void 0 || value === null) continue; | ||
| if (value === "" || value === void 0) continue; | ||
| const parts = key.split("."); | ||
@@ -252,4 +626,7 @@ let current = result; | ||
| targetValue.push(sourceValue); | ||
| } else if (typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue) && typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue)) { | ||
| const shouldCreateArray = this.shouldCreateArrayOfObjects(targetValue, sourceValue); | ||
| } else if (typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue) && !(targetValue instanceof Date) && typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue) && !(sourceValue instanceof Date)) { | ||
| const shouldCreateArray = this.shouldCreateArrayOfObjects( | ||
| targetValue, | ||
| sourceValue | ||
| ); | ||
| if (shouldCreateArray) { | ||
@@ -315,3 +692,3 @@ target[key] = [targetValue, sourceValue]; | ||
| } | ||
| } else if (value && typeof value === "object") { | ||
| } else if (value && typeof value === "object" && !(value instanceof Date)) { | ||
| checkForArrays(value, currentPath); | ||
@@ -326,3 +703,3 @@ } | ||
| } | ||
| static normalizeArrays(obj, arrayFields, path = "") { | ||
| static normalizeArrays(obj, arrayFields, forcedArrayFields, emptyArrayBehavior, path = "") { | ||
| const result = {}; | ||
@@ -334,11 +711,31 @@ for (const [key, value] of Object.entries(obj)) { | ||
| result[key] = value.map( | ||
| (item) => item && typeof item === "object" && !Array.isArray(item) ? this.normalizeArrays(item, arrayFields, currentPath) : item | ||
| (item) => item && typeof item === "object" && !Array.isArray(item) && !(item instanceof Date) ? this.normalizeArrays( | ||
| item, | ||
| arrayFields, | ||
| forcedArrayFields, | ||
| emptyArrayBehavior, | ||
| currentPath | ||
| ) : item | ||
| ); | ||
| } else if (value && typeof value === "object") { | ||
| result[key] = [this.normalizeArrays(value, arrayFields, currentPath)]; | ||
| } else if (value && typeof value === "object" && !(value instanceof Date)) { | ||
| result[key] = [ | ||
| this.normalizeArrays( | ||
| value, | ||
| arrayFields, | ||
| forcedArrayFields, | ||
| emptyArrayBehavior, | ||
| currentPath | ||
| ) | ||
| ]; | ||
| } else { | ||
| result[key] = [value]; | ||
| } | ||
| } else if (value && typeof value === "object" && !Array.isArray(value)) { | ||
| result[key] = this.normalizeArrays(value, arrayFields, currentPath); | ||
| } else if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) { | ||
| result[key] = this.normalizeArrays( | ||
| value, | ||
| arrayFields, | ||
| forcedArrayFields, | ||
| emptyArrayBehavior, | ||
| currentPath | ||
| ); | ||
| } else { | ||
@@ -348,2 +745,12 @@ result[key] = value; | ||
| } | ||
| for (const forcedPath of forcedArrayFields) { | ||
| const relativePath = path ? forcedPath.replace(`${path}.`, "") : forcedPath; | ||
| if (relativePath.includes(".")) continue; | ||
| if (forcedPath !== (path ? `${path}.${relativePath}` : relativePath)) continue; | ||
| if (!(relativePath in result)) { | ||
| if (emptyArrayBehavior === "empty-array") { | ||
| result[relativePath] = []; | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
@@ -356,6 +763,22 @@ } | ||
| /** | ||
| * Parse CSV file synchronously to nested JSON | ||
| * @param csvFilePath Path to CSV file | ||
| * @param options Parsing options | ||
| * Parse CSV file synchronously to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvFilePath - Path to the CSV file | ||
| * @param options - Parsing options | ||
| * @returns Array of nested JSON objects | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * interface Person { | ||
| * id: string; | ||
| * name: string; | ||
| * address: { city: string }; | ||
| * } | ||
| * | ||
| * const people = CsvParser.parseFileSync<Person>('people.csv'); | ||
| * console.log(people[0].address.city); | ||
| * ``` | ||
| */ | ||
@@ -367,6 +790,15 @@ static parseFileSync(csvFilePath, options = {}) { | ||
| /** | ||
| * Parse CSV file asynchronously to nested JSON | ||
| * @param csvFilePath Path to CSV file | ||
| * @param options Parsing options | ||
| * Parse CSV file asynchronously to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvFilePath - Path to the CSV file | ||
| * @param options - Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const people = await CsvParser.parseFile<Person>('people.csv'); | ||
| * ``` | ||
| */ | ||
@@ -378,26 +810,640 @@ static async parseFile(csvFilePath, options = {}) { | ||
| /** | ||
| * Parse CSV string content to nested JSON | ||
| * @param csvContent CSV content as string | ||
| * @param options Parsing options | ||
| * Parse CSV from readable stream to nested JSON. | ||
| * Note: This method buffers the entire stream before parsing. | ||
| * For true streaming with large files, use {@link CsvStreamParser}. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param stream - Readable stream containing CSV data | ||
| * @param options - Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const stream = fs.createReadStream('large-file.csv'); | ||
| * const result = await CsvParser.parseStream(stream); | ||
| * ``` | ||
| */ | ||
| static async parseStream(stream, options = {}) { | ||
| const csvContent = await CsvFileReader.readStream(stream, options); | ||
| return this.parseString(csvContent, options); | ||
| } | ||
| /** | ||
| * Parse CSV string content to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvContent - CSV content as string | ||
| * @param options - Parsing options | ||
| * @returns Array of nested JSON objects | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const csv = `id,name,address.city | ||
| * 1,John,NYC | ||
| * 2,Jane,LA`; | ||
| * | ||
| * const result = CsvParser.parseString(csv); | ||
| * // [ | ||
| * // { id: '1', name: 'John', address: { city: 'NYC' } }, | ||
| * // { id: '2', name: 'Jane', address: { city: 'LA' } } | ||
| * // ] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With continuation rows for arrays | ||
| * const csv = `id,tags | ||
| * 1,javascript | ||
| * ,typescript | ||
| * ,nodejs`; | ||
| * | ||
| * const result = CsvParser.parseString(csv); | ||
| * // [{ id: '1', tags: ['javascript', 'typescript', 'nodejs'] }] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With auto-parsing | ||
| * const csv = `id,active,score | ||
| * 1,true,95.5`; | ||
| * | ||
| * const result = CsvParser.parseString(csv, { | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true | ||
| * }); | ||
| * // [{ id: 1, active: true, score: 95.5 }] | ||
| * ``` | ||
| */ | ||
| static parseString(csvContent, options = {}) { | ||
| const records = CsvReader.parse(csvContent, options); | ||
| return NestedJsonConverter.convert(records); | ||
| return NestedJsonConverter.convert(records, options); | ||
| } | ||
| }; | ||
| // src/csv-stream-parser.ts | ||
| var import_node_stream = require("stream"); | ||
| var CsvStreamParser = class extends import_node_stream.Transform { | ||
| /** | ||
| * Parse CSV from readable stream to nested JSON | ||
| * @param stream Readable stream containing CSV data | ||
| * @param options Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * Creates a new streaming CSV parser. | ||
| * | ||
| * @param options - Parser options | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const parser = new CsvStreamParser({ | ||
| * delimiter: ';', | ||
| * quote: '"', | ||
| * skipRows: 1, | ||
| * autoParseNumbers: true | ||
| * }); | ||
| * ``` | ||
| */ | ||
| static async parseStream(stream, options = {}) { | ||
| const csvContent = await CsvFileReader.readStream(stream, options); | ||
| return this.parseString(csvContent, options); | ||
| constructor(options = {}) { | ||
| var _a; | ||
| super({ ...options, objectMode: true }); | ||
| this.buffer = ""; | ||
| this.headers = []; | ||
| this.headersProcessed = false; | ||
| this.rowsSkipped = 0; | ||
| this.dataRowIndex = 0; | ||
| this.bomStripped = false; | ||
| this.options = options; | ||
| this.delimiter = options.delimiter || ","; | ||
| this.quote = options.quote || '"'; | ||
| this.skipRows = options.skipRows || 0; | ||
| this.stripBom = options.stripBom !== false; | ||
| this.nullSet = new Set(((_a = options.nullValues) != null ? _a : ["null", "NULL", "nil", "NIL"]).map((v) => v.toLowerCase())); | ||
| } | ||
| /** | ||
| * Transform implementation - processes incoming chunks. | ||
| * @internal | ||
| */ | ||
| _transform(chunk, _encoding, callback) { | ||
| try { | ||
| let data = typeof chunk === "string" ? chunk : chunk.toString(this.options.encoding || "utf-8"); | ||
| if (this.stripBom && !this.bomStripped) { | ||
| data = this.stripBomFromString(data); | ||
| this.bomStripped = true; | ||
| } | ||
| this.buffer += data; | ||
| this.processBuffer(); | ||
| callback(); | ||
| } catch (error) { | ||
| callback(error instanceof Error ? error : new Error(String(error))); | ||
| } | ||
| } | ||
| /** | ||
| * Flush implementation - processes any remaining data. | ||
| * @internal | ||
| */ | ||
| _flush(callback) { | ||
| try { | ||
| if (this.buffer.trim()) { | ||
| this.processLine(this.buffer); | ||
| } | ||
| callback(); | ||
| } catch (error) { | ||
| callback(error instanceof Error ? error : new Error(String(error))); | ||
| } | ||
| } | ||
| /** | ||
| * Strip BOM from the beginning of a string. | ||
| */ | ||
| stripBomFromString(content) { | ||
| if (content.length === 0) return content; | ||
| if (content.charCodeAt(0) === 65279) { | ||
| return content.slice(1); | ||
| } | ||
| if (content.charCodeAt(0) === 65534) { | ||
| return content.slice(1); | ||
| } | ||
| return content; | ||
| } | ||
| /** | ||
| * Process the buffer and extract complete lines. | ||
| */ | ||
| processBuffer() { | ||
| let insideQuotes = false; | ||
| let lineStart = 0; | ||
| for (let i = 0; i < this.buffer.length; i++) { | ||
| const char = this.buffer[i]; | ||
| const nextChar = i + 1 < this.buffer.length ? this.buffer[i + 1] : ""; | ||
| if (char === this.quote) { | ||
| insideQuotes = !insideQuotes; | ||
| } else if (!insideQuotes) { | ||
| if (char === "\r" && nextChar === "\n") { | ||
| const line = this.buffer.slice(lineStart, i); | ||
| this.processLine(line); | ||
| i++; | ||
| lineStart = i + 1; | ||
| } else if (char === "\n" || char === "\r") { | ||
| const line = this.buffer.slice(lineStart, i); | ||
| this.processLine(line); | ||
| lineStart = i + 1; | ||
| } | ||
| } | ||
| } | ||
| this.buffer = this.buffer.slice(lineStart); | ||
| } | ||
| /** | ||
| * Process a single line of CSV data. | ||
| */ | ||
| processLine(line) { | ||
| if (line.trim() === "") { | ||
| return; | ||
| } | ||
| if (this.rowsSkipped < this.skipRows) { | ||
| this.rowsSkipped++; | ||
| return; | ||
| } | ||
| const values = this.parseLine(line); | ||
| if (!this.headersProcessed) { | ||
| this.headers = values; | ||
| if (this.options.headerTransformer) { | ||
| this.headers = this.headers.map(this.options.headerTransformer); | ||
| } | ||
| if (this.options.columnMapping) { | ||
| this.headers = this.headers.map((h) => { | ||
| var _a, _b; | ||
| return (_b = (_a = this.options.columnMapping) == null ? void 0 : _a[h]) != null ? _b : h; | ||
| }); | ||
| } | ||
| this.headersProcessed = true; | ||
| return; | ||
| } | ||
| const record = this.createRecord(values); | ||
| if (this.options.rowFilter && !this.options.rowFilter(record, this.dataRowIndex)) { | ||
| this.dataRowIndex++; | ||
| return; | ||
| } | ||
| this.dataRowIndex++; | ||
| const transformed = this.applyTransformations(record); | ||
| const nested = this.options.nested !== false; | ||
| if (nested) { | ||
| const nestedRecord = this.unflatten(transformed); | ||
| this.push(nestedRecord); | ||
| } else { | ||
| this.push(transformed); | ||
| } | ||
| } | ||
| /** | ||
| * Parse a single line into values. | ||
| */ | ||
| parseLine(line) { | ||
| const values = []; | ||
| let currentValue = ""; | ||
| let insideQuotes = false; | ||
| for (let i = 0; i < line.length; i++) { | ||
| const char = line[i]; | ||
| const nextChar = i + 1 < line.length ? line[i + 1] : ""; | ||
| if (char === this.quote) { | ||
| if (insideQuotes && nextChar === this.quote) { | ||
| currentValue += this.quote; | ||
| i++; | ||
| } else { | ||
| insideQuotes = !insideQuotes; | ||
| } | ||
| } else if (char === this.delimiter && !insideQuotes) { | ||
| values.push(currentValue); | ||
| currentValue = ""; | ||
| } else { | ||
| currentValue += char; | ||
| } | ||
| } | ||
| values.push(currentValue); | ||
| return values; | ||
| } | ||
| /** | ||
| * Create a record object from values array. | ||
| */ | ||
| createRecord(values) { | ||
| var _a; | ||
| const record = {}; | ||
| for (let i = 0; i < this.headers.length; i++) { | ||
| let value = i < values.length ? values[i] : ""; | ||
| if (value === "" && ((_a = this.options.defaultValues) == null ? void 0 : _a[this.headers[i]]) !== void 0) { | ||
| value = this.options.defaultValues[this.headers[i]]; | ||
| } | ||
| record[this.headers[i]] = value; | ||
| } | ||
| return record; | ||
| } | ||
| /** | ||
| * Apply value transformations to a record. | ||
| */ | ||
| applyTransformations(record) { | ||
| const { autoParseNumbers, autoParseBooleans, autoParseDates, valueTransformer, nullValues, nullRepresentation } = this.options; | ||
| if (!autoParseNumbers && !autoParseBooleans && !autoParseDates && !valueTransformer && nullValues === void 0) { | ||
| return record; | ||
| } | ||
| const transformed = {}; | ||
| for (const [header, value] of Object.entries(record)) { | ||
| let transformedValue = value; | ||
| if (value === "") { | ||
| if (nullValues !== void 0 && this.nullSet.has("")) { | ||
| transformedValue = this.applyNullRepresentation(nullRepresentation); | ||
| if (nullRepresentation === "omit") { | ||
| continue; | ||
| } | ||
| } | ||
| transformed[header] = transformedValue; | ||
| continue; | ||
| } | ||
| if (nullValues !== void 0 && this.nullSet.has(value.toLowerCase())) { | ||
| const nullVal = this.applyNullRepresentation(nullRepresentation); | ||
| if (nullRepresentation === "omit") { | ||
| continue; | ||
| } | ||
| transformed[header] = nullVal; | ||
| continue; | ||
| } | ||
| if (autoParseNumbers) { | ||
| const parsed = this.tryParseNumber(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (autoParseBooleans && typeof transformedValue === "string") { | ||
| const parsed = this.tryParseBoolean(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (autoParseDates && typeof transformedValue === "string") { | ||
| const parsed = this.tryParseDate(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (valueTransformer) { | ||
| transformedValue = valueTransformer(transformedValue, header); | ||
| } | ||
| transformed[header] = transformedValue; | ||
| } | ||
| return transformed; | ||
| } | ||
| /** | ||
| * Apply null representation based on option. | ||
| */ | ||
| applyNullRepresentation(representation) { | ||
| switch (representation) { | ||
| case "null": | ||
| return null; | ||
| case "undefined": | ||
| return void 0; | ||
| case "empty-string": | ||
| return ""; | ||
| case "omit": | ||
| default: | ||
| return void 0; | ||
| } | ||
| } | ||
| /** | ||
| * Try to parse a string as a number. | ||
| */ | ||
| tryParseNumber(value) { | ||
| if (value.trim() === "") return null; | ||
| if (/^0\d+$/.test(value)) return null; | ||
| const parsed = Number(value); | ||
| if (!Number.isNaN(parsed) && Number.isFinite(parsed)) { | ||
| return parsed; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Try to parse a string as a boolean. | ||
| */ | ||
| tryParseBoolean(value) { | ||
| const lower = value.toLowerCase().trim(); | ||
| if (lower === "true") return true; | ||
| if (lower === "false") return false; | ||
| return null; | ||
| } | ||
| /** | ||
| * Try to parse a string as a Date. | ||
| * Uses JavaScript's Date.parse() for recognition. | ||
| */ | ||
| tryParseDate(value) { | ||
| if (value.trim() === "" || /^-?\d+(\.\d+)?$/.test(value)) return null; | ||
| const timestamp = Date.parse(value); | ||
| if (!Number.isNaN(timestamp)) { | ||
| return new Date(timestamp); | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Unflatten a record with dot-notation keys into a nested object. | ||
| */ | ||
| unflatten(record) { | ||
| var _a; | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(record)) { | ||
| if (value === "" || value === void 0) continue; | ||
| const arraySuffix = (_a = this.options.arraySuffixIndicator) != null ? _a : "[]"; | ||
| const normalizedKey = key.split(".").map((part) => part.endsWith(arraySuffix) ? part.slice(0, -arraySuffix.length) : part).join("."); | ||
| const parts = normalizedKey.split("."); | ||
| let current = result; | ||
| for (let i = 0; i < parts.length - 1; i++) { | ||
| const part = parts[i]; | ||
| if (!current[part]) { | ||
| current[part] = {}; | ||
| } | ||
| current = current[part]; | ||
| } | ||
| current[parts[parts.length - 1]] = value; | ||
| } | ||
| return result; | ||
| } | ||
| }; | ||
| // src/json-to-csv.ts | ||
| var import_node_fs2 = __toESM(require("fs")); | ||
| var JsonToCsv = class { | ||
| /** | ||
| * Convert array of nested objects to CSV string. | ||
| * | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * @returns CSV string | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const csv = JsonToCsv.stringify([ | ||
| * { id: 1, name: 'Alice', address: { city: 'NYC' } } | ||
| * ]); | ||
| * // "id,name,address.city\n1,Alice,NYC" | ||
| * ``` | ||
| */ | ||
| static stringify(data, options = {}) { | ||
| if (!data || data.length === 0) { | ||
| return ""; | ||
| } | ||
| const delimiter = options.delimiter || ","; | ||
| const quote = options.quote || '"'; | ||
| const lineEnding = options.lineEnding || "\n"; | ||
| const includeHeader = options.includeHeader !== false; | ||
| const arrayMode = options.arrayMode || "rows"; | ||
| const headers = this.collectHeaders(data); | ||
| const rows = []; | ||
| if (includeHeader) { | ||
| rows.push(headers.map((h) => this.escapeValue(h, delimiter, quote)).join(delimiter)); | ||
| } | ||
| for (const obj of data) { | ||
| const flatRows = this.flattenObject(obj, headers, arrayMode); | ||
| for (const flatRow of flatRows) { | ||
| const values = headers.map((header) => { | ||
| const value = flatRow[header]; | ||
| return this.escapeValue(value, delimiter, quote); | ||
| }); | ||
| rows.push(values.join(delimiter)); | ||
| } | ||
| } | ||
| return rows.join(lineEnding); | ||
| } | ||
| /** | ||
| * Write nested objects to CSV file synchronously. | ||
| * | ||
| * @param filePath - Path to output file | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * JsonToCsv.writeFileSync('output.csv', data, { delimiter: ';' }); | ||
| * ``` | ||
| */ | ||
| static writeFileSync(filePath, data, options = {}) { | ||
| const csv = this.stringify(data, options); | ||
| const encoding = options.encoding || "utf-8"; | ||
| import_node_fs2.default.writeFileSync(filePath, csv, encoding); | ||
| } | ||
| /** | ||
| * Write nested objects to CSV file asynchronously. | ||
| * | ||
| * @param filePath - Path to output file | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * @returns Promise that resolves when file is written | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await JsonToCsv.writeFile('output.csv', data); | ||
| * ``` | ||
| */ | ||
| static async writeFile(filePath, data, options = {}) { | ||
| const csv = this.stringify(data, options); | ||
| const encoding = options.encoding || "utf-8"; | ||
| await import_node_fs2.default.promises.writeFile(filePath, csv, encoding); | ||
| } | ||
| /** | ||
| * Collect all unique headers from an array of nested objects. | ||
| * Headers are generated using dot-notation for nested properties. | ||
| */ | ||
| static collectHeaders(data) { | ||
| const headerSet = /* @__PURE__ */ new Set(); | ||
| for (const obj of data) { | ||
| this.collectHeadersFromObject(obj, "", headerSet); | ||
| } | ||
| const headers = Array.from(headerSet); | ||
| headers.sort((a, b) => { | ||
| const aDepth = a.split(".").length; | ||
| const bDepth = b.split(".").length; | ||
| if (aDepth !== bDepth) return aDepth - bDepth; | ||
| return a.localeCompare(b); | ||
| }); | ||
| return headers; | ||
| } | ||
| /** | ||
| * Recursively collect headers from a nested object. | ||
| */ | ||
| static collectHeadersFromObject(obj, prefix, headers) { | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| const fullKey = prefix ? `${prefix}.${key}` : key; | ||
| if (Array.isArray(value)) { | ||
| if (value.length > 0) { | ||
| const firstItem = value[0]; | ||
| if (firstItem && typeof firstItem === "object" && !Array.isArray(firstItem)) { | ||
| this.collectHeadersFromObject(firstItem, fullKey, headers); | ||
| } else { | ||
| headers.add(fullKey); | ||
| } | ||
| } else { | ||
| headers.add(fullKey); | ||
| } | ||
| } else if (value && typeof value === "object") { | ||
| this.collectHeadersFromObject(value, fullKey, headers); | ||
| } else { | ||
| headers.add(fullKey); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Flatten a nested object into one or more rows of flat key-value pairs. | ||
| * Arrays result in multiple rows (continuation rows). | ||
| */ | ||
| static flattenObject(obj, headers, arrayMode) { | ||
| const flatValues = {}; | ||
| const arrayValues = {}; | ||
| this.flattenToPathValues(obj, "", flatValues, arrayValues); | ||
| if (Object.keys(arrayValues).length === 0 || arrayMode === "json") { | ||
| const row = {}; | ||
| for (const header of headers) { | ||
| if (header in flatValues) { | ||
| row[header] = this.valueToString(flatValues[header]); | ||
| } else if (header in arrayValues) { | ||
| row[header] = JSON.stringify(arrayValues[header]); | ||
| } else { | ||
| row[header] = ""; | ||
| } | ||
| } | ||
| return [row]; | ||
| } | ||
| return this.generateContinuationRows(headers, flatValues, arrayValues); | ||
| } | ||
| /** | ||
| * Flatten a nested object to path-value pairs, separating arrays. | ||
| */ | ||
| static flattenToPathValues(obj, prefix, flatValues, arrayValues) { | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| const fullKey = prefix ? `${prefix}.${key}` : key; | ||
| if (Array.isArray(value)) { | ||
| if (value.length > 0 && typeof value[0] === "object" && value[0] !== null && !Array.isArray(value[0])) { | ||
| arrayValues[fullKey] = value; | ||
| } else { | ||
| arrayValues[fullKey] = value; | ||
| } | ||
| } else if (value && typeof value === "object") { | ||
| this.flattenToPathValues(value, fullKey, flatValues, arrayValues); | ||
| } else { | ||
| flatValues[fullKey] = value; | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Generate continuation rows for arrays. | ||
| * First row contains all values, subsequent rows only contain array continuations. | ||
| */ | ||
| static generateContinuationRows(headers, flatValues, arrayValues) { | ||
| let maxArrayLength = 1; | ||
| for (const arr of Object.values(arrayValues)) { | ||
| if (arr.length > maxArrayLength) { | ||
| maxArrayLength = arr.length; | ||
| } | ||
| } | ||
| const rows = []; | ||
| for (let i = 0; i < maxArrayLength; i++) { | ||
| const row = {}; | ||
| for (const header of headers) { | ||
| if (header in arrayValues) { | ||
| const arr = arrayValues[header]; | ||
| if (i < arr.length) { | ||
| const item = arr[i]; | ||
| if (item && typeof item === "object" && !Array.isArray(item)) { | ||
| const itemFlat = {}; | ||
| const itemArrays = {}; | ||
| this.flattenToPathValues(item, header, itemFlat, itemArrays); | ||
| for (const [path, val] of Object.entries(itemFlat)) { | ||
| if (headers.includes(path)) { | ||
| row[path] = this.valueToString(val); | ||
| } | ||
| } | ||
| } else { | ||
| row[header] = this.valueToString(item); | ||
| } | ||
| } else { | ||
| row[header] = ""; | ||
| } | ||
| } else if (i === 0 && header in flatValues) { | ||
| row[header] = this.valueToString(flatValues[header]); | ||
| } else if (!(header in row)) { | ||
| row[header] = ""; | ||
| } | ||
| } | ||
| rows.push(row); | ||
| } | ||
| return rows; | ||
| } | ||
| /** | ||
| * Convert a value to string for CSV output. | ||
| */ | ||
| static valueToString(value) { | ||
| if (value === null || value === void 0) { | ||
| return ""; | ||
| } | ||
| if (typeof value === "object") { | ||
| return JSON.stringify(value); | ||
| } | ||
| return String(value); | ||
| } | ||
| /** | ||
| * Escape a value for CSV output. | ||
| * Wraps in quotes if the value contains delimiter, quote, or newline. | ||
| */ | ||
| static escapeValue(value, delimiter, quote) { | ||
| if (value === void 0 || value === null) { | ||
| return ""; | ||
| } | ||
| const str = String(value); | ||
| const needsEscape = str.includes(delimiter) || str.includes(quote) || str.includes("\n") || str.includes("\r"); | ||
| if (needsEscape) { | ||
| const escaped = str.replace(new RegExp(quote, "g"), quote + quote); | ||
| return quote + escaped + quote; | ||
| } | ||
| return str; | ||
| } | ||
| }; | ||
| // Annotate the CommonJS export names for ESM import in node: | ||
| 0 && (module.exports = { | ||
| CsvParser | ||
| CsvEncodingError, | ||
| CsvFileNotFoundError, | ||
| CsvFileReader, | ||
| CsvParseError, | ||
| CsvParser, | ||
| CsvReader, | ||
| CsvStreamParser, | ||
| CsvValidationError, | ||
| JsonToCsv, | ||
| NestedJsonConverter | ||
| }); | ||
| //# sourceMappingURL=index.js.map |
+1085
-48
| // src/csv-file-reader.ts | ||
| import fs from "fs"; | ||
| // src/errors.ts | ||
| var CsvParseError = class _CsvParseError extends Error { | ||
| /** | ||
| * Creates a new CSV parse error | ||
| * @param message - Human-readable error description | ||
| * @param row - The 1-based row number where the error occurred (optional) | ||
| * @param column - The 1-based column number where the error occurred (optional) | ||
| */ | ||
| constructor(message, row, column) { | ||
| super(message); | ||
| this.row = row; | ||
| this.column = column; | ||
| this.name = "CsvParseError"; | ||
| if (Error.captureStackTrace) { | ||
| Error.captureStackTrace(this, _CsvParseError); | ||
| } | ||
| } | ||
| }; | ||
| var CsvFileNotFoundError = class _CsvFileNotFoundError extends CsvParseError { | ||
| /** | ||
| * Creates a new file not found error | ||
| * @param filePath - The path to the file that was not found | ||
| */ | ||
| constructor(filePath) { | ||
| super(`CSV file not found: ${filePath}`); | ||
| this.filePath = filePath; | ||
| this.name = "CsvFileNotFoundError"; | ||
| if (Error.captureStackTrace) { | ||
| Error.captureStackTrace(this, _CsvFileNotFoundError); | ||
| } | ||
| } | ||
| }; | ||
| var CsvValidationError = class _CsvValidationError extends CsvParseError { | ||
| /** | ||
| * Creates a new validation error | ||
| * @param message - Human-readable error description | ||
| * @param row - The 1-based row number where validation failed | ||
| * @param expectedColumns - The expected number of columns (from header) | ||
| * @param actualColumns - The actual number of columns found in the row | ||
| */ | ||
| constructor(message, row, expectedColumns, actualColumns) { | ||
| super(message, row); | ||
| this.expectedColumns = expectedColumns; | ||
| this.actualColumns = actualColumns; | ||
| this.name = "CsvValidationError"; | ||
| if (Error.captureStackTrace) { | ||
| Error.captureStackTrace(this, _CsvValidationError); | ||
| } | ||
| } | ||
| }; | ||
| var CsvEncodingError = class _CsvEncodingError extends CsvParseError { | ||
| /** | ||
| * Creates a new encoding error | ||
| * @param message - Human-readable error description | ||
| * @param encoding - The encoding that was being used | ||
| */ | ||
| constructor(message, encoding) { | ||
| super(message); | ||
| this.encoding = encoding; | ||
| this.name = "CsvEncodingError"; | ||
| if (Error.captureStackTrace) { | ||
| Error.captureStackTrace(this, _CsvEncodingError); | ||
| } | ||
| } | ||
| }; | ||
| // src/csv-file-reader.ts | ||
| var CsvFileReader = class { | ||
| /** | ||
| * Read CSV file synchronously | ||
| * Read CSV file synchronously. | ||
| * | ||
| * @param filePath - Path to the CSV file | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns The file content as a string | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const content = CsvFileReader.readFileSync('data.csv', { encoding: 'utf-8' }); | ||
| * ``` | ||
| */ | ||
| static readFileSync(filePath, options = {}) { | ||
| if (!fs.existsSync(filePath)) { | ||
| throw new Error(`CSV file ${filePath} not found.`); | ||
| throw new CsvFileNotFoundError(filePath); | ||
| } | ||
@@ -15,7 +93,17 @@ const encoding = options.encoding || "utf-8"; | ||
| /** | ||
| * Read CSV file asynchronously | ||
| * Read CSV file asynchronously. | ||
| * | ||
| * @param filePath - Path to the CSV file | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns Promise resolving to the file content as a string | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const content = await CsvFileReader.readFile('data.csv', { encoding: 'utf-16le' }); | ||
| * ``` | ||
| */ | ||
| static async readFile(filePath, options = {}) { | ||
| if (!fs.existsSync(filePath)) { | ||
| throw new Error(`CSV file ${filePath} not found.`); | ||
| throw new CsvFileNotFoundError(filePath); | ||
| } | ||
@@ -26,3 +114,13 @@ const encoding = options.encoding || "utf-8"; | ||
| /** | ||
| * Read CSV from readable stream | ||
| * Read CSV from a readable stream. | ||
| * | ||
| * @param stream - Readable stream containing CSV data | ||
| * @param options - Parser options (uses `encoding` option) | ||
| * @returns Promise resolving to the stream content as a string | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const stream = fs.createReadStream('large-file.csv'); | ||
| * const content = await CsvFileReader.readStream(stream); | ||
| * ``` | ||
| */ | ||
@@ -53,17 +151,55 @@ static async readStream(stream, options = {}) { | ||
| /** | ||
| * Parse CSV content into array of record objects | ||
| * Parse CSV content into an array of flat record objects. | ||
| * Each record maps header names to cell values. | ||
| * | ||
| * @param content - The CSV content as a string | ||
| * @param options - Parser options | ||
| * @returns Array of flat record objects with string values | ||
| * @throws {CsvValidationError} If validationMode is 'error' and row has too many columns | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Basic parsing | ||
| * const records = CsvReader.parse('id,name\n1,Alice'); | ||
| * // [{ id: '1', name: 'Alice' }] | ||
| * | ||
| * // With BOM stripping and skip rows | ||
| * const records = CsvReader.parse(content, { | ||
| * stripBom: true, | ||
| * skipRows: 2 | ||
| * }); | ||
| * ``` | ||
| */ | ||
| static parse(content, options = {}) { | ||
| var _a; | ||
| if (!content || content.trim() === "") { | ||
| return []; | ||
| } | ||
| const stripBom = options.stripBom !== false; | ||
| let processedContent = content; | ||
| if (stripBom) { | ||
| processedContent = this.stripBom(content); | ||
| } | ||
| const validationMode = options.validationMode || "warn"; | ||
| const delimiter = options.delimiter || ","; | ||
| const quote = options.quote || '"'; | ||
| const lines = this.splitLines(content); | ||
| const skipRows = options.skipRows || 0; | ||
| const lines = this.splitLines(processedContent); | ||
| if (lines.length === 0) return []; | ||
| const headers = this.parseLine(lines[0], delimiter, quote); | ||
| const dataStartIndex = skipRows; | ||
| if (dataStartIndex >= lines.length) return []; | ||
| let headers = this.parseLine(lines[dataStartIndex], delimiter, quote); | ||
| if (headers.length === 0) return []; | ||
| if (options.headerTransformer) { | ||
| headers = headers.map(options.headerTransformer); | ||
| } | ||
| if (options.columnMapping) { | ||
| headers = headers.map((h) => { | ||
| var _a2, _b; | ||
| return (_b = (_a2 = options.columnMapping) == null ? void 0 : _a2[h]) != null ? _b : h; | ||
| }); | ||
| } | ||
| const records = []; | ||
| for (let i = 1; i < lines.length; i++) { | ||
| let dataRowIndex = 0; | ||
| for (let i = dataStartIndex + 1; i < lines.length; i++) { | ||
| const line = lines[i].trim(); | ||
@@ -76,3 +212,3 @@ if (line === "") continue; | ||
| if (validationMode === "error") { | ||
| throw new Error(message); | ||
| throw new CsvValidationError(message, lineNumber, headers.length, values.length); | ||
| } | ||
@@ -85,5 +221,13 @@ if (validationMode === "warn") { | ||
| for (let j = 0; j < headers.length; j++) { | ||
| const value = j < values.length ? values[j] : ""; | ||
| let value = j < values.length ? values[j] : ""; | ||
| if (value === "" && ((_a = options.defaultValues) == null ? void 0 : _a[headers[j]]) !== void 0) { | ||
| value = options.defaultValues[headers[j]]; | ||
| } | ||
| record[headers[j]] = value; | ||
| } | ||
| if (options.rowFilter && !options.rowFilter(record, dataRowIndex)) { | ||
| dataRowIndex++; | ||
| continue; | ||
| } | ||
| dataRowIndex++; | ||
| records.push(record); | ||
@@ -94,4 +238,36 @@ } | ||
| /** | ||
| * Split CSV content into lines, respecting quoted fields with newlines | ||
| * Strip BOM (Byte Order Mark) from the beginning of content. | ||
| * Handles UTF-8 and UTF-16 BOMs. | ||
| * | ||
| * @param content - The content that may contain a BOM | ||
| * @returns Content with BOM removed | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const clean = CsvReader.stripBom('\uFEFFid,name'); | ||
| * // 'id,name' | ||
| * ``` | ||
| */ | ||
| static stripBom(content) { | ||
| if (content.length === 0) return content; | ||
| if (content.charCodeAt(0) === 65279) { | ||
| return content.slice(1); | ||
| } | ||
| if (content.charCodeAt(0) === 65534) { | ||
| return content.slice(1); | ||
| } | ||
| return content; | ||
| } | ||
| /** | ||
| * Split CSV content into lines, respecting quoted fields that may contain newlines. | ||
| * | ||
| * @param content - The CSV content | ||
| * @returns Array of lines (quoted newlines are preserved within lines) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const lines = CsvReader.splitLines('id,note\n1,"Line 1\nLine 2"'); | ||
| * // ['id,note', '1,"Line 1\nLine 2"'] | ||
| * ``` | ||
| */ | ||
| static splitLines(content) { | ||
@@ -124,3 +300,18 @@ const lines = []; | ||
| /** | ||
| * Parse a single CSV line into values, respecting quotes and delimiters | ||
| * Parse a single CSV line into an array of values, respecting quotes and delimiters. | ||
| * | ||
| * @param line - A single line of CSV data | ||
| * @param delimiter - The field delimiter (default: ',') | ||
| * @param quote - The quote character (default: '"') | ||
| * @returns Array of parsed values | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const values = CsvReader.parseLine('1,"Hello, World",test'); | ||
| * // ['1', 'Hello, World', 'test'] | ||
| * | ||
| * // Escaped quotes | ||
| * const values = CsvReader.parseLine('1,"Say ""Hello""",test'); | ||
| * // ['1', 'Say "Hello"', 'test'] | ||
| * ``` | ||
| */ | ||
@@ -156,11 +347,41 @@ static parseLine(line, delimiter = ",", quote = '"') { | ||
| /** | ||
| * Convert flat CSV records into nested JSON structure with array detection | ||
| * Convert flat CSV records into nested JSON structure with array detection. | ||
| * | ||
| * @param records - Array of flat CSV records with string values | ||
| * @param options - Parser options for customizing conversion | ||
| * @returns Array of nested JSON objects | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Basic conversion with dot notation | ||
| * const records = [{ 'person.name': 'John', 'person.city': 'NYC' }]; | ||
| * const result = NestedJsonConverter.convert(records); | ||
| * // [{ person: { name: 'John', city: 'NYC' } }] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With value transformation | ||
| * const records = [{ id: '1', active: 'true', score: '95.5' }]; | ||
| * const result = NestedJsonConverter.convert(records, { | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true | ||
| * }); | ||
| * // [{ id: 1, active: true, score: 95.5 }] | ||
| * ``` | ||
| */ | ||
| static convert(records) { | ||
| static convert(records, options = {}) { | ||
| var _a, _b; | ||
| if (records.length === 0) return []; | ||
| const firstKey = Object.keys(records[0])[0]; | ||
| const arraySuffix = (_a = options.arraySuffixIndicator) != null ? _a : "[]"; | ||
| const emptyArrayBehavior = (_b = options.emptyArrayBehavior) != null ? _b : "omit"; | ||
| const forcedArrayFields = this.detectForcedArrayFields(records, arraySuffix); | ||
| const normalizedRecords = this.normalizeHeaders(records, arraySuffix); | ||
| const transformedRecords = this.applyValueTransformations(normalizedRecords, options); | ||
| const firstKey = Object.keys(transformedRecords[0])[0]; | ||
| const groups = []; | ||
| let currentGroup = []; | ||
| for (const row of records) { | ||
| if (row[firstKey] && row[firstKey].trim() !== "") { | ||
| for (const row of transformedRecords) { | ||
| const firstValue = row[firstKey]; | ||
| if (firstValue && String(firstValue).trim() !== "") { | ||
| if (currentGroup.length > 0) { | ||
@@ -178,5 +399,149 @@ groups.push(currentGroup); | ||
| const processedGroups = groups.map((group) => this.processGroup(group)); | ||
| const arrayFields = this.detectArrayFields(processedGroups); | ||
| return processedGroups.map((group) => this.normalizeArrays(group, arrayFields)); | ||
| const autoArrayFields = this.detectArrayFields(processedGroups); | ||
| const allArrayFields = /* @__PURE__ */ new Set([...forcedArrayFields, ...autoArrayFields]); | ||
| return processedGroups.map( | ||
| (group) => this.normalizeArrays(group, allArrayFields, forcedArrayFields, emptyArrayBehavior) | ||
| ); | ||
| } | ||
| /** | ||
| * Apply value transformations (null detection, auto-parse numbers, booleans, dates, custom transformer). | ||
| * Transformation order: nullValues → autoParseNumbers → autoParseBooleans → autoParseDates → valueTransformer | ||
| */ | ||
| static applyValueTransformations(records, options) { | ||
| const { autoParseNumbers, autoParseBooleans, autoParseDates, valueTransformer, nullValues, nullRepresentation } = options; | ||
| const nullSet = new Set((nullValues != null ? nullValues : ["null", "NULL", "nil", "NIL"]).map((v) => v.toLowerCase())); | ||
| if (!autoParseNumbers && !autoParseBooleans && !autoParseDates && !valueTransformer && nullValues === void 0) { | ||
| return records; | ||
| } | ||
| return records.map((record) => { | ||
| const transformed = {}; | ||
| for (const [header, value] of Object.entries(record)) { | ||
| let transformedValue = value; | ||
| if (value === "") { | ||
| if (nullValues !== void 0 && nullSet.has("")) { | ||
| transformedValue = this.applyNullRepresentation(nullRepresentation); | ||
| if (nullRepresentation === "omit") { | ||
| continue; | ||
| } | ||
| } | ||
| transformed[header] = transformedValue; | ||
| continue; | ||
| } | ||
| if (nullValues !== void 0 && nullSet.has(value.toLowerCase())) { | ||
| const nullVal = this.applyNullRepresentation(nullRepresentation); | ||
| if (nullRepresentation === "omit") { | ||
| continue; | ||
| } | ||
| transformed[header] = nullVal; | ||
| continue; | ||
| } | ||
| if (autoParseNumbers) { | ||
| const parsed = this.tryParseNumber(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (autoParseBooleans && typeof transformedValue === "string") { | ||
| const parsed = this.tryParseBoolean(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (autoParseDates && typeof transformedValue === "string") { | ||
| const parsed = this.tryParseDate(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (valueTransformer) { | ||
| transformedValue = valueTransformer(transformedValue, header); | ||
| } | ||
| transformed[header] = transformedValue; | ||
| } | ||
| return transformed; | ||
| }); | ||
| } | ||
| /** | ||
| * Apply null representation based on option. | ||
| */ | ||
| static applyNullRepresentation(representation) { | ||
| switch (representation) { | ||
| case "null": | ||
| return null; | ||
| case "undefined": | ||
| return void 0; | ||
| case "empty-string": | ||
| return ""; | ||
| case "omit": | ||
| default: | ||
| return void 0; | ||
| } | ||
| } | ||
| /** | ||
| * Try to parse a string as a number. | ||
| * Returns null if the string is not a valid number. | ||
| */ | ||
| static tryParseNumber(value) { | ||
| if (value.trim() === "") return null; | ||
| if (/^0\d+$/.test(value)) return null; | ||
| const parsed = Number(value); | ||
| if (!Number.isNaN(parsed) && Number.isFinite(parsed)) { | ||
| return parsed; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Try to parse a string as a boolean. | ||
| * Returns null if the string is not 'true' or 'false' (case-insensitive). | ||
| */ | ||
| static tryParseBoolean(value) { | ||
| const lower = value.toLowerCase().trim(); | ||
| if (lower === "true") return true; | ||
| if (lower === "false") return false; | ||
| return null; | ||
| } | ||
| /** | ||
| * Try to parse a string as a Date. | ||
| * Returns null if the string is not a valid date. | ||
| * Uses JavaScript's Date.parse() for recognition. | ||
| */ | ||
| static tryParseDate(value) { | ||
| if (value.trim() === "" || /^-?\d+(\.\d+)?$/.test(value)) return null; | ||
| const timestamp = Date.parse(value); | ||
| if (!Number.isNaN(timestamp)) { | ||
| return new Date(timestamp); | ||
| } | ||
| return null; | ||
| } | ||
| static detectForcedArrayFields(records, arraySuffix) { | ||
| if (records.length === 0 || !arraySuffix) return /* @__PURE__ */ new Set(); | ||
| const forcedFields = /* @__PURE__ */ new Set(); | ||
| const headers = Object.keys(records[0]); | ||
| for (const header of headers) { | ||
| const parts = header.split("."); | ||
| let currentPath = ""; | ||
| for (let i = 0; i < parts.length; i++) { | ||
| let part = parts[i]; | ||
| if (part.endsWith(arraySuffix)) { | ||
| part = part.slice(0, -arraySuffix.length); | ||
| currentPath = currentPath ? `${currentPath}.${part}` : part; | ||
| forcedFields.add(currentPath); | ||
| } else { | ||
| currentPath = currentPath ? `${currentPath}.${part}` : part; | ||
| } | ||
| } | ||
| } | ||
| return forcedFields; | ||
| } | ||
| static normalizeHeaders(records, arraySuffix) { | ||
| if (!arraySuffix) return records; | ||
| return records.map((record) => { | ||
| const normalized = {}; | ||
| for (const [key, value] of Object.entries(record)) { | ||
| const normalizedKey = key.split(".").map((part) => part.endsWith(arraySuffix) ? part.slice(0, -arraySuffix.length) : part).join("."); | ||
| normalized[normalizedKey] = value; | ||
| } | ||
| return normalized; | ||
| }); | ||
| } | ||
| static processGroup(rows) { | ||
@@ -193,3 +558,3 @@ const result = {}; | ||
| for (const [key, value] of Object.entries(row)) { | ||
| if (value === "" || value === void 0 || value === null) continue; | ||
| if (value === "" || value === void 0) continue; | ||
| const parts = key.split("."); | ||
@@ -214,4 +579,7 @@ let current = result; | ||
| targetValue.push(sourceValue); | ||
| } else if (typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue) && typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue)) { | ||
| const shouldCreateArray = this.shouldCreateArrayOfObjects(targetValue, sourceValue); | ||
| } else if (typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue) && !(targetValue instanceof Date) && typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue) && !(sourceValue instanceof Date)) { | ||
| const shouldCreateArray = this.shouldCreateArrayOfObjects( | ||
| targetValue, | ||
| sourceValue | ||
| ); | ||
| if (shouldCreateArray) { | ||
@@ -277,3 +645,3 @@ target[key] = [targetValue, sourceValue]; | ||
| } | ||
| } else if (value && typeof value === "object") { | ||
| } else if (value && typeof value === "object" && !(value instanceof Date)) { | ||
| checkForArrays(value, currentPath); | ||
@@ -288,3 +656,3 @@ } | ||
| } | ||
| static normalizeArrays(obj, arrayFields, path = "") { | ||
| static normalizeArrays(obj, arrayFields, forcedArrayFields, emptyArrayBehavior, path = "") { | ||
| const result = {}; | ||
@@ -296,11 +664,31 @@ for (const [key, value] of Object.entries(obj)) { | ||
| result[key] = value.map( | ||
| (item) => item && typeof item === "object" && !Array.isArray(item) ? this.normalizeArrays(item, arrayFields, currentPath) : item | ||
| (item) => item && typeof item === "object" && !Array.isArray(item) && !(item instanceof Date) ? this.normalizeArrays( | ||
| item, | ||
| arrayFields, | ||
| forcedArrayFields, | ||
| emptyArrayBehavior, | ||
| currentPath | ||
| ) : item | ||
| ); | ||
| } else if (value && typeof value === "object") { | ||
| result[key] = [this.normalizeArrays(value, arrayFields, currentPath)]; | ||
| } else if (value && typeof value === "object" && !(value instanceof Date)) { | ||
| result[key] = [ | ||
| this.normalizeArrays( | ||
| value, | ||
| arrayFields, | ||
| forcedArrayFields, | ||
| emptyArrayBehavior, | ||
| currentPath | ||
| ) | ||
| ]; | ||
| } else { | ||
| result[key] = [value]; | ||
| } | ||
| } else if (value && typeof value === "object" && !Array.isArray(value)) { | ||
| result[key] = this.normalizeArrays(value, arrayFields, currentPath); | ||
| } else if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) { | ||
| result[key] = this.normalizeArrays( | ||
| value, | ||
| arrayFields, | ||
| forcedArrayFields, | ||
| emptyArrayBehavior, | ||
| currentPath | ||
| ); | ||
| } else { | ||
@@ -310,2 +698,12 @@ result[key] = value; | ||
| } | ||
| for (const forcedPath of forcedArrayFields) { | ||
| const relativePath = path ? forcedPath.replace(`${path}.`, "") : forcedPath; | ||
| if (relativePath.includes(".")) continue; | ||
| if (forcedPath !== (path ? `${path}.${relativePath}` : relativePath)) continue; | ||
| if (!(relativePath in result)) { | ||
| if (emptyArrayBehavior === "empty-array") { | ||
| result[relativePath] = []; | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
@@ -318,6 +716,22 @@ } | ||
| /** | ||
| * Parse CSV file synchronously to nested JSON | ||
| * @param csvFilePath Path to CSV file | ||
| * @param options Parsing options | ||
| * Parse CSV file synchronously to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvFilePath - Path to the CSV file | ||
| * @param options - Parsing options | ||
| * @returns Array of nested JSON objects | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * interface Person { | ||
| * id: string; | ||
| * name: string; | ||
| * address: { city: string }; | ||
| * } | ||
| * | ||
| * const people = CsvParser.parseFileSync<Person>('people.csv'); | ||
| * console.log(people[0].address.city); | ||
| * ``` | ||
| */ | ||
@@ -329,6 +743,15 @@ static parseFileSync(csvFilePath, options = {}) { | ||
| /** | ||
| * Parse CSV file asynchronously to nested JSON | ||
| * @param csvFilePath Path to CSV file | ||
| * @param options Parsing options | ||
| * Parse CSV file asynchronously to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvFilePath - Path to the CSV file | ||
| * @param options - Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * @throws {CsvFileNotFoundError} If the file does not exist | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const people = await CsvParser.parseFile<Person>('people.csv'); | ||
| * ``` | ||
| */ | ||
@@ -340,25 +763,639 @@ static async parseFile(csvFilePath, options = {}) { | ||
| /** | ||
| * Parse CSV string content to nested JSON | ||
| * @param csvContent CSV content as string | ||
| * @param options Parsing options | ||
| * Parse CSV from readable stream to nested JSON. | ||
| * Note: This method buffers the entire stream before parsing. | ||
| * For true streaming with large files, use {@link CsvStreamParser}. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param stream - Readable stream containing CSV data | ||
| * @param options - Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const stream = fs.createReadStream('large-file.csv'); | ||
| * const result = await CsvParser.parseStream(stream); | ||
| * ``` | ||
| */ | ||
| static async parseStream(stream, options = {}) { | ||
| const csvContent = await CsvFileReader.readStream(stream, options); | ||
| return this.parseString(csvContent, options); | ||
| } | ||
| /** | ||
| * Parse CSV string content to nested JSON. | ||
| * | ||
| * @typeParam T - The expected type of each record in the result array | ||
| * @param csvContent - CSV content as string | ||
| * @param options - Parsing options | ||
| * @returns Array of nested JSON objects | ||
| * @throws {CsvValidationError} If validationMode is 'error' and validation fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const csv = `id,name,address.city | ||
| * 1,John,NYC | ||
| * 2,Jane,LA`; | ||
| * | ||
| * const result = CsvParser.parseString(csv); | ||
| * // [ | ||
| * // { id: '1', name: 'John', address: { city: 'NYC' } }, | ||
| * // { id: '2', name: 'Jane', address: { city: 'LA' } } | ||
| * // ] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With continuation rows for arrays | ||
| * const csv = `id,tags | ||
| * 1,javascript | ||
| * ,typescript | ||
| * ,nodejs`; | ||
| * | ||
| * const result = CsvParser.parseString(csv); | ||
| * // [{ id: '1', tags: ['javascript', 'typescript', 'nodejs'] }] | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // With auto-parsing | ||
| * const csv = `id,active,score | ||
| * 1,true,95.5`; | ||
| * | ||
| * const result = CsvParser.parseString(csv, { | ||
| * autoParseNumbers: true, | ||
| * autoParseBooleans: true | ||
| * }); | ||
| * // [{ id: 1, active: true, score: 95.5 }] | ||
| * ``` | ||
| */ | ||
| static parseString(csvContent, options = {}) { | ||
| const records = CsvReader.parse(csvContent, options); | ||
| return NestedJsonConverter.convert(records); | ||
| return NestedJsonConverter.convert(records, options); | ||
| } | ||
| }; | ||
| // src/csv-stream-parser.ts | ||
| import { Transform } from "stream"; | ||
| var CsvStreamParser = class extends Transform { | ||
| /** | ||
| * Parse CSV from readable stream to nested JSON | ||
| * @param stream Readable stream containing CSV data | ||
| * @param options Parsing options | ||
| * @returns Promise resolving to array of nested JSON objects | ||
| * Creates a new streaming CSV parser. | ||
| * | ||
| * @param options - Parser options | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const parser = new CsvStreamParser({ | ||
| * delimiter: ';', | ||
| * quote: '"', | ||
| * skipRows: 1, | ||
| * autoParseNumbers: true | ||
| * }); | ||
| * ``` | ||
| */ | ||
| static async parseStream(stream, options = {}) { | ||
| const csvContent = await CsvFileReader.readStream(stream, options); | ||
| return this.parseString(csvContent, options); | ||
| constructor(options = {}) { | ||
| var _a; | ||
| super({ ...options, objectMode: true }); | ||
| this.buffer = ""; | ||
| this.headers = []; | ||
| this.headersProcessed = false; | ||
| this.rowsSkipped = 0; | ||
| this.dataRowIndex = 0; | ||
| this.bomStripped = false; | ||
| this.options = options; | ||
| this.delimiter = options.delimiter || ","; | ||
| this.quote = options.quote || '"'; | ||
| this.skipRows = options.skipRows || 0; | ||
| this.stripBom = options.stripBom !== false; | ||
| this.nullSet = new Set(((_a = options.nullValues) != null ? _a : ["null", "NULL", "nil", "NIL"]).map((v) => v.toLowerCase())); | ||
| } | ||
| /** | ||
| * Transform implementation - processes incoming chunks. | ||
| * @internal | ||
| */ | ||
| _transform(chunk, _encoding, callback) { | ||
| try { | ||
| let data = typeof chunk === "string" ? chunk : chunk.toString(this.options.encoding || "utf-8"); | ||
| if (this.stripBom && !this.bomStripped) { | ||
| data = this.stripBomFromString(data); | ||
| this.bomStripped = true; | ||
| } | ||
| this.buffer += data; | ||
| this.processBuffer(); | ||
| callback(); | ||
| } catch (error) { | ||
| callback(error instanceof Error ? error : new Error(String(error))); | ||
| } | ||
| } | ||
| /** | ||
| * Flush implementation - processes any remaining data. | ||
| * @internal | ||
| */ | ||
| _flush(callback) { | ||
| try { | ||
| if (this.buffer.trim()) { | ||
| this.processLine(this.buffer); | ||
| } | ||
| callback(); | ||
| } catch (error) { | ||
| callback(error instanceof Error ? error : new Error(String(error))); | ||
| } | ||
| } | ||
| /** | ||
| * Strip BOM from the beginning of a string. | ||
| */ | ||
| stripBomFromString(content) { | ||
| if (content.length === 0) return content; | ||
| if (content.charCodeAt(0) === 65279) { | ||
| return content.slice(1); | ||
| } | ||
| if (content.charCodeAt(0) === 65534) { | ||
| return content.slice(1); | ||
| } | ||
| return content; | ||
| } | ||
| /** | ||
| * Process the buffer and extract complete lines. | ||
| */ | ||
| processBuffer() { | ||
| let insideQuotes = false; | ||
| let lineStart = 0; | ||
| for (let i = 0; i < this.buffer.length; i++) { | ||
| const char = this.buffer[i]; | ||
| const nextChar = i + 1 < this.buffer.length ? this.buffer[i + 1] : ""; | ||
| if (char === this.quote) { | ||
| insideQuotes = !insideQuotes; | ||
| } else if (!insideQuotes) { | ||
| if (char === "\r" && nextChar === "\n") { | ||
| const line = this.buffer.slice(lineStart, i); | ||
| this.processLine(line); | ||
| i++; | ||
| lineStart = i + 1; | ||
| } else if (char === "\n" || char === "\r") { | ||
| const line = this.buffer.slice(lineStart, i); | ||
| this.processLine(line); | ||
| lineStart = i + 1; | ||
| } | ||
| } | ||
| } | ||
| this.buffer = this.buffer.slice(lineStart); | ||
| } | ||
| /** | ||
| * Process a single line of CSV data. | ||
| */ | ||
| processLine(line) { | ||
| if (line.trim() === "") { | ||
| return; | ||
| } | ||
| if (this.rowsSkipped < this.skipRows) { | ||
| this.rowsSkipped++; | ||
| return; | ||
| } | ||
| const values = this.parseLine(line); | ||
| if (!this.headersProcessed) { | ||
| this.headers = values; | ||
| if (this.options.headerTransformer) { | ||
| this.headers = this.headers.map(this.options.headerTransformer); | ||
| } | ||
| if (this.options.columnMapping) { | ||
| this.headers = this.headers.map((h) => { | ||
| var _a, _b; | ||
| return (_b = (_a = this.options.columnMapping) == null ? void 0 : _a[h]) != null ? _b : h; | ||
| }); | ||
| } | ||
| this.headersProcessed = true; | ||
| return; | ||
| } | ||
| const record = this.createRecord(values); | ||
| if (this.options.rowFilter && !this.options.rowFilter(record, this.dataRowIndex)) { | ||
| this.dataRowIndex++; | ||
| return; | ||
| } | ||
| this.dataRowIndex++; | ||
| const transformed = this.applyTransformations(record); | ||
| const nested = this.options.nested !== false; | ||
| if (nested) { | ||
| const nestedRecord = this.unflatten(transformed); | ||
| this.push(nestedRecord); | ||
| } else { | ||
| this.push(transformed); | ||
| } | ||
| } | ||
| /** | ||
| * Parse a single line into values. | ||
| */ | ||
| parseLine(line) { | ||
| const values = []; | ||
| let currentValue = ""; | ||
| let insideQuotes = false; | ||
| for (let i = 0; i < line.length; i++) { | ||
| const char = line[i]; | ||
| const nextChar = i + 1 < line.length ? line[i + 1] : ""; | ||
| if (char === this.quote) { | ||
| if (insideQuotes && nextChar === this.quote) { | ||
| currentValue += this.quote; | ||
| i++; | ||
| } else { | ||
| insideQuotes = !insideQuotes; | ||
| } | ||
| } else if (char === this.delimiter && !insideQuotes) { | ||
| values.push(currentValue); | ||
| currentValue = ""; | ||
| } else { | ||
| currentValue += char; | ||
| } | ||
| } | ||
| values.push(currentValue); | ||
| return values; | ||
| } | ||
| /** | ||
| * Create a record object from values array. | ||
| */ | ||
| createRecord(values) { | ||
| var _a; | ||
| const record = {}; | ||
| for (let i = 0; i < this.headers.length; i++) { | ||
| let value = i < values.length ? values[i] : ""; | ||
| if (value === "" && ((_a = this.options.defaultValues) == null ? void 0 : _a[this.headers[i]]) !== void 0) { | ||
| value = this.options.defaultValues[this.headers[i]]; | ||
| } | ||
| record[this.headers[i]] = value; | ||
| } | ||
| return record; | ||
| } | ||
| /** | ||
| * Apply value transformations to a record. | ||
| */ | ||
| applyTransformations(record) { | ||
| const { autoParseNumbers, autoParseBooleans, autoParseDates, valueTransformer, nullValues, nullRepresentation } = this.options; | ||
| if (!autoParseNumbers && !autoParseBooleans && !autoParseDates && !valueTransformer && nullValues === void 0) { | ||
| return record; | ||
| } | ||
| const transformed = {}; | ||
| for (const [header, value] of Object.entries(record)) { | ||
| let transformedValue = value; | ||
| if (value === "") { | ||
| if (nullValues !== void 0 && this.nullSet.has("")) { | ||
| transformedValue = this.applyNullRepresentation(nullRepresentation); | ||
| if (nullRepresentation === "omit") { | ||
| continue; | ||
| } | ||
| } | ||
| transformed[header] = transformedValue; | ||
| continue; | ||
| } | ||
| if (nullValues !== void 0 && this.nullSet.has(value.toLowerCase())) { | ||
| const nullVal = this.applyNullRepresentation(nullRepresentation); | ||
| if (nullRepresentation === "omit") { | ||
| continue; | ||
| } | ||
| transformed[header] = nullVal; | ||
| continue; | ||
| } | ||
| if (autoParseNumbers) { | ||
| const parsed = this.tryParseNumber(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (autoParseBooleans && typeof transformedValue === "string") { | ||
| const parsed = this.tryParseBoolean(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (autoParseDates && typeof transformedValue === "string") { | ||
| const parsed = this.tryParseDate(value); | ||
| if (parsed !== null) { | ||
| transformedValue = parsed; | ||
| } | ||
| } | ||
| if (valueTransformer) { | ||
| transformedValue = valueTransformer(transformedValue, header); | ||
| } | ||
| transformed[header] = transformedValue; | ||
| } | ||
| return transformed; | ||
| } | ||
| /** | ||
| * Apply null representation based on option. | ||
| */ | ||
| applyNullRepresentation(representation) { | ||
| switch (representation) { | ||
| case "null": | ||
| return null; | ||
| case "undefined": | ||
| return void 0; | ||
| case "empty-string": | ||
| return ""; | ||
| case "omit": | ||
| default: | ||
| return void 0; | ||
| } | ||
| } | ||
| /** | ||
| * Try to parse a string as a number. | ||
| */ | ||
| tryParseNumber(value) { | ||
| if (value.trim() === "") return null; | ||
| if (/^0\d+$/.test(value)) return null; | ||
| const parsed = Number(value); | ||
| if (!Number.isNaN(parsed) && Number.isFinite(parsed)) { | ||
| return parsed; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Try to parse a string as a boolean. | ||
| */ | ||
| tryParseBoolean(value) { | ||
| const lower = value.toLowerCase().trim(); | ||
| if (lower === "true") return true; | ||
| if (lower === "false") return false; | ||
| return null; | ||
| } | ||
| /** | ||
| * Try to parse a string as a Date. | ||
| * Uses JavaScript's Date.parse() for recognition. | ||
| */ | ||
| tryParseDate(value) { | ||
| if (value.trim() === "" || /^-?\d+(\.\d+)?$/.test(value)) return null; | ||
| const timestamp = Date.parse(value); | ||
| if (!Number.isNaN(timestamp)) { | ||
| return new Date(timestamp); | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Unflatten a record with dot-notation keys into a nested object. | ||
| */ | ||
| unflatten(record) { | ||
| var _a; | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(record)) { | ||
| if (value === "" || value === void 0) continue; | ||
| const arraySuffix = (_a = this.options.arraySuffixIndicator) != null ? _a : "[]"; | ||
| const normalizedKey = key.split(".").map((part) => part.endsWith(arraySuffix) ? part.slice(0, -arraySuffix.length) : part).join("."); | ||
| const parts = normalizedKey.split("."); | ||
| let current = result; | ||
| for (let i = 0; i < parts.length - 1; i++) { | ||
| const part = parts[i]; | ||
| if (!current[part]) { | ||
| current[part] = {}; | ||
| } | ||
| current = current[part]; | ||
| } | ||
| current[parts[parts.length - 1]] = value; | ||
| } | ||
| return result; | ||
| } | ||
| }; | ||
| // src/json-to-csv.ts | ||
| import fs2 from "fs"; | ||
| var JsonToCsv = class { | ||
| /** | ||
| * Convert array of nested objects to CSV string. | ||
| * | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * @returns CSV string | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const csv = JsonToCsv.stringify([ | ||
| * { id: 1, name: 'Alice', address: { city: 'NYC' } } | ||
| * ]); | ||
| * // "id,name,address.city\n1,Alice,NYC" | ||
| * ``` | ||
| */ | ||
| static stringify(data, options = {}) { | ||
| if (!data || data.length === 0) { | ||
| return ""; | ||
| } | ||
| const delimiter = options.delimiter || ","; | ||
| const quote = options.quote || '"'; | ||
| const lineEnding = options.lineEnding || "\n"; | ||
| const includeHeader = options.includeHeader !== false; | ||
| const arrayMode = options.arrayMode || "rows"; | ||
| const headers = this.collectHeaders(data); | ||
| const rows = []; | ||
| if (includeHeader) { | ||
| rows.push(headers.map((h) => this.escapeValue(h, delimiter, quote)).join(delimiter)); | ||
| } | ||
| for (const obj of data) { | ||
| const flatRows = this.flattenObject(obj, headers, arrayMode); | ||
| for (const flatRow of flatRows) { | ||
| const values = headers.map((header) => { | ||
| const value = flatRow[header]; | ||
| return this.escapeValue(value, delimiter, quote); | ||
| }); | ||
| rows.push(values.join(delimiter)); | ||
| } | ||
| } | ||
| return rows.join(lineEnding); | ||
| } | ||
| /** | ||
| * Write nested objects to CSV file synchronously. | ||
| * | ||
| * @param filePath - Path to output file | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * JsonToCsv.writeFileSync('output.csv', data, { delimiter: ';' }); | ||
| * ``` | ||
| */ | ||
| static writeFileSync(filePath, data, options = {}) { | ||
| const csv = this.stringify(data, options); | ||
| const encoding = options.encoding || "utf-8"; | ||
| fs2.writeFileSync(filePath, csv, encoding); | ||
| } | ||
| /** | ||
| * Write nested objects to CSV file asynchronously. | ||
| * | ||
| * @param filePath - Path to output file | ||
| * @param data - Array of objects to convert | ||
| * @param options - Conversion options | ||
| * @returns Promise that resolves when file is written | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await JsonToCsv.writeFile('output.csv', data); | ||
| * ``` | ||
| */ | ||
| static async writeFile(filePath, data, options = {}) { | ||
| const csv = this.stringify(data, options); | ||
| const encoding = options.encoding || "utf-8"; | ||
| await fs2.promises.writeFile(filePath, csv, encoding); | ||
| } | ||
| /** | ||
| * Collect all unique headers from an array of nested objects. | ||
| * Headers are generated using dot-notation for nested properties. | ||
| */ | ||
| static collectHeaders(data) { | ||
| const headerSet = /* @__PURE__ */ new Set(); | ||
| for (const obj of data) { | ||
| this.collectHeadersFromObject(obj, "", headerSet); | ||
| } | ||
| const headers = Array.from(headerSet); | ||
| headers.sort((a, b) => { | ||
| const aDepth = a.split(".").length; | ||
| const bDepth = b.split(".").length; | ||
| if (aDepth !== bDepth) return aDepth - bDepth; | ||
| return a.localeCompare(b); | ||
| }); | ||
| return headers; | ||
| } | ||
| /** | ||
| * Recursively collect headers from a nested object. | ||
| */ | ||
| static collectHeadersFromObject(obj, prefix, headers) { | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| const fullKey = prefix ? `${prefix}.${key}` : key; | ||
| if (Array.isArray(value)) { | ||
| if (value.length > 0) { | ||
| const firstItem = value[0]; | ||
| if (firstItem && typeof firstItem === "object" && !Array.isArray(firstItem)) { | ||
| this.collectHeadersFromObject(firstItem, fullKey, headers); | ||
| } else { | ||
| headers.add(fullKey); | ||
| } | ||
| } else { | ||
| headers.add(fullKey); | ||
| } | ||
| } else if (value && typeof value === "object") { | ||
| this.collectHeadersFromObject(value, fullKey, headers); | ||
| } else { | ||
| headers.add(fullKey); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Flatten a nested object into one or more rows of flat key-value pairs. | ||
| * Arrays result in multiple rows (continuation rows). | ||
| */ | ||
| static flattenObject(obj, headers, arrayMode) { | ||
| const flatValues = {}; | ||
| const arrayValues = {}; | ||
| this.flattenToPathValues(obj, "", flatValues, arrayValues); | ||
| if (Object.keys(arrayValues).length === 0 || arrayMode === "json") { | ||
| const row = {}; | ||
| for (const header of headers) { | ||
| if (header in flatValues) { | ||
| row[header] = this.valueToString(flatValues[header]); | ||
| } else if (header in arrayValues) { | ||
| row[header] = JSON.stringify(arrayValues[header]); | ||
| } else { | ||
| row[header] = ""; | ||
| } | ||
| } | ||
| return [row]; | ||
| } | ||
| return this.generateContinuationRows(headers, flatValues, arrayValues); | ||
| } | ||
| /** | ||
| * Flatten a nested object to path-value pairs, separating arrays. | ||
| */ | ||
| static flattenToPathValues(obj, prefix, flatValues, arrayValues) { | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| const fullKey = prefix ? `${prefix}.${key}` : key; | ||
| if (Array.isArray(value)) { | ||
| if (value.length > 0 && typeof value[0] === "object" && value[0] !== null && !Array.isArray(value[0])) { | ||
| arrayValues[fullKey] = value; | ||
| } else { | ||
| arrayValues[fullKey] = value; | ||
| } | ||
| } else if (value && typeof value === "object") { | ||
| this.flattenToPathValues(value, fullKey, flatValues, arrayValues); | ||
| } else { | ||
| flatValues[fullKey] = value; | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Generate continuation rows for arrays. | ||
| * First row contains all values, subsequent rows only contain array continuations. | ||
| */ | ||
| static generateContinuationRows(headers, flatValues, arrayValues) { | ||
| let maxArrayLength = 1; | ||
| for (const arr of Object.values(arrayValues)) { | ||
| if (arr.length > maxArrayLength) { | ||
| maxArrayLength = arr.length; | ||
| } | ||
| } | ||
| const rows = []; | ||
| for (let i = 0; i < maxArrayLength; i++) { | ||
| const row = {}; | ||
| for (const header of headers) { | ||
| if (header in arrayValues) { | ||
| const arr = arrayValues[header]; | ||
| if (i < arr.length) { | ||
| const item = arr[i]; | ||
| if (item && typeof item === "object" && !Array.isArray(item)) { | ||
| const itemFlat = {}; | ||
| const itemArrays = {}; | ||
| this.flattenToPathValues(item, header, itemFlat, itemArrays); | ||
| for (const [path, val] of Object.entries(itemFlat)) { | ||
| if (headers.includes(path)) { | ||
| row[path] = this.valueToString(val); | ||
| } | ||
| } | ||
| } else { | ||
| row[header] = this.valueToString(item); | ||
| } | ||
| } else { | ||
| row[header] = ""; | ||
| } | ||
| } else if (i === 0 && header in flatValues) { | ||
| row[header] = this.valueToString(flatValues[header]); | ||
| } else if (!(header in row)) { | ||
| row[header] = ""; | ||
| } | ||
| } | ||
| rows.push(row); | ||
| } | ||
| return rows; | ||
| } | ||
| /** | ||
| * Convert a value to string for CSV output. | ||
| */ | ||
| static valueToString(value) { | ||
| if (value === null || value === void 0) { | ||
| return ""; | ||
| } | ||
| if (typeof value === "object") { | ||
| return JSON.stringify(value); | ||
| } | ||
| return String(value); | ||
| } | ||
| /** | ||
| * Escape a value for CSV output. | ||
| * Wraps in quotes if the value contains delimiter, quote, or newline. | ||
| */ | ||
| static escapeValue(value, delimiter, quote) { | ||
| if (value === void 0 || value === null) { | ||
| return ""; | ||
| } | ||
| const str = String(value); | ||
| const needsEscape = str.includes(delimiter) || str.includes(quote) || str.includes("\n") || str.includes("\r"); | ||
| if (needsEscape) { | ||
| const escaped = str.replace(new RegExp(quote, "g"), quote + quote); | ||
| return quote + escaped + quote; | ||
| } | ||
| return str; | ||
| } | ||
| }; | ||
| export { | ||
| CsvParser | ||
| CsvEncodingError, | ||
| CsvFileNotFoundError, | ||
| CsvFileReader, | ||
| CsvParseError, | ||
| CsvParser, | ||
| CsvReader, | ||
| CsvStreamParser, | ||
| CsvValidationError, | ||
| JsonToCsv, | ||
| NestedJsonConverter | ||
| }; | ||
| //# sourceMappingURL=index.mjs.map |
+1
-1
| The MIT License (MIT) | ||
| Copyright © 2025 Cerios | ||
| Copyright © 2026 Cerios | ||
@@ -4,0 +4,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
+2
-6
| { | ||
| "name": "@cerios/csv-nested-json", | ||
| "version": "1.0.0", | ||
| "version": "1.1.0", | ||
| "author": "Ronald Veth - Cerios", | ||
@@ -23,5 +23,3 @@ "description": "Parse CSV files into nested JSON objects with support for dot notation, arrays, and complex data structures", | ||
| "types": "./dist/index.d.ts", | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "files": ["dist"], | ||
| "type": "commonjs", | ||
@@ -54,4 +52,2 @@ "sideEffects": false, | ||
| "changeset": "npx changeset", | ||
| "changeset:publish": "tsup && changeset publish", | ||
| "changeset:version": "changeset version && npm i", | ||
| "check": "biome check --write", | ||
@@ -58,0 +54,0 @@ "check-exports": "attw --pack .", |
+611
-133
@@ -12,8 +12,15 @@ # @cerios/csv-nested-json | ||
| - **Multiple Input Methods** - Parse from files (sync/async), strings, or streams | ||
| - **True Streaming Parser** - Memory-efficient parsing for very large files | ||
| - **Bidirectional Conversion** - Convert CSV to JSON and JSON back to CSV | ||
| - **Value Transformations** - Auto-parse numbers, booleans, dates, or use custom transformers | ||
| - **Header Transformations** - Transform and map column names | ||
| - **Row Filtering** - Filter rows during parsing for memory efficiency | ||
| - **RFC 4180 Compliant** - Handles quoted fields, escaped quotes, and various line endings | ||
| - **Flexible Delimiters** - Support for comma, semicolon, tab, pipe, and custom delimiters | ||
| - **Custom Encodings** - Handle different file encodings (UTF-8, Latin1, etc.) | ||
| - **BOM Handling** - Automatic Byte Order Mark detection and removal | ||
| - **TypeScript & JavaScript** - Full type definitions included | ||
| - **CommonJS & ESM** - Works in both module systems | ||
| - **Validation Modes** - Flexible error handling for malformed data | ||
| - **Custom Error Classes** - Detailed error information for debugging | ||
@@ -53,6 +60,10 @@ ## 📦 Installation | ||
| |--------|-------------| | ||
| | `parseFileSync()` | Parse CSV file synchronously | | ||
| | `parseFile()` | Parse CSV file asynchronously | | ||
| | `parseString()` | Parse CSV string content | | ||
| | `parseStream()` | Parse CSV from readable stream | | ||
| | `CsvParser.parseFileSync()` | Parse CSV file synchronously | | ||
| | `CsvParser.parseFile()` | Parse CSV file asynchronously | | ||
| | `CsvParser.parseString()` | Parse CSV string content | | ||
| | `CsvParser.parseStream()` | Parse CSV from readable stream | | ||
| | `CsvStreamParser` | True streaming parser for very large files | | ||
| | `JsonToCsv.stringify()` | Convert JSON objects to CSV string | | ||
| | `JsonToCsv.writeFileSync()` | Write JSON objects to CSV file (sync) | | ||
| | `JsonToCsv.writeFile()` | Write JSON objects to CSV file (async) | | ||
@@ -107,2 +118,71 @@ ## 🔧 Basic Usage | ||
| ### 5. True Streaming Parser (Memory Efficient) | ||
| For very large files where you want to process records one at a time without loading everything into memory: | ||
| ```typescript | ||
| import { CsvStreamParser } from '@cerios/csv-nested-json'; | ||
| import { createReadStream } from 'node:fs'; | ||
| const parser = new CsvStreamParser({ | ||
| autoParseNumbers: true, | ||
| autoParseBooleans: true | ||
| }); | ||
| // Using async iteration | ||
| const stream = createReadStream('./very-large-file.csv'); | ||
| for await (const record of stream.pipe(parser)) { | ||
| console.log('Parsed record:', record); | ||
| // Process each record as it's parsed | ||
| } | ||
| // Or using events | ||
| createReadStream('./very-large-file.csv') | ||
| .pipe(new CsvStreamParser()) | ||
| .on('data', (record) => { | ||
| console.log('Record:', record); | ||
| }) | ||
| .on('end', () => { | ||
| console.log('Done!'); | ||
| }) | ||
| .on('error', (err) => { | ||
| console.error('Error:', err); | ||
| }); | ||
| ``` | ||
| **When to use:** Files too large to fit in memory, real-time processing, ETL pipelines. | ||
| ### 6. Convert JSON to CSV | ||
| ```typescript | ||
| import { JsonToCsv } from '@cerios/csv-nested-json'; | ||
| const data = [ | ||
| { | ||
| id: '1', | ||
| name: 'Alice', | ||
| address: { city: 'NYC', zip: '10001' } | ||
| }, | ||
| { | ||
| id: '2', | ||
| name: 'Bob', | ||
| address: { city: 'LA', zip: '90001' } | ||
| } | ||
| ]; | ||
| // Convert to CSV string | ||
| const csvString = JsonToCsv.stringify(data); | ||
| console.log(csvString); | ||
| // Output: | ||
| // id,name,address.city,address.zip | ||
| // 1,Alice,NYC,10001 | ||
| // 2,Bob,LA,90001 | ||
| // Write directly to file | ||
| JsonToCsv.writeFileSync('./output.csv', data); | ||
| // Or async | ||
| await JsonToCsv.writeFile('./output.csv', data); | ||
| ``` | ||
| ## 🎯 Advanced Examples | ||
@@ -227,2 +307,237 @@ | ||
| ### Forced Array Fields with `[]` Suffix | ||
| Use the `[]` suffix in headers to force a field to always be an array, even with a single value: | ||
| **Input CSV:** | ||
| ```csv | ||
| id,name,tags[] | ||
| 1,Alice,javascript | ||
| 2,Bob,python | ||
| ``` | ||
| **Code:** | ||
| ```typescript | ||
| const result = CsvParser.parseString(csvContent); | ||
| ``` | ||
| **Output JSON:** | ||
| ```json | ||
| [ | ||
| { "id": "1", "name": "Alice", "tags": ["javascript"] }, | ||
| { "id": "2", "name": "Bob", "tags": ["python"] } | ||
| ] | ||
| ``` | ||
| ### Auto-Parse Numbers and Booleans | ||
| ```typescript | ||
| const csvContent = `id,name,age,price,active,verified | ||
| 1,Alice,30,19.99,true,FALSE | ||
| 2,Bob,25,29.99,false,TRUE`; | ||
| const result = CsvParser.parseString(csvContent, { | ||
| autoParseNumbers: true, | ||
| autoParseBooleans: true | ||
| }); | ||
| // Result: | ||
| // [ | ||
| // { id: 1, name: "Alice", age: 30, price: 19.99, active: true, verified: false }, | ||
| // { id: 2, name: "Bob", age: 25, price: 29.99, active: false, verified: true } | ||
| // ] | ||
| ``` | ||
| **Note:** Strings with leading zeros (like `"007"`) are preserved as strings to avoid data loss. | ||
| ### Auto-Parse Dates | ||
| ```typescript | ||
| const csvContent = `id,name,createdAt,updatedAt | ||
| 1,Alice,2024-01-15,2024-06-30T10:30:00Z`; | ||
| const result = CsvParser.parseString(csvContent, { | ||
| autoParseDates: true | ||
| }); | ||
| // Result: | ||
| // [ | ||
| // { | ||
| // id: "1", | ||
| // name: "Alice", | ||
| // createdAt: Date("2024-01-15"), | ||
| // updatedAt: Date("2024-06-30T10:30:00Z") | ||
| // } | ||
| // ] | ||
| ``` | ||
| ### Custom Value Transformer | ||
| ```typescript | ||
| const csvContent = `id,name,email | ||
| 1,alice,alice@example.com | ||
| 2,bob,bob@example.com`; | ||
| const result = CsvParser.parseString(csvContent, { | ||
| valueTransformer: (value, header) => { | ||
| // Uppercase names | ||
| if (header === 'name' && typeof value === 'string') { | ||
| return value.toUpperCase(); | ||
| } | ||
| return value; | ||
| } | ||
| }); | ||
| // Result: | ||
| // [ | ||
| // { id: "1", name: "ALICE", email: "alice@example.com" }, | ||
| // { id: "2", name: "BOB", email: "bob@example.com" } | ||
| // ] | ||
| ``` | ||
| ### Header Transformation | ||
| ```typescript | ||
| const csvContent = `User ID,First Name,Last Name,Email Address | ||
| 1,John,Doe,john@example.com`; | ||
| const result = CsvParser.parseString(csvContent, { | ||
| // Convert headers to camelCase | ||
| headerTransformer: (header) => { | ||
| return header | ||
| .toLowerCase() | ||
| .replace(/\s+(.)/g, (_, c) => c.toUpperCase()); | ||
| } | ||
| }); | ||
| // Result: | ||
| // [{ userId: "1", firstName: "John", lastName: "Doe", emailAddress: "john@example.com" }] | ||
| ``` | ||
| ### Column Mapping | ||
| ```typescript | ||
| const csvContent = `user_id,first_name,last_name | ||
| 1,John,Doe`; | ||
| const result = CsvParser.parseString(csvContent, { | ||
| columnMapping: { | ||
| 'user_id': 'id', | ||
| 'first_name': 'firstName', | ||
| 'last_name': 'lastName' | ||
| } | ||
| }); | ||
| // Result: | ||
| // [{ id: "1", firstName: "John", lastName: "Doe" }] | ||
| ``` | ||
| ### Row Filtering | ||
| Filter rows during parsing for better memory efficiency: | ||
| ```typescript | ||
| const csvContent = `id,name,status | ||
| 1,Alice,active | ||
| 2,Bob,deleted | ||
| 3,Charlie,active | ||
| 4,Diana,pending`; | ||
| const result = CsvParser.parseString(csvContent, { | ||
| rowFilter: (record, rowIndex) => { | ||
| // Only include active records | ||
| return record.status === 'active'; | ||
| } | ||
| }); | ||
| // Result: | ||
| // [ | ||
| // { id: "1", name: "Alice", status: "active" }, | ||
| // { id: "3", name: "Charlie", status: "active" } | ||
| // ] | ||
| ``` | ||
| ### Skip Rows (Metadata Headers) | ||
| ```typescript | ||
| const csvContent = `Report generated on 2024-01-15 | ||
| Source: Production Database | ||
| id,name,email | ||
| 1,Alice,alice@example.com | ||
| 2,Bob,bob@example.com`; | ||
| const result = CsvParser.parseString(csvContent, { | ||
| skipRows: 2 // Skip the first 2 metadata rows | ||
| }); | ||
| // Result: | ||
| // [ | ||
| // { id: "1", name: "Alice", email: "alice@example.com" }, | ||
| // { id: "2", name: "Bob", email: "bob@example.com" } | ||
| // ] | ||
| ``` | ||
| ### Default Values | ||
| ```typescript | ||
| const csvContent = `id,name,status,country | ||
| 1,Alice,, | ||
| 2,Bob,active,USA`; | ||
| const result = CsvParser.parseString(csvContent, { | ||
| defaultValues: { | ||
| status: 'pending', | ||
| country: 'Unknown' | ||
| } | ||
| }); | ||
| // Result: | ||
| // [ | ||
| // { id: "1", name: "Alice", status: "pending", country: "Unknown" }, | ||
| // { id: "2", name: "Bob", status: "active", country: "USA" } | ||
| // ] | ||
| ``` | ||
| ### Null Value Handling | ||
| ```typescript | ||
| const csvContent = `id,name,nickname | ||
| 1,Alice,N/A | ||
| 2,Bob,null | ||
| 3,Charlie,Bobby`; | ||
| const result = CsvParser.parseString(csvContent, { | ||
| nullValues: ['null', 'NULL', 'N/A', 'n/a', ''], | ||
| nullRepresentation: 'null' // or 'undefined', 'empty-string', 'omit' | ||
| }); | ||
| // Result with nullRepresentation: 'null': | ||
| // [ | ||
| // { id: "1", name: "Alice", nickname: null }, | ||
| // { id: "2", name: "Bob", nickname: null }, | ||
| // { id: "3", name: "Charlie", nickname: "Bobby" } | ||
| // ] | ||
| // Result with nullRepresentation: 'omit' (default): | ||
| // [ | ||
| // { id: "1", name: "Alice" }, | ||
| // { id: "2", name: "Bob" }, | ||
| // { id: "3", name: "Charlie", nickname: "Bobby" } | ||
| // ] | ||
| ``` | ||
| ### BOM Handling | ||
| The parser automatically strips UTF-8 and UTF-16 BOM by default: | ||
| ```typescript | ||
| // BOM is automatically handled | ||
| const result = CsvParser.parseFileSync('./windows-excel-export.csv'); | ||
| // Disable BOM stripping if needed | ||
| const result2 = CsvParser.parseString(csvContent, { | ||
| stripBom: false | ||
| }); | ||
| ``` | ||
| ### Complex Multi-Group Example | ||
@@ -442,2 +757,28 @@ | ||
| encoding?: BufferEncoding; // Default: 'utf-8' | ||
| // Row handling | ||
| skipRows?: number; // Default: 0 | ||
| stripBom?: boolean; // Default: true | ||
| rowFilter?: (record, rowIndex) => boolean; // Filter rows during parsing | ||
| // Value transformations | ||
| autoParseNumbers?: boolean; // Default: false | ||
| autoParseBooleans?: boolean; // Default: false | ||
| autoParseDates?: boolean; // Default: false | ||
| valueTransformer?: (value, header) => any; // Custom value transformer | ||
| // Header transformations | ||
| headerTransformer?: (header) => string; // Transform header names | ||
| columnMapping?: Record<string, string>; // Rename columns | ||
| // Array handling | ||
| arraySuffixIndicator?: string; // Default: '[]' | ||
| emptyArrayBehavior?: 'empty-array' | 'omit'; // Default: 'omit' | ||
| // Null handling | ||
| nullValues?: string[]; // Values to treat as null | ||
| nullRepresentation?: 'null' | 'undefined' | 'empty-string' | 'omit'; // Default: 'omit' | ||
| // Default values | ||
| defaultValues?: Record<string, string>; // Default values for empty cells | ||
| } | ||
@@ -453,11 +794,4 @@ ``` | ||
| - `'warn'` (default): Log a warning to console | ||
| - `'error'`: Throw an error | ||
| - `'error'`: Throw a `CsvValidationError` | ||
| **Example:** | ||
| ```typescript | ||
| const result = CsvParser.parseFile('data.csv', { | ||
| validationMode: 'error' | ||
| }); | ||
| ``` | ||
| #### `delimiter` | ||
@@ -470,11 +804,3 @@ | ||
| - `'|'` - Pipe-separated values | ||
| - Any custom single character | ||
| **Example:** | ||
| ```typescript | ||
| const result = CsvParser.parseString(csvData, { | ||
| delimiter: ';' | ||
| }); | ||
| ``` | ||
| #### `quote` | ||
@@ -485,14 +811,6 @@ | ||
| - `"'"` - Single quotes | ||
| - Any custom single character | ||
| **Example:** | ||
| ```typescript | ||
| const result = CsvParser.parseString(csvData, { | ||
| quote: "'" | ||
| }); | ||
| ``` | ||
| #### `encoding` | ||
| File encoding when reading from files or streams. Supported encodings: | ||
| File encoding when reading from files or streams: | ||
| - `'utf-8'` (default) | ||
@@ -502,11 +820,88 @@ - `'utf-16le'` | ||
| - `'ascii'` | ||
| - And all other Node.js supported encodings | ||
| **Example:** | ||
| #### `skipRows` | ||
| Number of rows to skip before the header row. Useful for files with metadata at the top. | ||
| #### `stripBom` | ||
| Automatically remove BOM (Byte Order Mark) from the beginning of content. Default: `true` | ||
| #### `autoParseNumbers` | ||
| Automatically convert numeric strings to numbers. Strings with leading zeros (like `"007"`) are preserved. | ||
| #### `autoParseBooleans` | ||
| Automatically convert `'true'`/`'false'` strings to booleans (case-insensitive). | ||
| #### `autoParseDates` | ||
| Automatically convert date strings to JavaScript Date objects using `Date.parse()`. | ||
| #### `valueTransformer` | ||
| Custom function to transform values after parsing. Called after auto-parse options. | ||
| ```typescript | ||
| const result = await CsvParser.parseFile('data.csv', { | ||
| encoding: 'latin1' | ||
| }); | ||
| valueTransformer: (value, header) => { | ||
| if (header === 'email') return value.toLowerCase(); | ||
| return value; | ||
| } | ||
| ``` | ||
| #### `headerTransformer` | ||
| Transform header names before processing. Useful for converting to camelCase, lowercase, etc. | ||
| ```typescript | ||
| headerTransformer: (header) => header.toLowerCase().replace(/\s+/g, '_') | ||
| ``` | ||
| #### `columnMapping` | ||
| Map/rename column headers. Applied after `headerTransformer`. | ||
| ```typescript | ||
| columnMapping: { 'user_id': 'id', 'first_name': 'firstName' } | ||
| ``` | ||
| #### `rowFilter` | ||
| Filter rows during parsing. More memory-efficient than filtering after parsing. | ||
| ```typescript | ||
| rowFilter: (record, rowIndex) => record.status === 'active' | ||
| ``` | ||
| #### `arraySuffixIndicator` | ||
| Suffix in headers to force array type. Default: `'[]'` | ||
| #### `emptyArrayBehavior` | ||
| How to handle forced array fields with no values: | ||
| - `'omit'` (default): Don't include the field | ||
| - `'empty-array'`: Include as `[]` | ||
| #### `nullValues` | ||
| Strings to interpret as null values. Default: `['null', 'NULL', 'nil', 'NIL']` | ||
| #### `nullRepresentation` | ||
| How to represent null values in output: | ||
| - `'omit'` (default): Remove the field | ||
| - `'null'`: Use JavaScript `null` | ||
| - `'undefined'`: Use JavaScript `undefined` | ||
| - `'empty-string'`: Use empty string `''` | ||
| #### `defaultValues` | ||
| Default values for columns when cells are empty. | ||
| ```typescript | ||
| defaultValues: { status: 'pending', country: 'Unknown' } | ||
| ``` | ||
| ### Complete Example with All Options | ||
@@ -516,6 +911,38 @@ | ||
| const result = await CsvParser.parseFile('./data.csv', { | ||
| validationMode: 'error', // Strict validation | ||
| delimiter: ',', // Comma-separated | ||
| quote: '"', // Double quotes for escaping | ||
| encoding: 'utf-8' // UTF-8 encoding | ||
| // Validation | ||
| validationMode: 'error', | ||
| // Parsing | ||
| delimiter: ',', | ||
| quote: '"', | ||
| encoding: 'utf-8', | ||
| // Row handling | ||
| skipRows: 2, | ||
| stripBom: true, | ||
| rowFilter: (record) => record.status !== 'deleted', | ||
| // Value transformations | ||
| autoParseNumbers: true, | ||
| autoParseBooleans: true, | ||
| autoParseDates: true, | ||
| valueTransformer: (value, header) => { | ||
| if (header === 'email') return value.toLowerCase(); | ||
| return value; | ||
| }, | ||
| // Header transformations | ||
| headerTransformer: (h) => h.toLowerCase().replace(/\s+/g, '_'), | ||
| columnMapping: { 'user_id': 'id' }, | ||
| // Array handling | ||
| arraySuffixIndicator: '[]', | ||
| emptyArrayBehavior: 'empty-array', | ||
| // Null handling | ||
| nullValues: ['null', 'N/A', '-'], | ||
| nullRepresentation: 'null', | ||
| // Defaults | ||
| defaultValues: { status: 'pending' } | ||
| }); | ||
@@ -528,93 +955,93 @@ ``` | ||
| #### `parseFileSync(filePath: string, options?: CsvParserOptions): any[]` | ||
| #### `parseFileSync<T>(filePath: string, options?: CsvParserOptions): T[]` | ||
| Parses a CSV file synchronously and returns an array of nested JSON objects. | ||
| **Parameters:** | ||
| - `filePath` (string): Path to the CSV file | ||
| - `options` (CsvParserOptions, optional): Configuration options | ||
| #### `parseFile<T>(filePath: string, options?: CsvParserOptions): Promise<T[]>` | ||
| **Returns:** | ||
| - `any[]`: Array of parsed objects with nested structures | ||
| Parses a CSV file asynchronously. | ||
| **Throws:** | ||
| - Error if the file does not exist | ||
| - Error if `validationMode` is `'error'` and a row has validation issues | ||
| #### `parseString<T>(csvContent: string, options?: CsvParserOptions): T[]` | ||
| **Example:** | ||
| ```typescript | ||
| const result = CsvParser.parseFileSync('./data.csv', { | ||
| validationMode: 'warn', | ||
| delimiter: ',' | ||
| }); | ||
| ``` | ||
| Parses CSV string content. | ||
| #### `parseFile(filePath: string, options?: CsvParserOptions): Promise<any[]>` | ||
| #### `parseStream<T>(stream: Readable, options?: CsvParserOptions): Promise<T[]>` | ||
| Parses a CSV file asynchronously and returns a promise that resolves to an array of nested JSON objects. | ||
| Parses CSV from a readable stream. | ||
| **Parameters:** | ||
| - `filePath` (string): Path to the CSV file | ||
| - `options` (CsvParserOptions, optional): Configuration options | ||
| ### CsvStreamParser Class | ||
| **Returns:** | ||
| - `Promise<any[]>`: Promise resolving to array of parsed objects | ||
| A Transform stream that parses CSV data chunk by chunk, emitting records as they become available. | ||
| **Throws:** | ||
| - Error if the file does not exist | ||
| - Error if `validationMode` is `'error'` and a row has validation issues | ||
| ```typescript | ||
| import { CsvStreamParser } from '@cerios/csv-nested-json'; | ||
| **Example:** | ||
| ```typescript | ||
| const result = await CsvParser.parseFile('./data.csv', { | ||
| encoding: 'utf-8' | ||
| const parser = new CsvStreamParser({ | ||
| nested: true, // Emit nested objects (default: true) | ||
| autoParseNumbers: true, | ||
| // ... other CsvParserOptions | ||
| }); | ||
| createReadStream('./large.csv') | ||
| .pipe(parser) | ||
| .on('data', (record) => console.log(record)) | ||
| .on('end', () => console.log('Done')); | ||
| ``` | ||
| #### `parseString(csvContent: string, options?: CsvParserOptions): any[]` | ||
| ### JsonToCsv Class | ||
| Parses CSV string content and returns an array of nested JSON objects. | ||
| #### `stringify(data: object[], options?: JsonToCsvOptions): string` | ||
| **Parameters:** | ||
| - `csvContent` (string): CSV content as string | ||
| - `options` (CsvParserOptions, optional): Configuration options | ||
| Convert array of objects to CSV string. | ||
| **Returns:** | ||
| - `any[]`: Array of parsed objects with nested structures | ||
| #### `writeFileSync(filePath: string, data: object[], options?: JsonToCsvOptions): void` | ||
| **Throws:** | ||
| - Error if `validationMode` is `'error'` and a row has validation issues | ||
| Write objects to CSV file synchronously. | ||
| **Example:** | ||
| ```typescript | ||
| const csvString = `id,name | ||
| 1,Alice | ||
| 2,Bob`; | ||
| #### `writeFile(filePath: string, data: object[], options?: JsonToCsvOptions): Promise<void>` | ||
| const result = CsvParser.parseString(csvString); | ||
| ``` | ||
| Write objects to CSV file asynchronously. | ||
| #### `parseStream(stream: Readable, options?: CsvParserOptions): Promise<any[]>` | ||
| ```typescript | ||
| import { JsonToCsv } from '@cerios/csv-nested-json'; | ||
| Parses CSV from a readable stream and returns a promise that resolves to an array of nested JSON objects. | ||
| const data = [ | ||
| { id: 1, user: { name: 'Alice', age: 30 }, tags: ['js', 'ts'] } | ||
| ]; | ||
| **Parameters:** | ||
| - `stream` (Readable): Node.js readable stream containing CSV data | ||
| - `options` (CsvParserOptions, optional): Configuration options | ||
| const csv = JsonToCsv.stringify(data, { | ||
| delimiter: ',', | ||
| quote: '"', | ||
| arrayMode: 'rows' // 'rows' (continuation rows) or 'json' (stringify arrays) | ||
| }); | ||
| ``` | ||
| **Returns:** | ||
| - `Promise<any[]>`: Promise resolving to array of parsed objects | ||
| ### Error Classes | ||
| **Throws:** | ||
| - Error if stream reading fails | ||
| - Error if `validationMode` is `'error'` and a row has validation issues | ||
| The library provides custom error classes for better error handling: | ||
| **Example:** | ||
| ```typescript | ||
| import { createReadStream } from 'node:fs'; | ||
| import { | ||
| CsvParseError, | ||
| CsvFileNotFoundError, | ||
| CsvValidationError, | ||
| CsvEncodingError | ||
| } from '@cerios/csv-nested-json'; | ||
| const stream = createReadStream('./data.csv'); | ||
| const result = await CsvParser.parseStream(stream, { | ||
| validationMode: 'ignore' | ||
| }); | ||
| try { | ||
| const result = CsvParser.parseFileSync('./data.csv', { | ||
| validationMode: 'error' | ||
| }); | ||
| } catch (error) { | ||
| if (error instanceof CsvFileNotFoundError) { | ||
| console.error(`File not found: ${error.filePath}`); | ||
| } else if (error instanceof CsvValidationError) { | ||
| console.error(`Validation error at row ${error.row}`); | ||
| console.error(`Expected ${error.expectedColumns}, got ${error.actualColumns}`); | ||
| } else if (error instanceof CsvEncodingError) { | ||
| console.error(`Encoding error: ${error.encoding}`); | ||
| } else if (error instanceof CsvParseError) { | ||
| console.error(`Parse error at row ${error.row}, column ${error.column}`); | ||
| } | ||
| } | ||
| ``` | ||
@@ -710,2 +1137,3 @@ | ||
| | `parseStream()` | Large files, memory efficiency | >100MB | No | | ||
| | `CsvStreamParser` | Very large files, ETL pipelines | Any size | No | | ||
@@ -758,2 +1186,3 @@ ### Traditional CSV Parsing | ||
| - ✅ **Various Line Endings:** Windows (CRLF), Unix (LF), Mac (CR) | ||
| - ✅ **BOM Handling:** UTF-8 and UTF-16 BOM automatically stripped | ||
| - ✅ **Empty Lines:** Automatically skipped | ||
@@ -782,32 +1211,51 @@ - ✅ **Flexible Column Counts:** Continuation rows can have different column counts | ||
| ```typescript | ||
| import { CsvParser, CsvParserOptions, ValidationMode } from '@cerios/csv-nested-json'; | ||
| import { | ||
| CsvParser, | ||
| CsvStreamParser, | ||
| JsonToCsv, | ||
| CsvParserOptions, | ||
| CsvParseError, | ||
| CsvValidationError, | ||
| NestedObject | ||
| } from '@cerios/csv-nested-json'; | ||
| const options: CsvParserOptions = { | ||
| validationMode: 'warn', | ||
| delimiter: ',', | ||
| quote: '"', | ||
| encoding: 'utf-8' | ||
| }; | ||
| // Generic type support | ||
| interface Person { | ||
| id: number; | ||
| name: string; | ||
| address: { | ||
| city: string; | ||
| zip: string; | ||
| }; | ||
| } | ||
| const result: any[] = CsvParser.parseFileSync('./data.csv', options); | ||
| const result = CsvParser.parseFileSync<Person>('people.csv', { | ||
| autoParseNumbers: true | ||
| }); | ||
| // result is typed as Person[] | ||
| console.log(result[0].address.city); | ||
| ``` | ||
| ### Type Definitions | ||
| ### Exported Types | ||
| ```typescript | ||
| // Options | ||
| type ValidationMode = 'ignore' | 'warn' | 'error'; | ||
| type EmptyArrayBehavior = 'empty-array' | 'omit'; | ||
| type NullRepresentation = 'null' | 'undefined' | 'empty-string' | 'omit'; | ||
| type ArrayMode = 'rows' | 'json'; | ||
| interface CsvParserOptions { | ||
| validationMode?: ValidationMode; | ||
| delimiter?: string; | ||
| quote?: string; | ||
| encoding?: BufferEncoding; | ||
| } | ||
| // Function types | ||
| type ValueTransformer = (value: unknown, header: string) => unknown; | ||
| type HeaderTransformer = (header: string) => string; | ||
| type RowFilter = (record: CsvRecord, rowIndex: number) => boolean; | ||
| abstract class CsvParser { | ||
| static parseFileSync(filePath: string, options?: CsvParserOptions): any[]; | ||
| static parseFile(filePath: string, options?: CsvParserOptions): Promise<any[]>; | ||
| static parseString(csvContent: string, options?: CsvParserOptions): any[]; | ||
| static parseStream(stream: Readable, options?: CsvParserOptions): Promise<any[]>; | ||
| } | ||
| // Data types | ||
| type CsvRecord = Record<string, string>; | ||
| type NestedObject = { [key: string]: NestedValue }; | ||
| type NestedValue = string | number | boolean | Date | null | NestedObject | NestedValue[]; | ||
| // Options interface | ||
| interface CsvParserOptions { /* ... */ } | ||
| ``` | ||
@@ -821,3 +1269,4 @@ | ||
| - Use `parseString()` for API responses and testing | ||
| - Use `parseStream()` for very large files | ||
| - Use `parseStream()` for large files | ||
| - Use `CsvStreamParser` for very large files or when you need to process records one at a time | ||
@@ -829,4 +1278,15 @@ 2. **Use Appropriate Validation Mode:** | ||
| 3. **Handle Errors Gracefully:** | ||
| 3. **Enable Auto-Parsing When Appropriate:** | ||
| ```typescript | ||
| const result = CsvParser.parseFileSync('./data.csv', { | ||
| autoParseNumbers: true, | ||
| autoParseBooleans: true, | ||
| autoParseDates: true | ||
| }); | ||
| ``` | ||
| 4. **Handle Errors Gracefully:** | ||
| ```typescript | ||
| import { CsvParseError, CsvValidationError } from '@cerios/csv-nested-json'; | ||
| try { | ||
@@ -837,18 +1297,36 @@ const result = CsvParser.parseFileSync('./data.csv', { | ||
| } catch (error) { | ||
| console.error('Failed to parse CSV:', error.message); | ||
| if (error instanceof CsvValidationError) { | ||
| console.error(`Row ${error.row}: expected ${error.expectedColumns} columns`); | ||
| } else { | ||
| throw error; | ||
| } | ||
| } | ||
| ``` | ||
| 4. **Use Streams for Large Files:** | ||
| 5. **Use Streaming for Large Files:** | ||
| ```typescript | ||
| // ✅ Good for large files | ||
| const stream = createReadStream('./large.csv'); | ||
| const result = await CsvParser.parseStream(stream); | ||
| // ✅ Good for very large files | ||
| const parser = new CsvStreamParser({ autoParseNumbers: true }); | ||
| for await (const record of createReadStream('./huge.csv').pipe(parser)) { | ||
| await processRecord(record); | ||
| } | ||
| // ❌ May cause memory issues | ||
| const result = CsvParser.parseFileSync('./large.csv'); | ||
| // ❌ May cause memory issues with large files | ||
| const result = CsvParser.parseFileSync('./huge.csv'); | ||
| ``` | ||
| 5. **Specify Encoding for Non-UTF8 Files:** | ||
| 6. **Use Row Filtering for Memory Efficiency:** | ||
| ```typescript | ||
| // ✅ Filter during parsing - uses less memory | ||
| const result = CsvParser.parseFileSync('./data.csv', { | ||
| rowFilter: (record) => record.status === 'active' | ||
| }); | ||
| // ❌ Filter after parsing - loads everything into memory first | ||
| const all = CsvParser.parseFileSync('./data.csv'); | ||
| const filtered = all.filter(r => r.status === 'active'); | ||
| ``` | ||
| 7. **Specify Encoding for Non-UTF8 Files:** | ||
| ```typescript | ||
| const result = await CsvParser.parseFile('./data.csv', { | ||
@@ -859,6 +1337,6 @@ encoding: 'latin1' | ||
| 6. **Use Consistent Column Headers:** | ||
| 8. **Use Consistent Column Headers:** | ||
| - Ensure the first column is always the identifier for grouping | ||
| - Use consistent dot notation for nested structures | ||
| - Keep header names descriptive and lowercase | ||
| - Keep header names descriptive and use `headerTransformer` for normalization | ||
@@ -865,0 +1343,0 @@ ## 🤝 Contributing |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
374548
276.57%3834
393.44%1338
55.58%4
100%1
Infinity%