@jackchuka/gql-ingest 2.2.1 → 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 (60) hide show
  1. package/README.md +181 -1
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.d.ts.map +1 -0
  4. package/dist/cli/index.js +237 -0
  5. package/dist/index.d.ts +16 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +1379 -0
  8. package/dist/index.js.map +7 -0
  9. package/dist/{config.d.ts → lib/config.d.ts} +3 -2
  10. package/dist/lib/config.d.ts.map +1 -0
  11. package/dist/lib/dependency-resolver.d.ts.map +1 -0
  12. package/dist/lib/events.d.ts +167 -0
  13. package/dist/lib/events.d.ts.map +1 -0
  14. package/dist/lib/gql-ingest.d.ts +155 -0
  15. package/dist/lib/gql-ingest.d.ts.map +1 -0
  16. package/dist/{graphql-client.d.ts → lib/graphql-client.d.ts} +8 -4
  17. package/dist/lib/graphql-client.d.ts.map +1 -0
  18. package/dist/lib/index.d.ts +8 -0
  19. package/dist/lib/index.d.ts.map +1 -0
  20. package/dist/lib/logger.d.ts +27 -0
  21. package/dist/lib/logger.d.ts.map +1 -0
  22. package/dist/lib/mapper.d.ts +49 -0
  23. package/dist/lib/mapper.d.ts.map +1 -0
  24. package/dist/{metrics.d.ts → lib/metrics.d.ts} +22 -8
  25. package/dist/lib/metrics.d.ts.map +1 -0
  26. package/dist/readers/csv.d.ts +0 -4
  27. package/dist/readers/csv.d.ts.map +1 -1
  28. package/dist/readers/index.d.ts +1 -1
  29. package/dist/readers/index.d.ts.map +1 -1
  30. package/package.json +17 -7
  31. package/dist/cli.d.ts +0 -2
  32. package/dist/cli.d.ts.map +0 -1
  33. package/dist/cli.js +0 -237
  34. package/dist/config.d.ts.map +0 -1
  35. package/dist/config.test.d.ts +0 -2
  36. package/dist/config.test.d.ts.map +0 -1
  37. package/dist/dependency-resolver.d.ts.map +0 -1
  38. package/dist/dependency-resolver.test.d.ts +0 -2
  39. package/dist/dependency-resolver.test.d.ts.map +0 -1
  40. package/dist/graphql-client.d.ts.map +0 -1
  41. package/dist/graphql-client.test.d.ts +0 -2
  42. package/dist/graphql-client.test.d.ts.map +0 -1
  43. package/dist/mapper.d.ts +0 -31
  44. package/dist/mapper.d.ts.map +0 -1
  45. package/dist/mapper.test.d.ts +0 -2
  46. package/dist/mapper.test.d.ts.map +0 -1
  47. package/dist/metrics.d.ts.map +0 -1
  48. package/dist/metrics.test.d.ts +0 -2
  49. package/dist/metrics.test.d.ts.map +0 -1
  50. package/dist/readers/csv.test.d.ts +0 -2
  51. package/dist/readers/csv.test.d.ts.map +0 -1
  52. package/dist/readers/data-reader.test.d.ts +0 -2
  53. package/dist/readers/data-reader.test.d.ts.map +0 -1
  54. package/dist/readers/json.test.d.ts +0 -2
  55. package/dist/readers/json.test.d.ts.map +0 -1
  56. package/dist/readers/jsonl.test.d.ts +0 -2
  57. package/dist/readers/jsonl.test.d.ts.map +0 -1
  58. package/dist/readers/yaml.test.d.ts +0 -2
  59. package/dist/readers/yaml.test.d.ts.map +0 -1
  60. /package/dist/{dependency-resolver.d.ts → lib/dependency-resolver.d.ts} +0 -0
package/dist/index.js ADDED
@@ -0,0 +1,1379 @@
1
+ // src/lib/gql-ingest.ts
2
+ import { EventEmitter } from "events";
3
+
4
+ // src/lib/graphql-client.ts
5
+ import { GraphQLClient } from "graphql-request";
6
+
7
+ // src/lib/config.ts
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import * as yaml from "js-yaml";
11
+
12
+ // src/lib/logger.ts
13
+ var noopLogger = {
14
+ debug: () => {
15
+ },
16
+ info: () => {
17
+ },
18
+ warn: () => {
19
+ },
20
+ error: () => {
21
+ }
22
+ };
23
+ function createConsoleLogger() {
24
+ return {
25
+ debug: (msg, ...args) => console.debug(`[gql-ingest] ${msg}`, ...args),
26
+ info: (msg, ...args) => console.info(`[gql-ingest] ${msg}`, ...args),
27
+ warn: (msg, ...args) => console.warn(`[gql-ingest] ${msg}`, ...args),
28
+ error: (msg, ...args) => console.error(`[gql-ingest] ${msg}`, ...args)
29
+ };
30
+ }
31
+ function createDefaultLogger(verbose = false) {
32
+ return verbose ? createConsoleLogger() : noopLogger;
33
+ }
34
+
35
+ // src/lib/config.ts
36
+ var DEFAULT_RETRY_CONFIG = {
37
+ maxAttempts: 3,
38
+ baseDelay: 1e3,
39
+ maxDelay: 3e4,
40
+ exponentialBackoff: true,
41
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504]
42
+ };
43
+ var DEFAULT_PARALLEL_CONFIG = {
44
+ concurrency: 1,
45
+ entityConcurrency: 1,
46
+ preserveRowOrder: true
47
+ };
48
+ var DEFAULT_CONFIG = {
49
+ retry: DEFAULT_RETRY_CONFIG,
50
+ parallelProcessing: DEFAULT_PARALLEL_CONFIG,
51
+ entityConfig: {},
52
+ entityDependencies: {}
53
+ };
54
+ function loadConfig(configDir, logger = noopLogger) {
55
+ const configPath = path.join(configDir, "config.yaml");
56
+ try {
57
+ if (!fs.existsSync(configPath)) {
58
+ logger.info("No config.yaml found, using default sequential processing");
59
+ return DEFAULT_CONFIG;
60
+ }
61
+ const fileContents = fs.readFileSync(configPath, "utf8");
62
+ const yamlConfig = yaml.load(fileContents);
63
+ return mergeWithDefaults(yamlConfig);
64
+ } catch (error) {
65
+ const errorMessage = error instanceof Error ? error.message : String(error);
66
+ logger.warn(`Warning: Failed to parse config.yaml: ${errorMessage}. Using defaults.`);
67
+ return DEFAULT_CONFIG;
68
+ }
69
+ }
70
+ function mergeWithDefaults(yamlConfig) {
71
+ return {
72
+ retry: {
73
+ ...DEFAULT_RETRY_CONFIG,
74
+ ...yamlConfig.retry
75
+ },
76
+ parallelProcessing: {
77
+ ...DEFAULT_PARALLEL_CONFIG,
78
+ ...yamlConfig.parallelProcessing
79
+ },
80
+ entityConfig: yamlConfig.entityConfig || {},
81
+ entityDependencies: yamlConfig.entityDependencies || {}
82
+ };
83
+ }
84
+ function getEntityConfig(entityName, globalConfig, logger = noopLogger) {
85
+ const entityOverrides = globalConfig.entityConfig[entityName] || {};
86
+ let finalConfig = {
87
+ ...globalConfig.parallelProcessing,
88
+ ...entityOverrides
89
+ };
90
+ if (finalConfig.preserveRowOrder && finalConfig.concurrency > 1) {
91
+ logger.warn(
92
+ `Entity '${entityName}': preserveRowOrder=true forces concurrency=1 (was ${finalConfig.concurrency})`
93
+ );
94
+ finalConfig.concurrency = 1;
95
+ }
96
+ return finalConfig;
97
+ }
98
+ function getRetryConfig(entityName, globalConfig) {
99
+ const entityOverrides = globalConfig.entityConfig[entityName]?.retry || {};
100
+ return {
101
+ ...globalConfig.retry,
102
+ ...entityOverrides
103
+ };
104
+ }
105
+
106
+ // src/lib/graphql-client.ts
107
+ var GraphQLClientWrapper = class {
108
+ constructor(endpoint, headers, metrics, logger = noopLogger) {
109
+ this.client = new GraphQLClient(endpoint, {
110
+ headers: headers || {}
111
+ });
112
+ this.metrics = metrics;
113
+ this.logger = logger;
114
+ }
115
+ async executeMutation(mutation, variables, retryConfig, signal) {
116
+ const config = retryConfig || DEFAULT_RETRY_CONFIG;
117
+ let lastError;
118
+ const totalStartTime = Date.now();
119
+ for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
120
+ if (signal?.aborted) {
121
+ throw new Error(`Request aborted: ${signal.reason || "cancelled"}`);
122
+ }
123
+ const attemptStartTime = Date.now();
124
+ try {
125
+ const result = await this.client.request(mutation, variables);
126
+ if (this.metrics) {
127
+ const duration = Date.now() - attemptStartTime;
128
+ this.metrics.recordRequestDuration(duration);
129
+ if (attempt > 0) {
130
+ this.metrics.recordRetrySuccess(attempt);
131
+ }
132
+ }
133
+ const totalDuration2 = Date.now() - totalStartTime;
134
+ const retryInfo = attempt > 0 ? ` (succeeded on attempt ${attempt + 1})` : "";
135
+ this.logger.debug(`\u2713 GraphQL request completed in ${totalDuration2}ms${retryInfo}`, result);
136
+ return result;
137
+ } catch (error) {
138
+ lastError = error;
139
+ const duration = Date.now() - attemptStartTime;
140
+ if (this.metrics) {
141
+ this.metrics.recordRequestDuration(duration);
142
+ }
143
+ if (attempt === config.maxAttempts - 1) {
144
+ if (this.metrics && attempt > 0) {
145
+ this.metrics.recordRetryFailure(attempt);
146
+ }
147
+ break;
148
+ }
149
+ if (!this.isRetryableError(error, config)) {
150
+ this.logger.error(
151
+ `\u2717 GraphQL request failed with non-retryable error in ${duration}ms`,
152
+ error
153
+ );
154
+ throw error;
155
+ }
156
+ const delay = this.calculateDelay(attempt, config);
157
+ this.logger.debug(
158
+ `\u23F3 GraphQL request failed (attempt ${attempt + 1}/${config.maxAttempts}), retrying in ${delay}ms...`
159
+ );
160
+ await this.sleepWithSignal(delay, signal);
161
+ }
162
+ }
163
+ const totalDuration = Date.now() - totalStartTime;
164
+ this.logger.error(
165
+ `\u2717 GraphQL request failed after ${config.maxAttempts} attempts in ${totalDuration}ms`,
166
+ lastError
167
+ );
168
+ throw lastError;
169
+ }
170
+ isRetryableError(error, config) {
171
+ if (!error.response) {
172
+ return true;
173
+ }
174
+ const status = error.response.status;
175
+ return config.retryableStatusCodes.includes(status);
176
+ }
177
+ calculateDelay(attempt, config) {
178
+ if (!config.exponentialBackoff) {
179
+ return config.baseDelay;
180
+ }
181
+ const exponentialDelay = config.baseDelay * Math.pow(2, attempt);
182
+ const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
183
+ const jitter = cappedDelay * 0.2 * (Math.random() - 0.5);
184
+ return Math.max(0, cappedDelay + jitter);
185
+ }
186
+ /**
187
+ * Sleep that can be interrupted by abort signal
188
+ */
189
+ sleepWithSignal(ms, signal) {
190
+ return new Promise((resolve, reject) => {
191
+ if (signal?.aborted) {
192
+ reject(new Error(`Request aborted: ${signal.reason || "cancelled"}`));
193
+ return;
194
+ }
195
+ const timeoutId = setTimeout(resolve, ms);
196
+ if (signal) {
197
+ const abortHandler = () => {
198
+ clearTimeout(timeoutId);
199
+ reject(new Error(`Request aborted: ${signal.reason || "cancelled"}`));
200
+ };
201
+ signal.addEventListener("abort", abortHandler, { once: true });
202
+ }
203
+ });
204
+ }
205
+ setHeaders(headers) {
206
+ this.client.setHeaders(headers);
207
+ }
208
+ };
209
+
210
+ // src/lib/mapper.ts
211
+ import fs6 from "fs";
212
+ import path2 from "path";
213
+ import { parse } from "graphql";
214
+
215
+ // src/readers/data-reader.ts
216
+ var DataReader = class {
217
+ /**
218
+ * Check if this reader can handle the given file
219
+ */
220
+ canHandle(filePath) {
221
+ const extension = filePath.split(".").pop()?.toLowerCase();
222
+ return extension ? this.getSupportedExtensions().includes(extension) : false;
223
+ }
224
+ };
225
+ var DataReaderFactory = class {
226
+ static {
227
+ this.readers = [];
228
+ }
229
+ static registerReader(reader) {
230
+ this.readers.push(reader);
231
+ }
232
+ static getReader(filePath, format) {
233
+ if (format) {
234
+ const reader2 = this.readers.find(
235
+ (r) => r.getSupportedExtensions().includes(format.toLowerCase())
236
+ );
237
+ if (reader2) return reader2;
238
+ }
239
+ const reader = this.readers.find((r) => r.canHandle(filePath));
240
+ if (!reader) {
241
+ throw new Error(
242
+ `No reader found for file: ${filePath}${format ? ` with format: ${format}` : ""}`
243
+ );
244
+ }
245
+ return reader;
246
+ }
247
+ static getSupportedFormats() {
248
+ const formats = /* @__PURE__ */ new Set();
249
+ this.readers.forEach((reader) => {
250
+ reader.getSupportedExtensions().forEach((ext) => formats.add(ext));
251
+ });
252
+ return Array.from(formats);
253
+ }
254
+ };
255
+
256
+ // src/readers/csv.ts
257
+ import fs2 from "fs";
258
+ import csv from "csv-parser";
259
+ var CsvReader = class extends DataReader {
260
+ getSupportedExtensions() {
261
+ return ["csv"];
262
+ }
263
+ async readFile(filePath) {
264
+ return new Promise((resolve, reject) => {
265
+ const results = [];
266
+ fs2.createReadStream(filePath).pipe(csv()).on("data", (data) => results.push(data)).on("end", () => resolve(results)).on("error", (error) => reject(error));
267
+ });
268
+ }
269
+ };
270
+
271
+ // src/readers/json.ts
272
+ import fs3 from "fs/promises";
273
+ var JsonReader = class extends DataReader {
274
+ getSupportedExtensions() {
275
+ return ["json"];
276
+ }
277
+ async readFile(filePath) {
278
+ const content = await fs3.readFile(filePath, "utf8");
279
+ const data = JSON.parse(content);
280
+ if (Array.isArray(data)) {
281
+ return data;
282
+ }
283
+ if (typeof data === "object" && data !== null) {
284
+ return [data];
285
+ }
286
+ throw new Error(`Invalid JSON data structure in file: ${filePath}. Expected array or object.`);
287
+ }
288
+ };
289
+
290
+ // src/readers/yaml.ts
291
+ import fs4 from "fs/promises";
292
+ import yaml2 from "js-yaml";
293
+ var YamlReader = class extends DataReader {
294
+ getSupportedExtensions() {
295
+ return ["yaml", "yml"];
296
+ }
297
+ async readFile(filePath) {
298
+ const content = await fs4.readFile(filePath, "utf8");
299
+ const data = yaml2.load(content);
300
+ if (Array.isArray(data)) {
301
+ return data;
302
+ }
303
+ if (typeof data === "object" && data !== null) {
304
+ return [data];
305
+ }
306
+ throw new Error(`Invalid YAML data structure in file: ${filePath}. Expected array or object.`);
307
+ }
308
+ };
309
+
310
+ // src/readers/jsonl.ts
311
+ import fs5 from "fs/promises";
312
+ var JsonlReader = class extends DataReader {
313
+ getSupportedExtensions() {
314
+ return ["jsonl", "ndjson"];
315
+ }
316
+ async readFile(filePath) {
317
+ const content = await fs5.readFile(filePath, "utf8");
318
+ const lines = content.split("\n").filter((line) => line.trim());
319
+ const results = [];
320
+ for (let i = 0; i < lines.length; i++) {
321
+ try {
322
+ const data = JSON.parse(lines[i]);
323
+ results.push(data);
324
+ } catch (error) {
325
+ const errorMessage = error instanceof Error ? error.message : String(error);
326
+ throw new Error(
327
+ `Invalid JSON at line ${i + 1} in file: ${filePath}. Error: ${errorMessage}`
328
+ );
329
+ }
330
+ }
331
+ return results;
332
+ }
333
+ };
334
+
335
+ // src/readers/index.ts
336
+ DataReaderFactory.registerReader(new CsvReader());
337
+ DataReaderFactory.registerReader(new JsonReader());
338
+ DataReaderFactory.registerReader(new YamlReader());
339
+ DataReaderFactory.registerReader(new JsonlReader());
340
+
341
+ // src/lib/metrics.ts
342
+ var MetricsCollector = class {
343
+ constructor() {
344
+ this.metrics = {
345
+ totalEntities: 0,
346
+ totalSuccesses: 0,
347
+ totalFailures: 0,
348
+ entityMetrics: /* @__PURE__ */ new Map(),
349
+ requestDurations: [],
350
+ retryAttempts: 0,
351
+ retrySuccesses: 0,
352
+ retryFailures: 0,
353
+ startTime: Date.now()
354
+ };
355
+ }
356
+ startEntityProcessing(entityName) {
357
+ if (!this.metrics.entityMetrics.has(entityName)) {
358
+ this.metrics.entityMetrics.set(entityName, {
359
+ entityName,
360
+ successCount: 0,
361
+ failureCount: 0,
362
+ startTime: Date.now()
363
+ });
364
+ }
365
+ }
366
+ recordSuccess(entityName) {
367
+ const entityMetric = this.metrics.entityMetrics.get(entityName);
368
+ if (entityMetric) {
369
+ entityMetric.successCount++;
370
+ this.metrics.totalSuccesses++;
371
+ this.metrics.totalEntities++;
372
+ }
373
+ }
374
+ recordFailure(entityName) {
375
+ const entityMetric = this.metrics.entityMetrics.get(entityName);
376
+ if (entityMetric) {
377
+ entityMetric.failureCount++;
378
+ this.metrics.totalFailures++;
379
+ this.metrics.totalEntities++;
380
+ }
381
+ }
382
+ finishEntityProcessing(entityName) {
383
+ const entityMetric = this.metrics.entityMetrics.get(entityName);
384
+ if (entityMetric) {
385
+ entityMetric.endTime = Date.now();
386
+ }
387
+ }
388
+ finishProcessing() {
389
+ this.metrics.endTime = Date.now();
390
+ return { ...this.metrics };
391
+ }
392
+ getEntityMetrics(entityName) {
393
+ return this.metrics.entityMetrics.get(entityName);
394
+ }
395
+ getMetrics() {
396
+ const endTime = this.metrics.endTime || Date.now();
397
+ const totalDuration = endTime - this.metrics.startTime;
398
+ const entities = {};
399
+ for (const [name, metrics] of this.metrics.entityMetrics) {
400
+ const entityEndTime = metrics.endTime || Date.now();
401
+ entities[name] = {
402
+ rowsProcessed: metrics.successCount + metrics.failureCount,
403
+ successfulRows: metrics.successCount,
404
+ failedRows: metrics.failureCount,
405
+ duration: entityEndTime - metrics.startTime
406
+ };
407
+ }
408
+ return {
409
+ totalRows: this.metrics.totalEntities,
410
+ successfulOperations: this.metrics.totalSuccesses,
411
+ failedOperations: this.metrics.totalFailures,
412
+ totalDuration,
413
+ entities
414
+ };
415
+ }
416
+ getTotalProcessed() {
417
+ return this.metrics.totalEntities;
418
+ }
419
+ getSuccessRate() {
420
+ if (this.metrics.totalEntities === 0) return 0;
421
+ return this.metrics.totalSuccesses / this.metrics.totalEntities * 100;
422
+ }
423
+ recordRequestDuration(duration) {
424
+ this.metrics.requestDurations.push(duration);
425
+ }
426
+ recordRetrySuccess(attempts) {
427
+ this.metrics.retryAttempts += attempts;
428
+ this.metrics.retrySuccesses++;
429
+ }
430
+ recordRetryFailure(attempts) {
431
+ this.metrics.retryAttempts += attempts;
432
+ this.metrics.retryFailures++;
433
+ }
434
+ getAverageRequestDuration() {
435
+ if (this.metrics.requestDurations.length === 0) return 0;
436
+ const sum = this.metrics.requestDurations.reduce((a, b) => a + b, 0);
437
+ return sum / this.metrics.requestDurations.length;
438
+ }
439
+ getDurationMs() {
440
+ const endTime = this.metrics.endTime || Date.now();
441
+ return endTime - this.metrics.startTime;
442
+ }
443
+ generateSummary() {
444
+ const duration = this.getDurationMs();
445
+ const successRate = this.getSuccessRate();
446
+ const avgRequestDuration = this.getAverageRequestDuration();
447
+ let summary = `
448
+ \u{1F4CA} Processing Summary:
449
+ `;
450
+ summary += ` Total Processed: ${this.metrics.totalEntities}
451
+ `;
452
+ summary += ` \u2713 Successes: ${this.metrics.totalSuccesses}
453
+ `;
454
+ summary += ` \u2717 Failures: ${this.metrics.totalFailures}
455
+ `;
456
+ summary += ` Success Rate: ${successRate.toFixed(1)}%
457
+ `;
458
+ summary += ` Duration: ${(duration / 1e3).toFixed(2)}s
459
+ `;
460
+ if (this.metrics.requestDurations.length > 0) {
461
+ summary += ` Avg Request Time: ${avgRequestDuration.toFixed(0)}ms
462
+ `;
463
+ }
464
+ if (this.metrics.retryAttempts > 0) {
465
+ summary += ` Retry Attempts: ${this.metrics.retryAttempts}
466
+ `;
467
+ summary += ` Retry Successes: ${this.metrics.retrySuccesses}
468
+ `;
469
+ summary += ` Retry Failures: ${this.metrics.retryFailures}
470
+ `;
471
+ }
472
+ if (this.metrics.entityMetrics.size > 0) {
473
+ summary += `
474
+ \u{1F4CB} Per-Entity Breakdown:
475
+ `;
476
+ for (const [entityName, entityMetric] of this.metrics.entityMetrics) {
477
+ const entityTotal = entityMetric.successCount + entityMetric.failureCount;
478
+ const entityRate = entityTotal > 0 ? entityMetric.successCount / entityTotal * 100 : 0;
479
+ const entityDuration = entityMetric.endTime ? entityMetric.endTime - entityMetric.startTime : 0;
480
+ summary += ` ${entityName}: ${entityTotal} total (${entityMetric.successCount} \u2713, ${entityMetric.failureCount} \u2717) - ${entityRate.toFixed(
481
+ 1
482
+ )}% success - ${(entityDuration / 1e3).toFixed(2)}s
483
+ `;
484
+ }
485
+ }
486
+ return summary;
487
+ }
488
+ };
489
+
490
+ // src/lib/mapper.ts
491
+ var DataMapper = class {
492
+ constructor(client, basePath = process.cwd(), metrics, logger = noopLogger, formatOverride) {
493
+ this.client = client;
494
+ this.basePath = basePath;
495
+ this.metrics = metrics || new MetricsCollector();
496
+ this.logger = logger;
497
+ this.formatOverride = formatOverride;
498
+ }
499
+ discoverMappings(configDir, entityFilter) {
500
+ const mappingsPath = path2.resolve(this.basePath, configDir, "mappings");
501
+ try {
502
+ const files = fs6.readdirSync(mappingsPath);
503
+ let jsonFiles = files.filter((file) => file.endsWith(".json"));
504
+ if (entityFilter && entityFilter.length > 0) {
505
+ const requestedEntities = new Set(entityFilter);
506
+ const foundEntities = /* @__PURE__ */ new Set();
507
+ jsonFiles = jsonFiles.filter((file) => {
508
+ const entityName = path2.basename(file, ".json");
509
+ if (requestedEntities.has(entityName)) {
510
+ foundEntities.add(entityName);
511
+ return true;
512
+ }
513
+ return false;
514
+ });
515
+ const notFound = entityFilter.filter((e) => !foundEntities.has(e));
516
+ if (notFound.length > 0) {
517
+ this.logger.warn(
518
+ `Warning: The following entities were not found in mappings: ${notFound.join(", ")}`
519
+ );
520
+ }
521
+ }
522
+ jsonFiles.sort();
523
+ this.logger.info(`Discovered ${jsonFiles.length} mapping files: ${jsonFiles.join(", ")}`);
524
+ return jsonFiles.map((file) => path2.join(configDir, "mappings", file));
525
+ } catch (error) {
526
+ const message = error instanceof Error ? error.message : String(error);
527
+ this.logger.error(`Error reading mappings directory ${mappingsPath}: ${message}`);
528
+ return [];
529
+ }
530
+ }
531
+ /**
532
+ * Process an entity (backward-compatible method)
533
+ */
534
+ async processEntity(configPath, parallelConfig, retryConfig) {
535
+ return this.processEntityWithEvents(configPath, parallelConfig, retryConfig);
536
+ }
537
+ /**
538
+ * Process an entity with event callbacks and abort support
539
+ */
540
+ async processEntityWithEvents(configPath, parallelConfig, retryConfig, signal, callbacks) {
541
+ const entityName = path2.basename(configPath, ".json");
542
+ const entityStartTime = Date.now();
543
+ this.logger.info(`Processing entity: ${configPath}`);
544
+ this.metrics.startEntityProcessing(entityName);
545
+ const configFullPath = path2.resolve(this.basePath, configPath);
546
+ const config = JSON.parse(fs6.readFileSync(configFullPath, "utf8"));
547
+ const configDir = path2.dirname(path2.dirname(configFullPath));
548
+ const dataFile = config.dataFile || config.csvFile;
549
+ if (!dataFile) {
550
+ throw new Error(`No data file specified in mapping config: ${configPath}`);
551
+ }
552
+ const dataPath = path2.resolve(configDir, dataFile);
553
+ const format = this.formatOverride || config.dataFormat;
554
+ const reader = DataReaderFactory.getReader(dataPath, format);
555
+ const data = await reader.readFile(dataPath);
556
+ const graphqlPath = path2.resolve(configDir, config.graphqlFile);
557
+ const mutation = fs6.readFileSync(graphqlPath, "utf8");
558
+ callbacks?.onEntityStart?.({
559
+ entityName,
560
+ mappingPath: configPath,
561
+ totalRows: data.length
562
+ });
563
+ if (parallelConfig && parallelConfig.concurrency > 1) {
564
+ await this.processRowsConcurrentlyWithEvents(
565
+ data,
566
+ mutation,
567
+ config.mapping,
568
+ entityName,
569
+ parallelConfig,
570
+ retryConfig,
571
+ signal,
572
+ callbacks
573
+ );
574
+ } else {
575
+ await this.processRowsSequentiallyWithEvents(
576
+ data,
577
+ mutation,
578
+ config.mapping,
579
+ entityName,
580
+ retryConfig,
581
+ signal,
582
+ callbacks
583
+ );
584
+ }
585
+ this.metrics.finishEntityProcessing(entityName);
586
+ const internalMetrics = this.metrics.getEntityMetrics(entityName);
587
+ const duration = Date.now() - entityStartTime;
588
+ const entityMetrics = internalMetrics ? {
589
+ rowsProcessed: internalMetrics.successCount + internalMetrics.failureCount,
590
+ successfulRows: internalMetrics.successCount,
591
+ failedRows: internalMetrics.failureCount,
592
+ duration
593
+ } : {
594
+ rowsProcessed: 0,
595
+ successfulRows: 0,
596
+ failedRows: 0,
597
+ duration
598
+ };
599
+ callbacks?.onEntityComplete?.({
600
+ entityName,
601
+ metrics: entityMetrics,
602
+ success: entityMetrics.failedRows === 0,
603
+ durationMs: duration
604
+ });
605
+ }
606
+ async processRowsSequentiallyWithEvents(data, mutation, mapping, entityName, retryConfig, signal, callbacks) {
607
+ const totalRows = data.length;
608
+ const variableTypes = this.extractVariableTypes(mutation);
609
+ for (let i = 0; i < data.length; i++) {
610
+ if (signal?.aborted) {
611
+ this.logger.info(`Processing cancelled at row ${i + 1}/${totalRows}`);
612
+ return;
613
+ }
614
+ const row = data[i];
615
+ const variables = this.mapRowToVariables(row, mapping, variableTypes);
616
+ const rowStartTime = Date.now();
617
+ try {
618
+ const result = await this.client.executeMutation(mutation, variables, retryConfig, signal);
619
+ this.metrics.recordSuccess(entityName);
620
+ callbacks?.onRowSuccess?.({
621
+ entityName,
622
+ rowIndex: i,
623
+ row,
624
+ result,
625
+ durationMs: Date.now() - rowStartTime
626
+ });
627
+ if ((i + 1) % Math.max(1, Math.floor(totalRows / 10)) === 0 || i === totalRows - 1) {
628
+ const progress = ((i + 1) / totalRows * 100).toFixed(1);
629
+ this.logger.info(`\u{1F4CA} Progress: ${i + 1}/${totalRows} (${progress}%) \u2713`);
630
+ }
631
+ } catch (error) {
632
+ if (signal?.aborted) {
633
+ this.logger.info(`Processing cancelled at row ${i + 1}/${totalRows}`);
634
+ return;
635
+ }
636
+ this.metrics.recordFailure(entityName);
637
+ this.logger.error(`\u2717 Failed to create entity for row ${i + 1}`, row, error);
638
+ callbacks?.onRowFailure?.({
639
+ entityName,
640
+ rowIndex: i,
641
+ row,
642
+ error: error instanceof Error ? error : new Error(String(error)),
643
+ retryAttempts: retryConfig?.maxAttempts ?? 3
644
+ });
645
+ }
646
+ }
647
+ }
648
+ async processRowsConcurrentlyWithEvents(data, mutation, mapping, entityName, parallelConfig, retryConfig, signal, callbacks) {
649
+ const concurrency = parallelConfig.concurrency;
650
+ this.logger.info(`Processing ${data.length} rows with concurrency: ${concurrency}`);
651
+ const variableTypes = this.extractVariableTypes(mutation);
652
+ const chunks = this.chunkArray(data, concurrency);
653
+ let processedCount = 0;
654
+ const totalRows = data.length;
655
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
656
+ if (signal?.aborted) {
657
+ this.logger.info(`Processing cancelled at chunk ${chunkIndex + 1}/${chunks.length}`);
658
+ return;
659
+ }
660
+ const chunk = chunks[chunkIndex];
661
+ const chunkStartIndex = chunkIndex * concurrency;
662
+ const promises = chunk.map(async (row, index) => {
663
+ const rowIndex = chunkStartIndex + index;
664
+ const variables = this.mapRowToVariables(row, mapping, variableTypes);
665
+ const rowStartTime = Date.now();
666
+ try {
667
+ const result = await this.client.executeMutation(
668
+ mutation,
669
+ variables,
670
+ retryConfig,
671
+ signal
672
+ );
673
+ this.metrics.recordSuccess(entityName);
674
+ callbacks?.onRowSuccess?.({
675
+ entityName,
676
+ rowIndex,
677
+ row,
678
+ result,
679
+ durationMs: Date.now() - rowStartTime
680
+ });
681
+ return { success: true, result, row, rowIndex };
682
+ } catch (error) {
683
+ this.metrics.recordFailure(entityName);
684
+ callbacks?.onRowFailure?.({
685
+ entityName,
686
+ rowIndex,
687
+ row,
688
+ error: error instanceof Error ? error : new Error(String(error)),
689
+ retryAttempts: retryConfig?.maxAttempts ?? 3
690
+ });
691
+ return { success: false, error, row, rowIndex };
692
+ }
693
+ });
694
+ const results = await Promise.allSettled(promises);
695
+ processedCount += chunk.length;
696
+ if (signal?.aborted) {
697
+ this.logger.info(`Processing cancelled after chunk ${chunkIndex + 1}/${chunks.length}`);
698
+ return;
699
+ }
700
+ let chunkSuccesses = 0;
701
+ let chunkFailures = 0;
702
+ results.forEach((result) => {
703
+ if (result.status === "fulfilled") {
704
+ const { success, error, row } = result.value;
705
+ if (success) {
706
+ chunkSuccesses++;
707
+ } else {
708
+ chunkFailures++;
709
+ this.logger.error(`\u2717 Failed to create entity for row`, row, error);
710
+ }
711
+ } else {
712
+ chunkFailures++;
713
+ this.logger.error(`\u2717 Promise rejected: ${result.reason}`);
714
+ }
715
+ });
716
+ const progress = (processedCount / totalRows * 100).toFixed(1);
717
+ this.logger.info(
718
+ `\u{1F4CA} Progress: ${processedCount}/${totalRows} (${progress}%) - Chunk ${chunkIndex + 1}: ${chunkSuccesses} \u2713, ${chunkFailures} \u2717`
719
+ );
720
+ }
721
+ }
722
+ chunkArray(array, chunkSize) {
723
+ const chunks = [];
724
+ for (let i = 0; i < array.length; i += chunkSize) {
725
+ chunks.push(array.slice(i, i + chunkSize));
726
+ }
727
+ return chunks;
728
+ }
729
+ mapRowToVariables(row, mapping, variableTypes) {
730
+ const variables = {};
731
+ for (const [graphqlVar, mappingValue] of Object.entries(mapping)) {
732
+ if (mappingValue === "$") {
733
+ variables[graphqlVar] = row;
734
+ } else if (typeof mappingValue === "string" && mappingValue.startsWith("$.")) {
735
+ const path3 = mappingValue.substring(2);
736
+ const value = this.getValueByPath(row, path3);
737
+ if (value !== void 0) {
738
+ const type = variableTypes[graphqlVar];
739
+ variables[graphqlVar] = this.convertValue(value, type, graphqlVar);
740
+ }
741
+ } else if (typeof mappingValue === "string" && row[mappingValue] !== void 0) {
742
+ const rawValue = row[mappingValue];
743
+ const type = variableTypes[graphqlVar];
744
+ variables[graphqlVar] = this.convertValue(rawValue, type, graphqlVar);
745
+ } else if (typeof mappingValue === "object" && mappingValue !== null) {
746
+ variables[graphqlVar] = this.mapNestedObject(row, mappingValue, variableTypes);
747
+ }
748
+ }
749
+ return variables;
750
+ }
751
+ getValueByPath(obj, path3) {
752
+ const parts = path3.split(".");
753
+ let current = obj;
754
+ for (const part of parts) {
755
+ if (current && typeof current === "object" && part in current) {
756
+ current = current[part];
757
+ } else {
758
+ return void 0;
759
+ }
760
+ }
761
+ return current;
762
+ }
763
+ mapNestedObject(row, mappingObj, variableTypes) {
764
+ if (Array.isArray(mappingObj)) {
765
+ return mappingObj.map((item) => this.mapNestedObject(row, item, variableTypes));
766
+ }
767
+ if (typeof mappingObj === "object" && mappingObj !== null) {
768
+ const result = {};
769
+ for (const [key, value] of Object.entries(mappingObj)) {
770
+ if (typeof value === "string" && value.startsWith("$.")) {
771
+ const path3 = value.substring(2);
772
+ let fieldValue = this.getValueByPath(row, path3);
773
+ if (key === "values" && typeof fieldValue === "string" && fieldValue.includes(",")) {
774
+ fieldValue = fieldValue.split(",").map((v) => v.trim());
775
+ }
776
+ result[key] = fieldValue;
777
+ } else if (typeof value === "string" && row[value] !== void 0) {
778
+ result[key] = row[value];
779
+ } else if (typeof value === "object") {
780
+ result[key] = this.mapNestedObject(row, value, variableTypes);
781
+ } else {
782
+ result[key] = value;
783
+ }
784
+ }
785
+ return result;
786
+ }
787
+ return mappingObj;
788
+ }
789
+ extractVariableTypes(mutation) {
790
+ const types = {};
791
+ try {
792
+ const document = parse(mutation);
793
+ for (const definition of document.definitions) {
794
+ if (definition.kind === "OperationDefinition" && definition.variableDefinitions) {
795
+ for (const variableDef of definition.variableDefinitions) {
796
+ const varName = variableDef.variable.name.value;
797
+ const typeName = this.extractTypeName(variableDef);
798
+ if (typeName) {
799
+ types[varName] = typeName;
800
+ }
801
+ }
802
+ }
803
+ }
804
+ } catch (error) {
805
+ const message = error instanceof Error ? error.message : String(error);
806
+ this.logger.error(`Error parsing GraphQL mutation: ${message}`);
807
+ }
808
+ return types;
809
+ }
810
+ extractTypeName(variableDef) {
811
+ const type = variableDef.type;
812
+ if (type.kind === "NonNullType") {
813
+ if (type.type.kind === "NamedType") {
814
+ return type.type.name.value;
815
+ }
816
+ } else if (type.kind === "NamedType") {
817
+ return type.name.value;
818
+ }
819
+ return null;
820
+ }
821
+ convertValue(value, type, varName) {
822
+ if (!type) {
823
+ return value;
824
+ }
825
+ if (typeof value !== "string") {
826
+ return value;
827
+ }
828
+ const trimmedValue = value.trim();
829
+ switch (type) {
830
+ case "Int":
831
+ const intValue = Number(trimmedValue);
832
+ if (isNaN(intValue) || !isFinite(intValue) || !Number.isInteger(intValue)) {
833
+ this.logger.warn(
834
+ `Warning: Cannot convert "${value}" to Int for variable $${varName}. Expected a valid integer. Using original value.`
835
+ );
836
+ return value;
837
+ }
838
+ return intValue;
839
+ case "Float":
840
+ const floatValue = Number(trimmedValue);
841
+ if (isNaN(floatValue) || !isFinite(floatValue)) {
842
+ this.logger.warn(
843
+ `Warning: Cannot convert "${value}" to Float for variable $${varName}. Expected a valid number. Using original value.`
844
+ );
845
+ return value;
846
+ }
847
+ return floatValue;
848
+ case "Boolean":
849
+ const lowerValue = trimmedValue.toLowerCase();
850
+ if (lowerValue === "true" || lowerValue === "1") return true;
851
+ if (lowerValue === "false" || lowerValue === "0") return false;
852
+ this.logger.warn(
853
+ `Warning: Cannot convert "${value}" to Boolean for variable $${varName}. Expected "true", "false", "1", or "0". Using original value.`
854
+ );
855
+ return value;
856
+ case "String":
857
+ return value;
858
+ default:
859
+ this.logger.debug(
860
+ `Unknown GraphQL type "${type}" for variable $${varName}. Keeping value as string.`
861
+ );
862
+ return value;
863
+ }
864
+ }
865
+ getMetrics() {
866
+ return this.metrics;
867
+ }
868
+ };
869
+
870
+ // src/lib/dependency-resolver.ts
871
+ var DependencyResolver = class {
872
+ constructor(entities, dependencies = {}, allowPartialResolution = false) {
873
+ this.entities = entities;
874
+ this.dependencies = dependencies;
875
+ this.allowPartialResolution = allowPartialResolution;
876
+ }
877
+ resolveExecutionOrder() {
878
+ const waves = [];
879
+ const processed = /* @__PURE__ */ new Set();
880
+ const inProgress = /* @__PURE__ */ new Set();
881
+ let waveNumber = 0;
882
+ while (processed.size < this.entities.length) {
883
+ const currentWave = [];
884
+ for (const entity of this.entities) {
885
+ if (processed.has(entity) || inProgress.has(entity)) {
886
+ continue;
887
+ }
888
+ const deps = this.dependencies[entity] || [];
889
+ const canProcess = deps.every(
890
+ (dep) => processed.has(dep) || this.allowPartialResolution && !this.entities.includes(dep)
891
+ );
892
+ if (canProcess) {
893
+ currentWave.push(entity);
894
+ inProgress.add(entity);
895
+ }
896
+ }
897
+ if (currentWave.length === 0) {
898
+ const remaining = this.entities.filter((e) => !processed.has(e));
899
+ throw new Error(
900
+ `Circular dependency detected or missing dependencies for entities: ${remaining.join(
901
+ ", "
902
+ )}`
903
+ );
904
+ }
905
+ waves.push({
906
+ entities: currentWave,
907
+ wave: waveNumber++
908
+ });
909
+ currentWave.forEach((entity) => {
910
+ processed.add(entity);
911
+ inProgress.delete(entity);
912
+ });
913
+ }
914
+ return waves;
915
+ }
916
+ validateDependencies() {
917
+ const errors = [];
918
+ const entitySet = new Set(this.entities);
919
+ for (const [entity, deps] of Object.entries(this.dependencies)) {
920
+ if (!entitySet.has(entity)) {
921
+ errors.push(`Entity '${entity}' has dependencies but is not in the entity list`);
922
+ continue;
923
+ }
924
+ for (const dep of deps) {
925
+ if (!entitySet.has(dep)) {
926
+ errors.push(`Entity '${entity}' depends on '${dep}' which does not exist`);
927
+ }
928
+ }
929
+ }
930
+ return errors;
931
+ }
932
+ getDependents(entityName) {
933
+ return Object.entries(this.dependencies).filter(([_, deps]) => deps.includes(entityName)).map(([entity, _]) => entity);
934
+ }
935
+ getDependencies(entityName) {
936
+ return this.dependencies[entityName] || [];
937
+ }
938
+ };
939
+
940
+ // src/lib/gql-ingest.ts
941
+ import { basename } from "path";
942
+
943
+ // src/lib/events.ts
944
+ var DEFAULT_EVENT_OPTIONS = {
945
+ emitRowEvents: true,
946
+ progressInterval: 1e3,
947
+ emitProgressEvents: true
948
+ };
949
+
950
+ // src/lib/gql-ingest.ts
951
+ var GQLIngest = class extends EventEmitter {
952
+ constructor(options) {
953
+ super();
954
+ // Cancellation state
955
+ this.abortController = null;
956
+ this.isProcessing = false;
957
+ this.startTime = 0;
958
+ this.progressIntervalId = null;
959
+ // Processing state for progress tracking
960
+ this.currentWave = 0;
961
+ this.totalWaves = 0;
962
+ this.entitiesCompleted = 0;
963
+ this.totalEntities = 0;
964
+ this.endpoint = options.endpoint;
965
+ this.headers = options.headers || {};
966
+ this.logger = options.logger ?? noopLogger;
967
+ this.formatOverride = options.formatOverride;
968
+ this.eventOptions = { ...DEFAULT_EVENT_OPTIONS, ...options.eventOptions };
969
+ this.metrics = new MetricsCollector();
970
+ this.client = new GraphQLClientWrapper(this.endpoint, this.headers, this.metrics, this.logger);
971
+ this.mapper = new DataMapper(
972
+ this.client,
973
+ process.cwd(),
974
+ this.metrics,
975
+ this.logger,
976
+ this.formatOverride
977
+ );
978
+ }
979
+ /**
980
+ * Cancel the current ingestion process
981
+ * @param reason Optional reason for cancellation
982
+ */
983
+ cancel(reason = "User requested cancellation") {
984
+ if (this.abortController && this.isProcessing) {
985
+ this.abortController.abort(reason);
986
+ }
987
+ }
988
+ /**
989
+ * Check if processing is currently in progress
990
+ */
991
+ get processing() {
992
+ return this.isProcessing;
993
+ }
994
+ /**
995
+ * Safely emit an event, catching any errors from listeners
996
+ */
997
+ safeEmit(event, payload) {
998
+ try {
999
+ return this.emit(event, payload);
1000
+ } catch (error) {
1001
+ this.logger.error(`Error in event listener for '${String(event)}':`, error);
1002
+ return false;
1003
+ }
1004
+ }
1005
+ /**
1006
+ * Start the progress interval timer
1007
+ */
1008
+ startProgressInterval() {
1009
+ if (!this.eventOptions.emitProgressEvents) return;
1010
+ this.progressIntervalId = setInterval(() => {
1011
+ this.emitProgressEvent();
1012
+ }, this.eventOptions.progressInterval);
1013
+ }
1014
+ /**
1015
+ * Stop the progress interval timer
1016
+ */
1017
+ stopProgressInterval() {
1018
+ if (this.progressIntervalId) {
1019
+ clearInterval(this.progressIntervalId);
1020
+ this.progressIntervalId = null;
1021
+ }
1022
+ }
1023
+ /**
1024
+ * Emit a progress event with current state
1025
+ */
1026
+ emitProgressEvent() {
1027
+ const metrics = this.metrics.getMetrics();
1028
+ const payload = {
1029
+ currentWave: this.currentWave,
1030
+ totalWaves: this.totalWaves,
1031
+ entitiesCompleted: this.entitiesCompleted,
1032
+ totalEntities: this.totalEntities,
1033
+ rowsProcessed: metrics.totalRows,
1034
+ successfulRows: metrics.successfulOperations,
1035
+ failedRows: metrics.failedOperations,
1036
+ progressPercent: this.totalEntities > 0 ? this.entitiesCompleted / this.totalEntities * 100 : 0,
1037
+ elapsedMs: Date.now() - this.startTime
1038
+ };
1039
+ this.safeEmit("progress", payload);
1040
+ }
1041
+ /**
1042
+ * Handle cancellation and emit event
1043
+ */
1044
+ handleCancellation(reason) {
1045
+ this.stopProgressInterval();
1046
+ const payload = {
1047
+ reason: reason || "Cancelled",
1048
+ metrics: this.metrics.getMetrics(),
1049
+ currentEntity: this.currentEntity,
1050
+ elapsedMs: Date.now() - this.startTime
1051
+ };
1052
+ this.safeEmit("cancelled", payload);
1053
+ return {
1054
+ metrics: this.metrics.getMetrics(),
1055
+ success: false,
1056
+ cancelled: true,
1057
+ errors: [`Operation cancelled: ${reason}`]
1058
+ };
1059
+ }
1060
+ /**
1061
+ * Combine multiple AbortSignals into one
1062
+ */
1063
+ combineSignals(...signals) {
1064
+ const controller = new AbortController();
1065
+ for (const signal of signals) {
1066
+ if (signal.aborted) {
1067
+ controller.abort(signal.reason);
1068
+ break;
1069
+ }
1070
+ signal.addEventListener("abort", () => controller.abort(signal.reason), {
1071
+ once: true
1072
+ });
1073
+ }
1074
+ return controller.signal;
1075
+ }
1076
+ /**
1077
+ * Ingest data from a configuration directory
1078
+ * @param configPath Path to configuration directory (containing data/, graphql/, mappings/ subdirectories)
1079
+ * @param options Optional ingestion options
1080
+ * @returns Promise with ingestion result
1081
+ */
1082
+ async ingest(configPath, options) {
1083
+ const errors = [];
1084
+ this.abortController = new AbortController();
1085
+ const signal = options?.signal ? this.combineSignals(options.signal, this.abortController.signal) : this.abortController.signal;
1086
+ this.isProcessing = true;
1087
+ this.startTime = Date.now();
1088
+ this.entitiesCompleted = 0;
1089
+ this.currentWave = 0;
1090
+ this.currentEntity = void 0;
1091
+ try {
1092
+ if (signal.aborted) {
1093
+ return this.handleCancellation(signal.reason);
1094
+ }
1095
+ this.metrics = new MetricsCollector();
1096
+ this.client = new GraphQLClientWrapper(
1097
+ this.endpoint,
1098
+ this.headers,
1099
+ this.metrics,
1100
+ this.logger
1101
+ );
1102
+ this.mapper = new DataMapper(
1103
+ this.client,
1104
+ process.cwd(),
1105
+ this.metrics,
1106
+ this.logger,
1107
+ options?.format ?? this.formatOverride
1108
+ );
1109
+ const config = loadConfig(configPath, this.logger);
1110
+ let entityFilter;
1111
+ if (options?.entities) {
1112
+ if (typeof options.entities === "string") {
1113
+ entityFilter = options.entities.split(",").map((e) => e.trim());
1114
+ } else {
1115
+ entityFilter = options.entities;
1116
+ }
1117
+ }
1118
+ const mappingPaths = this.mapper.discoverMappings(configPath, entityFilter);
1119
+ if (mappingPaths.length === 0) {
1120
+ const filterMsg = entityFilter ? ` matching entities: ${entityFilter.join(", ")}` : "";
1121
+ const warning = `No mapping files found in ${configPath}/mappings${filterMsg}`;
1122
+ this.logger.warn(warning);
1123
+ return {
1124
+ metrics: this.metrics.getMetrics(),
1125
+ success: false,
1126
+ errors: [warning]
1127
+ };
1128
+ }
1129
+ const entityNames = mappingPaths.map((path3) => basename(path3, ".json"));
1130
+ this.totalEntities = entityNames.length;
1131
+ const relevantDependencies = {};
1132
+ if (config.entityDependencies) {
1133
+ for (const entity of entityNames) {
1134
+ if (config.entityDependencies[entity]) {
1135
+ relevantDependencies[entity] = config.entityDependencies[entity];
1136
+ }
1137
+ }
1138
+ }
1139
+ const resolver = new DependencyResolver(
1140
+ entityNames,
1141
+ relevantDependencies,
1142
+ !!entityFilter
1143
+ // Allow partial resolution when using --entities
1144
+ );
1145
+ const validationErrors = resolver.validateDependencies();
1146
+ if (validationErrors.length > 0) {
1147
+ if (entityFilter) {
1148
+ this.logger.warn("\n\u26A0\uFE0F Warning: Dependency validation issues:");
1149
+ validationErrors.forEach((error) => this.logger.warn(` - ${error}`));
1150
+ this.logger.warn("This may cause errors if the dependent data doesn't already exist.\n");
1151
+ } else {
1152
+ this.logger.error("Dependency validation errors:");
1153
+ validationErrors.forEach((error) => {
1154
+ this.logger.error(` - ${error}`);
1155
+ errors.push(error);
1156
+ });
1157
+ return {
1158
+ metrics: this.metrics.getMetrics(),
1159
+ success: false,
1160
+ errors
1161
+ };
1162
+ }
1163
+ }
1164
+ const waves = resolver.resolveExecutionOrder();
1165
+ this.totalWaves = waves.length;
1166
+ const startedPayload = {
1167
+ configPath,
1168
+ totalEntities: entityNames.length,
1169
+ entityNames,
1170
+ totalWaves: waves.length,
1171
+ startTime: this.startTime
1172
+ };
1173
+ this.safeEmit("started", startedPayload);
1174
+ this.startProgressInterval();
1175
+ await this.processEntitiesInWaves(
1176
+ mappingPaths,
1177
+ resolver,
1178
+ this.mapper,
1179
+ config,
1180
+ this.logger,
1181
+ signal
1182
+ );
1183
+ if (signal.aborted) {
1184
+ return this.handleCancellation(signal.reason);
1185
+ }
1186
+ this.metrics.finishProcessing();
1187
+ this.stopProgressInterval();
1188
+ const finalMetrics = this.metrics.getMetrics();
1189
+ const allSuccessful = finalMetrics.failedOperations === 0;
1190
+ const finishedPayload = {
1191
+ metrics: finalMetrics,
1192
+ durationMs: Date.now() - this.startTime,
1193
+ allSuccessful
1194
+ };
1195
+ this.safeEmit("finished", finishedPayload);
1196
+ return {
1197
+ metrics: finalMetrics,
1198
+ success: true
1199
+ };
1200
+ } catch (error) {
1201
+ this.stopProgressInterval();
1202
+ if (signal.aborted) {
1203
+ return this.handleCancellation(signal.reason);
1204
+ }
1205
+ const errorMessage = error instanceof Error ? error.message : String(error);
1206
+ this.logger.error(`Error: ${errorMessage}`);
1207
+ errors.push(errorMessage);
1208
+ const erroredPayload = {
1209
+ error: error instanceof Error ? error : new Error(String(error)),
1210
+ metrics: this.metrics.getMetrics(),
1211
+ currentEntity: this.currentEntity,
1212
+ elapsedMs: Date.now() - this.startTime
1213
+ };
1214
+ this.safeEmit("errored", erroredPayload);
1215
+ return {
1216
+ metrics: this.metrics.getMetrics(),
1217
+ success: false,
1218
+ errors
1219
+ };
1220
+ } finally {
1221
+ this.isProcessing = false;
1222
+ this.abortController = null;
1223
+ this.stopProgressInterval();
1224
+ }
1225
+ }
1226
+ /**
1227
+ * Ingest specific entities from a configuration directory
1228
+ * @param configPath Path to configuration directory
1229
+ * @param entities Array of entity names to process
1230
+ * @returns Promise with ingestion result
1231
+ */
1232
+ async ingestEntities(configPath, entities) {
1233
+ return this.ingest(configPath, { entities });
1234
+ }
1235
+ /**
1236
+ * Get current processing metrics
1237
+ * @returns Current metrics
1238
+ */
1239
+ getMetrics() {
1240
+ return this.metrics.getMetrics();
1241
+ }
1242
+ /**
1243
+ * Get a summary of the current metrics
1244
+ * @returns Formatted summary string
1245
+ */
1246
+ getMetricsSummary() {
1247
+ return this.metrics.generateSummary();
1248
+ }
1249
+ /**
1250
+ * Get the GraphQL client for advanced usage
1251
+ * @returns GraphQL client wrapper
1252
+ */
1253
+ getClient() {
1254
+ return this.client;
1255
+ }
1256
+ /**
1257
+ * Get the data mapper for advanced usage
1258
+ * @returns Data mapper
1259
+ */
1260
+ getMapper() {
1261
+ return this.mapper;
1262
+ }
1263
+ /**
1264
+ * Set the logger instance
1265
+ * @param logger Logger instance to use
1266
+ */
1267
+ setLogger(logger) {
1268
+ this.logger = logger;
1269
+ this.client = new GraphQLClientWrapper(this.endpoint, this.headers, this.metrics, logger);
1270
+ this.mapper = new DataMapper(
1271
+ this.client,
1272
+ process.cwd(),
1273
+ this.metrics,
1274
+ logger,
1275
+ this.formatOverride
1276
+ );
1277
+ }
1278
+ /**
1279
+ * Update headers for GraphQL requests
1280
+ * @param headers New headers to use
1281
+ */
1282
+ setHeaders(headers) {
1283
+ this.headers = headers;
1284
+ this.client = new GraphQLClientWrapper(this.endpoint, headers, this.metrics, this.logger);
1285
+ this.mapper = new DataMapper(
1286
+ this.client,
1287
+ process.cwd(),
1288
+ this.metrics,
1289
+ this.logger,
1290
+ this.formatOverride
1291
+ );
1292
+ }
1293
+ /**
1294
+ * Process entities in dependency-aware waves with abort support
1295
+ */
1296
+ async processEntitiesInWaves(mappingPaths, resolver, mapper, config, logger, signal) {
1297
+ const waves = resolver.resolveExecutionOrder();
1298
+ const pathMap = new Map(mappingPaths.map((path3) => [basename(path3, ".json"), path3]));
1299
+ logger.info(`Processing ${waves.length} dependency waves...`);
1300
+ for (const wave of waves) {
1301
+ if (signal?.aborted) {
1302
+ return;
1303
+ }
1304
+ this.currentWave = wave.wave;
1305
+ logger.info(`Wave ${wave.wave + 1}: Processing entities [${wave.entities.join(", ")}]`);
1306
+ const entityConcurrency = config.parallelProcessing.entityConcurrency;
1307
+ const chunks = this.chunkArray(wave.entities, entityConcurrency);
1308
+ for (const chunk of chunks) {
1309
+ if (signal?.aborted) {
1310
+ return;
1311
+ }
1312
+ const entityPromises = chunk.map(async (entityName) => {
1313
+ if (signal?.aborted) {
1314
+ return;
1315
+ }
1316
+ const configPath = pathMap.get(entityName);
1317
+ if (configPath) {
1318
+ this.currentEntity = entityName;
1319
+ try {
1320
+ const entityConfig = getEntityConfig(entityName, config, logger);
1321
+ const retryConfig = getRetryConfig(entityName, config);
1322
+ await mapper.processEntityWithEvents(configPath, entityConfig, retryConfig, signal, {
1323
+ onEntityStart: (payload) => this.safeEmit("entityStart", {
1324
+ ...payload,
1325
+ waveIndex: wave.wave
1326
+ }),
1327
+ onEntityComplete: (payload) => {
1328
+ this.entitiesCompleted++;
1329
+ this.safeEmit("entityComplete", payload);
1330
+ },
1331
+ onRowSuccess: this.eventOptions.emitRowEvents ? (payload) => this.safeEmit("rowSuccess", payload) : void 0,
1332
+ onRowFailure: this.eventOptions.emitRowEvents ? (payload) => this.safeEmit("rowFailure", payload) : void 0
1333
+ });
1334
+ } catch (error) {
1335
+ const message = error instanceof Error ? error.message : String(error);
1336
+ logger.warn(`Warning: Could not process ${configPath}: ${message}`);
1337
+ }
1338
+ }
1339
+ });
1340
+ await Promise.allSettled(entityPromises);
1341
+ }
1342
+ }
1343
+ }
1344
+ /**
1345
+ * Utility function to chunk array into smaller arrays
1346
+ */
1347
+ chunkArray(array, chunkSize) {
1348
+ if (chunkSize <= 0) return [array];
1349
+ const chunks = [];
1350
+ for (let i = 0; i < array.length; i += chunkSize) {
1351
+ chunks.push(array.slice(i, i + chunkSize));
1352
+ }
1353
+ return chunks;
1354
+ }
1355
+ };
1356
+ export {
1357
+ CsvReader,
1358
+ DEFAULT_CONFIG,
1359
+ DEFAULT_EVENT_OPTIONS,
1360
+ DEFAULT_PARALLEL_CONFIG,
1361
+ DEFAULT_RETRY_CONFIG,
1362
+ DataMapper,
1363
+ DataReader,
1364
+ DataReaderFactory,
1365
+ DependencyResolver,
1366
+ GQLIngest,
1367
+ GraphQLClientWrapper,
1368
+ JsonReader,
1369
+ JsonlReader,
1370
+ MetricsCollector,
1371
+ YamlReader,
1372
+ createConsoleLogger,
1373
+ createDefaultLogger,
1374
+ getEntityConfig,
1375
+ getRetryConfig,
1376
+ loadConfig,
1377
+ noopLogger
1378
+ };
1379
+ //# sourceMappingURL=index.js.map