@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/src/mapper.ts CHANGED
@@ -1,5 +1,6 @@
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";
@@ -29,12 +30,39 @@ export class DataMapper {
29
30
  this.verbose = verbose;
30
31
  }
31
32
 
32
- discoverMappings(configDir: string): string[] {
33
+ discoverMappings(configDir: string, entityFilter?: string[]): string[] {
33
34
  const mappingsPath = path.resolve(this.basePath, configDir, "mappings");
34
35
 
35
36
  try {
36
37
  const files = fs.readdirSync(mappingsPath);
37
- const jsonFiles = files.filter((file) => file.endsWith(".json")).sort(); // Alphabetical order for consistent processing
38
+ let jsonFiles = files.filter((file) => file.endsWith(".json"));
39
+
40
+ // Apply entity filter if provided
41
+ if (entityFilter && entityFilter.length > 0) {
42
+ const requestedEntities = new Set(entityFilter);
43
+ const foundEntities = new Set<string>();
44
+
45
+ jsonFiles = jsonFiles.filter((file) => {
46
+ const entityName = path.basename(file, ".json");
47
+ if (requestedEntities.has(entityName)) {
48
+ foundEntities.add(entityName);
49
+ return true;
50
+ }
51
+ return false;
52
+ });
53
+
54
+ // Check for requested entities that were not found
55
+ const notFound = entityFilter.filter((e) => !foundEntities.has(e));
56
+ if (notFound.length > 0) {
57
+ console.warn(
58
+ `Warning: The following entities were not found in mappings: ${notFound.join(
59
+ ", "
60
+ )}`
61
+ );
62
+ }
63
+ }
64
+
65
+ jsonFiles.sort(); // Alphabetical order for consistent processing
38
66
 
39
67
  console.log(
40
68
  `Discovered ${jsonFiles.length} mapping files: ${jsonFiles.join(", ")}`
@@ -104,24 +132,33 @@ export class DataMapper {
104
132
  retryConfig?: RetryConfig
105
133
  ): Promise<void> {
106
134
  const totalRows = csvData.length;
107
-
135
+ const variableTypes = this.extractVariableTypes(mutation);
136
+
108
137
  for (let i = 0; i < csvData.length; i++) {
109
138
  const row = csvData[i];
110
- const variables = this.mapCsvRowToVariables(row, mapping);
139
+ const variables = this.mapCsvRowToVariables(row, mapping, variableTypes);
111
140
 
112
141
  try {
113
142
  await this.client.executeMutation(mutation, variables, retryConfig);
114
143
  this.metrics.recordSuccess(entityName);
115
-
144
+
116
145
  // Show progress every 10% or at the end (only in non-verbose mode)
117
- if (!this.verbose && ((i + 1) % Math.max(1, Math.floor(totalRows / 10)) === 0 || i === totalRows - 1)) {
146
+ if (
147
+ !this.verbose &&
148
+ ((i + 1) % Math.max(1, Math.floor(totalRows / 10)) === 0 ||
149
+ i === totalRows - 1)
150
+ ) {
118
151
  const progress = (((i + 1) / totalRows) * 100).toFixed(1);
119
152
  console.log(`šŸ“Š Progress: ${i + 1}/${totalRows} (${progress}%) āœ“`);
120
153
  }
121
154
  } catch (error) {
122
155
  this.metrics.recordFailure(entityName);
123
156
  if (!this.verbose) {
124
- console.error(`āœ— Failed to create entity for row ${i + 1}:`, row, error);
157
+ console.error(
158
+ `āœ— Failed to create entity for row ${i + 1}:`,
159
+ row,
160
+ error
161
+ );
125
162
  }
126
163
  }
127
164
  }
@@ -140,6 +177,9 @@ export class DataMapper {
140
177
  `Processing ${csvData.length} rows with concurrency: ${concurrency}`
141
178
  );
142
179
 
180
+ // Extract variable types once for all rows
181
+ const variableTypes = this.extractVariableTypes(mutation);
182
+
143
183
  // Split data into chunks for concurrent processing
144
184
  const chunks = this.chunkArray(csvData, concurrency);
145
185
  let processedCount = 0;
@@ -148,10 +188,18 @@ export class DataMapper {
148
188
  for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
149
189
  const chunk = chunks[chunkIndex];
150
190
  const promises = chunk.map(async (row) => {
151
- const variables = this.mapCsvRowToVariables(row, mapping);
191
+ const variables = this.mapCsvRowToVariables(
192
+ row,
193
+ mapping,
194
+ variableTypes
195
+ );
152
196
 
153
197
  try {
154
- const result = await this.client.executeMutation(mutation, variables, retryConfig);
198
+ const result = await this.client.executeMutation(
199
+ mutation,
200
+ variables,
201
+ retryConfig
202
+ );
155
203
  this.metrics.recordSuccess(entityName);
156
204
  return { success: true, result, row };
157
205
  } catch (error) {
@@ -166,7 +214,7 @@ export class DataMapper {
166
214
  // Count successes and failures in this chunk
167
215
  let chunkSuccesses = 0;
168
216
  let chunkFailures = 0;
169
-
217
+
170
218
  results.forEach((result) => {
171
219
  if (result.status === "fulfilled") {
172
220
  const { success, error, row } = result.value;
@@ -189,7 +237,11 @@ export class DataMapper {
189
237
  // Show progress update (only in non-verbose mode)
190
238
  if (!this.verbose) {
191
239
  const progress = ((processedCount / totalRows) * 100).toFixed(1);
192
- console.log(`šŸ“Š Progress: ${processedCount}/${totalRows} (${progress}%) - Chunk ${chunkIndex + 1}: ${chunkSuccesses} āœ“, ${chunkFailures} āœ—`);
240
+ console.log(
241
+ `šŸ“Š Progress: ${processedCount}/${totalRows} (${progress}%) - Chunk ${
242
+ chunkIndex + 1
243
+ }: ${chunkSuccesses} āœ“, ${chunkFailures} āœ—`
244
+ );
193
245
  }
194
246
  }
195
247
  }
@@ -204,19 +256,128 @@ export class DataMapper {
204
256
 
205
257
  private mapCsvRowToVariables(
206
258
  row: CsvRow,
207
- mapping: Record<string, string>
259
+ mapping: Record<string, string>,
260
+ variableTypes: Record<string, string>
208
261
  ): Record<string, any> {
209
262
  const variables: Record<string, any> = {};
210
263
 
211
264
  for (const [graphqlVar, csvColumn] of Object.entries(mapping)) {
212
265
  if (row[csvColumn] !== undefined) {
213
- variables[graphqlVar] = row[csvColumn];
266
+ const rawValue = row[csvColumn];
267
+ const type = variableTypes[graphqlVar];
268
+ variables[graphqlVar] = this.convertValue(rawValue, type, graphqlVar);
214
269
  }
215
270
  }
216
271
 
217
272
  return variables;
218
273
  }
219
274
 
275
+ private extractVariableTypes(mutation: string): Record<string, string> {
276
+ const types: Record<string, string> = {};
277
+
278
+ try {
279
+ const document: DocumentNode = parse(mutation);
280
+
281
+ // Find the operation (mutation/query) and extract variable definitions
282
+ for (const definition of document.definitions) {
283
+ if (
284
+ definition.kind === "OperationDefinition" &&
285
+ definition.variableDefinitions
286
+ ) {
287
+ for (const variableDef of definition.variableDefinitions) {
288
+ const varName = variableDef.variable.name.value;
289
+ const typeName = this.extractTypeName(variableDef);
290
+ if (typeName) {
291
+ types[varName] = typeName;
292
+ }
293
+ }
294
+ }
295
+ }
296
+ } catch (error) {
297
+ console.error("Error parsing GraphQL mutation:", error);
298
+ }
299
+
300
+ return types;
301
+ }
302
+
303
+ private extractTypeName(variableDef: VariableDefinitionNode): string | null {
304
+ const type = variableDef.type;
305
+
306
+ if (type.kind === "NonNullType") {
307
+ // Handle non-null types like String!
308
+ if (type.type.kind === "NamedType") {
309
+ return type.type.name.value;
310
+ }
311
+ } else if (type.kind === "NamedType") {
312
+ // Handle nullable types like String
313
+ return type.name.value;
314
+ }
315
+
316
+ return null;
317
+ }
318
+
319
+ private convertValue(
320
+ value: string,
321
+ type: string | undefined,
322
+ varName: string
323
+ ): any {
324
+ if (!type) {
325
+ // No type information available, keep as string
326
+ return value;
327
+ }
328
+
329
+ const trimmedValue = value.trim();
330
+
331
+ switch (type) {
332
+ case "Int":
333
+ const intValue = Number(trimmedValue);
334
+ // Validate that it's a valid integer (no decimals, NaN, or Infinity)
335
+ if (
336
+ isNaN(intValue) ||
337
+ !isFinite(intValue) ||
338
+ !Number.isInteger(intValue)
339
+ ) {
340
+ console.warn(
341
+ `Warning: Cannot convert "${value}" to Int for variable $${varName}. Expected a valid integer. Using original value.`
342
+ );
343
+ return value;
344
+ }
345
+ return intValue;
346
+
347
+ case "Float":
348
+ const floatValue = Number(trimmedValue);
349
+ // Number() is more strict than parseFloat() - it requires the entire string to be valid
350
+ if (isNaN(floatValue) || !isFinite(floatValue)) {
351
+ console.warn(
352
+ `Warning: Cannot convert "${value}" to Float for variable $${varName}. Expected a valid number. Using original value.`
353
+ );
354
+ return value;
355
+ }
356
+ return floatValue;
357
+
358
+ case "Boolean":
359
+ const lowerValue = trimmedValue.toLowerCase();
360
+ if (lowerValue === "true" || lowerValue === "1") return true;
361
+ if (lowerValue === "false" || lowerValue === "0") return false;
362
+ console.warn(
363
+ `Warning: Cannot convert "${value}" to Boolean for variable $${varName}. Expected "true", "false", "1", or "0". Using original value.`
364
+ );
365
+ return value;
366
+
367
+ case "String":
368
+ return value;
369
+
370
+ default:
371
+ // Unknown scalar type - keep as string for safety
372
+ if (this.verbose) {
373
+ console.log(
374
+ `Unknown GraphQL type "${type}" for variable $${varName}. Keeping value as string.`
375
+ );
376
+ }
377
+ return value;
378
+ }
379
+ }
380
+
220
381
  getMetrics(): MetricsCollector {
221
382
  return this.metrics;
222
383
  }
package/src/metrics.ts CHANGED
@@ -119,35 +119,43 @@ export class MetricsCollector {
119
119
  const duration = this.getDurationMs();
120
120
  const successRate = this.getSuccessRate();
121
121
  const avgRequestDuration = this.getAverageRequestDuration();
122
-
122
+
123
123
  let summary = `\nšŸ“Š Processing Summary:\n`;
124
124
  summary += ` Total Processed: ${this.metrics.totalEntities}\n`;
125
125
  summary += ` āœ“ Successes: ${this.metrics.totalSuccesses}\n`;
126
126
  summary += ` āœ— Failures: ${this.metrics.totalFailures}\n`;
127
127
  summary += ` Success Rate: ${successRate.toFixed(1)}%\n`;
128
128
  summary += ` Duration: ${(duration / 1000).toFixed(2)}s\n`;
129
-
129
+
130
130
  if (this.metrics.requestDurations.length > 0) {
131
131
  summary += ` Avg Request Time: ${avgRequestDuration.toFixed(0)}ms\n`;
132
132
  }
133
-
133
+
134
134
  if (this.metrics.retryAttempts > 0) {
135
135
  summary += ` Retry Attempts: ${this.metrics.retryAttempts}\n`;
136
136
  summary += ` Retry Successes: ${this.metrics.retrySuccesses}\n`;
137
137
  summary += ` Retry Failures: ${this.metrics.retryFailures}\n`;
138
138
  }
139
139
 
140
- if (this.metrics.entityMetrics.size > 1) {
140
+ if (this.metrics.entityMetrics.size > 0) {
141
141
  summary += `\nšŸ“‹ Per-Entity Breakdown:\n`;
142
142
  for (const [entityName, entityMetric] of this.metrics.entityMetrics) {
143
- const entityTotal = entityMetric.successCount + entityMetric.failureCount;
144
- const entityRate = entityTotal > 0 ? (entityMetric.successCount / entityTotal) * 100 : 0;
145
- const entityDuration = entityMetric.endTime ? entityMetric.endTime - entityMetric.startTime : 0;
146
-
147
- summary += ` ${entityName}: ${entityTotal} total (${entityMetric.successCount} āœ“, ${entityMetric.failureCount} āœ—) - ${entityRate.toFixed(1)}% success - ${(entityDuration / 1000).toFixed(2)}s\n`;
143
+ const entityTotal =
144
+ entityMetric.successCount + entityMetric.failureCount;
145
+ const entityRate =
146
+ entityTotal > 0 ? (entityMetric.successCount / entityTotal) * 100 : 0;
147
+ const entityDuration = entityMetric.endTime
148
+ ? entityMetric.endTime - entityMetric.startTime
149
+ : 0;
150
+
151
+ summary += ` ${entityName}: ${entityTotal} total (${
152
+ entityMetric.successCount
153
+ } āœ“, ${entityMetric.failureCount} āœ—) - ${entityRate.toFixed(
154
+ 1
155
+ )}% success - ${(entityDuration / 1000).toFixed(2)}s\n`;
148
156
  }
149
157
  }
150
158
 
151
159
  return summary;
152
160
  }
153
- }
161
+ }