@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +162 -6
  3. package/bin/cli.js +55 -52
  4. package/dist/dependency-resolver.d.ts +2 -1
  5. package/dist/dependency-resolver.d.ts.map +1 -1
  6. package/dist/mapper.d.ts +10 -5
  7. package/dist/mapper.d.ts.map +1 -1
  8. package/dist/metrics.d.ts.map +1 -1
  9. package/dist/readers/csv.d.ts +10 -0
  10. package/dist/readers/csv.d.ts.map +1 -0
  11. package/dist/readers/csv.test.d.ts +2 -0
  12. package/dist/readers/csv.test.d.ts.map +1 -0
  13. package/dist/readers/data-reader.d.ts +21 -0
  14. package/dist/readers/data-reader.d.ts.map +1 -0
  15. package/dist/readers/data-reader.test.d.ts +2 -0
  16. package/dist/readers/data-reader.test.d.ts.map +1 -0
  17. package/dist/readers/index.d.ts +6 -0
  18. package/dist/readers/index.d.ts.map +1 -0
  19. package/dist/readers/json.d.ts +6 -0
  20. package/dist/readers/json.d.ts.map +1 -0
  21. package/dist/readers/json.test.d.ts +2 -0
  22. package/dist/readers/json.test.d.ts.map +1 -0
  23. package/dist/readers/jsonl.d.ts +6 -0
  24. package/dist/readers/jsonl.d.ts.map +1 -0
  25. package/dist/readers/jsonl.test.d.ts +2 -0
  26. package/dist/readers/jsonl.test.d.ts.map +1 -0
  27. package/dist/readers/yaml.d.ts +6 -0
  28. package/dist/readers/yaml.d.ts.map +1 -0
  29. package/dist/readers/yaml.test.d.ts +2 -0
  30. package/dist/readers/yaml.test.d.ts.map +1 -0
  31. package/package.json +1 -1
  32. package/src/cli.ts +49 -8
  33. package/src/dependency-resolver.test.ts +15 -1
  34. package/src/dependency-resolver.ts +6 -2
  35. package/src/graphql-client.test.ts +19 -4
  36. package/src/mapper.test.ts +115 -64
  37. package/src/mapper.ts +176 -32
  38. package/src/metrics.ts +18 -10
  39. package/src/{csv-reader.test.ts → readers/csv.test.ts} +1 -1
  40. package/src/readers/csv.ts +29 -0
  41. package/src/readers/data-reader.test.ts +104 -0
  42. package/src/readers/data-reader.ts +61 -0
  43. package/src/readers/index.ts +18 -0
  44. package/src/readers/json.test.ts +80 -0
  45. package/src/readers/json.ts +27 -0
  46. package/src/readers/jsonl.test.ts +96 -0
  47. package/src/readers/jsonl.ts +28 -0
  48. package/src/readers/yaml.test.ts +95 -0
  49. package/src/readers/yaml.ts +28 -0
  50. package/dist/csv-reader.d.ts +0 -5
  51. package/dist/csv-reader.d.ts.map +0 -1
  52. package/dist/csv-reader.test.d.ts +0 -2
  53. package/dist/csv-reader.test.d.ts.map +0 -1
  54. package/src/csv-reader.ts +0 -18
@@ -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("./csv-reader");
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 { readCsvFile } = require("./csv-reader");
118
- readCsvFile.mockResolvedValue(mockCsvData);
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(readCsvFile).toHaveBeenCalledWith(
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(mockMutation, {
142
- name: "John",
143
- email: "john@example.com",
144
- }, undefined);
145
- expect(mockClient.executeMutation).toHaveBeenCalledWith(mockMutation, {
146
- name: "Jane",
147
- email: "jane@example.com",
148
- }, undefined);
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 { readCsvFile } = require("./csv-reader");
169
- readCsvFile.mockResolvedValue(mockCsvData);
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 { readCsvFile } = require("./csv-reader");
214
- readCsvFile.mockResolvedValue(mockCsvData);
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(mockMutation, {
223
- name: "Widget",
224
- price: "19.99",
225
- sku: "W001",
226
- }, undefined);
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 { readCsvFile } = require("./csv-reader");
252
- readCsvFile.mockResolvedValue(mockCsvData);
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(mockMutation, {
261
- name: "John",
262
- email: "john@example.com",
263
- }, undefined);
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 { readCsvFile } = require("./csv-reader");
282
- readCsvFile.mockResolvedValue(mockCsvData);
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 { readCsvFile } = require("./csv-reader");
313
- readCsvFile.mockResolvedValue(mockCsvData);
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 { readCsvFile } = require("./csv-reader");
369
- readCsvFile.mockResolvedValue(mockCsvData);
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(mockMutation, {
378
- name: "Widget",
379
- price: 19.99,
380
- quantity: 10,
381
- active: true,
382
- }, undefined);
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 { readCsvFile } = require("./csv-reader");
417
- readCsvFile.mockResolvedValue(mockCsvData);
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(mockMutation, {
428
- name: "Widget",
429
- price: "invalid_price",
430
- quantity: "invalid_quantity",
431
- }, undefined);
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 { readCsvFile } = require("./csv-reader");
477
- readCsvFile.mockResolvedValue(mockCsvData);
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(mockMutation, {
489
- int_field: "1.5",
490
- float_field: "Infinity",
491
- }, undefined);
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(mockMutation, {
494
- int_field: "not_a_number",
495
- float_field: "1.2.3",
496
- }, undefined);
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 { readCsvFile } = require("./csv-reader");
531
- readCsvFile.mockResolvedValue(mockCsvData);
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(mockClient, testBasePath, mockMetrics, true);
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(mockMutation, {
545
- name: "Widget",
546
- custom_field: "123",
547
- }, undefined);
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 { readCsvFile, CsvRow } from "./csv-reader";
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
- csvFile: string;
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
- const jsonFiles = files.filter((file) => file.endsWith(".json")).sort(); // Alphabetical order for consistent processing
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
- // Read CSV data (relative to config directory)
70
- const csvPath = path.resolve(configDir, config.csvFile);
71
- const csvData = await readCsvFile(csvPath);
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
- csvData,
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
- csvData,
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
- csvData: CsvRow[],
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 = csvData.length;
152
+ const totalRows = data.length;
108
153
  const variableTypes = this.extractVariableTypes(mutation);
109
154
 
110
- for (let i = 0; i < csvData.length; i++) {
111
- const row = csvData[i];
112
- const variables = this.mapCsvRowToVariables(row, mapping, variableTypes);
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
- csvData: CsvRow[],
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 ${csvData.length} rows with concurrency: ${concurrency}`
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(csvData, concurrency);
202
+ const chunks = this.chunkArray(data, concurrency);
158
203
  let processedCount = 0;
159
- const totalRows = csvData.length;
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.mapCsvRowToVariables(row, mapping, variableTypes);
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 mapCsvRowToVariables(
227
- row: CsvRow,
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, csvColumn] of Object.entries(mapping)) {
234
- if (row[csvColumn] !== undefined) {
235
- const rawValue = row[csvColumn];
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(value: string, type: string | undefined, varName: string): any {
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 string
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 (isNaN(intValue) || !isFinite(intValue) || !Number.isInteger(intValue)) {
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
  );