jtcsv 1.2.0 → 2.1.1
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 +272 -329
- package/bin/jtcsv.js +1092 -97
- package/cli-tui.js +0 -0
- package/csv-to-json.js +385 -311
- package/dist/jtcsv.cjs.js +1619 -0
- package/dist/jtcsv.cjs.js.map +1 -0
- package/dist/jtcsv.esm.js +1599 -0
- package/dist/jtcsv.esm.js.map +1 -0
- package/dist/jtcsv.umd.js +1625 -0
- package/dist/jtcsv.umd.js.map +1 -0
- package/examples/cli-tool.js +186 -0
- package/examples/express-api.js +167 -0
- package/examples/large-dataset-example.js +185 -0
- package/examples/plugin-excel-exporter.js +407 -0
- package/examples/simple-usage.js +280 -0
- package/examples/streaming-example.js +419 -0
- package/index.d.ts +288 -1
- package/index.js +23 -0
- package/json-save.js +1 -1
- package/json-to-csv.js +130 -89
- package/package.json +139 -13
- package/plugins/README.md +373 -0
- package/plugins/express-middleware/README.md +306 -0
- package/plugins/express-middleware/example.js +136 -0
- package/plugins/express-middleware/index.d.ts +114 -0
- package/plugins/express-middleware/index.js +360 -0
- package/plugins/express-middleware/package.json +52 -0
- package/plugins/fastify-plugin/index.js +406 -0
- package/plugins/fastify-plugin/package.json +55 -0
- package/plugins/nextjs-api/README.md +452 -0
- package/plugins/nextjs-api/examples/ConverterComponent.jsx +386 -0
- package/plugins/nextjs-api/examples/api-convert.js +69 -0
- package/plugins/nextjs-api/index.js +388 -0
- package/plugins/nextjs-api/package.json +63 -0
- package/plugins/nextjs-api/route.js +372 -0
- package/src/browser/browser-functions.js +189 -0
- package/src/browser/csv-to-json-browser.js +442 -0
- package/src/browser/errors-browser.js +194 -0
- package/src/browser/index.js +79 -0
- package/src/browser/json-to-csv-browser.js +309 -0
- package/src/browser/workers/csv-parser.worker.js +359 -0
- package/src/browser/workers/worker-pool.js +467 -0
- package/src/core/delimiter-cache.js +186 -0
- package/src/core/plugin-system.js +472 -0
- package/src/core/transform-hooks.js +350 -0
- package/src/engines/fast-path-engine-new.js +338 -0
- package/src/engines/fast-path-engine.js +836 -0
- package/src/formats/ndjson-parser.js +419 -0
- package/src/formats/tsv-parser.js +336 -0
- package/src/index-with-plugins.js +371 -0
- package/stream-csv-to-json.js +1 -1
- package/stream-json-to-csv.js +1 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TSV (Tab-Separated Values) парсер
|
|
3
|
+
* Специализированная поддержка TSV формата
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @date 2026-01-23
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { csvToJson } = require('../../csv-to-json');
|
|
10
|
+
const { jsonToCsv } = require('../../json-to-csv');
|
|
11
|
+
const { ValidationError, SecurityError, FileSystemError } = require('../../errors');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
function validateTsvFilePath(filePath) {
|
|
15
|
+
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
16
|
+
throw new ValidationError('File path must be a non-empty string');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!filePath.toLowerCase().endsWith('.tsv')) {
|
|
20
|
+
throw new ValidationError('File must have .tsv extension');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const normalizedPath = path.normalize(filePath);
|
|
24
|
+
if (normalizedPath.includes('..') ||
|
|
25
|
+
/\\\.\.\\|\/\.\.\//.test(filePath) ||
|
|
26
|
+
filePath.startsWith('..') ||
|
|
27
|
+
filePath.includes('/..')) {
|
|
28
|
+
throw new SecurityError('Directory traversal detected in file path');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return path.resolve(filePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class TsvParser {
|
|
35
|
+
/**
|
|
36
|
+
* Конвертирует массив объектов в TSV строку
|
|
37
|
+
* @param {Array} data - Массив объектов
|
|
38
|
+
* @param {Object} options - Опции форматирования
|
|
39
|
+
* @returns {string} TSV строка
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const data = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
|
|
43
|
+
* const tsv = TsvParser.jsonToTsv(data);
|
|
44
|
+
* // Результат: "id\tname\n1\tJohn\n2\tJane"
|
|
45
|
+
*/
|
|
46
|
+
static jsonToTsv(data, options = {}) {
|
|
47
|
+
const defaultOptions = {
|
|
48
|
+
delimiter: '\t',
|
|
49
|
+
includeHeaders: true,
|
|
50
|
+
...options
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return jsonToCsv(data, defaultOptions);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Конвертирует TSV строку в массив объектов
|
|
58
|
+
* @param {string} tsvString - TSV строка
|
|
59
|
+
* @param {Object} options - Опции парсинга
|
|
60
|
+
* @returns {Array} Массив объектов
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* const tsv = "id\tname\n1\tJohn\n2\tJane";
|
|
64
|
+
* const data = TsvParser.tsvToJson(tsv);
|
|
65
|
+
* // Результат: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
|
|
66
|
+
*/
|
|
67
|
+
static tsvToJson(tsvString, options = {}) {
|
|
68
|
+
const defaultOptions = {
|
|
69
|
+
delimiter: '\t',
|
|
70
|
+
autoDetect: false,
|
|
71
|
+
hasHeaders: true,
|
|
72
|
+
...options
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return csvToJson(tsvString, defaultOptions);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Автоматически определяет является ли строка TSV
|
|
80
|
+
* @param {string} sample - Образец данных
|
|
81
|
+
* @returns {boolean} True если это TSV
|
|
82
|
+
*/
|
|
83
|
+
static isTsv(sample) {
|
|
84
|
+
if (!sample || typeof sample !== 'string') {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const lines = sample.split('\n').slice(0, 10);
|
|
89
|
+
let tabCount = 0;
|
|
90
|
+
let commaCount = 0;
|
|
91
|
+
let semicolonCount = 0;
|
|
92
|
+
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (line.trim() === '') {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Считаем разделители
|
|
99
|
+
tabCount += (line.match(/\t/g) || []).length;
|
|
100
|
+
commaCount += (line.match(/,/g) || []).length;
|
|
101
|
+
semicolonCount += (line.match(/;/g) || []).length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Если табуляций больше чем других разделителей, считаем это TSV
|
|
105
|
+
return tabCount > commaCount && tabCount > semicolonCount;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Создает TransformStream для конвертации JSON в TSV
|
|
110
|
+
* @param {Object} options - Опции конвертации
|
|
111
|
+
* @returns {TransformStream} Transform stream
|
|
112
|
+
*/
|
|
113
|
+
static createJsonToTsvStream(options = {}) {
|
|
114
|
+
const { createJsonToCsvStream } = require('../../stream-json-to-csv');
|
|
115
|
+
|
|
116
|
+
return createJsonToCsvStream({
|
|
117
|
+
delimiter: '\t',
|
|
118
|
+
...options
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Создает TransformStream для конвертации TSV в JSON
|
|
124
|
+
* @param {Object} options - Опции конвертации
|
|
125
|
+
* @returns {TransformStream} Transform stream
|
|
126
|
+
*/
|
|
127
|
+
static createTsvToJsonStream(options = {}) {
|
|
128
|
+
const { createCsvToJsonStream } = require('../../stream-csv-to-json');
|
|
129
|
+
|
|
130
|
+
return createCsvToJsonStream({
|
|
131
|
+
delimiter: '\t',
|
|
132
|
+
autoDetect: false,
|
|
133
|
+
...options
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Читает TSV файл и конвертирует в JSON
|
|
139
|
+
* @param {string} filePath - Путь к TSV файлу
|
|
140
|
+
* @param {Object} options - Опции парсинга
|
|
141
|
+
* @returns {Promise<Array>} Promise с массивом объектов
|
|
142
|
+
*/
|
|
143
|
+
static async readTsvAsJson(filePath, options = {}) {
|
|
144
|
+
const fs = require('fs').promises;
|
|
145
|
+
const safePath = validateTsvFilePath(filePath);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const tsvContent = await fs.readFile(safePath, 'utf8');
|
|
149
|
+
return csvToJson(tsvContent, {
|
|
150
|
+
delimiter: '\t',
|
|
151
|
+
autoDetect: false,
|
|
152
|
+
...options
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof ValidationError || error instanceof SecurityError) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
if (error.code === 'ENOENT') {
|
|
159
|
+
throw new FileSystemError(`File not found: ${safePath}`, error);
|
|
160
|
+
}
|
|
161
|
+
if (error.code === 'EACCES') {
|
|
162
|
+
throw new FileSystemError(`Permission denied: ${safePath}`, error);
|
|
163
|
+
}
|
|
164
|
+
if (error.code === 'EISDIR') {
|
|
165
|
+
throw new FileSystemError(`Path is a directory: ${safePath}`, error);
|
|
166
|
+
}
|
|
167
|
+
throw new FileSystemError(`Failed to read TSV file: ${error.message}`, error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Синхронно читает TSV файл и конвертирует в JSON
|
|
173
|
+
* @param {string} filePath - Путь к TSV файлу
|
|
174
|
+
* @param {Object} options - Опции парсинга
|
|
175
|
+
* @returns {Array} Массив объектов
|
|
176
|
+
*/
|
|
177
|
+
static readTsvAsJsonSync(filePath, options = {}) {
|
|
178
|
+
const fs = require('fs');
|
|
179
|
+
const safePath = validateTsvFilePath(filePath);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const tsvContent = fs.readFileSync(safePath, 'utf8');
|
|
183
|
+
return csvToJson(tsvContent, {
|
|
184
|
+
delimiter: '\t',
|
|
185
|
+
autoDetect: false,
|
|
186
|
+
...options
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof ValidationError || error instanceof SecurityError) {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
if (error.code === 'ENOENT') {
|
|
193
|
+
throw new FileSystemError(`File not found: ${safePath}`, error);
|
|
194
|
+
}
|
|
195
|
+
if (error.code === 'EACCES') {
|
|
196
|
+
throw new FileSystemError(`Permission denied: ${safePath}`, error);
|
|
197
|
+
}
|
|
198
|
+
if (error.code === 'EISDIR') {
|
|
199
|
+
throw new FileSystemError(`Path is a directory: ${safePath}`, error);
|
|
200
|
+
}
|
|
201
|
+
throw new FileSystemError(`Failed to read TSV file: ${error.message}`, error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Сохраняет массив объектов как TSV файл
|
|
207
|
+
* @param {Array} data - Массив объектов
|
|
208
|
+
* @param {string} filePath - Путь для сохранения
|
|
209
|
+
* @param {Object} options - Опции сохранения
|
|
210
|
+
* @returns {Promise<void>}
|
|
211
|
+
*/
|
|
212
|
+
static async saveAsTsv(data, filePath, options = {}) {
|
|
213
|
+
const fs = require('fs').promises;
|
|
214
|
+
const safePath = validateTsvFilePath(filePath);
|
|
215
|
+
const tsvContent = this.jsonToTsv(data, options);
|
|
216
|
+
const dir = path.dirname(safePath);
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await fs.mkdir(dir, { recursive: true });
|
|
220
|
+
await fs.writeFile(safePath, tsvContent, 'utf8');
|
|
221
|
+
return safePath;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error.code === 'ENOENT') {
|
|
224
|
+
throw new FileSystemError(`Directory does not exist: ${dir}`, error);
|
|
225
|
+
}
|
|
226
|
+
if (error.code === 'EACCES') {
|
|
227
|
+
throw new FileSystemError(`Permission denied: ${safePath}`, error);
|
|
228
|
+
}
|
|
229
|
+
if (error.code === 'ENOSPC') {
|
|
230
|
+
throw new FileSystemError(`No space left on device: ${safePath}`, error);
|
|
231
|
+
}
|
|
232
|
+
throw new FileSystemError(`Failed to save TSV file: ${error.message}`, error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Синхронно сохраняет массив объектов как TSV файл
|
|
238
|
+
* @param {Array} data - Массив объектов
|
|
239
|
+
* @param {string} filePath - Путь для сохранения
|
|
240
|
+
* @param {Object} options - Опции сохранения
|
|
241
|
+
*/
|
|
242
|
+
static saveAsTsvSync(data, filePath, options = {}) {
|
|
243
|
+
const fs = require('fs');
|
|
244
|
+
const safePath = validateTsvFilePath(filePath);
|
|
245
|
+
const tsvContent = this.jsonToTsv(data, options);
|
|
246
|
+
|
|
247
|
+
fs.mkdirSync(path.dirname(safePath), { recursive: true });
|
|
248
|
+
fs.writeFileSync(safePath, tsvContent, 'utf8');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Валидирует TSV строку
|
|
253
|
+
* @param {string} tsvString - TSV строка для валидации
|
|
254
|
+
* @param {Object} options - Опции валидации
|
|
255
|
+
* @returns {Object} Результат валидации
|
|
256
|
+
*/
|
|
257
|
+
static validateTsv(tsvString, options = {}) {
|
|
258
|
+
const { requireConsistentColumns = true } = options;
|
|
259
|
+
|
|
260
|
+
if (!tsvString || typeof tsvString !== 'string') {
|
|
261
|
+
return {
|
|
262
|
+
valid: false,
|
|
263
|
+
error: 'Input must be a non-empty string',
|
|
264
|
+
details: { inputType: typeof tsvString }
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const lines = tsvString.split('\n').filter(line => line.trim() !== '');
|
|
269
|
+
|
|
270
|
+
if (lines.length === 0) {
|
|
271
|
+
return {
|
|
272
|
+
valid: false,
|
|
273
|
+
error: 'No data found in TSV',
|
|
274
|
+
details: { lineCount: 0 }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const columnCounts = [];
|
|
279
|
+
const errors = [];
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < lines.length; i++) {
|
|
282
|
+
const line = lines[i];
|
|
283
|
+
const columns = line.split('\t');
|
|
284
|
+
columnCounts.push(columns.length);
|
|
285
|
+
|
|
286
|
+
// Проверяем наличие пустых полей (если требуется)
|
|
287
|
+
if (options.disallowEmptyFields) {
|
|
288
|
+
const emptyFields = columns.filter(field => field.trim() === '');
|
|
289
|
+
if (emptyFields.length > 0) {
|
|
290
|
+
errors.push({
|
|
291
|
+
line: i + 1,
|
|
292
|
+
error: `Found ${emptyFields.length} empty field(s)`,
|
|
293
|
+
fields: emptyFields.map((_, idx) => idx + 1)
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Проверяем консистентность колонки
|
|
300
|
+
if (requireConsistentColumns && columnCounts.length > 1) {
|
|
301
|
+
const firstCount = columnCounts[0];
|
|
302
|
+
const inconsistentLines = [];
|
|
303
|
+
|
|
304
|
+
for (let i = 1; i < columnCounts.length; i++) {
|
|
305
|
+
if (columnCounts[i] !== firstCount) {
|
|
306
|
+
inconsistentLines.push({
|
|
307
|
+
line: i + 1,
|
|
308
|
+
expected: firstCount,
|
|
309
|
+
actual: columnCounts[i]
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (inconsistentLines.length > 0) {
|
|
315
|
+
errors.push({
|
|
316
|
+
error: 'Inconsistent column count',
|
|
317
|
+
details: inconsistentLines
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
valid: errors.length === 0,
|
|
324
|
+
stats: {
|
|
325
|
+
totalLines: lines.length,
|
|
326
|
+
totalColumns: columnCounts[0] || 0,
|
|
327
|
+
minColumns: Math.min(...columnCounts),
|
|
328
|
+
maxColumns: Math.max(...columnCounts),
|
|
329
|
+
consistentColumns: new Set(columnCounts).size === 1
|
|
330
|
+
},
|
|
331
|
+
errors: errors.length > 0 ? errors : undefined
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = TsvParser;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JTCSV с поддержкой плагинов
|
|
3
|
+
* Расширяемая версия основного API с plugin system
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @date 2026-01-22
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const PluginManager = require('./core/plugin-system');
|
|
10
|
+
const FastPathEngine = require('./engines/fast-path-engine');
|
|
11
|
+
const NdjsonParser = require('./formats/ndjson-parser');
|
|
12
|
+
|
|
13
|
+
// Импортируем основные функции
|
|
14
|
+
const coreJsonToCsv = require('../json-to-csv').jsonToCsv;
|
|
15
|
+
const coreCsvToJson = require('../csv-to-json').csvToJson;
|
|
16
|
+
const coreCsvToJsonIterator = require('../csv-to-json').csvToJsonIterator;
|
|
17
|
+
const coreSaveAsCsv = require('../json-to-csv').saveAsCsv;
|
|
18
|
+
const coreReadCsvAsJson = require('../csv-to-json').readCsvAsJson;
|
|
19
|
+
|
|
20
|
+
class JtcsvWithPlugins {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.pluginManager = new PluginManager();
|
|
23
|
+
this.fastPathEngine = new FastPathEngine();
|
|
24
|
+
this.options = {
|
|
25
|
+
enableFastPath: true,
|
|
26
|
+
enablePlugins: true,
|
|
27
|
+
...options
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Регистрируем встроенные плагины
|
|
31
|
+
this._registerBuiltinPlugins();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Регистрирует встроенные плагины
|
|
36
|
+
*/
|
|
37
|
+
_registerBuiltinPlugins() {
|
|
38
|
+
// Fast Path Engine плагин
|
|
39
|
+
this.pluginManager.use('fast-path-engine', {
|
|
40
|
+
name: 'Fast Path Engine',
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
description: 'Оптимизированный парсер CSV с автоматическим выбором стратегии',
|
|
43
|
+
hooks: {
|
|
44
|
+
'before:csvToJson': (csv, context) => {
|
|
45
|
+
if (this.options.enableFastPath && context.options?.useFastPath !== false) {
|
|
46
|
+
// Используем fast path engine для анализа
|
|
47
|
+
const sample = csv.substring(0, Math.min(1000, csv.length));
|
|
48
|
+
const structure = this.fastPathEngine.analyzeStructure(sample, context.options);
|
|
49
|
+
|
|
50
|
+
context.metadata.fastPathStructure = structure;
|
|
51
|
+
console.log(`🚀 Используется ${structure.recommendedEngine} парсер`);
|
|
52
|
+
}
|
|
53
|
+
return csv;
|
|
54
|
+
},
|
|
55
|
+
'after:csvToJson': (result, context) => {
|
|
56
|
+
if (context.metadata?.fastPathStructure) {
|
|
57
|
+
context.metadata.fastPathStats = this.fastPathEngine.getStats();
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// NDJSON плагин
|
|
65
|
+
this.pluginManager.use('ndjson-support', {
|
|
66
|
+
name: 'NDJSON Support',
|
|
67
|
+
version: '1.0.0',
|
|
68
|
+
description: 'Поддержка Newline Delimited JSON формата',
|
|
69
|
+
hooks: {
|
|
70
|
+
'before:parse': (input, context) => {
|
|
71
|
+
if (context.options?.format === 'ndjson') {
|
|
72
|
+
// Парсим NDJSON
|
|
73
|
+
return NdjsonParser.fromNdjson(input, context.options);
|
|
74
|
+
}
|
|
75
|
+
return input;
|
|
76
|
+
},
|
|
77
|
+
'after:serialize': (output, context) => {
|
|
78
|
+
if (context.options?.format === 'ndjson') {
|
|
79
|
+
// Сериализуем в NDJSON
|
|
80
|
+
return NdjsonParser.toNdjson(output, context.options);
|
|
81
|
+
}
|
|
82
|
+
return output;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Валидация данных плагин
|
|
88
|
+
this.pluginManager.use('data-validation', {
|
|
89
|
+
name: 'Data Validation',
|
|
90
|
+
version: '1.0.0',
|
|
91
|
+
description: 'Валидация входных и выходных данных',
|
|
92
|
+
hooks: {
|
|
93
|
+
'validation': (data, context) => {
|
|
94
|
+
if (!data) {
|
|
95
|
+
throw new Error('Данные не могут быть пустыми');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (context.operation === 'jsonToCsv' && !Array.isArray(data)) {
|
|
99
|
+
throw new Error('Для конвертации в CSV данные должны быть массивом');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return data;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
middlewares: [
|
|
106
|
+
async (ctx, next) => {
|
|
107
|
+
// Валидация перед выполнением
|
|
108
|
+
await this.pluginManager.executeHooks('validation', ctx.input, ctx);
|
|
109
|
+
await next();
|
|
110
|
+
// Валидация после выполнения
|
|
111
|
+
await this.pluginManager.executeHooks('validation', ctx.result, ctx);
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Логирование плагин
|
|
117
|
+
this.pluginManager.use('logging', {
|
|
118
|
+
name: 'Logging',
|
|
119
|
+
version: '1.0.0',
|
|
120
|
+
description: 'Логирование операций',
|
|
121
|
+
hooks: {
|
|
122
|
+
'before:csvToJson': (csv, context) => {
|
|
123
|
+
console.log(`📥 Начало csvToJson, размер: ${csv.length} байт`);
|
|
124
|
+
return csv;
|
|
125
|
+
},
|
|
126
|
+
'after:csvToJson': (result, context) => {
|
|
127
|
+
console.log(`📤 Завершение csvToJson, результат: ${result.length} записей`);
|
|
128
|
+
return result;
|
|
129
|
+
},
|
|
130
|
+
'before:jsonToCsv': (json, context) => {
|
|
131
|
+
console.log(`📥 Начало jsonToCsv, записей: ${json.length}`);
|
|
132
|
+
return json;
|
|
133
|
+
},
|
|
134
|
+
'after:jsonToCsv': (csv, context) => {
|
|
135
|
+
console.log(`📤 Завершение jsonToCsv, размер: ${csv.length} байт`);
|
|
136
|
+
return csv;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Конвертирует CSV в JSON с поддержкой плагинов
|
|
144
|
+
* @param {string} csv - CSV данные
|
|
145
|
+
* @param {Object} options - Опции парсинга
|
|
146
|
+
* @returns {Promise<Array>} JSON данные
|
|
147
|
+
*/
|
|
148
|
+
async csvToJson(csv, options = {}) {
|
|
149
|
+
if (!this.options.enablePlugins) {
|
|
150
|
+
return coreCsvToJson(csv, options);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return this.pluginManager.executeWithPlugins(
|
|
154
|
+
'csvToJson',
|
|
155
|
+
csv,
|
|
156
|
+
options,
|
|
157
|
+
(input, opts) => {
|
|
158
|
+
if (this.options.enableFastPath && opts?.useFastPath !== false) {
|
|
159
|
+
return coreCsvToJson(input, { ...opts, useFastPath: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return coreCsvToJson(input, opts);
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Convert CSV to JSON rows as async iterator with plugin hooks.
|
|
169
|
+
* @param {string} csv - CSV input
|
|
170
|
+
* @param {Object} options - Conversion options
|
|
171
|
+
* @returns {AsyncGenerator} Async iterator of rows
|
|
172
|
+
*/
|
|
173
|
+
async *csvToJsonIterator(csv, options = {}) {
|
|
174
|
+
if (!this.options.enablePlugins) {
|
|
175
|
+
for await (const row of coreCsvToJsonIterator(csv, options)) {
|
|
176
|
+
yield row;
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const iterator = await this.pluginManager.executeWithPlugins(
|
|
182
|
+
'csvToJson',
|
|
183
|
+
csv,
|
|
184
|
+
options,
|
|
185
|
+
(input, opts) => {
|
|
186
|
+
if (this.options.enableFastPath && opts?.useFastPath !== false) {
|
|
187
|
+
return coreCsvToJsonIterator(input, { ...opts, useFastPath: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return coreCsvToJsonIterator(input, opts);
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
for await (const row of iterator) {
|
|
195
|
+
yield row;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Конвертирует JSON в CSV с поддержкой плагинов
|
|
201
|
+
* @param {Array} json - JSON данные
|
|
202
|
+
* @param {Object} options - Опции сериализации
|
|
203
|
+
* @returns {Promise<string>} CSV данные
|
|
204
|
+
*/
|
|
205
|
+
async jsonToCsv(json, options = {}) {
|
|
206
|
+
if (!this.options.enablePlugins) {
|
|
207
|
+
return coreJsonToCsv(json, options);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return this.pluginManager.executeWithPlugins(
|
|
211
|
+
'jsonToCsv',
|
|
212
|
+
json,
|
|
213
|
+
options,
|
|
214
|
+
coreJsonToCsv
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Сохраняет JSON как CSV файл
|
|
220
|
+
* @param {Array} data - JSON данные
|
|
221
|
+
* @param {string} filePath - Путь к файлу
|
|
222
|
+
* @param {Object} options - Опции
|
|
223
|
+
* @returns {Promise<void>}
|
|
224
|
+
*/
|
|
225
|
+
async saveAsCsv(data, filePath, options = {}) {
|
|
226
|
+
if (!this.options.enablePlugins) {
|
|
227
|
+
return coreSaveAsCsv(data, filePath, options);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const csv = await this.jsonToCsv(data, options);
|
|
231
|
+
|
|
232
|
+
// Используем плагины для сохранения
|
|
233
|
+
return this.pluginManager.executeWithPlugins(
|
|
234
|
+
'saveAsCsv',
|
|
235
|
+
{ data: csv, filePath },
|
|
236
|
+
options,
|
|
237
|
+
async (input) => {
|
|
238
|
+
const fs = require('fs').promises;
|
|
239
|
+
await fs.writeFile(input.filePath, input.data, 'utf8');
|
|
240
|
+
return input.filePath;
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Читает CSV файл и конвертирует в JSON
|
|
247
|
+
* @param {string} filePath - Путь к файлу
|
|
248
|
+
* @param {Object} options - Опции
|
|
249
|
+
* @returns {Promise<Array>} JSON данные
|
|
250
|
+
*/
|
|
251
|
+
async readCsvAsJson(filePath, options = {}) {
|
|
252
|
+
if (!this.options.enablePlugins) {
|
|
253
|
+
return coreReadCsvAsJson(filePath, options);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Читаем файл
|
|
257
|
+
const fs = require('fs').promises;
|
|
258
|
+
const csv = await fs.readFile(filePath, 'utf8');
|
|
259
|
+
|
|
260
|
+
// Конвертируем с использованием плагинов
|
|
261
|
+
return this.csvToJson(csv, options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Парсит NDJSON данные
|
|
266
|
+
* @param {string|ReadableStream} input - NDJSON данные
|
|
267
|
+
* @param {Object} options - Опции
|
|
268
|
+
* @returns {Promise<Array>} JSON данные
|
|
269
|
+
*/
|
|
270
|
+
async parseNdjson(input, options = {}) {
|
|
271
|
+
if (typeof input === 'string') {
|
|
272
|
+
return NdjsonParser.fromNdjson(input, options);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Для потоков
|
|
276
|
+
const result = [];
|
|
277
|
+
for await (const obj of NdjsonParser.parseStream(input, options)) {
|
|
278
|
+
result.push(obj);
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Конвертирует JSON в NDJSON
|
|
285
|
+
* @param {Array} data - JSON данные
|
|
286
|
+
* @param {Object} options - Опции
|
|
287
|
+
* @returns {string} NDJSON строка
|
|
288
|
+
*/
|
|
289
|
+
toNdjson(data, options = {}) {
|
|
290
|
+
return NdjsonParser.toNdjson(data, options);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Регистрирует плагин
|
|
295
|
+
* @param {string} name - Имя плагина
|
|
296
|
+
* @param {Object} plugin - Конфигурация плагина
|
|
297
|
+
* @returns {JtcsvWithPlugins} this для chaining
|
|
298
|
+
*/
|
|
299
|
+
use(name, plugin) {
|
|
300
|
+
this.pluginManager.use(name, plugin);
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Возвращает менеджер плагинов
|
|
306
|
+
* @returns {PluginManager}
|
|
307
|
+
*/
|
|
308
|
+
getPluginManager() {
|
|
309
|
+
return this.pluginManager;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Возвращает fast path engine
|
|
314
|
+
* @returns {FastPathEngine}
|
|
315
|
+
*/
|
|
316
|
+
getFastPathEngine() {
|
|
317
|
+
return this.fastPathEngine;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Возвращает список плагинов
|
|
322
|
+
* @returns {Array}
|
|
323
|
+
*/
|
|
324
|
+
listPlugins() {
|
|
325
|
+
return this.pluginManager.listPlugins();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Возвращает статистику
|
|
330
|
+
* @returns {Object}
|
|
331
|
+
*/
|
|
332
|
+
getStats() {
|
|
333
|
+
return {
|
|
334
|
+
plugins: this.pluginManager.getStats(),
|
|
335
|
+
fastPath: this.fastPathEngine.getStats(),
|
|
336
|
+
options: this.options
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Настраивает опции
|
|
342
|
+
* @param {Object} newOptions - Новые опции
|
|
343
|
+
*/
|
|
344
|
+
configure(newOptions) {
|
|
345
|
+
this.options = { ...this.options, ...newOptions };
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Создает экземпляр с настройками по умолчанию
|
|
351
|
+
* @param {Object} options - Опции
|
|
352
|
+
* @returns {JtcsvWithPlugins}
|
|
353
|
+
*/
|
|
354
|
+
static create(options = {}) {
|
|
355
|
+
return new JtcsvWithPlugins(options);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Экспортируем основной класс
|
|
360
|
+
module.exports = JtcsvWithPlugins;
|
|
361
|
+
|
|
362
|
+
// Экспортируем утилиты
|
|
363
|
+
module.exports.PluginManager = PluginManager;
|
|
364
|
+
module.exports.FastPathEngine = FastPathEngine;
|
|
365
|
+
module.exports.NdjsonParser = NdjsonParser;
|
|
366
|
+
|
|
367
|
+
// Экспортируем фабричный метод
|
|
368
|
+
module.exports.create = JtcsvWithPlugins.create;
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
|
package/stream-csv-to-json.js
CHANGED