jtcsv 2.2.8 → 3.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/README.md +204 -115
- package/bin/jtcsv.ts +2612 -0
- package/browser.d.ts +142 -0
- package/dist/benchmark.js +446 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/bin/jtcsv.js +1940 -0
- package/dist/bin/jtcsv.js.map +1 -0
- package/dist/csv-to-json.js +1262 -0
- package/dist/csv-to-json.js.map +1 -0
- package/dist/errors.js +291 -0
- package/dist/errors.js.map +1 -0
- package/dist/eslint.config.js +147 -0
- package/dist/eslint.config.js.map +1 -0
- package/dist/index-core.js +95 -0
- package/dist/index-core.js.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/json-save.js +229 -0
- package/dist/json-save.js.map +1 -0
- package/dist/json-to-csv.js +576 -0
- package/dist/json-to-csv.js.map +1 -0
- package/dist/jtcsv-core.cjs.js +1736 -0
- package/dist/jtcsv-core.cjs.js.map +1 -0
- package/dist/jtcsv-core.esm.js +1708 -0
- package/dist/jtcsv-core.esm.js.map +1 -0
- package/dist/jtcsv-core.umd.js +1742 -0
- package/dist/jtcsv-core.umd.js.map +1 -0
- package/dist/jtcsv-full.cjs.js +2241 -0
- package/dist/jtcsv-full.cjs.js.map +1 -0
- package/dist/jtcsv-full.esm.js +2209 -0
- package/dist/jtcsv-full.esm.js.map +1 -0
- package/dist/jtcsv-full.umd.js +2247 -0
- package/dist/jtcsv-full.umd.js.map +1 -0
- package/dist/jtcsv-workers.esm.js +768 -0
- package/dist/jtcsv-workers.esm.js.map +1 -0
- package/dist/jtcsv-workers.umd.js +782 -0
- package/dist/jtcsv-workers.umd.js.map +1 -0
- package/dist/jtcsv.cjs.js +1996 -2048
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +1992 -2048
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +2157 -2209
- package/dist/jtcsv.umd.js.map +1 -1
- package/dist/plugins/express-middleware/index.js +350 -0
- package/dist/plugins/express-middleware/index.js.map +1 -0
- package/dist/plugins/fastify-plugin/index.js +315 -0
- package/dist/plugins/fastify-plugin/index.js.map +1 -0
- package/dist/plugins/hono/index.js +111 -0
- package/dist/plugins/hono/index.js.map +1 -0
- package/dist/plugins/nestjs/index.js +112 -0
- package/dist/plugins/nestjs/index.js.map +1 -0
- package/dist/plugins/nuxt/index.js +53 -0
- package/dist/plugins/nuxt/index.js.map +1 -0
- package/dist/plugins/remix/index.js +133 -0
- package/dist/plugins/remix/index.js.map +1 -0
- package/dist/plugins/sveltekit/index.js +155 -0
- package/dist/plugins/sveltekit/index.js.map +1 -0
- package/dist/plugins/trpc/index.js +136 -0
- package/dist/plugins/trpc/index.js.map +1 -0
- package/dist/run-demo.js +49 -0
- package/dist/run-demo.js.map +1 -0
- package/dist/src/browser/browser-functions.js +193 -0
- package/dist/src/browser/browser-functions.js.map +1 -0
- package/dist/src/browser/core.js +123 -0
- package/dist/src/browser/core.js.map +1 -0
- package/dist/src/browser/csv-to-json-browser.js +353 -0
- package/dist/src/browser/csv-to-json-browser.js.map +1 -0
- package/dist/src/browser/errors-browser.js +219 -0
- package/dist/src/browser/errors-browser.js.map +1 -0
- package/dist/src/browser/extensions/plugins.js +106 -0
- package/dist/src/browser/extensions/plugins.js.map +1 -0
- package/dist/src/browser/extensions/workers.js +66 -0
- package/dist/src/browser/extensions/workers.js.map +1 -0
- package/dist/src/browser/index.js +140 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/browser/json-to-csv-browser.js +225 -0
- package/dist/src/browser/json-to-csv-browser.js.map +1 -0
- package/dist/src/browser/streams.js +340 -0
- package/dist/src/browser/streams.js.map +1 -0
- package/dist/src/browser/workers/csv-parser.worker.js +264 -0
- package/dist/src/browser/workers/csv-parser.worker.js.map +1 -0
- package/dist/src/browser/workers/worker-pool.js +338 -0
- package/dist/src/browser/workers/worker-pool.js.map +1 -0
- package/dist/src/core/delimiter-cache.js +196 -0
- package/dist/src/core/delimiter-cache.js.map +1 -0
- package/dist/src/core/node-optimizations.js +279 -0
- package/dist/src/core/node-optimizations.js.map +1 -0
- package/dist/src/core/plugin-system.js +399 -0
- package/dist/src/core/plugin-system.js.map +1 -0
- package/dist/src/core/transform-hooks.js +348 -0
- package/dist/src/core/transform-hooks.js.map +1 -0
- package/dist/src/engines/fast-path-engine-new.js +262 -0
- package/dist/src/engines/fast-path-engine-new.js.map +1 -0
- package/dist/src/engines/fast-path-engine.js +671 -0
- package/dist/src/engines/fast-path-engine.js.map +1 -0
- package/dist/src/errors.js +18 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/formats/ndjson-parser.js +332 -0
- package/dist/src/formats/ndjson-parser.js.map +1 -0
- package/dist/src/formats/tsv-parser.js +230 -0
- package/dist/src/formats/tsv-parser.js.map +1 -0
- package/dist/src/index-with-plugins.js +259 -0
- package/dist/src/index-with-plugins.js.map +1 -0
- package/dist/src/types/index.js +3 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/utils/bom-utils.js +267 -0
- package/dist/src/utils/bom-utils.js.map +1 -0
- package/dist/src/utils/encoding-support.js +77 -0
- package/dist/src/utils/encoding-support.js.map +1 -0
- package/dist/src/utils/schema-validator.js +609 -0
- package/dist/src/utils/schema-validator.js.map +1 -0
- package/dist/src/utils/transform-loader.js +281 -0
- package/dist/src/utils/transform-loader.js.map +1 -0
- package/dist/src/utils/validators.js +40 -0
- package/dist/src/utils/validators.js.map +1 -0
- package/dist/src/utils/zod-adapter.js +144 -0
- package/dist/src/utils/zod-adapter.js.map +1 -0
- package/dist/src/web-server/index.js +648 -0
- package/dist/src/web-server/index.js.map +1 -0
- package/dist/src/workers/csv-multithreaded.js +211 -0
- package/dist/src/workers/csv-multithreaded.js.map +1 -0
- package/dist/src/workers/csv-parser.worker.js +179 -0
- package/dist/src/workers/csv-parser.worker.js.map +1 -0
- package/dist/src/workers/worker-pool.js +228 -0
- package/dist/src/workers/worker-pool.js.map +1 -0
- package/dist/stream-csv-to-json.js +665 -0
- package/dist/stream-csv-to-json.js.map +1 -0
- package/dist/stream-json-to-csv.js +389 -0
- package/dist/stream-json-to-csv.js.map +1 -0
- package/examples/advanced/conditional-transformations.ts +446 -0
- package/examples/advanced/csv-parser.worker.ts +89 -0
- package/examples/advanced/nested-objects-example.ts +306 -0
- package/examples/advanced/performance-optimization.ts +504 -0
- package/examples/advanced/run-demo-server.ts +116 -0
- package/examples/advanced/web-worker-usage.html +874 -0
- package/examples/async-multithreaded-example.ts +335 -0
- package/examples/cli-advanced-usage.md +290 -0
- package/examples/{cli-batch-processing.js → cli-batch-processing.ts} +38 -38
- package/examples/{cli-tool.js → cli-tool.ts} +5 -8
- package/examples/{error-handling.js → error-handling.ts} +356 -324
- package/examples/{express-api.js → express-api.ts} +161 -164
- package/examples/{large-dataset-example.js → large-dataset-example.ts} +201 -182
- package/examples/{ndjson-processing.js → ndjson-processing.ts} +456 -434
- package/examples/{plugin-excel-exporter.js → plugin-excel-exporter.ts} +6 -7
- package/examples/react-integration.tsx +637 -0
- package/examples/{schema-validation.js → schema-validation.ts} +2 -2
- package/examples/simple-usage.ts +194 -0
- package/examples/{streaming-example.js → streaming-example.ts} +12 -12
- package/index.d.ts +187 -18
- package/package.json +75 -81
- package/plugins.d.ts +37 -0
- package/schema.d.ts +103 -0
- package/src/browser/browser-functions.ts +402 -0
- package/src/browser/core.ts +152 -0
- package/src/browser/csv-to-json-browser.d.ts +3 -0
- package/src/browser/csv-to-json-browser.ts +494 -0
- package/src/browser/{errors-browser.js → errors-browser.ts} +305 -197
- package/src/browser/extensions/plugins.ts +93 -0
- package/src/browser/extensions/workers.ts +39 -0
- package/src/browser/globals.d.ts +5 -0
- package/src/browser/index.ts +192 -0
- package/src/browser/json-to-csv-browser.d.ts +3 -0
- package/src/browser/json-to-csv-browser.ts +338 -0
- package/src/browser/streams.ts +403 -0
- package/src/browser/workers/{csv-parser.worker.js → csv-parser.worker.ts} +3 -3
- package/src/browser/workers/{worker-pool.js → worker-pool.ts} +51 -30
- package/src/core/delimiter-cache.ts +320 -0
- package/src/core/{node-optimizations.js → node-optimizations.ts} +448 -407
- package/src/core/plugin-system.ts +588 -0
- package/src/core/transform-hooks.ts +566 -0
- package/src/engines/{fast-path-engine-new.js → fast-path-engine-new.ts} +11 -2
- package/src/engines/{fast-path-engine.js → fast-path-engine.ts} +79 -53
- package/src/errors.ts +1 -0
- package/src/formats/{ndjson-parser.js → ndjson-parser.ts} +24 -16
- package/src/formats/{tsv-parser.js → tsv-parser.ts} +18 -17
- package/src/{index-with-plugins.js → index-with-plugins.ts} +381 -357
- package/src/types/index.ts +275 -0
- package/src/utils/bom-utils.ts +373 -0
- package/src/utils/encoding-support.ts +155 -0
- package/src/utils/{schema-validator.js → schema-validator.ts} +814 -589
- package/src/utils/transform-loader.ts +389 -0
- package/src/utils/validators.ts +35 -0
- package/src/utils/zod-adapter.ts +280 -0
- package/src/web-server/{index.js → index.ts} +19 -19
- package/src/workers/csv-multithreaded.ts +310 -0
- package/src/workers/csv-parser.worker.ts +227 -0
- package/src/workers/worker-pool.ts +409 -0
- package/bin/jtcsv.js +0 -2462
- package/csv-to-json.js +0 -688
- package/errors.js +0 -208
- package/examples/simple-usage.js +0 -282
- package/index.js +0 -68
- package/json-save.js +0 -254
- package/json-to-csv.js +0 -526
- package/plugins/README.md +0 -91
- package/plugins/express-middleware/README.md +0 -64
- package/plugins/express-middleware/example.js +0 -136
- package/plugins/express-middleware/index.d.ts +0 -114
- package/plugins/express-middleware/index.js +0 -360
- package/plugins/express-middleware/package.json +0 -52
- package/plugins/fastify-plugin/index.js +0 -406
- package/plugins/fastify-plugin/package.json +0 -55
- package/plugins/hono/README.md +0 -28
- package/plugins/hono/index.d.ts +0 -12
- package/plugins/hono/index.js +0 -36
- package/plugins/hono/package.json +0 -35
- package/plugins/nestjs/README.md +0 -35
- package/plugins/nestjs/index.d.ts +0 -25
- package/plugins/nestjs/index.js +0 -77
- package/plugins/nestjs/package.json +0 -37
- package/plugins/nextjs-api/README.md +0 -57
- package/plugins/nextjs-api/examples/ConverterComponent.jsx +0 -386
- package/plugins/nextjs-api/examples/api-convert.js +0 -69
- package/plugins/nextjs-api/index.js +0 -387
- package/plugins/nextjs-api/package.json +0 -63
- package/plugins/nextjs-api/route.js +0 -371
- package/plugins/nuxt/README.md +0 -24
- package/plugins/nuxt/index.js +0 -21
- package/plugins/nuxt/package.json +0 -35
- package/plugins/nuxt/runtime/composables/useJtcsv.js +0 -6
- package/plugins/nuxt/runtime/plugin.js +0 -6
- package/plugins/remix/README.md +0 -26
- package/plugins/remix/index.d.ts +0 -16
- package/plugins/remix/index.js +0 -62
- package/plugins/remix/package.json +0 -35
- package/plugins/sveltekit/README.md +0 -28
- package/plugins/sveltekit/index.d.ts +0 -17
- package/plugins/sveltekit/index.js +0 -54
- package/plugins/sveltekit/package.json +0 -33
- package/plugins/trpc/README.md +0 -25
- package/plugins/trpc/index.d.ts +0 -7
- package/plugins/trpc/index.js +0 -32
- package/plugins/trpc/package.json +0 -34
- package/src/browser/browser-functions.js +0 -219
- package/src/browser/csv-to-json-browser.js +0 -700
- package/src/browser/index.js +0 -113
- package/src/browser/json-to-csv-browser.js +0 -309
- package/src/browser/streams.js +0 -393
- package/src/core/delimiter-cache.js +0 -186
- package/src/core/plugin-system.js +0 -476
- package/src/core/transform-hooks.js +0 -350
- package/src/errors.js +0 -26
- package/src/utils/transform-loader.js +0 -205
- package/stream-csv-to-json.js +0 -542
- package/stream-json-to-csv.js +0 -464
- /package/examples/{web-workers-advanced.js → web-workers-advanced.ts} +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Loader Utility
|
|
3
|
+
*
|
|
4
|
+
* Utility for loading and applying transform functions from JavaScript files
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as fsPromises from 'fs/promises';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as vm from 'vm';
|
|
11
|
+
import {
|
|
12
|
+
ValidationError,
|
|
13
|
+
SecurityError,
|
|
14
|
+
ConfigurationError
|
|
15
|
+
} from '../errors';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validates transform function
|
|
19
|
+
*/
|
|
20
|
+
function validateTransformFunction(fn: Function): boolean {
|
|
21
|
+
if (typeof fn !== 'function') {
|
|
22
|
+
throw new ValidationError('Transform must export a function');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check function arity (should accept 1-2 parameters)
|
|
26
|
+
const functionString = fn.toString();
|
|
27
|
+
const paramMatch = functionString.match(/\(([^)]*)\)/);
|
|
28
|
+
if (paramMatch) {
|
|
29
|
+
const params = paramMatch[1].split(',').map(p => p.trim()).filter(p => p);
|
|
30
|
+
if (params.length === 0 || params.length > 2) {
|
|
31
|
+
throw new ValidationError('Transform function should accept 1-2 parameters: (row, index)');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Loads transform function from a JavaScript file
|
|
40
|
+
*
|
|
41
|
+
* @param transformPath - Path to JavaScript file with transform function
|
|
42
|
+
* @returns Transform function
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // transform.js
|
|
46
|
+
* module.exports = function(row, index) {
|
|
47
|
+
* return { ...row, processed: true, index };
|
|
48
|
+
* };
|
|
49
|
+
*
|
|
50
|
+
* // Usage
|
|
51
|
+
* const transform = loadTransform('./transform.js');
|
|
52
|
+
* const result = transform({ id: 1, name: 'John' }, 0);
|
|
53
|
+
*/
|
|
54
|
+
export function loadTransform(transformPath: string): Function {
|
|
55
|
+
if (!transformPath || typeof transformPath !== 'string') {
|
|
56
|
+
throw new ValidationError('Transform path must be a string');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate file path
|
|
60
|
+
const safePath = path.resolve(transformPath);
|
|
61
|
+
|
|
62
|
+
// Prevent directory traversal
|
|
63
|
+
const normalizedPath = path.normalize(transformPath);
|
|
64
|
+
if (normalizedPath.includes('..') ||
|
|
65
|
+
/\\\.\.\\|\/\.\.\//.test(transformPath) ||
|
|
66
|
+
transformPath.startsWith('..') ||
|
|
67
|
+
transformPath.includes('/..')) {
|
|
68
|
+
throw new SecurityError('Directory traversal detected in transform file path');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check file exists and has .js extension
|
|
72
|
+
if (!fs.existsSync(safePath)) {
|
|
73
|
+
throw new ValidationError(`Transform file not found: ${transformPath}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!safePath.toLowerCase().endsWith('.js')) {
|
|
77
|
+
throw new ValidationError('Transform file must have .js extension');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Read and evaluate the transform file in a safe context
|
|
82
|
+
const transformCode = fs.readFileSync(safePath, 'utf8');
|
|
83
|
+
|
|
84
|
+
// Create a safe context with limited access
|
|
85
|
+
const sandbox = {
|
|
86
|
+
console,
|
|
87
|
+
require,
|
|
88
|
+
module: { exports: {} },
|
|
89
|
+
exports: {},
|
|
90
|
+
__filename: safePath,
|
|
91
|
+
__dirname: path.dirname(safePath),
|
|
92
|
+
Buffer,
|
|
93
|
+
process: {
|
|
94
|
+
env: process.env,
|
|
95
|
+
cwd: process.cwd,
|
|
96
|
+
platform: process.platform
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Create a context and run the code
|
|
101
|
+
const context = vm.createContext(sandbox);
|
|
102
|
+
const script = new vm.Script(transformCode, { filename: safePath });
|
|
103
|
+
script.runInContext(context);
|
|
104
|
+
|
|
105
|
+
// Get the exported function
|
|
106
|
+
const transformFn = (context as any).module.exports || (context as any).exports;
|
|
107
|
+
|
|
108
|
+
// Handle default export for ES6 modules
|
|
109
|
+
const finalTransform = transformFn.default || transformFn;
|
|
110
|
+
|
|
111
|
+
// Validate the transform function
|
|
112
|
+
validateTransformFunction(finalTransform);
|
|
113
|
+
|
|
114
|
+
return finalTransform;
|
|
115
|
+
} catch (error: any) {
|
|
116
|
+
if (error instanceof ValidationError || error instanceof SecurityError) {
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (error.code === 'EACCES') {
|
|
121
|
+
throw new SecurityError(`Permission denied reading transform file: ${transformPath}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
throw new ValidationError(`Failed to load transform function: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates a transform hook for use with csvToJson/jsonToCsv hooks system
|
|
130
|
+
*
|
|
131
|
+
* @param transform - Transform function or path to transform file
|
|
132
|
+
* @returns Transform hook function
|
|
133
|
+
*/
|
|
134
|
+
export function createTransformHook(transform: string | Function): (row: any, index: number, context: any) => any {
|
|
135
|
+
let transformFn: Function;
|
|
136
|
+
|
|
137
|
+
if (typeof transform === 'string') {
|
|
138
|
+
// Load transform from file
|
|
139
|
+
transformFn = loadTransform(transform);
|
|
140
|
+
} else if (typeof transform === 'function') {
|
|
141
|
+
// Use provided function
|
|
142
|
+
validateTransformFunction(transform);
|
|
143
|
+
transformFn = transform;
|
|
144
|
+
} else {
|
|
145
|
+
throw new ValidationError('Transform must be a function or a path to a JavaScript file');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Return a hook function compatible with hooks.perRow
|
|
149
|
+
return function (row: any, index: number, context: any): any {
|
|
150
|
+
try {
|
|
151
|
+
return transformFn(row, index);
|
|
152
|
+
} catch (error: any) {
|
|
153
|
+
// Log error but don't crash - return original row
|
|
154
|
+
console.error(`Transform error at row ${index}: ${error.message}`);
|
|
155
|
+
if (process.env['NODE_ENV'] === 'development') {
|
|
156
|
+
console.error(error.stack);
|
|
157
|
+
}
|
|
158
|
+
return row;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Applies transform to data array
|
|
165
|
+
*
|
|
166
|
+
* @param data - Array of data to transform
|
|
167
|
+
* @param transform - Transform function or path to transform file
|
|
168
|
+
* @returns Transformed data
|
|
169
|
+
*/
|
|
170
|
+
export function applyTransform(data: any[], transform: string | Function): any[] {
|
|
171
|
+
if (!Array.isArray(data)) {
|
|
172
|
+
throw new ValidationError('Data must be an array');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const transformHook = createTransformHook(transform);
|
|
176
|
+
|
|
177
|
+
return data.map((row, index) => {
|
|
178
|
+
return transformHook(row, index, { operation: 'applyTransform' });
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Creates a TransformHooks instance with transform function
|
|
184
|
+
*
|
|
185
|
+
* @param transform - Transform function or path to transform file
|
|
186
|
+
* @returns TransformHooks instance
|
|
187
|
+
*/
|
|
188
|
+
export function createTransformHooksWithTransform(transform: string | Function): any {
|
|
189
|
+
const { TransformHooks } = require('../core/transform-hooks');
|
|
190
|
+
const hooks = new TransformHooks();
|
|
191
|
+
|
|
192
|
+
const transformHook = createTransformHook(transform);
|
|
193
|
+
hooks.perRow(transformHook);
|
|
194
|
+
|
|
195
|
+
return hooks;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Async version of loadTransform that reads file asynchronously
|
|
200
|
+
*
|
|
201
|
+
* @param transformPath - Path to JavaScript file with transform function
|
|
202
|
+
* @returns Promise with transform function
|
|
203
|
+
*/
|
|
204
|
+
export async function loadTransformAsync(transformPath: string): Promise<Function> {
|
|
205
|
+
if (!transformPath || typeof transformPath !== 'string') {
|
|
206
|
+
throw new ValidationError('Transform path must be a string');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Validate file path
|
|
210
|
+
const safePath = path.resolve(transformPath);
|
|
211
|
+
|
|
212
|
+
// Prevent directory traversal
|
|
213
|
+
const normalizedPath = path.normalize(transformPath);
|
|
214
|
+
if (normalizedPath.includes('..') ||
|
|
215
|
+
/\\\.\.\\|\/\.\.\//.test(transformPath) ||
|
|
216
|
+
transformPath.startsWith('..') ||
|
|
217
|
+
transformPath.includes('/..')) {
|
|
218
|
+
throw new SecurityError('Directory traversal detected in transform file path');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check file exists and has .js extension
|
|
222
|
+
try {
|
|
223
|
+
await fsPromises.access(safePath);
|
|
224
|
+
} catch {
|
|
225
|
+
throw new ValidationError(`Transform file not found: ${transformPath}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!safePath.toLowerCase().endsWith('.js')) {
|
|
229
|
+
throw new ValidationError('Transform file must have .js extension');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
// Read and evaluate the transform file in a safe context
|
|
234
|
+
const transformCode = await fsPromises.readFile(safePath, 'utf8');
|
|
235
|
+
|
|
236
|
+
// Create a safe context with limited access
|
|
237
|
+
const sandbox = {
|
|
238
|
+
console,
|
|
239
|
+
require,
|
|
240
|
+
module: { exports: {} },
|
|
241
|
+
exports: {},
|
|
242
|
+
__filename: safePath,
|
|
243
|
+
__dirname: path.dirname(safePath),
|
|
244
|
+
Buffer,
|
|
245
|
+
process: {
|
|
246
|
+
env: process.env,
|
|
247
|
+
cwd: process.cwd,
|
|
248
|
+
platform: process.platform
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Create a context and run the code
|
|
253
|
+
const context = vm.createContext(sandbox);
|
|
254
|
+
const script = new vm.Script(transformCode, { filename: safePath });
|
|
255
|
+
script.runInContext(context);
|
|
256
|
+
|
|
257
|
+
// Get the exported function
|
|
258
|
+
const transformFn = (context as any).module.exports || (context as any).exports;
|
|
259
|
+
|
|
260
|
+
// Handle default export for ES6 modules
|
|
261
|
+
const finalTransform = transformFn.default || transformFn;
|
|
262
|
+
|
|
263
|
+
// Validate the transform function
|
|
264
|
+
validateTransformFunction(finalTransform);
|
|
265
|
+
|
|
266
|
+
return finalTransform;
|
|
267
|
+
} catch (error: any) {
|
|
268
|
+
if (error instanceof ValidationError || error instanceof SecurityError) {
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (error.code === 'EACCES') {
|
|
273
|
+
throw new SecurityError(`Permission denied reading transform file: ${transformPath}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
throw new ValidationError(`Failed to load transform function: ${error.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Async version of applyTransform that uses worker threads for parallel transformation
|
|
282
|
+
*
|
|
283
|
+
* @param data - Array of data to transform
|
|
284
|
+
* @param transform - Transform function or path to transform file
|
|
285
|
+
* @returns Promise with transformed data
|
|
286
|
+
*/
|
|
287
|
+
export async function applyTransformAsync(data: any[], transform: string | Function): Promise<any[]> {
|
|
288
|
+
if (!Array.isArray(data)) {
|
|
289
|
+
throw new ValidationError('Data must be an array');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// For large datasets, use worker pool
|
|
293
|
+
if (data.length > 1000) {
|
|
294
|
+
const { createWorkerPool } = require('../workers/worker-pool');
|
|
295
|
+
const pool = createWorkerPool({
|
|
296
|
+
workerCount: Math.min(4, require('os').cpus().length),
|
|
297
|
+
workerScript: require.resolve('./transform-worker.js')
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// Load transform function
|
|
302
|
+
const transformFn = typeof transform === 'string'
|
|
303
|
+
? await loadTransformAsync(transform)
|
|
304
|
+
: transform;
|
|
305
|
+
|
|
306
|
+
// Execute transforms in parallel
|
|
307
|
+
const transformPromises = data.map((row, index) =>
|
|
308
|
+
pool.execute({ row, index, transform: transformFn.toString() })
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const results = await Promise.all(transformPromises);
|
|
312
|
+
return results.map(result => result.transformedRow);
|
|
313
|
+
} finally {
|
|
314
|
+
await pool.terminate();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// For small datasets, transform synchronously
|
|
319
|
+
return applyTransform(data, transform);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Creates an async transform hook that can be used with async hooks
|
|
324
|
+
*
|
|
325
|
+
* @param transform - Transform function or path to transform file
|
|
326
|
+
* @returns Async transform hook function
|
|
327
|
+
*/
|
|
328
|
+
export function createAsyncTransformHook(transform: string | Function): (row: any, index: number, context: any) => Promise<any> {
|
|
329
|
+
const syncHook = createTransformHook(transform);
|
|
330
|
+
|
|
331
|
+
return async function (row: any, index: number, context: any): Promise<any> {
|
|
332
|
+
return Promise.resolve(syncHook(row, index, context));
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Validates that a transform function can be safely executed
|
|
338
|
+
*
|
|
339
|
+
* @param transformFn - Transform function to validate
|
|
340
|
+
* @returns Validation result
|
|
341
|
+
*/
|
|
342
|
+
export function validateTransformSafety(transformFn: Function): { safe: boolean; issues: string[] } {
|
|
343
|
+
const issues: string[] = [];
|
|
344
|
+
|
|
345
|
+
// Check for dangerous patterns
|
|
346
|
+
const functionString = transformFn.toString().toLowerCase();
|
|
347
|
+
|
|
348
|
+
const dangerousPatterns = [
|
|
349
|
+
'eval(',
|
|
350
|
+
'new function',
|
|
351
|
+
'settimeout',
|
|
352
|
+
'setinterval',
|
|
353
|
+
'process.exit',
|
|
354
|
+
'require(',
|
|
355
|
+
'fs.',
|
|
356
|
+
'child_process',
|
|
357
|
+
'exec(',
|
|
358
|
+
'spawn(',
|
|
359
|
+
'vm.run'
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
for (const pattern of dangerousPatterns) {
|
|
363
|
+
if (functionString.includes(pattern)) {
|
|
364
|
+
issues.push(`Potentially dangerous pattern detected: ${pattern}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check for infinite loops
|
|
369
|
+
if (functionString.includes('while(true)') || functionString.includes('for(;;)')) {
|
|
370
|
+
issues.push('Potential infinite loop detected');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
safe: issues.length === 0,
|
|
375
|
+
issues
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export default {
|
|
380
|
+
loadTransform,
|
|
381
|
+
loadTransformAsync,
|
|
382
|
+
createTransformHook,
|
|
383
|
+
createAsyncTransformHook,
|
|
384
|
+
applyTransform,
|
|
385
|
+
applyTransformAsync,
|
|
386
|
+
createTransformHooksWithTransform,
|
|
387
|
+
validateTransformFunction,
|
|
388
|
+
validateTransformSafety
|
|
389
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function isEmail(value: string): boolean {
|
|
2
|
+
if (typeof value !== 'string') {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isUrl(value: string): boolean {
|
|
9
|
+
if (typeof value !== 'string') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
new URL(value);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isDate(value: string | Date): boolean {
|
|
21
|
+
if (value instanceof Date) {
|
|
22
|
+
return !isNaN(value.getTime());
|
|
23
|
+
}
|
|
24
|
+
if (typeof value !== 'string') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const date = new Date(value);
|
|
28
|
+
return !isNaN(date.getTime());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const validators = {
|
|
32
|
+
isEmail,
|
|
33
|
+
isUrl,
|
|
34
|
+
isDate
|
|
35
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod adapter for JTCSV schema validation.
|
|
3
|
+
*
|
|
4
|
+
* Provides integration with Zod schemas for CSV validation.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const { z } = require('zod');
|
|
8
|
+
* const { createZodValidationHook } = require('./zod-adapter');
|
|
9
|
+
*
|
|
10
|
+
* const schema = z.object({
|
|
11
|
+
* name: z.string().min(1),
|
|
12
|
+
* age: z.number().int().min(0).max(150),
|
|
13
|
+
* email: z.string().email()
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* const validationHook = createZodValidationHook(schema);
|
|
17
|
+
*
|
|
18
|
+
* // Use with csvToJson
|
|
19
|
+
* const data = await csvToJson(csv, {
|
|
20
|
+
* hooks: { perRow: validationHook }
|
|
21
|
+
* });
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { ValidationError } from '../errors';
|
|
25
|
+
|
|
26
|
+
// Conditional imports for optional dependencies
|
|
27
|
+
type ZodSchema = any;
|
|
28
|
+
type YupSchema = any;
|
|
29
|
+
|
|
30
|
+
export interface ZodValidationOptions {
|
|
31
|
+
coerce?: boolean;
|
|
32
|
+
mode?: 'strict' | 'collect';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface YupValidationOptions {
|
|
36
|
+
abortEarly?: boolean;
|
|
37
|
+
stripUnknown?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ValidatedParserOptions {
|
|
41
|
+
library?: 'zod' | 'yup';
|
|
42
|
+
[key: string]: any;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type RowHook = (row: any, index: number, context: any) => any | Promise<any>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a validation hook from a Zod schema.
|
|
49
|
+
*
|
|
50
|
+
* @param zodSchema - Zod schema instance
|
|
51
|
+
* @param options - Validation options
|
|
52
|
+
* @param options.coerce - Whether to coerce values according to Zod's coerce (default: true)
|
|
53
|
+
* @param options.mode - 'strict' (throw on first error) or 'collect' (collect all errors)
|
|
54
|
+
* @returns Validation hook compatible with JTCSV hooks.perRow
|
|
55
|
+
*/
|
|
56
|
+
export function createZodValidationHook(
|
|
57
|
+
zodSchema: ZodSchema,
|
|
58
|
+
options: ZodValidationOptions = {}
|
|
59
|
+
): RowHook {
|
|
60
|
+
const { coerce = true, mode = 'strict' } = options;
|
|
61
|
+
|
|
62
|
+
// Check if Zod is available
|
|
63
|
+
let zod: any;
|
|
64
|
+
try {
|
|
65
|
+
zod = require('zod');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Zod is not installed. Please install zod: npm install zod'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Ensure the passed schema is a Zod schema
|
|
73
|
+
if (!zodSchema || typeof zodSchema.safeParse !== 'function') {
|
|
74
|
+
throw new ValidationError('Provided schema is not a valid Zod schema');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Return hook function
|
|
78
|
+
return function (row: any, index: number, context: any): any {
|
|
79
|
+
try {
|
|
80
|
+
const result = zodSchema.safeParse(row);
|
|
81
|
+
|
|
82
|
+
if (!result.success) {
|
|
83
|
+
const errors = result.error.errors;
|
|
84
|
+
const firstError = errors[0];
|
|
85
|
+
const path = firstError.path?.join('.') || '';
|
|
86
|
+
const message = firstError.message;
|
|
87
|
+
|
|
88
|
+
if (mode === 'strict') {
|
|
89
|
+
throw new ValidationError(
|
|
90
|
+
`Row ${index + 1}: ${path ? `Field "${path}": ` : ''}${message}`
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
// In collect mode, we attach errors to row metadata
|
|
94
|
+
// For simplicity, we still throw but could be extended
|
|
95
|
+
console.warn(`Row ${index + 1}: ${path ? `Field "${path}": ` : ''}${message}`);
|
|
96
|
+
return row;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Return validated (and possibly coerced) data
|
|
101
|
+
return result.data;
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
if (error instanceof ValidationError) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
// Unexpected error - log and return original row
|
|
107
|
+
console.error(`Zod validation error at row ${index}: ${error.message}`);
|
|
108
|
+
if (process.env['NODE_ENV'] === 'development') {
|
|
109
|
+
console.error(error.stack);
|
|
110
|
+
}
|
|
111
|
+
return row;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates a Yup validation hook.
|
|
118
|
+
*
|
|
119
|
+
* @param yupSchema - Yup schema instance
|
|
120
|
+
* @param options - Validation options
|
|
121
|
+
* @returns Validation hook
|
|
122
|
+
*/
|
|
123
|
+
export function createYupValidationHook(
|
|
124
|
+
yupSchema: YupSchema,
|
|
125
|
+
options: YupValidationOptions = {}
|
|
126
|
+
): RowHook {
|
|
127
|
+
const { abortEarly = false, stripUnknown = true } = options;
|
|
128
|
+
|
|
129
|
+
// Check if Yup is available
|
|
130
|
+
let yup: any;
|
|
131
|
+
try {
|
|
132
|
+
yup = require('yup');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
'Yup is not installed. Please install yup: npm install yup'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!yupSchema || typeof yupSchema.validate !== 'function') {
|
|
140
|
+
throw new ValidationError('Provided schema is not a valid Yup schema');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return async function (row: any, index: number, context: any): Promise<any> {
|
|
144
|
+
try {
|
|
145
|
+
const validated = await yupSchema.validate(row, { abortEarly, stripUnknown });
|
|
146
|
+
return validated;
|
|
147
|
+
} catch (error: any) {
|
|
148
|
+
if (error.name === 'ValidationError') {
|
|
149
|
+
throw new ValidationError(`Row ${index + 1}: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
console.error(`Yup validation error at row ${index}: ${error.message}`);
|
|
152
|
+
return row;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Higher-order function that creates a csvToJson wrapper with schema validation.
|
|
159
|
+
*
|
|
160
|
+
* @param schema - Zod or Yup schema
|
|
161
|
+
* @param adapterOptions - Adapter-specific options
|
|
162
|
+
* @returns Function that takes csv and options, returns validated data
|
|
163
|
+
*/
|
|
164
|
+
export function createValidatedParser(
|
|
165
|
+
schema: ZodSchema | YupSchema,
|
|
166
|
+
adapterOptions: ValidatedParserOptions = {}
|
|
167
|
+
): (csv: string, parseOptions?: any) => Promise<any[]> {
|
|
168
|
+
const { library = 'zod', ...options } = adapterOptions;
|
|
169
|
+
|
|
170
|
+
let validationHook: RowHook;
|
|
171
|
+
if (library === 'zod') {
|
|
172
|
+
validationHook = createZodValidationHook(schema as ZodSchema, options);
|
|
173
|
+
} else if (library === 'yup') {
|
|
174
|
+
validationHook = createYupValidationHook(schema as YupSchema, options);
|
|
175
|
+
} else {
|
|
176
|
+
throw new ValidationError(`Unsupported validation library: ${library}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return async function (csv: string, parseOptions: any = {}): Promise<any[]> {
|
|
180
|
+
const { csvToJson } = require('../index');
|
|
181
|
+
const hooks = parseOptions.hooks || {};
|
|
182
|
+
// Merge validation hook with existing perRow hook
|
|
183
|
+
const existingPerRow = hooks.perRow;
|
|
184
|
+
hooks.perRow = function (row: any, index: number, context: any): any {
|
|
185
|
+
let validated = row;
|
|
186
|
+
if (existingPerRow) {
|
|
187
|
+
validated = existingPerRow(validated, index, context);
|
|
188
|
+
}
|
|
189
|
+
return validationHook(validated, index, context);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return csvToJson(csv, { ...parseOptions, hooks });
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Async version of createValidatedParser that uses worker threads for validation.
|
|
198
|
+
*
|
|
199
|
+
* @param schema - Zod or Yup schema
|
|
200
|
+
* @param adapterOptions - Adapter-specific options
|
|
201
|
+
* @returns Async function that validates CSV data in parallel
|
|
202
|
+
*/
|
|
203
|
+
export function createAsyncValidatedParser(
|
|
204
|
+
schema: ZodSchema | YupSchema,
|
|
205
|
+
adapterOptions: ValidatedParserOptions = {}
|
|
206
|
+
): (csv: string, parseOptions?: any) => Promise<any[]> {
|
|
207
|
+
const { library = 'zod', ...options } = adapterOptions;
|
|
208
|
+
|
|
209
|
+
return async function (csv: string, parseOptions: any = {}): Promise<any[]> {
|
|
210
|
+
const { csvToJson } = require('../index');
|
|
211
|
+
const { createWorkerPool } = require('../workers/worker-pool');
|
|
212
|
+
|
|
213
|
+
// Create worker pool for parallel validation
|
|
214
|
+
const pool = createWorkerPool({
|
|
215
|
+
workerCount: Math.min(4, require('os').cpus().length),
|
|
216
|
+
workerScript: require.resolve('./validation-worker.js')
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// Parse CSV without validation first
|
|
221
|
+
const data = await csvToJson(csv, parseOptions);
|
|
222
|
+
|
|
223
|
+
// Validate in parallel using worker pool
|
|
224
|
+
const validationPromises = data.map((row: any, index: number) =>
|
|
225
|
+
pool.execute({ row, index, schema, library, options })
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const validatedRows = await Promise.all(validationPromises);
|
|
229
|
+
return validatedRows;
|
|
230
|
+
} finally {
|
|
231
|
+
await pool.terminate();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Creates a validation hook that works asynchronously with Zod schemas.
|
|
238
|
+
*
|
|
239
|
+
* @param zodSchema - Zod schema instance
|
|
240
|
+
* @param options - Validation options
|
|
241
|
+
* @returns Async validation hook
|
|
242
|
+
*/
|
|
243
|
+
export function createAsyncZodValidationHook(
|
|
244
|
+
zodSchema: ZodSchema,
|
|
245
|
+
options: ZodValidationOptions = {}
|
|
246
|
+
): RowHook {
|
|
247
|
+
const hook = createZodValidationHook(zodSchema, options);
|
|
248
|
+
|
|
249
|
+
return async function (row: any, index: number, context: any): Promise<any> {
|
|
250
|
+
// For async compatibility, wrap in Promise
|
|
251
|
+
return Promise.resolve(hook(row, index, context));
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Creates a validation hook that works asynchronously with Yup schemas.
|
|
257
|
+
*
|
|
258
|
+
* @param yupSchema - Yup schema instance
|
|
259
|
+
* @param options - Validation options
|
|
260
|
+
* @returns Async validation hook
|
|
261
|
+
*/
|
|
262
|
+
export function createAsyncYupValidationHook(
|
|
263
|
+
yupSchema: YupSchema,
|
|
264
|
+
options: YupValidationOptions = {}
|
|
265
|
+
): RowHook {
|
|
266
|
+
const hook = createYupValidationHook(yupSchema, options);
|
|
267
|
+
|
|
268
|
+
return async function (row: any, index: number, context: any): Promise<any> {
|
|
269
|
+
return hook(row, index, context);
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export default {
|
|
274
|
+
createZodValidationHook,
|
|
275
|
+
createYupValidationHook,
|
|
276
|
+
createValidatedParser,
|
|
277
|
+
createAsyncValidatedParser,
|
|
278
|
+
createAsyncZodValidationHook,
|
|
279
|
+
createAsyncYupValidationHook
|
|
280
|
+
};
|