@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.
- package/README.md +181 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +237 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1379 -0
- package/dist/index.js.map +7 -0
- package/dist/{config.d.ts → lib/config.d.ts} +3 -2
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/dependency-resolver.d.ts.map +1 -0
- package/dist/lib/events.d.ts +167 -0
- package/dist/lib/events.d.ts.map +1 -0
- package/dist/lib/gql-ingest.d.ts +155 -0
- package/dist/lib/gql-ingest.d.ts.map +1 -0
- package/dist/{graphql-client.d.ts → lib/graphql-client.d.ts} +8 -4
- package/dist/lib/graphql-client.d.ts.map +1 -0
- package/dist/lib/index.d.ts +8 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/logger.d.ts +27 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/mapper.d.ts +49 -0
- package/dist/lib/mapper.d.ts.map +1 -0
- package/dist/{metrics.d.ts → lib/metrics.d.ts} +22 -8
- package/dist/lib/metrics.d.ts.map +1 -0
- package/dist/readers/csv.d.ts +0 -4
- package/dist/readers/csv.d.ts.map +1 -1
- package/dist/readers/index.d.ts +1 -1
- package/dist/readers/index.d.ts.map +1 -1
- package/package.json +17 -7
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -237
- package/dist/config.d.ts.map +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/dependency-resolver.d.ts.map +0 -1
- package/dist/dependency-resolver.test.d.ts +0 -2
- package/dist/dependency-resolver.test.d.ts.map +0 -1
- package/dist/graphql-client.d.ts.map +0 -1
- package/dist/graphql-client.test.d.ts +0 -2
- package/dist/graphql-client.test.d.ts.map +0 -1
- package/dist/mapper.d.ts +0 -31
- package/dist/mapper.d.ts.map +0 -1
- package/dist/mapper.test.d.ts +0 -2
- package/dist/mapper.test.d.ts.map +0 -1
- package/dist/metrics.d.ts.map +0 -1
- package/dist/metrics.test.d.ts +0 -2
- package/dist/metrics.test.d.ts.map +0 -1
- package/dist/readers/csv.test.d.ts +0 -2
- package/dist/readers/csv.test.d.ts.map +0 -1
- package/dist/readers/data-reader.test.d.ts +0 -2
- package/dist/readers/data-reader.test.d.ts.map +0 -1
- package/dist/readers/json.test.d.ts +0 -2
- package/dist/readers/json.test.d.ts.map +0 -1
- package/dist/readers/jsonl.test.d.ts +0 -2
- package/dist/readers/jsonl.test.d.ts.map +0 -1
- package/dist/readers/yaml.test.d.ts +0 -2
- package/dist/readers/yaml.test.d.ts.map +0 -1
- /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
|