@forzalabs/remora 0.0.44-nasco.3 → 0.0.46-nasco.3
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/Constants.js +1 -1
- package/definitions/json_schemas/consumer-schema.json +38 -0
- package/definitions/json_schemas/producer-schema.json +2 -1
- package/engines/CryptoEngine.js +4 -0
- package/engines/consumer/PostProcessor.js +8 -7
- package/engines/dataset/Dataset.js +11 -6
- package/engines/dataset/DatasetManager.js +4 -4
- package/engines/dataset/DatasetRecord.js +16 -1
- package/engines/execution/ExecutionEnvironment.js +1 -0
- package/engines/execution/RequestExecutor.js +0 -1
- package/engines/producer/ProducerManager.js +2 -1
- package/engines/transform/JoinEngine.js +2 -1
- package/engines/transform/TransformationEngine.js +36 -3
- package/engines/validation/Validator.js +12 -1
- package/package.json +1 -1
package/Constants.js
CHANGED
|
@@ -173,6 +173,10 @@
|
|
|
173
173
|
},
|
|
174
174
|
"default": {
|
|
175
175
|
"description": "Default value of the field if it is missing (or on error if specified)"
|
|
176
|
+
},
|
|
177
|
+
"hidden": {
|
|
178
|
+
"type": "boolean",
|
|
179
|
+
"description": "If set, the field is kept and used during processing, but omitted when exporting the data"
|
|
176
180
|
}
|
|
177
181
|
},
|
|
178
182
|
"required": [
|
|
@@ -428,6 +432,10 @@
|
|
|
428
432
|
},
|
|
429
433
|
"default": {
|
|
430
434
|
"description": "Default value of the field if it is missing (or on error if specified)"
|
|
435
|
+
},
|
|
436
|
+
"hidden": {
|
|
437
|
+
"type": "boolean",
|
|
438
|
+
"description": "If set, the field is kept and used during processing, but omitted when exporting the data"
|
|
431
439
|
}
|
|
432
440
|
},
|
|
433
441
|
"required": [
|
|
@@ -732,6 +740,36 @@
|
|
|
732
740
|
},
|
|
733
741
|
"required": ["append"],
|
|
734
742
|
"additionalProperties": false
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
"type": "object",
|
|
746
|
+
"properties": {
|
|
747
|
+
"combine_fields": {
|
|
748
|
+
"type": "object",
|
|
749
|
+
"properties": {
|
|
750
|
+
"fields": {
|
|
751
|
+
"type": "array",
|
|
752
|
+
"items": {
|
|
753
|
+
"type": "string"
|
|
754
|
+
},
|
|
755
|
+
"description": "Array of field names to combine",
|
|
756
|
+
"minItems": 1
|
|
757
|
+
},
|
|
758
|
+
"separator": {
|
|
759
|
+
"type": "string",
|
|
760
|
+
"description": "Optional separator between fields (default: empty string)"
|
|
761
|
+
},
|
|
762
|
+
"template": {
|
|
763
|
+
"type": "string",
|
|
764
|
+
"description": "Optional template string with placeholders like '{field1} - {field2}'"
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
"required": ["fields"],
|
|
768
|
+
"additionalProperties": false
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
"required": ["combine_fields"],
|
|
772
|
+
"additionalProperties": false
|
|
735
773
|
}
|
|
736
774
|
]
|
|
737
775
|
}
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
"mask",
|
|
71
71
|
"crypt",
|
|
72
72
|
"random",
|
|
73
|
-
"seeded-random"
|
|
73
|
+
"seeded-random",
|
|
74
|
+
"none"
|
|
74
75
|
],
|
|
75
76
|
"description": "Masking type to apply to this dimension. 'hash' replaces with a hashed value. 'mask' replaces characters with a mask character. 'crypt' encrypts the value. 'random' replaces with a random value. 'seeded-random' replaces with a random value generated from a seed. You can use environment variables by using the {your-env-var} notation",
|
|
76
77
|
"examples": [
|
package/engines/CryptoEngine.js
CHANGED
|
@@ -18,6 +18,8 @@ class CryptoEngineClass {
|
|
|
18
18
|
throw new Error('Not implemented yet');
|
|
19
19
|
case 'mask':
|
|
20
20
|
throw new Error('Not implemented yet');
|
|
21
|
+
case 'none':
|
|
22
|
+
return `${fieldReference} AS "${fieldName}"`;
|
|
21
23
|
default:
|
|
22
24
|
throw new Error('This type is not ');
|
|
23
25
|
}
|
|
@@ -55,6 +57,8 @@ class CryptoEngineClass {
|
|
|
55
57
|
throw new Error('Not implemented yet');
|
|
56
58
|
case 'mask':
|
|
57
59
|
throw new Error('Not implemented yet');
|
|
60
|
+
case 'none':
|
|
61
|
+
return value;
|
|
58
62
|
default:
|
|
59
63
|
throw new Error(`This type doesn't exist`);
|
|
60
64
|
}
|
|
@@ -34,7 +34,7 @@ class PostProcessorClass {
|
|
|
34
34
|
(0, Affirm_1.default)(dataset, 'Invalid dataset');
|
|
35
35
|
const fields = ConsumerManager_1.default.getExpandedFields(consumer);
|
|
36
36
|
let newDataset = yield this.dropDimensions(dataset, consumer);
|
|
37
|
-
newDataset = this.
|
|
37
|
+
newDataset = this.updateDimensions(newDataset, consumer);
|
|
38
38
|
newDataset = yield this.reorderDimensions(newDataset, consumer);
|
|
39
39
|
newDataset = yield newDataset.map(record => {
|
|
40
40
|
var _a, _b;
|
|
@@ -53,16 +53,15 @@ class PostProcessorClass {
|
|
|
53
53
|
});
|
|
54
54
|
return newDataset;
|
|
55
55
|
});
|
|
56
|
-
this.
|
|
56
|
+
this.updateDimensions = (dataset, consumer) => {
|
|
57
57
|
const dimensions = dataset.getDimensions();
|
|
58
58
|
const fields = ConsumerManager_1.default.getExpandedFields(consumer);
|
|
59
59
|
for (const dim of dimensions) {
|
|
60
60
|
// This dimension is wanted by the consumer, check if it needs renaming
|
|
61
61
|
const consumerField = fields.find(x => x.cField.key === dim.name);
|
|
62
62
|
if (consumerField) {
|
|
63
|
-
const { cField: { key, alias } } = consumerField;
|
|
64
|
-
|
|
65
|
-
dataset.renameDimension(key, alias);
|
|
63
|
+
const { cField: { key, alias, hidden } } = consumerField;
|
|
64
|
+
dataset.updateDimension(key, { name: alias, hidden: hidden });
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
67
|
return dataset;
|
|
@@ -206,7 +205,8 @@ class PostProcessorClass {
|
|
|
206
205
|
const newDimensions = expectedFieldNames.map((key, index) => ({
|
|
207
206
|
name: key,
|
|
208
207
|
key: key,
|
|
209
|
-
index: index
|
|
208
|
+
index: index,
|
|
209
|
+
hidden: null
|
|
210
210
|
}));
|
|
211
211
|
// Create the row string
|
|
212
212
|
const values = newDimensions.map(dim => {
|
|
@@ -227,7 +227,8 @@ class PostProcessorClass {
|
|
|
227
227
|
const newDimensions = columns.map((col, index) => ({
|
|
228
228
|
name: col.nameInProducer,
|
|
229
229
|
key: col.nameInProducer,
|
|
230
|
-
index: index
|
|
230
|
+
index: index,
|
|
231
|
+
hidden: null
|
|
231
232
|
}));
|
|
232
233
|
// Update the dataset dimensions
|
|
233
234
|
resDataset['_dimensions'] = newDimensions;
|
|
@@ -678,12 +678,17 @@ class Dataset {
|
|
|
678
678
|
return this;
|
|
679
679
|
});
|
|
680
680
|
this.getDimensions = () => this._dimensions;
|
|
681
|
-
this.
|
|
682
|
-
const dimension = this._dimensions.find(x => x.name ===
|
|
683
|
-
(0, Affirm_1.default)(dimension, `Trying to
|
|
684
|
-
this._startOperation('
|
|
685
|
-
|
|
686
|
-
|
|
681
|
+
this.updateDimension = (dimensionName, newValues) => {
|
|
682
|
+
const dimension = this._dimensions.find(x => x.name === dimensionName);
|
|
683
|
+
(0, Affirm_1.default)(dimension, `Trying to update the dataset dimension "${dimensionName}", but none was found (${this._dimensions.map(x => x.name).join(', ')})`);
|
|
684
|
+
this._startOperation('update-dimension');
|
|
685
|
+
const { hidden, name } = newValues;
|
|
686
|
+
if (name && name.length > 0)
|
|
687
|
+
dimension.name = name;
|
|
688
|
+
if (hidden)
|
|
689
|
+
dimension.hidden = hidden;
|
|
690
|
+
this._finishOperation('update-dimension');
|
|
691
|
+
return this;
|
|
687
692
|
};
|
|
688
693
|
this.dropDimensions = (dimensionNames) => __awaiter(this, void 0, void 0, function* () {
|
|
689
694
|
if (dimensionNames.length === 0)
|
|
@@ -49,7 +49,7 @@ class DatasetManagerClass {
|
|
|
49
49
|
const headerLine = firstLine;
|
|
50
50
|
const rawDimensions = ParseManager_1.default._extractHeader(headerLine, delimiterChar, producer, discover);
|
|
51
51
|
return {
|
|
52
|
-
dimensions: rawDimensions.map(x => ({ key: x.name, name: x.saveAs, index: x.index })),
|
|
52
|
+
dimensions: rawDimensions.map(x => ({ key: x.name, name: x.saveAs, index: x.index, hidden: null })),
|
|
53
53
|
delimiter: delimiterChar
|
|
54
54
|
};
|
|
55
55
|
}
|
|
@@ -64,7 +64,7 @@ class DatasetManagerClass {
|
|
|
64
64
|
const columnKey = (_b = pColumn.aliasInProducer) !== null && _b !== void 0 ? _b : pColumn.nameInProducer;
|
|
65
65
|
const csvColumnIndex = keys.findIndex(x => x === columnKey);
|
|
66
66
|
(0, Affirm_1.default)(csvColumnIndex > -1, `The column "${pColumn.nameInProducer}" (with key "${columnKey}") of producer "${producer.name}" doesn't exist in the underlying dataset.`);
|
|
67
|
-
dimensions.push({ index: csvColumnIndex, key: columnKey, name: pColumn.nameInProducer });
|
|
67
|
+
dimensions.push({ index: csvColumnIndex, key: columnKey, name: pColumn.nameInProducer, hidden: null });
|
|
68
68
|
}
|
|
69
69
|
const delimiterChar = (_c = file.delimiter) !== null && _c !== void 0 ? _c : ',';
|
|
70
70
|
return { dimensions, delimiter: delimiterChar };
|
|
@@ -76,7 +76,7 @@ class DatasetManagerClass {
|
|
|
76
76
|
const source = Environment_1.default.getSource(producer.source);
|
|
77
77
|
const columns = FileCompiler_1.default.compileProducer(producer, source);
|
|
78
78
|
return {
|
|
79
|
-
dimensions: columns.map((x, i) => { var _a; return ({ key: (_a = x.aliasInProducer) !== null && _a !== void 0 ? _a : x.nameInProducer, name: x.nameInProducer, index: i }); }),
|
|
79
|
+
dimensions: columns.map((x, i) => { var _a; return ({ key: (_a = x.aliasInProducer) !== null && _a !== void 0 ? _a : x.nameInProducer, name: x.nameInProducer, index: i, hidden: null }); }),
|
|
80
80
|
delimiter: delimiterChar
|
|
81
81
|
};
|
|
82
82
|
}
|
|
@@ -84,7 +84,7 @@ class DatasetManagerClass {
|
|
|
84
84
|
const delimiterChar = (_e = producer.settings.delimiter) !== null && _e !== void 0 ? _e : ',';
|
|
85
85
|
const rawDimensions = ParseManager_1.default._extractHeader(firstLine, delimiterChar, producer, discover);
|
|
86
86
|
return {
|
|
87
|
-
dimensions: rawDimensions.map(x => ({ key: x.name, name: x.saveAs, index: x.index })),
|
|
87
|
+
dimensions: rawDimensions.map(x => ({ key: x.name, name: x.saveAs, index: x.index, hidden: null })),
|
|
88
88
|
delimiter: delimiterChar
|
|
89
89
|
};
|
|
90
90
|
}
|
|
@@ -46,10 +46,25 @@ class DatasetRecord {
|
|
|
46
46
|
this._value = newValue;
|
|
47
47
|
return this;
|
|
48
48
|
};
|
|
49
|
-
this.toJSON = () =>
|
|
49
|
+
this.toJSON = () => {
|
|
50
|
+
if (this._dimensions.some(x => x.hidden)) {
|
|
51
|
+
// remove the not wanted dimension
|
|
52
|
+
const clonedValue = structuredClone(this._value);
|
|
53
|
+
for (const dim of this._dimensions) {
|
|
54
|
+
if (dim.hidden)
|
|
55
|
+
delete clonedValue[dim.name];
|
|
56
|
+
}
|
|
57
|
+
return JSON.stringify(clonedValue);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
return JSON.stringify(this._value);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
50
63
|
this.toCSV = (delimiter) => {
|
|
51
64
|
const myDelimtier = delimiter !== null && delimiter !== void 0 ? delimiter : this._delimiter;
|
|
65
|
+
// remove the not wanted dimension
|
|
52
66
|
const line = this._dimensions
|
|
67
|
+
.filter(x => !x.hidden)
|
|
53
68
|
.map(x => { var _a, _b; return `"${Algo_1.default.replaceAll((_b = (_a = this._value[x.name]) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : '', '"', '""')}"`; })
|
|
54
69
|
.join(myDelimtier);
|
|
55
70
|
return line;
|
|
@@ -123,6 +123,7 @@ class ExecutionEnvironment {
|
|
|
123
123
|
}
|
|
124
124
|
case 'export-file': {
|
|
125
125
|
(0, Affirm_1.default)(planStep.output, `Invalid output in export-file step`);
|
|
126
|
+
(0, Affirm_1.default)(this._resultingDataset, 'Invalid resulting dataset in export-file step');
|
|
126
127
|
const res = yield FileExporter_1.default.export(this._consumer, planStep.output, this._resultingDataset);
|
|
127
128
|
result.fileUri = res;
|
|
128
129
|
break;
|
|
@@ -21,7 +21,6 @@ class RequestExecutorClass {
|
|
|
21
21
|
this.execute = (dataset, request) => __awaiter(this, void 0, void 0, function* () {
|
|
22
22
|
(0, Affirm_1.default)(dataset, 'Invalid data');
|
|
23
23
|
(0, Affirm_1.default)(request, 'Invalid request');
|
|
24
|
-
(0, Affirm_1.default)(Array.isArray(dataset), `Invalid data type: should be an array`);
|
|
25
24
|
if (request.filters)
|
|
26
25
|
dataset = yield this.applyFilters(dataset, request.filters);
|
|
27
26
|
if (request.order)
|
|
@@ -16,7 +16,8 @@ class ProducerManagerClass {
|
|
|
16
16
|
'mask',
|
|
17
17
|
'crypt',
|
|
18
18
|
'random',
|
|
19
|
-
'seeded-random'
|
|
19
|
+
'seeded-random',
|
|
20
|
+
'none'
|
|
20
21
|
];
|
|
21
22
|
(0, Affirm_1.default)(allowedMaskValues.includes(mask), `Mask value "${mask}" in dimension "${dimension.name}" is not a valid value.`);
|
|
22
23
|
return mask;
|
|
@@ -103,7 +103,8 @@ class JoinEngineClass {
|
|
|
103
103
|
const resultDimensions = consumerColumns.map((col, index) => ({
|
|
104
104
|
name: col.consumerAlias || col.consumerKey,
|
|
105
105
|
key: col.consumerAlias || col.consumerKey,
|
|
106
|
-
index
|
|
106
|
+
index,
|
|
107
|
+
hidden: null
|
|
107
108
|
}));
|
|
108
109
|
// Initialize the result dataset with proper dimensions
|
|
109
110
|
resultDataset.getDimensions().length = 0;
|
|
@@ -31,10 +31,13 @@ class TransformationEngineClass {
|
|
|
31
31
|
const value = record.getValue(fieldKey);
|
|
32
32
|
if (!Algo_1.default.hasVal(value) && Algo_1.default.hasVal(field.default))
|
|
33
33
|
record.setValue(fieldKey, field.default);
|
|
34
|
+
else if (!Algo_1.default.hasVal(value) && this.isFieldCombinationTransformation(field.transform))
|
|
35
|
+
// For field combination transformations, we don't skip null values as they might combine with other fields
|
|
36
|
+
continue;
|
|
34
37
|
else if (!Algo_1.default.hasVal(value))
|
|
35
38
|
continue;
|
|
36
39
|
try {
|
|
37
|
-
record.setValue(fieldKey, this.applyTransformations(value, field.transform, field));
|
|
40
|
+
record.setValue(fieldKey, this.applyTransformations(value, field.transform, field, record));
|
|
38
41
|
}
|
|
39
42
|
catch (error) {
|
|
40
43
|
switch (field.onError) {
|
|
@@ -52,13 +55,19 @@ class TransformationEngineClass {
|
|
|
52
55
|
return record;
|
|
53
56
|
});
|
|
54
57
|
});
|
|
55
|
-
this.
|
|
58
|
+
this.isFieldCombinationTransformation = (transformation) => {
|
|
59
|
+
if (Array.isArray(transformation)) {
|
|
60
|
+
return transformation.some(t => this.isFieldCombinationTransformation(t));
|
|
61
|
+
}
|
|
62
|
+
return 'combine_fields' in transformation;
|
|
63
|
+
};
|
|
64
|
+
this.applyTransformations = (value, transformations, field, record) => {
|
|
56
65
|
var _a;
|
|
57
66
|
if (Array.isArray(transformations)) {
|
|
58
67
|
// Process array transformations without creating intermediate arrays
|
|
59
68
|
let result = value;
|
|
60
69
|
for (const transform of transformations) {
|
|
61
|
-
result = this.applyTransformations(result, transform, field);
|
|
70
|
+
result = this.applyTransformations(result, transform, field, record);
|
|
62
71
|
}
|
|
63
72
|
return result;
|
|
64
73
|
}
|
|
@@ -190,6 +199,30 @@ class TransformationEngineClass {
|
|
|
190
199
|
return transformations.prepend + TypeCaster_1.default.cast(value, 'string');
|
|
191
200
|
if ('append' in transformations)
|
|
192
201
|
return TypeCaster_1.default.cast(value, 'string') + transformations.append;
|
|
202
|
+
if ('combine_fields' in transformations) {
|
|
203
|
+
if (!record) {
|
|
204
|
+
throw new Error(`Cannot apply combine_fields transformation without record context in field '${field.key}'`);
|
|
205
|
+
}
|
|
206
|
+
const { fields, separator = '', template } = transformations.combine_fields;
|
|
207
|
+
// Get values from the specified fields
|
|
208
|
+
const fieldValues = fields.map(fieldName => {
|
|
209
|
+
const fieldValue = record.getValue(fieldName);
|
|
210
|
+
return fieldValue !== null && fieldValue !== undefined ? String(fieldValue) : '';
|
|
211
|
+
});
|
|
212
|
+
// If template is provided, use it for formatting
|
|
213
|
+
if (template) {
|
|
214
|
+
let result = template;
|
|
215
|
+
for (let i = 0; i < fields.length; i++) {
|
|
216
|
+
const placeholder = `{${fields[i]}}`;
|
|
217
|
+
result = result.replace(new RegExp(placeholder, 'g'), fieldValues[i]);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// Otherwise, join with separator
|
|
223
|
+
return fieldValues.join(separator);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
193
226
|
if ('conditional' in transformations) {
|
|
194
227
|
for (const clause of transformations.conditional.clauses) {
|
|
195
228
|
if (this.evaluateCondition(value, clause.if)) {
|
|
@@ -138,6 +138,7 @@ class ValidatorClass {
|
|
|
138
138
|
return errors;
|
|
139
139
|
};
|
|
140
140
|
const validateTransformations = (fields) => {
|
|
141
|
+
var _a;
|
|
141
142
|
const errors = [];
|
|
142
143
|
const trxsFields = fields.filter(x => x.transform);
|
|
143
144
|
for (const field of trxsFields) {
|
|
@@ -149,7 +150,17 @@ class ValidatorClass {
|
|
|
149
150
|
for (const trans of trxToValidate) {
|
|
150
151
|
const trxKeys = Object.keys(trans);
|
|
151
152
|
if (trxKeys.length !== 1)
|
|
152
|
-
errors.push(`There can only be 1 transformation type in your transformation pipeline. Field "${field.key}" got ${trxKeys.length}
|
|
153
|
+
errors.push(`There can only be 1 transformation type in your transformation pipeline. Field "${field.key}" got ${trxKeys.length}.`);
|
|
154
|
+
if ('combine_fields' in trans) {
|
|
155
|
+
const { combine_fields } = trans;
|
|
156
|
+
if (!combine_fields.fields || combine_fields.fields.length === 0)
|
|
157
|
+
errors.push(`The "combine_field" transformation is missing the "fields" property ("${field.key}").`);
|
|
158
|
+
const missingFieldsInConsumer = combine_fields.fields
|
|
159
|
+
.map(x => ({ field: x, found: fields.find(k => { var _a; return ((_a = k.alias) !== null && _a !== void 0 ? _a : k.key) === x; }) }))
|
|
160
|
+
.filter(x => !x.found);
|
|
161
|
+
if (missingFieldsInConsumer.length > 0)
|
|
162
|
+
errors.push(`The requested field(s) for a transformation is missing in the consumer -> missing field(s): "${missingFieldsInConsumer.map(x => x.field).join(', ')}"; field transformation: "${(_a = field.alias) !== null && _a !== void 0 ? _a : field.key}";`);
|
|
163
|
+
}
|
|
153
164
|
}
|
|
154
165
|
}
|
|
155
166
|
return errors;
|