@jackchuka/gql-ingest 1.4.0 → 2.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/LICENSE +21 -0
- package/README.md +162 -6
- package/bin/cli.js +55 -52
- package/dist/dependency-resolver.d.ts +2 -1
- package/dist/dependency-resolver.d.ts.map +1 -1
- package/dist/mapper.d.ts +10 -5
- package/dist/mapper.d.ts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/readers/csv.d.ts +10 -0
- package/dist/readers/csv.d.ts.map +1 -0
- package/dist/readers/csv.test.d.ts +2 -0
- package/dist/readers/csv.test.d.ts.map +1 -0
- package/dist/readers/data-reader.d.ts +21 -0
- package/dist/readers/data-reader.d.ts.map +1 -0
- package/dist/readers/data-reader.test.d.ts +2 -0
- package/dist/readers/data-reader.test.d.ts.map +1 -0
- package/dist/readers/index.d.ts +6 -0
- package/dist/readers/index.d.ts.map +1 -0
- package/dist/readers/json.d.ts +6 -0
- package/dist/readers/json.d.ts.map +1 -0
- package/dist/readers/json.test.d.ts +2 -0
- package/dist/readers/json.test.d.ts.map +1 -0
- package/dist/readers/jsonl.d.ts +6 -0
- package/dist/readers/jsonl.d.ts.map +1 -0
- package/dist/readers/jsonl.test.d.ts +2 -0
- package/dist/readers/jsonl.test.d.ts.map +1 -0
- package/dist/readers/yaml.d.ts +6 -0
- package/dist/readers/yaml.d.ts.map +1 -0
- package/dist/readers/yaml.test.d.ts +2 -0
- package/dist/readers/yaml.test.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +49 -8
- package/src/dependency-resolver.test.ts +15 -1
- package/src/dependency-resolver.ts +6 -2
- package/src/graphql-client.test.ts +19 -4
- package/src/mapper.test.ts +115 -64
- package/src/mapper.ts +176 -32
- package/src/metrics.ts +18 -10
- package/src/{csv-reader.test.ts → readers/csv.test.ts} +1 -1
- package/src/readers/csv.ts +29 -0
- package/src/readers/data-reader.test.ts +104 -0
- package/src/readers/data-reader.ts +61 -0
- package/src/readers/index.ts +18 -0
- package/src/readers/json.test.ts +80 -0
- package/src/readers/json.ts +27 -0
- package/src/readers/jsonl.test.ts +96 -0
- package/src/readers/jsonl.ts +28 -0
- package/src/readers/yaml.test.ts +95 -0
- package/src/readers/yaml.ts +28 -0
- package/dist/csv-reader.d.ts +0 -5
- package/dist/csv-reader.d.ts.map +0 -1
- package/dist/csv-reader.test.d.ts +0 -2
- package/dist/csv-reader.test.d.ts.map +0 -1
- package/src/csv-reader.ts +0 -18
package/src/mapper.test.ts
CHANGED
|
@@ -3,9 +3,18 @@ import path from "path";
|
|
|
3
3
|
import { DataMapper } from "./mapper";
|
|
4
4
|
import { GraphQLClientWrapper } from "./graphql-client";
|
|
5
5
|
import { MetricsCollector } from "./metrics";
|
|
6
|
+
import { readCsvFile, DataReaderFactory } from "./readers";
|
|
6
7
|
|
|
7
8
|
jest.mock("fs");
|
|
8
|
-
jest.mock("./
|
|
9
|
+
jest.mock("./readers", () => ({
|
|
10
|
+
...jest.requireActual("./readers"),
|
|
11
|
+
readCsvFile: jest.fn(),
|
|
12
|
+
DataReaderFactory: {
|
|
13
|
+
getReader: jest.fn().mockReturnValue({
|
|
14
|
+
readFile: jest.fn(),
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
9
18
|
|
|
10
19
|
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
11
20
|
|
|
@@ -114,8 +123,8 @@ describe("DataMapper", () => {
|
|
|
114
123
|
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
115
124
|
.mockReturnValueOnce(mockMutation);
|
|
116
125
|
|
|
117
|
-
const {
|
|
118
|
-
|
|
126
|
+
const { DataReaderFactory } = require("./readers");
|
|
127
|
+
DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
|
|
119
128
|
|
|
120
129
|
mockClient.executeMutation.mockResolvedValue({
|
|
121
130
|
createUser: { id: "123" },
|
|
@@ -129,8 +138,9 @@ describe("DataMapper", () => {
|
|
|
129
138
|
path.resolve(testBasePath, "configs/test/mappings/users.json"),
|
|
130
139
|
"utf8"
|
|
131
140
|
);
|
|
132
|
-
expect(
|
|
133
|
-
path.resolve(testBasePath, "configs/test", "data/users.csv")
|
|
141
|
+
expect(DataReaderFactory.getReader).toHaveBeenCalledWith(
|
|
142
|
+
path.resolve(testBasePath, "configs/test", "data/users.csv"),
|
|
143
|
+
undefined
|
|
134
144
|
);
|
|
135
145
|
expect(mockFs.readFileSync).toHaveBeenCalledWith(
|
|
136
146
|
path.resolve(testBasePath, "configs/test", "graphql/users.graphql"),
|
|
@@ -138,14 +148,22 @@ describe("DataMapper", () => {
|
|
|
138
148
|
);
|
|
139
149
|
|
|
140
150
|
expect(mockClient.executeMutation).toHaveBeenCalledTimes(2);
|
|
141
|
-
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
152
|
+
mockMutation,
|
|
153
|
+
{
|
|
154
|
+
name: "John",
|
|
155
|
+
email: "john@example.com",
|
|
156
|
+
},
|
|
157
|
+
undefined
|
|
158
|
+
);
|
|
159
|
+
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
160
|
+
mockMutation,
|
|
161
|
+
{
|
|
162
|
+
name: "Jane",
|
|
163
|
+
email: "jane@example.com",
|
|
164
|
+
},
|
|
165
|
+
undefined
|
|
166
|
+
);
|
|
149
167
|
|
|
150
168
|
consoleSpy.mockRestore();
|
|
151
169
|
});
|
|
@@ -165,8 +183,8 @@ describe("DataMapper", () => {
|
|
|
165
183
|
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
166
184
|
.mockReturnValueOnce(mockMutation);
|
|
167
185
|
|
|
168
|
-
const {
|
|
169
|
-
|
|
186
|
+
const { DataReaderFactory } = require("./readers");
|
|
187
|
+
DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
|
|
170
188
|
|
|
171
189
|
mockClient.executeMutation.mockRejectedValue(new Error("GraphQL error"));
|
|
172
190
|
|
|
@@ -210,8 +228,8 @@ describe("DataMapper", () => {
|
|
|
210
228
|
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
211
229
|
.mockReturnValueOnce(mockMutation);
|
|
212
230
|
|
|
213
|
-
const {
|
|
214
|
-
|
|
231
|
+
const { DataReaderFactory } = require("./readers");
|
|
232
|
+
DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
|
|
215
233
|
|
|
216
234
|
mockClient.executeMutation.mockResolvedValue({
|
|
217
235
|
createProduct: { id: "456" },
|
|
@@ -219,11 +237,15 @@ describe("DataMapper", () => {
|
|
|
219
237
|
|
|
220
238
|
await dataMapper.processEntity("configs/test/mappings/products.json");
|
|
221
239
|
|
|
222
|
-
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
241
|
+
mockMutation,
|
|
242
|
+
{
|
|
243
|
+
name: "Widget",
|
|
244
|
+
price: "19.99",
|
|
245
|
+
sku: "W001",
|
|
246
|
+
},
|
|
247
|
+
undefined
|
|
248
|
+
);
|
|
227
249
|
});
|
|
228
250
|
|
|
229
251
|
it("should handle missing CSV columns gracefully", async () => {
|
|
@@ -248,8 +270,8 @@ describe("DataMapper", () => {
|
|
|
248
270
|
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
249
271
|
.mockReturnValueOnce(mockMutation);
|
|
250
272
|
|
|
251
|
-
const {
|
|
252
|
-
|
|
273
|
+
const { DataReaderFactory } = require("./readers");
|
|
274
|
+
DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
|
|
253
275
|
|
|
254
276
|
mockClient.executeMutation.mockResolvedValue({
|
|
255
277
|
createUser: { id: "789" },
|
|
@@ -257,10 +279,14 @@ describe("DataMapper", () => {
|
|
|
257
279
|
|
|
258
280
|
await dataMapper.processEntity("configs/test/mappings/users.json");
|
|
259
281
|
|
|
260
|
-
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
282
|
+
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
283
|
+
mockMutation,
|
|
284
|
+
{
|
|
285
|
+
name: "John",
|
|
286
|
+
email: "john@example.com",
|
|
287
|
+
},
|
|
288
|
+
undefined
|
|
289
|
+
);
|
|
264
290
|
});
|
|
265
291
|
|
|
266
292
|
it("should call metrics methods during successful processing", async () => {
|
|
@@ -278,8 +304,8 @@ describe("DataMapper", () => {
|
|
|
278
304
|
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
279
305
|
.mockReturnValueOnce(mockMutation);
|
|
280
306
|
|
|
281
|
-
const {
|
|
282
|
-
|
|
307
|
+
const { DataReaderFactory } = require("./readers");
|
|
308
|
+
DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
|
|
283
309
|
|
|
284
310
|
mockClient.executeMutation.mockResolvedValue({
|
|
285
311
|
createUser: { id: "123" },
|
|
@@ -309,8 +335,8 @@ describe("DataMapper", () => {
|
|
|
309
335
|
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
310
336
|
.mockReturnValueOnce(mockMutation);
|
|
311
337
|
|
|
312
|
-
const {
|
|
313
|
-
|
|
338
|
+
const { DataReaderFactory } = require("./readers");
|
|
339
|
+
DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
|
|
314
340
|
|
|
315
341
|
mockClient.executeMutation.mockRejectedValue(new Error("GraphQL error"));
|
|
316
342
|
|
|
@@ -365,8 +391,8 @@ describe("DataMapper", () => {
|
|
|
365
391
|
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
366
392
|
.mockReturnValueOnce(mockMutation);
|
|
367
393
|
|
|
368
|
-
const {
|
|
369
|
-
|
|
394
|
+
const { DataReaderFactory } = require("./readers");
|
|
395
|
+
DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
|
|
370
396
|
|
|
371
397
|
mockClient.executeMutation.mockResolvedValue({
|
|
372
398
|
createProduct: { id: "123" },
|
|
@@ -374,12 +400,16 @@ describe("DataMapper", () => {
|
|
|
374
400
|
|
|
375
401
|
await dataMapper.processEntity("configs/test/mappings/products.json");
|
|
376
402
|
|
|
377
|
-
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
403
|
+
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
404
|
+
mockMutation,
|
|
405
|
+
{
|
|
406
|
+
name: "Widget",
|
|
407
|
+
price: 19.99,
|
|
408
|
+
quantity: 10,
|
|
409
|
+
active: true,
|
|
410
|
+
},
|
|
411
|
+
undefined
|
|
412
|
+
);
|
|
383
413
|
});
|
|
384
414
|
|
|
385
415
|
it("should handle invalid numeric conversions gracefully", async () => {
|
|
@@ -413,8 +443,8 @@ describe("DataMapper", () => {
|
|
|
413
443
|
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
414
444
|
.mockReturnValueOnce(mockMutation);
|
|
415
445
|
|
|
416
|
-
const {
|
|
417
|
-
|
|
446
|
+
const { DataReaderFactory } = require("./readers");
|
|
447
|
+
DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
|
|
418
448
|
|
|
419
449
|
mockClient.executeMutation.mockResolvedValue({
|
|
420
450
|
createProduct: { id: "123" },
|
|
@@ -424,11 +454,15 @@ describe("DataMapper", () => {
|
|
|
424
454
|
|
|
425
455
|
await dataMapper.processEntity("configs/test/mappings/products.json");
|
|
426
456
|
|
|
427
|
-
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
457
|
+
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
458
|
+
mockMutation,
|
|
459
|
+
{
|
|
460
|
+
name: "Widget",
|
|
461
|
+
price: "invalid_price",
|
|
462
|
+
quantity: "invalid_quantity",
|
|
463
|
+
},
|
|
464
|
+
undefined
|
|
465
|
+
);
|
|
432
466
|
|
|
433
467
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
434
468
|
'Warning: Cannot convert "invalid_price" to Float for variable $price. Expected a valid number. Using original value.'
|
|
@@ -473,8 +507,8 @@ describe("DataMapper", () => {
|
|
|
473
507
|
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
474
508
|
.mockReturnValueOnce(mockMutation);
|
|
475
509
|
|
|
476
|
-
const {
|
|
477
|
-
|
|
510
|
+
const { DataReaderFactory } = require("./readers");
|
|
511
|
+
DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
|
|
478
512
|
|
|
479
513
|
mockClient.executeMutation.mockResolvedValue({
|
|
480
514
|
createProduct: { id: "123" },
|
|
@@ -485,15 +519,23 @@ describe("DataMapper", () => {
|
|
|
485
519
|
await dataMapper.processEntity("configs/test/mappings/products.json");
|
|
486
520
|
|
|
487
521
|
// Should keep invalid values as strings
|
|
488
|
-
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
522
|
+
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
523
|
+
mockMutation,
|
|
524
|
+
{
|
|
525
|
+
int_field: "1.5",
|
|
526
|
+
float_field: "Infinity",
|
|
527
|
+
},
|
|
528
|
+
undefined
|
|
529
|
+
);
|
|
492
530
|
|
|
493
|
-
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
531
|
+
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
532
|
+
mockMutation,
|
|
533
|
+
{
|
|
534
|
+
int_field: "not_a_number",
|
|
535
|
+
float_field: "1.2.3",
|
|
536
|
+
},
|
|
537
|
+
undefined
|
|
538
|
+
);
|
|
497
539
|
|
|
498
540
|
consoleSpy.mockRestore();
|
|
499
541
|
});
|
|
@@ -527,24 +569,33 @@ describe("DataMapper", () => {
|
|
|
527
569
|
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
528
570
|
.mockReturnValueOnce(mockMutation);
|
|
529
571
|
|
|
530
|
-
const {
|
|
531
|
-
|
|
572
|
+
const { DataReaderFactory } = require("./readers");
|
|
573
|
+
DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
|
|
532
574
|
|
|
533
575
|
mockClient.executeMutation.mockResolvedValue({
|
|
534
576
|
createProduct: { id: "123" },
|
|
535
577
|
});
|
|
536
578
|
|
|
537
579
|
// Create verbose mapper to test the logging
|
|
538
|
-
const verboseMapper = new DataMapper(
|
|
580
|
+
const verboseMapper = new DataMapper(
|
|
581
|
+
mockClient,
|
|
582
|
+
testBasePath,
|
|
583
|
+
mockMetrics,
|
|
584
|
+
true
|
|
585
|
+
);
|
|
539
586
|
const consoleSpy = jest.spyOn(console, "log").mockImplementation();
|
|
540
587
|
|
|
541
588
|
await verboseMapper.processEntity("configs/test/mappings/products.json");
|
|
542
589
|
|
|
543
590
|
// Should keep custom scalar as string
|
|
544
|
-
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
591
|
+
expect(mockClient.executeMutation).toHaveBeenCalledWith(
|
|
592
|
+
mockMutation,
|
|
593
|
+
{
|
|
594
|
+
name: "Widget",
|
|
595
|
+
custom_field: "123",
|
|
596
|
+
},
|
|
597
|
+
undefined
|
|
598
|
+
);
|
|
548
599
|
|
|
549
600
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
550
601
|
'Unknown GraphQL type "CustomScalar" for variable $custom_field. Keeping value as string.'
|
package/src/mapper.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { parse, DocumentNode, VariableDefinitionNode } from "graphql";
|
|
4
|
-
import {
|
|
4
|
+
import { DataReaderFactory, DataRow } from "./readers";
|
|
5
5
|
import { GraphQLClientWrapper } from "./graphql-client";
|
|
6
6
|
import { MetricsCollector } from "./metrics";
|
|
7
7
|
import { ParallelProcessingConfig, RetryConfig } from "./config";
|
|
8
8
|
|
|
9
9
|
export interface MappingConfig {
|
|
10
|
-
|
|
10
|
+
// Legacy CSV support
|
|
11
|
+
csvFile?: string;
|
|
12
|
+
// New flexible data file support
|
|
13
|
+
dataFile?: string;
|
|
14
|
+
dataFormat?: string;
|
|
11
15
|
graphqlFile: string;
|
|
12
|
-
mapping: Record<string, string>;
|
|
16
|
+
mapping: Record<string, string | any>;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export class DataMapper {
|
|
@@ -17,25 +21,55 @@ export class DataMapper {
|
|
|
17
21
|
private basePath: string;
|
|
18
22
|
private metrics: MetricsCollector;
|
|
19
23
|
private verbose: boolean;
|
|
24
|
+
private formatOverride?: string;
|
|
20
25
|
|
|
21
26
|
constructor(
|
|
22
27
|
client: GraphQLClientWrapper,
|
|
23
28
|
basePath: string = process.cwd(),
|
|
24
29
|
metrics?: MetricsCollector,
|
|
25
|
-
verbose: boolean = false
|
|
30
|
+
verbose: boolean = false,
|
|
31
|
+
formatOverride?: string
|
|
26
32
|
) {
|
|
27
33
|
this.client = client;
|
|
28
34
|
this.basePath = basePath;
|
|
29
35
|
this.metrics = metrics || new MetricsCollector();
|
|
30
36
|
this.verbose = verbose;
|
|
37
|
+
this.formatOverride = formatOverride;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
discoverMappings(configDir: string): string[] {
|
|
40
|
+
discoverMappings(configDir: string, entityFilter?: string[]): string[] {
|
|
34
41
|
const mappingsPath = path.resolve(this.basePath, configDir, "mappings");
|
|
35
42
|
|
|
36
43
|
try {
|
|
37
44
|
const files = fs.readdirSync(mappingsPath);
|
|
38
|
-
|
|
45
|
+
let jsonFiles = files.filter((file) => file.endsWith(".json"));
|
|
46
|
+
|
|
47
|
+
// Apply entity filter if provided
|
|
48
|
+
if (entityFilter && entityFilter.length > 0) {
|
|
49
|
+
const requestedEntities = new Set(entityFilter);
|
|
50
|
+
const foundEntities = new Set<string>();
|
|
51
|
+
|
|
52
|
+
jsonFiles = jsonFiles.filter((file) => {
|
|
53
|
+
const entityName = path.basename(file, ".json");
|
|
54
|
+
if (requestedEntities.has(entityName)) {
|
|
55
|
+
foundEntities.add(entityName);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Check for requested entities that were not found
|
|
62
|
+
const notFound = entityFilter.filter((e) => !foundEntities.has(e));
|
|
63
|
+
if (notFound.length > 0) {
|
|
64
|
+
console.warn(
|
|
65
|
+
`Warning: The following entities were not found in mappings: ${notFound.join(
|
|
66
|
+
", "
|
|
67
|
+
)}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
jsonFiles.sort(); // Alphabetical order for consistent processing
|
|
39
73
|
|
|
40
74
|
console.log(
|
|
41
75
|
`Discovered ${jsonFiles.length} mapping files: ${jsonFiles.join(", ")}`
|
|
@@ -66,9 +100,20 @@ export class DataMapper {
|
|
|
66
100
|
// Extract config directory (parent of mappings directory)
|
|
67
101
|
const configDir = path.dirname(path.dirname(configFullPath));
|
|
68
102
|
|
|
69
|
-
//
|
|
70
|
-
const
|
|
71
|
-
|
|
103
|
+
// Determine data file path (support both legacy csvFile and new dataFile)
|
|
104
|
+
const dataFile = config.dataFile || config.csvFile;
|
|
105
|
+
if (!dataFile) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`No data file specified in mapping config: ${configPath}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const dataPath = path.resolve(configDir, dataFile);
|
|
112
|
+
|
|
113
|
+
// Get appropriate reader (prioritize CLI format override, then config format)
|
|
114
|
+
const format = this.formatOverride || config.dataFormat;
|
|
115
|
+
const reader = DataReaderFactory.getReader(dataPath, format);
|
|
116
|
+
const data = await reader.readFile(dataPath);
|
|
72
117
|
|
|
73
118
|
// Read GraphQL mutation (relative to config directory)
|
|
74
119
|
const graphqlPath = path.resolve(configDir, config.graphqlFile);
|
|
@@ -77,7 +122,7 @@ export class DataMapper {
|
|
|
77
122
|
// Process rows with optional parallelization
|
|
78
123
|
if (parallelConfig && parallelConfig.concurrency > 1) {
|
|
79
124
|
await this.processRowsConcurrently(
|
|
80
|
-
|
|
125
|
+
data,
|
|
81
126
|
mutation,
|
|
82
127
|
config.mapping,
|
|
83
128
|
entityName,
|
|
@@ -86,7 +131,7 @@ export class DataMapper {
|
|
|
86
131
|
);
|
|
87
132
|
} else {
|
|
88
133
|
await this.processRowsSequentially(
|
|
89
|
-
|
|
134
|
+
data,
|
|
90
135
|
mutation,
|
|
91
136
|
config.mapping,
|
|
92
137
|
entityName,
|
|
@@ -98,18 +143,18 @@ export class DataMapper {
|
|
|
98
143
|
}
|
|
99
144
|
|
|
100
145
|
private async processRowsSequentially(
|
|
101
|
-
|
|
146
|
+
data: DataRow[],
|
|
102
147
|
mutation: string,
|
|
103
|
-
mapping: Record<string, string>,
|
|
148
|
+
mapping: Record<string, string | any>,
|
|
104
149
|
entityName: string,
|
|
105
150
|
retryConfig?: RetryConfig
|
|
106
151
|
): Promise<void> {
|
|
107
|
-
const totalRows =
|
|
152
|
+
const totalRows = data.length;
|
|
108
153
|
const variableTypes = this.extractVariableTypes(mutation);
|
|
109
154
|
|
|
110
|
-
for (let i = 0; i <
|
|
111
|
-
const row =
|
|
112
|
-
const variables = this.
|
|
155
|
+
for (let i = 0; i < data.length; i++) {
|
|
156
|
+
const row = data[i];
|
|
157
|
+
const variables = this.mapRowToVariables(row, mapping, variableTypes);
|
|
113
158
|
|
|
114
159
|
try {
|
|
115
160
|
await this.client.executeMutation(mutation, variables, retryConfig);
|
|
@@ -138,30 +183,30 @@ export class DataMapper {
|
|
|
138
183
|
}
|
|
139
184
|
|
|
140
185
|
private async processRowsConcurrently(
|
|
141
|
-
|
|
186
|
+
data: DataRow[],
|
|
142
187
|
mutation: string,
|
|
143
|
-
mapping: Record<string, string>,
|
|
188
|
+
mapping: Record<string, string | any>,
|
|
144
189
|
entityName: string,
|
|
145
190
|
parallelConfig: ParallelProcessingConfig,
|
|
146
191
|
retryConfig?: RetryConfig
|
|
147
192
|
): Promise<void> {
|
|
148
193
|
const concurrency = parallelConfig.concurrency;
|
|
149
194
|
console.log(
|
|
150
|
-
`Processing ${
|
|
195
|
+
`Processing ${data.length} rows with concurrency: ${concurrency}`
|
|
151
196
|
);
|
|
152
197
|
|
|
153
198
|
// Extract variable types once for all rows
|
|
154
199
|
const variableTypes = this.extractVariableTypes(mutation);
|
|
155
200
|
|
|
156
201
|
// Split data into chunks for concurrent processing
|
|
157
|
-
const chunks = this.chunkArray(
|
|
202
|
+
const chunks = this.chunkArray(data, concurrency);
|
|
158
203
|
let processedCount = 0;
|
|
159
|
-
const totalRows =
|
|
204
|
+
const totalRows = data.length;
|
|
160
205
|
|
|
161
206
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
162
207
|
const chunk = chunks[chunkIndex];
|
|
163
208
|
const promises = chunk.map(async (row) => {
|
|
164
|
-
const variables = this.
|
|
209
|
+
const variables = this.mapRowToVariables(row, mapping, variableTypes);
|
|
165
210
|
|
|
166
211
|
try {
|
|
167
212
|
const result = await this.client.executeMutation(
|
|
@@ -223,24 +268,110 @@ export class DataMapper {
|
|
|
223
268
|
return chunks;
|
|
224
269
|
}
|
|
225
270
|
|
|
226
|
-
private
|
|
227
|
-
row:
|
|
228
|
-
mapping: Record<string, string>,
|
|
271
|
+
private mapRowToVariables(
|
|
272
|
+
row: DataRow,
|
|
273
|
+
mapping: Record<string, string | any>,
|
|
229
274
|
variableTypes: Record<string, string>
|
|
230
275
|
): Record<string, any> {
|
|
231
276
|
const variables: Record<string, any> = {};
|
|
232
277
|
|
|
233
|
-
for (const [graphqlVar,
|
|
234
|
-
|
|
235
|
-
|
|
278
|
+
for (const [graphqlVar, mappingValue] of Object.entries(mapping)) {
|
|
279
|
+
// Handle direct mapping for nested data (e.g., "input": "$")
|
|
280
|
+
if (mappingValue === "$") {
|
|
281
|
+
// Use the entire row as the variable value
|
|
282
|
+
variables[graphqlVar] = row;
|
|
283
|
+
}
|
|
284
|
+
// Handle path-based mapping for nested data (e.g., "input.name": "$.product.name")
|
|
285
|
+
else if (
|
|
286
|
+
typeof mappingValue === "string" &&
|
|
287
|
+
mappingValue.startsWith("$.")
|
|
288
|
+
) {
|
|
289
|
+
const path = mappingValue.substring(2); // Remove '$.'
|
|
290
|
+
const value = this.getValueByPath(row, path);
|
|
291
|
+
if (value !== undefined) {
|
|
292
|
+
const type = variableTypes[graphqlVar];
|
|
293
|
+
variables[graphqlVar] = this.convertValue(value, type, graphqlVar);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Handle traditional flat mapping (e.g., "name": "product_name")
|
|
297
|
+
else if (
|
|
298
|
+
typeof mappingValue === "string" &&
|
|
299
|
+
row[mappingValue] !== undefined
|
|
300
|
+
) {
|
|
301
|
+
const rawValue = row[mappingValue];
|
|
236
302
|
const type = variableTypes[graphqlVar];
|
|
237
303
|
variables[graphqlVar] = this.convertValue(rawValue, type, graphqlVar);
|
|
238
304
|
}
|
|
305
|
+
// Handle complex mapping object
|
|
306
|
+
else if (typeof mappingValue === "object" && mappingValue !== null) {
|
|
307
|
+
variables[graphqlVar] = this.mapNestedObject(
|
|
308
|
+
row,
|
|
309
|
+
mappingValue,
|
|
310
|
+
variableTypes
|
|
311
|
+
);
|
|
312
|
+
}
|
|
239
313
|
}
|
|
240
314
|
|
|
241
315
|
return variables;
|
|
242
316
|
}
|
|
243
317
|
|
|
318
|
+
private getValueByPath(obj: any, path: string): any {
|
|
319
|
+
const parts = path.split(".");
|
|
320
|
+
let current = obj;
|
|
321
|
+
|
|
322
|
+
for (const part of parts) {
|
|
323
|
+
if (current && typeof current === "object" && part in current) {
|
|
324
|
+
current = current[part];
|
|
325
|
+
} else {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return current;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private mapNestedObject(
|
|
334
|
+
row: DataRow,
|
|
335
|
+
mappingObj: any,
|
|
336
|
+
variableTypes: Record<string, string>
|
|
337
|
+
): any {
|
|
338
|
+
if (Array.isArray(mappingObj)) {
|
|
339
|
+
return mappingObj.map((item) =>
|
|
340
|
+
this.mapNestedObject(row, item, variableTypes)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (typeof mappingObj === "object" && mappingObj !== null) {
|
|
345
|
+
const result: any = {};
|
|
346
|
+
for (const [key, value] of Object.entries(mappingObj)) {
|
|
347
|
+
if (typeof value === "string" && value.startsWith("$.")) {
|
|
348
|
+
const path = value.substring(2);
|
|
349
|
+
let fieldValue = this.getValueByPath(row, path);
|
|
350
|
+
|
|
351
|
+
// Handle special case for array fields (e.g., comma-separated values)
|
|
352
|
+
if (
|
|
353
|
+
key === "values" &&
|
|
354
|
+
typeof fieldValue === "string" &&
|
|
355
|
+
fieldValue.includes(",")
|
|
356
|
+
) {
|
|
357
|
+
fieldValue = fieldValue.split(",").map((v) => v.trim());
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
result[key] = fieldValue;
|
|
361
|
+
} else if (typeof value === "string" && row[value] !== undefined) {
|
|
362
|
+
result[key] = row[value];
|
|
363
|
+
} else if (typeof value === "object") {
|
|
364
|
+
result[key] = this.mapNestedObject(row, value, variableTypes);
|
|
365
|
+
} else {
|
|
366
|
+
result[key] = value;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return mappingObj;
|
|
373
|
+
}
|
|
374
|
+
|
|
244
375
|
private extractVariableTypes(mutation: string): Record<string, string> {
|
|
245
376
|
const types: Record<string, string> = {};
|
|
246
377
|
|
|
@@ -285,9 +416,18 @@ export class DataMapper {
|
|
|
285
416
|
return null;
|
|
286
417
|
}
|
|
287
418
|
|
|
288
|
-
private convertValue(
|
|
419
|
+
private convertValue(
|
|
420
|
+
value: any,
|
|
421
|
+
type: string | undefined,
|
|
422
|
+
varName: string
|
|
423
|
+
): any {
|
|
289
424
|
if (!type) {
|
|
290
|
-
// No type information available, keep as
|
|
425
|
+
// No type information available, keep as is
|
|
426
|
+
return value;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// For non-string values (objects, arrays), return as is
|
|
430
|
+
if (typeof value !== "string") {
|
|
291
431
|
return value;
|
|
292
432
|
}
|
|
293
433
|
|
|
@@ -297,7 +437,11 @@ export class DataMapper {
|
|
|
297
437
|
case "Int":
|
|
298
438
|
const intValue = Number(trimmedValue);
|
|
299
439
|
// Validate that it's a valid integer (no decimals, NaN, or Infinity)
|
|
300
|
-
if (
|
|
440
|
+
if (
|
|
441
|
+
isNaN(intValue) ||
|
|
442
|
+
!isFinite(intValue) ||
|
|
443
|
+
!Number.isInteger(intValue)
|
|
444
|
+
) {
|
|
301
445
|
console.warn(
|
|
302
446
|
`Warning: Cannot convert "${value}" to Int for variable $${varName}. Expected a valid integer. Using original value.`
|
|
303
447
|
);
|