@bulkimport/core 0.1.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/dist/index.js ADDED
@@ -0,0 +1,777 @@
1
+ // src/domain/model/ImportStatus.ts
2
+ var ImportStatus = {
3
+ CREATED: "CREATED",
4
+ PREVIEWING: "PREVIEWING",
5
+ PREVIEWED: "PREVIEWED",
6
+ PROCESSING: "PROCESSING",
7
+ PAUSED: "PAUSED",
8
+ COMPLETED: "COMPLETED",
9
+ ABORTED: "ABORTED",
10
+ FAILED: "FAILED"
11
+ };
12
+ var VALID_TRANSITIONS = {
13
+ [ImportStatus.CREATED]: [ImportStatus.PREVIEWING, ImportStatus.PROCESSING],
14
+ [ImportStatus.PREVIEWING]: [ImportStatus.PREVIEWED, ImportStatus.FAILED],
15
+ [ImportStatus.PREVIEWED]: [ImportStatus.PROCESSING],
16
+ [ImportStatus.PROCESSING]: [ImportStatus.PAUSED, ImportStatus.COMPLETED, ImportStatus.ABORTED, ImportStatus.FAILED],
17
+ [ImportStatus.PAUSED]: [ImportStatus.PROCESSING, ImportStatus.ABORTED],
18
+ [ImportStatus.COMPLETED]: [],
19
+ [ImportStatus.ABORTED]: [],
20
+ [ImportStatus.FAILED]: []
21
+ };
22
+ function canTransition(from, to) {
23
+ return VALID_TRANSITIONS[from].includes(to);
24
+ }
25
+
26
+ // src/domain/model/Batch.ts
27
+ function createBatch(id, index, records) {
28
+ return {
29
+ id,
30
+ index,
31
+ status: "PENDING",
32
+ records,
33
+ processedCount: 0,
34
+ failedCount: 0
35
+ };
36
+ }
37
+
38
+ // src/domain/model/Record.ts
39
+ function createPendingRecord(index, raw) {
40
+ return {
41
+ index,
42
+ raw,
43
+ parsed: raw,
44
+ status: "pending",
45
+ errors: []
46
+ };
47
+ }
48
+ function markRecordValid(record, parsed) {
49
+ return { ...record, parsed, status: "valid", errors: [] };
50
+ }
51
+ function markRecordInvalid(record, errors) {
52
+ return { ...record, status: "invalid", errors };
53
+ }
54
+ function markRecordProcessed(record) {
55
+ return { ...record, status: "processed" };
56
+ }
57
+ function markRecordFailed(record, error) {
58
+ return { ...record, status: "failed", processingError: error };
59
+ }
60
+
61
+ // src/domain/model/ValidationResult.ts
62
+ function validResult() {
63
+ return { isValid: true, errors: [] };
64
+ }
65
+ function invalidResult(errors) {
66
+ return { isValid: false, errors };
67
+ }
68
+
69
+ // src/domain/services/SchemaValidator.ts
70
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
71
+ var SchemaValidator = class {
72
+ constructor(schema) {
73
+ this.schema = schema;
74
+ }
75
+ validate(record) {
76
+ const errors = [];
77
+ for (const field of this.schema.fields) {
78
+ const value = record[field.name];
79
+ const fieldErrors = this.validateField(field, value);
80
+ errors.push(...fieldErrors);
81
+ }
82
+ if (this.schema.strict) {
83
+ const definedFields = new Set(this.schema.fields.map((f) => f.name));
84
+ for (const key of Object.keys(record)) {
85
+ if (!definedFields.has(key)) {
86
+ errors.push({
87
+ field: key,
88
+ message: `Unknown field '${key}' is not allowed in strict mode`,
89
+ code: "UNKNOWN_FIELD",
90
+ value: record[key]
91
+ });
92
+ }
93
+ }
94
+ }
95
+ return errors.length === 0 ? validResult() : invalidResult(errors);
96
+ }
97
+ applyTransforms(record) {
98
+ const transformed = { ...record };
99
+ for (const field of this.schema.fields) {
100
+ if (field.transform && transformed[field.name] !== void 0) {
101
+ transformed[field.name] = field.transform(transformed[field.name]);
102
+ }
103
+ if (transformed[field.name] === void 0 && field.defaultValue !== void 0) {
104
+ transformed[field.name] = field.defaultValue;
105
+ }
106
+ }
107
+ return transformed;
108
+ }
109
+ validateField(field, value) {
110
+ const errors = [];
111
+ if (this.isEmpty(value)) {
112
+ if (field.required) {
113
+ errors.push({
114
+ field: field.name,
115
+ message: `Field '${field.name}' is required`,
116
+ code: "REQUIRED",
117
+ value
118
+ });
119
+ }
120
+ return errors;
121
+ }
122
+ if (field.type !== "custom") {
123
+ const typeError = this.validateType(field, value);
124
+ if (typeError) {
125
+ errors.push(typeError);
126
+ return errors;
127
+ }
128
+ }
129
+ if (field.pattern) {
130
+ const stringValue = String(value);
131
+ if (!field.pattern.test(stringValue)) {
132
+ errors.push({
133
+ field: field.name,
134
+ message: `Field '${field.name}' does not match pattern ${String(field.pattern)}`,
135
+ code: "PATTERN_MISMATCH",
136
+ value
137
+ });
138
+ }
139
+ }
140
+ if (field.customValidator) {
141
+ const result = field.customValidator(value);
142
+ if (!result.valid) {
143
+ errors.push({
144
+ field: field.name,
145
+ message: result.message ?? `Custom validation failed for field '${field.name}'`,
146
+ code: "CUSTOM_VALIDATION",
147
+ value
148
+ });
149
+ }
150
+ }
151
+ return errors;
152
+ }
153
+ validateType(field, value) {
154
+ const stringValue = String(value);
155
+ switch (field.type) {
156
+ case "number": {
157
+ const num = Number(stringValue);
158
+ if (isNaN(num)) {
159
+ return {
160
+ field: field.name,
161
+ message: `Field '${field.name}' must be a number`,
162
+ code: "TYPE_MISMATCH",
163
+ value
164
+ };
165
+ }
166
+ return null;
167
+ }
168
+ case "boolean": {
169
+ const lower = stringValue.toLowerCase();
170
+ if (!["true", "false", "1", "0", "yes", "no"].includes(lower)) {
171
+ return {
172
+ field: field.name,
173
+ message: `Field '${field.name}' must be a boolean`,
174
+ code: "TYPE_MISMATCH",
175
+ value
176
+ };
177
+ }
178
+ return null;
179
+ }
180
+ case "date": {
181
+ const date = new Date(stringValue);
182
+ if (isNaN(date.getTime())) {
183
+ return {
184
+ field: field.name,
185
+ message: `Field '${field.name}' must be a valid date`,
186
+ code: "TYPE_MISMATCH",
187
+ value
188
+ };
189
+ }
190
+ return null;
191
+ }
192
+ case "email": {
193
+ if (!EMAIL_PATTERN.test(stringValue)) {
194
+ return {
195
+ field: field.name,
196
+ message: `Field '${field.name}' must be a valid email`,
197
+ code: "TYPE_MISMATCH",
198
+ value
199
+ };
200
+ }
201
+ return null;
202
+ }
203
+ case "string":
204
+ return null;
205
+ default:
206
+ return null;
207
+ }
208
+ }
209
+ isEmpty(value) {
210
+ return value === void 0 || value === null || value === "";
211
+ }
212
+ };
213
+
214
+ // src/application/EventBus.ts
215
+ var EventBus = class {
216
+ constructor() {
217
+ this.handlers = /* @__PURE__ */ new Map();
218
+ }
219
+ on(type, handler) {
220
+ const existing = this.handlers.get(type) ?? /* @__PURE__ */ new Set();
221
+ existing.add(handler);
222
+ this.handlers.set(type, existing);
223
+ }
224
+ off(type, handler) {
225
+ const existing = this.handlers.get(type);
226
+ if (existing) {
227
+ existing.delete(handler);
228
+ }
229
+ }
230
+ emit(event) {
231
+ const handlers = this.handlers.get(event.type);
232
+ if (handlers) {
233
+ for (const handler of handlers) {
234
+ handler(event);
235
+ }
236
+ }
237
+ }
238
+ };
239
+
240
+ // src/infrastructure/state/InMemoryStateStore.ts
241
+ var InMemoryStateStore = class {
242
+ constructor() {
243
+ this.jobs = /* @__PURE__ */ new Map();
244
+ this.records = /* @__PURE__ */ new Map();
245
+ }
246
+ saveJobState(job) {
247
+ this.jobs.set(job.id, job);
248
+ return Promise.resolve();
249
+ }
250
+ getJobState(jobId) {
251
+ return Promise.resolve(this.jobs.get(jobId) ?? null);
252
+ }
253
+ updateBatchState(jobId, batchId, state) {
254
+ const job = this.jobs.get(jobId);
255
+ if (!job) return Promise.resolve();
256
+ const batches = job.batches.map(
257
+ (b) => b.id === batchId ? { ...b, status: state.status, processedCount: state.processedCount, failedCount: state.failedCount } : b
258
+ );
259
+ this.jobs.set(jobId, { ...job, batches });
260
+ return Promise.resolve();
261
+ }
262
+ saveProcessedRecord(jobId, _batchId, record) {
263
+ const key = jobId;
264
+ const existing = this.records.get(key) ?? [];
265
+ const index = existing.findIndex((r) => r.index === record.index);
266
+ if (index >= 0) {
267
+ existing[index] = record;
268
+ } else {
269
+ existing.push(record);
270
+ }
271
+ this.records.set(key, existing);
272
+ return Promise.resolve();
273
+ }
274
+ getFailedRecords(jobId) {
275
+ const all = this.records.get(jobId) ?? [];
276
+ return Promise.resolve(all.filter((r) => r.status === "failed" || r.status === "invalid"));
277
+ }
278
+ getPendingRecords(jobId) {
279
+ const all = this.records.get(jobId) ?? [];
280
+ return Promise.resolve(all.filter((r) => r.status === "pending" || r.status === "valid"));
281
+ }
282
+ getProcessedRecords(jobId) {
283
+ const all = this.records.get(jobId) ?? [];
284
+ return Promise.resolve(all.filter((r) => r.status === "processed"));
285
+ }
286
+ getProgress(jobId) {
287
+ const job = this.jobs.get(jobId);
288
+ const all = this.records.get(jobId) ?? [];
289
+ const processed = all.filter((r) => r.status === "processed").length;
290
+ const failed = all.filter((r) => r.status === "failed" || r.status === "invalid").length;
291
+ const total = job?.totalRecords ?? all.length;
292
+ const pending = total - processed - failed;
293
+ const elapsed = job?.startedAt ? Date.now() - job.startedAt : 0;
294
+ const currentBatch = job?.batches.filter((b) => b.status === "COMPLETED").length ?? 0;
295
+ const totalBatches = job?.batches.length ?? 0;
296
+ return Promise.resolve({
297
+ totalRecords: total,
298
+ processedRecords: processed,
299
+ failedRecords: failed,
300
+ pendingRecords: pending,
301
+ percentage: total > 0 ? Math.round(processed / total * 100) : 0,
302
+ currentBatch,
303
+ totalBatches,
304
+ elapsedMs: elapsed
305
+ });
306
+ }
307
+ };
308
+
309
+ // src/BulkImport.ts
310
+ var BulkImport = class {
311
+ constructor(config) {
312
+ this.source = null;
313
+ this.parser = null;
314
+ this.status = "CREATED";
315
+ this.batches = [];
316
+ this.allRecords = [];
317
+ this.totalRecords = 0;
318
+ this.abortController = null;
319
+ this.pausePromise = null;
320
+ this.config = {
321
+ ...config,
322
+ batchSize: config.batchSize ?? 100,
323
+ continueOnError: config.continueOnError ?? false
324
+ };
325
+ this.validator = new SchemaValidator(config.schema);
326
+ this.eventBus = new EventBus();
327
+ this.stateStore = config.stateStore ?? new InMemoryStateStore();
328
+ this.jobId = crypto.randomUUID();
329
+ }
330
+ from(source, parser) {
331
+ this.source = source;
332
+ this.parser = parser;
333
+ return this;
334
+ }
335
+ on(type, handler) {
336
+ this.eventBus.on(type, handler);
337
+ return this;
338
+ }
339
+ async preview(maxRecords = 10) {
340
+ this.assertSourceConfigured();
341
+ this.transitionTo("PREVIEWING");
342
+ const records = await this.parseRecords(maxRecords);
343
+ const validRecords = [];
344
+ const invalidRecords = [];
345
+ const columns = /* @__PURE__ */ new Set();
346
+ for (const record of records) {
347
+ for (const key of Object.keys(record.raw)) {
348
+ columns.add(key);
349
+ }
350
+ const transformed = this.validator.applyTransforms(record.raw);
351
+ const result = this.validator.validate(transformed);
352
+ if (result.isValid) {
353
+ validRecords.push(markRecordValid(record, transformed));
354
+ } else {
355
+ invalidRecords.push(markRecordInvalid(record, result.errors));
356
+ }
357
+ }
358
+ this.transitionTo("PREVIEWED");
359
+ return {
360
+ validRecords,
361
+ invalidRecords,
362
+ totalSampled: records.length,
363
+ columns: [...columns]
364
+ };
365
+ }
366
+ async start(processor) {
367
+ this.assertSourceConfigured();
368
+ this.assertCanStart();
369
+ this.transitionTo("PROCESSING");
370
+ this.abortController = new AbortController();
371
+ this.startedAt = Date.now();
372
+ const allRawRecords = await this.parseRecords();
373
+ this.totalRecords = allRawRecords.length;
374
+ this.batches = this.splitIntoBatches(allRawRecords);
375
+ await this.saveState();
376
+ this.eventBus.emit({
377
+ type: "import:started",
378
+ jobId: this.jobId,
379
+ totalRecords: this.totalRecords,
380
+ totalBatches: this.batches.length,
381
+ timestamp: Date.now()
382
+ });
383
+ try {
384
+ for (let i = 0; i < this.batches.length; i++) {
385
+ if (this.abortController.signal.aborted) break;
386
+ await this.checkPause();
387
+ const batch = this.batches[i];
388
+ if (!batch) break;
389
+ await this.processBatch(batch, processor);
390
+ }
391
+ if (!this.abortController.signal.aborted && this.status !== "ABORTED") {
392
+ this.transitionTo("COMPLETED");
393
+ const summary = this.buildSummary();
394
+ this.eventBus.emit({
395
+ type: "import:completed",
396
+ jobId: this.jobId,
397
+ summary,
398
+ timestamp: Date.now()
399
+ });
400
+ }
401
+ } catch (error) {
402
+ if (this.status !== "ABORTED") {
403
+ this.transitionTo("FAILED");
404
+ this.eventBus.emit({
405
+ type: "import:failed",
406
+ jobId: this.jobId,
407
+ error: error instanceof Error ? error.message : String(error),
408
+ timestamp: Date.now()
409
+ });
410
+ }
411
+ }
412
+ await this.saveState();
413
+ }
414
+ async pause() {
415
+ if (this.status !== "PROCESSING") {
416
+ throw new Error(`Cannot pause import from status '${this.status}'`);
417
+ }
418
+ this.transitionTo("PAUSED");
419
+ this.pausePromise = this.createPausePromise();
420
+ const progress = this.buildProgress();
421
+ this.eventBus.emit({
422
+ type: "import:paused",
423
+ jobId: this.jobId,
424
+ progress,
425
+ timestamp: Date.now()
426
+ });
427
+ await this.saveState();
428
+ }
429
+ resume() {
430
+ if (this.status === "ABORTED") {
431
+ throw new Error("Cannot resume an aborted import");
432
+ }
433
+ if (this.status !== "PAUSED") {
434
+ throw new Error(`Cannot resume import from status '${this.status}'`);
435
+ }
436
+ this.transitionTo("PROCESSING");
437
+ if (this.pausePromise) {
438
+ this.pausePromise.resolve();
439
+ this.pausePromise = null;
440
+ }
441
+ }
442
+ async abort() {
443
+ if (this.status !== "PROCESSING" && this.status !== "PAUSED") {
444
+ throw new Error(`Cannot abort import from status '${this.status}'`);
445
+ }
446
+ this.transitionTo("ABORTED");
447
+ this.abortController?.abort();
448
+ if (this.pausePromise) {
449
+ this.pausePromise.resolve();
450
+ this.pausePromise = null;
451
+ }
452
+ const progress = this.buildProgress();
453
+ this.eventBus.emit({
454
+ type: "import:aborted",
455
+ jobId: this.jobId,
456
+ progress,
457
+ timestamp: Date.now()
458
+ });
459
+ await this.saveState();
460
+ }
461
+ getStatus() {
462
+ return {
463
+ state: this.status,
464
+ progress: this.buildProgress(),
465
+ batches: this.batches
466
+ };
467
+ }
468
+ getFailedRecords() {
469
+ return this.allRecords.filter((r) => r.status === "failed" || r.status === "invalid");
470
+ }
471
+ getPendingRecords() {
472
+ return this.allRecords.filter((r) => r.status === "pending" || r.status === "valid");
473
+ }
474
+ getJobId() {
475
+ return this.jobId;
476
+ }
477
+ // --- Private methods ---
478
+ async processBatch(batch, processor) {
479
+ const batchIndex = this.batches.indexOf(batch);
480
+ this.updateBatchStatus(batch.id, "PROCESSING");
481
+ this.eventBus.emit({
482
+ type: "batch:started",
483
+ jobId: this.jobId,
484
+ batchId: batch.id,
485
+ batchIndex,
486
+ recordCount: batch.records.length,
487
+ timestamp: Date.now()
488
+ });
489
+ let processedCount = 0;
490
+ let failedCount = 0;
491
+ for (const record of batch.records) {
492
+ if (this.abortController?.signal.aborted) break;
493
+ await this.checkPause();
494
+ const transformed = this.validator.applyTransforms(record.raw);
495
+ const validation = this.validator.validate(transformed);
496
+ if (!validation.isValid) {
497
+ const invalidRecord = markRecordInvalid(record, validation.errors);
498
+ this.updateRecord(record.index, invalidRecord);
499
+ failedCount++;
500
+ this.eventBus.emit({
501
+ type: "record:failed",
502
+ jobId: this.jobId,
503
+ batchId: batch.id,
504
+ recordIndex: record.index,
505
+ error: validation.errors.map((e) => e.message).join("; "),
506
+ record: invalidRecord,
507
+ timestamp: Date.now()
508
+ });
509
+ if (!this.config.continueOnError) {
510
+ throw new Error(`Validation failed for record ${String(record.index)}`);
511
+ }
512
+ continue;
513
+ }
514
+ const validRecord = markRecordValid(record, transformed);
515
+ try {
516
+ const context = {
517
+ jobId: this.jobId,
518
+ batchId: batch.id,
519
+ batchIndex,
520
+ recordIndex: record.index,
521
+ totalRecords: this.totalRecords,
522
+ signal: this.abortController?.signal ?? new AbortController().signal
523
+ };
524
+ await processor(validRecord.parsed, context);
525
+ const processed = markRecordProcessed(validRecord);
526
+ this.updateRecord(record.index, processed);
527
+ processedCount++;
528
+ this.eventBus.emit({
529
+ type: "record:processed",
530
+ jobId: this.jobId,
531
+ batchId: batch.id,
532
+ recordIndex: record.index,
533
+ timestamp: Date.now()
534
+ });
535
+ } catch (error) {
536
+ const failedRecord = markRecordFailed(validRecord, error instanceof Error ? error.message : String(error));
537
+ this.updateRecord(record.index, failedRecord);
538
+ failedCount++;
539
+ this.eventBus.emit({
540
+ type: "record:failed",
541
+ jobId: this.jobId,
542
+ batchId: batch.id,
543
+ recordIndex: record.index,
544
+ error: error instanceof Error ? error.message : String(error),
545
+ record: failedRecord,
546
+ timestamp: Date.now()
547
+ });
548
+ if (!this.config.continueOnError) {
549
+ throw error;
550
+ }
551
+ }
552
+ }
553
+ this.updateBatchStatus(batch.id, "COMPLETED", processedCount, failedCount);
554
+ this.eventBus.emit({
555
+ type: "batch:completed",
556
+ jobId: this.jobId,
557
+ batchId: batch.id,
558
+ batchIndex,
559
+ processedCount,
560
+ failedCount,
561
+ totalCount: batch.records.length,
562
+ timestamp: Date.now()
563
+ });
564
+ this.emitProgress();
565
+ }
566
+ async parseRecords(maxRecords) {
567
+ const source = this.source;
568
+ const parser = this.parser;
569
+ if (!source || !parser) {
570
+ throw new Error("Source and parser must be configured. Call .from(source, parser) first.");
571
+ }
572
+ const records = [];
573
+ let index = 0;
574
+ for await (const chunk of source.read()) {
575
+ for await (const raw of parser.parse(chunk)) {
576
+ if (maxRecords !== void 0 && records.length >= maxRecords) {
577
+ return records;
578
+ }
579
+ records.push(createPendingRecord(index, raw));
580
+ index++;
581
+ }
582
+ }
583
+ return records;
584
+ }
585
+ splitIntoBatches(records) {
586
+ const batches = [];
587
+ const { batchSize } = this.config;
588
+ for (let i = 0; i < records.length; i += batchSize) {
589
+ const batchRecords = records.slice(i, i + batchSize);
590
+ const batchId = crypto.randomUUID();
591
+ batches.push(createBatch(batchId, batches.length, batchRecords));
592
+ }
593
+ this.allRecords = [...records];
594
+ return batches;
595
+ }
596
+ updateRecord(index, record) {
597
+ const pos = this.allRecords.findIndex((r) => r.index === index);
598
+ if (pos >= 0) {
599
+ this.allRecords[pos] = record;
600
+ }
601
+ }
602
+ updateBatchStatus(batchId, status, processedCount, failedCount) {
603
+ this.batches = this.batches.map(
604
+ (b) => b.id === batchId ? {
605
+ ...b,
606
+ status,
607
+ processedCount: processedCount ?? b.processedCount,
608
+ failedCount: failedCount ?? b.failedCount
609
+ } : b
610
+ );
611
+ }
612
+ transitionTo(newStatus) {
613
+ if (!canTransition(this.status, newStatus)) {
614
+ throw new Error(`Invalid state transition: ${this.status} \u2192 ${newStatus}`);
615
+ }
616
+ this.status = newStatus;
617
+ }
618
+ buildProgress() {
619
+ const processed = this.allRecords.filter((r) => r.status === "processed").length;
620
+ const failed = this.allRecords.filter((r) => r.status === "failed" || r.status === "invalid").length;
621
+ const pending = this.totalRecords - processed - failed;
622
+ const completedBatches = this.batches.filter((b) => b.status === "COMPLETED").length;
623
+ const elapsed = this.startedAt ? Date.now() - this.startedAt : 0;
624
+ return {
625
+ totalRecords: this.totalRecords,
626
+ processedRecords: processed,
627
+ failedRecords: failed,
628
+ pendingRecords: pending,
629
+ percentage: this.totalRecords > 0 ? Math.round(processed / this.totalRecords * 100) : 0,
630
+ currentBatch: completedBatches,
631
+ totalBatches: this.batches.length,
632
+ elapsedMs: elapsed
633
+ };
634
+ }
635
+ buildSummary() {
636
+ const processed = this.allRecords.filter((r) => r.status === "processed").length;
637
+ const failed = this.allRecords.filter((r) => r.status === "failed" || r.status === "invalid").length;
638
+ const skipped = this.totalRecords - processed - failed;
639
+ const elapsed = this.startedAt ? Date.now() - this.startedAt : 0;
640
+ return { total: this.totalRecords, processed, failed, skipped, elapsedMs: elapsed };
641
+ }
642
+ emitProgress() {
643
+ this.eventBus.emit({
644
+ type: "import:progress",
645
+ jobId: this.jobId,
646
+ progress: this.buildProgress(),
647
+ timestamp: Date.now()
648
+ });
649
+ }
650
+ async saveState() {
651
+ const state = {
652
+ id: this.jobId,
653
+ config: {
654
+ schema: this.config.schema,
655
+ batchSize: this.config.batchSize,
656
+ continueOnError: this.config.continueOnError
657
+ },
658
+ status: this.status,
659
+ batches: this.batches,
660
+ totalRecords: this.totalRecords,
661
+ startedAt: this.startedAt
662
+ };
663
+ await this.stateStore.saveJobState(state);
664
+ }
665
+ async checkPause() {
666
+ if (this.pausePromise) {
667
+ await this.pausePromise.promise;
668
+ }
669
+ }
670
+ createPausePromise() {
671
+ let resolveRef;
672
+ const promise = new Promise((resolve) => {
673
+ resolveRef = resolve;
674
+ });
675
+ return { resolve: resolveRef, promise };
676
+ }
677
+ assertSourceConfigured() {
678
+ if (!this.source || !this.parser) {
679
+ throw new Error("Source and parser must be configured. Call .from(source, parser) first.");
680
+ }
681
+ }
682
+ assertCanStart() {
683
+ if (this.status !== "PREVIEWED" && this.status !== "CREATED") {
684
+ throw new Error(`Cannot start import from status '${this.status}'`);
685
+ }
686
+ }
687
+ };
688
+
689
+ // src/domain/model/BatchStatus.ts
690
+ var BatchStatus = {
691
+ PENDING: "PENDING",
692
+ PROCESSING: "PROCESSING",
693
+ PAUSED: "PAUSED",
694
+ COMPLETED: "COMPLETED",
695
+ FAILED: "FAILED"
696
+ };
697
+
698
+ // src/infrastructure/parsers/CsvParser.ts
699
+ import Papa from "papaparse";
700
+ var CsvParser = class {
701
+ constructor(options) {
702
+ this.options = {
703
+ delimiter: options?.delimiter,
704
+ encoding: options?.encoding ?? "utf-8",
705
+ hasHeader: options?.hasHeader ?? true
706
+ };
707
+ }
708
+ *parse(data) {
709
+ const content = typeof data === "string" ? data : data.toString("utf-8");
710
+ const result = Papa.parse(content, {
711
+ header: this.options.hasHeader,
712
+ delimiter: this.options.delimiter || void 0,
713
+ skipEmptyLines: true,
714
+ dynamicTyping: false
715
+ });
716
+ for (const row of result.data) {
717
+ if (this.isEmptyRow(row)) continue;
718
+ yield row;
719
+ }
720
+ }
721
+ detect(sample) {
722
+ const content = typeof sample === "string" ? sample : sample.toString("utf-8");
723
+ const firstLines = content.split("\n").slice(0, 5).join("\n");
724
+ const delimiters = [",", ";", " ", "|"];
725
+ let bestDelimiter = ",";
726
+ let maxColumns = 0;
727
+ for (const delimiter of delimiters) {
728
+ const result = Papa.parse(firstLines, { delimiter, header: false });
729
+ const firstRow = result.data[0];
730
+ if (firstRow && firstRow.length > maxColumns) {
731
+ maxColumns = firstRow.length;
732
+ bestDelimiter = delimiter;
733
+ }
734
+ }
735
+ return {
736
+ delimiter: bestDelimiter,
737
+ encoding: "utf-8",
738
+ hasHeader: true
739
+ };
740
+ }
741
+ isEmptyRow(row) {
742
+ return Object.values(row).every((v) => v === null || v === void 0 || v === "");
743
+ }
744
+ };
745
+
746
+ // src/infrastructure/sources/BufferSource.ts
747
+ var BufferSource = class {
748
+ constructor(data, metadata) {
749
+ this.content = typeof data === "string" ? data : data.toString("utf-8");
750
+ this.meta = {
751
+ fileName: metadata?.fileName ?? "buffer-input",
752
+ fileSize: this.content.length,
753
+ mimeType: metadata?.mimeType ?? "text/plain"
754
+ };
755
+ }
756
+ async *read() {
757
+ yield await Promise.resolve(this.content);
758
+ }
759
+ sample(maxBytes) {
760
+ if (maxBytes && maxBytes < this.content.length) {
761
+ return Promise.resolve(this.content.slice(0, maxBytes));
762
+ }
763
+ return Promise.resolve(this.content);
764
+ }
765
+ metadata() {
766
+ return this.meta;
767
+ }
768
+ };
769
+ export {
770
+ BatchStatus,
771
+ BufferSource,
772
+ BulkImport,
773
+ CsvParser,
774
+ ImportStatus,
775
+ InMemoryStateStore
776
+ };
777
+ //# sourceMappingURL=index.js.map