@jackchuka/gql-ingest 2.1.0 → 2.2.1
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 +7 -7
- package/dist/cli.js +237 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/dependency-resolver.d.ts.map +1 -1
- package/dist/graphql-client.d.ts.map +1 -1
- package/dist/mapper.d.ts +1 -1
- package/dist/mapper.d.ts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/readers/data-reader.d.ts.map +1 -1
- package/dist/readers/json.d.ts.map +1 -1
- package/dist/readers/jsonl.d.ts.map +1 -1
- package/dist/readers/yaml.d.ts.map +1 -1
- package/package.json +31 -25
- package/bin/cli.js +0 -231
- package/src/cli.ts +0 -187
- package/src/config.test.ts +0 -272
- package/src/config.ts +0 -125
- package/src/dependency-resolver.test.ts +0 -211
- package/src/dependency-resolver.ts +0 -102
- package/src/graphql-client.test.ts +0 -219
- package/src/graphql-client.ts +0 -151
- package/src/mapper.test.ts +0 -607
- package/src/mapper.ts +0 -489
- package/src/metrics.test.ts +0 -207
- package/src/metrics.ts +0 -161
- package/src/readers/csv.test.ts +0 -82
- package/src/readers/csv.ts +0 -29
- package/src/readers/data-reader.test.ts +0 -104
- package/src/readers/data-reader.ts +0 -61
- package/src/readers/index.ts +0 -18
- package/src/readers/json.test.ts +0 -80
- package/src/readers/json.ts +0 -27
- package/src/readers/jsonl.test.ts +0 -96
- package/src/readers/jsonl.ts +0 -28
- package/src/readers/yaml.test.ts +0 -95
- package/src/readers/yaml.ts +0 -28
package/src/mapper.ts
DELETED
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { parse, DocumentNode, VariableDefinitionNode } from "graphql";
|
|
4
|
-
import { DataReaderFactory, DataRow } from "./readers";
|
|
5
|
-
import { GraphQLClientWrapper } from "./graphql-client";
|
|
6
|
-
import { MetricsCollector } from "./metrics";
|
|
7
|
-
import { ParallelProcessingConfig, RetryConfig } from "./config";
|
|
8
|
-
|
|
9
|
-
export interface MappingConfig {
|
|
10
|
-
// Legacy CSV support
|
|
11
|
-
csvFile?: string;
|
|
12
|
-
// New flexible data file support
|
|
13
|
-
dataFile?: string;
|
|
14
|
-
dataFormat?: string;
|
|
15
|
-
graphqlFile: string;
|
|
16
|
-
mapping: Record<string, string | any>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class DataMapper {
|
|
20
|
-
private client: GraphQLClientWrapper;
|
|
21
|
-
private basePath: string;
|
|
22
|
-
private metrics: MetricsCollector;
|
|
23
|
-
private verbose: boolean;
|
|
24
|
-
private formatOverride?: string;
|
|
25
|
-
|
|
26
|
-
constructor(
|
|
27
|
-
client: GraphQLClientWrapper,
|
|
28
|
-
basePath: string = process.cwd(),
|
|
29
|
-
metrics?: MetricsCollector,
|
|
30
|
-
verbose: boolean = false,
|
|
31
|
-
formatOverride?: string
|
|
32
|
-
) {
|
|
33
|
-
this.client = client;
|
|
34
|
-
this.basePath = basePath;
|
|
35
|
-
this.metrics = metrics || new MetricsCollector();
|
|
36
|
-
this.verbose = verbose;
|
|
37
|
-
this.formatOverride = formatOverride;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
discoverMappings(configDir: string, entityFilter?: string[]): string[] {
|
|
41
|
-
const mappingsPath = path.resolve(this.basePath, configDir, "mappings");
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const files = fs.readdirSync(mappingsPath);
|
|
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
|
|
73
|
-
|
|
74
|
-
console.log(
|
|
75
|
-
`Discovered ${jsonFiles.length} mapping files: ${jsonFiles.join(", ")}`
|
|
76
|
-
);
|
|
77
|
-
return jsonFiles.map((file) => path.join(configDir, "mappings", file));
|
|
78
|
-
} catch (error) {
|
|
79
|
-
console.error(`Error reading mappings directory ${mappingsPath}:`, error);
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async processEntity(
|
|
85
|
-
configPath: string,
|
|
86
|
-
parallelConfig?: ParallelProcessingConfig,
|
|
87
|
-
retryConfig?: RetryConfig
|
|
88
|
-
): Promise<void> {
|
|
89
|
-
const entityName = path.basename(configPath, ".json");
|
|
90
|
-
console.log(`Processing entity: ${configPath}`);
|
|
91
|
-
|
|
92
|
-
this.metrics.startEntityProcessing(entityName);
|
|
93
|
-
|
|
94
|
-
// Read mapping configuration
|
|
95
|
-
const configFullPath = path.resolve(this.basePath, configPath);
|
|
96
|
-
const config: MappingConfig = JSON.parse(
|
|
97
|
-
fs.readFileSync(configFullPath, "utf8")
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// Extract config directory (parent of mappings directory)
|
|
101
|
-
const configDir = path.dirname(path.dirname(configFullPath));
|
|
102
|
-
|
|
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);
|
|
117
|
-
|
|
118
|
-
// Read GraphQL mutation (relative to config directory)
|
|
119
|
-
const graphqlPath = path.resolve(configDir, config.graphqlFile);
|
|
120
|
-
const mutation = fs.readFileSync(graphqlPath, "utf8");
|
|
121
|
-
|
|
122
|
-
// Process rows with optional parallelization
|
|
123
|
-
if (parallelConfig && parallelConfig.concurrency > 1) {
|
|
124
|
-
await this.processRowsConcurrently(
|
|
125
|
-
data,
|
|
126
|
-
mutation,
|
|
127
|
-
config.mapping,
|
|
128
|
-
entityName,
|
|
129
|
-
parallelConfig,
|
|
130
|
-
retryConfig
|
|
131
|
-
);
|
|
132
|
-
} else {
|
|
133
|
-
await this.processRowsSequentially(
|
|
134
|
-
data,
|
|
135
|
-
mutation,
|
|
136
|
-
config.mapping,
|
|
137
|
-
entityName,
|
|
138
|
-
retryConfig
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
this.metrics.finishEntityProcessing(entityName);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private async processRowsSequentially(
|
|
146
|
-
data: DataRow[],
|
|
147
|
-
mutation: string,
|
|
148
|
-
mapping: Record<string, string | any>,
|
|
149
|
-
entityName: string,
|
|
150
|
-
retryConfig?: RetryConfig
|
|
151
|
-
): Promise<void> {
|
|
152
|
-
const totalRows = data.length;
|
|
153
|
-
const variableTypes = this.extractVariableTypes(mutation);
|
|
154
|
-
|
|
155
|
-
for (let i = 0; i < data.length; i++) {
|
|
156
|
-
const row = data[i];
|
|
157
|
-
const variables = this.mapRowToVariables(row, mapping, variableTypes);
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
await this.client.executeMutation(mutation, variables, retryConfig);
|
|
161
|
-
this.metrics.recordSuccess(entityName);
|
|
162
|
-
|
|
163
|
-
// Show progress every 10% or at the end (only in non-verbose mode)
|
|
164
|
-
if (
|
|
165
|
-
!this.verbose &&
|
|
166
|
-
((i + 1) % Math.max(1, Math.floor(totalRows / 10)) === 0 ||
|
|
167
|
-
i === totalRows - 1)
|
|
168
|
-
) {
|
|
169
|
-
const progress = (((i + 1) / totalRows) * 100).toFixed(1);
|
|
170
|
-
console.log(`📊 Progress: ${i + 1}/${totalRows} (${progress}%) ✓`);
|
|
171
|
-
}
|
|
172
|
-
} catch (error) {
|
|
173
|
-
this.metrics.recordFailure(entityName);
|
|
174
|
-
if (!this.verbose) {
|
|
175
|
-
console.error(
|
|
176
|
-
`✗ Failed to create entity for row ${i + 1}:`,
|
|
177
|
-
row,
|
|
178
|
-
error
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private async processRowsConcurrently(
|
|
186
|
-
data: DataRow[],
|
|
187
|
-
mutation: string,
|
|
188
|
-
mapping: Record<string, string | any>,
|
|
189
|
-
entityName: string,
|
|
190
|
-
parallelConfig: ParallelProcessingConfig,
|
|
191
|
-
retryConfig?: RetryConfig
|
|
192
|
-
): Promise<void> {
|
|
193
|
-
const concurrency = parallelConfig.concurrency;
|
|
194
|
-
console.log(
|
|
195
|
-
`Processing ${data.length} rows with concurrency: ${concurrency}`
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
// Extract variable types once for all rows
|
|
199
|
-
const variableTypes = this.extractVariableTypes(mutation);
|
|
200
|
-
|
|
201
|
-
// Split data into chunks for concurrent processing
|
|
202
|
-
const chunks = this.chunkArray(data, concurrency);
|
|
203
|
-
let processedCount = 0;
|
|
204
|
-
const totalRows = data.length;
|
|
205
|
-
|
|
206
|
-
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
207
|
-
const chunk = chunks[chunkIndex];
|
|
208
|
-
const promises = chunk.map(async (row) => {
|
|
209
|
-
const variables = this.mapRowToVariables(row, mapping, variableTypes);
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const result = await this.client.executeMutation(
|
|
213
|
-
mutation,
|
|
214
|
-
variables,
|
|
215
|
-
retryConfig
|
|
216
|
-
);
|
|
217
|
-
this.metrics.recordSuccess(entityName);
|
|
218
|
-
return { success: true, result, row };
|
|
219
|
-
} catch (error) {
|
|
220
|
-
this.metrics.recordFailure(entityName);
|
|
221
|
-
return { success: false, error, row };
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const results = await Promise.allSettled(promises);
|
|
226
|
-
processedCount += chunk.length;
|
|
227
|
-
|
|
228
|
-
// Count successes and failures in this chunk
|
|
229
|
-
let chunkSuccesses = 0;
|
|
230
|
-
let chunkFailures = 0;
|
|
231
|
-
|
|
232
|
-
results.forEach((result) => {
|
|
233
|
-
if (result.status === "fulfilled") {
|
|
234
|
-
const { success, error, row } = result.value;
|
|
235
|
-
if (success) {
|
|
236
|
-
chunkSuccesses++;
|
|
237
|
-
} else {
|
|
238
|
-
chunkFailures++;
|
|
239
|
-
if (!this.verbose) {
|
|
240
|
-
console.error(`✗ Failed to create entity for row:`, row, error);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
} else {
|
|
244
|
-
chunkFailures++;
|
|
245
|
-
if (!this.verbose) {
|
|
246
|
-
console.error(`✗ Promise rejected:`, result.reason);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
// Show progress update (only in non-verbose mode)
|
|
252
|
-
if (!this.verbose) {
|
|
253
|
-
const progress = ((processedCount / totalRows) * 100).toFixed(1);
|
|
254
|
-
console.log(
|
|
255
|
-
`📊 Progress: ${processedCount}/${totalRows} (${progress}%) - Chunk ${
|
|
256
|
-
chunkIndex + 1
|
|
257
|
-
}: ${chunkSuccesses} ✓, ${chunkFailures} ✗`
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
private chunkArray<T>(array: T[], chunkSize: number): T[][] {
|
|
264
|
-
const chunks: T[][] = [];
|
|
265
|
-
for (let i = 0; i < array.length; i += chunkSize) {
|
|
266
|
-
chunks.push(array.slice(i, i + chunkSize));
|
|
267
|
-
}
|
|
268
|
-
return chunks;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
private mapRowToVariables(
|
|
272
|
-
row: DataRow,
|
|
273
|
-
mapping: Record<string, string | any>,
|
|
274
|
-
variableTypes: Record<string, string>
|
|
275
|
-
): Record<string, any> {
|
|
276
|
-
const variables: Record<string, any> = {};
|
|
277
|
-
|
|
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];
|
|
302
|
-
const type = variableTypes[graphqlVar];
|
|
303
|
-
variables[graphqlVar] = this.convertValue(rawValue, type, graphqlVar);
|
|
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
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return variables;
|
|
316
|
-
}
|
|
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
|
-
|
|
375
|
-
private extractVariableTypes(mutation: string): Record<string, string> {
|
|
376
|
-
const types: Record<string, string> = {};
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
const document: DocumentNode = parse(mutation);
|
|
380
|
-
|
|
381
|
-
// Find the operation (mutation/query) and extract variable definitions
|
|
382
|
-
for (const definition of document.definitions) {
|
|
383
|
-
if (
|
|
384
|
-
definition.kind === "OperationDefinition" &&
|
|
385
|
-
definition.variableDefinitions
|
|
386
|
-
) {
|
|
387
|
-
for (const variableDef of definition.variableDefinitions) {
|
|
388
|
-
const varName = variableDef.variable.name.value;
|
|
389
|
-
const typeName = this.extractTypeName(variableDef);
|
|
390
|
-
if (typeName) {
|
|
391
|
-
types[varName] = typeName;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
} catch (error) {
|
|
397
|
-
console.error("Error parsing GraphQL mutation:", error);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return types;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
private extractTypeName(variableDef: VariableDefinitionNode): string | null {
|
|
404
|
-
const type = variableDef.type;
|
|
405
|
-
|
|
406
|
-
if (type.kind === "NonNullType") {
|
|
407
|
-
// Handle non-null types like String!
|
|
408
|
-
if (type.type.kind === "NamedType") {
|
|
409
|
-
return type.type.name.value;
|
|
410
|
-
}
|
|
411
|
-
} else if (type.kind === "NamedType") {
|
|
412
|
-
// Handle nullable types like String
|
|
413
|
-
return type.name.value;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return null;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
private convertValue(
|
|
420
|
-
value: any,
|
|
421
|
-
type: string | undefined,
|
|
422
|
-
varName: string
|
|
423
|
-
): any {
|
|
424
|
-
if (!type) {
|
|
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") {
|
|
431
|
-
return value;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const trimmedValue = value.trim();
|
|
435
|
-
|
|
436
|
-
switch (type) {
|
|
437
|
-
case "Int":
|
|
438
|
-
const intValue = Number(trimmedValue);
|
|
439
|
-
// Validate that it's a valid integer (no decimals, NaN, or Infinity)
|
|
440
|
-
if (
|
|
441
|
-
isNaN(intValue) ||
|
|
442
|
-
!isFinite(intValue) ||
|
|
443
|
-
!Number.isInteger(intValue)
|
|
444
|
-
) {
|
|
445
|
-
console.warn(
|
|
446
|
-
`Warning: Cannot convert "${value}" to Int for variable $${varName}. Expected a valid integer. Using original value.`
|
|
447
|
-
);
|
|
448
|
-
return value;
|
|
449
|
-
}
|
|
450
|
-
return intValue;
|
|
451
|
-
|
|
452
|
-
case "Float":
|
|
453
|
-
const floatValue = Number(trimmedValue);
|
|
454
|
-
// Number() is more strict than parseFloat() - it requires the entire string to be valid
|
|
455
|
-
if (isNaN(floatValue) || !isFinite(floatValue)) {
|
|
456
|
-
console.warn(
|
|
457
|
-
`Warning: Cannot convert "${value}" to Float for variable $${varName}. Expected a valid number. Using original value.`
|
|
458
|
-
);
|
|
459
|
-
return value;
|
|
460
|
-
}
|
|
461
|
-
return floatValue;
|
|
462
|
-
|
|
463
|
-
case "Boolean":
|
|
464
|
-
const lowerValue = trimmedValue.toLowerCase();
|
|
465
|
-
if (lowerValue === "true" || lowerValue === "1") return true;
|
|
466
|
-
if (lowerValue === "false" || lowerValue === "0") return false;
|
|
467
|
-
console.warn(
|
|
468
|
-
`Warning: Cannot convert "${value}" to Boolean for variable $${varName}. Expected "true", "false", "1", or "0". Using original value.`
|
|
469
|
-
);
|
|
470
|
-
return value;
|
|
471
|
-
|
|
472
|
-
case "String":
|
|
473
|
-
return value;
|
|
474
|
-
|
|
475
|
-
default:
|
|
476
|
-
// Unknown scalar type - keep as string for safety
|
|
477
|
-
if (this.verbose) {
|
|
478
|
-
console.log(
|
|
479
|
-
`Unknown GraphQL type "${type}" for variable $${varName}. Keeping value as string.`
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
return value;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
getMetrics(): MetricsCollector {
|
|
487
|
-
return this.metrics;
|
|
488
|
-
}
|
|
489
|
-
}
|
package/src/metrics.test.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { MetricsCollector } from './metrics';
|
|
2
|
-
|
|
3
|
-
describe('MetricsCollector', () => {
|
|
4
|
-
let metrics: MetricsCollector;
|
|
5
|
-
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
metrics = new MetricsCollector();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
describe('initialization', () => {
|
|
11
|
-
it('should initialize with zero counts', () => {
|
|
12
|
-
expect(metrics.getTotalProcessed()).toBe(0);
|
|
13
|
-
expect(metrics.getSuccessRate()).toBe(0);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should have a start time', () => {
|
|
17
|
-
expect(metrics.getDurationMs()).toBeGreaterThanOrEqual(0);
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe('entity processing', () => {
|
|
22
|
-
it('should start entity processing', () => {
|
|
23
|
-
metrics.startEntityProcessing('testEntity');
|
|
24
|
-
const entityMetric = metrics.getEntityMetrics('testEntity');
|
|
25
|
-
|
|
26
|
-
expect(entityMetric).toBeDefined();
|
|
27
|
-
expect(entityMetric?.entityName).toBe('testEntity');
|
|
28
|
-
expect(entityMetric?.successCount).toBe(0);
|
|
29
|
-
expect(entityMetric?.failureCount).toBe(0);
|
|
30
|
-
expect(entityMetric?.startTime).toBeGreaterThan(0);
|
|
31
|
-
expect(entityMetric?.endTime).toBeUndefined();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should not create duplicate entity metrics', () => {
|
|
35
|
-
metrics.startEntityProcessing('testEntity');
|
|
36
|
-
metrics.startEntityProcessing('testEntity');
|
|
37
|
-
|
|
38
|
-
const entityMetric = metrics.getEntityMetrics('testEntity');
|
|
39
|
-
expect(entityMetric).toBeDefined();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should finish entity processing', () => {
|
|
43
|
-
metrics.startEntityProcessing('testEntity');
|
|
44
|
-
metrics.finishEntityProcessing('testEntity');
|
|
45
|
-
|
|
46
|
-
const entityMetric = metrics.getEntityMetrics('testEntity');
|
|
47
|
-
expect(entityMetric?.endTime).toBeGreaterThan(0);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe('success and failure recording', () => {
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
metrics.startEntityProcessing('testEntity');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should record successes', () => {
|
|
57
|
-
metrics.recordSuccess('testEntity');
|
|
58
|
-
metrics.recordSuccess('testEntity');
|
|
59
|
-
|
|
60
|
-
const entityMetric = metrics.getEntityMetrics('testEntity');
|
|
61
|
-
expect(entityMetric?.successCount).toBe(2);
|
|
62
|
-
expect(entityMetric?.failureCount).toBe(0);
|
|
63
|
-
expect(metrics.getTotalProcessed()).toBe(2);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should record failures', () => {
|
|
67
|
-
metrics.recordFailure('testEntity');
|
|
68
|
-
metrics.recordFailure('testEntity');
|
|
69
|
-
|
|
70
|
-
const entityMetric = metrics.getEntityMetrics('testEntity');
|
|
71
|
-
expect(entityMetric?.successCount).toBe(0);
|
|
72
|
-
expect(entityMetric?.failureCount).toBe(2);
|
|
73
|
-
expect(metrics.getTotalProcessed()).toBe(2);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should record mixed results', () => {
|
|
77
|
-
metrics.recordSuccess('testEntity');
|
|
78
|
-
metrics.recordFailure('testEntity');
|
|
79
|
-
metrics.recordSuccess('testEntity');
|
|
80
|
-
|
|
81
|
-
const entityMetric = metrics.getEntityMetrics('testEntity');
|
|
82
|
-
expect(entityMetric?.successCount).toBe(2);
|
|
83
|
-
expect(entityMetric?.failureCount).toBe(1);
|
|
84
|
-
expect(metrics.getTotalProcessed()).toBe(3);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should handle unknown entity gracefully', () => {
|
|
88
|
-
expect(() => {
|
|
89
|
-
metrics.recordSuccess('unknownEntity');
|
|
90
|
-
}).not.toThrow();
|
|
91
|
-
|
|
92
|
-
expect(metrics.getTotalProcessed()).toBe(0);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('multiple entities', () => {
|
|
97
|
-
beforeEach(() => {
|
|
98
|
-
metrics.startEntityProcessing('entity1');
|
|
99
|
-
metrics.startEntityProcessing('entity2');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('should track multiple entities separately', () => {
|
|
103
|
-
metrics.recordSuccess('entity1');
|
|
104
|
-
metrics.recordSuccess('entity1');
|
|
105
|
-
metrics.recordFailure('entity2');
|
|
106
|
-
|
|
107
|
-
const entity1Metrics = metrics.getEntityMetrics('entity1');
|
|
108
|
-
const entity2Metrics = metrics.getEntityMetrics('entity2');
|
|
109
|
-
|
|
110
|
-
expect(entity1Metrics?.successCount).toBe(2);
|
|
111
|
-
expect(entity1Metrics?.failureCount).toBe(0);
|
|
112
|
-
expect(entity2Metrics?.successCount).toBe(0);
|
|
113
|
-
expect(entity2Metrics?.failureCount).toBe(1);
|
|
114
|
-
expect(metrics.getTotalProcessed()).toBe(3);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('success rate calculation', () => {
|
|
119
|
-
beforeEach(() => {
|
|
120
|
-
metrics.startEntityProcessing('testEntity');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('should calculate 100% success rate', () => {
|
|
124
|
-
metrics.recordSuccess('testEntity');
|
|
125
|
-
metrics.recordSuccess('testEntity');
|
|
126
|
-
|
|
127
|
-
expect(metrics.getSuccessRate()).toBe(100);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should calculate 0% success rate', () => {
|
|
131
|
-
metrics.recordFailure('testEntity');
|
|
132
|
-
metrics.recordFailure('testEntity');
|
|
133
|
-
|
|
134
|
-
expect(metrics.getSuccessRate()).toBe(0);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should calculate partial success rate', () => {
|
|
138
|
-
metrics.recordSuccess('testEntity');
|
|
139
|
-
metrics.recordFailure('testEntity');
|
|
140
|
-
metrics.recordFailure('testEntity');
|
|
141
|
-
|
|
142
|
-
expect(metrics.getSuccessRate()).toBeCloseTo(33.3, 1);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should return 0% for no processed items', () => {
|
|
146
|
-
expect(metrics.getSuccessRate()).toBe(0);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('summary generation', () => {
|
|
151
|
-
it('should generate summary for empty metrics', () => {
|
|
152
|
-
const summary = metrics.generateSummary();
|
|
153
|
-
|
|
154
|
-
expect(summary).toContain('Total Processed: 0');
|
|
155
|
-
expect(summary).toContain('Successes: 0');
|
|
156
|
-
expect(summary).toContain('Failures: 0');
|
|
157
|
-
expect(summary).toContain('Success Rate: 0.0%');
|
|
158
|
-
expect(summary).toContain('Duration:');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should generate summary with single entity', () => {
|
|
162
|
-
metrics.startEntityProcessing('testEntity');
|
|
163
|
-
metrics.recordSuccess('testEntity');
|
|
164
|
-
metrics.recordFailure('testEntity');
|
|
165
|
-
metrics.finishEntityProcessing('testEntity');
|
|
166
|
-
|
|
167
|
-
const summary = metrics.generateSummary();
|
|
168
|
-
|
|
169
|
-
expect(summary).toContain('Total Processed: 2');
|
|
170
|
-
expect(summary).toContain('Successes: 1');
|
|
171
|
-
expect(summary).toContain('Failures: 1');
|
|
172
|
-
expect(summary).toContain('Success Rate: 50.0%');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should generate summary with multiple entities', () => {
|
|
176
|
-
metrics.startEntityProcessing('entity1');
|
|
177
|
-
metrics.recordSuccess('entity1');
|
|
178
|
-
metrics.finishEntityProcessing('entity1');
|
|
179
|
-
|
|
180
|
-
metrics.startEntityProcessing('entity2');
|
|
181
|
-
metrics.recordFailure('entity2');
|
|
182
|
-
metrics.finishEntityProcessing('entity2');
|
|
183
|
-
|
|
184
|
-
const summary = metrics.generateSummary();
|
|
185
|
-
|
|
186
|
-
expect(summary).toContain('Total Processed: 2');
|
|
187
|
-
expect(summary).toContain('Per-Entity Breakdown');
|
|
188
|
-
expect(summary).toContain('entity1:');
|
|
189
|
-
expect(summary).toContain('entity2:');
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe('finish processing', () => {
|
|
194
|
-
it('should set end time and return metrics', () => {
|
|
195
|
-
metrics.startEntityProcessing('testEntity');
|
|
196
|
-
metrics.recordSuccess('testEntity');
|
|
197
|
-
|
|
198
|
-
const finalMetrics = metrics.finishProcessing();
|
|
199
|
-
|
|
200
|
-
expect(finalMetrics.endTime).toBeGreaterThan(0);
|
|
201
|
-
expect(finalMetrics.totalEntities).toBe(1);
|
|
202
|
-
expect(finalMetrics.totalSuccesses).toBe(1);
|
|
203
|
-
expect(finalMetrics.totalFailures).toBe(0);
|
|
204
|
-
expect(finalMetrics.entityMetrics.size).toBe(1);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
});
|