@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.
- package/LICENSE +21 -0
- package/README.md +24 -0
- package/bin/cli.js +213 -35
- package/dist/dependency-resolver.d.ts +2 -1
- package/dist/dependency-resolver.d.ts.map +1 -1
- package/dist/mapper.d.ts +4 -1
- package/dist/mapper.d.ts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +43 -7
- package/src/dependency-resolver.test.ts +15 -1
- package/src/dependency-resolver.ts +6 -2
- package/src/mapper.test.ts +221 -0
- package/src/mapper.ts +174 -13
- package/src/metrics.ts +18 -10
|
@@ -8,7 +8,8 @@ export interface ExecutionWave {
|
|
|
8
8
|
export declare class DependencyResolver {
|
|
9
9
|
private dependencies;
|
|
10
10
|
private entities;
|
|
11
|
-
|
|
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;
|
|
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
|
package/dist/mapper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mapper.d.ts","sourceRoot":"","sources":["../src/mapper.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/metrics.d.ts.map
CHANGED
|
@@ -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;
|
|
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
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(
|
|
78
|
+
const mappingPaths = mapper.discoverMappings(
|
|
79
|
+
options.config,
|
|
80
|
+
entityFilter
|
|
81
|
+
);
|
|
70
82
|
|
|
71
83
|
if (mappingPaths.length === 0) {
|
|
72
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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) =>
|
|
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);
|
package/src/mapper.test.ts
CHANGED
|
@@ -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
|
});
|