@mhubcloud/tms_core 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -4
- package/utils.js +295 -0
package/package.json
CHANGED
package/utils.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const Ajv = require("ajv");
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
let aiClient;
|
|
7
|
+
if (process.env.APPINSIGHTS_INSTRUMENTATIONKEY) {
|
|
8
|
+
const appInsights = require("applicationinsights");
|
|
9
|
+
appInsights.setup().start(); //Set einvironmet variable APPINSIGHTS_INSTRUMENTATIONKEY, so setup() can be called w/o parameters
|
|
10
|
+
aiClient = appInsights.defaultClient;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Base class for Event Publisher classes
|
|
15
|
+
* Facilitates registration and configuration
|
|
16
|
+
*/
|
|
17
|
+
class EventPublisherBase {
|
|
18
|
+
/**
|
|
19
|
+
* Createa new insatnce of the @see EventPublisherBase class
|
|
20
|
+
* @param {Object} config Publisher configuration (in platform-specific format)
|
|
21
|
+
*/
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validates input data that they can be passed to mapper
|
|
28
|
+
* @param {Object} data Input data to validate
|
|
29
|
+
* @returns {boolean} Validation result
|
|
30
|
+
*/
|
|
31
|
+
validate(data) {
|
|
32
|
+
return true; //Override this method in custom publisher for custom validation
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Registers a new instance of @see EventPublisherBase -derived class with array of publishers
|
|
37
|
+
* @param {EventPublisher[]} publishers Tha publishers array to register with
|
|
38
|
+
* @param {Object} config Configuration to pass to new instance
|
|
39
|
+
* Call this static method on @see EventPublisherBase -derived class.
|
|
40
|
+
* It will create its instance and pass @param config to its constructor.
|
|
41
|
+
* Than it will add resulting instance of @param publishers.
|
|
42
|
+
*/
|
|
43
|
+
static register(publishers, config) {
|
|
44
|
+
publishers.push(new this(config) /* Trick: Creates instance of the specific derived class `register` has been called on */);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
EventPublisherBase: EventPublisherBase,
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Provides access to Application Insights logging (event reporting)
|
|
53
|
+
* @type {appInsights.TelemetryClient|null}
|
|
54
|
+
*/
|
|
55
|
+
appInsights: aiClient || null,
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Computes SHA-256 hash of a string
|
|
59
|
+
* @param {string} email The vaue to hash
|
|
60
|
+
* @returns {string} @param email SHA-256 hash; returns email if it is 𝑓𝑎𝑙𝑠𝑒 (null, undefined etc.)
|
|
61
|
+
*/
|
|
62
|
+
encodeEmail: function encodeEmail(email) {
|
|
63
|
+
if (!email) return email;
|
|
64
|
+
return crypto.createHash('sha256').update(email.toLowerCase(), "ascii").digest('hex').toLowerCase();
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Handles HTTP response from node-fetch
|
|
69
|
+
* @param {*} response Node-fetch response object
|
|
70
|
+
* @param {string} event for which response is
|
|
71
|
+
* @returns {Object} Deserialized response JSON
|
|
72
|
+
* Throws exception in case of response error code
|
|
73
|
+
*/
|
|
74
|
+
handleResponse: async function handleResponse(response, event) {
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`HTTP Error code: ${response.status}, event: ${event}, body: ${await response.text()}`);
|
|
77
|
+
}
|
|
78
|
+
const body = await response.text();
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(body);
|
|
81
|
+
} catch (ex) {
|
|
82
|
+
return body;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a deep copy of given object removing any properties or array items with undefined value
|
|
88
|
+
* @param {*} obj Objec or array to copy. If value is neither array nor object it's returned as passed.
|
|
89
|
+
* @returns Deep copy of @param obj (providing array or object has bee passed in). Any properties or array item with undefined value are skipped when making the copy, recursively. Returns @param obj untouched if neither array nor object has been passed in.
|
|
90
|
+
*/
|
|
91
|
+
removeUndefined: function removeUndefined(obj) {
|
|
92
|
+
let ret;
|
|
93
|
+
if (obj instanceof Array) {
|
|
94
|
+
ret = [];
|
|
95
|
+
for (let i = 0; i < obj.length; i++) {
|
|
96
|
+
if (obj[i] !== undefined)
|
|
97
|
+
ret.push(removeUndefined(obj[i]));
|
|
98
|
+
}
|
|
99
|
+
} else if (obj instanceof Object) {
|
|
100
|
+
ret = {};
|
|
101
|
+
for (const prp in obj) {
|
|
102
|
+
if (obj.hasOwnProperty(prp) && obj[prp] !== undefined)
|
|
103
|
+
ret[prp] = removeUndefined(obj[prp]);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
return obj;
|
|
107
|
+
}
|
|
108
|
+
return ret;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Validates an object against the schema it states or given schema
|
|
113
|
+
* @param {Object} data An object to validate. The shema must be provided in its `$schema` property if ot passed in @param schema
|
|
114
|
+
* @param {string} schemaSource Name of a folder to look for schemas. Schemas JSON format are looked for in subfolder `customizable/schemas` of given folder.
|
|
115
|
+
* @param {string} schema Full URL id (i.e. URL starting with https://schema.measurementhub.net/), or just relative part of that URL to resolve schem in @param schemaSource`/customizable/schemas`
|
|
116
|
+
* @returns {boolean} Validation result, true if successful, Exception if unsuccessfull
|
|
117
|
+
*/
|
|
118
|
+
validate: function validate(data, schemaSource, schema = null) {
|
|
119
|
+
schema = schema || data.$schema;
|
|
120
|
+
if (!schema) {
|
|
121
|
+
throw ("Data doesn't have schema and schema not passed");
|
|
122
|
+
}
|
|
123
|
+
if (schema.indexOf("https://schema.measurementhub.net/") !== 0 && (schema.indexOf("http://") === 0 || schema.indexOf("https://") == 0)) {
|
|
124
|
+
throw (`Schema ${data.$schema} is not supported, only https://schema.measurementhub.net/... and schemas identified by relative URL shemas are supported`);
|
|
125
|
+
}
|
|
126
|
+
let parts = schema.split('/');
|
|
127
|
+
if (parts.indexOf('..') >= 0) { //Directory traversal attack protection
|
|
128
|
+
throw (`It's invalid for schema URI to contain '..' segment, such as in ${schema}`);
|
|
129
|
+
}
|
|
130
|
+
if (schema.indexOf("http://") === 0 || schema.indexOf("https://") == 0)
|
|
131
|
+
parts = parts.slice(3);
|
|
132
|
+
|
|
133
|
+
const schemaLocation = `${schemaSource}/customizable/schemas/${parts.join('/')}.json`;
|
|
134
|
+
if (!fs.existsSync(schemaLocation)) {
|
|
135
|
+
throw (`Schema file ${schemaLocation} not found`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let schemaStr;
|
|
139
|
+
try {
|
|
140
|
+
schemaStr = fs.readFileSync(schemaLocation).toString();
|
|
141
|
+
} catch (ex) {
|
|
142
|
+
throw (`Failed to load schema ${schema}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let schemaObj;
|
|
146
|
+
try {
|
|
147
|
+
schemaObj = JSON.parse(schemaStr);
|
|
148
|
+
} catch (ex) {
|
|
149
|
+
throw (`Failed to deserialize ${schema}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const ajv = new Ajv({ allErrors: true });
|
|
153
|
+
let validate;
|
|
154
|
+
try {
|
|
155
|
+
validate = ajv.compile(schemaObj);
|
|
156
|
+
} catch (ex) {
|
|
157
|
+
throw (`Failed to compile schema ${schema}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const valid = validate(data);
|
|
161
|
+
if (!valid) {
|
|
162
|
+
throw (`Schema vaidation failed for schema ${schema}, ${validate.errors.length} errors: ${JSON.stringify(validate.errors)}`);
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Enhances header before sending test request to secondary target (like Pipedream)
|
|
169
|
+
* @param {Object} headers Request header dictionary. This contains the original headers and will be ehnanced.
|
|
170
|
+
* @param {string} origUrl Original URL the request has been sent to
|
|
171
|
+
* @param {*} origRequest The entire original request options
|
|
172
|
+
* @param {*} origResponse Response recievend from original request
|
|
173
|
+
*/
|
|
174
|
+
prepareTestRequestHeaders: function prepareTestRequestHeaders(headers, origUrl, origRequest, origResponse) {
|
|
175
|
+
headers["x-mhub-orig-url"] = origUrl;
|
|
176
|
+
headers["x-mhub-orig-method"] = origRequest.method;
|
|
177
|
+
headers["x-mhub-orig-response-status-code"] = origResponse.status;
|
|
178
|
+
headers["x-mhub-orig-response-status-message"] = origResponse.statusText;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extracts ad code from standardly structured event data (in legacy or new format)
|
|
183
|
+
* @param {{ads:(LegacyAd|Ad)[]}} data Event data to extract ad code from. As you can see the `ads` field is accepted in 2 formats the first being legacy and the later being new one.
|
|
184
|
+
* @param {string} adPlatform value of `data.ads.adPlatform` field to filter ad codes by platform (this is for new style data; ignored for legacy data)
|
|
185
|
+
* @param {string} adCode value of `data.ads.adType` field to filter items in `data.ads` by (this is for legacy style data); als used for new style data to identify the main parameter from `data.ads.adCodes` to obtain
|
|
186
|
+
* @param {(code: AdCodeInfo, ad: LegacyAd|Ad) => string|any} format If this custom function is provided it forats the code returned, so the function does not return object but string formated by this function. But nothing prevents it from returning object if the consumer can hanlde it.
|
|
187
|
+
* @returns {AdCodeInfo|string|null|any} Ad code and time when it was recorded of newest event (by `visitAt`) with given ad code from @param data.ads; when @param format is provided returns string result of that function.
|
|
188
|
+
*/
|
|
189
|
+
extractAdCode: function extractAdCode(data, adPlatform, adCode, format = null) {
|
|
190
|
+
const ad = data.ads.filter(x => x.adType === adCode /*legacy*/ || x.adPlatform === adPlatform /*new*/)
|
|
191
|
+
.sort((a, b) => b.visitAt - a.visitAt)[0];
|
|
192
|
+
const code = ad?.adCode /*legacy*/ || (ad?.adCodes ? ad.adCodes[adCode] : null);
|
|
193
|
+
if (code) {
|
|
194
|
+
const ret = { code: code, visitAt: ad.visitAt };
|
|
195
|
+
if (format) return format(ret, ad);
|
|
196
|
+
return ret;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Logs message to Azure Function context and to Application Insights (if configured)
|
|
203
|
+
* @param {*} context Azure Function context
|
|
204
|
+
* @param {0|1|2|3|4} level Application Insights log level according to https://docs.microsoft.com/en-us/dotnet/api/microsoft.applicationinsights.datacontracts.severitylevel?view=azure-dotnet
|
|
205
|
+
* @param {string} message The message to log
|
|
206
|
+
* @param {Object} properties Properties to pass to Application Insights trace
|
|
207
|
+
*/
|
|
208
|
+
log2: function log2(context, level, message, properties) {
|
|
209
|
+
context.log(message);
|
|
210
|
+
aiClient?.trackTrace(
|
|
211
|
+
{
|
|
212
|
+
message: message,
|
|
213
|
+
severityLevel: level,
|
|
214
|
+
properties: properties
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Logs exception to Azure Function context and to Application Insights (if configured)
|
|
221
|
+
* @param {object} exception
|
|
222
|
+
*/
|
|
223
|
+
logException: function logException(exception) {
|
|
224
|
+
console.log(exception);
|
|
225
|
+
aiClient?.trackException({ exception: exception });
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* For http request. This function is for edge cases. Use Fetch if possible.
|
|
230
|
+
* @param {object} params (hostname, port, path, method)
|
|
231
|
+
* @param {object} postData
|
|
232
|
+
* @returns
|
|
233
|
+
*/
|
|
234
|
+
httpRequest: function httpRequest(params, postData) {
|
|
235
|
+
return new Promise(function (resolve, reject) {
|
|
236
|
+
var req = https.request(params, function (res) {
|
|
237
|
+
// reject on bad status
|
|
238
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
239
|
+
return reject(new Error('statusCode=' + res.statusCode));
|
|
240
|
+
}
|
|
241
|
+
// cumulate data
|
|
242
|
+
var body = [];
|
|
243
|
+
res.on('data', function (chunk) {
|
|
244
|
+
body.push(chunk);
|
|
245
|
+
});
|
|
246
|
+
// resolve on end
|
|
247
|
+
res.on('end', function () {
|
|
248
|
+
try {
|
|
249
|
+
if (body.length > 0) {
|
|
250
|
+
body = JSON.parse(Buffer.concat(body).toString());
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
reject(e);
|
|
254
|
+
}
|
|
255
|
+
resolve(body);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
// reject on request error
|
|
259
|
+
req.on('error', function (err) {
|
|
260
|
+
// This is not a "Second reject", just a different sort of failure
|
|
261
|
+
reject(err);
|
|
262
|
+
});
|
|
263
|
+
if (postData) {
|
|
264
|
+
req.write(postData);
|
|
265
|
+
}
|
|
266
|
+
// IMPORTANT
|
|
267
|
+
req.end();
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Transforms an object to be flat, for instance for GA4
|
|
273
|
+
* @param {object} event Event that is received
|
|
274
|
+
* @param {sring} separator Separator that will be used to unnest the structure. The default separator is underscore _
|
|
275
|
+
* @returns
|
|
276
|
+
*/
|
|
277
|
+
flatten: function flatten(event, separator) {
|
|
278
|
+
return Object.assign(
|
|
279
|
+
{},
|
|
280
|
+
...function _flatten(o, separator = '_', parent) {
|
|
281
|
+
return [].concat(...Object.keys(o)
|
|
282
|
+
.map(key => {
|
|
283
|
+
if (typeof o[key] === 'object' && o[key] !== null) {
|
|
284
|
+
const newParent = parent ? (parent + separator + key) : key;
|
|
285
|
+
return _flatten(o[key], separator, newParent);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const newKey = parent ? (parent + separator + key) : key;
|
|
289
|
+
return { [newKey]: o[key] }
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
}(event, separator))
|
|
294
|
+
}
|
|
295
|
+
};
|