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.
Files changed (140) hide show
  1. package/README.md +31 -1
  2. package/bin/jtcsv.js +891 -821
  3. package/bin/jtcsv.ts +2534 -0
  4. package/csv-to-json.js +168 -145
  5. package/dist/jtcsv-core.cjs.js +1407 -0
  6. package/dist/jtcsv-core.cjs.js.map +1 -0
  7. package/dist/jtcsv-core.esm.js +1379 -0
  8. package/dist/jtcsv-core.esm.js.map +1 -0
  9. package/dist/jtcsv-core.umd.js +1413 -0
  10. package/dist/jtcsv-core.umd.js.map +1 -0
  11. package/dist/jtcsv-full.cjs.js +1912 -0
  12. package/dist/jtcsv-full.cjs.js.map +1 -0
  13. package/dist/jtcsv-full.esm.js +1880 -0
  14. package/dist/jtcsv-full.esm.js.map +1 -0
  15. package/dist/jtcsv-full.umd.js +1918 -0
  16. package/dist/jtcsv-full.umd.js.map +1 -0
  17. package/dist/jtcsv-workers.esm.js +759 -0
  18. package/dist/jtcsv-workers.esm.js.map +1 -0
  19. package/dist/jtcsv-workers.umd.js +773 -0
  20. package/dist/jtcsv-workers.umd.js.map +1 -0
  21. package/dist/jtcsv.cjs.js +61 -19
  22. package/dist/jtcsv.cjs.js.map +1 -1
  23. package/dist/jtcsv.esm.js +61 -19
  24. package/dist/jtcsv.esm.js.map +1 -1
  25. package/dist/jtcsv.umd.js +61 -19
  26. package/dist/jtcsv.umd.js.map +1 -1
  27. package/errors.js +188 -2
  28. package/examples/advanced/conditional-transformations.js +446 -0
  29. package/examples/advanced/conditional-transformations.ts +446 -0
  30. package/examples/advanced/csv-parser.worker.js +89 -0
  31. package/examples/advanced/csv-parser.worker.ts +89 -0
  32. package/examples/advanced/nested-objects-example.js +306 -0
  33. package/examples/advanced/nested-objects-example.ts +306 -0
  34. package/examples/advanced/performance-optimization.js +504 -0
  35. package/examples/advanced/performance-optimization.ts +504 -0
  36. package/examples/advanced/run-demo-server.js +116 -0
  37. package/examples/advanced/run-demo-server.ts +116 -0
  38. package/examples/advanced/web-worker-usage.html +874 -0
  39. package/examples/async-multithreaded-example.ts +335 -0
  40. package/examples/cli-advanced-usage.md +288 -0
  41. package/examples/cli-batch-processing.ts +38 -0
  42. package/examples/cli-tool.js +0 -3
  43. package/examples/cli-tool.ts +183 -0
  44. package/examples/error-handling.js +21 -7
  45. package/examples/error-handling.ts +356 -0
  46. package/examples/express-api.js +0 -3
  47. package/examples/express-api.ts +164 -0
  48. package/examples/large-dataset-example.js +0 -3
  49. package/examples/large-dataset-example.ts +204 -0
  50. package/examples/ndjson-processing.js +1 -1
  51. package/examples/ndjson-processing.ts +456 -0
  52. package/examples/plugin-excel-exporter.js +3 -4
  53. package/examples/plugin-excel-exporter.ts +406 -0
  54. package/examples/react-integration.tsx +637 -0
  55. package/examples/schema-validation.ts +640 -0
  56. package/examples/simple-usage.js +254 -254
  57. package/examples/simple-usage.ts +194 -0
  58. package/examples/streaming-example.js +4 -5
  59. package/examples/streaming-example.ts +419 -0
  60. package/examples/web-workers-advanced.ts +28 -0
  61. package/index.d.ts +1 -3
  62. package/index.js +15 -1
  63. package/json-save.js +9 -3
  64. package/json-to-csv.js +168 -21
  65. package/package.json +69 -10
  66. package/plugins/express-middleware/README.md +21 -2
  67. package/plugins/express-middleware/example.js +3 -4
  68. package/plugins/express-middleware/example.ts +135 -0
  69. package/plugins/express-middleware/index.d.ts +1 -1
  70. package/plugins/express-middleware/index.js +270 -118
  71. package/plugins/express-middleware/index.ts +557 -0
  72. package/plugins/fastify-plugin/index.js +2 -4
  73. package/plugins/fastify-plugin/index.ts +443 -0
  74. package/plugins/hono/index.ts +226 -0
  75. package/plugins/nestjs/index.ts +201 -0
  76. package/plugins/nextjs-api/examples/ConverterComponent.tsx +386 -0
  77. package/plugins/nextjs-api/examples/api-convert.js +0 -2
  78. package/plugins/nextjs-api/examples/api-convert.ts +67 -0
  79. package/plugins/nextjs-api/index.tsx +339 -0
  80. package/plugins/nextjs-api/route.js +2 -3
  81. package/plugins/nextjs-api/route.ts +370 -0
  82. package/plugins/nuxt/index.ts +94 -0
  83. package/plugins/nuxt/runtime/composables/useJtcsv.ts +100 -0
  84. package/plugins/nuxt/runtime/plugin.ts +71 -0
  85. package/plugins/remix/index.js +1 -1
  86. package/plugins/remix/index.ts +260 -0
  87. package/plugins/sveltekit/index.js +1 -1
  88. package/plugins/sveltekit/index.ts +301 -0
  89. package/plugins/trpc/index.ts +267 -0
  90. package/src/browser/browser-functions.ts +402 -0
  91. package/src/browser/core.js +92 -0
  92. package/src/browser/core.ts +152 -0
  93. package/src/browser/csv-to-json-browser.d.ts +3 -0
  94. package/src/browser/csv-to-json-browser.js +36 -14
  95. package/src/browser/csv-to-json-browser.ts +264 -0
  96. package/src/browser/errors-browser.ts +303 -0
  97. package/src/browser/extensions/plugins.js +92 -0
  98. package/src/browser/extensions/plugins.ts +93 -0
  99. package/src/browser/extensions/workers.js +39 -0
  100. package/src/browser/extensions/workers.ts +39 -0
  101. package/src/browser/globals.d.ts +5 -0
  102. package/src/browser/index.ts +192 -0
  103. package/src/browser/json-to-csv-browser.d.ts +3 -0
  104. package/src/browser/json-to-csv-browser.js +13 -3
  105. package/src/browser/json-to-csv-browser.ts +262 -0
  106. package/src/browser/streams.js +12 -2
  107. package/src/browser/streams.ts +336 -0
  108. package/src/browser/workers/csv-parser.worker.ts +377 -0
  109. package/src/browser/workers/worker-pool.ts +548 -0
  110. package/src/core/delimiter-cache.js +22 -8
  111. package/src/core/delimiter-cache.ts +310 -0
  112. package/src/core/node-optimizations.ts +449 -0
  113. package/src/core/plugin-system.js +29 -11
  114. package/src/core/plugin-system.ts +400 -0
  115. package/src/core/transform-hooks.ts +558 -0
  116. package/src/engines/fast-path-engine-new.ts +347 -0
  117. package/src/engines/fast-path-engine.ts +854 -0
  118. package/src/errors.ts +72 -0
  119. package/src/formats/ndjson-parser.ts +469 -0
  120. package/src/formats/tsv-parser.ts +334 -0
  121. package/src/index-with-plugins.js +16 -9
  122. package/src/index-with-plugins.ts +395 -0
  123. package/src/types/index.ts +255 -0
  124. package/src/utils/bom-utils.js +259 -0
  125. package/src/utils/bom-utils.ts +373 -0
  126. package/src/utils/encoding-support.js +124 -0
  127. package/src/utils/encoding-support.ts +155 -0
  128. package/src/utils/schema-validator.js +19 -19
  129. package/src/utils/schema-validator.ts +819 -0
  130. package/src/utils/transform-loader.js +1 -1
  131. package/src/utils/transform-loader.ts +389 -0
  132. package/src/utils/zod-adapter.js +170 -0
  133. package/src/utils/zod-adapter.ts +280 -0
  134. package/src/web-server/index.js +10 -10
  135. package/src/web-server/index.ts +683 -0
  136. package/src/workers/csv-multithreaded.ts +310 -0
  137. package/src/workers/csv-parser.worker.ts +227 -0
  138. package/src/workers/worker-pool.ts +409 -0
  139. package/stream-csv-to-json.js +26 -8
  140. package/stream-json-to-csv.js +1 -0
@@ -0,0 +1,558 @@
1
+ /**
2
+ * Transform Hooks System
3
+ * Система хуков для трансформации данных перед/после конвертации
4
+ *
5
+ * @version 2.0.0
6
+ * @date 2026-01-29
7
+ */
8
+
9
+ import { ValidationError } from '../errors';
10
+
11
+ export interface HookContext {
12
+ [key: string]: any;
13
+ }
14
+
15
+ export type HookFunction<T = any, R = any> = (data: T, context?: HookContext) => R;
16
+ export type PerRowHookFunction<T = any, R = any> = (row: T, index: number, context?: HookContext) => R;
17
+ export type AsyncHookFunction<T = any, R = any> = (data: T, context?: HookContext) => Promise<R>;
18
+ export type AsyncPerRowHookFunction<T = any, R = any> = (row: T, index: number, context?: HookContext) => Promise<R>;
19
+
20
+ export interface TransformHooksOptions {
21
+ hooks?: {
22
+ beforeConvert?: Array<HookFunction | AsyncHookFunction>;
23
+ afterConvert?: Array<HookFunction | AsyncHookFunction>;
24
+ perRow?: Array<PerRowHookFunction | AsyncPerRowHookFunction>;
25
+ };
26
+ }
27
+
28
+ export class TransformHooks {
29
+ private hooks: {
30
+ beforeConvert: Array<HookFunction | AsyncHookFunction>;
31
+ afterConvert: Array<HookFunction | AsyncHookFunction>;
32
+ perRow: Array<PerRowHookFunction | AsyncPerRowHookFunction>;
33
+ };
34
+
35
+ constructor(options?: TransformHooksOptions) {
36
+ this.hooks = {
37
+ beforeConvert: options?.hooks?.beforeConvert || [],
38
+ afterConvert: options?.hooks?.afterConvert || [],
39
+ perRow: options?.hooks?.perRow || []
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Регистрирует хук beforeConvert
45
+ * @param hook - Функция хука
46
+ * @returns this для цепочки вызовов
47
+ */
48
+ beforeConvert(hook: HookFunction | AsyncHookFunction): this {
49
+ if (typeof hook !== 'function') {
50
+ throw new ValidationError('beforeConvert hook must be a function');
51
+ }
52
+ this.hooks.beforeConvert.push(hook);
53
+ return this;
54
+ }
55
+
56
+ /**
57
+ * Регистрирует хук afterConvert
58
+ * @param hook - Функция хука
59
+ * @returns this для цепочки вызовов
60
+ */
61
+ afterConvert(hook: HookFunction | AsyncHookFunction): this {
62
+ if (typeof hook !== 'function') {
63
+ throw new ValidationError('afterConvert hook must be a function');
64
+ }
65
+ this.hooks.afterConvert.push(hook);
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * Регистрирует per-row хук
71
+ * @param hook - Функция хука
72
+ * @returns this для цепочки вызовов
73
+ */
74
+ perRow(hook: PerRowHookFunction | AsyncPerRowHookFunction): this {
75
+ if (typeof hook !== 'function') {
76
+ throw new ValidationError('perRow hook must be a function');
77
+ }
78
+ this.hooks.perRow.push(hook);
79
+ return this;
80
+ }
81
+
82
+ /**
83
+ * Применяет beforeConvert хуки
84
+ * @param data - Входные данные
85
+ * @param context - Контекст выполнения
86
+ * @returns Трансформированные данные
87
+ */
88
+ applyBeforeConvert<T = any, R = any>(data: T, context: HookContext = {}): R {
89
+ let result: any = data;
90
+ for (const hook of this.hooks.beforeConvert) {
91
+ result = hook(result, context);
92
+ }
93
+ return result as R;
94
+ }
95
+
96
+ /**
97
+ * Применяет beforeConvert хуки асинхронно
98
+ * @param data - Входные данные
99
+ * @param context - Контекст выполнения
100
+ * @returns Promise с трансформированными данными
101
+ */
102
+ async applyBeforeConvertAsync<T = any, R = any>(data: T, context: HookContext = {}): Promise<R> {
103
+ let result: any = data;
104
+ for (const hook of this.hooks.beforeConvert) {
105
+ if (this.isAsyncFunction(hook)) {
106
+ result = await (hook as AsyncHookFunction)(result, context);
107
+ } else {
108
+ result = (hook as HookFunction)(result, context);
109
+ }
110
+ }
111
+ return result as R;
112
+ }
113
+
114
+ /**
115
+ * Применяет afterConvert хуки
116
+ * @param data - Выходные данные
117
+ * @param context - Контекст выполнения
118
+ * @returns Трансформированные данные
119
+ */
120
+ applyAfterConvert<T = any, R = any>(data: T, context: HookContext = {}): R {
121
+ let result: any = data;
122
+ for (const hook of this.hooks.afterConvert) {
123
+ result = hook(result, context);
124
+ }
125
+ return result as R;
126
+ }
127
+
128
+ /**
129
+ * Применяет afterConvert хуки асинхронно
130
+ * @param data - Выходные данные
131
+ * @param context - Контекст выполнения
132
+ * @returns Promise с трансформированными данными
133
+ */
134
+ async applyAfterConvertAsync<T = any, R = any>(data: T, context: HookContext = {}): Promise<R> {
135
+ let result: any = data;
136
+ for (const hook of this.hooks.afterConvert) {
137
+ if (this.isAsyncFunction(hook)) {
138
+ result = await (hook as AsyncHookFunction)(result, context);
139
+ } else {
140
+ result = (hook as HookFunction)(result, context);
141
+ }
142
+ }
143
+ return result as R;
144
+ }
145
+
146
+ /**
147
+ * Применяет per-row хуки
148
+ * @param row - Строка данных
149
+ * @param index - Индекс строки
150
+ * @param context - Контекст выполнения
151
+ * @returns Трансформированная строка
152
+ */
153
+ applyPerRow<T = any, R = any>(row: T, index: number, context: HookContext = {}): R {
154
+ let result: any = row;
155
+ for (const hook of this.hooks.perRow) {
156
+ result = hook(result, index, context);
157
+ }
158
+ return result as R;
159
+ }
160
+
161
+ /**
162
+ * Применяет per-row хуки асинхронно
163
+ * @param row - Строка данных
164
+ * @param index - Индекс строки
165
+ * @param context - Контекст выполнения
166
+ * @returns Promise с трансформированной строкой
167
+ */
168
+ async applyPerRowAsync<T = any, R = any>(row: T, index: number, context: HookContext = {}): Promise<R> {
169
+ let result: any = row;
170
+ for (const hook of this.hooks.perRow) {
171
+ if (this.isAsyncFunction(hook)) {
172
+ result = await (hook as AsyncPerRowHookFunction)(result, index, context);
173
+ } else {
174
+ result = (hook as PerRowHookFunction)(result, index, context);
175
+ }
176
+ }
177
+ return result as R;
178
+ }
179
+
180
+ /**
181
+ * Применяет все хуки к массиву данных
182
+ * @param data - Массив данных
183
+ * @param context - Контекст выполнения
184
+ * @returns Трансформированный массив
185
+ */
186
+ applyAll<T = any, R = any>(data: T[], context: HookContext = {}): R[] {
187
+ if (!Array.isArray(data)) {
188
+ throw new ValidationError('Data must be an array for applyAll');
189
+ }
190
+
191
+ // Применяем beforeConvert хуки
192
+ let processedData: any[] = this.applyBeforeConvert(data, context);
193
+
194
+ // Применяем per-row хуки к каждой строке
195
+ if (this.hooks.perRow.length > 0) {
196
+ processedData = processedData.map((row: any, index: number) =>
197
+ this.applyPerRow(row, index, context)
198
+ );
199
+ }
200
+
201
+ // Применяем afterConvert хуки
202
+ return this.applyAfterConvert(processedData, context) as R[];
203
+ }
204
+
205
+ /**
206
+ * Применяет все хуки к массиву данных асинхронно
207
+ * @param data - Массив данных
208
+ * @param context - Контекст выполнения
209
+ * @returns Promise с трансформированным массивом
210
+ */
211
+ async applyAllAsync<T = any, R = any>(data: T[], context: HookContext = {}): Promise<R[]> {
212
+ if (!Array.isArray(data)) {
213
+ throw new ValidationError('Data must be an array for applyAll');
214
+ }
215
+
216
+ // Применяем beforeConvert хуки асинхронно
217
+ let processedData = await this.applyBeforeConvertAsync(data, context);
218
+
219
+ // Применяем per-row хуки асинхронно к каждой строке
220
+ if (this.hooks.perRow.length > 0) {
221
+ const processedRows: any[] = [];
222
+ for (let i = 0; i < processedData.length; i++) {
223
+ const processedRow = await this.applyPerRowAsync(processedData[i], i, context);
224
+ processedRows.push(processedRow);
225
+ }
226
+ processedData = processedRows;
227
+ }
228
+
229
+ // Применяем afterConvert хуки асинхронно
230
+ return await this.applyAfterConvertAsync(processedData, context) as R[];
231
+ }
232
+
233
+ /**
234
+ * Создает копию системы хуков
235
+ * @returns Новая копия
236
+ */
237
+ clone(): TransformHooks {
238
+ const cloned = new TransformHooks();
239
+ cloned.hooks = {
240
+ beforeConvert: [...this.hooks.beforeConvert],
241
+ afterConvert: [...this.hooks.afterConvert],
242
+ perRow: [...this.hooks.perRow]
243
+ };
244
+ return cloned;
245
+ }
246
+
247
+ /**
248
+ * Очищает все хуки
249
+ */
250
+ clear(): void {
251
+ this.hooks = {
252
+ beforeConvert: [],
253
+ afterConvert: [],
254
+ perRow: []
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Возвращает статистику по хукам
260
+ * @returns Статистика
261
+ */
262
+ getStats(): { beforeConvert: number; afterConvert: number; perRow: number; total: number } {
263
+ return {
264
+ beforeConvert: this.hooks.beforeConvert.length,
265
+ afterConvert: this.hooks.afterConvert.length,
266
+ perRow: this.hooks.perRow.length,
267
+ total: this.hooks.beforeConvert.length +
268
+ this.hooks.afterConvert.length +
269
+ this.hooks.perRow.length
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Проверяет, является ли функция асинхронной
275
+ * @private
276
+ */
277
+ private isAsyncFunction(fn: any): boolean {
278
+ return fn.constructor.name === 'AsyncFunction' ||
279
+ (typeof fn === 'function' && fn.constructor === (async () => {}).constructor);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Предопределенные хуки
285
+ */
286
+ export const predefinedHooks = {
287
+ /**
288
+ * Хук для фильтрации данных
289
+ * @param predicate - Функция-предикат
290
+ * @returns Хук фильтрации
291
+ */
292
+ filter<T>(predicate: (item: T, index: number) => boolean): HookFunction<T[], T[]> {
293
+ return (data: T[]) => {
294
+ if (Array.isArray(data)) {
295
+ return data.filter(predicate);
296
+ }
297
+ return data;
298
+ };
299
+ },
300
+
301
+ /**
302
+ * Асинхронный хук для фильтрации данных
303
+ * @param predicate - Асинхронная функция-предикат
304
+ * @returns Асинхронный хук фильтрации
305
+ */
306
+ filterAsync<T>(predicate: (item: T, index: number) => Promise<boolean>): AsyncHookFunction<T[], T[]> {
307
+ return async (data: T[]) => {
308
+ if (Array.isArray(data)) {
309
+ const filtered: T[] = [];
310
+ for (let i = 0; i < data.length; i++) {
311
+ if (await predicate(data[i], i)) {
312
+ filtered.push(data[i]);
313
+ }
314
+ }
315
+ return filtered;
316
+ }
317
+ return data;
318
+ };
319
+ },
320
+
321
+ /**
322
+ * Хук для маппинга данных
323
+ * @param mapper - Функция-маппер
324
+ * @returns Хук маппинга
325
+ */
326
+ map<T, R>(mapper: (item: T, index: number) => R): HookFunction<T[], R[]> {
327
+ return (data: T[]) => {
328
+ if (Array.isArray(data)) {
329
+ return data.map(mapper);
330
+ }
331
+ return data as any;
332
+ };
333
+ },
334
+
335
+ /**
336
+ * Асинхронный хук для маппинга данных
337
+ * @param mapper - Асинхронная функция-маппер
338
+ * @returns Асинхронный хук маппинга
339
+ */
340
+ mapAsync<T, R>(mapper: (item: T, index: number) => Promise<R>): AsyncHookFunction<T[], R[]> {
341
+ return async (data: T[]) => {
342
+ if (Array.isArray(data)) {
343
+ const mapped: R[] = [];
344
+ for (let i = 0; i < data.length; i++) {
345
+ mapped.push(await mapper(data[i], i));
346
+ }
347
+ return mapped;
348
+ }
349
+ return data as any;
350
+ };
351
+ },
352
+
353
+ /**
354
+ * Хук для сортировки данных
355
+ * @param compareFn - Функция сравнения
356
+ * @returns Хук сортировки
357
+ */
358
+ sort<T>(compareFn?: (a: T, b: T) => number): HookFunction<T[], T[]> {
359
+ return (data: T[]) => {
360
+ if (Array.isArray(data)) {
361
+ return [...data].sort(compareFn);
362
+ }
363
+ return data;
364
+ };
365
+ },
366
+
367
+ /**
368
+ * Хук для ограничения количества записей
369
+ * @param limit - Максимальное количество записей
370
+ * @returns Хук ограничения
371
+ */
372
+ limit<T>(limit: number): HookFunction<T[], T[]> {
373
+ return (data: T[]) => {
374
+ if (Array.isArray(data)) {
375
+ return data.slice(0, limit);
376
+ }
377
+ return data;
378
+ };
379
+ },
380
+
381
+ /**
382
+ * Хук для добавления метаданных
383
+ * @param metadata - Метаданные для добавления
384
+ * @returns Хук добавления метаданных
385
+ */
386
+ addMetadata<T extends Record<string, any>>(metadata: Record<string, any>): HookFunction<T[], Array<T & { _metadata: any }>> {
387
+ return (data: T[], context?: HookContext) => {
388
+ if (Array.isArray(data)) {
389
+ return data.map(item => ({
390
+ ...item,
391
+ _metadata: {
392
+ ...metadata,
393
+ timestamp: new Date().toISOString(),
394
+ context
395
+ }
396
+ })) as Array<T & { _metadata: any }>;
397
+ }
398
+ return data as any;
399
+ };
400
+ },
401
+
402
+ /**
403
+ * Хук для преобразования ключей
404
+ * @param keyTransformer - Функция преобразования ключей
405
+ * @returns Хук преобразования ключей
406
+ */
407
+ transformKeys<T extends Record<string, any>>(keyTransformer: (key: string) => string): HookFunction<T[], Array<Record<string, any>>> {
408
+ return (data: T[]) => {
409
+ if (Array.isArray(data)) {
410
+ return data.map(item => {
411
+ const transformed: Record<string, any> = {};
412
+ for (const [key, value] of Object.entries(item)) {
413
+ transformed[keyTransformer(key)] = value;
414
+ }
415
+ return transformed;
416
+ });
417
+ }
418
+ return data;
419
+ };
420
+ },
421
+
422
+ /**
423
+ * Хук для преобразования значений
424
+ * @param valueTransformer - Функция преобразования значений
425
+ * @returns Хук преобразования значений
426
+ */
427
+ transformValues<T extends Record<string, any>>(valueTransformer: (value: any, key: string) => any): HookFunction<T[], Array<Record<string, any>>> {
428
+ return (data: T[]) => {
429
+ if (Array.isArray(data)) {
430
+ return data.map(item => {
431
+ const transformed: Record<string, any> = {};
432
+ for (const [key, value] of Object.entries(item)) {
433
+ transformed[key] = valueTransformer(value, key);
434
+ }
435
+ return transformed;
436
+ });
437
+ }
438
+ return data;
439
+ };
440
+ },
441
+
442
+ /**
443
+ * Хук для валидации данных
444
+ * @param validator - Функция валидации
445
+ * @param onError - Обработчик ошибки
446
+ * @returns Хук валидации
447
+ */
448
+ validate<T>(validator: (item: T, index: number) => boolean, onError: (errors: any[]) => void = console.error): HookFunction<T[], T[]> {
449
+ return (data: T[]) => {
450
+ if (Array.isArray(data)) {
451
+ const validData: T[] = [];
452
+ const errors: any[] = [];
453
+
454
+ data.forEach((item, index) => {
455
+ try {
456
+ const isValid = validator(item, index);
457
+ if (isValid) {
458
+ validData.push(item);
459
+ } else {
460
+ errors.push({ index, item, reason: 'Validation failed' });
461
+ }
462
+ } catch (error: any) {
463
+ errors.push({ index, item, error: error.message });
464
+ }
465
+ });
466
+
467
+ if (errors.length > 0) {
468
+ onError(errors);
469
+ }
470
+
471
+ return validData;
472
+ }
473
+ return data;
474
+ };
475
+ },
476
+
477
+ /**
478
+ * Асинхронный хук для валидации данных
479
+ * @param validator - Асинхронная функция валидации
480
+ * @param onError - Обработчик ошибки
481
+ * @returns Асинхронный хук валидации
482
+ */
483
+ validateAsync<T>(validator: (item: T, index: number) => Promise<boolean>, onError: (errors: any[]) => void = console.error): AsyncHookFunction<T[], T[]> {
484
+ return async (data: T[]) => {
485
+ if (Array.isArray(data)) {
486
+ const validData: T[] = [];
487
+ const errors: any[] = [];
488
+
489
+ for (let i = 0; i < data.length; i++) {
490
+ try {
491
+ const isValid = await validator(data[i], i);
492
+ if (isValid) {
493
+ validData.push(data[i]);
494
+ } else {
495
+ errors.push({ index: i, item: data[i], reason: 'Validation failed' });
496
+ }
497
+ } catch (error: any) {
498
+ errors.push({ index: i, item: data[i], error: error.message });
499
+ }
500
+ }
501
+
502
+ if (errors.length > 0) {
503
+ onError(errors);
504
+ }
505
+
506
+ return validData;
507
+ }
508
+ return data;
509
+ };
510
+ },
511
+
512
+ /**
513
+ * Хук для дедупликации данных
514
+ * @param keySelector - Функция выбора ключа
515
+ * @returns Хук дедупликации
516
+ */
517
+ deduplicate<T>(keySelector: (item: T) => string = JSON.stringify): HookFunction<T[], T[]> {
518
+ return (data: T[]) => {
519
+ if (Array.isArray(data)) {
520
+ const seen = new Set<string>();
521
+ return data.filter(item => {
522
+ const key = keySelector(item);
523
+ if (seen.has(key)) {
524
+ return false;
525
+ }
526
+ seen.add(key);
527
+ return true;
528
+ });
529
+ }
530
+ return data;
531
+ };
532
+ },
533
+
534
+ /**
535
+ * Асинхронный хук для дедупликации данных
536
+ * @param keySelector - Асинхронная функция выбора ключа
537
+ * @returns Асинхронный хук дедупликации
538
+ */
539
+ deduplicateAsync<T>(keySelector: (item: T) => Promise<string> = async (item) => JSON.stringify(item)): AsyncHookFunction<T[], T[]> {
540
+ return async (data: T[]) => {
541
+ if (Array.isArray(data)) {
542
+ const seen = new Set<string>();
543
+ const filtered: T[] = [];
544
+
545
+ for (const item of data) {
546
+ const key = await keySelector(item);
547
+ if (!seen.has(key)) {
548
+ seen.add(key);
549
+ filtered.push(item);
550
+ }
551
+ }
552
+
553
+ return filtered;
554
+ }
555
+ return data;
556
+ };
557
+ }
558
+ };