@jackchuka/gql-ingest 4.0.0 → 4.2.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/dist/index.js CHANGED
@@ -14314,6 +14314,12 @@ var MetricsCollector = class {
14314
14314
  };
14315
14315
 
14316
14316
  // src/lib/mapper.ts
14317
+ var RefResolutionError = class extends Error {
14318
+ constructor(message) {
14319
+ super(message);
14320
+ this.name = "RefResolutionError";
14321
+ }
14322
+ };
14317
14323
  var DataMapper = class {
14318
14324
  constructor(client, basePath = process.cwd(), metrics, logger = noopLogger, formatOverride) {
14319
14325
  this.client = client;
@@ -14331,7 +14337,7 @@ var DataMapper = class {
14331
14337
  /**
14332
14338
  * Process an entity with event callbacks and abort support
14333
14339
  */
14334
- async processEntityWithEvents(configPath, parallelConfig, retryConfig, signal, callbacks) {
14340
+ async processEntityWithEvents(configPath, parallelConfig, retryConfig, signal, callbacks, outputStore) {
14335
14341
  const entityStartTime = Date.now();
14336
14342
  const configFullPath = path.resolve(this.basePath, configPath);
14337
14343
  const config2 = JSON.parse(fs6.readFileSync(configFullPath, "utf8"));
@@ -14359,7 +14365,9 @@ var DataMapper = class {
14359
14365
  parallelConfig,
14360
14366
  retryConfig,
14361
14367
  signal,
14362
- callbacks
14368
+ callbacks,
14369
+ config2.outputCapture,
14370
+ outputStore
14363
14371
  );
14364
14372
  } else {
14365
14373
  await this.processRowsSequentiallyWithEvents(
@@ -14369,7 +14377,9 @@ var DataMapper = class {
14369
14377
  entityName,
14370
14378
  retryConfig,
14371
14379
  signal,
14372
- callbacks
14380
+ callbacks,
14381
+ config2.outputCapture,
14382
+ outputStore
14373
14383
  );
14374
14384
  }
14375
14385
  this.metrics.finishEntityProcessing(entityName);
@@ -14393,7 +14403,7 @@ var DataMapper = class {
14393
14403
  durationMs: duration3
14394
14404
  });
14395
14405
  }
14396
- async processRowsSequentiallyWithEvents(data, mutation, mapping, entityName, retryConfig, signal, callbacks) {
14406
+ async processRowsSequentiallyWithEvents(data, mutation, mapping, entityName, retryConfig, signal, callbacks, outputCapture, outputStore) {
14397
14407
  const totalRows = data.length;
14398
14408
  const variableTypes = this.extractVariableTypes(mutation);
14399
14409
  for (let i = 0; i < data.length; i++) {
@@ -14402,11 +14412,12 @@ var DataMapper = class {
14402
14412
  return;
14403
14413
  }
14404
14414
  const row = data[i];
14405
- const variables = this.mapRowToVariables(row, mapping, variableTypes);
14406
14415
  const rowStartTime = Date.now();
14407
14416
  try {
14417
+ const variables = this.mapRowToVariables(row, mapping, variableTypes, outputStore);
14408
14418
  const result = await this.client.executeMutation(mutation, variables, retryConfig, signal);
14409
14419
  this.metrics.recordSuccess(entityName);
14420
+ this.captureOutput(row, result, entityName, outputCapture, outputStore);
14410
14421
  callbacks?.onRowSuccess?.({
14411
14422
  entityName,
14412
14423
  rowIndex: i,
@@ -14435,7 +14446,7 @@ var DataMapper = class {
14435
14446
  }
14436
14447
  }
14437
14448
  }
14438
- async processRowsConcurrentlyWithEvents(data, mutation, mapping, entityName, parallelConfig, retryConfig, signal, callbacks) {
14449
+ async processRowsConcurrentlyWithEvents(data, mutation, mapping, entityName, parallelConfig, retryConfig, signal, callbacks, outputCapture, outputStore) {
14439
14450
  const concurrency = parallelConfig.concurrency;
14440
14451
  this.logger.info(`Processing ${data.length} rows with concurrency: ${concurrency}`);
14441
14452
  const variableTypes = this.extractVariableTypes(mutation);
@@ -14451,9 +14462,9 @@ var DataMapper = class {
14451
14462
  const chunkStartIndex = chunkIndex * concurrency;
14452
14463
  const promises = chunk.map(async (row, index) => {
14453
14464
  const rowIndex = chunkStartIndex + index;
14454
- const variables = this.mapRowToVariables(row, mapping, variableTypes);
14455
14465
  const rowStartTime = Date.now();
14456
14466
  try {
14467
+ const variables = this.mapRowToVariables(row, mapping, variableTypes, outputStore);
14457
14468
  const result = await this.client.executeMutation(
14458
14469
  mutation,
14459
14470
  variables,
@@ -14461,6 +14472,7 @@ var DataMapper = class {
14461
14472
  signal
14462
14473
  );
14463
14474
  this.metrics.recordSuccess(entityName);
14475
+ this.captureOutput(row, result, entityName, outputCapture, outputStore);
14464
14476
  callbacks?.onRowSuccess?.({
14465
14477
  entityName,
14466
14478
  rowIndex,
@@ -14516,13 +14528,13 @@ var DataMapper = class {
14516
14528
  }
14517
14529
  return chunks;
14518
14530
  }
14519
- mapRowToVariables(row, mapping, variableTypes) {
14531
+ mapRowToVariables(row, mapping, variableTypes, outputStore) {
14520
14532
  const variables = {};
14521
14533
  for (const [graphqlVar, mappingValue] of Object.entries(mapping)) {
14522
14534
  if (mappingValue === "$") {
14523
14535
  variables[graphqlVar] = row;
14524
14536
  } else if (typeof mappingValue === "string" && mappingValue.startsWith("$.")) {
14525
- const dataPath = mappingValue.substring(2);
14537
+ const dataPath = this.stripJsonPathPrefix(mappingValue);
14526
14538
  const value = this.getValueByPath(row, dataPath);
14527
14539
  if (value !== void 0) {
14528
14540
  const type = variableTypes[graphqlVar];
@@ -14532,8 +14544,15 @@ var DataMapper = class {
14532
14544
  const rawValue = row[mappingValue];
14533
14545
  const type = variableTypes[graphqlVar];
14534
14546
  variables[graphqlVar] = this.convertValue(rawValue, type, graphqlVar);
14547
+ } else if (this.isRefMapping(mappingValue)) {
14548
+ variables[graphqlVar] = this.resolveRef(row, mappingValue, outputStore);
14535
14549
  } else if (typeof mappingValue === "object" && mappingValue !== null) {
14536
- variables[graphqlVar] = this.mapNestedObject(row, mappingValue, variableTypes);
14550
+ variables[graphqlVar] = this.mapNestedObject(row, mappingValue, variableTypes, outputStore);
14551
+ }
14552
+ }
14553
+ if (outputStore) {
14554
+ for (const [key, value] of Object.entries(variables)) {
14555
+ variables[key] = this.resolveRefsInData(value, row, outputStore);
14537
14556
  }
14538
14557
  }
14539
14558
  return variables;
@@ -14550,15 +14569,18 @@ var DataMapper = class {
14550
14569
  }
14551
14570
  return current;
14552
14571
  }
14553
- mapNestedObject(row, mappingObj, variableTypes) {
14572
+ mapNestedObject(row, mappingObj, variableTypes, outputStore) {
14554
14573
  if (Array.isArray(mappingObj)) {
14555
- return mappingObj.map((item) => this.mapNestedObject(row, item, variableTypes));
14574
+ return mappingObj.map((item) => this.mapNestedObject(row, item, variableTypes, outputStore));
14556
14575
  }
14557
14576
  if (typeof mappingObj === "object" && mappingObj !== null) {
14577
+ if (this.isRefMapping(mappingObj)) {
14578
+ return this.resolveRef(row, mappingObj, outputStore);
14579
+ }
14558
14580
  const result = {};
14559
14581
  for (const [key, value] of Object.entries(mappingObj)) {
14560
14582
  if (typeof value === "string" && value.startsWith("$.")) {
14561
- const dataPath = value.substring(2);
14583
+ const dataPath = this.stripJsonPathPrefix(value);
14562
14584
  let fieldValue = this.getValueByPath(row, dataPath);
14563
14585
  if (key === "values" && typeof fieldValue === "string" && fieldValue.includes(",")) {
14564
14586
  fieldValue = fieldValue.split(",").map((v) => v.trim());
@@ -14566,8 +14588,10 @@ var DataMapper = class {
14566
14588
  result[key] = fieldValue;
14567
14589
  } else if (typeof value === "string" && row[value] !== void 0) {
14568
14590
  result[key] = row[value];
14591
+ } else if (this.isRefMapping(value)) {
14592
+ result[key] = this.resolveRef(row, value, outputStore);
14569
14593
  } else if (typeof value === "object") {
14570
- result[key] = this.mapNestedObject(row, value, variableTypes);
14594
+ result[key] = this.mapNestedObject(row, value, variableTypes, outputStore);
14571
14595
  } else {
14572
14596
  result[key] = value;
14573
14597
  }
@@ -14576,6 +14600,113 @@ var DataMapper = class {
14576
14600
  }
14577
14601
  return mappingObj;
14578
14602
  }
14603
+ /**
14604
+ * Recursively walk a resolved value and resolve any $ref objects found in data.
14605
+ * Unlike mapping-level $ref (where key is a JSONPath into the row), data-level
14606
+ * $ref uses key as a literal lookup value unless it starts with "$.".
14607
+ */
14608
+ resolveRefsInData(value, row, outputStore) {
14609
+ if (Array.isArray(value)) {
14610
+ let changed = false;
14611
+ const mapped = value.map((item) => {
14612
+ const resolved = this.resolveRefsInData(item, row, outputStore);
14613
+ if (resolved !== item) changed = true;
14614
+ return resolved;
14615
+ });
14616
+ return changed ? mapped : value;
14617
+ }
14618
+ if (typeof value === "object" && value !== null) {
14619
+ if (this.isRefMapping(value)) {
14620
+ const keyStr = value.key.startsWith("$.") ? String(this.getValueByPath(row, this.stripJsonPathPrefix(value.key)) ?? value.key) : value.key;
14621
+ return this.lookupRef(outputStore, value.$ref, keyStr, value.field);
14622
+ }
14623
+ let changed = false;
14624
+ const result = {};
14625
+ for (const [k, v] of Object.entries(value)) {
14626
+ const resolved = this.resolveRefsInData(v, row, outputStore);
14627
+ if (resolved !== v) changed = true;
14628
+ result[k] = resolved;
14629
+ }
14630
+ return changed ? result : value;
14631
+ }
14632
+ return value;
14633
+ }
14634
+ stripJsonPathPrefix(jsonPath) {
14635
+ return jsonPath.startsWith("$.") ? jsonPath.substring(2) : jsonPath;
14636
+ }
14637
+ isRefMapping(value) {
14638
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && "key" in value && "field" in value;
14639
+ }
14640
+ /** Resolve a mapping-level $ref — throws on any failure so the row is skipped. */
14641
+ resolveRef(row, ref, outputStore) {
14642
+ if (!outputStore) {
14643
+ throw new RefResolutionError(`$ref to "${ref.$ref}" found but no output store is available`);
14644
+ }
14645
+ const keyPath = this.stripJsonPathPrefix(ref.key);
14646
+ const lookupKeyValue = this.getValueByPath(row, keyPath);
14647
+ if (lookupKeyValue === void 0) {
14648
+ throw new RefResolutionError(`$ref lookup key "${ref.key}" not found in current row`);
14649
+ }
14650
+ const keyStr = String(lookupKeyValue);
14651
+ const value = this.lookupRef(outputStore, ref.$ref, keyStr, ref.field);
14652
+ if (value === void 0) {
14653
+ throw new RefResolutionError(
14654
+ `$ref resolution failed for "${ref.$ref}[${keyStr}].${ref.field}"`
14655
+ );
14656
+ }
14657
+ return value;
14658
+ }
14659
+ /**
14660
+ * Core ref lookup: entityName -> key -> field.
14661
+ * Returns undefined with a warning on miss (caller decides whether to throw).
14662
+ */
14663
+ lookupRef(outputStore, entityName, keyStr, field) {
14664
+ const entityStore = outputStore.get(entityName);
14665
+ if (!entityStore) {
14666
+ this.logger.warn(`$ref entity "${entityName}" not found in output store`);
14667
+ return void 0;
14668
+ }
14669
+ const captured = entityStore.get(keyStr);
14670
+ if (!captured) {
14671
+ this.logger.warn(`$ref key "${keyStr}" not found in entity "${entityName}" output store`);
14672
+ return void 0;
14673
+ }
14674
+ const value = captured[field];
14675
+ if (value === void 0) {
14676
+ this.logger.warn(
14677
+ `$ref field "${field}" not found in captured output for "${entityName}[${keyStr}]"`
14678
+ );
14679
+ }
14680
+ return value;
14681
+ }
14682
+ captureOutput(row, result, entityName, outputCapture, outputStore) {
14683
+ if (!outputCapture || !outputStore) return;
14684
+ const keyPath = this.stripJsonPathPrefix(outputCapture.key);
14685
+ const keyValue = this.getValueByPath(row, keyPath);
14686
+ if (keyValue === void 0) {
14687
+ this.logger.warn(
14688
+ `outputCapture key "${outputCapture.key}" not found in input row for entity "${entityName}". Skipping capture.`
14689
+ );
14690
+ return;
14691
+ }
14692
+ const keyStr = String(keyValue);
14693
+ const captured = {};
14694
+ for (const [fieldName, responsePath] of Object.entries(outputCapture.fields)) {
14695
+ const dataPath = this.stripJsonPathPrefix(responsePath);
14696
+ captured[fieldName] = this.getValueByPath(result, dataPath);
14697
+ }
14698
+ let entityStore = outputStore.get(entityName);
14699
+ if (!entityStore) {
14700
+ entityStore = /* @__PURE__ */ new Map();
14701
+ outputStore.set(entityName, entityStore);
14702
+ }
14703
+ if (entityStore.has(keyStr)) {
14704
+ this.logger.warn(
14705
+ `outputCapture: duplicate key "${keyStr}" for entity "${entityName}" \u2014 overwriting previous value`
14706
+ );
14707
+ }
14708
+ entityStore.set(keyStr, captured);
14709
+ }
14579
14710
  extractVariableTypes(mutation) {
14580
14711
  const types = {};
14581
14712
  try {
@@ -14742,6 +14873,7 @@ var DEFAULT_EVENT_OPTIONS = {
14742
14873
  var GQLIngest = class extends EventEmitter {
14743
14874
  constructor(options) {
14744
14875
  super();
14876
+ this.outputStore = /* @__PURE__ */ new Map();
14745
14877
  // Cancellation state
14746
14878
  this.abortController = null;
14747
14879
  this.isProcessing = false;
@@ -14884,6 +15016,7 @@ var GQLIngest = class extends EventEmitter {
14884
15016
  return this.handleCancellation(signal.reason);
14885
15017
  }
14886
15018
  this.metrics = new MetricsCollector();
15019
+ this.outputStore = /* @__PURE__ */ new Map();
14887
15020
  this.client = new GraphQLClientWrapper(
14888
15021
  this.endpoint,
14889
15022
  this.headers,
@@ -14961,7 +15094,8 @@ var GQLIngest = class extends EventEmitter {
14961
15094
  this.mapper,
14962
15095
  config2,
14963
15096
  this.logger,
14964
- signal
15097
+ signal,
15098
+ this.outputStore
14965
15099
  );
14966
15100
  if (signal.aborted) {
14967
15101
  return this.handleCancellation(signal.reason);
@@ -15067,7 +15201,7 @@ var GQLIngest = class extends EventEmitter {
15067
15201
  /**
15068
15202
  * Process entities in dependency-aware waves with abort support
15069
15203
  */
15070
- async processEntitiesInWaves(pathMap, resolver, mapper, config2, logger, signal) {
15204
+ async processEntitiesInWaves(pathMap, resolver, mapper, config2, logger, signal, outputStore) {
15071
15205
  const waves = resolver.resolveExecutionOrder();
15072
15206
  logger.info(`Processing ${waves.length} dependency waves...`);
15073
15207
  for (const wave of waves) {
@@ -15092,18 +15226,25 @@ var GQLIngest = class extends EventEmitter {
15092
15226
  try {
15093
15227
  const entityConfig = getEntityConfig(entityName, config2, logger);
15094
15228
  const retryConfig = getRetryConfig(entityName, config2);
15095
- await mapper.processEntityWithEvents(configPath, entityConfig, retryConfig, signal, {
15096
- onEntityStart: (payload) => this.safeEmit("entityStart", {
15097
- ...payload,
15098
- waveIndex: wave.wave
15099
- }),
15100
- onEntityComplete: (payload) => {
15101
- this.entitiesCompleted++;
15102
- this.safeEmit("entityComplete", payload);
15229
+ await mapper.processEntityWithEvents(
15230
+ configPath,
15231
+ entityConfig,
15232
+ retryConfig,
15233
+ signal,
15234
+ {
15235
+ onEntityStart: (payload) => this.safeEmit("entityStart", {
15236
+ ...payload,
15237
+ waveIndex: wave.wave
15238
+ }),
15239
+ onEntityComplete: (payload) => {
15240
+ this.entitiesCompleted++;
15241
+ this.safeEmit("entityComplete", payload);
15242
+ },
15243
+ onRowSuccess: this.eventOptions.emitRowEvents ? (payload) => this.safeEmit("rowSuccess", payload) : void 0,
15244
+ onRowFailure: this.eventOptions.emitRowEvents ? (payload) => this.safeEmit("rowFailure", payload) : void 0
15103
15245
  },
15104
- onRowSuccess: this.eventOptions.emitRowEvents ? (payload) => this.safeEmit("rowSuccess", payload) : void 0,
15105
- onRowFailure: this.eventOptions.emitRowEvents ? (payload) => this.safeEmit("rowFailure", payload) : void 0
15106
- });
15246
+ outputStore
15247
+ );
15107
15248
  } catch (error48) {
15108
15249
  const message = error48 instanceof Error ? error48.message : String(error48);
15109
15250
  logger.warn(`Warning: Could not process ${configPath}: ${message}`);