jtcsv 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts ADDED
@@ -0,0 +1,348 @@
1
+ declare module 'jtcsv' {
2
+ import { Readable, Writable, Transform } from 'stream';
3
+
4
+ // JSON to CSV interfaces
5
+ export interface JsonToCsvOptions {
6
+ /** CSV delimiter (default: ';') */
7
+ delimiter?: string;
8
+ /** Include headers row (default: true) */
9
+ includeHeaders?: boolean;
10
+ /** Rename column headers { oldKey: newKey } */
11
+ renameMap?: Record<string, string>;
12
+ /** Template for guaranteed column order */
13
+ template?: Record<string, any>;
14
+ /** Maximum number of records to process (default: 1,000,000) */
15
+ maxRecords?: number;
16
+ /** Prevent CSV injection attacks by escaping formulas (default: true) */
17
+ preventCsvInjection?: boolean;
18
+ /** Ensure RFC 4180 compliance (proper quoting, line endings) (default: true) */
19
+ rfc4180Compliant?: boolean;
20
+ }
21
+
22
+ export interface SaveAsCsvOptions extends JsonToCsvOptions {
23
+ /** Validate file path security (default: true) */
24
+ validatePath?: boolean;
25
+ }
26
+
27
+ // CSV to JSON interfaces
28
+ export interface CsvToJsonOptions {
29
+ /** CSV delimiter (default: ';') */
30
+ delimiter?: string;
31
+ /** Whether CSV has headers row (default: true) */
32
+ hasHeaders?: boolean;
33
+ /** Map for renaming column headers { newKey: oldKey } */
34
+ renameMap?: Record<string, string>;
35
+ /** Trim whitespace from values (default: true) */
36
+ trim?: boolean;
37
+ /** Parse numeric values (default: false) */
38
+ parseNumbers?: boolean;
39
+ /** Parse boolean values (default: false) */
40
+ parseBooleans?: boolean;
41
+ /** Maximum number of rows to process (default: 1,000,000) */
42
+ maxRows?: number;
43
+ }
44
+
45
+ // JSON save interfaces
46
+ export interface SaveAsJsonOptions {
47
+ /** Format JSON with indentation (default: false) */
48
+ prettyPrint?: boolean;
49
+ /** Maximum file size in bytes (default: 10MB = 10485760) */
50
+ maxSize?: number;
51
+ }
52
+
53
+ // Streaming interfaces
54
+ export interface JsonToCsvStreamOptions extends JsonToCsvOptions {
55
+ /** Custom transform function for each row */
56
+ transform?: (row: Record<string, any>) => Record<string, any>;
57
+ /** JSON schema for validation and formatting */
58
+ schema?: Record<string, any>;
59
+ /** Add UTF-8 BOM for Excel compatibility (default: true) */
60
+ addBOM?: boolean;
61
+ }
62
+
63
+ export interface CsvToJsonStreamOptions extends CsvToJsonOptions {
64
+ /** Custom transform function for each row */
65
+ transform?: (row: Record<string, any>) => Record<string, any>;
66
+ /** JSON schema for validation and formatting */
67
+ schema?: Record<string, any>;
68
+ }
69
+
70
+ // Error classes
71
+ export class JtcsvError extends Error {
72
+ code: string;
73
+ constructor(message: string, code?: string);
74
+ }
75
+
76
+ export class ValidationError extends JtcsvError {
77
+ constructor(message: string);
78
+ }
79
+
80
+ export class SecurityError extends JtcsvError {
81
+ constructor(message: string);
82
+ }
83
+
84
+ export class FileSystemError extends JtcsvError {
85
+ originalError?: Error;
86
+ constructor(message: string, originalError?: Error);
87
+ }
88
+
89
+ export class ParsingError extends JtcsvError {
90
+ lineNumber?: number;
91
+ column?: number;
92
+ constructor(message: string, lineNumber?: number, column?: number);
93
+ }
94
+
95
+ export class LimitError extends JtcsvError {
96
+ limit: number;
97
+ actual: number;
98
+ constructor(message: string, limit: number, actual: number);
99
+ }
100
+
101
+ export class ConfigurationError extends JtcsvError {
102
+ constructor(message: string);
103
+ }
104
+
105
+ // Utility functions
106
+ export function createErrorMessage(type: string, details: string): string;
107
+ export function handleError(error: Error, context?: Record<string, any>): void;
108
+ export function safeExecute<T>(
109
+ fn: () => Promise<T>,
110
+ errorType: string,
111
+ context?: Record<string, any>
112
+ ): Promise<T>;
113
+ export function safeExecute<T>(
114
+ fn: () => T,
115
+ errorType: string,
116
+ context?: Record<string, any>
117
+ ): T;
118
+
119
+ /**
120
+ * Convert JSON array to CSV string
121
+ * @param data Array of objects to convert
122
+ * @param options Conversion options
123
+ * @returns CSV string
124
+ * @throws {ValidationError} If data is not an array
125
+ * @throws {LimitError} If record limit exceeded
126
+ * @throws {ConfigurationError} If options are invalid
127
+ */
128
+ export function jsonToCsv<T extends Record<string, any>>(
129
+ data: T[],
130
+ options?: JsonToCsvOptions
131
+ ): string;
132
+
133
+ /**
134
+ * Preprocess data by unwrapping nested objects and arrays
135
+ * @param data Input data array
136
+ * @returns Processed data with flattened structure
137
+ */
138
+ export function preprocessData<T extends Record<string, any>>(
139
+ data: T[]
140
+ ): Record<string, any>[];
141
+
142
+ /**
143
+ * Save JSON data as CSV file with security validation
144
+ * @param data Array of objects to convert
145
+ * @param filePath Output file path (must end with .csv)
146
+ * @param options Conversion and security options
147
+ * @returns Promise that resolves when file is saved
148
+ * @throws {ValidationError} If file path is invalid
149
+ * @throws {SecurityError} If directory traversal detected
150
+ * @throws {FileSystemError} If file system operation fails
151
+ */
152
+ export function saveAsCsv<T extends Record<string, any>>(
153
+ data: T[],
154
+ filePath: string,
155
+ options?: SaveAsCsvOptions
156
+ ): Promise<void>;
157
+
158
+ /**
159
+ * Deeply unwrap values (internal utility)
160
+ * @param value Value to unwrap
161
+ * @param depth Current depth
162
+ * @param maxDepth Maximum depth (default: 10)
163
+ * @returns Unwrapped value
164
+ */
165
+ export function deepUnwrap(
166
+ value: any,
167
+ depth?: number,
168
+ maxDepth?: number
169
+ ): any;
170
+
171
+ /**
172
+ * Convert CSV string to JSON array
173
+ * @param csv CSV string to convert
174
+ * @param options Conversion options
175
+ * @returns JSON array
176
+ * @throws {ValidationError} If input is not a string
177
+ * @throws {ParsingError} If CSV parsing fails
178
+ * @throws {LimitError} If row limit exceeded
179
+ */
180
+ export function csvToJson(
181
+ csv: string,
182
+ options?: CsvToJsonOptions
183
+ ): Record<string, any>[];
184
+
185
+ /**
186
+ * Read CSV file and convert it to JSON array
187
+ * @param filePath Path to CSV file
188
+ * @param options Conversion options
189
+ * @returns Promise that resolves to JSON array
190
+ * @throws {ValidationError} If file path is invalid
191
+ * @throws {SecurityError} If directory traversal detected
192
+ * @throws {FileSystemError} If file not found or unreadable
193
+ */
194
+ export function readCsvAsJson(
195
+ filePath: string,
196
+ options?: CsvToJsonOptions
197
+ ): Promise<Record<string, any>[]>;
198
+
199
+ /**
200
+ * Synchronously read CSV file and convert it to JSON array
201
+ * @param filePath Path to CSV file
202
+ * @param options Conversion options
203
+ * @returns JSON array
204
+ * @throws {ValidationError} If file path is invalid
205
+ * @throws {SecurityError} If directory traversal detected
206
+ * @throws {FileSystemError} If file not found or unreadable
207
+ */
208
+ export function readCsvAsJsonSync(
209
+ filePath: string,
210
+ options?: CsvToJsonOptions
211
+ ): Record<string, any>[];
212
+
213
+ /**
214
+ * Save data as JSON file with security validation
215
+ * @param data Data to save as JSON
216
+ * @param filePath Output file path (must end with .json)
217
+ * @param options Save options
218
+ * @returns Promise that resolves when file is saved
219
+ * @throws {ValidationError} If file path is invalid or data cannot be stringified
220
+ * @throws {SecurityError} If directory traversal detected
221
+ * @throws {FileSystemError} If file system operation fails
222
+ * @throws {LimitError} If file size exceeds limit
223
+ */
224
+ export function saveAsJson(
225
+ data: any,
226
+ filePath: string,
227
+ options?: SaveAsJsonOptions
228
+ ): Promise<void>;
229
+
230
+ /**
231
+ * Synchronously save data as JSON file with security validation
232
+ * @param data Data to save as JSON
233
+ * @param filePath Output file path (must end with .json)
234
+ * @param options Save options
235
+ * @returns Path to saved file
236
+ * @throws {ValidationError} If file path is invalid or data cannot be stringified
237
+ * @throws {SecurityError} If directory traversal detected
238
+ * @throws {FileSystemError} If file system operation fails
239
+ * @throws {LimitError} If file size exceeds limit
240
+ */
241
+ export function saveAsJsonSync(
242
+ data: any,
243
+ filePath: string,
244
+ options?: SaveAsJsonOptions
245
+ ): string;
246
+
247
+ /**
248
+ * Validate file path to prevent path traversal attacks (internal)
249
+ * @param filePath File path to validate
250
+ * @returns Validated absolute path
251
+ * @throws {ValidationError} If path is invalid
252
+ * @throws {SecurityError} If path traversal detected
253
+ */
254
+ export function validateFilePath(filePath: string): string;
255
+
256
+ // Streaming JSON to CSV functions
257
+
258
+ /**
259
+ * Creates a transform stream that converts JSON objects to CSV rows
260
+ * @param options Configuration options
261
+ * @returns Transform stream
262
+ */
263
+ export function createJsonToCsvStream(
264
+ options?: JsonToCsvStreamOptions
265
+ ): Transform;
266
+
267
+ /**
268
+ * Converts a readable stream of JSON objects to CSV and writes to a writable stream
269
+ * @param inputStream Readable stream of JSON objects
270
+ * @param outputStream Writable stream for CSV output
271
+ * @param options Configuration options
272
+ * @returns Promise that resolves when streaming is complete
273
+ */
274
+ export function streamJsonToCsv(
275
+ inputStream: Readable,
276
+ outputStream: Writable,
277
+ options?: JsonToCsvStreamOptions
278
+ ): Promise<void>;
279
+
280
+ /**
281
+ * Converts JSON to CSV and saves it to a file using streaming
282
+ * @param inputStream Readable stream of JSON objects
283
+ * @param filePath Path to save the CSV file
284
+ * @param options Configuration options
285
+ * @returns Promise that resolves when file is saved
286
+ */
287
+ export function saveJsonStreamAsCsv(
288
+ inputStream: Readable,
289
+ filePath: string,
290
+ options?: JsonToCsvStreamOptions
291
+ ): Promise<void>;
292
+
293
+ /**
294
+ * Creates a readable stream from an array of JSON objects
295
+ * @param data Array of JSON objects
296
+ * @returns Readable stream
297
+ */
298
+ export function createJsonReadableStream(
299
+ data: Record<string, any>[]
300
+ ): Readable;
301
+
302
+ /**
303
+ * Creates a writable stream that collects CSV data
304
+ * @returns Writable stream that collects data
305
+ */
306
+ export function createCsvCollectorStream(): Writable;
307
+
308
+ // Streaming CSV to JSON functions
309
+
310
+ /**
311
+ * Creates a transform stream that converts CSV chunks to JSON objects
312
+ * @param options Configuration options
313
+ * @returns Transform stream
314
+ */
315
+ export function createCsvToJsonStream(
316
+ options?: CsvToJsonStreamOptions
317
+ ): Transform;
318
+
319
+ /**
320
+ * Converts a readable stream of CSV text to JSON objects
321
+ * @param inputStream Readable stream of CSV text
322
+ * @param outputStream Writable stream for JSON objects
323
+ * @param options Configuration options
324
+ * @returns Promise that resolves when streaming is complete
325
+ */
326
+ export function streamCsvToJson(
327
+ inputStream: Readable,
328
+ outputStream: Writable,
329
+ options?: CsvToJsonStreamOptions
330
+ ): Promise<void>;
331
+
332
+ /**
333
+ * Reads CSV file and converts it to JSON using streaming
334
+ * @param filePath Path to CSV file
335
+ * @param options Configuration options
336
+ * @returns Readable stream of JSON objects
337
+ */
338
+ export function createCsvFileToJsonStream(
339
+ filePath: string,
340
+ options?: CsvToJsonStreamOptions
341
+ ): Promise<Readable>;
342
+
343
+ /**
344
+ * Creates a writable stream that collects JSON objects into an array
345
+ * @returns Writable stream that collects data
346
+ */
347
+ export function createJsonCollectorStream(): Writable;
348
+ }
package/index.js ADDED
@@ -0,0 +1,44 @@
1
+ // Main entry point for the jtcsv module
2
+ // Exports both JSON→CSV and CSV→JSON functions
3
+
4
+ const jsonToCsvModule = require('./json-to-csv');
5
+ const csvToJsonModule = require('./csv-to-json');
6
+ const errorsModule = require('./errors');
7
+ const jsonSaveModule = require('./json-save');
8
+ const streamJsonToCsvModule = require('./stream-json-to-csv');
9
+ const streamCsvToJsonModule = require('./stream-csv-to-json');
10
+
11
+ // Combine all exports
12
+ module.exports = {
13
+ // JSON to CSV functions
14
+ jsonToCsv: jsonToCsvModule.jsonToCsv,
15
+ preprocessData: jsonToCsvModule.preprocessData,
16
+ saveAsCsv: jsonToCsvModule.saveAsCsv,
17
+ deepUnwrap: jsonToCsvModule.deepUnwrap,
18
+ validateFilePath: jsonToCsvModule.validateFilePath,
19
+
20
+ // CSV to JSON functions
21
+ csvToJson: csvToJsonModule.csvToJson,
22
+ readCsvAsJson: csvToJsonModule.readCsvAsJson,
23
+ readCsvAsJsonSync: csvToJsonModule.readCsvAsJsonSync,
24
+
25
+ // JSON save functions
26
+ saveAsJson: jsonSaveModule.saveAsJson,
27
+ saveAsJsonSync: jsonSaveModule.saveAsJsonSync,
28
+
29
+ // Streaming JSON to CSV functions
30
+ createJsonToCsvStream: streamJsonToCsvModule.createJsonToCsvStream,
31
+ streamJsonToCsv: streamJsonToCsvModule.streamJsonToCsv,
32
+ saveJsonStreamAsCsv: streamJsonToCsvModule.saveJsonStreamAsCsv,
33
+ createJsonReadableStream: streamJsonToCsvModule.createJsonReadableStream,
34
+ createCsvCollectorStream: streamJsonToCsvModule.createCsvCollectorStream,
35
+
36
+ // Streaming CSV to JSON functions
37
+ createCsvToJsonStream: streamCsvToJsonModule.createCsvToJsonStream,
38
+ streamCsvToJson: streamCsvToJsonModule.streamCsvToJson,
39
+ createCsvFileToJsonStream: streamCsvToJsonModule.createCsvFileToJsonStream,
40
+ createJsonCollectorStream: streamCsvToJsonModule.createJsonCollectorStream,
41
+
42
+ // Error classes
43
+ ...errorsModule
44
+ };
package/json-save.js ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * JSON Save Module - Node.js Module
3
+ *
4
+ * A lightweight module for saving JSON data to files with security validation.
5
+ *
6
+ * @module json-save
7
+ */
8
+
9
+ const {
10
+ ValidationError,
11
+ SecurityError,
12
+ FileSystemError,
13
+ LimitError,
14
+ ConfigurationError,
15
+ safeExecute
16
+ } = require('./errors');
17
+
18
+ /**
19
+ * Validates file path for JSON saving
20
+ * @private
21
+ */
22
+ function validateJsonFilePath(filePath) {
23
+ const path = require('path');
24
+
25
+ // Basic validation
26
+ if (typeof filePath !== 'string' || filePath.trim() === '') {
27
+ throw new ValidationError('File path must be a non-empty string');
28
+ }
29
+
30
+ // Ensure file has .json extension
31
+ if (!filePath.toLowerCase().endsWith('.json')) {
32
+ throw new ValidationError('File must have .json extension');
33
+ }
34
+
35
+ // Get absolute path and check for traversal
36
+ const absolutePath = path.resolve(filePath);
37
+ const normalizedPath = path.normalize(filePath);
38
+
39
+ // Prevent directory traversal attacks
40
+ if (normalizedPath.includes('..') ||
41
+ /\\\.\.\\|\/\.\.\//.test(filePath) ||
42
+ filePath.startsWith('..') ||
43
+ filePath.includes('/..')) {
44
+ throw new SecurityError('Directory traversal detected in file path');
45
+ }
46
+
47
+ return absolutePath;
48
+ }
49
+
50
+ /**
51
+ * Validates JSON data and options
52
+ * @private
53
+ */
54
+ function validateJsonData(data, options) {
55
+ // Validate data
56
+ if (data === undefined || data === null) {
57
+ throw new ValidationError('Data cannot be null or undefined');
58
+ }
59
+
60
+ // Validate options
61
+ if (options && typeof options !== 'object') {
62
+ throw new ConfigurationError('Options must be an object');
63
+ }
64
+
65
+ // Validate prettyPrint
66
+ if (options?.prettyPrint !== undefined && typeof options.prettyPrint !== 'boolean') {
67
+ throw new ConfigurationError('prettyPrint must be a boolean');
68
+ }
69
+
70
+ // Validate maxSize
71
+ if (options?.maxSize && (typeof options.maxSize !== 'number' || options.maxSize <= 0)) {
72
+ throw new ConfigurationError('maxSize must be a positive number');
73
+ }
74
+
75
+ return true;
76
+ }
77
+
78
+ /**
79
+ * Saves JSON data to a file
80
+ *
81
+ * @param {*} data - Data to save as JSON
82
+ * @param {string} filePath - Path to save the JSON file
83
+ * @param {Object} [options] - Configuration options
84
+ * @param {boolean} [options.prettyPrint=false] - Format JSON with indentation
85
+ * @param {number} [options.maxSize=10485760] - Maximum file size in bytes (default: 10MB)
86
+ * @returns {Promise<void>}
87
+ *
88
+ * @example
89
+ * const { saveAsJson } = require('./json-save');
90
+ *
91
+ * const data = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
92
+ * await saveAsJson(data, './output.json', { prettyPrint: true });
93
+ */
94
+ async function saveAsJson(data, filePath, options = {}) {
95
+ return safeExecute(async () => {
96
+ const fs = require('fs').promises;
97
+
98
+ // Validate file path
99
+ const safePath = validateJsonFilePath(filePath);
100
+
101
+ // Validate data and options
102
+ validateJsonData(data, options);
103
+
104
+ const opts = options && typeof options === 'object' ? options : {};
105
+ const {
106
+ prettyPrint = false,
107
+ maxSize = 10485760 // 10MB default limit
108
+ } = opts;
109
+
110
+ // Convert data to JSON string
111
+ let jsonString;
112
+ try {
113
+ if (prettyPrint) {
114
+ jsonString = JSON.stringify(data, null, 2);
115
+ } else {
116
+ jsonString = JSON.stringify(data);
117
+ }
118
+ } catch (error) {
119
+ if (error.message.includes('circular') || error.message.includes('Converting circular')) {
120
+ throw new ValidationError('Data contains circular references');
121
+ }
122
+ throw new ValidationError(`Failed to stringify JSON: ${error.message}`);
123
+ }
124
+
125
+ // Check size limit
126
+ const byteSize = Buffer.byteLength(jsonString, 'utf8');
127
+ if (byteSize > maxSize) {
128
+ throw new LimitError(
129
+ `JSON size exceeds maximum limit of ${maxSize} bytes`,
130
+ maxSize,
131
+ byteSize
132
+ );
133
+ }
134
+
135
+ // Ensure directory exists
136
+ const dir = require('path').dirname(safePath);
137
+
138
+ try {
139
+ await fs.mkdir(dir, { recursive: true });
140
+
141
+ // Write file
142
+ await fs.writeFile(safePath, jsonString, 'utf8');
143
+
144
+ return safePath;
145
+ } catch (error) {
146
+ if (error.code === 'ENOENT') {
147
+ throw new FileSystemError(`Directory does not exist: ${dir}`, error);
148
+ }
149
+ if (error.code === 'EACCES') {
150
+ throw new FileSystemError(`Permission denied: ${safePath}`, error);
151
+ }
152
+ if (error.code === 'ENOSPC') {
153
+ throw new FileSystemError(`No space left on device: ${safePath}`, error);
154
+ }
155
+
156
+ throw new FileSystemError(`Failed to write JSON file: ${error.message}`, error);
157
+ }
158
+ }, 'FILE_SYSTEM_ERROR', { function: 'saveAsJson' });
159
+ }
160
+
161
+ /**
162
+ * Synchronously saves JSON data to a file
163
+ *
164
+ * @param {*} data - Data to save as JSON
165
+ * @param {string} filePath - Path to save the JSON file
166
+ * @param {Object} [options] - Configuration options (same as saveAsJson)
167
+ * @returns {string} Path to saved file
168
+ */
169
+ function saveAsJsonSync(data, filePath, options = {}) {
170
+ return safeExecute(() => {
171
+ const fs = require('fs');
172
+
173
+ // Validate file path
174
+ const safePath = validateJsonFilePath(filePath);
175
+
176
+ // Validate data and options
177
+ validateJsonData(data, options);
178
+
179
+ const opts = options && typeof options === 'object' ? options : {};
180
+ const {
181
+ prettyPrint = false,
182
+ maxSize = 10485760 // 10MB default limit
183
+ } = opts;
184
+
185
+ // Convert data to JSON string
186
+ let jsonString;
187
+ try {
188
+ if (prettyPrint) {
189
+ jsonString = JSON.stringify(data, null, 2);
190
+ } else {
191
+ jsonString = JSON.stringify(data);
192
+ }
193
+ } catch (error) {
194
+ if (error.message.includes('circular') || error.message.includes('Converting circular')) {
195
+ throw new ValidationError('Data contains circular references');
196
+ }
197
+ throw new ValidationError(`Failed to stringify JSON: ${error.message}`);
198
+ }
199
+
200
+ // Check size limit
201
+ const byteSize = Buffer.byteLength(jsonString, 'utf8');
202
+ if (byteSize > maxSize) {
203
+ throw new LimitError(
204
+ `JSON size exceeds maximum limit of ${maxSize} bytes`,
205
+ maxSize,
206
+ byteSize
207
+ );
208
+ }
209
+
210
+ // Ensure directory exists
211
+ const dir = require('path').dirname(safePath);
212
+
213
+ try {
214
+ if (!fs.existsSync(dir)) {
215
+ fs.mkdirSync(dir, { recursive: true });
216
+ }
217
+
218
+ // Write file
219
+ fs.writeFileSync(safePath, jsonString, 'utf8');
220
+
221
+ return safePath;
222
+ } catch (error) {
223
+ if (error.code === 'ENOENT') {
224
+ throw new FileSystemError(`Directory does not exist: ${dir}`, error);
225
+ }
226
+ if (error.code === 'EACCES') {
227
+ throw new FileSystemError(`Permission denied: ${safePath}`, error);
228
+ }
229
+ if (error.code === 'ENOSPC') {
230
+ throw new FileSystemError(`No space left on device: ${safePath}`, error);
231
+ }
232
+
233
+ throw new FileSystemError(`Failed to write JSON file: ${error.message}`, error);
234
+ }
235
+ }, 'FILE_SYSTEM_ERROR', { function: 'saveAsJsonSync' });
236
+ }
237
+
238
+ // Export the functions
239
+ module.exports = {
240
+ saveAsJson,
241
+ saveAsJsonSync,
242
+ validateJsonFilePath
243
+ };
244
+
245
+ // For ES6 module compatibility
246
+ if (typeof module !== 'undefined' && module.exports) {
247
+ module.exports.default = saveAsJson;
248
+ }