@forzalabs/remora 0.0.19 → 0.0.21
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 +6 -2
- package/actions/automap.js +5 -1
- package/actions/deploy.js +0 -1
- package/actions/run.js +9 -3
- package/auth/JWTManager.js +1 -1
- package/database/DatabaseEngine.js +13 -3
- package/definitions/json_schemas/consumer-schema.json +392 -0
- package/definitions/json_schemas/producer-schema.json +4 -0
- package/definitions/transform/Transformations.js +2 -0
- package/drivers/DriverFactory.js +39 -1
- package/drivers/LocalDriver.js +70 -2
- package/drivers/S3Driver.js +52 -0
- package/engines/DataframeManager.js +55 -0
- package/engines/Environment.js +13 -5
- package/engines/ProducerEngine.js +2 -1
- package/engines/UsageDataManager.js +110 -0
- package/engines/UserManager.js +2 -2
- package/engines/Validator.js +1 -1
- package/engines/ai/AutoMapperEngine.js +2 -2
- package/engines/ai/LLM.js +51 -26
- package/engines/consumer/ConsumerManager.js +2 -1
- package/engines/execution/ExecutionEnvironment.js +8 -1
- package/engines/execution/ExecutionPlanner.js +8 -4
- package/engines/file/FileContentBuilder.js +34 -0
- package/engines/file/FileExporter.js +32 -49
- package/engines/transform/TransformationEngine.js +220 -0
- package/engines/transform/TypeCaster.js +33 -0
- package/engines/validation/Validator.js +22 -5
- package/helper/Helper.js +7 -0
- package/index.js +7 -0
- package/licencing/LicenceManager.js +64 -0
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ const SQLBuilder_1 = __importDefault(require("../sql/SQLBuilder"));
|
|
|
23
23
|
const SQLCompiler_1 = __importDefault(require("../sql/SQLCompiler"));
|
|
24
24
|
const ExecutionPlanner_1 = __importDefault(require("./ExecutionPlanner"));
|
|
25
25
|
const RequestExecutor_1 = __importDefault(require("./RequestExecutor"));
|
|
26
|
+
const TransformationEngine_1 = __importDefault(require("../transform/TransformationEngine"));
|
|
26
27
|
class ExecutionEnvironment {
|
|
27
28
|
constructor(consumer) {
|
|
28
29
|
this.run = (options) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -30,7 +31,8 @@ class ExecutionEnvironment {
|
|
|
30
31
|
const plan = ExecutionPlanner_1.default.plan(this._consumer, options);
|
|
31
32
|
(0, Affirm_1.default)(plan, `Invalid execution plan`);
|
|
32
33
|
(0, Affirm_1.default)(plan.length > 0, `Empty execution plan`);
|
|
33
|
-
const
|
|
34
|
+
const start = performance.now();
|
|
35
|
+
const result = { shape: ConsumerEngine_1.default.getOutputShape(this._consumer), _elapsedMS: -1 };
|
|
34
36
|
for (const planStep of plan) {
|
|
35
37
|
switch (planStep.type) {
|
|
36
38
|
case 'compile-consumer-to-SQL': {
|
|
@@ -99,10 +101,15 @@ class ExecutionEnvironment {
|
|
|
99
101
|
this._fetchedData = RequestExecutor_1.default._applyFilters(this._fetchedData, this._consumer.filters.map(x => x.rule));
|
|
100
102
|
break;
|
|
101
103
|
}
|
|
104
|
+
case 'apply-transformations': {
|
|
105
|
+
this._fetchedData = TransformationEngine_1.default.apply(this._consumer, this._fetchedData);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
102
108
|
default: throw new Error(`Invalid execution plan step type "${planStep.type}"`);
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
result.data = this._fetchedData;
|
|
112
|
+
result._elapsedMS = performance.now() - start;
|
|
106
113
|
return result;
|
|
107
114
|
});
|
|
108
115
|
this._consumer = consumer;
|
|
@@ -18,7 +18,7 @@ class ExecutionPlannerClas {
|
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
20
|
this.plan = (consumer, options) => {
|
|
21
|
-
var _a;
|
|
21
|
+
var _a, _b;
|
|
22
22
|
(0, Affirm_1.default)(consumer, 'Invalid consumer');
|
|
23
23
|
const [source, producer] = ConsumerManager_1.default.getSource(consumer);
|
|
24
24
|
const producerEngine = source.engine;
|
|
@@ -39,7 +39,7 @@ class ExecutionPlannerClas {
|
|
|
39
39
|
plan.push({ type: 'read-file-lines', producer: prod, lines: { from: (_a = options.offset) !== null && _a !== void 0 ? _a : 0, to: options.limit ? (options.offset + options.limit) : undefined } });
|
|
40
40
|
else
|
|
41
41
|
plan.push({ type: 'read-file-whole', producer: prod });
|
|
42
|
-
if (prod.settings.fileType.toUpperCase() === 'CSV') {
|
|
42
|
+
if (((_b = prod.settings.fileType) === null || _b === void 0 ? void 0 : _b.toUpperCase()) === 'CSV') {
|
|
43
43
|
plan.push({ type: 'csv-to-json', producer: prod });
|
|
44
44
|
}
|
|
45
45
|
if (prod.dimensions.some(x => { var _a, _b; return ((_a = x.alias) === null || _a === void 0 ? void 0 : _a.includes('{')) || ((_b = x.alias) === null || _b === void 0 ? void 0 : _b.includes('[')); }))
|
|
@@ -51,16 +51,20 @@ class ExecutionPlannerClas {
|
|
|
51
51
|
}
|
|
52
52
|
default: throw new Error(`Engine "${producerEngine}" not supported`);
|
|
53
53
|
}
|
|
54
|
-
//
|
|
54
|
+
// At this point I have the data loaded in memory
|
|
55
55
|
// TODO: can I handle streaming data? (e.g. a file that is too big to fit in memory)
|
|
56
56
|
// TODO: how to handle pagination of SQL results?
|
|
57
|
+
// Apply the transormations to the fields of the consumer
|
|
58
|
+
// TODO: transformations can also be applied directly to the producer... how???
|
|
59
|
+
if (consumer.fields.some(x => Algo_1.default.hasVal(x.transform)))
|
|
60
|
+
plan.push({ type: 'apply-transformations' });
|
|
57
61
|
const engineClass = this.getEngineClass(producerEngine);
|
|
58
62
|
for (const output of consumer.outputs) {
|
|
59
63
|
switch (output.format.toUpperCase()) {
|
|
60
64
|
case 'JSON': {
|
|
61
65
|
if (engineClass === 'file' && Algo_1.default.hasVal(options))
|
|
62
66
|
plan.push({ type: 'apply-execution-request-to-result' });
|
|
63
|
-
// TODO: test if it is
|
|
67
|
+
// TODO: test if it is needed and if it doesn't break soething else
|
|
64
68
|
if (engineClass === 'sql')
|
|
65
69
|
plan.push({ type: 'post-process-json' });
|
|
66
70
|
plan.push({ type: 'export-file', output });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const Affirm_1 = __importDefault(require("../../core/Affirm"));
|
|
7
|
+
const Environment_1 = __importDefault(require("../Environment"));
|
|
8
|
+
class FileContentBuilderClass {
|
|
9
|
+
constructor() {
|
|
10
|
+
/**
|
|
11
|
+
* Converts an array of string to a single string separated with the separator.
|
|
12
|
+
* In the V8 engine there is a maximum length to a string so I can't just join it all.
|
|
13
|
+
* I use this to create chunks that are not too long.
|
|
14
|
+
*/
|
|
15
|
+
this.compose = (lines, separator) => {
|
|
16
|
+
Affirm_1.default.hasValue(lines, 'Invalid lines');
|
|
17
|
+
Affirm_1.default.hasValue(lines, 'Invalid separator');
|
|
18
|
+
const maxStringLength = parseInt(Environment_1.default.get('STRING_MAX_CHARACTERS_LENGTH'));
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let currentChunk = '';
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
currentChunk += (line + separator);
|
|
23
|
+
if (currentChunk.length >= maxStringLength) {
|
|
24
|
+
chunks.push(currentChunk);
|
|
25
|
+
currentChunk = '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
chunks.push(currentChunk);
|
|
29
|
+
return chunks;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const FileContentBuilder = new FileContentBuilderClass();
|
|
34
|
+
exports.default = FileContentBuilder;
|
|
@@ -1,37 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
3
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
4
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -50,14 +17,22 @@ const Algo_1 = __importDefault(require("../../core/Algo"));
|
|
|
50
17
|
const DSTE_1 = __importDefault(require("../../core/dste/DSTE"));
|
|
51
18
|
const DriverFactory_1 = __importDefault(require("../../drivers/DriverFactory"));
|
|
52
19
|
const Environment_1 = __importDefault(require("../Environment"));
|
|
53
|
-
const
|
|
20
|
+
const FileContentBuilder_1 = __importDefault(require("./FileContentBuilder"));
|
|
54
21
|
class FileExporterClass {
|
|
55
22
|
constructor() {
|
|
56
23
|
this.export = (consumer, output, data) => __awaiter(this, void 0, void 0, function* () {
|
|
57
24
|
(0, Affirm_1.default)(consumer, `Invalid consumer`);
|
|
58
25
|
(0, Affirm_1.default)(output, `Invalid output`);
|
|
59
26
|
(0, Affirm_1.default)(data, `Invalid export data`);
|
|
60
|
-
|
|
27
|
+
const preparedData = this._prepareData(consumer, output, data);
|
|
28
|
+
const writeRes = yield this._writeData(consumer, output, preparedData);
|
|
29
|
+
return writeRes;
|
|
30
|
+
});
|
|
31
|
+
this._prepareData = (consumer, output, data) => {
|
|
32
|
+
(0, Affirm_1.default)(consumer, `Invalid consumer`);
|
|
33
|
+
(0, Affirm_1.default)(output, `Invalid output`);
|
|
34
|
+
(0, Affirm_1.default)(data, `Invalid export data`);
|
|
35
|
+
let exportDataChunks = null;
|
|
61
36
|
let extension = null;
|
|
62
37
|
// build the actual file in the requested format
|
|
63
38
|
switch (output.format.toUpperCase()) {
|
|
@@ -71,8 +46,7 @@ class FileExporterClass {
|
|
|
71
46
|
const rowValues = columns.map(c => { var _a; return Algo_1.default.replaceAll((_a = c === null || c === void 0 ? void 0 : c.toString()) !== null && _a !== void 0 ? _a : '', '"', '""'); }).map(c => `"${c}"`).join(',');
|
|
72
47
|
lines.push(rowValues);
|
|
73
48
|
}
|
|
74
|
-
|
|
75
|
-
exportData = csv;
|
|
49
|
+
exportDataChunks = FileContentBuilder_1.default.compose(lines, '\n');
|
|
76
50
|
extension = 'csv';
|
|
77
51
|
break;
|
|
78
52
|
}
|
|
@@ -82,8 +56,7 @@ class FileExporterClass {
|
|
|
82
56
|
const row = data[i];
|
|
83
57
|
lines.push(JSON.stringify(row));
|
|
84
58
|
}
|
|
85
|
-
|
|
86
|
-
exportData = jsonl;
|
|
59
|
+
exportDataChunks = FileContentBuilder_1.default.compose(lines, '\n');
|
|
87
60
|
extension = 'jsonl';
|
|
88
61
|
break;
|
|
89
62
|
}
|
|
@@ -91,27 +64,37 @@ class FileExporterClass {
|
|
|
91
64
|
throw new Error(`Consumer output "${output.format}" not implemented yet`);
|
|
92
65
|
}
|
|
93
66
|
}
|
|
67
|
+
return { textChunks: exportDataChunks, extension };
|
|
68
|
+
};
|
|
69
|
+
this._writeData = (consumer, output, preparedData) => __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
(0, Affirm_1.default)(consumer, `Invalid consumer`);
|
|
71
|
+
(0, Affirm_1.default)(output, `Invalid output`);
|
|
72
|
+
(0, Affirm_1.default)(preparedData, `Invalid prepared data`);
|
|
94
73
|
// export it where it needs to go
|
|
95
74
|
const source = Environment_1.default.getSource(output.exportDestination);
|
|
96
75
|
(0, Affirm_1.default)(source, `Invalid consumer "${consumer.name}" export location source. Make sure that the export location is an available source.`);
|
|
76
|
+
const { extension, textChunks } = preparedData;
|
|
77
|
+
const name = this._composeFileName(consumer, extension);
|
|
97
78
|
switch (source.engine) {
|
|
98
79
|
case 'local': {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const date = DSTE_1.default.now().toISOString().split('.')[0];
|
|
103
|
-
const path = `${folder}/${consumer.name}_${Algo_1.default.replaceAll(date, ':', '-')}.${extension}`;
|
|
104
|
-
fs.writeFileSync(path, exportData);
|
|
105
|
-
return path;
|
|
80
|
+
const driver = yield DriverFactory_1.default.instantiateDestination(source);
|
|
81
|
+
const res = yield driver.multipartUpload({ contents: textChunks, name });
|
|
82
|
+
return res.key;
|
|
106
83
|
}
|
|
107
84
|
case 'aws-s3': {
|
|
108
85
|
const driver = yield DriverFactory_1.default.instantiateDestination(source);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
86
|
+
if (textChunks.length === 1) {
|
|
87
|
+
const res = yield driver.uploadFile({ content: textChunks[0], name });
|
|
88
|
+
return res.key;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const res = yield driver.multipartUpload({ contents: textChunks, name });
|
|
92
|
+
return res.key;
|
|
93
|
+
}
|
|
112
94
|
}
|
|
113
95
|
}
|
|
114
96
|
});
|
|
97
|
+
this._composeFileName = (consumer, extension) => `${consumer.name}_${Algo_1.default.replaceAll(DSTE_1.default.now().toISOString().split('.')[0], ':', '-')}.${extension}`;
|
|
115
98
|
}
|
|
116
99
|
}
|
|
117
100
|
const FileExporter = new FileExporterClass();
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const Affirm_1 = __importDefault(require("../../core/Affirm"));
|
|
7
|
+
const Algo_1 = __importDefault(require("../../core/Algo"));
|
|
8
|
+
const TypeCaster_1 = __importDefault(require("./TypeCaster"));
|
|
9
|
+
class TransformationEngineClass {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.apply = (consumer, data) => {
|
|
12
|
+
(0, Affirm_1.default)(consumer, 'Invalid consumer');
|
|
13
|
+
Affirm_1.default.hasValue(data, 'Invalid data');
|
|
14
|
+
const fieldsToTransform = consumer.fields.filter(field => Algo_1.default.hasVal(field.transform));
|
|
15
|
+
Affirm_1.default.hasItems(fieldsToTransform, 'No fields with transformations');
|
|
16
|
+
// Process the data records in place to improve performance instead of copying to a new array
|
|
17
|
+
for (const record of data) {
|
|
18
|
+
for (const field of fieldsToTransform) {
|
|
19
|
+
if (!field.transform)
|
|
20
|
+
continue;
|
|
21
|
+
const value = record[field.key];
|
|
22
|
+
if (!Algo_1.default.hasVal(value) && Algo_1.default.hasVal(field.default))
|
|
23
|
+
record[field.key] = field.default;
|
|
24
|
+
else if (!Algo_1.default.hasVal(value))
|
|
25
|
+
continue;
|
|
26
|
+
try {
|
|
27
|
+
record[field.key] = this.applyTransformations(value, field.transform, field.key);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
switch (field.onError) {
|
|
31
|
+
case 'set_default':
|
|
32
|
+
record[field.key] = field.default;
|
|
33
|
+
break;
|
|
34
|
+
case 'skip':
|
|
35
|
+
break;
|
|
36
|
+
case 'fail':
|
|
37
|
+
default:
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return data;
|
|
44
|
+
};
|
|
45
|
+
this.applyTransformations = (value, transformations, fieldName) => {
|
|
46
|
+
var _a;
|
|
47
|
+
if (Array.isArray(transformations)) {
|
|
48
|
+
// Process array transformations without creating intermediate arrays
|
|
49
|
+
let result = value;
|
|
50
|
+
for (const transform of transformations) {
|
|
51
|
+
result = this.applyTransformations(result, transform, fieldName);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
// Single transformation
|
|
56
|
+
if ('cast' in transformations) {
|
|
57
|
+
return TypeCaster_1.default.cast(value, transformations.cast);
|
|
58
|
+
}
|
|
59
|
+
if ('multiply' in transformations) {
|
|
60
|
+
const num = TypeCaster_1.default.cast(value, 'number');
|
|
61
|
+
if (isNaN(num))
|
|
62
|
+
throw new Error(`Cannot multiply non-numeric value in field '${fieldName}'`);
|
|
63
|
+
return num * transformations.multiply;
|
|
64
|
+
}
|
|
65
|
+
if ('add' in transformations) {
|
|
66
|
+
const num = TypeCaster_1.default.cast(value, 'number');
|
|
67
|
+
if (isNaN(num))
|
|
68
|
+
throw new Error(`Cannot add to non-numeric value in field '${fieldName}'`);
|
|
69
|
+
return num + transformations.add;
|
|
70
|
+
}
|
|
71
|
+
if ('extract' in transformations) {
|
|
72
|
+
const date = TypeCaster_1.default.cast(value, 'date');
|
|
73
|
+
if (isNaN(date.getTime()))
|
|
74
|
+
throw new Error(`Invalid date for extraction in field '${fieldName}'`);
|
|
75
|
+
switch (transformations.extract) {
|
|
76
|
+
case 'year': return date.getFullYear();
|
|
77
|
+
case 'month': return date.getMonth() + 1; // 1-based month
|
|
78
|
+
case 'day': return date.getDate();
|
|
79
|
+
case 'hour': return date.getHours();
|
|
80
|
+
case 'minute': return date.getMinutes();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if ('concat' in transformations) {
|
|
84
|
+
if (!Array.isArray(value))
|
|
85
|
+
throw new Error(`Cannot concat non-array value in field '${fieldName}'`);
|
|
86
|
+
return value.join(transformations.concat.separator);
|
|
87
|
+
}
|
|
88
|
+
if ('split' in transformations) {
|
|
89
|
+
if (typeof value !== 'string')
|
|
90
|
+
throw new Error(`Cannot split non-string value in field '${fieldName}'`);
|
|
91
|
+
const parts = value.split(transformations.split.separator);
|
|
92
|
+
if (transformations.split.index >= parts.length) {
|
|
93
|
+
throw new Error(`Split index ${transformations.split.index} out of bounds in field '${fieldName}'`);
|
|
94
|
+
}
|
|
95
|
+
return parts[transformations.split.index];
|
|
96
|
+
}
|
|
97
|
+
if ('regex_match' in transformations) {
|
|
98
|
+
if (typeof value !== 'string')
|
|
99
|
+
throw new Error(`Cannot apply regex_match to non-string value in field '${fieldName}'`);
|
|
100
|
+
try {
|
|
101
|
+
const regex = new RegExp(transformations.regex_match.pattern, transformations.regex_match.flags);
|
|
102
|
+
return regex.test(value);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new Error(`Invalid regex pattern in field '${fieldName}': ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if ('regex_replace' in transformations) {
|
|
109
|
+
if (typeof value !== 'string')
|
|
110
|
+
throw new Error(`Cannot apply regex_replace to non-string value in field '${fieldName}'`);
|
|
111
|
+
try {
|
|
112
|
+
const regex = new RegExp(transformations.regex_replace.pattern, transformations.regex_replace.flags);
|
|
113
|
+
return value.replace(regex, transformations.regex_replace.replacement);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new Error(`Invalid regex pattern in field '${fieldName}': ${error.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if ('regex_extract' in transformations) {
|
|
120
|
+
if (typeof value !== 'string')
|
|
121
|
+
throw new Error(`Cannot apply regex_extract to non-string value in field '${fieldName}'`);
|
|
122
|
+
try {
|
|
123
|
+
const regex = new RegExp(transformations.regex_extract.pattern, transformations.regex_extract.flags);
|
|
124
|
+
const matches = value.match(regex);
|
|
125
|
+
if (!matches)
|
|
126
|
+
return null;
|
|
127
|
+
const groupIndex = transformations.regex_extract.group;
|
|
128
|
+
return (_a = matches[groupIndex]) !== null && _a !== void 0 ? _a : null;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw new Error(`Invalid regex pattern in field '${fieldName}': ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if ('trim' in transformations) {
|
|
135
|
+
if (typeof value !== 'string')
|
|
136
|
+
throw new Error(`Cannot trim non-string value in field '${fieldName}'`);
|
|
137
|
+
return value.trim();
|
|
138
|
+
}
|
|
139
|
+
if ('to_lowercase' in transformations) {
|
|
140
|
+
if (typeof value !== 'string')
|
|
141
|
+
throw new Error(`Cannot convert non-string value to lowercase in field '${fieldName}'`);
|
|
142
|
+
return value.toLowerCase();
|
|
143
|
+
}
|
|
144
|
+
if ('to_uppercase' in transformations) {
|
|
145
|
+
if (typeof value !== 'string')
|
|
146
|
+
throw new Error(`Cannot convert non-string value to uppercase in field '${fieldName}'`);
|
|
147
|
+
return value.toUpperCase();
|
|
148
|
+
}
|
|
149
|
+
if ('capitalize' in transformations) {
|
|
150
|
+
if (typeof value !== 'string')
|
|
151
|
+
throw new Error(`Cannot capitalize non-string value in field '${fieldName}'`);
|
|
152
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
153
|
+
}
|
|
154
|
+
if ('substring' in transformations) {
|
|
155
|
+
if (typeof value !== 'string')
|
|
156
|
+
throw new Error(`Cannot take substring of non-string value in field '${fieldName}'`);
|
|
157
|
+
const { start, end } = transformations.substring;
|
|
158
|
+
return end !== undefined ? value.substring(start, end) : value.substring(start);
|
|
159
|
+
}
|
|
160
|
+
if ('pad_start' in transformations) {
|
|
161
|
+
if (typeof value !== 'string')
|
|
162
|
+
throw new Error(`Cannot pad non-string value in field '${fieldName}'`);
|
|
163
|
+
const { length, char } = transformations.pad_start;
|
|
164
|
+
if (char.length !== 1)
|
|
165
|
+
throw new Error(`Pad character must be exactly one character in field '${fieldName}'`);
|
|
166
|
+
return value.padStart(length, char);
|
|
167
|
+
}
|
|
168
|
+
if ('pad_end' in transformations) {
|
|
169
|
+
if (typeof value !== 'string')
|
|
170
|
+
throw new Error(`Cannot pad non-string value in field '${fieldName}'`);
|
|
171
|
+
const { length, char } = transformations.pad_end;
|
|
172
|
+
if (char.length !== 1)
|
|
173
|
+
throw new Error(`Pad character must be exactly one character in field '${fieldName}'`);
|
|
174
|
+
return value.padEnd(length, char);
|
|
175
|
+
}
|
|
176
|
+
if ('prepend' in transformations)
|
|
177
|
+
return transformations.prepend + TypeCaster_1.default.cast(value, 'string');
|
|
178
|
+
if ('append' in transformations)
|
|
179
|
+
return TypeCaster_1.default.cast(value, 'string') + transformations.append;
|
|
180
|
+
if ('conditional' in transformations) {
|
|
181
|
+
for (const clause of transformations.conditional.clauses) {
|
|
182
|
+
if (this.evaluateCondition(value, clause.if)) {
|
|
183
|
+
return clause.then;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return transformations.conditional.else !== undefined ? transformations.conditional.else : value;
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
};
|
|
190
|
+
this.evaluateCondition = (value, condition) => {
|
|
191
|
+
if ('greater_than' in condition) {
|
|
192
|
+
return TypeCaster_1.default.cast(value, 'number') > condition.greater_than;
|
|
193
|
+
}
|
|
194
|
+
if ('greater_than_or_equal' in condition) {
|
|
195
|
+
return TypeCaster_1.default.cast(value, 'number') >= condition.greater_than_or_equal;
|
|
196
|
+
}
|
|
197
|
+
if ('less_than' in condition) {
|
|
198
|
+
return TypeCaster_1.default.cast(value, 'number') < condition.less_than;
|
|
199
|
+
}
|
|
200
|
+
if ('less_than_or_equal' in condition) {
|
|
201
|
+
return TypeCaster_1.default.cast(value, 'number') <= condition.less_than_or_equal;
|
|
202
|
+
}
|
|
203
|
+
if ('equals' in condition) {
|
|
204
|
+
return value === condition.equals;
|
|
205
|
+
}
|
|
206
|
+
if ('not_equals' in condition) {
|
|
207
|
+
return value !== condition.not_equals;
|
|
208
|
+
}
|
|
209
|
+
if ('in' in condition) {
|
|
210
|
+
return condition.in.includes(value);
|
|
211
|
+
}
|
|
212
|
+
if ('not_in' in condition) {
|
|
213
|
+
return !condition.not_in.includes(value);
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const TransformationEngine = new TransformationEngineClass();
|
|
220
|
+
exports.default = TransformationEngine;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class TypeCasterClass {
|
|
4
|
+
/**
|
|
5
|
+
* Casts the value to the requested type (only if needed)
|
|
6
|
+
*/
|
|
7
|
+
cast(value, type) {
|
|
8
|
+
switch (type) {
|
|
9
|
+
case 'boolean': {
|
|
10
|
+
if (typeof value === 'boolean')
|
|
11
|
+
return value;
|
|
12
|
+
else
|
|
13
|
+
return Boolean(value);
|
|
14
|
+
}
|
|
15
|
+
case 'date':
|
|
16
|
+
return new Date(value);
|
|
17
|
+
case 'number': {
|
|
18
|
+
if (typeof value === 'number')
|
|
19
|
+
return value;
|
|
20
|
+
else
|
|
21
|
+
return Number(value);
|
|
22
|
+
}
|
|
23
|
+
case 'string': {
|
|
24
|
+
if (typeof value === 'string')
|
|
25
|
+
return value;
|
|
26
|
+
else
|
|
27
|
+
return String(value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const TypeCaster = new TypeCasterClass();
|
|
33
|
+
exports.default = TypeCaster;
|
|
@@ -76,7 +76,7 @@ class ValidatorClass {
|
|
|
76
76
|
(0, Affirm_1.default)(consumer, 'Invalid consumer');
|
|
77
77
|
const errors = [];
|
|
78
78
|
try {
|
|
79
|
-
// TODO: check that a consumer
|
|
79
|
+
// TODO: check that a consumer doesn't consume hitself
|
|
80
80
|
const allFieldsWithNoFrom = consumer.fields.filter(x => x.key === '*' && !x.from);
|
|
81
81
|
if (allFieldsWithNoFrom.length > 0 && consumer.producers.length > 1)
|
|
82
82
|
errors.push(`Field with key "*" was used without specifying the "from" producer and multiple producers were found.`);
|
|
@@ -126,17 +126,34 @@ class ValidatorClass {
|
|
|
126
126
|
});
|
|
127
127
|
return errors;
|
|
128
128
|
};
|
|
129
|
+
const validateTransformations = (fields) => {
|
|
130
|
+
const errors = [];
|
|
131
|
+
const trxsFields = fields.filter(x => x.transform);
|
|
132
|
+
for (const field of trxsFields) {
|
|
133
|
+
const transformations = Object.keys(field.transform);
|
|
134
|
+
if (transformations.length !== 1)
|
|
135
|
+
errors.push(`There can only be 1 transformation type in your transformation pipeline. Field "${field.key}" got ${transformations.length}`);
|
|
136
|
+
}
|
|
137
|
+
return errors;
|
|
138
|
+
};
|
|
129
139
|
errors.push(...validateGroupingLevels(consumer.fields));
|
|
140
|
+
errors.push(...validateTransformations(consumer.fields));
|
|
130
141
|
// Validation outputs
|
|
131
142
|
const duplicatesOutputs = Algo_1.default.duplicatesObject(consumer.outputs, 'format');
|
|
132
143
|
if (duplicatesOutputs.length > 0) {
|
|
133
144
|
const duplicatesTypes = Algo_1.default.uniq(duplicatesOutputs.map(x => x.format));
|
|
134
145
|
errors.push(`There are outputs with the same type. (duplicates type: ${duplicatesTypes.join(' and ')})`);
|
|
135
146
|
}
|
|
136
|
-
for (
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
errors.push(`An output SQL cannot be both direct and accelerated
|
|
147
|
+
for (const output of consumer.outputs) {
|
|
148
|
+
const format = output.format.toUpperCase();
|
|
149
|
+
if (format === 'SQL' && output.accellerated && output.direct)
|
|
150
|
+
errors.push(`An output SQL cannot be both direct and accelerated (output: ${format})`);
|
|
151
|
+
if ((format === 'CSV' || format === 'JSON' || format === 'PARQUET')) {
|
|
152
|
+
if (!output.exportDestination)
|
|
153
|
+
errors.push(`A static file output must have an export destination set (${format})`);
|
|
154
|
+
else if (!Environment_1.default.getSource(output.exportDestination))
|
|
155
|
+
errors.push(`The export destination "${output.exportDestination}" was not found in the sources.`);
|
|
156
|
+
}
|
|
140
157
|
}
|
|
141
158
|
}
|
|
142
159
|
catch (e) {
|
package/helper/Helper.js
CHANGED
|
@@ -59,6 +59,13 @@ const Helper = {
|
|
|
59
59
|
}
|
|
60
60
|
read(directory);
|
|
61
61
|
return functionFiles;
|
|
62
|
+
},
|
|
63
|
+
formatDateToYYYYMM(date) {
|
|
64
|
+
if (!date)
|
|
65
|
+
return '';
|
|
66
|
+
const year = date.getFullYear();
|
|
67
|
+
const month = ('0' + (date.getMonth() + 1)).slice(-2);
|
|
68
|
+
return `${year}-${month}`;
|
|
62
69
|
}
|
|
63
70
|
};
|
|
64
71
|
exports.default = Helper;
|
package/index.js
CHANGED
|
@@ -16,8 +16,15 @@ const discover_1 = require("./actions/discover");
|
|
|
16
16
|
const automap_1 = require("./actions/automap");
|
|
17
17
|
const create_producer_1 = require("./actions/create_producer");
|
|
18
18
|
const create_consumer_1 = require("./actions/create_consumer");
|
|
19
|
+
const LicenceManager_1 = __importDefault(require("./licencing/LicenceManager"));
|
|
19
20
|
dotenv_1.default.configDotenv();
|
|
20
21
|
const program = new commander_1.Command();
|
|
22
|
+
const remoraLicenceKey = process.env.REMORA_LICENCE_KEY;
|
|
23
|
+
const check = LicenceManager_1.default.validate(remoraLicenceKey);
|
|
24
|
+
if (!check.valid) {
|
|
25
|
+
console.error(`Invalid Remora licence key, the product is not active: remember to set "REMORA_LICENCE_KEY" environment variable.`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
21
28
|
program
|
|
22
29
|
.version(Constants_1.default.cliVersion + '', '-v, --version', 'Display the version of the CLI')
|
|
23
30
|
.description('CLI tool for setting up and managing Data-Remora');
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
7
|
+
const PUBLICK_KEY = `-----BEGIN PUBLIC KEY-----
|
|
8
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7BWugM83YKGzTyZ6kJyy
|
|
9
|
+
M01JoGYBQYn/9H9utQQyC/uugV4g9d7vv87I2yUfqiHtx7BQj0mOGctqnK7vuRcg
|
|
10
|
+
py7fghTjsUj0MDhs8cEpc+47m0xdsU9MQQ8Xze8B3FcdvCc4xtT5nruTAUWMGIAM
|
|
11
|
+
WLHG8lWxkHjTSZn4S0RdpRBB7fw4jyVkc+vXCTtVtYQwMWjcErNFrdCenvci4Vus
|
|
12
|
+
nKlyfKS6ccLQphOc/I2w6zllFwD/l90vQeQ6kDO01aJLOmJPzvlU/TSP/lcxCYTA
|
|
13
|
+
TgQJQxd5UdnTSDjNw+Uu/jzZ5mVlCbtYH1RoJ6cGNglzN8Q50mVk/iVyoZCZ94b4
|
|
14
|
+
bwIDAQAB
|
|
15
|
+
-----END PUBLIC KEY-----`;
|
|
16
|
+
class LicenceManagerClass {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.generate = (customer, expirationDays, features, privateKey) => {
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const expirationDate = new Date(now.getTime() + expirationDays * 24 * 60 * 60 * 1000);
|
|
21
|
+
const licenceData = {
|
|
22
|
+
customer,
|
|
23
|
+
features,
|
|
24
|
+
issued: now.toISOString(),
|
|
25
|
+
expires: expirationDate.toISOString()
|
|
26
|
+
};
|
|
27
|
+
const licenceString = JSON.stringify(licenceData);
|
|
28
|
+
const sign = crypto_1.default.createSign('SHA256');
|
|
29
|
+
sign.update(licenceString);
|
|
30
|
+
sign.end();
|
|
31
|
+
const signature = sign.sign(privateKey, 'base64');
|
|
32
|
+
const licence = { data: licenceData, signature };
|
|
33
|
+
return Buffer.from(JSON.stringify(licence)).toString('base64');
|
|
34
|
+
};
|
|
35
|
+
this.validate = (licence) => {
|
|
36
|
+
try {
|
|
37
|
+
const licenceJSON = Buffer.from(licence, 'base64').toString();
|
|
38
|
+
const licenceData = JSON.parse(licenceJSON);
|
|
39
|
+
const { data, signature } = licenceData;
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const expirationDate = new Date(data.expires);
|
|
42
|
+
if (now > expirationDate)
|
|
43
|
+
return { valid: false, reason: 'License expired', expiryDate: expirationDate };
|
|
44
|
+
const verify = crypto_1.default.createVerify('SHA256');
|
|
45
|
+
verify.update(JSON.stringify(data));
|
|
46
|
+
const isSignatureValid = verify.verify(PUBLICK_KEY, signature, 'base64');
|
|
47
|
+
if (!isSignatureValid)
|
|
48
|
+
return { valid: false, reason: 'Invalid license signature' };
|
|
49
|
+
return {
|
|
50
|
+
valid: true,
|
|
51
|
+
expiryDate: expirationDate,
|
|
52
|
+
daysRemaining: Math.ceil((expirationDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)),
|
|
53
|
+
features: data.features,
|
|
54
|
+
customer: data.customer
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return { valid: false, reason: 'License parsing error', details: error.message };
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const LicenceManager = new LicenceManagerClass();
|
|
64
|
+
exports.default = LicenceManager;
|