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
@@ -0,0 +1,594 @@
1
+ /**
2
+ * Schema Validator Utility
3
+ *
4
+ * Utility for loading and applying JSON schema validation in CLI
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const {
11
+ ValidationError,
12
+ SecurityError,
13
+ ConfigurationError
14
+ } = require('../errors');
15
+
16
+ /**
17
+ * Loads JSON schema from file or string
18
+ *
19
+ * @param {string} schemaPathOrJson - Path to JSON file or JSON string
20
+ * @returns {Object} Parsed JSON schema
21
+ */
22
+ function loadSchema(schemaPathOrJson) {
23
+ if (!schemaPathOrJson || typeof schemaPathOrJson !== 'string') {
24
+ throw new ValidationError('Schema must be a string (JSON or file path)');
25
+ }
26
+
27
+ let schemaString = schemaPathOrJson;
28
+
29
+ // Check if it's a file path (ends with .json or contains path separators)
30
+ const isFilePath = schemaPathOrJson.endsWith('.json') ||
31
+ schemaPathOrJson.includes('/') ||
32
+ schemaPathOrJson.includes('\\');
33
+
34
+ if (isFilePath) {
35
+ // Validate file path
36
+ const safePath = path.resolve(schemaPathOrJson);
37
+
38
+ // Prevent directory traversal
39
+ const normalizedPath = path.normalize(schemaPathOrJson);
40
+ if (normalizedPath.includes('..') ||
41
+ /\\\.\.\\|\/\.\.\//.test(schemaPathOrJson) ||
42
+ schemaPathOrJson.startsWith('..') ||
43
+ schemaPathOrJson.includes('/..')) {
44
+ throw new SecurityError('Directory traversal detected in schema file path');
45
+ }
46
+
47
+ // Check file exists and has .json extension
48
+ if (!fs.existsSync(safePath)) {
49
+ throw new ValidationError(`Schema file not found: ${schemaPathOrJson}`);
50
+ }
51
+
52
+ if (!safePath.toLowerCase().endsWith('.json')) {
53
+ throw new ValidationError('Schema file must have .json extension');
54
+ }
55
+
56
+ try {
57
+ schemaString = fs.readFileSync(safePath, 'utf8');
58
+ } catch (error) {
59
+ if (error.code === 'EACCES') {
60
+ throw new SecurityError(`Permission denied reading schema file: ${schemaPathOrJson}`);
61
+ }
62
+ throw new ValidationError(`Failed to read schema file: ${error.message}`);
63
+ }
64
+ }
65
+
66
+ // Parse JSON schema
67
+ try {
68
+ const schema = JSON.parse(schemaString);
69
+
70
+ // Validate basic schema structure
71
+ if (typeof schema !== 'object' || schema === null) {
72
+ throw new ValidationError('Schema must be a JSON object');
73
+ }
74
+
75
+ return schema;
76
+ } catch (error) {
77
+ if (error instanceof SyntaxError) {
78
+ throw new ValidationError(`Invalid JSON in schema: ${error.message}`);
79
+ }
80
+ throw new ValidationError(`Failed to parse schema: ${error.message}`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Creates a validation hook for use with csvToJson/jsonToCsv hooks system
86
+ *
87
+ * @param {string|Object} schema - Schema object or path to schema file
88
+ * @returns {Function} Validation hook function
89
+ */
90
+ function createValidationHook(schema) {
91
+ let schemaObj;
92
+
93
+ if (typeof schema === 'string') {
94
+ // Load schema from file or JSON string
95
+ schemaObj = loadSchema(schema);
96
+ } else if (typeof schema === 'object' && schema !== null) {
97
+ // Use provided schema object
98
+ schemaObj = schema;
99
+ } else {
100
+ throw new ValidationError('Schema must be an object or a path to a JSON file');
101
+ }
102
+
103
+ // Try to use @jtcsv/validator if available
104
+ let validator;
105
+ try {
106
+ const JtcsvValidator = require('../../packages/jtcsv-validator/src/index');
107
+ validator = new JtcsvValidator();
108
+
109
+ // Convert simple schema format to validator format
110
+ if (schemaObj.fields) {
111
+ // Assume it's already in validator format
112
+ validator.schema(schemaObj.fields);
113
+ } else {
114
+ // Convert simple field definitions
115
+ Object.entries(schemaObj).forEach(([field, rule]) => {
116
+ if (typeof rule === 'object') {
117
+ validator.field(field, rule);
118
+ }
119
+ });
120
+ }
121
+ } catch (error) {
122
+ // Fallback to simple validation if validator is not available
123
+ console.warn('@jtcsv/validator not available, using simple validation');
124
+ validator = createSimpleValidator(schemaObj);
125
+ }
126
+
127
+ // Return a hook function compatible with hooks.perRow
128
+ return function(row, index, context) {
129
+ try {
130
+ const result = validator.validate([row], {
131
+ stopOnFirstError: true,
132
+ transform: false
133
+ });
134
+
135
+ if (!result.valid && result.errors.length > 0) {
136
+ const error = result.errors[0];
137
+ throw new ValidationError(
138
+ `Row ${index + 1}: ${error.message} (field: ${error.field})`
139
+ );
140
+ }
141
+
142
+ return row;
143
+ } catch (error) {
144
+ if (error instanceof ValidationError) {
145
+ throw error;
146
+ }
147
+ // Log error but don't crash - return original row
148
+ console.error(`Validation error at row ${index}: ${error.message}`);
149
+ if (process.env.NODE_ENV === 'development') {
150
+ console.error(error.stack);
151
+ }
152
+ return row;
153
+ }
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Creates a simple validator for fallback when @jtcsv/validator is not available
159
+ *
160
+ * @private
161
+ */
162
+ function createSimpleValidator(schema) {
163
+ return {
164
+ validate(data, options = {}) {
165
+ const errors = [];
166
+ const warnings = [];
167
+
168
+ if (!Array.isArray(data)) {
169
+ return {
170
+ valid: false,
171
+ errors: [{ type: 'INVALID_DATA', message: 'Data must be an array' }],
172
+ warnings: [],
173
+ summary: {
174
+ totalRows: 0,
175
+ validRows: 0,
176
+ errorCount: 1,
177
+ warningCount: 0
178
+ }
179
+ };
180
+ }
181
+
182
+ for (let i = 0; i < data.length; i++) {
183
+ const row = data[i];
184
+
185
+ for (const [field, rule] of Object.entries(schema)) {
186
+ const value = row[field];
187
+
188
+ // Check required
189
+ if (rule.required && (value === undefined || value === null || value === '')) {
190
+ errors.push({
191
+ row: i + 1,
192
+ type: 'REQUIRED',
193
+ field,
194
+ message: `Field "${field}" is required`,
195
+ value
196
+ });
197
+ continue;
198
+ }
199
+
200
+ // Skip further validation if value is empty and not required
201
+ if (value === undefined || value === null || value === '') {
202
+ continue;
203
+ }
204
+
205
+ // Check type
206
+ if (rule.type) {
207
+ const types = Array.isArray(rule.type) ? rule.type : [rule.type];
208
+ let typeValid = false;
209
+
210
+ for (const type of types) {
211
+ if (checkType(value, type)) {
212
+ typeValid = true;
213
+ break;
214
+ }
215
+ }
216
+
217
+ if (!typeValid) {
218
+ errors.push({
219
+ row: i + 1,
220
+ type: 'TYPE',
221
+ field,
222
+ message: `Field "${field}" must be of type ${types.join(' or ')}`,
223
+ value,
224
+ expected: types
225
+ });
226
+ }
227
+ }
228
+
229
+ // Check min/max for strings
230
+ if (rule.min !== undefined && typeof value === 'string' && value.length < rule.min) {
231
+ errors.push({
232
+ row: i + 1,
233
+ type: 'MIN_LENGTH',
234
+ field,
235
+ message: `Field "${field}" must be at least ${rule.min} characters`,
236
+ value,
237
+ min: rule.min
238
+ });
239
+ }
240
+
241
+ if (rule.max !== undefined && typeof value === 'string' && value.length > rule.max) {
242
+ errors.push({
243
+ row: i + 1,
244
+ type: 'MAX_LENGTH',
245
+ field,
246
+ message: `Field "${field}" must be at most ${rule.max} characters`,
247
+ value,
248
+ max: rule.max
249
+ });
250
+ }
251
+
252
+ // Check min/max for numbers
253
+ if (rule.min !== undefined && typeof value === 'number' && value < rule.min) {
254
+ errors.push({
255
+ row: i + 1,
256
+ type: 'MIN_VALUE',
257
+ field,
258
+ message: `Field "${field}" must be at least ${rule.min}`,
259
+ value,
260
+ min: rule.min
261
+ });
262
+ }
263
+
264
+ if (rule.max !== undefined && typeof value === 'number' && value > rule.max) {
265
+ errors.push({
266
+ row: i + 1,
267
+ type: 'MAX_VALUE',
268
+ field,
269
+ message: `Field "${field}" must be at most ${rule.max}`,
270
+ value,
271
+ max: rule.max
272
+ });
273
+ }
274
+
275
+ // Check pattern
276
+ if (rule.pattern && typeof value === 'string') {
277
+ const pattern = rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern);
278
+ if (!pattern.test(value)) {
279
+ errors.push({
280
+ row: i + 1,
281
+ type: 'PATTERN',
282
+ field,
283
+ message: `Field "${field}" must match pattern`,
284
+ value,
285
+ pattern: pattern.toString()
286
+ });
287
+ }
288
+ }
289
+
290
+ // Check enum
291
+ if (rule.enum && Array.isArray(rule.enum) && !rule.enum.includes(value)) {
292
+ errors.push({
293
+ row: i + 1,
294
+ type: 'ENUM',
295
+ field,
296
+ message: `Field "${field}" must be one of: ${rule.enum.join(', ')}`,
297
+ value,
298
+ allowed: rule.enum
299
+ });
300
+ }
301
+ }
302
+ }
303
+
304
+ return {
305
+ valid: errors.length === 0,
306
+ errors,
307
+ warnings,
308
+ summary: {
309
+ totalRows: data.length,
310
+ validRows: data.length - errors.length,
311
+ errorCount: errors.length,
312
+ warningCount: warnings.length
313
+ }
314
+ };
315
+ }
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Checks if value matches type
321
+ *
322
+ * @private
323
+ */
324
+ function checkType(value, type) {
325
+ switch (type) {
326
+ case 'string':
327
+ return typeof value === 'string';
328
+ case 'number':
329
+ return typeof value === 'number' && !isNaN(value);
330
+ case 'boolean':
331
+ return typeof value === 'boolean';
332
+ case 'integer':
333
+ return Number.isInteger(value);
334
+ case 'float':
335
+ return typeof value === 'number' && !Number.isInteger(value);
336
+ case 'date':
337
+ return value instanceof Date && !isNaN(value);
338
+ case 'array':
339
+ return Array.isArray(value);
340
+ case 'object':
341
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
342
+ default:
343
+ return false;
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Applies schema validation to data array
349
+ *
350
+ * @param {Array} data - Array of data to validate
351
+ * @param {string|Object} schema - Schema object or path to schema file
352
+ * @returns {Object} Validation result
353
+ */
354
+ function applySchemaValidation(data, schema) {
355
+ if (!Array.isArray(data)) {
356
+ throw new ValidationError('Data must be an array');
357
+ }
358
+
359
+ const validationHook = createValidationHook(schema);
360
+ const errors = [];
361
+ const validatedData = [];
362
+
363
+ for (let i = 0; i < data.length; i++) {
364
+ try {
365
+ const validatedRow = validationHook(data[i], i, { operation: 'validate' });
366
+ validatedData.push(validatedRow);
367
+ } catch (error) {
368
+ if (error instanceof ValidationError) {
369
+ errors.push({
370
+ row: i + 1,
371
+ message: error.message,
372
+ data: data[i]
373
+ });
374
+ } else {
375
+ // Skip rows with non-validation errors
376
+ validatedData.push(data[i]);
377
+ }
378
+ }
379
+ }
380
+
381
+ return {
382
+ valid: errors.length === 0,
383
+ errors,
384
+ data: validatedData,
385
+ summary: {
386
+ totalRows: data.length,
387
+ validRows: validatedData.length,
388
+ errorCount: errors.length,
389
+ errorRate: data.length > 0 ? (errors.length / data.length) * 100 : 0
390
+ }
391
+ };
392
+ }
393
+
394
+ /**
395
+ * Creates a TransformHooks instance with validation
396
+ *
397
+ * @param {string|Object} schema - Schema object or path to schema file
398
+ * @returns {Object} TransformHooks instance
399
+ */
400
+ function createValidationHooks(schema) {
401
+ const { TransformHooks } = require('../core/transform-hooks');
402
+ const hooks = new TransformHooks();
403
+
404
+ const validationHook = createValidationHook(schema);
405
+ hooks.perRow(validationHook);
406
+
407
+ return hooks;
408
+ }
409
+
410
+ /**
411
+ * Creates schema validators from JSON schema
412
+ *
413
+ * @param {Object} schema - JSON schema
414
+ * @returns {Object} Validators object
415
+ */
416
+ function createSchemaValidators(schema) {
417
+ const validators = {};
418
+
419
+ // Handle both JSON Schema format and simple format
420
+ const properties = schema.properties || schema;
421
+ const requiredFields = schema.required || [];
422
+
423
+ if (!properties || typeof properties !== 'object') {
424
+ return validators;
425
+ }
426
+
427
+ for (const [key, definition] of Object.entries(properties)) {
428
+ const validator = {
429
+ type: definition.type,
430
+ required: requiredFields.includes(key)
431
+ };
432
+
433
+ // Add format function for dates and other formats
434
+ if (definition.type === 'string' && definition.format) {
435
+ validator.format = (value) => {
436
+ // Handle date-time format
437
+ if (definition.format === 'date-time') {
438
+ if (value instanceof Date) {
439
+ return value.toISOString();
440
+ }
441
+ /* istanbul ignore next */
442
+ if (typeof value === 'string') {
443
+ // Try to parse as date
444
+ const date = new Date(value);
445
+ if (!isNaN(date.getTime())) {
446
+ return date.toISOString();
447
+ }
448
+ }
449
+ }
450
+ // Handle email format
451
+ if (definition.format === 'email') {
452
+ if (typeof value === 'string') {
453
+ return value.toLowerCase().trim();
454
+ }
455
+ }
456
+ // Handle uri format
457
+ if (definition.format === 'uri') {
458
+ if (typeof value === 'string') {
459
+ return value.trim();
460
+ }
461
+ }
462
+ return value;
463
+ };
464
+ }
465
+
466
+ // Add validation function
467
+ validator.validate = (value) => {
468
+ if (value === null || value === undefined) {
469
+ return !validator.required;
470
+ }
471
+
472
+ // Type validation
473
+ if (definition.type === 'string' && typeof value !== 'string') {
474
+ // For date-time format, also accept Date objects
475
+ if (definition.format === 'date-time' && value instanceof Date) {
476
+ return true;
477
+ }
478
+ return false;
479
+ }
480
+ if (definition.type === 'number' && typeof value !== 'number') {
481
+ return false;
482
+ }
483
+ if (definition.type === 'integer' && (!Number.isInteger(value) || typeof value !== 'number')) {
484
+ return false;
485
+ }
486
+ if (definition.type === 'boolean' && typeof value !== 'boolean') {
487
+ return false;
488
+ }
489
+ if (definition.type === 'array' && !Array.isArray(value)) {
490
+ return false;
491
+ }
492
+ if (definition.type === 'object' && (typeof value !== 'object' || value === null || Array.isArray(value))) {
493
+ return false;
494
+ }
495
+
496
+ // Additional constraints for strings
497
+ if (definition.type === 'string') {
498
+ if (definition.minLength !== undefined && value.length < definition.minLength) {
499
+ return false;
500
+ }
501
+ if (definition.maxLength !== undefined && value.length > definition.maxLength) {
502
+ return false;
503
+ }
504
+ if (definition.pattern && !new RegExp(definition.pattern).test(value)) {
505
+ return false;
506
+ }
507
+ if (definition.format === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
508
+ return false;
509
+ }
510
+ if (definition.format === 'uri') {
511
+ try {
512
+ new URL(value);
513
+ } catch {
514
+ return false;
515
+ }
516
+ }
517
+ }
518
+
519
+ // Additional constraints for numbers
520
+ if (definition.type === 'number' || definition.type === 'integer') {
521
+ if (definition.minimum !== undefined && value < definition.minimum) {
522
+ return false;
523
+ }
524
+ if (definition.maximum !== undefined && value > definition.maximum) {
525
+ return false;
526
+ }
527
+ if (definition.exclusiveMinimum !== undefined && value <= definition.exclusiveMinimum) {
528
+ return false;
529
+ }
530
+ if (definition.exclusiveMaximum !== undefined && value >= definition.exclusiveMaximum) {
531
+ return false;
532
+ }
533
+ if (definition.multipleOf !== undefined && value % definition.multipleOf !== 0) {
534
+ return false;
535
+ }
536
+ }
537
+
538
+ // Additional constraints for arrays
539
+ if (definition.type === 'array') {
540
+ if (definition.minItems !== undefined && value.length < definition.minItems) {
541
+ return false;
542
+ }
543
+ if (definition.maxItems !== undefined && value.length > definition.maxItems) {
544
+ return false;
545
+ }
546
+ if (definition.uniqueItems && new Set(value).size !== value.length) {
547
+ return false;
548
+ }
549
+ // Validate array items if schema is provided
550
+ if (definition.items) {
551
+ for (const item of value) {
552
+ const itemValidator = createSchemaValidators({ properties: { item: definition.items } });
553
+ if (itemValidator.item && !itemValidator.item.validate(item)) {
554
+ return false;
555
+ }
556
+ }
557
+ }
558
+ }
559
+
560
+ // Additional constraints for objects
561
+ if (definition.type === 'object' && definition.properties) {
562
+ const nestedValidators = createSchemaValidators(definition);
563
+ for (const [nestedKey, nestedValidator] of Object.entries(nestedValidators)) {
564
+ if (value[nestedKey] !== undefined && !nestedValidator.validate(value[nestedKey])) {
565
+ return false;
566
+ }
567
+ if (nestedValidator.required && value[nestedKey] === undefined) {
568
+ return false;
569
+ }
570
+ }
571
+ }
572
+
573
+ // Check enum
574
+ if (definition.enum && !definition.enum.includes(value)) {
575
+ return false;
576
+ }
577
+
578
+ return true;
579
+ };
580
+
581
+ validators[key] = validator;
582
+ }
583
+
584
+ return validators;
585
+ }
586
+
587
+ module.exports = {
588
+ loadSchema,
589
+ createValidationHook,
590
+ applySchemaValidation,
591
+ createValidationHooks,
592
+ checkType,
593
+ createSchemaValidators // Add this line
594
+ };