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/LICENSE +21 -0
- package/README.md +393 -0
- package/bin/jtcsv.js +394 -0
- package/cli-tui.js +0 -0
- package/csv-to-json.js +492 -0
- package/errors.js +188 -0
- package/index.d.ts +348 -0
- package/index.js +44 -0
- package/json-save.js +248 -0
- package/json-to-csv.js +430 -0
- package/package.json +87 -0
- package/stream-csv-to-json.js +613 -0
- package/stream-json-to-csv.js +532 -0
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
|
+
}
|