@jackchuka/gql-ingest 1.3.0 → 1.5.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.
@@ -8,7 +8,8 @@ export interface ExecutionWave {
8
8
  export declare class DependencyResolver {
9
9
  private dependencies;
10
10
  private entities;
11
- constructor(entities: string[], dependencies?: DependencyGraph);
11
+ private allowPartialResolution;
12
+ constructor(entities: string[], dependencies?: DependencyGraph, allowPartialResolution?: boolean);
12
13
  resolveExecutionOrder(): ExecutionWave[];
13
14
  validateDependencies(): string[];
14
15
  getDependents(entityName: string): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"dependency-resolver.d.ts","sourceRoot":"","sources":["../src/dependency-resolver.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAW;gBAEf,QAAQ,EAAE,MAAM,EAAE,EAAE,YAAY,GAAE,eAAoB;IAKlE,qBAAqB,IAAI,aAAa,EAAE;IA8CxC,oBAAoB,IAAI,MAAM,EAAE;IAwBhC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAM3C,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;CAG9C"}
1
+ {"version":3,"file":"dependency-resolver.d.ts","sourceRoot":"","sources":["../src/dependency-resolver.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,sBAAsB,CAAU;gBAE5B,QAAQ,EAAE,MAAM,EAAE,EAAE,YAAY,GAAE,eAAoB,EAAE,sBAAsB,GAAE,OAAe;IAM3G,qBAAqB,IAAI,aAAa,EAAE;IAgDxC,oBAAoB,IAAI,MAAM,EAAE;IAwBhC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAM3C,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;CAG9C"}
package/dist/mapper.d.ts CHANGED
@@ -12,12 +12,15 @@ export declare class DataMapper {
12
12
  private metrics;
13
13
  private verbose;
14
14
  constructor(client: GraphQLClientWrapper, basePath?: string, metrics?: MetricsCollector, verbose?: boolean);
15
- discoverMappings(configDir: string): string[];
15
+ discoverMappings(configDir: string, entityFilter?: string[]): string[];
16
16
  processEntity(configPath: string, parallelConfig?: ParallelProcessingConfig, retryConfig?: RetryConfig): Promise<void>;
17
17
  private processRowsSequentially;
18
18
  private processRowsConcurrently;
19
19
  private chunkArray;
20
20
  private mapCsvRowToVariables;
21
+ private extractVariableTypes;
22
+ private extractTypeName;
23
+ private convertValue;
21
24
  getMetrics(): MetricsCollector;
22
25
  }
23
26
  //# sourceMappingURL=mapper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.d.ts","sourceRoot":"","sources":["../src/mapper.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,OAAO,CAAU;gBAGvB,MAAM,EAAE,oBAAoB,EAC5B,QAAQ,GAAE,MAAsB,EAChC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,OAAO,GAAE,OAAe;IAQ1B,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE;IAiBvC,aAAa,CACjB,UAAU,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,wBAAwB,EACzC,WAAW,CAAC,EAAE,WAAW,GACxB,OAAO,CAAC,IAAI,CAAC;YA8CF,uBAAuB;YA+BvB,uBAAuB;IAmErC,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,oBAAoB;IAe5B,UAAU,IAAI,gBAAgB;CAG/B"}
1
+ {"version":3,"file":"mapper.d.ts","sourceRoot":"","sources":["../src/mapper.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,OAAO,CAAU;gBAGvB,MAAM,EAAE,oBAAoB,EAC5B,QAAQ,GAAE,MAAsB,EAChC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,OAAO,GAAE,OAAe;IAQ1B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;IA4ChE,aAAa,CACjB,UAAU,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,wBAAwB,EACzC,WAAW,CAAC,EAAE,WAAW,GACxB,OAAO,CAAC,IAAI,CAAC;YA8CF,uBAAuB;YAwCvB,uBAAuB;IAkFrC,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,oBAAoB;IAkB5B,OAAO,CAAC,oBAAoB;IA4B5B,OAAO,CAAC,eAAe;IAgBvB,OAAO,CAAC,YAAY;IA8DpB,UAAU,IAAI,gBAAgB;CAG/B"}
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC1C,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAoB;;IAgBnC,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAW/C,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IASvC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IASvC,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOhD,gBAAgB,IAAI,iBAAiB;IAKrC,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI/D,iBAAiB,IAAI,MAAM;IAI3B,cAAc,IAAI,MAAM;IAKxB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAI7C,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK1C,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK1C,yBAAyB,IAAI,MAAM;IAMnC,aAAa,IAAI,MAAM;IAKvB,eAAe,IAAI,MAAM;CAmC1B"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC1C,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAoB;;IAgBnC,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAW/C,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IASvC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IASvC,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOhD,gBAAgB,IAAI,iBAAiB;IAKrC,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI/D,iBAAiB,IAAI,MAAM;IAI3B,cAAc,IAAI,MAAM;IAKxB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAI7C,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK1C,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK1C,yBAAyB,IAAI,MAAM;IAMnC,aAAa,IAAI,MAAM;IAKvB,eAAe,IAAI,MAAM;CA2C1B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackchuka/gql-ingest",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "A CLI tool for ingesting data from CSV files into a GraphQL API",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
package/src/cli.ts CHANGED
@@ -31,6 +31,10 @@ program
31
31
  "-c, --config <path>",
32
32
  "Path to configuration directory (containing data/, graphql/, mappings/ subdirectories)"
33
33
  )
34
+ .option(
35
+ "-n, --entities <entities>",
36
+ "Comma-separated list of specific entities to process (e.g., users,products)"
37
+ )
34
38
  .option(
35
39
  "-h, --headers <headers>",
36
40
  "JSON string of headers to include in requests"
@@ -65,29 +69,61 @@ program
65
69
  options.verbose
66
70
  );
67
71
 
72
+ // Parse entities filter if provided
73
+ const entityFilter = options.entities
74
+ ? options.entities.split(",").map((e: string) => e.trim())
75
+ : undefined;
76
+
68
77
  // Discover all mapping files dynamically
69
- const mappingPaths = mapper.discoverMappings(options.config);
78
+ const mappingPaths = mapper.discoverMappings(
79
+ options.config,
80
+ entityFilter
81
+ );
70
82
 
71
83
  if (mappingPaths.length === 0) {
72
- console.warn(`No mapping files found in ${options.config}/mappings`);
84
+ const filterMsg = entityFilter
85
+ ? ` matching entities: ${entityFilter.join(", ")}`
86
+ : "";
87
+ console.warn(
88
+ `No mapping files found in ${options.config}/mappings${filterMsg}`
89
+ );
73
90
  return;
74
91
  }
75
92
 
76
93
  // Extract entity names from mapping paths
77
94
  const entityNames = mappingPaths.map((path) => basename(path, ".json"));
78
95
 
79
- // Setup dependency resolver
96
+ // Filter dependencies to only include those relevant to selected entities
97
+ const relevantDependencies: Record<string, string[]> = {};
98
+ if (config.entityDependencies) {
99
+ for (const entity of entityNames) {
100
+ if (config.entityDependencies[entity]) {
101
+ relevantDependencies[entity] = config.entityDependencies[entity];
102
+ }
103
+ }
104
+ }
105
+
106
+ // Setup dependency resolver with filtered dependencies
80
107
  const resolver = new DependencyResolver(
81
108
  entityNames,
82
- config.entityDependencies
109
+ relevantDependencies,
110
+ !!entityFilter // Allow partial resolution when using --entities
83
111
  );
84
112
 
85
113
  // Validate dependencies
86
114
  const validationErrors = resolver.validateDependencies();
87
115
  if (validationErrors.length > 0) {
88
- console.error("Dependency validation errors:");
89
- validationErrors.forEach((error) => console.error(` - ${error}`));
90
- process.exit(1);
116
+ if (entityFilter) {
117
+ // When using --entities flag, show warnings instead of errors
118
+ console.warn("\n⚠️ Warning: Dependency validation issues:");
119
+ validationErrors.forEach((error) => console.warn(` - ${error}`));
120
+ console.warn("This may cause errors if the dependent data doesn't already exist.\n");
121
+ } else {
122
+ // Strict validation when processing all entities
123
+ console.error("Dependency validation errors:");
124
+ validationErrors.forEach((error) => console.error(` - ${error}`));
125
+ process.exit(1);
126
+ }
91
127
  }
92
128
 
93
129
  // Process entities in dependency-aware waves
@@ -88,12 +88,26 @@ describe("DependencyResolver", () => {
88
88
  a: ["missing"],
89
89
  b: ["a"],
90
90
  };
91
- const resolver = new DependencyResolver(entities, dependencies);
91
+ const resolver = new DependencyResolver(entities, dependencies, false);
92
92
 
93
93
  expect(() => resolver.resolveExecutionOrder()).toThrow(
94
94
  "Circular dependency detected or missing dependencies for entities: a, b"
95
95
  );
96
96
  });
97
+
98
+ it("should allow entities with dependencies not in the entity list when partial resolution is enabled", () => {
99
+ const entities = ["a", "b"];
100
+ const dependencies = {
101
+ a: ["missing"],
102
+ b: ["a"],
103
+ };
104
+ const resolver = new DependencyResolver(entities, dependencies, true);
105
+
106
+ const waves = resolver.resolveExecutionOrder();
107
+ expect(waves).toHaveLength(2);
108
+ expect(waves[0].entities).toEqual(["a"]);
109
+ expect(waves[1].entities).toEqual(["b"]);
110
+ });
97
111
  });
98
112
 
99
113
  describe("validateDependencies", () => {
@@ -10,10 +10,12 @@ export interface ExecutionWave {
10
10
  export class DependencyResolver {
11
11
  private dependencies: DependencyGraph;
12
12
  private entities: string[];
13
+ private allowPartialResolution: boolean;
13
14
 
14
- constructor(entities: string[], dependencies: DependencyGraph = {}) {
15
+ constructor(entities: string[], dependencies: DependencyGraph = {}, allowPartialResolution: boolean = false) {
15
16
  this.entities = entities;
16
17
  this.dependencies = dependencies;
18
+ this.allowPartialResolution = allowPartialResolution;
17
19
  }
18
20
 
19
21
  resolveExecutionOrder(): ExecutionWave[] {
@@ -31,7 +33,9 @@ export class DependencyResolver {
31
33
  }
32
34
 
33
35
  const deps = this.dependencies[entity] || [];
34
- const canProcess = deps.every((dep) => processed.has(dep));
36
+ const canProcess = deps.every((dep) =>
37
+ processed.has(dep) || (this.allowPartialResolution && !this.entities.includes(dep))
38
+ );
35
39
 
36
40
  if (canProcess) {
37
41
  currentWave.push(entity);
@@ -331,5 +331,226 @@ describe("DataMapper", () => {
331
331
  const metrics = dataMapper.getMetrics();
332
332
  expect(metrics).toBe(mockMetrics);
333
333
  });
334
+
335
+ it("should convert numeric types from CSV strings to proper GraphQL types", async () => {
336
+ const mockConfig = {
337
+ csvFile: "data/products.csv",
338
+ graphqlFile: "graphql/products.graphql",
339
+ mapping: {
340
+ name: "product_name",
341
+ price: "product_price",
342
+ quantity: "product_quantity",
343
+ active: "product_active",
344
+ },
345
+ };
346
+
347
+ const mockCsvData = [
348
+ {
349
+ product_name: "Widget",
350
+ product_price: "19.99",
351
+ product_quantity: "10",
352
+ product_active: "true",
353
+ },
354
+ ];
355
+
356
+ const mockMutation = `
357
+ mutation CreateProduct($name: String!, $price: Float!, $quantity: Int!, $active: Boolean!) {
358
+ createProduct(input: { name: $name, price: $price, quantity: $quantity, active: $active }) {
359
+ id
360
+ }
361
+ }
362
+ `;
363
+
364
+ mockFs.readFileSync
365
+ .mockReturnValueOnce(JSON.stringify(mockConfig))
366
+ .mockReturnValueOnce(mockMutation);
367
+
368
+ const { readCsvFile } = require("./csv-reader");
369
+ readCsvFile.mockResolvedValue(mockCsvData);
370
+
371
+ mockClient.executeMutation.mockResolvedValue({
372
+ createProduct: { id: "123" },
373
+ });
374
+
375
+ await dataMapper.processEntity("configs/test/mappings/products.json");
376
+
377
+ expect(mockClient.executeMutation).toHaveBeenCalledWith(mockMutation, {
378
+ name: "Widget",
379
+ price: 19.99,
380
+ quantity: 10,
381
+ active: true,
382
+ }, undefined);
383
+ });
384
+
385
+ it("should handle invalid numeric conversions gracefully", async () => {
386
+ const mockConfig = {
387
+ csvFile: "data/products.csv",
388
+ graphqlFile: "graphql/products.graphql",
389
+ mapping: {
390
+ name: "product_name",
391
+ price: "product_price",
392
+ quantity: "product_quantity",
393
+ },
394
+ };
395
+
396
+ const mockCsvData = [
397
+ {
398
+ product_name: "Widget",
399
+ product_price: "invalid_price",
400
+ product_quantity: "invalid_quantity",
401
+ },
402
+ ];
403
+
404
+ const mockMutation = `
405
+ mutation CreateProduct($name: String!, $price: Float!, $quantity: Int!) {
406
+ createProduct(input: { name: $name, price: $price, quantity: $quantity }) {
407
+ id
408
+ }
409
+ }
410
+ `;
411
+
412
+ mockFs.readFileSync
413
+ .mockReturnValueOnce(JSON.stringify(mockConfig))
414
+ .mockReturnValueOnce(mockMutation);
415
+
416
+ const { readCsvFile } = require("./csv-reader");
417
+ readCsvFile.mockResolvedValue(mockCsvData);
418
+
419
+ mockClient.executeMutation.mockResolvedValue({
420
+ createProduct: { id: "123" },
421
+ });
422
+
423
+ const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
424
+
425
+ await dataMapper.processEntity("configs/test/mappings/products.json");
426
+
427
+ expect(mockClient.executeMutation).toHaveBeenCalledWith(mockMutation, {
428
+ name: "Widget",
429
+ price: "invalid_price",
430
+ quantity: "invalid_quantity",
431
+ }, undefined);
432
+
433
+ expect(consoleSpy).toHaveBeenCalledWith(
434
+ 'Warning: Cannot convert "invalid_price" to Float for variable $price. Expected a valid number. Using original value.'
435
+ );
436
+ expect(consoleSpy).toHaveBeenCalledWith(
437
+ 'Warning: Cannot convert "invalid_quantity" to Int for variable $quantity. Expected a valid integer. Using original value.'
438
+ );
439
+
440
+ consoleSpy.mockRestore();
441
+ });
442
+
443
+ it("should handle edge cases in numeric conversion safely", async () => {
444
+ const mockConfig = {
445
+ csvFile: "data/products.csv",
446
+ graphqlFile: "graphql/products.graphql",
447
+ mapping: {
448
+ int_field: "int_value",
449
+ float_field: "float_value",
450
+ },
451
+ };
452
+
453
+ const mockCsvData = [
454
+ {
455
+ int_value: "1.5", // Float in Int field - should remain string
456
+ float_value: "Infinity", // Invalid float - should remain string
457
+ },
458
+ {
459
+ int_value: "not_a_number", // Invalid int - should remain string
460
+ float_value: "1.2.3", // Invalid number format - should remain string
461
+ },
462
+ ];
463
+
464
+ const mockMutation = `
465
+ mutation CreateProduct($int_field: Int!, $float_field: Float!) {
466
+ createProduct(input: { int_field: $int_field, float_field: $float_field }) {
467
+ id
468
+ }
469
+ }
470
+ `;
471
+
472
+ mockFs.readFileSync
473
+ .mockReturnValueOnce(JSON.stringify(mockConfig))
474
+ .mockReturnValueOnce(mockMutation);
475
+
476
+ const { readCsvFile } = require("./csv-reader");
477
+ readCsvFile.mockResolvedValue(mockCsvData);
478
+
479
+ mockClient.executeMutation.mockResolvedValue({
480
+ createProduct: { id: "123" },
481
+ });
482
+
483
+ const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
484
+
485
+ await dataMapper.processEntity("configs/test/mappings/products.json");
486
+
487
+ // Should keep invalid values as strings
488
+ expect(mockClient.executeMutation).toHaveBeenCalledWith(mockMutation, {
489
+ int_field: "1.5",
490
+ float_field: "Infinity",
491
+ }, undefined);
492
+
493
+ expect(mockClient.executeMutation).toHaveBeenCalledWith(mockMutation, {
494
+ int_field: "not_a_number",
495
+ float_field: "1.2.3",
496
+ }, undefined);
497
+
498
+ consoleSpy.mockRestore();
499
+ });
500
+
501
+ it("should keep unknown scalar types as strings", async () => {
502
+ const mockConfig = {
503
+ csvFile: "data/products.csv",
504
+ graphqlFile: "graphql/products.graphql",
505
+ mapping: {
506
+ name: "product_name",
507
+ custom_field: "custom_value",
508
+ },
509
+ };
510
+
511
+ const mockCsvData = [
512
+ {
513
+ product_name: "Widget",
514
+ custom_value: "123",
515
+ },
516
+ ];
517
+
518
+ const mockMutation = `
519
+ mutation CreateProduct($name: String!, $custom_field: CustomScalar!) {
520
+ createProduct(input: { name: $name, custom_field: $custom_field }) {
521
+ id
522
+ }
523
+ }
524
+ `;
525
+
526
+ mockFs.readFileSync
527
+ .mockReturnValueOnce(JSON.stringify(mockConfig))
528
+ .mockReturnValueOnce(mockMutation);
529
+
530
+ const { readCsvFile } = require("./csv-reader");
531
+ readCsvFile.mockResolvedValue(mockCsvData);
532
+
533
+ mockClient.executeMutation.mockResolvedValue({
534
+ createProduct: { id: "123" },
535
+ });
536
+
537
+ // Create verbose mapper to test the logging
538
+ const verboseMapper = new DataMapper(mockClient, testBasePath, mockMetrics, true);
539
+ const consoleSpy = jest.spyOn(console, "log").mockImplementation();
540
+
541
+ await verboseMapper.processEntity("configs/test/mappings/products.json");
542
+
543
+ // Should keep custom scalar as string
544
+ expect(mockClient.executeMutation).toHaveBeenCalledWith(mockMutation, {
545
+ name: "Widget",
546
+ custom_field: "123",
547
+ }, undefined);
548
+
549
+ expect(consoleSpy).toHaveBeenCalledWith(
550
+ 'Unknown GraphQL type "CustomScalar" for variable $custom_field. Keeping value as string.'
551
+ );
552
+
553
+ consoleSpy.mockRestore();
554
+ });
334
555
  });
335
556
  });