@forzalabs/remora 1.1.6 → 1.1.8

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/index.js CHANGED
@@ -13306,14 +13306,27 @@ var Logger = class {
13306
13306
  constructor() {
13307
13307
  this.setLevel = (level) => this._level = level;
13308
13308
  this.enableFileLogging = (folder, file) => {
13309
+ this._fileLoggingFolder = folder;
13310
+ this._fileLoggingFile = file;
13309
13311
  FileLogService_default.enable(folder, file);
13310
13312
  console.log(`Enabled file logger.`);
13311
13313
  };
13314
+ this.getConfig = () => ({
13315
+ level: this._level,
13316
+ fileLogging: FileLogService_default._enabled ? { folder: this._fileLoggingFolder, file: this._fileLoggingFile } : void 0
13317
+ });
13318
+ this.initFromConfig = (config) => {
13319
+ this._level = config.level;
13320
+ if (config.fileLogging)
13321
+ this.enableFileLogging(config.fileLogging.folder, config.fileLogging.file);
13322
+ };
13312
13323
  this.log = (message, level) => {
13313
13324
  const myLevel = level ?? this._level;
13314
13325
  if (myLevel !== "debug") return;
13315
- console.log(import_chalk.default.cyanBright("DEBUG"), message);
13316
- FileLogService_default.write("DEBUG", String(message));
13326
+ if (FileLogService_default._enabled)
13327
+ FileLogService_default.write("DEBUG", String(message));
13328
+ else
13329
+ console.log(import_chalk.default.cyanBright("DEBUG"), message);
13317
13330
  };
13318
13331
  this.info = (message) => {
13319
13332
  console.info(message);
@@ -13448,7 +13461,7 @@ var import_promises = __toESM(require("fs/promises"), 1);
13448
13461
 
13449
13462
  // ../../packages/constants/src/Constants.ts
13450
13463
  var CONSTANTS = {
13451
- cliVersion: "1.1.6",
13464
+ cliVersion: "1.1.8",
13452
13465
  backendVersion: 1,
13453
13466
  backendPort: 5088,
13454
13467
  workerVersion: 2,
@@ -13792,6 +13805,27 @@ var ValidatorClass = class {
13792
13805
  if (consumer.options) {
13793
13806
  if (Algo_default.hasVal(consumer.options.distinct) && Algo_default.hasVal(consumer.options.distinctOn))
13794
13807
  errors.push(`Can't specify a "distinct" and a "distinctOn" clause on the same consumer (${consumer.name}); use one or the other.`);
13808
+ if (Algo_default.hasVal(consumer.options.pivot)) {
13809
+ if (Algo_default.hasVal(consumer.options.distinct) || Algo_default.hasVal(consumer.options.distinctOn))
13810
+ errors.push(`Can't specify "pivot" together with "distinct" or "distinctOn" on the same consumer (${consumer.name}).`);
13811
+ const { pivot } = consumer.options;
13812
+ if (!pivot.rowKeys || pivot.rowKeys.length === 0)
13813
+ errors.push(`Pivot option requires at least one "rowKeys" field (${consumer.name}).`);
13814
+ if (!pivot.pivotColumn)
13815
+ errors.push(`Pivot option requires a "pivotColumn" (${consumer.name}).`);
13816
+ if (!pivot.valueColumn)
13817
+ errors.push(`Pivot option requires a "valueColumn" (${consumer.name}).`);
13818
+ if (!pivot.aggregation)
13819
+ errors.push(`Pivot option requires an "aggregation" function (${consumer.name}).`);
13820
+ const validAggregations = ["sum", "count", "avg", "min", "max"];
13821
+ if (pivot.aggregation && !validAggregations.includes(pivot.aggregation))
13822
+ errors.push(`Invalid pivot aggregation "${pivot.aggregation}" in consumer "${consumer.name}". Valid values: ${validAggregations.join(", ")}`);
13823
+ const allFieldKeys = consumer.fields.map((x) => x.alias ?? x.key);
13824
+ const pivotFields = [...pivot.rowKeys ?? [], pivot.pivotColumn, pivot.valueColumn].filter(Boolean);
13825
+ const missingFields = pivotFields.filter((f) => !allFieldKeys.includes(f));
13826
+ if (missingFields.length > 0)
13827
+ errors.push(`Pivot references field(s) "${missingFields.join(", ")}" that are not present in the consumer "${consumer.name}".`);
13828
+ }
13795
13829
  }
13796
13830
  } catch (e) {
13797
13831
  if (errors.length === 0)
@@ -18646,85 +18680,73 @@ var DeveloperEngineClass = class {
18646
18680
  const record = {};
18647
18681
  for (const dimension of dimensions) {
18648
18682
  if (dimension.sourceFilename) continue;
18649
- record[dimension.name] = this.generateMockValue(dimension, i);
18683
+ const key = dimension.alias || dimension.name;
18684
+ record[key] = this.generateMockValue(dimension, i);
18650
18685
  }
18651
18686
  records.push(record);
18652
18687
  }
18653
18688
  return records;
18654
18689
  };
18655
18690
  this.generateMockValue = (dimension, index) => {
18656
- const { name, type } = dimension;
18691
+ const { name, type, format: format2, pk } = dimension;
18657
18692
  const nameLower = name.toLowerCase();
18658
- if (this.matchesPattern(nameLower, ["id", "identifier", "key", "pk"])) {
18693
+ if (pk) {
18694
+ if (type === "number") return index + 1;
18659
18695
  return `${index + 1}`;
18660
18696
  }
18661
- if (this.matchesPattern(nameLower, ["first_name", "firstname", "fname", "given_name"])) {
18662
- return this.pickRandom(["John", "Jane", "Michael", "Sarah", "David", "Emily", "Robert", "Lisa", "James", "Mary"]);
18663
- }
18664
- if (this.matchesPattern(nameLower, ["last_name", "lastname", "lname", "surname", "family_name"])) {
18665
- return this.pickRandom(["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Martinez", "Wilson"]);
18666
- }
18667
- if (this.matchesPattern(nameLower, ["name", "full_name", "fullname"])) {
18668
- const firstNames = ["John", "Jane", "Michael", "Sarah", "David", "Emily"];
18669
- const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia"];
18670
- return `${this.pickRandom(firstNames)} ${this.pickRandom(lastNames)}`;
18671
- }
18672
- if (this.matchesPattern(nameLower, ["email", "mail"])) {
18673
- return `user${index + 1}@example.com`;
18674
- }
18675
- if (this.matchesPattern(nameLower, ["phone", "telephone", "mobile", "cell"])) {
18676
- return `555-${String(Math.floor(Math.random() * 900) + 100).padStart(3, "0")}-${String(Math.floor(Math.random() * 9e3) + 1e3).padStart(4, "0")}`;
18677
- }
18678
- if (this.matchesPattern(nameLower, ["address", "street", "addr"])) {
18679
- const streets = ["Main St", "Oak Ave", "Elm Dr", "Pine Rd", "Maple Ln", "Cedar Blvd"];
18680
- return `${Math.floor(Math.random() * 9999) + 1} ${this.pickRandom(streets)}`;
18681
- }
18682
- if (this.matchesPattern(nameLower, ["city", "town"])) {
18683
- return this.pickRandom(["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", "San Antonio", "San Diego"]);
18684
- }
18685
- if (this.matchesPattern(nameLower, ["state", "province"])) {
18686
- return this.pickRandom(["CA", "TX", "FL", "NY", "PA", "IL", "OH", "GA", "NC", "MI"]);
18687
- }
18688
- if (this.matchesPattern(nameLower, ["zip", "postal", "zipcode"])) {
18689
- return String(Math.floor(Math.random() * 9e4) + 1e4);
18690
- }
18691
- if (this.matchesPattern(nameLower, ["country"])) {
18692
- return this.pickRandom(["USA", "Canada", "UK", "Germany", "France", "Australia"]);
18693
- }
18694
- if (this.matchesPattern(nameLower, ["age", "years"])) {
18695
- return Math.floor(Math.random() * 80) + 18;
18696
- }
18697
- if (this.matchesPattern(nameLower, ["sex", "gender"])) {
18698
- return this.pickRandom(["M", "F", "Male", "Female"]);
18699
- }
18700
- if (this.matchesPattern(nameLower, ["birth", "dob", "birthdate"])) {
18701
- const year = Math.floor(Math.random() * 60) + 1940;
18702
- const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, "0");
18703
- const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, "0");
18704
- return `${year}-${month}-${day}`;
18705
- }
18706
- if (this.matchesPattern(nameLower, ["date", "created", "updated", "timestamp"])) {
18707
- const year = Math.floor(Math.random() * 5) + 2020;
18708
- const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, "0");
18709
- const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, "0");
18710
- return `${year}-${month}-${day}`;
18711
- }
18712
- if (this.matchesPattern(nameLower, ["amount", "price", "cost", "total", "balance"])) {
18713
- return (Math.random() * 1e3).toFixed(2);
18697
+ const contextual = this.generateContextualValue(nameLower, type, index);
18698
+ if (contextual !== void 0) return contextual;
18699
+ return this.generateValueByType(type, index, format2);
18700
+ };
18701
+ this.generateContextualValue = (nameLower, type, index) => {
18702
+ if (type === "string") {
18703
+ if (this.matchesPattern(nameLower, ["id", "identifier", "key", "pk"]))
18704
+ return `${index + 1}`;
18705
+ if (this.matchesPattern(nameLower, ["first_name", "firstname", "fname", "given_name"]))
18706
+ return this.pickRandom(["John", "Jane", "Michael", "Sarah", "David", "Emily", "Robert", "Lisa", "James", "Mary"]);
18707
+ if (this.matchesPattern(nameLower, ["last_name", "lastname", "lname", "surname", "family_name"]))
18708
+ return this.pickRandom(["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Martinez", "Wilson"]);
18709
+ if (this.matchesPattern(nameLower, ["name", "full_name", "fullname"])) {
18710
+ const firstNames = ["John", "Jane", "Michael", "Sarah", "David", "Emily"];
18711
+ const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia"];
18712
+ return `${this.pickRandom(firstNames)} ${this.pickRandom(lastNames)}`;
18713
+ }
18714
+ if (this.matchesPattern(nameLower, ["email", "mail"]))
18715
+ return `user${index + 1}@example.com`;
18716
+ if (this.matchesPattern(nameLower, ["phone", "telephone", "mobile", "cell"]))
18717
+ return `555-${String(Math.floor(Math.random() * 900) + 100).padStart(3, "0")}-${String(Math.floor(Math.random() * 9e3) + 1e3).padStart(4, "0")}`;
18718
+ if (this.matchesPattern(nameLower, ["address", "street", "addr"])) {
18719
+ const streets = ["Main St", "Oak Ave", "Elm Dr", "Pine Rd", "Maple Ln", "Cedar Blvd"];
18720
+ return `${Math.floor(Math.random() * 9999) + 1} ${this.pickRandom(streets)}`;
18721
+ }
18722
+ if (this.matchesPattern(nameLower, ["city", "town"]))
18723
+ return this.pickRandom(["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", "San Antonio", "San Diego"]);
18724
+ if (this.matchesPattern(nameLower, ["state", "province"]))
18725
+ return this.pickRandom(["CA", "TX", "FL", "NY", "PA", "IL", "OH", "GA", "NC", "MI"]);
18726
+ if (this.matchesPattern(nameLower, ["zip", "postal", "zipcode"]))
18727
+ return String(Math.floor(Math.random() * 9e4) + 1e4);
18728
+ if (this.matchesPattern(nameLower, ["country"]))
18729
+ return this.pickRandom(["USA", "Canada", "UK", "Germany", "France", "Australia"]);
18730
+ if (this.matchesPattern(nameLower, ["sex", "gender"]))
18731
+ return this.pickRandom(["M", "F", "Male", "Female"]);
18732
+ if (this.matchesPattern(nameLower, ["status"]))
18733
+ return this.pickRandom(["active", "inactive", "pending", "completed", "cancelled"]);
18734
+ if (this.matchesPattern(nameLower, ["type", "category"]))
18735
+ return this.pickRandom(["TypeA", "TypeB", "TypeC", "TypeD"]);
18736
+ if (this.matchesPattern(nameLower, ["description", "desc", "notes", "comment"]))
18737
+ return `Sample description for record ${index + 1}`;
18738
+ }
18739
+ if (type === "number") {
18740
+ if (this.matchesPattern(nameLower, ["age", "years"]))
18741
+ return Math.floor(Math.random() * 80) + 18;
18742
+ if (this.matchesPattern(nameLower, ["amount", "price", "cost", "total", "balance"]))
18743
+ return parseFloat((Math.random() * 1e3).toFixed(2));
18744
+ if (this.matchesPattern(nameLower, ["quantity", "count", "qty"]))
18745
+ return Math.floor(Math.random() * 100) + 1;
18746
+ if (this.matchesPattern(nameLower, ["id", "identifier", "key", "pk"]))
18747
+ return index + 1;
18714
18748
  }
18715
- if (this.matchesPattern(nameLower, ["quantity", "count", "qty"])) {
18716
- return Math.floor(Math.random() * 100) + 1;
18717
- }
18718
- if (this.matchesPattern(nameLower, ["status"])) {
18719
- return this.pickRandom(["active", "inactive", "pending", "completed", "cancelled"]);
18720
- }
18721
- if (this.matchesPattern(nameLower, ["type", "category"])) {
18722
- return this.pickRandom(["TypeA", "TypeB", "TypeC", "TypeD"]);
18723
- }
18724
- if (this.matchesPattern(nameLower, ["description", "desc", "notes", "comment"])) {
18725
- return `Sample description for record ${index + 1}`;
18726
- }
18727
- return this.generateValueByType(type, index);
18749
+ return void 0;
18728
18750
  };
18729
18751
  this.matchesPattern = (fieldName, patterns) => {
18730
18752
  return patterns.some((pattern) => fieldName.includes(pattern));
@@ -18732,7 +18754,7 @@ var DeveloperEngineClass = class {
18732
18754
  this.pickRandom = (arr) => {
18733
18755
  return arr[Math.floor(Math.random() * arr.length)];
18734
18756
  };
18735
- this.generateValueByType = (type, index) => {
18757
+ this.generateValueByType = (type, index, format2) => {
18736
18758
  switch (type) {
18737
18759
  case "string":
18738
18760
  return `value_${index + 1}`;
@@ -18742,9 +18764,14 @@ var DeveloperEngineClass = class {
18742
18764
  return Math.random() > 0.5;
18743
18765
  case "datetime": {
18744
18766
  const year = Math.floor(Math.random() * 5) + 2020;
18745
- const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, "0");
18746
- const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, "0");
18747
- return `${year}-${month}-${day}`;
18767
+ const month = Math.floor(Math.random() * 12) + 1;
18768
+ const day = Math.floor(Math.random() * 28) + 1;
18769
+ const hour = Math.floor(Math.random() * 24);
18770
+ const minute = Math.floor(Math.random() * 60);
18771
+ const second = Math.floor(Math.random() * 60);
18772
+ const date = (0, import_dayjs2.default)(new Date(year, month - 1, day, hour, minute, second));
18773
+ if (format2) return date.format(format2);
18774
+ return date.format("YYYY-MM-DD");
18748
18775
  }
18749
18776
  default:
18750
18777
  return `value_${index + 1}`;
@@ -18874,7 +18901,7 @@ var ConsumerManagerClass = class {
18874
18901
  column = columns.find((x) => x.owner === field.from && x.nameInProducer === field.key);
18875
18902
  } else if (consumer.producers.length === 1 && !field.from) {
18876
18903
  column = columns.find((x) => x.nameInProducer === field.key);
18877
- } else if (!field.fixed) {
18904
+ } else if (!field.fixed && !field.copyFrom) {
18878
18905
  const matches = columns.filter((x) => x.nameInProducer === field.key);
18879
18906
  Affirm_default(matches.length > 0, `Consumer "${consumer.name}" misconfiguration: the field "${field.key}" is not found in any of the included producers (${consumer.producers.map((x) => x.name).join(", ")})`);
18880
18907
  if (matches.length === 1) {
@@ -18885,7 +18912,7 @@ var ConsumerManagerClass = class {
18885
18912
  column = matches[0];
18886
18913
  }
18887
18914
  if (!column) {
18888
- if (field.fixed === true && Algo_default.hasVal(field.default)) {
18915
+ if (field.fixed === true && Algo_default.hasVal(field.default) || field.copyFrom) {
18889
18916
  column = {
18890
18917
  aliasInProducer: field.key,
18891
18918
  nameInProducer: field.alias ?? field.key,
@@ -18927,7 +18954,7 @@ var ConsumerManagerClass = class {
18927
18954
  this.getOutputShape = (consumer) => {
18928
18955
  Affirm_default(consumer, `Invalid consumer`);
18929
18956
  const compiled = this.compile(consumer);
18930
- const outDimensions = compiled.map((x) => ({
18957
+ let outDimensions = compiled.map((x) => ({
18931
18958
  name: x.consumerAlias ?? x.consumerKey,
18932
18959
  type: x.dimension?.type,
18933
18960
  classification: x.dimension?.classification,
@@ -18935,6 +18962,20 @@ var ConsumerManagerClass = class {
18935
18962
  mask: ProducerManager_default.getMask(x.dimension),
18936
18963
  pk: x.dimension?.pk
18937
18964
  }));
18965
+ if (consumer.options?.pivot) {
18966
+ const { rowKeys, pivotValues, columnPrefix = "", valueColumn } = consumer.options.pivot;
18967
+ const rowDimensions = outDimensions.filter((x) => rowKeys.includes(x.name));
18968
+ const valueType = outDimensions.find((x) => x.name === valueColumn)?.type ?? "number";
18969
+ if (pivotValues && pivotValues.length > 0) {
18970
+ const pivotDimensions = pivotValues.map((pv) => ({
18971
+ name: columnPrefix + pv,
18972
+ type: valueType
18973
+ }));
18974
+ outDimensions = [...rowDimensions, ...pivotDimensions];
18975
+ } else {
18976
+ outDimensions = rowDimensions;
18977
+ }
18978
+ }
18938
18979
  return {
18939
18980
  _version: consumer._version,
18940
18981
  name: consumer.name,
@@ -20118,6 +20159,8 @@ var ConsumerExecutorClass = class {
20118
20159
  if (!dimension) {
20119
20160
  if (cField.fixed && Algo_default.hasVal(cField.default))
20120
20161
  record[fieldKey] = cField.default;
20162
+ else if (cField.copyFrom)
20163
+ record[fieldKey] = record[cField.copyFrom];
20121
20164
  else
20122
20165
  throw new Error(`The requested field "${cField.key}" from the consumer is not present in the underlying producer "${producer.name}" (${dimensions.map((x) => x.name).join(", ")})`);
20123
20166
  }
@@ -20240,6 +20283,113 @@ var ConsumerExecutorClass = class {
20240
20283
  await import_promises8.default.rename(tempWorkPath, datasetPath);
20241
20284
  return winners.size;
20242
20285
  };
20286
+ this.processPivot = async (consumer, datasetPath) => {
20287
+ const { pivot } = consumer.options;
20288
+ const { rowKeys, pivotColumn, valueColumn, aggregation, columnPrefix = "" } = pivot;
20289
+ const internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
20290
+ const internalFields = ConsumerManager_default.getExpandedFields(consumer);
20291
+ let pivotValues = pivot.pivotValues;
20292
+ if (!pivotValues) {
20293
+ pivotValues = [];
20294
+ const discoverySet = /* @__PURE__ */ new Set();
20295
+ const discoverReader = import_fs13.default.createReadStream(datasetPath);
20296
+ const discoverLineReader = import_readline7.default.createInterface({ input: discoverReader, crlfDelay: Infinity });
20297
+ for await (const line of discoverLineReader) {
20298
+ const record = this._parseLine(line, internalRecordFormat, internalFields);
20299
+ const val = String(record[pivotColumn] ?? "");
20300
+ if (!discoverySet.has(val)) {
20301
+ discoverySet.add(val);
20302
+ pivotValues.push(val);
20303
+ }
20304
+ }
20305
+ discoverLineReader.close();
20306
+ if (!discoverReader.destroyed) {
20307
+ await new Promise((resolve) => {
20308
+ discoverReader.once("close", resolve);
20309
+ discoverReader.destroy();
20310
+ });
20311
+ }
20312
+ }
20313
+ const groups = /* @__PURE__ */ new Map();
20314
+ const reader = import_fs13.default.createReadStream(datasetPath);
20315
+ const lineReader = import_readline7.default.createInterface({ input: reader, crlfDelay: Infinity });
20316
+ for await (const line of lineReader) {
20317
+ const record = this._parseLine(line, internalRecordFormat, internalFields);
20318
+ const compositeKey = rowKeys.map((k) => String(record[k] ?? "")).join("|");
20319
+ const pivotVal = String(record[pivotColumn] ?? "");
20320
+ const numericVal = Number(record[valueColumn]) || 0;
20321
+ if (!groups.has(compositeKey)) {
20322
+ const rowRecord = {};
20323
+ for (const k of rowKeys) rowRecord[k] = record[k];
20324
+ groups.set(compositeKey, { rowRecord, cells: /* @__PURE__ */ new Map() });
20325
+ }
20326
+ const group = groups.get(compositeKey);
20327
+ if (!group.cells.has(pivotVal)) {
20328
+ group.cells.set(pivotVal, { sum: 0, count: 0, min: Infinity, max: -Infinity });
20329
+ }
20330
+ const cell = group.cells.get(pivotVal);
20331
+ cell.sum += numericVal;
20332
+ cell.count++;
20333
+ cell.min = Math.min(cell.min, numericVal);
20334
+ cell.max = Math.max(cell.max, numericVal);
20335
+ }
20336
+ lineReader.close();
20337
+ const pivotedFields = [
20338
+ ...rowKeys.map((k) => ({ cField: { key: k }, finalKey: k })),
20339
+ ...pivotValues.map((pv) => ({ cField: { key: columnPrefix + pv }, finalKey: columnPrefix + pv }))
20340
+ ];
20341
+ const tempWorkPath = datasetPath + "_tmp";
20342
+ const writer = import_fs13.default.createWriteStream(tempWorkPath);
20343
+ let outputCount = 0;
20344
+ for (const { rowRecord, cells } of groups.values()) {
20345
+ const outputRecord = { ...rowRecord };
20346
+ for (const pv of pivotValues) {
20347
+ const colName = columnPrefix + pv;
20348
+ const cell = cells.get(pv);
20349
+ if (!cell) {
20350
+ outputRecord[colName] = 0;
20351
+ continue;
20352
+ }
20353
+ switch (aggregation) {
20354
+ case "sum":
20355
+ outputRecord[colName] = cell.sum;
20356
+ break;
20357
+ case "count":
20358
+ outputRecord[colName] = cell.count;
20359
+ break;
20360
+ case "avg":
20361
+ outputRecord[colName] = cell.count > 0 ? cell.sum / cell.count : 0;
20362
+ break;
20363
+ case "min":
20364
+ outputRecord[colName] = cell.min === Infinity ? 0 : cell.min;
20365
+ break;
20366
+ case "max":
20367
+ outputRecord[colName] = cell.max === -Infinity ? 0 : cell.max;
20368
+ break;
20369
+ }
20370
+ }
20371
+ const line = OutputExecutor_default.outputRecord(outputRecord, consumer, pivotedFields);
20372
+ writer.write(line + "\n");
20373
+ outputCount++;
20374
+ }
20375
+ await new Promise((resolve, reject) => {
20376
+ writer.on("close", resolve);
20377
+ writer.on("error", reject);
20378
+ writer.end();
20379
+ });
20380
+ if (!reader.destroyed) {
20381
+ await new Promise((resolve) => {
20382
+ reader.once("close", resolve);
20383
+ reader.destroy();
20384
+ });
20385
+ }
20386
+ await import_promises8.default.unlink(datasetPath);
20387
+ await import_promises8.default.rename(tempWorkPath, datasetPath);
20388
+ return outputCount;
20389
+ };
20390
+ this._parseLine = (line, format2, fields) => {
20391
+ return format2 === "CSV" || format2 === "TXT" ? LineParser_default._internalParseCSV(line, fields) : LineParser_default._internalParseJSON(line);
20392
+ };
20243
20393
  /**
20244
20394
  * Determines if the new record should replace the existing record based on the resolution strategy
20245
20395
  */
@@ -20606,7 +20756,8 @@ var ExecutorOrchestratorClass = class {
20606
20756
  prodDimensions,
20607
20757
  workerId,
20608
20758
  scope,
20609
- options
20759
+ options,
20760
+ loggerConfig: Logger_default.getConfig()
20610
20761
  };
20611
20762
  _progress.register((currentWorkerIndex + 1).toString(), prod.name, fileIndex, totalFiles);
20612
20763
  scope.workersId.push(workerId);
@@ -20637,6 +20788,12 @@ var ExecutorOrchestratorClass = class {
20637
20788
  postOperation.totalOutputCount = unifiedOutputCount;
20638
20789
  }
20639
20790
  }
20791
+ if (consumer.options?.pivot) {
20792
+ counter = performance.now();
20793
+ const unifiedOutputCount = await ConsumerExecutor_default.processPivot(consumer, ExecutorScope_default2.getMainPath(scope));
20794
+ tracker.measure("process-pivot:main", performance.now() - counter);
20795
+ postOperation.totalOutputCount = unifiedOutputCount;
20796
+ }
20640
20797
  counter = performance.now();
20641
20798
  Logger_default.log(`Consumer "${consumer.name}": exporting results`);
20642
20799
  const exportRes = await OutputExecutor_default.exportResult(consumer, ConsumerManager_default.getExpandedFields(consumer), scope);
@@ -166,6 +166,10 @@
166
166
  "fixed": {
167
167
  "type": "boolean",
168
168
  "description": "If set, \"default\" must have a value. This field is not searched in the underlying dataset, but is a fixed value set by the \"default\" prop."
169
+ },
170
+ "copyFrom": {
171
+ "type": "string",
172
+ "description": "If set, this field will be added as new to the consumer dataset and will be a copy of the specified field. Use the alias if set, otherwise the key. The source field should come before this field in the fields list."
169
173
  }
170
174
  },
171
175
  "required": [
@@ -408,6 +412,46 @@
408
412
  },
409
413
  "required": ["keys", "resolution"],
410
414
  "additionalProperties": false
415
+ },
416
+ "pivot": {
417
+ "type": "object",
418
+ "description": "Performs a pivot operation that transforms row values into columns. Groups data by the specified row keys, takes distinct values from the pivot column and creates a new column for each, aggregating the value column with the specified function. Cannot be used together with 'distinct' or 'distinctOn'.",
419
+ "properties": {
420
+ "rowKeys": {
421
+ "type": "array",
422
+ "items": {
423
+ "type": "string"
424
+ },
425
+ "minItems": 1,
426
+ "description": "The field(s) that identify a row in the pivoted output (the GROUP BY keys). Use the 'alias' if specified."
427
+ },
428
+ "pivotColumn": {
429
+ "type": "string",
430
+ "description": "The field whose distinct values become new columns in the output."
431
+ },
432
+ "valueColumn": {
433
+ "type": "string",
434
+ "description": "The field whose values are aggregated into each pivot cell."
435
+ },
436
+ "aggregation": {
437
+ "type": "string",
438
+ "enum": ["sum", "count", "avg", "min", "max"],
439
+ "description": "The aggregation function to apply when combining values."
440
+ },
441
+ "pivotValues": {
442
+ "type": "array",
443
+ "items": {
444
+ "type": "string"
445
+ },
446
+ "description": "If provided, only these values from the pivot column will be used as output columns. This avoids a discovery pass over the data and makes the output shape statically known. If omitted, the distinct values are discovered automatically."
447
+ },
448
+ "columnPrefix": {
449
+ "type": "string",
450
+ "description": "Optional prefix for the generated pivot column names (e.g. 'revenue_' produces 'revenue_East', 'revenue_West')."
451
+ }
452
+ },
453
+ "required": ["rowKeys", "pivotColumn", "valueColumn", "aggregation"],
454
+ "additionalProperties": false
411
455
  }
412
456
  },
413
457
  "additionalProperties": false
@@ -519,6 +563,10 @@
519
563
  "fixed": {
520
564
  "type": "boolean",
521
565
  "description": "If set, \"default\" must have a value. This field is not searched in the underlying dataset, but is a fixed value set by the \"default\" prop."
566
+ },
567
+ "copyFrom": {
568
+ "type": "string",
569
+ "description": "If set, this field will be added as new to the consumer dataset and will be a copy of the specified field. Use the alias if set, otherwise the key. The source field should come before this field in the fields list."
522
570
  }
523
571
  },
524
572
  "required": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forzalabs/remora",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "A powerful CLI tool for seamless data translation.",
5
5
  "main": "index.js",
6
6
  "private": false,
@@ -13300,14 +13300,27 @@ var Logger = class {
13300
13300
  constructor() {
13301
13301
  this.setLevel = (level) => this._level = level;
13302
13302
  this.enableFileLogging = (folder, file) => {
13303
+ this._fileLoggingFolder = folder;
13304
+ this._fileLoggingFile = file;
13303
13305
  FileLogService_default.enable(folder, file);
13304
13306
  console.log(`Enabled file logger.`);
13305
13307
  };
13308
+ this.getConfig = () => ({
13309
+ level: this._level,
13310
+ fileLogging: FileLogService_default._enabled ? { folder: this._fileLoggingFolder, file: this._fileLoggingFile } : void 0
13311
+ });
13312
+ this.initFromConfig = (config) => {
13313
+ this._level = config.level;
13314
+ if (config.fileLogging)
13315
+ this.enableFileLogging(config.fileLogging.folder, config.fileLogging.file);
13316
+ };
13306
13317
  this.log = (message, level) => {
13307
13318
  const myLevel = level ?? this._level;
13308
13319
  if (myLevel !== "debug") return;
13309
- console.log(import_chalk.default.cyanBright("DEBUG"), message);
13310
- FileLogService_default.write("DEBUG", String(message));
13320
+ if (FileLogService_default._enabled)
13321
+ FileLogService_default.write("DEBUG", String(message));
13322
+ else
13323
+ console.log(import_chalk.default.cyanBright("DEBUG"), message);
13311
13324
  };
13312
13325
  this.info = (message) => {
13313
13326
  console.info(message);
@@ -13442,7 +13455,7 @@ var import_promises = __toESM(require("fs/promises"), 1);
13442
13455
 
13443
13456
  // ../../packages/constants/src/Constants.ts
13444
13457
  var CONSTANTS = {
13445
- cliVersion: "1.1.6",
13458
+ cliVersion: "1.1.8",
13446
13459
  backendVersion: 1,
13447
13460
  backendPort: 5088,
13448
13461
  workerVersion: 2,
@@ -13786,6 +13799,27 @@ var ValidatorClass = class {
13786
13799
  if (consumer.options) {
13787
13800
  if (Algo_default.hasVal(consumer.options.distinct) && Algo_default.hasVal(consumer.options.distinctOn))
13788
13801
  errors.push(`Can't specify a "distinct" and a "distinctOn" clause on the same consumer (${consumer.name}); use one or the other.`);
13802
+ if (Algo_default.hasVal(consumer.options.pivot)) {
13803
+ if (Algo_default.hasVal(consumer.options.distinct) || Algo_default.hasVal(consumer.options.distinctOn))
13804
+ errors.push(`Can't specify "pivot" together with "distinct" or "distinctOn" on the same consumer (${consumer.name}).`);
13805
+ const { pivot } = consumer.options;
13806
+ if (!pivot.rowKeys || pivot.rowKeys.length === 0)
13807
+ errors.push(`Pivot option requires at least one "rowKeys" field (${consumer.name}).`);
13808
+ if (!pivot.pivotColumn)
13809
+ errors.push(`Pivot option requires a "pivotColumn" (${consumer.name}).`);
13810
+ if (!pivot.valueColumn)
13811
+ errors.push(`Pivot option requires a "valueColumn" (${consumer.name}).`);
13812
+ if (!pivot.aggregation)
13813
+ errors.push(`Pivot option requires an "aggregation" function (${consumer.name}).`);
13814
+ const validAggregations = ["sum", "count", "avg", "min", "max"];
13815
+ if (pivot.aggregation && !validAggregations.includes(pivot.aggregation))
13816
+ errors.push(`Invalid pivot aggregation "${pivot.aggregation}" in consumer "${consumer.name}". Valid values: ${validAggregations.join(", ")}`);
13817
+ const allFieldKeys = consumer.fields.map((x) => x.alias ?? x.key);
13818
+ const pivotFields = [...pivot.rowKeys ?? [], pivot.pivotColumn, pivot.valueColumn].filter(Boolean);
13819
+ const missingFields = pivotFields.filter((f) => !allFieldKeys.includes(f));
13820
+ if (missingFields.length > 0)
13821
+ errors.push(`Pivot references field(s) "${missingFields.join(", ")}" that are not present in the consumer "${consumer.name}".`);
13822
+ }
13789
13823
  }
13790
13824
  } catch (e) {
13791
13825
  if (errors.length === 0)
@@ -17988,85 +18022,73 @@ var DeveloperEngineClass = class {
17988
18022
  const record = {};
17989
18023
  for (const dimension of dimensions) {
17990
18024
  if (dimension.sourceFilename) continue;
17991
- record[dimension.name] = this.generateMockValue(dimension, i);
18025
+ const key = dimension.alias || dimension.name;
18026
+ record[key] = this.generateMockValue(dimension, i);
17992
18027
  }
17993
18028
  records.push(record);
17994
18029
  }
17995
18030
  return records;
17996
18031
  };
17997
18032
  this.generateMockValue = (dimension, index) => {
17998
- const { name, type } = dimension;
18033
+ const { name, type, format: format2, pk } = dimension;
17999
18034
  const nameLower = name.toLowerCase();
18000
- if (this.matchesPattern(nameLower, ["id", "identifier", "key", "pk"])) {
18035
+ if (pk) {
18036
+ if (type === "number") return index + 1;
18001
18037
  return `${index + 1}`;
18002
18038
  }
18003
- if (this.matchesPattern(nameLower, ["first_name", "firstname", "fname", "given_name"])) {
18004
- return this.pickRandom(["John", "Jane", "Michael", "Sarah", "David", "Emily", "Robert", "Lisa", "James", "Mary"]);
18005
- }
18006
- if (this.matchesPattern(nameLower, ["last_name", "lastname", "lname", "surname", "family_name"])) {
18007
- return this.pickRandom(["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Martinez", "Wilson"]);
18008
- }
18009
- if (this.matchesPattern(nameLower, ["name", "full_name", "fullname"])) {
18010
- const firstNames = ["John", "Jane", "Michael", "Sarah", "David", "Emily"];
18011
- const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia"];
18012
- return `${this.pickRandom(firstNames)} ${this.pickRandom(lastNames)}`;
18013
- }
18014
- if (this.matchesPattern(nameLower, ["email", "mail"])) {
18015
- return `user${index + 1}@example.com`;
18016
- }
18017
- if (this.matchesPattern(nameLower, ["phone", "telephone", "mobile", "cell"])) {
18018
- return `555-${String(Math.floor(Math.random() * 900) + 100).padStart(3, "0")}-${String(Math.floor(Math.random() * 9e3) + 1e3).padStart(4, "0")}`;
18019
- }
18020
- if (this.matchesPattern(nameLower, ["address", "street", "addr"])) {
18021
- const streets = ["Main St", "Oak Ave", "Elm Dr", "Pine Rd", "Maple Ln", "Cedar Blvd"];
18022
- return `${Math.floor(Math.random() * 9999) + 1} ${this.pickRandom(streets)}`;
18023
- }
18024
- if (this.matchesPattern(nameLower, ["city", "town"])) {
18025
- return this.pickRandom(["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", "San Antonio", "San Diego"]);
18026
- }
18027
- if (this.matchesPattern(nameLower, ["state", "province"])) {
18028
- return this.pickRandom(["CA", "TX", "FL", "NY", "PA", "IL", "OH", "GA", "NC", "MI"]);
18029
- }
18030
- if (this.matchesPattern(nameLower, ["zip", "postal", "zipcode"])) {
18031
- return String(Math.floor(Math.random() * 9e4) + 1e4);
18032
- }
18033
- if (this.matchesPattern(nameLower, ["country"])) {
18034
- return this.pickRandom(["USA", "Canada", "UK", "Germany", "France", "Australia"]);
18035
- }
18036
- if (this.matchesPattern(nameLower, ["age", "years"])) {
18037
- return Math.floor(Math.random() * 80) + 18;
18038
- }
18039
- if (this.matchesPattern(nameLower, ["sex", "gender"])) {
18040
- return this.pickRandom(["M", "F", "Male", "Female"]);
18041
- }
18042
- if (this.matchesPattern(nameLower, ["birth", "dob", "birthdate"])) {
18043
- const year = Math.floor(Math.random() * 60) + 1940;
18044
- const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, "0");
18045
- const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, "0");
18046
- return `${year}-${month}-${day}`;
18047
- }
18048
- if (this.matchesPattern(nameLower, ["date", "created", "updated", "timestamp"])) {
18049
- const year = Math.floor(Math.random() * 5) + 2020;
18050
- const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, "0");
18051
- const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, "0");
18052
- return `${year}-${month}-${day}`;
18053
- }
18054
- if (this.matchesPattern(nameLower, ["amount", "price", "cost", "total", "balance"])) {
18055
- return (Math.random() * 1e3).toFixed(2);
18039
+ const contextual = this.generateContextualValue(nameLower, type, index);
18040
+ if (contextual !== void 0) return contextual;
18041
+ return this.generateValueByType(type, index, format2);
18042
+ };
18043
+ this.generateContextualValue = (nameLower, type, index) => {
18044
+ if (type === "string") {
18045
+ if (this.matchesPattern(nameLower, ["id", "identifier", "key", "pk"]))
18046
+ return `${index + 1}`;
18047
+ if (this.matchesPattern(nameLower, ["first_name", "firstname", "fname", "given_name"]))
18048
+ return this.pickRandom(["John", "Jane", "Michael", "Sarah", "David", "Emily", "Robert", "Lisa", "James", "Mary"]);
18049
+ if (this.matchesPattern(nameLower, ["last_name", "lastname", "lname", "surname", "family_name"]))
18050
+ return this.pickRandom(["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Martinez", "Wilson"]);
18051
+ if (this.matchesPattern(nameLower, ["name", "full_name", "fullname"])) {
18052
+ const firstNames = ["John", "Jane", "Michael", "Sarah", "David", "Emily"];
18053
+ const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia"];
18054
+ return `${this.pickRandom(firstNames)} ${this.pickRandom(lastNames)}`;
18055
+ }
18056
+ if (this.matchesPattern(nameLower, ["email", "mail"]))
18057
+ return `user${index + 1}@example.com`;
18058
+ if (this.matchesPattern(nameLower, ["phone", "telephone", "mobile", "cell"]))
18059
+ return `555-${String(Math.floor(Math.random() * 900) + 100).padStart(3, "0")}-${String(Math.floor(Math.random() * 9e3) + 1e3).padStart(4, "0")}`;
18060
+ if (this.matchesPattern(nameLower, ["address", "street", "addr"])) {
18061
+ const streets = ["Main St", "Oak Ave", "Elm Dr", "Pine Rd", "Maple Ln", "Cedar Blvd"];
18062
+ return `${Math.floor(Math.random() * 9999) + 1} ${this.pickRandom(streets)}`;
18063
+ }
18064
+ if (this.matchesPattern(nameLower, ["city", "town"]))
18065
+ return this.pickRandom(["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", "San Antonio", "San Diego"]);
18066
+ if (this.matchesPattern(nameLower, ["state", "province"]))
18067
+ return this.pickRandom(["CA", "TX", "FL", "NY", "PA", "IL", "OH", "GA", "NC", "MI"]);
18068
+ if (this.matchesPattern(nameLower, ["zip", "postal", "zipcode"]))
18069
+ return String(Math.floor(Math.random() * 9e4) + 1e4);
18070
+ if (this.matchesPattern(nameLower, ["country"]))
18071
+ return this.pickRandom(["USA", "Canada", "UK", "Germany", "France", "Australia"]);
18072
+ if (this.matchesPattern(nameLower, ["sex", "gender"]))
18073
+ return this.pickRandom(["M", "F", "Male", "Female"]);
18074
+ if (this.matchesPattern(nameLower, ["status"]))
18075
+ return this.pickRandom(["active", "inactive", "pending", "completed", "cancelled"]);
18076
+ if (this.matchesPattern(nameLower, ["type", "category"]))
18077
+ return this.pickRandom(["TypeA", "TypeB", "TypeC", "TypeD"]);
18078
+ if (this.matchesPattern(nameLower, ["description", "desc", "notes", "comment"]))
18079
+ return `Sample description for record ${index + 1}`;
18080
+ }
18081
+ if (type === "number") {
18082
+ if (this.matchesPattern(nameLower, ["age", "years"]))
18083
+ return Math.floor(Math.random() * 80) + 18;
18084
+ if (this.matchesPattern(nameLower, ["amount", "price", "cost", "total", "balance"]))
18085
+ return parseFloat((Math.random() * 1e3).toFixed(2));
18086
+ if (this.matchesPattern(nameLower, ["quantity", "count", "qty"]))
18087
+ return Math.floor(Math.random() * 100) + 1;
18088
+ if (this.matchesPattern(nameLower, ["id", "identifier", "key", "pk"]))
18089
+ return index + 1;
18056
18090
  }
18057
- if (this.matchesPattern(nameLower, ["quantity", "count", "qty"])) {
18058
- return Math.floor(Math.random() * 100) + 1;
18059
- }
18060
- if (this.matchesPattern(nameLower, ["status"])) {
18061
- return this.pickRandom(["active", "inactive", "pending", "completed", "cancelled"]);
18062
- }
18063
- if (this.matchesPattern(nameLower, ["type", "category"])) {
18064
- return this.pickRandom(["TypeA", "TypeB", "TypeC", "TypeD"]);
18065
- }
18066
- if (this.matchesPattern(nameLower, ["description", "desc", "notes", "comment"])) {
18067
- return `Sample description for record ${index + 1}`;
18068
- }
18069
- return this.generateValueByType(type, index);
18091
+ return void 0;
18070
18092
  };
18071
18093
  this.matchesPattern = (fieldName, patterns) => {
18072
18094
  return patterns.some((pattern) => fieldName.includes(pattern));
@@ -18074,7 +18096,7 @@ var DeveloperEngineClass = class {
18074
18096
  this.pickRandom = (arr) => {
18075
18097
  return arr[Math.floor(Math.random() * arr.length)];
18076
18098
  };
18077
- this.generateValueByType = (type, index) => {
18099
+ this.generateValueByType = (type, index, format2) => {
18078
18100
  switch (type) {
18079
18101
  case "string":
18080
18102
  return `value_${index + 1}`;
@@ -18084,9 +18106,14 @@ var DeveloperEngineClass = class {
18084
18106
  return Math.random() > 0.5;
18085
18107
  case "datetime": {
18086
18108
  const year = Math.floor(Math.random() * 5) + 2020;
18087
- const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, "0");
18088
- const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, "0");
18089
- return `${year}-${month}-${day}`;
18109
+ const month = Math.floor(Math.random() * 12) + 1;
18110
+ const day = Math.floor(Math.random() * 28) + 1;
18111
+ const hour = Math.floor(Math.random() * 24);
18112
+ const minute = Math.floor(Math.random() * 60);
18113
+ const second = Math.floor(Math.random() * 60);
18114
+ const date = (0, import_dayjs2.default)(new Date(year, month - 1, day, hour, minute, second));
18115
+ if (format2) return date.format(format2);
18116
+ return date.format("YYYY-MM-DD");
18090
18117
  }
18091
18118
  default:
18092
18119
  return `value_${index + 1}`;
@@ -18216,7 +18243,7 @@ var ConsumerManagerClass = class {
18216
18243
  column = columns.find((x) => x.owner === field.from && x.nameInProducer === field.key);
18217
18244
  } else if (consumer.producers.length === 1 && !field.from) {
18218
18245
  column = columns.find((x) => x.nameInProducer === field.key);
18219
- } else if (!field.fixed) {
18246
+ } else if (!field.fixed && !field.copyFrom) {
18220
18247
  const matches = columns.filter((x) => x.nameInProducer === field.key);
18221
18248
  Affirm_default(matches.length > 0, `Consumer "${consumer.name}" misconfiguration: the field "${field.key}" is not found in any of the included producers (${consumer.producers.map((x) => x.name).join(", ")})`);
18222
18249
  if (matches.length === 1) {
@@ -18227,7 +18254,7 @@ var ConsumerManagerClass = class {
18227
18254
  column = matches[0];
18228
18255
  }
18229
18256
  if (!column) {
18230
- if (field.fixed === true && Algo_default.hasVal(field.default)) {
18257
+ if (field.fixed === true && Algo_default.hasVal(field.default) || field.copyFrom) {
18231
18258
  column = {
18232
18259
  aliasInProducer: field.key,
18233
18260
  nameInProducer: field.alias ?? field.key,
@@ -18269,7 +18296,7 @@ var ConsumerManagerClass = class {
18269
18296
  this.getOutputShape = (consumer) => {
18270
18297
  Affirm_default(consumer, `Invalid consumer`);
18271
18298
  const compiled = this.compile(consumer);
18272
- const outDimensions = compiled.map((x) => ({
18299
+ let outDimensions = compiled.map((x) => ({
18273
18300
  name: x.consumerAlias ?? x.consumerKey,
18274
18301
  type: x.dimension?.type,
18275
18302
  classification: x.dimension?.classification,
@@ -18277,6 +18304,20 @@ var ConsumerManagerClass = class {
18277
18304
  mask: ProducerManager_default.getMask(x.dimension),
18278
18305
  pk: x.dimension?.pk
18279
18306
  }));
18307
+ if (consumer.options?.pivot) {
18308
+ const { rowKeys, pivotValues, columnPrefix = "", valueColumn } = consumer.options.pivot;
18309
+ const rowDimensions = outDimensions.filter((x) => rowKeys.includes(x.name));
18310
+ const valueType = outDimensions.find((x) => x.name === valueColumn)?.type ?? "number";
18311
+ if (pivotValues && pivotValues.length > 0) {
18312
+ const pivotDimensions = pivotValues.map((pv) => ({
18313
+ name: columnPrefix + pv,
18314
+ type: valueType
18315
+ }));
18316
+ outDimensions = [...rowDimensions, ...pivotDimensions];
18317
+ } else {
18318
+ outDimensions = rowDimensions;
18319
+ }
18320
+ }
18280
18321
  return {
18281
18322
  _version: consumer._version,
18282
18323
  name: consumer.name,
@@ -19722,6 +19763,8 @@ var ConsumerExecutorClass = class {
19722
19763
  if (!dimension) {
19723
19764
  if (cField.fixed && Algo_default.hasVal(cField.default))
19724
19765
  record[fieldKey] = cField.default;
19766
+ else if (cField.copyFrom)
19767
+ record[fieldKey] = record[cField.copyFrom];
19725
19768
  else
19726
19769
  throw new Error(`The requested field "${cField.key}" from the consumer is not present in the underlying producer "${producer.name}" (${dimensions.map((x) => x.name).join(", ")})`);
19727
19770
  }
@@ -19844,6 +19887,113 @@ var ConsumerExecutorClass = class {
19844
19887
  await import_promises8.default.rename(tempWorkPath, datasetPath);
19845
19888
  return winners.size;
19846
19889
  };
19890
+ this.processPivot = async (consumer, datasetPath) => {
19891
+ const { pivot } = consumer.options;
19892
+ const { rowKeys, pivotColumn, valueColumn, aggregation, columnPrefix = "" } = pivot;
19893
+ const internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
19894
+ const internalFields = ConsumerManager_default.getExpandedFields(consumer);
19895
+ let pivotValues = pivot.pivotValues;
19896
+ if (!pivotValues) {
19897
+ pivotValues = [];
19898
+ const discoverySet = /* @__PURE__ */ new Set();
19899
+ const discoverReader = import_fs11.default.createReadStream(datasetPath);
19900
+ const discoverLineReader = import_readline7.default.createInterface({ input: discoverReader, crlfDelay: Infinity });
19901
+ for await (const line of discoverLineReader) {
19902
+ const record = this._parseLine(line, internalRecordFormat, internalFields);
19903
+ const val = String(record[pivotColumn] ?? "");
19904
+ if (!discoverySet.has(val)) {
19905
+ discoverySet.add(val);
19906
+ pivotValues.push(val);
19907
+ }
19908
+ }
19909
+ discoverLineReader.close();
19910
+ if (!discoverReader.destroyed) {
19911
+ await new Promise((resolve) => {
19912
+ discoverReader.once("close", resolve);
19913
+ discoverReader.destroy();
19914
+ });
19915
+ }
19916
+ }
19917
+ const groups = /* @__PURE__ */ new Map();
19918
+ const reader = import_fs11.default.createReadStream(datasetPath);
19919
+ const lineReader = import_readline7.default.createInterface({ input: reader, crlfDelay: Infinity });
19920
+ for await (const line of lineReader) {
19921
+ const record = this._parseLine(line, internalRecordFormat, internalFields);
19922
+ const compositeKey = rowKeys.map((k) => String(record[k] ?? "")).join("|");
19923
+ const pivotVal = String(record[pivotColumn] ?? "");
19924
+ const numericVal = Number(record[valueColumn]) || 0;
19925
+ if (!groups.has(compositeKey)) {
19926
+ const rowRecord = {};
19927
+ for (const k of rowKeys) rowRecord[k] = record[k];
19928
+ groups.set(compositeKey, { rowRecord, cells: /* @__PURE__ */ new Map() });
19929
+ }
19930
+ const group = groups.get(compositeKey);
19931
+ if (!group.cells.has(pivotVal)) {
19932
+ group.cells.set(pivotVal, { sum: 0, count: 0, min: Infinity, max: -Infinity });
19933
+ }
19934
+ const cell = group.cells.get(pivotVal);
19935
+ cell.sum += numericVal;
19936
+ cell.count++;
19937
+ cell.min = Math.min(cell.min, numericVal);
19938
+ cell.max = Math.max(cell.max, numericVal);
19939
+ }
19940
+ lineReader.close();
19941
+ const pivotedFields = [
19942
+ ...rowKeys.map((k) => ({ cField: { key: k }, finalKey: k })),
19943
+ ...pivotValues.map((pv) => ({ cField: { key: columnPrefix + pv }, finalKey: columnPrefix + pv }))
19944
+ ];
19945
+ const tempWorkPath = datasetPath + "_tmp";
19946
+ const writer = import_fs11.default.createWriteStream(tempWorkPath);
19947
+ let outputCount = 0;
19948
+ for (const { rowRecord, cells } of groups.values()) {
19949
+ const outputRecord = { ...rowRecord };
19950
+ for (const pv of pivotValues) {
19951
+ const colName = columnPrefix + pv;
19952
+ const cell = cells.get(pv);
19953
+ if (!cell) {
19954
+ outputRecord[colName] = 0;
19955
+ continue;
19956
+ }
19957
+ switch (aggregation) {
19958
+ case "sum":
19959
+ outputRecord[colName] = cell.sum;
19960
+ break;
19961
+ case "count":
19962
+ outputRecord[colName] = cell.count;
19963
+ break;
19964
+ case "avg":
19965
+ outputRecord[colName] = cell.count > 0 ? cell.sum / cell.count : 0;
19966
+ break;
19967
+ case "min":
19968
+ outputRecord[colName] = cell.min === Infinity ? 0 : cell.min;
19969
+ break;
19970
+ case "max":
19971
+ outputRecord[colName] = cell.max === -Infinity ? 0 : cell.max;
19972
+ break;
19973
+ }
19974
+ }
19975
+ const line = OutputExecutor_default.outputRecord(outputRecord, consumer, pivotedFields);
19976
+ writer.write(line + "\n");
19977
+ outputCount++;
19978
+ }
19979
+ await new Promise((resolve, reject) => {
19980
+ writer.on("close", resolve);
19981
+ writer.on("error", reject);
19982
+ writer.end();
19983
+ });
19984
+ if (!reader.destroyed) {
19985
+ await new Promise((resolve) => {
19986
+ reader.once("close", resolve);
19987
+ reader.destroy();
19988
+ });
19989
+ }
19990
+ await import_promises8.default.unlink(datasetPath);
19991
+ await import_promises8.default.rename(tempWorkPath, datasetPath);
19992
+ return outputCount;
19993
+ };
19994
+ this._parseLine = (line, format2, fields) => {
19995
+ return format2 === "CSV" || format2 === "TXT" ? LineParser_default._internalParseCSV(line, fields) : LineParser_default._internalParseJSON(line);
19996
+ };
19847
19997
  /**
19848
19998
  * Determines if the new record should replace the existing record based on the resolution strategy
19849
19999
  */
@@ -20365,7 +20515,8 @@ var ExecutorOrchestratorClass = class {
20365
20515
  prodDimensions,
20366
20516
  workerId,
20367
20517
  scope,
20368
- options
20518
+ options,
20519
+ loggerConfig: Logger_default.getConfig()
20369
20520
  };
20370
20521
  _progress.register((currentWorkerIndex + 1).toString(), prod.name, fileIndex, totalFiles);
20371
20522
  scope.workersId.push(workerId);
@@ -20396,6 +20547,12 @@ var ExecutorOrchestratorClass = class {
20396
20547
  postOperation.totalOutputCount = unifiedOutputCount;
20397
20548
  }
20398
20549
  }
20550
+ if (consumer.options?.pivot) {
20551
+ counter = performance.now();
20552
+ const unifiedOutputCount = await ConsumerExecutor_default.processPivot(consumer, ExecutorScope_default2.getMainPath(scope));
20553
+ tracker.measure("process-pivot:main", performance.now() - counter);
20554
+ postOperation.totalOutputCount = unifiedOutputCount;
20555
+ }
20399
20556
  counter = performance.now();
20400
20557
  Logger_default.log(`Consumer "${consumer.name}": exporting results`);
20401
20558
  const exportRes = await OutputExecutor_default.exportResult(consumer, ConsumerManager_default.getExpandedFields(consumer), scope);
@@ -20580,6 +20737,8 @@ var ExecutorOrchestrator = new ExecutorOrchestratorClass();
20580
20737
  import_dotenv.default.configDotenv();
20581
20738
  var run = async (workerData) => {
20582
20739
  Environment_default.load("./");
20740
+ if (workerData.loggerConfig)
20741
+ Logger_default.initFromConfig(workerData.loggerConfig);
20583
20742
  try {
20584
20743
  const {
20585
20744
  workerId,