vtlab-generic-functions 1.0.41 → 1.0.43

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.
@@ -18,14 +18,16 @@ module.exports = class Lambda {
18
18
  ftpConnection;
19
19
  jobsByTrackingId;
20
20
  DEBUG;
21
+ context;
21
22
 
22
23
  results = {
23
24
  totalError: 0, totalSuccess: 0, error: [], success: [],
24
25
  };
25
26
 
26
- constructor({ carrier, event, DEBUG = false }) {
27
+ constructor({ carrier, event, DEBUG = false, context }) {
27
28
  this.DEBUG = false;
28
29
  this.carrier = carrier;
30
+ this.context = context;
29
31
  if (event.body) {
30
32
  //Locally we receive event.body, event.body needs to be parsed. In AWS all fields are into event. (event={lodId:'...', payload:[]})
31
33
  event = JSON.parse(event.body);
@@ -196,7 +198,7 @@ module.exports = class Lambda {
196
198
  };
197
199
 
198
200
  logMessage = async (message) => {
199
- await cloudwatch.putLogEvents(`${this.logUuid} ${message}`, this.logGroupName, 2, this.logStreamName);
201
+ await cloudwatch.putLogEvents(`${this?.context?.awsRequestId || '-'} ${this.logUuid} ${message}`, this.logGroupName, 2, this.logStreamName);
200
202
  };
201
203
 
202
204
  logError = async (error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vtlab-generic-functions",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "scripts": {
5
5
  "test": "jest"
6
6
  },
@@ -10,6 +10,7 @@
10
10
  "@aws-sdk/client-ses": "^3.478.0",
11
11
  "@aws-sdk/client-sqs": "^3.478.0",
12
12
  "@aws-sdk/s3-request-presigner": "^3.484.0",
13
+ "@supabase/supabase-js": "^2.110.0",
13
14
  "axios": "^1.6.2",
14
15
  "countries-code": "^1.1.0",
15
16
  "country-data": "0.0.31",
@@ -0,0 +1,407 @@
1
+ const { createClient } = require("@supabase/supabase-js");
2
+ const Joi = require("joi");
3
+ const { randomBytes } = require("crypto");
4
+
5
+ const VALID_ENVIRONMENTS = [
6
+ "production",
7
+ "develop",
8
+ "demo",
9
+ "staging",
10
+ "testing",
11
+ "uat"
12
+ ];
13
+
14
+ const statusMap = {
15
+ error: "FAIL",
16
+ warning: "WARNING",
17
+ ok: "SUCCESS"
18
+ };
19
+
20
+ let defaultClient;
21
+
22
+ const generateRandomString = (length = 10) => {
23
+ return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
24
+ };
25
+
26
+ const formatError = (err) => {
27
+ if (!err) return "";
28
+ if (typeof err === "string") return err;
29
+ if (err.response && err.response.data) {
30
+ try {
31
+ return JSON.stringify(err.response.data);
32
+ } catch (_) {
33
+ return String(err.response.data);
34
+ }
35
+ }
36
+ if (err.message) return err.message;
37
+ try {
38
+ return JSON.stringify(err);
39
+ } catch (_) {
40
+ return String(err);
41
+ }
42
+ };
43
+
44
+ const normalizeEnvironment = (environment) => {
45
+ if (!environment || !VALID_ENVIRONMENTS.includes(environment)) {
46
+ return "develop";
47
+ }
48
+
49
+ return environment;
50
+ };
51
+
52
+ const createSupabaseAdminClient = ({ supabaseUrl, serviceRoleKey }) => {
53
+ if (!supabaseUrl || !serviceRoleKey) {
54
+ throw new Error("Telemetry Supabase configuration missing: explicit supabaseUrl and serviceRoleKey are required");
55
+ }
56
+
57
+ return createClient(supabaseUrl, serviceRoleKey, {
58
+ auth: {
59
+ autoRefreshToken: true,
60
+ persistSession: true
61
+ },
62
+ global: {
63
+ headers: {
64
+ apikey: serviceRoleKey,
65
+ Authorization: `Bearer ${serviceRoleKey}`
66
+ }
67
+ }
68
+ });
69
+ };
70
+
71
+ const chunkArray = (items, chunkSize) => {
72
+ const chunks = [];
73
+ for (let i = 0; i < items.length; i += chunkSize) {
74
+ chunks.push(items.slice(i, i + chunkSize));
75
+ }
76
+ return chunks;
77
+ };
78
+
79
+ const parseContentBeforeSaving = (record) => {
80
+ return {
81
+ uuid: record.uuid,
82
+ environment: record.environment,
83
+ process_run_id: record.processId,
84
+ thread_id: record.threadId || null,
85
+ status: statusMap[record.status],
86
+ created_at: record.date,
87
+ message: record.message,
88
+ last_event: record.lastEvent,
89
+ details: record.details || {}
90
+ };
91
+ };
92
+
93
+ const createTelemetryClient = (options = {}) => {
94
+ let runtimeConfig;
95
+ let supabaseClient;
96
+ const logger = options.logger || console;
97
+
98
+ const resolveRuntimeConfig = async () => {
99
+ if (runtimeConfig) return runtimeConfig;
100
+
101
+ runtimeConfig = {
102
+ environment: normalizeEnvironment(options.environment),
103
+ supabaseUrl: options.supabaseUrl,
104
+ serviceRoleKey: options.serviceRoleKey
105
+ };
106
+
107
+ return runtimeConfig;
108
+ };
109
+
110
+ const getSupabaseClient = async () => {
111
+ if (supabaseClient) return supabaseClient;
112
+
113
+ if (options.supabaseClient) {
114
+ supabaseClient = options.supabaseClient;
115
+ return supabaseClient;
116
+ }
117
+
118
+ const resolvedConfig = await resolveRuntimeConfig();
119
+ supabaseClient = createSupabaseAdminClient({
120
+ supabaseUrl: resolvedConfig.supabaseUrl,
121
+ serviceRoleKey: resolvedConfig.serviceRoleKey
122
+ });
123
+
124
+ return supabaseClient;
125
+ };
126
+
127
+ const findProcessId = async (name, account, environment) => {
128
+ if (!account) {
129
+ logger.log(`No account provided to find processId in supabase for process ${name} in environment ${environment}`);
130
+ return false;
131
+ }
132
+
133
+ try {
134
+ const client = await getSupabaseClient();
135
+ let query = client.from("processes").select("id").eq("name", name).eq("account", account);
136
+ if (environment) {
137
+ query = query.eq("environment", environment);
138
+ }
139
+
140
+ const { data: processId, error: processIdError } = await query
141
+ .order("created_at", { ascending: false })
142
+ .limit(1)
143
+ .maybeSingle();
144
+
145
+ if (processIdError) {
146
+ logger.log(`Error finding processId in supabase: ${formatError(processIdError)} for process ${name} in account ${account} in environment ${environment}`);
147
+ return false;
148
+ }
149
+
150
+ return processId ? processId.id : false;
151
+ } catch (e) {
152
+ logger.log(`Error finding processId in supabase: ${formatError(e)} for process ${name} in account ${account} in environment ${environment}`);
153
+ return false;
154
+ }
155
+ };
156
+
157
+ const createProcess = async (params) => {
158
+ const { processName, account, environment, logGroup, logStream } = params;
159
+
160
+ if (!account) {
161
+ logger.log("No account provided to create process in supabase", processName);
162
+ return false;
163
+ }
164
+
165
+ try {
166
+ const client = await getSupabaseClient();
167
+ let { data: processId, error: processIdError } = await client
168
+ .from("processes")
169
+ .insert({
170
+ name: processName,
171
+ description: `Process ${processName}`,
172
+ account,
173
+ environment,
174
+ log_group: logGroup,
175
+ log_stream: logStream,
176
+ })
177
+ .select("id")
178
+ .single();
179
+
180
+ if (processIdError) {
181
+ logger.log(`Error creating process in supabase: ${formatError(processIdError)} for process ${processName} in account ${account} in environment ${environment}`);
182
+ processId = await findProcessId(processName, account, environment);
183
+ }
184
+
185
+ if (!processId) {
186
+ logger.log(`Error finding processId in supabase for process ${processName} in account ${account} in environment ${environment}`);
187
+ return false;
188
+ }
189
+
190
+ return processId.id || processId;
191
+ } catch (e) {
192
+ logger.log(`Error creating process in supabase: ${formatError(e)} for process ${processName} in account ${account} in environment ${environment}`);
193
+ return false;
194
+ }
195
+ };
196
+
197
+ const createProcessRun = async (processId, params) => {
198
+ if (!processId) {
199
+ throw new Error(`Error creating process run in supabase: processId is required but got ${processId}`);
200
+ }
201
+
202
+ const client = await getSupabaseClient();
203
+ let body = {
204
+ uuid: params.uuid || params.logExecutionUUID || generateRandomString(10),
205
+ environment: params.environment,
206
+ account: params.account || "",
207
+ status: "STARTED"
208
+ };
209
+
210
+ const insertProcessRun = async () => {
211
+ return client
212
+ .from("processes_runs")
213
+ .insert({
214
+ process_id: processId,
215
+ ...body
216
+ })
217
+ .select("id")
218
+ .single();
219
+ };
220
+
221
+ let { data: processRunId, error: processRunIdError } = await insertProcessRun();
222
+
223
+ const duplicateConstraintMessage = 'duplicate key value violates unique constraint "processes_runs_uuid_key"';
224
+ if ((processRunIdError || !processRunId) && (processRunIdError && (processRunIdError.code === "23505" || (processRunIdError.message || "").includes(duplicateConstraintMessage)))) {
225
+ body = {
226
+ ...body,
227
+ uuid: `${body.uuid}-${generateRandomString(3)}`
228
+ };
229
+
230
+ ({ data: processRunId, error: processRunIdError } = await insertProcessRun());
231
+ }
232
+
233
+ if (processRunIdError || !processRunId) {
234
+ throw new Error(`Error creating process run in supabase: ${formatError(processRunIdError)}`);
235
+ }
236
+
237
+ return {
238
+ id: processRunId.id,
239
+ uuid: body.uuid
240
+ };
241
+ };
242
+
243
+ const createTelemetryHeader = async (params = {}) => {
244
+ try {
245
+ const resolvedConfig = await resolveRuntimeConfig();
246
+ const telemetryParams = {
247
+ ...params,
248
+ environment: params.environment || resolvedConfig.environment
249
+ };
250
+
251
+ let processId = await findProcessId(
252
+ telemetryParams.processName,
253
+ telemetryParams.account,
254
+ telemetryParams.environment
255
+ );
256
+
257
+ if (!processId) {
258
+ processId = await createProcess(telemetryParams);
259
+ if (!processId) {
260
+ logger.log("Error creating process in supabase", telemetryParams.processName, telemetryParams.account);
261
+ return false;
262
+ }
263
+ }
264
+
265
+ const processRunIdResults = await createProcessRun(processId, telemetryParams);
266
+
267
+ return {
268
+ processId: processRunIdResults.id,
269
+ uuid: processRunIdResults.uuid
270
+ };
271
+ } catch (e) {
272
+ logger.log("Error creating telemetry header", e);
273
+ return false;
274
+ }
275
+ };
276
+
277
+ const saveBulkEvents = async (events) => {
278
+ const client = await getSupabaseClient();
279
+ const { error: processEventsError } = await client.from("processes_events").insert(events);
280
+ if (processEventsError) {
281
+ throw new Error(`Error saving bulk events in supabase: ${formatError(processEventsError)}`);
282
+ }
283
+ return true;
284
+ };
285
+
286
+ const sendTelemetryEvents = async (events = []) => {
287
+ try {
288
+ if (!Array.isArray(events) || events.length === 0) {
289
+ return true;
290
+ }
291
+
292
+ const resolvedConfig = await resolveRuntimeConfig();
293
+ const validationSchema = Joi.object({
294
+ environment: Joi.string().valid(...VALID_ENVIRONMENTS).default(resolvedConfig.environment),
295
+ processId: Joi.string().required(),
296
+ uuid: Joi.string().required(),
297
+ date: Joi.string().isoDate().required().default(new Date().toISOString()),
298
+ threadId: Joi.string().allow(null).optional(),
299
+ message: Joi.string().max(300).truncate().required(),
300
+ lastEvent: Joi.boolean().default(false),
301
+ status: Joi.string().valid("error", "ok", "warning").default("ok"),
302
+ details: Joi.object().unknown(true).default({})
303
+ }).required();
304
+
305
+ const chunks = chunkArray(events, 10);
306
+ const sanitizedEvents = chunks.map((chunk) => {
307
+ return chunk.map((event) => {
308
+ try {
309
+ const validationResult = validationSchema.validate(event);
310
+ if (validationResult.error) {
311
+ throw validationResult.error;
312
+ }
313
+ return validationResult.value;
314
+ } catch (e) {
315
+ logger.log(e);
316
+ return null;
317
+ }
318
+ }).filter((event) => event !== null);
319
+ });
320
+
321
+ for (const chunk of sanitizedEvents) {
322
+ try {
323
+ if (chunk.length === 0) continue;
324
+ const processEvents = chunk.map((event) => parseContentBeforeSaving(event));
325
+ const processEventsSaved = await saveBulkEvents(processEvents);
326
+ if (!processEventsSaved) {
327
+ logger.log("Error saving process events in supabase", processEvents);
328
+ return false;
329
+ }
330
+ } catch (_) {
331
+ continue;
332
+ }
333
+ }
334
+
335
+ return true;
336
+ } catch (e) {
337
+ logger.log("Error sending telemetry events", e);
338
+ return false;
339
+ }
340
+ };
341
+
342
+ const createTelemetryEmitter = (telemetryHeader) => {
343
+ let telemetryEndEmitted = false;
344
+
345
+ const normalizeMessage = (message) => {
346
+ if (typeof message !== "string") {
347
+ return "";
348
+ }
349
+
350
+ return message.length > 300 ? `${message.slice(0, 297)}...` : message;
351
+ };
352
+
353
+ const emitTelemetryEvent = async (message, status = "ok", extra = {}) => {
354
+ const isLastEvent = extra && extra.lastEvent === true;
355
+
356
+ if (!telemetryHeader) {
357
+ return false;
358
+ }
359
+
360
+ if (isLastEvent) {
361
+ telemetryEndEmitted = true;
362
+ }
363
+
364
+ try {
365
+ await sendTelemetryEvents([
366
+ {
367
+ processId: telemetryHeader.processId,
368
+ uuid: telemetryHeader.uuid,
369
+ date: new Date().toISOString(),
370
+ message: normalizeMessage(message),
371
+ status,
372
+ ...extra
373
+ }
374
+ ]);
375
+ return true;
376
+ } catch (_) {
377
+ return false;
378
+ }
379
+ };
380
+
381
+ return {
382
+ emitTelemetryEvent,
383
+ hasEmittedLastEvent: () => telemetryEndEmitted
384
+ };
385
+ };
386
+
387
+ return {
388
+ createTelemetryHeader,
389
+ sendTelemetryEvents,
390
+ createTelemetryEmitter
391
+ };
392
+ };
393
+
394
+ const getDefaultClient = () => {
395
+ if (!defaultClient) {
396
+ defaultClient = createTelemetryClient();
397
+ }
398
+
399
+ return defaultClient;
400
+ };
401
+
402
+ module.exports = {
403
+ createTelemetryClient,
404
+ createTelemetryHeader: (...args) => getDefaultClient().createTelemetryHeader(...args),
405
+ sendTelemetryEvents: (...args) => getDefaultClient().sendTelemetryEvents(...args),
406
+ createTelemetryEmitter: (...args) => getDefaultClient().createTelemetryEmitter(...args)
407
+ };