@jackchuka/gql-ingest 1.2.0 ā 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -31
- package/bin/cli.js +220 -41
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/graphql-client.d.ts +6 -2
- package/dist/graphql-client.d.ts.map +1 -1
- package/dist/mapper.d.ts +5 -2
- package/dist/mapper.d.ts.map +1 -1
- package/dist/metrics.d.ts +5 -0
- package/dist/metrics.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +6 -4
- package/src/config.test.ts +69 -1
- package/src/config.ts +35 -0
- package/src/graphql-client.test.ts +127 -1
- package/src/graphql-client.ts +132 -32
- package/src/mapper.test.ts +225 -4
- package/src/mapper.ts +145 -18
- package/src/metrics.ts +22 -0
package/src/mapper.test.ts
CHANGED
|
@@ -141,11 +141,11 @@ describe("DataMapper", () => {
|
|
|
141
141
|
expect(mockClient.executeMutation).toHaveBeenCalledWith(mockMutation, {
|
|
142
142
|
name: "John",
|
|
143
143
|
email: "john@example.com",
|
|
144
|
-
});
|
|
144
|
+
}, undefined);
|
|
145
145
|
expect(mockClient.executeMutation).toHaveBeenCalledWith(mockMutation, {
|
|
146
146
|
name: "Jane",
|
|
147
147
|
email: "jane@example.com",
|
|
148
|
-
});
|
|
148
|
+
}, undefined);
|
|
149
149
|
|
|
150
150
|
consoleSpy.mockRestore();
|
|
151
151
|
});
|
|
@@ -223,7 +223,7 @@ describe("DataMapper", () => {
|
|
|
223
223
|
name: "Widget",
|
|
224
224
|
price: "19.99",
|
|
225
225
|
sku: "W001",
|
|
226
|
-
});
|
|
226
|
+
}, undefined);
|
|
227
227
|
});
|
|
228
228
|
|
|
229
229
|
it("should handle missing CSV columns gracefully", async () => {
|
|
@@ -260,7 +260,7 @@ describe("DataMapper", () => {
|
|
|
260
260
|
expect(mockClient.executeMutation).toHaveBeenCalledWith(mockMutation, {
|
|
261
261
|
name: "John",
|
|
262
262
|
email: "john@example.com",
|
|
263
|
-
});
|
|
263
|
+
}, undefined);
|
|
264
264
|
});
|
|
265
265
|
|
|
266
266
|
it("should call metrics methods during successful processing", async () => {
|
|
@@ -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
|
});
|
package/src/mapper.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { parse, DocumentNode, VariableDefinitionNode } from "graphql";
|
|
3
4
|
import { readCsvFile, CsvRow } from "./csv-reader";
|
|
4
5
|
import { GraphQLClientWrapper } from "./graphql-client";
|
|
5
6
|
import { MetricsCollector } from "./metrics";
|
|
6
|
-
import { ParallelProcessingConfig } from "./config";
|
|
7
|
+
import { ParallelProcessingConfig, RetryConfig } from "./config";
|
|
7
8
|
|
|
8
9
|
export interface MappingConfig {
|
|
9
10
|
csvFile: string;
|
|
@@ -48,7 +49,8 @@ export class DataMapper {
|
|
|
48
49
|
|
|
49
50
|
async processEntity(
|
|
50
51
|
configPath: string,
|
|
51
|
-
parallelConfig?: ParallelProcessingConfig
|
|
52
|
+
parallelConfig?: ParallelProcessingConfig,
|
|
53
|
+
retryConfig?: RetryConfig
|
|
52
54
|
): Promise<void> {
|
|
53
55
|
const entityName = path.basename(configPath, ".json");
|
|
54
56
|
console.log(`Processing entity: ${configPath}`);
|
|
@@ -79,14 +81,16 @@ export class DataMapper {
|
|
|
79
81
|
mutation,
|
|
80
82
|
config.mapping,
|
|
81
83
|
entityName,
|
|
82
|
-
parallelConfig
|
|
84
|
+
parallelConfig,
|
|
85
|
+
retryConfig
|
|
83
86
|
);
|
|
84
87
|
} else {
|
|
85
88
|
await this.processRowsSequentially(
|
|
86
89
|
csvData,
|
|
87
90
|
mutation,
|
|
88
91
|
config.mapping,
|
|
89
|
-
entityName
|
|
92
|
+
entityName,
|
|
93
|
+
retryConfig
|
|
90
94
|
);
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -97,27 +101,37 @@ export class DataMapper {
|
|
|
97
101
|
csvData: CsvRow[],
|
|
98
102
|
mutation: string,
|
|
99
103
|
mapping: Record<string, string>,
|
|
100
|
-
entityName: string
|
|
104
|
+
entityName: string,
|
|
105
|
+
retryConfig?: RetryConfig
|
|
101
106
|
): Promise<void> {
|
|
102
107
|
const totalRows = csvData.length;
|
|
103
|
-
|
|
108
|
+
const variableTypes = this.extractVariableTypes(mutation);
|
|
109
|
+
|
|
104
110
|
for (let i = 0; i < csvData.length; i++) {
|
|
105
111
|
const row = csvData[i];
|
|
106
|
-
const variables = this.mapCsvRowToVariables(row, mapping);
|
|
112
|
+
const variables = this.mapCsvRowToVariables(row, mapping, variableTypes);
|
|
107
113
|
|
|
108
114
|
try {
|
|
109
|
-
await this.client.executeMutation(mutation, variables);
|
|
115
|
+
await this.client.executeMutation(mutation, variables, retryConfig);
|
|
110
116
|
this.metrics.recordSuccess(entityName);
|
|
111
|
-
|
|
117
|
+
|
|
112
118
|
// Show progress every 10% or at the end (only in non-verbose mode)
|
|
113
|
-
if (
|
|
119
|
+
if (
|
|
120
|
+
!this.verbose &&
|
|
121
|
+
((i + 1) % Math.max(1, Math.floor(totalRows / 10)) === 0 ||
|
|
122
|
+
i === totalRows - 1)
|
|
123
|
+
) {
|
|
114
124
|
const progress = (((i + 1) / totalRows) * 100).toFixed(1);
|
|
115
125
|
console.log(`š Progress: ${i + 1}/${totalRows} (${progress}%) ā`);
|
|
116
126
|
}
|
|
117
127
|
} catch (error) {
|
|
118
128
|
this.metrics.recordFailure(entityName);
|
|
119
129
|
if (!this.verbose) {
|
|
120
|
-
console.error(
|
|
130
|
+
console.error(
|
|
131
|
+
`ā Failed to create entity for row ${i + 1}:`,
|
|
132
|
+
row,
|
|
133
|
+
error
|
|
134
|
+
);
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
137
|
}
|
|
@@ -128,13 +142,17 @@ export class DataMapper {
|
|
|
128
142
|
mutation: string,
|
|
129
143
|
mapping: Record<string, string>,
|
|
130
144
|
entityName: string,
|
|
131
|
-
parallelConfig: ParallelProcessingConfig
|
|
145
|
+
parallelConfig: ParallelProcessingConfig,
|
|
146
|
+
retryConfig?: RetryConfig
|
|
132
147
|
): Promise<void> {
|
|
133
148
|
const concurrency = parallelConfig.concurrency;
|
|
134
149
|
console.log(
|
|
135
150
|
`Processing ${csvData.length} rows with concurrency: ${concurrency}`
|
|
136
151
|
);
|
|
137
152
|
|
|
153
|
+
// Extract variable types once for all rows
|
|
154
|
+
const variableTypes = this.extractVariableTypes(mutation);
|
|
155
|
+
|
|
138
156
|
// Split data into chunks for concurrent processing
|
|
139
157
|
const chunks = this.chunkArray(csvData, concurrency);
|
|
140
158
|
let processedCount = 0;
|
|
@@ -143,10 +161,14 @@ export class DataMapper {
|
|
|
143
161
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
144
162
|
const chunk = chunks[chunkIndex];
|
|
145
163
|
const promises = chunk.map(async (row) => {
|
|
146
|
-
const variables = this.mapCsvRowToVariables(row, mapping);
|
|
164
|
+
const variables = this.mapCsvRowToVariables(row, mapping, variableTypes);
|
|
147
165
|
|
|
148
166
|
try {
|
|
149
|
-
const result = await this.client.executeMutation(
|
|
167
|
+
const result = await this.client.executeMutation(
|
|
168
|
+
mutation,
|
|
169
|
+
variables,
|
|
170
|
+
retryConfig
|
|
171
|
+
);
|
|
150
172
|
this.metrics.recordSuccess(entityName);
|
|
151
173
|
return { success: true, result, row };
|
|
152
174
|
} catch (error) {
|
|
@@ -161,7 +183,7 @@ export class DataMapper {
|
|
|
161
183
|
// Count successes and failures in this chunk
|
|
162
184
|
let chunkSuccesses = 0;
|
|
163
185
|
let chunkFailures = 0;
|
|
164
|
-
|
|
186
|
+
|
|
165
187
|
results.forEach((result) => {
|
|
166
188
|
if (result.status === "fulfilled") {
|
|
167
189
|
const { success, error, row } = result.value;
|
|
@@ -184,7 +206,11 @@ export class DataMapper {
|
|
|
184
206
|
// Show progress update (only in non-verbose mode)
|
|
185
207
|
if (!this.verbose) {
|
|
186
208
|
const progress = ((processedCount / totalRows) * 100).toFixed(1);
|
|
187
|
-
console.log(
|
|
209
|
+
console.log(
|
|
210
|
+
`š Progress: ${processedCount}/${totalRows} (${progress}%) - Chunk ${
|
|
211
|
+
chunkIndex + 1
|
|
212
|
+
}: ${chunkSuccesses} ā, ${chunkFailures} ā`
|
|
213
|
+
);
|
|
188
214
|
}
|
|
189
215
|
}
|
|
190
216
|
}
|
|
@@ -199,19 +225,120 @@ export class DataMapper {
|
|
|
199
225
|
|
|
200
226
|
private mapCsvRowToVariables(
|
|
201
227
|
row: CsvRow,
|
|
202
|
-
mapping: Record<string, string
|
|
228
|
+
mapping: Record<string, string>,
|
|
229
|
+
variableTypes: Record<string, string>
|
|
203
230
|
): Record<string, any> {
|
|
204
231
|
const variables: Record<string, any> = {};
|
|
205
232
|
|
|
206
233
|
for (const [graphqlVar, csvColumn] of Object.entries(mapping)) {
|
|
207
234
|
if (row[csvColumn] !== undefined) {
|
|
208
|
-
|
|
235
|
+
const rawValue = row[csvColumn];
|
|
236
|
+
const type = variableTypes[graphqlVar];
|
|
237
|
+
variables[graphqlVar] = this.convertValue(rawValue, type, graphqlVar);
|
|
209
238
|
}
|
|
210
239
|
}
|
|
211
240
|
|
|
212
241
|
return variables;
|
|
213
242
|
}
|
|
214
243
|
|
|
244
|
+
private extractVariableTypes(mutation: string): Record<string, string> {
|
|
245
|
+
const types: Record<string, string> = {};
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const document: DocumentNode = parse(mutation);
|
|
249
|
+
|
|
250
|
+
// Find the operation (mutation/query) and extract variable definitions
|
|
251
|
+
for (const definition of document.definitions) {
|
|
252
|
+
if (
|
|
253
|
+
definition.kind === "OperationDefinition" &&
|
|
254
|
+
definition.variableDefinitions
|
|
255
|
+
) {
|
|
256
|
+
for (const variableDef of definition.variableDefinitions) {
|
|
257
|
+
const varName = variableDef.variable.name.value;
|
|
258
|
+
const typeName = this.extractTypeName(variableDef);
|
|
259
|
+
if (typeName) {
|
|
260
|
+
types[varName] = typeName;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error("Error parsing GraphQL mutation:", error);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return types;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private extractTypeName(variableDef: VariableDefinitionNode): string | null {
|
|
273
|
+
const type = variableDef.type;
|
|
274
|
+
|
|
275
|
+
if (type.kind === "NonNullType") {
|
|
276
|
+
// Handle non-null types like String!
|
|
277
|
+
if (type.type.kind === "NamedType") {
|
|
278
|
+
return type.type.name.value;
|
|
279
|
+
}
|
|
280
|
+
} else if (type.kind === "NamedType") {
|
|
281
|
+
// Handle nullable types like String
|
|
282
|
+
return type.name.value;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private convertValue(value: string, type: string | undefined, varName: string): any {
|
|
289
|
+
if (!type) {
|
|
290
|
+
// No type information available, keep as string
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const trimmedValue = value.trim();
|
|
295
|
+
|
|
296
|
+
switch (type) {
|
|
297
|
+
case "Int":
|
|
298
|
+
const intValue = Number(trimmedValue);
|
|
299
|
+
// Validate that it's a valid integer (no decimals, NaN, or Infinity)
|
|
300
|
+
if (isNaN(intValue) || !isFinite(intValue) || !Number.isInteger(intValue)) {
|
|
301
|
+
console.warn(
|
|
302
|
+
`Warning: Cannot convert "${value}" to Int for variable $${varName}. Expected a valid integer. Using original value.`
|
|
303
|
+
);
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
return intValue;
|
|
307
|
+
|
|
308
|
+
case "Float":
|
|
309
|
+
const floatValue = Number(trimmedValue);
|
|
310
|
+
// Number() is more strict than parseFloat() - it requires the entire string to be valid
|
|
311
|
+
if (isNaN(floatValue) || !isFinite(floatValue)) {
|
|
312
|
+
console.warn(
|
|
313
|
+
`Warning: Cannot convert "${value}" to Float for variable $${varName}. Expected a valid number. Using original value.`
|
|
314
|
+
);
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
return floatValue;
|
|
318
|
+
|
|
319
|
+
case "Boolean":
|
|
320
|
+
const lowerValue = trimmedValue.toLowerCase();
|
|
321
|
+
if (lowerValue === "true" || lowerValue === "1") return true;
|
|
322
|
+
if (lowerValue === "false" || lowerValue === "0") return false;
|
|
323
|
+
console.warn(
|
|
324
|
+
`Warning: Cannot convert "${value}" to Boolean for variable $${varName}. Expected "true", "false", "1", or "0". Using original value.`
|
|
325
|
+
);
|
|
326
|
+
return value;
|
|
327
|
+
|
|
328
|
+
case "String":
|
|
329
|
+
return value;
|
|
330
|
+
|
|
331
|
+
default:
|
|
332
|
+
// Unknown scalar type - keep as string for safety
|
|
333
|
+
if (this.verbose) {
|
|
334
|
+
console.log(
|
|
335
|
+
`Unknown GraphQL type "${type}" for variable $${varName}. Keeping value as string.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
215
342
|
getMetrics(): MetricsCollector {
|
|
216
343
|
return this.metrics;
|
|
217
344
|
}
|
package/src/metrics.ts
CHANGED
|
@@ -12,6 +12,9 @@ export interface ProcessingMetrics {
|
|
|
12
12
|
totalFailures: number;
|
|
13
13
|
entityMetrics: Map<string, EntityMetrics>;
|
|
14
14
|
requestDurations: number[];
|
|
15
|
+
retryAttempts: number;
|
|
16
|
+
retrySuccesses: number;
|
|
17
|
+
retryFailures: number;
|
|
15
18
|
startTime: number;
|
|
16
19
|
endTime?: number;
|
|
17
20
|
}
|
|
@@ -26,6 +29,9 @@ export class MetricsCollector {
|
|
|
26
29
|
totalFailures: 0,
|
|
27
30
|
entityMetrics: new Map(),
|
|
28
31
|
requestDurations: [],
|
|
32
|
+
retryAttempts: 0,
|
|
33
|
+
retrySuccesses: 0,
|
|
34
|
+
retryFailures: 0,
|
|
29
35
|
startTime: Date.now(),
|
|
30
36
|
};
|
|
31
37
|
}
|
|
@@ -88,6 +94,16 @@ export class MetricsCollector {
|
|
|
88
94
|
this.metrics.requestDurations.push(duration);
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
recordRetrySuccess(attempts: number): void {
|
|
98
|
+
this.metrics.retryAttempts += attempts;
|
|
99
|
+
this.metrics.retrySuccesses++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
recordRetryFailure(attempts: number): void {
|
|
103
|
+
this.metrics.retryAttempts += attempts;
|
|
104
|
+
this.metrics.retryFailures++;
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
getAverageRequestDuration(): number {
|
|
92
108
|
if (this.metrics.requestDurations.length === 0) return 0;
|
|
93
109
|
const sum = this.metrics.requestDurations.reduce((a, b) => a + b, 0);
|
|
@@ -114,6 +130,12 @@ export class MetricsCollector {
|
|
|
114
130
|
if (this.metrics.requestDurations.length > 0) {
|
|
115
131
|
summary += ` Avg Request Time: ${avgRequestDuration.toFixed(0)}ms\n`;
|
|
116
132
|
}
|
|
133
|
+
|
|
134
|
+
if (this.metrics.retryAttempts > 0) {
|
|
135
|
+
summary += ` Retry Attempts: ${this.metrics.retryAttempts}\n`;
|
|
136
|
+
summary += ` Retry Successes: ${this.metrics.retrySuccesses}\n`;
|
|
137
|
+
summary += ` Retry Failures: ${this.metrics.retryFailures}\n`;
|
|
138
|
+
}
|
|
117
139
|
|
|
118
140
|
if (this.metrics.entityMetrics.size > 1) {
|
|
119
141
|
summary += `\nš Per-Entity Breakdown:\n`;
|