jtcsv 2.2.7 → 3.0.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 +31 -1
- package/bin/jtcsv.js +891 -821
- package/bin/jtcsv.ts +2534 -0
- package/csv-to-json.js +168 -145
- package/dist/jtcsv-core.cjs.js +1407 -0
- package/dist/jtcsv-core.cjs.js.map +1 -0
- package/dist/jtcsv-core.esm.js +1379 -0
- package/dist/jtcsv-core.esm.js.map +1 -0
- package/dist/jtcsv-core.umd.js +1413 -0
- package/dist/jtcsv-core.umd.js.map +1 -0
- package/dist/jtcsv-full.cjs.js +1912 -0
- package/dist/jtcsv-full.cjs.js.map +1 -0
- package/dist/jtcsv-full.esm.js +1880 -0
- package/dist/jtcsv-full.esm.js.map +1 -0
- package/dist/jtcsv-full.umd.js +1918 -0
- package/dist/jtcsv-full.umd.js.map +1 -0
- package/dist/jtcsv-workers.esm.js +759 -0
- package/dist/jtcsv-workers.esm.js.map +1 -0
- package/dist/jtcsv-workers.umd.js +773 -0
- package/dist/jtcsv-workers.umd.js.map +1 -0
- package/dist/jtcsv.cjs.js +61 -19
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +61 -19
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +61 -19
- package/dist/jtcsv.umd.js.map +1 -1
- package/errors.js +188 -2
- package/examples/advanced/conditional-transformations.js +446 -0
- package/examples/advanced/conditional-transformations.ts +446 -0
- package/examples/advanced/csv-parser.worker.js +89 -0
- package/examples/advanced/csv-parser.worker.ts +89 -0
- package/examples/advanced/nested-objects-example.js +306 -0
- package/examples/advanced/nested-objects-example.ts +306 -0
- package/examples/advanced/performance-optimization.js +504 -0
- package/examples/advanced/performance-optimization.ts +504 -0
- package/examples/advanced/run-demo-server.js +116 -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 +288 -0
- package/examples/cli-batch-processing.ts +38 -0
- package/examples/cli-tool.js +0 -3
- package/examples/cli-tool.ts +183 -0
- package/examples/error-handling.js +21 -7
- package/examples/error-handling.ts +356 -0
- package/examples/express-api.js +0 -3
- package/examples/express-api.ts +164 -0
- package/examples/large-dataset-example.js +0 -3
- package/examples/large-dataset-example.ts +204 -0
- package/examples/ndjson-processing.js +1 -1
- package/examples/ndjson-processing.ts +456 -0
- package/examples/plugin-excel-exporter.js +3 -4
- package/examples/plugin-excel-exporter.ts +406 -0
- package/examples/react-integration.tsx +637 -0
- package/examples/schema-validation.ts +640 -0
- package/examples/simple-usage.js +254 -254
- package/examples/simple-usage.ts +194 -0
- package/examples/streaming-example.js +4 -5
- package/examples/streaming-example.ts +419 -0
- package/examples/web-workers-advanced.ts +28 -0
- package/index.d.ts +1 -3
- package/index.js +15 -1
- package/json-save.js +9 -3
- package/json-to-csv.js +168 -21
- package/package.json +69 -10
- package/plugins/express-middleware/README.md +21 -2
- package/plugins/express-middleware/example.js +3 -4
- package/plugins/express-middleware/example.ts +135 -0
- package/plugins/express-middleware/index.d.ts +1 -1
- package/plugins/express-middleware/index.js +270 -118
- package/plugins/express-middleware/index.ts +557 -0
- package/plugins/fastify-plugin/index.js +2 -4
- package/plugins/fastify-plugin/index.ts +443 -0
- package/plugins/hono/index.ts +226 -0
- package/plugins/nestjs/index.ts +201 -0
- package/plugins/nextjs-api/examples/ConverterComponent.tsx +386 -0
- package/plugins/nextjs-api/examples/api-convert.js +0 -2
- package/plugins/nextjs-api/examples/api-convert.ts +67 -0
- package/plugins/nextjs-api/index.tsx +339 -0
- package/plugins/nextjs-api/route.js +2 -3
- package/plugins/nextjs-api/route.ts +370 -0
- package/plugins/nuxt/index.ts +94 -0
- package/plugins/nuxt/runtime/composables/useJtcsv.ts +100 -0
- package/plugins/nuxt/runtime/plugin.ts +71 -0
- package/plugins/remix/index.js +1 -1
- package/plugins/remix/index.ts +260 -0
- package/plugins/sveltekit/index.js +1 -1
- package/plugins/sveltekit/index.ts +301 -0
- package/plugins/trpc/index.ts +267 -0
- package/src/browser/browser-functions.ts +402 -0
- package/src/browser/core.js +92 -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.js +36 -14
- package/src/browser/csv-to-json-browser.ts +264 -0
- package/src/browser/errors-browser.ts +303 -0
- package/src/browser/extensions/plugins.js +92 -0
- package/src/browser/extensions/plugins.ts +93 -0
- package/src/browser/extensions/workers.js +39 -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.js +13 -3
- package/src/browser/json-to-csv-browser.ts +262 -0
- package/src/browser/streams.js +12 -2
- package/src/browser/streams.ts +336 -0
- package/src/browser/workers/csv-parser.worker.ts +377 -0
- package/src/browser/workers/worker-pool.ts +548 -0
- package/src/core/delimiter-cache.js +22 -8
- package/src/core/delimiter-cache.ts +310 -0
- package/src/core/node-optimizations.ts +449 -0
- package/src/core/plugin-system.js +29 -11
- package/src/core/plugin-system.ts +400 -0
- package/src/core/transform-hooks.ts +558 -0
- package/src/engines/fast-path-engine-new.ts +347 -0
- package/src/engines/fast-path-engine.ts +854 -0
- package/src/errors.ts +72 -0
- package/src/formats/ndjson-parser.ts +469 -0
- package/src/formats/tsv-parser.ts +334 -0
- package/src/index-with-plugins.js +16 -9
- package/src/index-with-plugins.ts +395 -0
- package/src/types/index.ts +255 -0
- package/src/utils/bom-utils.js +259 -0
- package/src/utils/bom-utils.ts +373 -0
- package/src/utils/encoding-support.js +124 -0
- package/src/utils/encoding-support.ts +155 -0
- package/src/utils/schema-validator.js +19 -19
- package/src/utils/schema-validator.ts +819 -0
- package/src/utils/transform-loader.js +1 -1
- package/src/utils/transform-loader.ts +389 -0
- package/src/utils/zod-adapter.js +170 -0
- package/src/utils/zod-adapter.ts +280 -0
- package/src/web-server/index.js +10 -10
- package/src/web-server/index.ts +683 -0
- 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/stream-csv-to-json.js +26 -8
- package/stream-json-to-csv.js +1 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware для JTCSV
|
|
3
|
+
* Автоматическая конвертация CSV/JSON в HTTP запросах
|
|
4
|
+
*
|
|
5
|
+
* @version 2.0.0 - TypeScript версия с асинхронной поддержкой
|
|
6
|
+
* @date 2026-01-29
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Request, Response, NextFunction } from 'express';
|
|
10
|
+
import { csvToJson, csvToJsonAsync, jsonToCsv, jsonToCsvAsync } from '../../index-core';
|
|
11
|
+
import { JtcsvError, ValidationError, SecurityError, FileSystemError } from '../../errors';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Интерфейс опций Express middleware
|
|
15
|
+
*/
|
|
16
|
+
export interface JtcsvExpressMiddlewareOptions {
|
|
17
|
+
/** Максимальный размер тела запроса (default: '10mb') */
|
|
18
|
+
maxSize?: string;
|
|
19
|
+
/** Максимальный размер файла (например, '500MB', default: '500MB') */
|
|
20
|
+
maxFileSize?: string;
|
|
21
|
+
/** Максимальный размер поля в байтах (default: 1MB) */
|
|
22
|
+
maxFieldSize?: number;
|
|
23
|
+
/** Таймаут обработки в миллисекундах (default: 300000 = 5 минут) */
|
|
24
|
+
timeout?: number;
|
|
25
|
+
/** Автоматическое определение формата (default: true) */
|
|
26
|
+
autoDetect?: boolean;
|
|
27
|
+
/** Разделитель CSV (default: ',') */
|
|
28
|
+
delimiter?: string;
|
|
29
|
+
/** Включить Fast-Path Engine (default: true) */
|
|
30
|
+
enableFastPath?: boolean;
|
|
31
|
+
/** Защита от CSV инъекций (default: true) */
|
|
32
|
+
preventCsvInjection?: boolean;
|
|
33
|
+
/** Соблюдение RFC4180 (default: true) */
|
|
34
|
+
rfc4180Compliant?: boolean;
|
|
35
|
+
/** Дополнительные опции конвертации */
|
|
36
|
+
conversionOptions?: Record<string, any>;
|
|
37
|
+
/** Использовать асинхронные версии функций (default: true) */
|
|
38
|
+
useAsync?: boolean;
|
|
39
|
+
/** Использовать многопоточную обработку для больших данных (default: auto-detect) */
|
|
40
|
+
useWorkers?: boolean;
|
|
41
|
+
/** Количество worker'ов для многопоточной обработки (default: CPU cores - 1) */
|
|
42
|
+
workerCount?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Интерфейс конвертированных данных в request object
|
|
47
|
+
*/
|
|
48
|
+
export interface ConvertedData {
|
|
49
|
+
/** Конвертированные данные */
|
|
50
|
+
data: any;
|
|
51
|
+
/** Формат вывода */
|
|
52
|
+
format: 'json' | 'csv';
|
|
53
|
+
/** Формат ввода */
|
|
54
|
+
inputFormat: 'json' | 'csv' | 'unknown';
|
|
55
|
+
/** Формат вывода */
|
|
56
|
+
outputFormat: 'json' | 'csv';
|
|
57
|
+
/** Статистика обработки */
|
|
58
|
+
stats: {
|
|
59
|
+
inputSize: number;
|
|
60
|
+
outputSize: number;
|
|
61
|
+
processingTime: number;
|
|
62
|
+
conversion: string;
|
|
63
|
+
workerCount?: number;
|
|
64
|
+
};
|
|
65
|
+
/** Опции конвертации */
|
|
66
|
+
options: Record<string, any>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Расширенный интерфейс Request с конвертированными данными
|
|
71
|
+
*/
|
|
72
|
+
declare global {
|
|
73
|
+
namespace Express {
|
|
74
|
+
interface Request {
|
|
75
|
+
/** Конвертированные данные */
|
|
76
|
+
converted?: ConvertedData;
|
|
77
|
+
/** Время начала обработки */
|
|
78
|
+
startTime?: number;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Преобразует строку размера (например, '500MB') в байты
|
|
85
|
+
* @param sizeStr - Строка размера (например, '10MB', '1GB', '500KB')
|
|
86
|
+
* @returns Размер в байтах
|
|
87
|
+
*/
|
|
88
|
+
function parseSizeToBytes(sizeStr: string | number): number {
|
|
89
|
+
if (typeof sizeStr === 'number') {
|
|
90
|
+
return sizeStr;
|
|
91
|
+
}
|
|
92
|
+
if (typeof sizeStr !== 'string') {
|
|
93
|
+
return 10 * 1024 * 1024; // default 10MB
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$/i);
|
|
97
|
+
if (!match) {
|
|
98
|
+
return 10 * 1024 * 1024;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const value = parseFloat(match[1]);
|
|
102
|
+
const unit = match[2].toUpperCase();
|
|
103
|
+
|
|
104
|
+
switch (unit) {
|
|
105
|
+
case 'B': return value;
|
|
106
|
+
case 'KB': return value * 1024;
|
|
107
|
+
case 'MB': return value * 1024 * 1024;
|
|
108
|
+
case 'GB': return value * 1024 * 1024 * 1024;
|
|
109
|
+
case 'TB': return value * 1024 * 1024 * 1024 * 1024;
|
|
110
|
+
default: return value * 1024 * 1024;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Express middleware для обработки CSV/JSON конвертации
|
|
116
|
+
*
|
|
117
|
+
* @param options - Опции middleware
|
|
118
|
+
* @returns Express middleware
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Базовое использование
|
|
122
|
+
* const app = express();
|
|
123
|
+
* app.use(express.json());
|
|
124
|
+
* app.use(express.text({ type: 'text/csv' }));
|
|
125
|
+
* app.use(jtcsvExpressMiddleware());
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* // С кастомными опциями
|
|
129
|
+
* app.use(jtcsvExpressMiddleware({
|
|
130
|
+
* maxSize: '50mb',
|
|
131
|
+
* maxFileSize: '1GB',
|
|
132
|
+
* maxFieldSize: 5 * 1024 * 1024, // 5MB
|
|
133
|
+
* timeout: 600000, // 10 минут
|
|
134
|
+
* delimiter: ';',
|
|
135
|
+
* enableFastPath: true,
|
|
136
|
+
* useAsync: true, // Использовать асинхронные функции
|
|
137
|
+
* useWorkers: true // Включить многопоточную обработку
|
|
138
|
+
* }));
|
|
139
|
+
*/
|
|
140
|
+
export function jtcsvExpressMiddleware(options: JtcsvExpressMiddlewareOptions = {}): (req: Request, res: Response, next: NextFunction) => Promise<void> {
|
|
141
|
+
const {
|
|
142
|
+
maxSize = '10mb',
|
|
143
|
+
maxFileSize = '500MB',
|
|
144
|
+
maxFieldSize = 1024 * 1024, // 1MB
|
|
145
|
+
timeout = 300000, // 5 minutes
|
|
146
|
+
autoDetect = true,
|
|
147
|
+
delimiter = ',',
|
|
148
|
+
enableFastPath = true,
|
|
149
|
+
preventCsvInjection = true,
|
|
150
|
+
rfc4180Compliant = true,
|
|
151
|
+
useAsync = true,
|
|
152
|
+
useWorkers,
|
|
153
|
+
workerCount,
|
|
154
|
+
conversionOptions = {}
|
|
155
|
+
} = options;
|
|
156
|
+
|
|
157
|
+
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
158
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
159
|
+
console.log('[jtcsv-middleware] Request received:', req.method, req.url, req.headers['content-type']);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Устанавливаем время начала обработки
|
|
163
|
+
req.startTime = Date.now();
|
|
164
|
+
|
|
165
|
+
// Пропускаем запросы без тела
|
|
166
|
+
if (!req.body || (typeof req.body !== 'string' && typeof req.body !== 'object')) {
|
|
167
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
168
|
+
console.log('[jtcsv-middleware] No body, skipping');
|
|
169
|
+
}
|
|
170
|
+
return next();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Проверка размера файла
|
|
174
|
+
const contentLength = req.get('content-length');
|
|
175
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
176
|
+
console.log(`[jtcsv-middleware] Content-Length: "${contentLength}"`);
|
|
177
|
+
}
|
|
178
|
+
if (contentLength && contentLength.trim() !== '') {
|
|
179
|
+
const maxBytes = parseSizeToBytes(maxFileSize);
|
|
180
|
+
const contentLengthInt = parseInt(contentLength, 10);
|
|
181
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
182
|
+
console.log(`[jtcsv-middleware] maxBytes: ${maxBytes}, contentLengthInt: ${contentLengthInt}`);
|
|
183
|
+
}
|
|
184
|
+
if (contentLengthInt > maxBytes) {
|
|
185
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
186
|
+
console.log(`[jtcsv-middleware] File size limit exceeded: ${contentLengthInt} > ${maxBytes}`);
|
|
187
|
+
}
|
|
188
|
+
res.status(413).json({
|
|
189
|
+
success: false,
|
|
190
|
+
error: `File size exceeds limit of ${maxFileSize}`,
|
|
191
|
+
code: 'FILE_SIZE_LIMIT_EXCEEDED'
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const contentType = req.get('content-type') || '';
|
|
198
|
+
const acceptHeader = req.get('accept') || 'application/json';
|
|
199
|
+
|
|
200
|
+
// Определяем формат входных данных заранее
|
|
201
|
+
let inputFormat: 'json' | 'csv' | 'unknown' = 'unknown';
|
|
202
|
+
const inputData = req.body;
|
|
203
|
+
|
|
204
|
+
if (autoDetect) {
|
|
205
|
+
if (contentType.includes('application/json') ||
|
|
206
|
+
(req.body !== null && typeof req.body === 'object' && !Array.isArray(req.body))) {
|
|
207
|
+
inputFormat = 'json';
|
|
208
|
+
} else if (contentType.includes('text/csv') ||
|
|
209
|
+
contentType.includes('text/plain') ||
|
|
210
|
+
(typeof req.body === 'string' && req.body.includes(','))) {
|
|
211
|
+
inputFormat = 'csv';
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// Ручное определение на основе content-type
|
|
215
|
+
if (contentType.includes('application/json')) {
|
|
216
|
+
inputFormat = 'json';
|
|
217
|
+
} else if (contentType.includes('text/csv')) {
|
|
218
|
+
inputFormat = 'csv';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Если формат не определен, пропускаем
|
|
223
|
+
if (inputFormat === 'unknown') {
|
|
224
|
+
return next();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Установка таймаута
|
|
228
|
+
let timeoutId: NodeJS.Timeout;
|
|
229
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
230
|
+
timeoutId = setTimeout(() => {
|
|
231
|
+
reject(new Error(`Request processing timeout (${timeout}ms)`));
|
|
232
|
+
}, timeout);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
// Обернем основную логику в Promise.race с таймаутом
|
|
237
|
+
const processingPromise = (async () => {
|
|
238
|
+
// Определяем желаемый формат вывода на основе Accept header
|
|
239
|
+
let outputFormat: 'json' | 'csv' = 'json';
|
|
240
|
+
if (acceptHeader.includes('text/csv')) {
|
|
241
|
+
outputFormat = 'csv';
|
|
242
|
+
} else if (req.query.format === 'csv') {
|
|
243
|
+
outputFormat = 'csv';
|
|
244
|
+
} else if ((req.body as any)?.format === 'csv') {
|
|
245
|
+
outputFormat = 'csv';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Опции конвертации
|
|
249
|
+
const mergedConversionOptions: Record<string, any> = {
|
|
250
|
+
delimiter,
|
|
251
|
+
preventCsvInjection,
|
|
252
|
+
rfc4180Compliant,
|
|
253
|
+
useFastPath: enableFastPath,
|
|
254
|
+
maxFieldSize,
|
|
255
|
+
useWorkers,
|
|
256
|
+
workerCount,
|
|
257
|
+
...req.query,
|
|
258
|
+
...conversionOptions
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Удаляем параметры, которые не относятся к конвертации
|
|
262
|
+
delete mergedConversionOptions.maxSize;
|
|
263
|
+
delete mergedConversionOptions.maxFileSize;
|
|
264
|
+
delete mergedConversionOptions.maxFieldSize;
|
|
265
|
+
delete mergedConversionOptions.timeout;
|
|
266
|
+
delete mergedConversionOptions.autoDetect;
|
|
267
|
+
delete mergedConversionOptions.enableFastPath;
|
|
268
|
+
delete mergedConversionOptions.useAsync;
|
|
269
|
+
delete mergedConversionOptions.useWorkers;
|
|
270
|
+
delete mergedConversionOptions.workerCount;
|
|
271
|
+
|
|
272
|
+
let result: any;
|
|
273
|
+
const stats: ConvertedData['stats'] = {
|
|
274
|
+
inputSize: 0,
|
|
275
|
+
outputSize: 0,
|
|
276
|
+
processingTime: 0,
|
|
277
|
+
conversion: `${inputFormat}→${outputFormat}`
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const startTime = Date.now();
|
|
281
|
+
|
|
282
|
+
// Выполняем конвертацию
|
|
283
|
+
if (inputFormat === 'json' && outputFormat === 'csv') {
|
|
284
|
+
const jsonData = typeof inputData === 'string' ? JSON.parse(inputData) : inputData;
|
|
285
|
+
stats.inputSize = Buffer.byteLength(JSON.stringify(jsonData));
|
|
286
|
+
|
|
287
|
+
if (useAsync) {
|
|
288
|
+
result = await jsonToCsvAsync(jsonData, mergedConversionOptions);
|
|
289
|
+
} else {
|
|
290
|
+
result = jsonToCsv(jsonData, mergedConversionOptions);
|
|
291
|
+
}
|
|
292
|
+
stats.outputSize = Buffer.byteLength(result);
|
|
293
|
+
|
|
294
|
+
} else if (inputFormat === 'csv' && outputFormat === 'json') {
|
|
295
|
+
const csvData = typeof inputData === 'string' ? inputData : String(inputData);
|
|
296
|
+
stats.inputSize = Buffer.byteLength(csvData);
|
|
297
|
+
|
|
298
|
+
if (useAsync) {
|
|
299
|
+
result = await csvToJsonAsync(csvData, mergedConversionOptions);
|
|
300
|
+
} else {
|
|
301
|
+
result = csvToJson(csvData, mergedConversionOptions);
|
|
302
|
+
}
|
|
303
|
+
stats.outputSize = Buffer.byteLength(JSON.stringify(result));
|
|
304
|
+
|
|
305
|
+
} else {
|
|
306
|
+
// Нет необходимости в конвертации
|
|
307
|
+
result = inputData;
|
|
308
|
+
stats.conversion = 'none';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
stats.processingTime = Date.now() - startTime;
|
|
312
|
+
|
|
313
|
+
// Сохраняем результат в request object
|
|
314
|
+
req.converted = {
|
|
315
|
+
data: result,
|
|
316
|
+
format: outputFormat,
|
|
317
|
+
inputFormat,
|
|
318
|
+
outputFormat,
|
|
319
|
+
stats,
|
|
320
|
+
options: mergedConversionOptions
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Устанавливаем соответствующий Content-Type для ответа
|
|
324
|
+
if (outputFormat === 'csv') {
|
|
325
|
+
res.set('Content-Type', 'text/csv; charset=utf-8');
|
|
326
|
+
} else {
|
|
327
|
+
res.set('Content-Type', 'application/json; charset=utf-8');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
next();
|
|
331
|
+
})();
|
|
332
|
+
|
|
333
|
+
// Ждем либо обработку, либо таймаут
|
|
334
|
+
await Promise.race([processingPromise, timeoutPromise]);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
// Обработка ошибок конвертации
|
|
337
|
+
const err = error as Error & { code?: string };
|
|
338
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
339
|
+
console.log('[jtcsv-middleware] Conversion error:', err.message, err.stack?.split('\n')[0]);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Определяем статус код на основе типа ошибки
|
|
343
|
+
let statusCode = 400;
|
|
344
|
+
if (err.message.includes('timeout') || err.message.includes('Timeout')) {
|
|
345
|
+
statusCode = 408; // Request Timeout
|
|
346
|
+
} else if (err.message.includes('File size exceeds limit')) {
|
|
347
|
+
statusCode = 413; // Payload Too Large
|
|
348
|
+
} else if (err instanceof SecurityError) {
|
|
349
|
+
statusCode = 403; // Forbidden
|
|
350
|
+
} else if (err instanceof ValidationError) {
|
|
351
|
+
statusCode = 422; // Unprocessable Entity
|
|
352
|
+
} else if (err instanceof FileSystemError) {
|
|
353
|
+
statusCode = 500; // Internal Server Error
|
|
354
|
+
} else if (err instanceof JtcsvError) {
|
|
355
|
+
statusCode = 400; // Bad Request
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const errorResponse: any = {
|
|
359
|
+
success: false,
|
|
360
|
+
error: err.message,
|
|
361
|
+
code: err.code || 'CONVERSION_ERROR',
|
|
362
|
+
timestamp: new Date().toISOString()
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Добавляем дополнительную информацию для отладки
|
|
366
|
+
if (process.env.NODE_ENV === 'development') {
|
|
367
|
+
errorResponse.stack = err.stack;
|
|
368
|
+
errorResponse.details = {
|
|
369
|
+
contentType: req.get('content-type'),
|
|
370
|
+
contentLength: req.get('content-length'),
|
|
371
|
+
method: req.method,
|
|
372
|
+
url: req.url
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
res.status(statusCode).json(errorResponse);
|
|
377
|
+
} finally {
|
|
378
|
+
// Очищаем таймаут
|
|
379
|
+
if (timeoutId) {
|
|
380
|
+
clearTimeout(timeoutId);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Express route для конвертации CSV в JSON
|
|
388
|
+
*
|
|
389
|
+
* @param options - Опции конвертации
|
|
390
|
+
* @returns Express route handler
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* app.post('/api/csv-to-json', jtcsvCsvToJsonRoute());
|
|
394
|
+
*/
|
|
395
|
+
export function jtcsvCsvToJsonRoute(options: Record<string, any> = {}): (req: Request, res: Response) => Promise<void> {
|
|
396
|
+
return async (req: Request, res: Response): Promise<void> => {
|
|
397
|
+
try {
|
|
398
|
+
const csvData = req.body;
|
|
399
|
+
|
|
400
|
+
if (!csvData || (typeof csvData !== 'string' && !Buffer.isBuffer(csvData))) {
|
|
401
|
+
res.status(400).json({
|
|
402
|
+
success: false,
|
|
403
|
+
error: 'CSV data is required'
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const csvString = Buffer.isBuffer(csvData) ? csvData.toString() : csvData;
|
|
409
|
+
const result = await csvToJsonAsync(csvString, options);
|
|
410
|
+
|
|
411
|
+
res.json({
|
|
412
|
+
success: true,
|
|
413
|
+
data: result,
|
|
414
|
+
stats: {
|
|
415
|
+
rows: result.length,
|
|
416
|
+
processingTime: Date.now() - (req.startTime || Date.now())
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
} catch (error) {
|
|
420
|
+
const err = error as Error;
|
|
421
|
+
res.status(400).json({
|
|
422
|
+
success: false,
|
|
423
|
+
error: err.message
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Express route для конвертации JSON в CSV
|
|
431
|
+
*
|
|
432
|
+
* @param options - Опции конвертации
|
|
433
|
+
* @returns Express route handler
|
|
434
|
+
*
|
|
435
|
+
* @example
|
|
436
|
+
* app.post('/api/json-to-csv', jtcsvJsonToCsvRoute());
|
|
437
|
+
*/
|
|
438
|
+
export function jtcsvJsonToCsvRoute(options: Record<string, any> = {}): (req: Request, res: Response) => Promise<void> {
|
|
439
|
+
return async (req: Request, res: Response): Promise<void> => {
|
|
440
|
+
try {
|
|
441
|
+
const jsonData = req.body;
|
|
442
|
+
|
|
443
|
+
if (!jsonData || (typeof jsonData !== 'object' && typeof jsonData !== 'string')) {
|
|
444
|
+
res.status(400).json({
|
|
445
|
+
success: false,
|
|
446
|
+
error: 'JSON data is required'
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;
|
|
452
|
+
const result = await jsonToCsvAsync(data, options);
|
|
453
|
+
|
|
454
|
+
res.set('Content-Type', 'text/csv; charset=utf-8');
|
|
455
|
+
res.set('Content-Disposition', 'attachment; filename="data.csv"');
|
|
456
|
+
|
|
457
|
+
res.send(result);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
const err = error as Error;
|
|
460
|
+
res.status(400).json({
|
|
461
|
+
success: false,
|
|
462
|
+
error: err.message
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Express route для загрузки CSV файла
|
|
470
|
+
*
|
|
471
|
+
* @param options - Опции конвертации
|
|
472
|
+
* @returns Express route handler
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* const multer = require('multer');
|
|
476
|
+
* const upload = multer({ dest: 'uploads/' });
|
|
477
|
+
* app.post('/api/upload-csv', upload.single('file'), jtcsvUploadCsvRoute());
|
|
478
|
+
*/
|
|
479
|
+
export function jtcsvUploadCsvRoute(options: Record<string, any> = {}): (req: Request, res: Response) => Promise<void> {
|
|
480
|
+
return async (req: Request, res: Response): Promise<void> => {
|
|
481
|
+
try {
|
|
482
|
+
if (!(req as any).file) {
|
|
483
|
+
res.status(400).json({
|
|
484
|
+
success: false,
|
|
485
|
+
error: 'CSV file is required'
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const fs = require('fs').promises;
|
|
491
|
+
const csvData = await fs.readFile((req as any).file.path, 'utf8');
|
|
492
|
+
|
|
493
|
+
const result = await csvToJsonAsync(csvData, options);
|
|
494
|
+
|
|
495
|
+
// Очищаем временный файл
|
|
496
|
+
await fs.unlink((req as any).file.path);
|
|
497
|
+
|
|
498
|
+
res.json({
|
|
499
|
+
success: true,
|
|
500
|
+
data: result,
|
|
501
|
+
stats: {
|
|
502
|
+
rows: result.length,
|
|
503
|
+
fileSize: (req as any).file.size,
|
|
504
|
+
processingTime: Date.now() - (req.startTime || Date.now())
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
} catch (error) {
|
|
508
|
+
const err = error as Error;
|
|
509
|
+
res.status(400).json({
|
|
510
|
+
success: false,
|
|
511
|
+
error: err.message
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Health check endpoint для JTCSV
|
|
519
|
+
*
|
|
520
|
+
* @returns Express route handler
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* app.get('/api/health', jtcsvHealthCheck());
|
|
524
|
+
*/
|
|
525
|
+
export function jtcsvHealthCheck(): (req: Request, res: Response) => void {
|
|
526
|
+
return (req: Request, res: Response): void => {
|
|
527
|
+
res.json({
|
|
528
|
+
service: 'jtcsv-express-middleware',
|
|
529
|
+
status: 'healthy',
|
|
530
|
+
version: '2.0.0',
|
|
531
|
+
timestamp: new Date().toISOString(),
|
|
532
|
+
features: {
|
|
533
|
+
csvToJson: true,
|
|
534
|
+
jsonToCsv: true,
|
|
535
|
+
fastPathEngine: true,
|
|
536
|
+
csvInjectionProtection: true,
|
|
537
|
+
streaming: true,
|
|
538
|
+
asyncProcessing: true,
|
|
539
|
+
workerPool: true
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Экспорт всех функций
|
|
546
|
+
export default {
|
|
547
|
+
middleware: jtcsvExpressMiddleware,
|
|
548
|
+
csvToJsonRoute: jtcsvCsvToJsonRoute,
|
|
549
|
+
jsonToCsvRoute: jtcsvJsonToCsvRoute,
|
|
550
|
+
uploadCsvRoute: jtcsvUploadCsvRoute,
|
|
551
|
+
healthCheck: jtcsvHealthCheck,
|
|
552
|
+
|
|
553
|
+
// Aliases для удобства
|
|
554
|
+
jtcsvMiddleware: jtcsvExpressMiddleware,
|
|
555
|
+
createMiddleware: jtcsvExpressMiddleware
|
|
556
|
+
};
|
|
557
|
+
|
|
@@ -234,7 +234,7 @@ async function jtcsvFastifyPlugin(fastify, options = {}) {
|
|
|
234
234
|
|
|
235
235
|
try {
|
|
236
236
|
let inputFormat = 'unknown';
|
|
237
|
-
|
|
237
|
+
const outputFormat = format || (acceptHeader.includes('text/csv') ? 'csv' : 'json');
|
|
238
238
|
|
|
239
239
|
// Определяем формат входных данных
|
|
240
240
|
if (contentType.includes('application/json') || Array.isArray(request.body)) {
|
|
@@ -255,7 +255,7 @@ async function jtcsvFastifyPlugin(fastify, options = {}) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
let result;
|
|
258
|
-
|
|
258
|
+
const stats = {
|
|
259
259
|
inputSize: 0,
|
|
260
260
|
outputSize: 0,
|
|
261
261
|
processingTime: 0,
|
|
@@ -402,5 +402,3 @@ module.exports = fp(jtcsvFastifyPlugin, {
|
|
|
402
402
|
|
|
403
403
|
// Экспортируем также как обычную функцию
|
|
404
404
|
module.exports.jtcsvFastifyPlugin = jtcsvFastifyPlugin;
|
|
405
|
-
|
|
406
|
-
|