jtcsv 2.1.3 → 2.2.2

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.
Files changed (52) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +60 -341
  3. package/bin/jtcsv.js +2462 -1372
  4. package/csv-to-json.js +35 -26
  5. package/dist/jtcsv.cjs.js +807 -133
  6. package/dist/jtcsv.cjs.js.map +1 -1
  7. package/dist/jtcsv.esm.js +800 -134
  8. package/dist/jtcsv.esm.js.map +1 -1
  9. package/dist/jtcsv.umd.js +807 -133
  10. package/dist/jtcsv.umd.js.map +1 -1
  11. package/errors.js +20 -0
  12. package/examples/browser-vanilla.html +37 -0
  13. package/examples/cli-batch-processing.js +38 -0
  14. package/examples/error-handling.js +324 -0
  15. package/examples/ndjson-processing.js +434 -0
  16. package/examples/react-integration.jsx +637 -0
  17. package/examples/schema-validation.js +640 -0
  18. package/examples/simple-usage.js +10 -7
  19. package/examples/typescript-example.ts +486 -0
  20. package/examples/web-workers-advanced.js +28 -0
  21. package/index.d.ts +2 -0
  22. package/json-save.js +2 -1
  23. package/json-to-csv.js +171 -131
  24. package/package.json +20 -4
  25. package/plugins/README.md +41 -467
  26. package/plugins/express-middleware/README.md +32 -274
  27. package/plugins/hono/README.md +16 -13
  28. package/plugins/nestjs/README.md +13 -11
  29. package/plugins/nextjs-api/README.md +28 -423
  30. package/plugins/nextjs-api/index.js +1 -2
  31. package/plugins/nextjs-api/route.js +1 -2
  32. package/plugins/nuxt/README.md +6 -7
  33. package/plugins/remix/README.md +9 -9
  34. package/plugins/sveltekit/README.md +8 -8
  35. package/plugins/trpc/README.md +8 -5
  36. package/src/browser/browser-functions.js +33 -3
  37. package/src/browser/csv-to-json-browser.js +269 -11
  38. package/src/browser/errors-browser.js +19 -1
  39. package/src/browser/index.js +39 -5
  40. package/src/browser/streams.js +393 -0
  41. package/src/browser/workers/csv-parser.worker.js +20 -2
  42. package/src/browser/workers/worker-pool.js +507 -447
  43. package/src/core/plugin-system.js +4 -0
  44. package/src/engines/fast-path-engine.js +31 -23
  45. package/src/errors.js +26 -0
  46. package/src/formats/ndjson-parser.js +54 -5
  47. package/src/formats/tsv-parser.js +4 -1
  48. package/src/utils/schema-validator.js +594 -0
  49. package/src/utils/transform-loader.js +205 -0
  50. package/src/web-server/index.js +683 -0
  51. package/stream-csv-to-json.js +16 -87
  52. package/stream-json-to-csv.js +18 -86
package/json-to-csv.js CHANGED
@@ -16,6 +16,8 @@ const {
16
16
  safeExecute
17
17
  } = require('./errors');
18
18
 
19
+ // Add schema validator import
20
+ const { createSchemaValidators } = require('./src/utils/schema-validator');
19
21
  /**
20
22
  * Validates input data and options
21
23
  * @private
@@ -62,6 +64,11 @@ function validateInput(data, options) {
62
64
  throw new ConfigurationError('rfc4180Compliant must be a boolean');
63
65
  }
64
66
 
67
+ // Validate schema
68
+ if (options?.schema && typeof options.schema !== 'object') {
69
+ throw new ConfigurationError('schema must be an object');
70
+ }
71
+
65
72
  return true;
66
73
  }
67
74
 
@@ -77,6 +84,7 @@ function validateInput(data, options) {
77
84
  * @param {number} [options.maxRecords] - Maximum number of records to process (optional, no limit by default)
78
85
  * @param {boolean} [options.preventCsvInjection=true] - Prevent CSV injection attacks by escaping formulas
79
86
  * @param {boolean} [options.rfc4180Compliant=true] - Ensure RFC 4180 compliance (proper quoting, line endings)
87
+ * @param {Object} [options.schema] - JSON schema for data validation and formatting
80
88
  * @returns {string} CSV formatted string
81
89
  *
82
90
  * @example
@@ -108,9 +116,16 @@ function jsonToCsv(data, options = {}) {
108
116
  template = {},
109
117
  maxRecords,
110
118
  preventCsvInjection = true,
111
- rfc4180Compliant = true
119
+ rfc4180Compliant = true,
120
+ schema = null
112
121
  } = opts;
113
122
 
123
+ // Initialize schema validators if schema is provided
124
+ let schemaValidators = null;
125
+ if (schema) {
126
+ schemaValidators = createSchemaValidators(schema);
127
+ }
128
+
114
129
  // Handle empty data
115
130
  if (data.length === 0) {
116
131
  return '';
@@ -135,68 +150,68 @@ function jsonToCsv(data, options = {}) {
135
150
  );
136
151
  }
137
152
 
138
- // Get all unique keys from all objects with minimal allocations.
139
- const allKeys = new Set();
140
- const originalKeys = [];
141
- for (let i = 0; i < data.length; i++) {
142
- const item = data[i];
143
- if (!item || typeof item !== 'object') {
144
- continue;
145
- }
146
- for (const key in item) {
147
- if (Object.prototype.hasOwnProperty.call(item, key) && !allKeys.has(key)) {
148
- allKeys.add(key);
149
- originalKeys.push(key);
150
- }
151
- }
152
- }
153
-
154
- const hasRenameMap = Object.keys(renameMap).length > 0;
155
- const hasTemplate = Object.keys(template).length > 0;
156
-
157
- // Apply rename map to create header names.
158
- let headers = originalKeys;
159
- let reverseRenameMap = null;
160
- if (hasRenameMap) {
161
- headers = new Array(originalKeys.length);
162
- reverseRenameMap = {};
163
- for (let i = 0; i < originalKeys.length; i++) {
164
- const key = originalKeys[i];
165
- const header = renameMap[key] || key;
166
- headers[i] = header;
167
- reverseRenameMap[header] = key;
168
- }
169
- }
170
-
171
- // Apply template ordering if provided.
172
- let finalHeaders = headers;
173
- if (hasTemplate) {
174
- const templateKeys = Object.keys(template);
175
- const templateHeaders = hasRenameMap
176
- ? templateKeys.map(key => renameMap[key] || key)
177
- : templateKeys;
178
- const templateHeaderSet = new Set(templateHeaders);
179
- const extraHeaders = [];
180
- for (let i = 0; i < headers.length; i++) {
181
- const header = headers[i];
182
- if (!templateHeaderSet.has(header)) {
183
- extraHeaders.push(header);
184
- }
185
- }
186
- finalHeaders = templateHeaders.concat(extraHeaders);
187
- }
188
-
189
- const finalKeys = new Array(finalHeaders.length);
190
- if (hasRenameMap) {
191
- for (let i = 0; i < finalHeaders.length; i++) {
192
- const header = finalHeaders[i];
193
- finalKeys[i] = reverseRenameMap[header] || header;
194
- }
195
- } else {
196
- for (let i = 0; i < finalHeaders.length; i++) {
197
- finalKeys[i] = finalHeaders[i];
198
- }
199
- }
153
+ // Get all unique keys from all objects with minimal allocations.
154
+ const allKeys = new Set();
155
+ const originalKeys = [];
156
+ for (let i = 0; i < data.length; i++) {
157
+ const item = data[i];
158
+ if (!item || typeof item !== 'object') {
159
+ continue;
160
+ }
161
+ for (const key in item) {
162
+ if (Object.prototype.hasOwnProperty.call(item, key) && !allKeys.has(key)) {
163
+ allKeys.add(key);
164
+ originalKeys.push(key);
165
+ }
166
+ }
167
+ }
168
+
169
+ const hasRenameMap = Object.keys(renameMap).length > 0;
170
+ const hasTemplate = Object.keys(template).length > 0;
171
+
172
+ // Apply rename map to create header names.
173
+ let headers = originalKeys;
174
+ let reverseRenameMap = null;
175
+ if (hasRenameMap) {
176
+ headers = new Array(originalKeys.length);
177
+ reverseRenameMap = {};
178
+ for (let i = 0; i < originalKeys.length; i++) {
179
+ const key = originalKeys[i];
180
+ const header = renameMap[key] || key;
181
+ headers[i] = header;
182
+ reverseRenameMap[header] = key;
183
+ }
184
+ }
185
+
186
+ // Apply template ordering if provided.
187
+ let finalHeaders = headers;
188
+ if (hasTemplate) {
189
+ const templateKeys = Object.keys(template);
190
+ const templateHeaders = hasRenameMap
191
+ ? templateKeys.map(key => renameMap[key] || key)
192
+ : templateKeys;
193
+ const templateHeaderSet = new Set(templateHeaders);
194
+ const extraHeaders = [];
195
+ for (let i = 0; i < headers.length; i++) {
196
+ const header = headers[i];
197
+ if (!templateHeaderSet.has(header)) {
198
+ extraHeaders.push(header);
199
+ }
200
+ }
201
+ finalHeaders = templateHeaders.concat(extraHeaders);
202
+ }
203
+
204
+ const finalKeys = new Array(finalHeaders.length);
205
+ if (hasRenameMap) {
206
+ for (let i = 0; i < finalHeaders.length; i++) {
207
+ const header = finalHeaders[i];
208
+ finalKeys[i] = reverseRenameMap[header] || header;
209
+ }
210
+ } else {
211
+ for (let i = 0; i < finalHeaders.length; i++) {
212
+ finalKeys[i] = finalHeaders[i];
213
+ }
214
+ }
200
215
 
201
216
  /**
202
217
  * Escapes a value for CSV format with CSV injection protection
@@ -205,75 +220,100 @@ function jsonToCsv(data, options = {}) {
205
220
  * @param {*} value - The value to escape
206
221
  * @returns {string} Escaped CSV value
207
222
  */
208
- const quoteRegex = /"/g;
209
- const delimiterCode = delimiter.charCodeAt(0);
210
-
211
- const escapeValue = (value) => {
212
- if (value === null || value === undefined || value === '') {
213
- return '';
214
- }
215
-
216
- let stringValue = value;
217
- if (typeof stringValue !== 'string') {
218
- stringValue = String(stringValue);
219
- }
220
-
221
- // CSV Injection protection - escape formulas if enabled
222
- let escapedValue = stringValue;
223
- if (preventCsvInjection) {
224
- const firstCharCode = stringValue.charCodeAt(0);
225
- if (firstCharCode === 61 || firstCharCode === 43 || firstCharCode === 45 || firstCharCode === 64) {
226
- escapedValue = "'" + stringValue;
227
- }
228
- }
229
-
230
- let needsQuoting = false;
231
- let hasQuote = false;
232
- for (let i = 0; i < escapedValue.length; i++) {
233
- const code = escapedValue.charCodeAt(i);
234
- if (code === 34) {
235
- hasQuote = true;
236
- needsQuoting = true;
237
- } else if (code === delimiterCode || code === 10 || code === 13) {
238
- needsQuoting = true;
239
- }
240
- }
241
-
242
- if (needsQuoting) {
243
- const quotedValue = hasQuote ? escapedValue.replace(quoteRegex, '""') : escapedValue;
244
- return `"${quotedValue}"`;
245
- }
246
-
247
- return escapedValue;
248
- };
223
+ const quoteRegex = /"/g;
224
+ const delimiterCode = delimiter.charCodeAt(0);
225
+
226
+ const escapeValue = (value) => {
227
+ if (value === null || value === undefined || value === '') {
228
+ return '';
229
+ }
230
+
231
+ let stringValue = value;
232
+ if (typeof stringValue !== 'string') {
233
+ stringValue = String(stringValue);
234
+ }
235
+
236
+ // CSV Injection protection - escape formulas if enabled
237
+ let escapedValue = stringValue;
238
+ if (preventCsvInjection) {
239
+ const firstCharCode = stringValue.charCodeAt(0);
240
+ if (firstCharCode === 61 || firstCharCode === 43 || firstCharCode === 45 || firstCharCode === 64) {
241
+ escapedValue = "'" + stringValue;
242
+ }
243
+ }
244
+
245
+ let needsQuoting = false;
246
+ let hasQuote = false;
247
+ for (let i = 0; i < escapedValue.length; i++) {
248
+ const code = escapedValue.charCodeAt(i);
249
+ if (code === 34) {
250
+ hasQuote = true;
251
+ needsQuoting = true;
252
+ } else if (code === delimiterCode || code === 10 || code === 13) {
253
+ needsQuoting = true;
254
+ }
255
+ }
256
+
257
+ if (needsQuoting) {
258
+ const quotedValue = hasQuote ? escapedValue.replace(quoteRegex, '""') : escapedValue;
259
+ return `"${quotedValue}"`;
260
+ }
261
+
262
+ return escapedValue;
263
+ };
264
+
265
+ // Build CSV rows.
266
+ const rows = [];
267
+ const columnCount = finalHeaders.length;
268
+
269
+ // Add headers row if requested.
270
+ if (includeHeaders && columnCount > 0) {
271
+ rows.push(finalHeaders.join(delimiter));
272
+ }
273
+
274
+ // Add data rows.
275
+ const rowValues = new Array(columnCount);
276
+ for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
277
+ const item = data[rowIndex];
278
+ if (!item || typeof item !== 'object') {
279
+ continue;
280
+ }
281
+
282
+ // Apply schema validation and formatting if schema is provided
283
+ let processedItem = item;
284
+ if (schemaValidators) {
285
+ processedItem = { ...item };
286
+ for (const [key, validator] of Object.entries(schemaValidators)) {
287
+ if (key in processedItem) {
288
+ const value = processedItem[key];
289
+ // Validate value
290
+ if (!validator.validate(value)) {
291
+ throw new ValidationError(
292
+ `Row ${rowIndex + 1}: Value for field "${key}" does not match schema`
293
+ );
294
+ }
295
+ // Format value if formatter exists
296
+ if (validator.format) {
297
+ processedItem[key] = validator.format(value);
298
+ }
299
+ } else if (validator.required) {
300
+ throw new ValidationError(
301
+ `Row ${rowIndex + 1}: Required field "${key}" is missing`
302
+ );
303
+ }
304
+ }
305
+ }
306
+
307
+ for (let i = 0; i < columnCount; i++) {
308
+ rowValues[i] = escapeValue(processedItem[finalKeys[i]]);
309
+ }
310
+
311
+ rows.push(rowValues.join(delimiter));
312
+ }
249
313
 
250
- // Build CSV rows.
251
- const rows = [];
252
- const columnCount = finalHeaders.length;
253
-
254
- // Add headers row if requested.
255
- if (includeHeaders && columnCount > 0) {
256
- rows.push(finalHeaders.join(delimiter));
257
- }
258
-
259
- // Add data rows.
260
- const rowValues = new Array(columnCount);
261
- for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
262
- const item = data[rowIndex];
263
- if (!item || typeof item !== 'object') {
264
- continue;
265
- }
266
-
267
- for (let i = 0; i < columnCount; i++) {
268
- rowValues[i] = escapeValue(item[finalKeys[i]]);
269
- }
270
-
271
- rows.push(rowValues.join(delimiter));
272
- }
273
-
274
- // RFC 4180: Each record is located on a separate line, delimited by a line break (CRLF).
275
- const lineEnding = rfc4180Compliant ? '\r\n' : '\n';
276
- return rows.join(lineEnding);
314
+ // RFC 4180: Each record is located on a separate line, delimited by a line break (CRLF).
315
+ const lineEnding = rfc4180Compliant ? '\r\n' : '\n';
316
+ return rows.join(lineEnding);
277
317
  }, 'PARSE_FAILED', { function: 'jsonToCsv' });
278
318
  }
279
319
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jtcsv",
3
- "version": "2.1.3",
3
+ "version": "2.2.2",
4
4
  "description": "Complete JSON<->CSV and CSV<->JSON converter for Node.js and Browser with streaming, security, Web Workers, TypeScript support, and optional ecosystem (zero-deps core)",
5
5
  "main": "index.js",
6
6
  "browser": "dist/jtcsv.umd.js",
@@ -87,6 +87,11 @@
87
87
  "test:fastpath": "jest __tests__/fast-path-engine.test.js",
88
88
  "test:ndjson": "jest __tests__/ndjson-parser.test.js",
89
89
  "test:performance": "jest __tests__/*.test.js --testNamePattern=\"Производительность|производительность\"",
90
+ "test:benchmark": "jest __tests__/benchmark-suite.test.js --testTimeout=60000",
91
+ "test:load": "jest __tests__/load-tests.test.js --testTimeout=300000",
92
+ "test:load:large": "LOAD_TEST_SIZE=large jest __tests__/load-tests.test.js --testTimeout=300000",
93
+ "test:security": "jest __tests__/security-fuzzing.test.js --testTimeout=60000",
94
+ "test:memory": "node --expose-gc node_modules/jest/bin/jest __tests__/memory-profiling.test.js --testTimeout=120000",
90
95
  "test:express": "cd plugins/express-middleware && npm test",
91
96
  "test:fastify": "cd plugins/fastify-plugin && npm test",
92
97
  "test:nextjs": "cd plugins/nextjs-api && npm test",
@@ -110,9 +115,13 @@
110
115
  "size:why": "size-limit --why",
111
116
  "benchmark": "node benchmark.js",
112
117
  "benchmark:fastpath": "node benchmark.js --fastpath",
118
+ "profile:perf": "node scripts/profile-performance.js",
113
119
  "example:plugins": "node examples/plugin-excel-exporter.js",
114
120
  "example:express": "cd plugins/express-middleware && node example.js",
115
- "plugins:build": "npm run build && cd plugins/express-middleware && npm run build && cd ../fastify-plugin && npm run build && cd ../nextjs-api && npm run build"
121
+ "plugins:build": "npm run build && cd plugins/express-middleware && npm run build && cd ../fastify-plugin && npm run build && cd ../nextjs-api && npm run build",
122
+ "docs": "typedoc",
123
+ "docs:watch": "typedoc --watch",
124
+ "docs:serve": "typedoc && npx serve docs/api"
116
125
  },
117
126
  "keywords": [
118
127
  "json",
@@ -174,7 +183,7 @@
174
183
  },
175
184
  "homepage": "https://github.com/Linol-Hamelton/jtcsv#readme",
176
185
  "engines": {
177
- "node": ">=12.0.0"
186
+ "node": ">=18.0.0"
178
187
  },
179
188
  "files": [
180
189
  "index.js",
@@ -191,6 +200,9 @@
191
200
  "examples/",
192
201
  "plugins/"
193
202
  ],
203
+ "dependencies": {
204
+ "glob": "10.5.0"
205
+ },
194
206
  "devDependencies": {
195
207
  "@babel/core": "^7.23.0",
196
208
  "@babel/preset-env": "^7.22.0",
@@ -199,10 +211,14 @@
199
211
  "@rollup/plugin-node-resolve": "^15.0.0",
200
212
  "@rollup/plugin-terser": "^0.4.0",
201
213
  "@size-limit/preset-small-lib": "12.0.0",
214
+ "blessed": "^0.1.81",
215
+ "blessed-contrib": "4.11.0",
202
216
  "eslint": "8.57.1",
203
217
  "jest": "^29.0.0",
218
+ "jest-environment-jsdom": "30.2.0",
204
219
  "rollup": "^4.0.0",
205
- "size-limit": "12.0.0"
220
+ "size-limit": "12.0.0",
221
+ "typedoc": "^0.25.0"
206
222
  },
207
223
  "type": "commonjs",
208
224
  "size-limit": [