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,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fastify plugin для JTCSV
|
|
3
|
+
* Плагин для автоматической конвертации CSV/JSON в Fastify приложениях
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @date 2026-01-23
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fp = require('fastify-plugin');
|
|
10
|
+
import { csvToJson, csvToJsonAsync, jsonToCsv, jsonToCsvAsync } from '../../index-core';
|
|
11
|
+
import { CsvToJsonOptions, JsonToCsvOptions } from '../../src/types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Опции плагина Fastify для JTCSV
|
|
15
|
+
*/
|
|
16
|
+
export interface JtcsvFastifyPluginOptions {
|
|
17
|
+
/** Префикс для маршрутов API */
|
|
18
|
+
prefix?: string;
|
|
19
|
+
/** Разделитель CSV по умолчанию */
|
|
20
|
+
delimiter?: string;
|
|
21
|
+
/** Включить fast-path engine */
|
|
22
|
+
enableFastPath?: boolean;
|
|
23
|
+
/** Защита от CSV инъекций */
|
|
24
|
+
preventCsvInjection?: boolean;
|
|
25
|
+
/** Соответствие стандарту RFC 4180 */
|
|
26
|
+
rfc4180Compliant?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Статистика обработки
|
|
31
|
+
*/
|
|
32
|
+
interface ProcessingStats {
|
|
33
|
+
rows?: number;
|
|
34
|
+
size?: number;
|
|
35
|
+
processingTime: number;
|
|
36
|
+
inputSize?: number;
|
|
37
|
+
outputSize?: number;
|
|
38
|
+
conversion?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ответ API
|
|
43
|
+
*/
|
|
44
|
+
interface ApiResponse<T = any> {
|
|
45
|
+
success: boolean;
|
|
46
|
+
data?: T;
|
|
47
|
+
stats?: ProcessingStats;
|
|
48
|
+
error?: string;
|
|
49
|
+
code?: string;
|
|
50
|
+
details?: string;
|
|
51
|
+
format?: string;
|
|
52
|
+
inputFormat?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fastify plugin для JTCSV
|
|
57
|
+
*
|
|
58
|
+
* @param fastify - Fastify instance
|
|
59
|
+
* @param options - Опции плагина
|
|
60
|
+
* @param next - Callback
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // Базовое использование
|
|
64
|
+
* const fastify = require('fastify')();
|
|
65
|
+
* await fastify.register(require('@jtcsv/fastify'), {
|
|
66
|
+
* prefix: '/api/convert'
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // С кастомными опциями
|
|
71
|
+
* await fastify.register(require('@jtcsv/fastify'), {
|
|
72
|
+
* prefix: '/api',
|
|
73
|
+
* enableFastPath: true,
|
|
74
|
+
* delimiter: ';'
|
|
75
|
+
* });
|
|
76
|
+
*/
|
|
77
|
+
async function jtcsvFastifyPlugin(fastify: any, options: JtcsvFastifyPluginOptions = {}) {
|
|
78
|
+
const {
|
|
79
|
+
prefix = '/convert',
|
|
80
|
+
delimiter = ',',
|
|
81
|
+
enableFastPath = true,
|
|
82
|
+
preventCsvInjection = true,
|
|
83
|
+
rfc4180Compliant = true
|
|
84
|
+
} = options;
|
|
85
|
+
|
|
86
|
+
// Health check endpoint
|
|
87
|
+
fastify.get(`${prefix}/health`, async () => {
|
|
88
|
+
return {
|
|
89
|
+
service: 'jtcsv-fastify-plugin',
|
|
90
|
+
status: 'healthy',
|
|
91
|
+
version: '1.0.0',
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
features: {
|
|
94
|
+
csvToJson: true,
|
|
95
|
+
jsonToCsv: true,
|
|
96
|
+
fastPathEngine: enableFastPath,
|
|
97
|
+
csvInjectionProtection: preventCsvInjection,
|
|
98
|
+
streaming: true,
|
|
99
|
+
ndjson: true
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// CSV to JSON endpoint
|
|
105
|
+
fastify.post(`${prefix}/csv-to-json`, {
|
|
106
|
+
schema: {
|
|
107
|
+
body: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
required: ['csv'],
|
|
110
|
+
properties: {
|
|
111
|
+
csv: { type: 'string' },
|
|
112
|
+
delimiter: { type: 'string', default: delimiter },
|
|
113
|
+
parseNumbers: { type: 'boolean', default: true },
|
|
114
|
+
parseBooleans: { type: 'boolean', default: true },
|
|
115
|
+
useFastPath: { type: 'boolean', default: enableFastPath }
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
response: {
|
|
119
|
+
200: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
success: { type: 'boolean' },
|
|
123
|
+
data: { type: 'array' },
|
|
124
|
+
stats: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
rows: { type: 'number' },
|
|
128
|
+
processingTime: { type: 'number' }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}, async (request: any, reply: any) => {
|
|
136
|
+
const startTime = Date.now();
|
|
137
|
+
const {
|
|
138
|
+
csv,
|
|
139
|
+
delimiter: reqDelimiter = delimiter,
|
|
140
|
+
parseNumbers = true,
|
|
141
|
+
parseBooleans = true,
|
|
142
|
+
useFastPath = enableFastPath
|
|
143
|
+
} = request.body;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const json = await csvToJsonAsync(csv, {
|
|
147
|
+
delimiter: reqDelimiter,
|
|
148
|
+
parseNumbers,
|
|
149
|
+
parseBooleans,
|
|
150
|
+
useFastPath,
|
|
151
|
+
preventCsvInjection,
|
|
152
|
+
rfc4180Compliant
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
success: true,
|
|
157
|
+
data: json,
|
|
158
|
+
stats: {
|
|
159
|
+
rows: json.length,
|
|
160
|
+
processingTime: Date.now() - startTime
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
} catch (error: any) {
|
|
164
|
+
reply.code(400);
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
error: error.message,
|
|
168
|
+
code: 'CSV_CONVERSION_ERROR'
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// JSON to CSV endpoint
|
|
174
|
+
fastify.post(`${prefix}/json-to-csv`, {
|
|
175
|
+
schema: {
|
|
176
|
+
body: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
required: ['json'],
|
|
179
|
+
properties: {
|
|
180
|
+
json: { type: 'array' },
|
|
181
|
+
delimiter: { type: 'string', default: delimiter },
|
|
182
|
+
includeHeaders: { type: 'boolean', default: true }
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
response: {
|
|
186
|
+
200: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
success: { type: 'boolean' },
|
|
190
|
+
data: { type: 'string' },
|
|
191
|
+
stats: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
size: { type: 'number' },
|
|
195
|
+
processingTime: { type: 'number' }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}, async (request: any, reply: any) => {
|
|
203
|
+
const startTime = Date.now();
|
|
204
|
+
const {
|
|
205
|
+
json,
|
|
206
|
+
delimiter: reqDelimiter = delimiter,
|
|
207
|
+
includeHeaders = true
|
|
208
|
+
} = request.body;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const csv = await jsonToCsvAsync(json, {
|
|
212
|
+
delimiter: reqDelimiter,
|
|
213
|
+
includeHeaders,
|
|
214
|
+
preventCsvInjection,
|
|
215
|
+
rfc4180Compliant
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
success: true,
|
|
220
|
+
data: csv,
|
|
221
|
+
stats: {
|
|
222
|
+
size: Buffer.byteLength(csv),
|
|
223
|
+
processingTime: Date.now() - startTime
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
} catch (error: any) {
|
|
227
|
+
reply.code(400);
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: error.message,
|
|
231
|
+
code: 'JSON_CONVERSION_ERROR'
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Универсальный endpoint для автоматической конвертации
|
|
237
|
+
fastify.post(`${prefix}/auto`, {
|
|
238
|
+
schema: {
|
|
239
|
+
body: {
|
|
240
|
+
oneOf: [
|
|
241
|
+
{ type: 'string' }, // CSV data
|
|
242
|
+
{ type: 'array' }, // JSON array
|
|
243
|
+
{ type: 'object' } // JSON object
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
headers: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {
|
|
249
|
+
'content-type': {
|
|
250
|
+
type: 'string',
|
|
251
|
+
enum: ['application/json', 'text/csv', 'text/plain']
|
|
252
|
+
},
|
|
253
|
+
'accept': {
|
|
254
|
+
type: 'string',
|
|
255
|
+
enum: ['application/json', 'text/csv']
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
querystring: {
|
|
260
|
+
type: 'object',
|
|
261
|
+
properties: {
|
|
262
|
+
format: { type: 'string', enum: ['json', 'csv'] },
|
|
263
|
+
delimiter: { type: 'string' }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}, async (request: any, reply: any) => {
|
|
268
|
+
const startTime = Date.now();
|
|
269
|
+
const contentType = request.headers['content-type'] || '';
|
|
270
|
+
const acceptHeader = request.headers['accept'] || 'application/json';
|
|
271
|
+
const { format, delimiter: queryDelimiter } = request.query;
|
|
272
|
+
|
|
273
|
+
const reqDelimiter = queryDelimiter || delimiter;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
let inputFormat = 'unknown';
|
|
277
|
+
const outputFormat = format || (acceptHeader.includes('text/csv') ? 'csv' : 'json');
|
|
278
|
+
|
|
279
|
+
// Определяем формат входных данных
|
|
280
|
+
if (contentType.includes('application/json') || Array.isArray(request.body)) {
|
|
281
|
+
inputFormat = 'json';
|
|
282
|
+
} else if (contentType.includes('text/csv') ||
|
|
283
|
+
contentType.includes('text/plain') ||
|
|
284
|
+
(typeof request.body === 'string' && request.body.includes(','))) {
|
|
285
|
+
inputFormat = 'csv';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (inputFormat === 'unknown') {
|
|
289
|
+
reply.code(400);
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
error: 'Unable to determine input format',
|
|
293
|
+
code: 'UNKNOWN_FORMAT'
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let result: any;
|
|
298
|
+
const stats: ProcessingStats = {
|
|
299
|
+
inputSize: 0,
|
|
300
|
+
outputSize: 0,
|
|
301
|
+
processingTime: 0,
|
|
302
|
+
conversion: `${inputFormat}→${outputFormat}`
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const conversionOptions = {
|
|
306
|
+
delimiter: reqDelimiter,
|
|
307
|
+
useFastPath: enableFastPath,
|
|
308
|
+
preventCsvInjection,
|
|
309
|
+
rfc4180Compliant
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (inputFormat === 'json' && outputFormat === 'csv') {
|
|
313
|
+
const jsonData = Array.isArray(request.body) ? request.body : [request.body];
|
|
314
|
+
stats.inputSize = Buffer.byteLength(JSON.stringify(jsonData));
|
|
315
|
+
|
|
316
|
+
result = await jsonToCsvAsync(jsonData, {
|
|
317
|
+
...conversionOptions,
|
|
318
|
+
includeHeaders: true
|
|
319
|
+
});
|
|
320
|
+
stats.outputSize = Buffer.byteLength(result);
|
|
321
|
+
|
|
322
|
+
reply.header('Content-Type', 'text/csv; charset=utf-8');
|
|
323
|
+
|
|
324
|
+
} else if (inputFormat === 'csv' && outputFormat === 'json') {
|
|
325
|
+
const csvData = typeof request.body === 'string' ? request.body : String(request.body);
|
|
326
|
+
stats.inputSize = Buffer.byteLength(csvData);
|
|
327
|
+
|
|
328
|
+
result = await csvToJsonAsync(csvData, {
|
|
329
|
+
...conversionOptions,
|
|
330
|
+
parseNumbers: true,
|
|
331
|
+
parseBooleans: true
|
|
332
|
+
});
|
|
333
|
+
stats.outputSize = Buffer.byteLength(JSON.stringify(result));
|
|
334
|
+
|
|
335
|
+
reply.header('Content-Type', 'application/json; charset=utf-8');
|
|
336
|
+
|
|
337
|
+
} else {
|
|
338
|
+
// Нет необходимости в конвертации
|
|
339
|
+
result = request.body;
|
|
340
|
+
stats.conversion = 'none';
|
|
341
|
+
stats.inputSize = Buffer.byteLength(JSON.stringify(result));
|
|
342
|
+
stats.outputSize = stats.inputSize;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
stats.processingTime = Date.now() - startTime;
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
success: true,
|
|
349
|
+
data: result,
|
|
350
|
+
format: outputFormat,
|
|
351
|
+
inputFormat,
|
|
352
|
+
stats
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
} catch (error: any) {
|
|
356
|
+
reply.code(400);
|
|
357
|
+
return {
|
|
358
|
+
success: false,
|
|
359
|
+
error: error.message,
|
|
360
|
+
code: 'CONVERSION_ERROR',
|
|
361
|
+
details: (process.env as any)['NODE_ENV'] === 'development' ? error.stack : undefined
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Streaming endpoint для больших файлов
|
|
367
|
+
fastify.post(`${prefix}/stream`, {
|
|
368
|
+
schema: {
|
|
369
|
+
body: {
|
|
370
|
+
type: 'object',
|
|
371
|
+
required: ['direction'],
|
|
372
|
+
properties: {
|
|
373
|
+
direction: {
|
|
374
|
+
type: 'string',
|
|
375
|
+
enum: ['csv-to-json', 'json-to-csv']
|
|
376
|
+
},
|
|
377
|
+
delimiter: { type: 'string', default: delimiter }
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}, async (request: any, reply: any) => {
|
|
382
|
+
const { direction, delimiter: reqDelimiter = delimiter } = request.body;
|
|
383
|
+
|
|
384
|
+
if (direction === 'csv-to-json') {
|
|
385
|
+
reply.header('Content-Type', 'application/x-ndjson');
|
|
386
|
+
reply.header('Transfer-Encoding', 'chunked');
|
|
387
|
+
|
|
388
|
+
// Здесь будет реализация streaming
|
|
389
|
+
// Пока возвращаем заглушку
|
|
390
|
+
reply.send(JSON.stringify({
|
|
391
|
+
success: false,
|
|
392
|
+
error: 'Streaming endpoint not implemented yet',
|
|
393
|
+
code: 'NOT_IMPLEMENTED'
|
|
394
|
+
}));
|
|
395
|
+
|
|
396
|
+
} else if (direction === 'json-to-csv') {
|
|
397
|
+
reply.header('Content-Type', 'text/csv');
|
|
398
|
+
reply.header('Transfer-Encoding', 'chunked');
|
|
399
|
+
|
|
400
|
+
// Здесь будет реализация streaming
|
|
401
|
+
reply.send('Streaming endpoint not implemented yet\n');
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Декоратор для прямого доступа к функциям конвертации
|
|
406
|
+
fastify.decorate('jtcsv', {
|
|
407
|
+
csvToJson: async (csv: string, options: Partial<CsvToJsonOptions> = {}) => {
|
|
408
|
+
return csvToJsonAsync(csv, {
|
|
409
|
+
delimiter,
|
|
410
|
+
useFastPath: enableFastPath,
|
|
411
|
+
preventCsvInjection,
|
|
412
|
+
rfc4180Compliant,
|
|
413
|
+
...options
|
|
414
|
+
});
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
jsonToCsv: async (json: any[], options: Partial<JsonToCsvOptions> = {}) => {
|
|
418
|
+
return jsonToCsvAsync(json, {
|
|
419
|
+
delimiter,
|
|
420
|
+
preventCsvInjection,
|
|
421
|
+
rfc4180Compliant,
|
|
422
|
+
...options
|
|
423
|
+
});
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
health: () => ({
|
|
427
|
+
service: 'jtcsv-fastify-plugin',
|
|
428
|
+
status: 'healthy',
|
|
429
|
+
version: '1.0.0'
|
|
430
|
+
})
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
console.log(`✅ JTCSV Fastify plugin зарегистрирован с префиксом: ${prefix}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Экспортируем как Fastify plugin
|
|
437
|
+
export default fp(jtcsvFastifyPlugin, {
|
|
438
|
+
fastify: '4.x',
|
|
439
|
+
name: '@jtcsv/fastify'
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Экспортируем также как обычную функцию
|
|
443
|
+
export { jtcsvFastifyPlugin };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono plugin for jtcsv
|
|
3
|
+
* Provides middleware and response helpers for CSV processing in Hono applications
|
|
4
|
+
* @module plugins/hono
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { csvToJson, jsonToCsv } from '../../index-core';
|
|
8
|
+
import type { CsvToJsonOptions, JsonToCsvOptions } from '../../src/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hono context type (simplified)
|
|
12
|
+
*/
|
|
13
|
+
interface HonoContext {
|
|
14
|
+
req: {
|
|
15
|
+
text(): Promise<string>;
|
|
16
|
+
};
|
|
17
|
+
set(key: string, value: any): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for CSV middleware
|
|
22
|
+
*/
|
|
23
|
+
export interface CsvMiddlewareOptions extends CsvToJsonOptions {
|
|
24
|
+
// Additional options specific to CSV middleware
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for CSV response
|
|
29
|
+
*/
|
|
30
|
+
export interface CsvResponseOptions extends JsonToCsvOptions {
|
|
31
|
+
// Additional options specific to CSV response
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalize filename for CSV download
|
|
36
|
+
*/
|
|
37
|
+
function normalizeFilename(filename?: string): string {
|
|
38
|
+
if (!filename || typeof filename !== 'string') {
|
|
39
|
+
return 'export.csv';
|
|
40
|
+
}
|
|
41
|
+
return filename.includes('.') ? filename : `${filename}.csv`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* CSV middleware for Hono
|
|
46
|
+
* Parses incoming CSV request bodies and attaches parsed data to context
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* app.post('/upload', csvMiddleware(), async (c) => {
|
|
50
|
+
* const data = c.get('csv');
|
|
51
|
+
* return c.json({ success: true, data });
|
|
52
|
+
* });
|
|
53
|
+
*/
|
|
54
|
+
export function csvMiddleware(options: CsvMiddlewareOptions = {}): any {
|
|
55
|
+
return async (c: HonoContext, next: () => Promise<void>) => {
|
|
56
|
+
try {
|
|
57
|
+
const csvText = await c.req.text();
|
|
58
|
+
const rows = await csvToJson(csvText, options);
|
|
59
|
+
c.set('csv', rows);
|
|
60
|
+
await next();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Re-throw error for Hono error handling
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Async CSV middleware for Hono
|
|
70
|
+
* Uses async/await for better performance with large files
|
|
71
|
+
*/
|
|
72
|
+
export function asyncCsvMiddleware(options: CsvMiddlewareOptions = {}): any {
|
|
73
|
+
return async (c: HonoContext, next: () => Promise<void>) => {
|
|
74
|
+
try {
|
|
75
|
+
const csvText = await c.req.text();
|
|
76
|
+
const rows = await csvToJson(csvText, options);
|
|
77
|
+
c.set('csv', rows);
|
|
78
|
+
await next();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Creates a CSV response for Hono
|
|
87
|
+
* Converts data to CSV and returns appropriate headers
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* app.get('/export', async (c) => {
|
|
91
|
+
* const data = await getData();
|
|
92
|
+
* const response = createCsvResponse(data, 'export.csv');
|
|
93
|
+
* return new Response(response.csv, { headers: response.headers });
|
|
94
|
+
* });
|
|
95
|
+
*/
|
|
96
|
+
export function createCsvResponse(
|
|
97
|
+
data: any,
|
|
98
|
+
filename: string = 'export.csv',
|
|
99
|
+
options: CsvResponseOptions = {}
|
|
100
|
+
): { csv: string; headers: Record<string, string> } {
|
|
101
|
+
const safeName = normalizeFilename(filename);
|
|
102
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
103
|
+
const csv = jsonToCsv(rows, options);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
csv,
|
|
107
|
+
headers: {
|
|
108
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
109
|
+
'Content-Disposition': `attachment; filename="${safeName}"`
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Async version of createCsvResponse
|
|
116
|
+
* Uses async/await for better performance with large files
|
|
117
|
+
*/
|
|
118
|
+
export async function createAsyncCsvResponse(
|
|
119
|
+
data: any,
|
|
120
|
+
filename: string = 'export.csv',
|
|
121
|
+
options: CsvResponseOptions = {}
|
|
122
|
+
): Promise<{ csv: string; headers: Record<string, string> }> {
|
|
123
|
+
const safeName = normalizeFilename(filename);
|
|
124
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
125
|
+
const csv = await jsonToCsv(rows, options);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
csv,
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
131
|
+
'Content-Disposition': `attachment; filename="${safeName}"`
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* CSV response helper for Hono
|
|
138
|
+
* Returns a Response object with CSV data
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* app.get('/export', async (c) => {
|
|
142
|
+
* const data = await getData();
|
|
143
|
+
* return csvResponse(c, data, 'export.csv');
|
|
144
|
+
* });
|
|
145
|
+
*/
|
|
146
|
+
export function csvResponse(
|
|
147
|
+
data: any,
|
|
148
|
+
filename: string = 'export.csv',
|
|
149
|
+
options: CsvResponseOptions = {}
|
|
150
|
+
): Response {
|
|
151
|
+
const { csv, headers } = createCsvResponse(data, filename, options);
|
|
152
|
+
return new Response(csv, { headers });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Async CSV response helper for Hono
|
|
157
|
+
* Returns a Response object with CSV data using async processing
|
|
158
|
+
*/
|
|
159
|
+
export async function asyncCsvResponse(
|
|
160
|
+
data: any,
|
|
161
|
+
filename: string = 'export.csv',
|
|
162
|
+
options: CsvResponseOptions = {}
|
|
163
|
+
): Promise<Response> {
|
|
164
|
+
const { csv, headers } = await createAsyncCsvResponse(data, filename, options);
|
|
165
|
+
return new Response(csv, { headers });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* CSV parsing endpoint helper
|
|
170
|
+
* Creates a route handler that parses CSV and returns JSON
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* app.post('/parse', csvParseEndpoint());
|
|
174
|
+
*/
|
|
175
|
+
export function csvParseEndpoint(options: CsvMiddlewareOptions = {}): any {
|
|
176
|
+
return async (c: HonoContext) => {
|
|
177
|
+
try {
|
|
178
|
+
const csvText = await c.req.text();
|
|
179
|
+
const rows = await csvToJson(csvText, options);
|
|
180
|
+
return new Response(JSON.stringify(rows), {
|
|
181
|
+
headers: { 'Content-Type': 'application/json' }
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return new Response(
|
|
185
|
+
JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }),
|
|
186
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* CSV download endpoint helper
|
|
194
|
+
* Creates a route handler that returns CSV data
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* app.get('/download', csvDownloadEndpoint(async () => await getData()));
|
|
198
|
+
*/
|
|
199
|
+
export function csvDownloadEndpoint(
|
|
200
|
+
dataProvider: () => Promise<any> | any,
|
|
201
|
+
filename: string = 'export.csv',
|
|
202
|
+
options: CsvResponseOptions = {}
|
|
203
|
+
): any {
|
|
204
|
+
return async (c: HonoContext) => {
|
|
205
|
+
try {
|
|
206
|
+
const data = await (typeof dataProvider === 'function' ? dataProvider() : dataProvider);
|
|
207
|
+
return asyncCsvResponse(data, filename, options);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return new Response(
|
|
210
|
+
JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }),
|
|
211
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default {
|
|
218
|
+
csvMiddleware,
|
|
219
|
+
asyncCsvMiddleware,
|
|
220
|
+
createCsvResponse,
|
|
221
|
+
createAsyncCsvResponse,
|
|
222
|
+
csvResponse,
|
|
223
|
+
asyncCsvResponse,
|
|
224
|
+
csvParseEndpoint,
|
|
225
|
+
csvDownloadEndpoint,
|
|
226
|
+
};
|