jtcsv 2.1.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 +43 -15
- package/bin/jtcsv.js +1030 -117
- package/cli-tui.js +1494 -1
- package/csv-to-json.js +385 -311
- package/index.d.ts +288 -5
- package/index.js +23 -0
- package/json-to-csv.js +130 -89
- package/package.json +25 -13
- package/src/core/delimiter-cache.js +186 -0
- package/src/core/transform-hooks.js +350 -0
- package/src/engines/fast-path-engine.js +829 -340
- package/src/formats/tsv-parser.js +336 -0
- package/src/index-with-plugins.js +36 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jtcsv",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Complete JSON↔CSV and CSV↔JSON converter for Node.js and Browser with streaming, security, Web Workers, TypeScript support, and Plugin System - Zero dependencies",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"browser": "dist/jtcsv.umd.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"jtcsv": "bin/jtcsv.js"
|
|
11
11
|
},
|
|
12
|
-
|
|
12
|
+
"exports": {
|
|
13
13
|
".": {
|
|
14
14
|
"require": "./index.js",
|
|
15
15
|
"import": "./dist/jtcsv.esm.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"test:plugins": "jest __tests__/plugin-system.test.js",
|
|
57
57
|
"test:fastpath": "jest __tests__/fast-path-engine.test.js",
|
|
58
58
|
"test:ndjson": "jest __tests__/ndjson-parser.test.js",
|
|
59
|
-
|
|
59
|
+
"test:performance": "jest __tests__/*.test.js --testNamePattern=\"Производительность|производительность\"",
|
|
60
60
|
"test:express": "cd plugins/express-middleware && npm test",
|
|
61
61
|
"test:fastify": "cd plugins/fastify-plugin && npm test",
|
|
62
62
|
"test:nextjs": "cd plugins/nextjs-api && npm test",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"gui",
|
|
119
119
|
"tool",
|
|
120
120
|
"utility",
|
|
121
|
-
|
|
121
|
+
"plugin-system",
|
|
122
122
|
"ndjson",
|
|
123
123
|
"fast-path",
|
|
124
124
|
"optimization",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"stream-csv-to-json.js",
|
|
156
156
|
"json-save.js",
|
|
157
157
|
"index.d.ts",
|
|
158
|
-
|
|
158
|
+
"bin/",
|
|
159
159
|
"cli-tui.js",
|
|
160
160
|
"dist/",
|
|
161
161
|
"src/",
|
|
@@ -169,33 +169,45 @@
|
|
|
169
169
|
"@rollup/plugin-commonjs": "^25.0.0",
|
|
170
170
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
171
171
|
"@rollup/plugin-terser": "^0.4.0",
|
|
172
|
+
"@size-limit/preset-small-lib": "12.0.0",
|
|
172
173
|
"eslint": "8.57.1",
|
|
173
174
|
"jest": "^29.0.0",
|
|
174
175
|
"rollup": "^4.0.0",
|
|
175
|
-
"size-limit": "
|
|
176
|
+
"size-limit": "12.0.0"
|
|
176
177
|
},
|
|
177
178
|
"optionalDependencies": {
|
|
178
179
|
"blessed": "^0.1.81",
|
|
179
|
-
"blessed-contrib": "
|
|
180
|
+
"blessed-contrib": "4.11.0",
|
|
180
181
|
"exceljs": "^4.4.0"
|
|
181
182
|
},
|
|
183
|
+
"overrides": {
|
|
184
|
+
"xml2js": "0.6.2"
|
|
185
|
+
},
|
|
182
186
|
"type": "commonjs",
|
|
183
187
|
"size-limit": [
|
|
184
188
|
{
|
|
185
189
|
"path": "dist/jtcsv.umd.js",
|
|
186
|
-
"limit": "
|
|
190
|
+
"limit": "60 KB",
|
|
191
|
+
"ignore": [
|
|
192
|
+
"url"
|
|
193
|
+
]
|
|
187
194
|
},
|
|
188
195
|
{
|
|
189
196
|
"path": "dist/jtcsv.esm.js",
|
|
190
|
-
"limit": "
|
|
197
|
+
"limit": "55 KB",
|
|
198
|
+
"ignore": [
|
|
199
|
+
"url"
|
|
200
|
+
]
|
|
191
201
|
}
|
|
192
202
|
],
|
|
193
203
|
"browserslist": [
|
|
194
204
|
"defaults",
|
|
195
205
|
"not IE 11",
|
|
196
206
|
"maintained node versions"
|
|
197
|
-
]
|
|
207
|
+
],
|
|
208
|
+
"dependencies": {
|
|
209
|
+
"csv-parser": "3.2.0",
|
|
210
|
+
"json2csv": "6.0.0-alpha.2",
|
|
211
|
+
"papaparse": "5.5.3"
|
|
212
|
+
}
|
|
198
213
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Кэширование результатов авто-детектирования разделителя
|
|
3
|
+
* Использует WeakMap и LRU кэш для оптимизации производительности
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @date 2026-01-23
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class DelimiterCache {
|
|
10
|
+
constructor(maxSize = 100) {
|
|
11
|
+
this.weakMap = new WeakMap();
|
|
12
|
+
this.lruCache = new Map();
|
|
13
|
+
this.maxSize = maxSize;
|
|
14
|
+
this.stats = {
|
|
15
|
+
hits: 0,
|
|
16
|
+
misses: 0,
|
|
17
|
+
evictions: 0,
|
|
18
|
+
size: 0
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Генерирует ключ кэша на основе строки и кандидатов
|
|
24
|
+
* @private
|
|
25
|
+
*/
|
|
26
|
+
_generateKey(csv, candidates) {
|
|
27
|
+
// Используем хэш первых 1000 символов для производительности
|
|
28
|
+
const sample = csv.substring(0, Math.min(1000, csv.length));
|
|
29
|
+
const candidatesKey = candidates.join(',');
|
|
30
|
+
return `${this._hashString(sample)}:${candidatesKey}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Простая хэш-функция для строк
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
_hashString(str) {
|
|
38
|
+
let hash = 0;
|
|
39
|
+
for (let i = 0; i < str.length; i++) {
|
|
40
|
+
const char = str.charCodeAt(i);
|
|
41
|
+
hash = ((hash << 5) - hash) + char;
|
|
42
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
43
|
+
}
|
|
44
|
+
return hash.toString(36);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Получает значение из кэша
|
|
49
|
+
* @param {string} csv - CSV строка
|
|
50
|
+
* @param {Array} candidates - Кандидаты разделителей
|
|
51
|
+
* @returns {string|null} Кэшированный разделитель или null
|
|
52
|
+
*/
|
|
53
|
+
get(csv, candidates) {
|
|
54
|
+
const key = this._generateKey(csv, candidates);
|
|
55
|
+
|
|
56
|
+
// Проверяем LRU кэш
|
|
57
|
+
if (this.lruCache.has(key)) {
|
|
58
|
+
// Обновляем позицию в LRU
|
|
59
|
+
const value = this.lruCache.get(key);
|
|
60
|
+
this.lruCache.delete(key);
|
|
61
|
+
this.lruCache.set(key, value);
|
|
62
|
+
this.stats.hits++;
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.stats.misses++;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Сохраняет значение в кэш
|
|
72
|
+
* @param {string} csv - CSV строка
|
|
73
|
+
* @param {Array} candidates - Кандидаты разделителей
|
|
74
|
+
* @param {string} delimiter - Найденный разделитель
|
|
75
|
+
*/
|
|
76
|
+
set(csv, candidates, delimiter) {
|
|
77
|
+
const key = this._generateKey(csv, candidates);
|
|
78
|
+
|
|
79
|
+
// Проверяем необходимость вытеснения из LRU кэша
|
|
80
|
+
if (this.lruCache.size >= this.maxSize) {
|
|
81
|
+
// Удаляем самый старый элемент (первый в Map)
|
|
82
|
+
const firstKey = this.lruCache.keys().next().value;
|
|
83
|
+
this.lruCache.delete(firstKey);
|
|
84
|
+
this.stats.evictions++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Сохраняем в LRU кэш
|
|
88
|
+
this.lruCache.set(key, delimiter);
|
|
89
|
+
this.stats.size = this.lruCache.size;
|
|
90
|
+
|
|
91
|
+
// Также сохраняем в WeakMap если csv является объектом
|
|
92
|
+
if (typeof csv === 'object' && csv !== null) {
|
|
93
|
+
this.weakMap.set(csv, { candidates, delimiter });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Очищает кэш
|
|
99
|
+
*/
|
|
100
|
+
clear() {
|
|
101
|
+
this.lruCache.clear();
|
|
102
|
+
this.weakMap = new WeakMap();
|
|
103
|
+
this.stats = {
|
|
104
|
+
hits: 0,
|
|
105
|
+
misses: 0,
|
|
106
|
+
evictions: 0,
|
|
107
|
+
size: 0
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Возвращает статистику кэша
|
|
113
|
+
* @returns {Object} Статистика
|
|
114
|
+
*/
|
|
115
|
+
getStats() {
|
|
116
|
+
const total = this.stats.hits + this.stats.misses;
|
|
117
|
+
return {
|
|
118
|
+
...this.stats,
|
|
119
|
+
hitRate: total > 0 ? (this.stats.hits / total) * 100 : 0,
|
|
120
|
+
totalRequests: total
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Оптимизированная версия autoDetectDelimiter с кэшированием
|
|
126
|
+
* @param {string} csv - CSV строка
|
|
127
|
+
* @param {Array} candidates - Кандидаты разделителей
|
|
128
|
+
* @param {DelimiterCache} cache - Экземпляр кэша (опционально)
|
|
129
|
+
* @returns {string} Найденный разделитель
|
|
130
|
+
*/
|
|
131
|
+
static autoDetectDelimiter(csv, candidates = [';', ',', '\t', '|'], cache = null) {
|
|
132
|
+
if (!csv || typeof csv !== 'string') {
|
|
133
|
+
return ';';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Проверяем кэш если он предоставлен
|
|
137
|
+
if (cache) {
|
|
138
|
+
const cached = cache.get(csv, candidates);
|
|
139
|
+
if (cached !== null) {
|
|
140
|
+
return cached;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const lines = csv.split('\n').filter(line => line.trim().length > 0);
|
|
145
|
+
|
|
146
|
+
if (lines.length === 0) {
|
|
147
|
+
return ';';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Используем первую непустую строку для детектирования
|
|
151
|
+
const firstLine = lines[0];
|
|
152
|
+
|
|
153
|
+
const counts = {};
|
|
154
|
+
candidates.forEach(delim => {
|
|
155
|
+
const escapedDelim = delim.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
156
|
+
const regex = new RegExp(escapedDelim, 'g');
|
|
157
|
+
const matches = firstLine.match(regex);
|
|
158
|
+
counts[delim] = matches ? matches.length : 0;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Находим разделитель с максимальным количеством
|
|
162
|
+
let maxCount = -1;
|
|
163
|
+
let detectedDelimiter = ';';
|
|
164
|
+
|
|
165
|
+
for (const [delim, count] of Object.entries(counts)) {
|
|
166
|
+
if (count > maxCount) {
|
|
167
|
+
maxCount = count;
|
|
168
|
+
detectedDelimiter = delim;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Если разделитель не найден или есть ничья, возвращаем стандартный
|
|
173
|
+
if (maxCount === 0) {
|
|
174
|
+
detectedDelimiter = ';';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Сохраняем в кэш если он предоставлен
|
|
178
|
+
if (cache) {
|
|
179
|
+
cache.set(csv, candidates, detectedDelimiter);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return detectedDelimiter;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = DelimiterCache;
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Hooks System
|
|
3
|
+
* Система хуков для трансформации данных перед/после конвертации
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @date 2026-01-23
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { ValidationError } = require('../../errors');
|
|
10
|
+
|
|
11
|
+
class TransformHooks {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.hooks = {
|
|
14
|
+
beforeConvert: [],
|
|
15
|
+
afterConvert: [],
|
|
16
|
+
perRow: []
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Регистрирует хук beforeConvert
|
|
22
|
+
* @param {Function} hook - Функция хука
|
|
23
|
+
* @returns {TransformHooks} this для цепочки вызовов
|
|
24
|
+
*/
|
|
25
|
+
beforeConvert(hook) {
|
|
26
|
+
if (typeof hook !== 'function') {
|
|
27
|
+
throw new ValidationError('beforeConvert hook must be a function');
|
|
28
|
+
}
|
|
29
|
+
this.hooks.beforeConvert.push(hook);
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Регистрирует хук afterConvert
|
|
35
|
+
* @param {Function} hook - Функция хука
|
|
36
|
+
* @returns {TransformHooks} this для цепочки вызовов
|
|
37
|
+
*/
|
|
38
|
+
afterConvert(hook) {
|
|
39
|
+
if (typeof hook !== 'function') {
|
|
40
|
+
throw new ValidationError('afterConvert hook must be a function');
|
|
41
|
+
}
|
|
42
|
+
this.hooks.afterConvert.push(hook);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Регистрирует per-row хук
|
|
48
|
+
* @param {Function} hook - Функция хука
|
|
49
|
+
* @returns {TransformHooks} this для цепочки вызовов
|
|
50
|
+
*/
|
|
51
|
+
perRow(hook) {
|
|
52
|
+
if (typeof hook !== 'function') {
|
|
53
|
+
throw new ValidationError('perRow hook must be a function');
|
|
54
|
+
}
|
|
55
|
+
this.hooks.perRow.push(hook);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Применяет beforeConvert хуки
|
|
61
|
+
* @param {any} data - Входные данные
|
|
62
|
+
* @param {Object} context - Контекст выполнения
|
|
63
|
+
* @returns {any} Трансформированные данные
|
|
64
|
+
*/
|
|
65
|
+
applyBeforeConvert(data, context = {}) {
|
|
66
|
+
let result = data;
|
|
67
|
+
for (const hook of this.hooks.beforeConvert) {
|
|
68
|
+
result = hook(result, context);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Применяет afterConvert хуки
|
|
75
|
+
* @param {any} data - Выходные данные
|
|
76
|
+
* @param {Object} context - Контекст выполнения
|
|
77
|
+
* @returns {any} Трансформированные данные
|
|
78
|
+
*/
|
|
79
|
+
applyAfterConvert(data, context = {}) {
|
|
80
|
+
let result = data;
|
|
81
|
+
for (const hook of this.hooks.afterConvert) {
|
|
82
|
+
result = hook(result, context);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Применяет per-row хуки
|
|
89
|
+
* @param {any} row - Строка данных
|
|
90
|
+
* @param {number} index - Индекс строки
|
|
91
|
+
* @param {Object} context - Контекст выполнения
|
|
92
|
+
* @returns {any} Трансформированная строка
|
|
93
|
+
*/
|
|
94
|
+
applyPerRow(row, index, context = {}) {
|
|
95
|
+
let result = row;
|
|
96
|
+
for (const hook of this.hooks.perRow) {
|
|
97
|
+
result = hook(result, index, context);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Применяет все хуки к массиву данных
|
|
104
|
+
* @param {Array} data - Массив данных
|
|
105
|
+
* @param {Object} context - Контекст выполнения
|
|
106
|
+
* @returns {Array} Трансформированный массив
|
|
107
|
+
*/
|
|
108
|
+
applyAll(data, context = {}) {
|
|
109
|
+
if (!Array.isArray(data)) {
|
|
110
|
+
throw new ValidationError('Data must be an array for applyAll');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Применяем beforeConvert хуки
|
|
114
|
+
let processedData = this.applyBeforeConvert(data, context);
|
|
115
|
+
|
|
116
|
+
// Применяем per-row хуки к каждой строке
|
|
117
|
+
if (this.hooks.perRow.length > 0) {
|
|
118
|
+
processedData = processedData.map((row, index) =>
|
|
119
|
+
this.applyPerRow(row, index, context)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Применяем afterConvert хуки
|
|
124
|
+
return this.applyAfterConvert(processedData, context);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Создает копию системы хуков
|
|
129
|
+
* @returns {TransformHooks} Новая копия
|
|
130
|
+
*/
|
|
131
|
+
clone() {
|
|
132
|
+
const cloned = new TransformHooks();
|
|
133
|
+
cloned.hooks = {
|
|
134
|
+
beforeConvert: [...this.hooks.beforeConvert],
|
|
135
|
+
afterConvert: [...this.hooks.afterConvert],
|
|
136
|
+
perRow: [...this.hooks.perRow]
|
|
137
|
+
};
|
|
138
|
+
return cloned;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Очищает все хуки
|
|
143
|
+
*/
|
|
144
|
+
clear() {
|
|
145
|
+
this.hooks = {
|
|
146
|
+
beforeConvert: [],
|
|
147
|
+
afterConvert: [],
|
|
148
|
+
perRow: []
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Возвращает статистику по хукам
|
|
154
|
+
* @returns {Object} Статистика
|
|
155
|
+
*/
|
|
156
|
+
getStats() {
|
|
157
|
+
return {
|
|
158
|
+
beforeConvert: this.hooks.beforeConvert.length,
|
|
159
|
+
afterConvert: this.hooks.afterConvert.length,
|
|
160
|
+
perRow: this.hooks.perRow.length,
|
|
161
|
+
total: this.hooks.beforeConvert.length +
|
|
162
|
+
this.hooks.afterConvert.length +
|
|
163
|
+
this.hooks.perRow.length
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Предопределенные хуки
|
|
170
|
+
*/
|
|
171
|
+
const predefinedHooks = {
|
|
172
|
+
/**
|
|
173
|
+
* Хук для фильтрации данных
|
|
174
|
+
* @param {Function} predicate - Функция-предикат
|
|
175
|
+
* @returns {Function} Хук фильтрации
|
|
176
|
+
*/
|
|
177
|
+
filter(predicate) {
|
|
178
|
+
return (data) => {
|
|
179
|
+
if (Array.isArray(data)) {
|
|
180
|
+
return data.filter(predicate);
|
|
181
|
+
}
|
|
182
|
+
return data;
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Хук для маппинга данных
|
|
188
|
+
* @param {Function} mapper - Функция-маппер
|
|
189
|
+
* @returns {Function} Хук маппинга
|
|
190
|
+
*/
|
|
191
|
+
map(mapper) {
|
|
192
|
+
return (data) => {
|
|
193
|
+
if (Array.isArray(data)) {
|
|
194
|
+
return data.map(mapper);
|
|
195
|
+
}
|
|
196
|
+
return data;
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Хук для сортировки данных
|
|
202
|
+
* @param {Function} compareFn - Функция сравнения
|
|
203
|
+
* @returns {Function} Хук сортировки
|
|
204
|
+
*/
|
|
205
|
+
sort(compareFn) {
|
|
206
|
+
return (data) => {
|
|
207
|
+
if (Array.isArray(data)) {
|
|
208
|
+
return [...data].sort(compareFn);
|
|
209
|
+
}
|
|
210
|
+
return data;
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Хук для ограничения количества записей
|
|
216
|
+
* @param {number} limit - Максимальное количество записей
|
|
217
|
+
* @returns {Function} Хук ограничения
|
|
218
|
+
*/
|
|
219
|
+
limit(limit) {
|
|
220
|
+
return (data) => {
|
|
221
|
+
if (Array.isArray(data)) {
|
|
222
|
+
return data.slice(0, limit);
|
|
223
|
+
}
|
|
224
|
+
return data;
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Хук для добавления метаданных
|
|
230
|
+
* @param {Object} metadata - Метаданные для добавления
|
|
231
|
+
* @returns {Function} Хук добавления метаданных
|
|
232
|
+
*/
|
|
233
|
+
addMetadata(metadata) {
|
|
234
|
+
return (data, context) => {
|
|
235
|
+
if (Array.isArray(data)) {
|
|
236
|
+
return data.map(item => ({
|
|
237
|
+
...item,
|
|
238
|
+
_metadata: {
|
|
239
|
+
...metadata,
|
|
240
|
+
timestamp: new Date().toISOString(),
|
|
241
|
+
context
|
|
242
|
+
}
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
return data;
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Хук для преобразования ключей
|
|
251
|
+
* @param {Function} keyTransformer - Функция преобразования ключей
|
|
252
|
+
* @returns {Function} Хук преобразования ключей
|
|
253
|
+
*/
|
|
254
|
+
transformKeys(keyTransformer) {
|
|
255
|
+
return (data) => {
|
|
256
|
+
if (Array.isArray(data)) {
|
|
257
|
+
return data.map(item => {
|
|
258
|
+
const transformed = {};
|
|
259
|
+
for (const [key, value] of Object.entries(item)) {
|
|
260
|
+
transformed[keyTransformer(key)] = value;
|
|
261
|
+
}
|
|
262
|
+
return transformed;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return data;
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Хук для преобразования значений
|
|
271
|
+
* @param {Function} valueTransformer - Функция преобразования значений
|
|
272
|
+
* @returns {Function} Хук преобразования значений
|
|
273
|
+
*/
|
|
274
|
+
transformValues(valueTransformer) {
|
|
275
|
+
return (data) => {
|
|
276
|
+
if (Array.isArray(data)) {
|
|
277
|
+
return data.map(item => {
|
|
278
|
+
const transformed = {};
|
|
279
|
+
for (const [key, value] of Object.entries(item)) {
|
|
280
|
+
transformed[key] = valueTransformer(value, key);
|
|
281
|
+
}
|
|
282
|
+
return transformed;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return data;
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Хук для валидации данных
|
|
291
|
+
* @param {Function} validator - Функция валидации
|
|
292
|
+
* @param {Function} onError - Обработчик ошибки
|
|
293
|
+
* @returns {Function} Хук валидации
|
|
294
|
+
*/
|
|
295
|
+
validate(validator, onError = console.error) {
|
|
296
|
+
return (data) => {
|
|
297
|
+
if (Array.isArray(data)) {
|
|
298
|
+
const validData = [];
|
|
299
|
+
const errors = [];
|
|
300
|
+
|
|
301
|
+
data.forEach((item, index) => {
|
|
302
|
+
try {
|
|
303
|
+
const isValid = validator(item, index);
|
|
304
|
+
if (isValid) {
|
|
305
|
+
validData.push(item);
|
|
306
|
+
} else {
|
|
307
|
+
errors.push({ index, item, reason: 'Validation failed' });
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
errors.push({ index, item, error: error.message });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (errors.length > 0) {
|
|
315
|
+
onError('Validation errors:', errors);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return validData;
|
|
319
|
+
}
|
|
320
|
+
return data;
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Хук для дедупликации данных
|
|
326
|
+
* @param {Function} keySelector - Функция выбора ключа
|
|
327
|
+
* @returns {Function} Хук дедупликации
|
|
328
|
+
*/
|
|
329
|
+
deduplicate(keySelector = JSON.stringify) {
|
|
330
|
+
return (data) => {
|
|
331
|
+
if (Array.isArray(data)) {
|
|
332
|
+
const seen = new Set();
|
|
333
|
+
return data.filter(item => {
|
|
334
|
+
const key = keySelector(item);
|
|
335
|
+
if (seen.has(key)) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
seen.add(key);
|
|
339
|
+
return true;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return data;
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
module.exports = {
|
|
348
|
+
TransformHooks,
|
|
349
|
+
predefinedHooks
|
|
350
|
+
};
|