@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 +237 -80
- package/json_schemas/consumer-schema.json +48 -0
- package/package.json +1 -1
- package/workers/ExecutorWorker.js +239 -80
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
|
-
|
|
13316
|
-
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
18693
|
+
if (pk) {
|
|
18694
|
+
if (type === "number") return index + 1;
|
|
18659
18695
|
return `${index + 1}`;
|
|
18660
18696
|
}
|
|
18661
|
-
|
|
18662
|
-
|
|
18663
|
-
|
|
18664
|
-
|
|
18665
|
-
|
|
18666
|
-
|
|
18667
|
-
|
|
18668
|
-
|
|
18669
|
-
|
|
18670
|
-
|
|
18671
|
-
|
|
18672
|
-
|
|
18673
|
-
|
|
18674
|
-
|
|
18675
|
-
|
|
18676
|
-
|
|
18677
|
-
|
|
18678
|
-
|
|
18679
|
-
|
|
18680
|
-
|
|
18681
|
-
|
|
18682
|
-
|
|
18683
|
-
|
|
18684
|
-
|
|
18685
|
-
|
|
18686
|
-
|
|
18687
|
-
|
|
18688
|
-
|
|
18689
|
-
|
|
18690
|
-
|
|
18691
|
-
|
|
18692
|
-
|
|
18693
|
-
|
|
18694
|
-
|
|
18695
|
-
|
|
18696
|
-
|
|
18697
|
-
|
|
18698
|
-
|
|
18699
|
-
|
|
18700
|
-
|
|
18701
|
-
|
|
18702
|
-
|
|
18703
|
-
|
|
18704
|
-
|
|
18705
|
-
|
|
18706
|
-
|
|
18707
|
-
|
|
18708
|
-
|
|
18709
|
-
|
|
18710
|
-
|
|
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
|
-
|
|
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 =
|
|
18746
|
-
const day =
|
|
18747
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
13310
|
-
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
18035
|
+
if (pk) {
|
|
18036
|
+
if (type === "number") return index + 1;
|
|
18001
18037
|
return `${index + 1}`;
|
|
18002
18038
|
}
|
|
18003
|
-
|
|
18004
|
-
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18014
|
-
|
|
18015
|
-
|
|
18016
|
-
|
|
18017
|
-
|
|
18018
|
-
|
|
18019
|
-
|
|
18020
|
-
|
|
18021
|
-
|
|
18022
|
-
|
|
18023
|
-
|
|
18024
|
-
|
|
18025
|
-
|
|
18026
|
-
|
|
18027
|
-
|
|
18028
|
-
|
|
18029
|
-
|
|
18030
|
-
|
|
18031
|
-
|
|
18032
|
-
|
|
18033
|
-
|
|
18034
|
-
|
|
18035
|
-
|
|
18036
|
-
|
|
18037
|
-
|
|
18038
|
-
|
|
18039
|
-
|
|
18040
|
-
|
|
18041
|
-
|
|
18042
|
-
|
|
18043
|
-
|
|
18044
|
-
|
|
18045
|
-
|
|
18046
|
-
|
|
18047
|
-
|
|
18048
|
-
|
|
18049
|
-
|
|
18050
|
-
|
|
18051
|
-
|
|
18052
|
-
|
|
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
|
-
|
|
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 =
|
|
18088
|
-
const day =
|
|
18089
|
-
|
|
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
|
-
|
|
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,
|