idea-aws 4.3.4 → 4.4.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.
@@ -1,535 +0,0 @@
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 (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
- Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.ResourceController = void 0;
27
- const fs_1 = require("fs");
28
- const Lambda = __importStar(require("@aws-sdk/client-lambda"));
29
- const EventBridge = __importStar(require("@aws-sdk/client-eventbridge"));
30
- const idea_toolbox_1 = require("idea-toolbox");
31
- const metrics_1 = require("./metrics");
32
- const genericController_1 = require("./genericController");
33
- const dynamoDB_1 = require("./dynamoDB");
34
- /**
35
- * An abstract class to inherit to manage API requests (AWS API Gateway) in an AWS Lambda function.
36
- */
37
- class ResourceController extends genericController_1.GenericController {
38
- constructor(event, callback, options = {}) {
39
- super(event, callback);
40
- this.initError = false;
41
- this.project = process?.env?.PROJECT;
42
- this.stage = process?.env?.STAGE;
43
- this.resource = process?.env?.RESOURCE;
44
- this.clientVersion = '?';
45
- this.clientPlatform = '?';
46
- this.clientBundle = null;
47
- this.templateMatcher = /{{\s?([^{}\s]*)\s?}}/g;
48
- ///
49
- /// REQUEST HANDLERS
50
- ///
51
- this.handleRequest = async () => {
52
- this.logger.info('START', { event: this.getEventSummary() });
53
- if (this.initError)
54
- return;
55
- try {
56
- await this.checkAuthBeforeRequest();
57
- try {
58
- let response;
59
- if (this.resourceId) {
60
- switch (this.httpMethod) {
61
- // resource/{resourceId}
62
- case 'GET':
63
- response = await this.getResource();
64
- break;
65
- case 'POST':
66
- response = await this.postResource();
67
- break;
68
- case 'PUT':
69
- response = await this.putResource();
70
- break;
71
- case 'DELETE':
72
- response = await this.deleteResource();
73
- break;
74
- case 'PATCH':
75
- response = await this.patchResource();
76
- break;
77
- case 'HEAD':
78
- response = await this.headResource();
79
- break;
80
- default:
81
- this.done(new genericController_1.HandledError('Unsupported method'));
82
- }
83
- }
84
- else {
85
- switch (this.httpMethod) {
86
- // resource
87
- case 'GET':
88
- response = await this.getResources();
89
- break;
90
- case 'POST':
91
- response = await this.postResources();
92
- break;
93
- case 'PUT':
94
- response = await this.putResources();
95
- break;
96
- case 'DELETE':
97
- response = await this.deleteResources();
98
- break;
99
- case 'PATCH':
100
- response = await this.patchResources();
101
- break;
102
- case 'HEAD':
103
- response = await this.headResources();
104
- break;
105
- default:
106
- this.done(new genericController_1.HandledError('Unsupported method'));
107
- }
108
- }
109
- this.done(null, response);
110
- }
111
- catch (err) {
112
- this.done(this.handleControllerError(err, 'HANDLER-ERROR', 'Operation failed'));
113
- }
114
- }
115
- catch (err) {
116
- this.done(this.handleControllerError(err, 'AUTH-CHECK-ERROR', 'Forbidden'));
117
- }
118
- };
119
- this.event = event;
120
- this.callback = callback;
121
- try {
122
- if (event.version === '2.0')
123
- this.initFromEventV2(event, options);
124
- else
125
- this.initFromEventV1(event, options);
126
- this.logRequestsWithKey = options.logRequestsWithKey;
127
- // acquire some info about the client, if available
128
- if (this.queryParams['_v']) {
129
- this.clientVersion = this.queryParams['_v'];
130
- delete this.queryParams['_v'];
131
- }
132
- if (this.queryParams['_p']) {
133
- this.clientPlatform = this.queryParams['_p'];
134
- delete this.queryParams['_p'];
135
- }
136
- if (this.queryParams['_b']) {
137
- this.clientBundle = this.queryParams['_b'];
138
- delete this.queryParams['_b'];
139
- }
140
- if (options.useMetrics)
141
- this.prepareMetrics();
142
- }
143
- catch (err) {
144
- this.initError = true;
145
- this.done(this.handleControllerError(err, 'INIT-ERROR', 'Malformed request'));
146
- }
147
- }
148
- initFromEventV2(event, options) {
149
- this.authorization = event.headers.authorization;
150
- const authorizer = event.requestContext?.authorizer ?? {};
151
- const contextFromAuthorizer = authorizer.lambda ?? authorizer.jwt?.claims ?? {};
152
- this.principalId = contextFromAuthorizer.principalId ?? contextFromAuthorizer.sub ?? null;
153
- this.cognitoUser = authorizer.jwt?.claims ? new idea_toolbox_1.CognitoUser(authorizer.jwt?.claims) : null;
154
- this.auth0User = contextFromAuthorizer.auth0User ? new idea_toolbox_1.Auth0User(contextFromAuthorizer.auth0User) : null;
155
- this.stage = this.stage ?? event.requestContext.stage;
156
- this.httpMethod = event.requestContext.http.method;
157
- this.resourcePath = event.routeKey.replace('+', ''); // {proxy+} -> {proxy}
158
- this.path = event.rawPath;
159
- this.pathParameters = {};
160
- for (const param in event.pathParameters)
161
- this.pathParameters[param] = event.pathParameters[param] ? decodeURIComponent(event.pathParameters[param]) : null;
162
- this.resourceId = this.pathParameters[options.resourceId || 'proxy'];
163
- this.queryParams = event.queryStringParameters || {};
164
- try {
165
- this.body = (event.body ? JSON.parse(event.body) : {}) || {};
166
- }
167
- catch (error) {
168
- throw new genericController_1.HandledError('Malformed body');
169
- }
170
- }
171
- initFromEventV1(event, options) {
172
- this.authorization = event.headers.Authorization;
173
- this.claims = event.requestContext.authorizer?.claims || {};
174
- this.principalId = this.claims.sub;
175
- this.cognitoUser = this.principalId ? new idea_toolbox_1.CognitoUser(this.claims) : null;
176
- this.auth0User = null;
177
- this.stage = this.stage ?? event.requestContext.stage;
178
- this.httpMethod = event.httpMethod;
179
- this.resourcePath = event.resource.replace('+', ''); // {proxy+} -> {proxy}
180
- this.path = event.path;
181
- this.pathParameters = {};
182
- for (const param in event.pathParameters)
183
- this.pathParameters[param] = event.pathParameters[param] ? decodeURIComponent(event.pathParameters[param]) : null;
184
- this.resourceId = this.pathParameters[options.resourceId || 'proxy'];
185
- this.queryParams = event.queryStringParameters || {};
186
- try {
187
- this.body = (event.body ? JSON.parse(event.body) : {}) || {};
188
- }
189
- catch (error) {
190
- throw new genericController_1.HandledError('Malformed body');
191
- }
192
- }
193
- getEventSummary() {
194
- return {
195
- httpMethod: this.httpMethod,
196
- path: this.path,
197
- principalId: this.principalId,
198
- queryParams: this.queryParams,
199
- body: this.body,
200
- version: this.clientVersion,
201
- platform: this.clientPlatform,
202
- bundle: this.clientBundle
203
- };
204
- }
205
- /**
206
- * Force the parsing of a query parameter as an array of strings.
207
- */
208
- getQueryParamAsArray(paramName) {
209
- if (!this.queryParams[paramName])
210
- return [];
211
- else if (Array.isArray(this.queryParams[paramName]))
212
- return this.queryParams[paramName];
213
- else
214
- return String(this.queryParams[paramName]).split(',');
215
- }
216
- done(error, rawResult, statusCode = this.returnStatusCode ?? (error ? 400 : 200)) {
217
- const result = error ? { message: error.message } : rawResult ?? {};
218
- this.logger.debug('END-DETAIL', { result: Array.isArray(result) ? { array: result.length } : result });
219
- const finalLogContent = { statusCode, event: this.getEventSummary() };
220
- if (error) {
221
- if (error.unhandled)
222
- this.logger.error('END-FAILED', error, finalLogContent);
223
- else
224
- this.logger.warn('END-FAILED', error, finalLogContent);
225
- }
226
- else
227
- this.logger.info('END-SUCCESS', finalLogContent);
228
- if (this.logRequestsWithKey)
229
- this.storeLog(!error);
230
- if (this.metrics)
231
- this.publishMetrics(statusCode, error);
232
- this.callback(null, {
233
- statusCode: String(statusCode),
234
- body: JSON.stringify(result),
235
- headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }
236
- });
237
- }
238
- /**
239
- * To @override
240
- */
241
- async checkAuthBeforeRequest() {
242
- return;
243
- }
244
- /**
245
- * To @override
246
- */
247
- async getResource() {
248
- throw new genericController_1.HandledError('Unsupported method');
249
- }
250
- /**
251
- * To @override
252
- */
253
- async postResource() {
254
- throw new genericController_1.HandledError('Unsupported method');
255
- }
256
- /**
257
- * To @override
258
- */
259
- async putResource() {
260
- throw new genericController_1.HandledError('Unsupported method');
261
- }
262
- /**
263
- * To @override
264
- */
265
- async deleteResource() {
266
- throw new genericController_1.HandledError('Unsupported method');
267
- }
268
- /**
269
- * To @override
270
- */
271
- async headResource() {
272
- throw new genericController_1.HandledError('Unsupported method');
273
- }
274
- /**
275
- * To @override
276
- */
277
- async getResources() {
278
- throw new genericController_1.HandledError('Unsupported method');
279
- }
280
- /**
281
- * To @override
282
- */
283
- async postResources() {
284
- throw new genericController_1.HandledError('Unsupported method');
285
- }
286
- /**
287
- * To @override
288
- */
289
- async putResources() {
290
- throw new genericController_1.HandledError('Unsupported method');
291
- }
292
- /**
293
- * To @override
294
- */
295
- async patchResource() {
296
- throw new genericController_1.HandledError('Unsupported method');
297
- }
298
- /**
299
- * To @override
300
- */
301
- async patchResources() {
302
- throw new genericController_1.HandledError('Unsupported method');
303
- }
304
- /**
305
- * To @override
306
- */
307
- async deleteResources() {
308
- throw new genericController_1.HandledError('Unsupported method');
309
- }
310
- /**
311
- * To @override
312
- */
313
- async headResources() {
314
- throw new genericController_1.HandledError('Unsupported method');
315
- }
316
- ///
317
- /// HELPERS
318
- ///
319
- /**
320
- * Store the log associated to the request (no response/error handling).
321
- */
322
- async storeLog(succeeded) {
323
- const log = new idea_toolbox_1.APIRequestLog({
324
- logId: this.logRequestsWithKey,
325
- userId: this.principalId,
326
- resource: this.resourcePath,
327
- path: this.path,
328
- resourceId: this.resourceId,
329
- method: this.httpMethod,
330
- succeeded
331
- });
332
- // optionally add a track of the action
333
- if (this.httpMethod === 'PATCH' && this.body && this.body.action)
334
- log.action = this.body.action;
335
- try {
336
- await new dynamoDB_1.DynamoDB().put({ TableName: 'idea_logs', Item: log });
337
- }
338
- catch (error) {
339
- // ignore
340
- }
341
- }
342
- /**
343
- * Check whether shared resource exists in the back-end (translation, template, etc.).
344
- * Search for the specified file path in both the Lambda function's main folder and the layers folder.
345
- */
346
- sharedResourceExists(filePath) {
347
- return (0, fs_1.existsSync)(`assets/${filePath}`) || (0, fs_1.existsSync)(`/opt/nodejs/assets/${filePath}`);
348
- }
349
- /**
350
- * Load a shared resource in the back-end (translation, template, etc.).
351
- * Search for the specified file path in both the Lambda function's main folder and the layers folder.
352
- */
353
- loadSharedResource(filePath) {
354
- let path = null;
355
- if ((0, fs_1.existsSync)(`assets/${filePath}`))
356
- path = `assets/${filePath}`;
357
- else if ((0, fs_1.existsSync)(`/opt/nodejs/assets/${filePath}`))
358
- path = `/opt/nodejs/assets/${filePath}`;
359
- return path ? (0, fs_1.readFileSync)(path, { encoding: 'utf-8' }) : null;
360
- }
361
- /**
362
- * Prepare the CloudWatch metrics at the beginning of a request.
363
- */
364
- prepareMetrics() {
365
- this.metrics = new metrics_1.CloudWatchMetrics({ project: this.project });
366
- this.metrics.addDimension('stage', this.stage);
367
- this.metrics.addDimension('resource', this.resource);
368
- this.metrics.addDimension('method', this.httpMethod);
369
- this.metrics.addDimension('target', this.resourceId ? 'id' : 'list');
370
- this.metrics.addDimension('action', this.body?.action);
371
- this.metrics.addDimension('userId', this.principalId);
372
- this.metrics.addDimension('clientVersion', this.clientVersion);
373
- this.metrics.addDimension('clientPlatform', this.clientPlatform);
374
- this.metrics.addDimension('clientBundle', this.clientBundle ?? '-');
375
- this.metrics.addMetadata('resourceId', this.resourceId);
376
- }
377
- /**
378
- * Publish the CloudWatch metrics (default and custom-defined) at the end of a reqeust.
379
- */
380
- publishMetrics(statusCode, error) {
381
- if (!this.metrics)
382
- return;
383
- this.metrics.addMetric('request');
384
- this.metrics.addMetric('statusCode', statusCode);
385
- if (error) {
386
- this.metrics.addMetric('failed');
387
- this.metrics.addMetadata('error', error.name);
388
- }
389
- else
390
- this.metrics.addMetric('success');
391
- this.metrics.publishStoredMetrics();
392
- }
393
- ///
394
- /// MANAGE INTERNAL API REQUESTS (lambda invokes masked as API requests)
395
- ///
396
- /**
397
- * Simulate an internal API request, invoking directly the lambda and therefore saving resources.
398
- * @return the body of the response
399
- * @deprecated don't run a Lambda from another Lambda (bad practice)
400
- */
401
- async invokeInternalAPIRequest(params) {
402
- if (params.lambda)
403
- return await this.invokeInternalAPIRequestWithLambda(params);
404
- if (params.eventBridge)
405
- return await this.invokeInternalAPIRequestWithEventBridge(params);
406
- throw new Error('Either "lambda" or "eventBus" parameters must be set.');
407
- }
408
- async invokeInternalAPIRequestWithLambda(params) {
409
- const command = new Lambda.InvokeCommand({
410
- FunctionName: params.lambda,
411
- InvocationType: 'RequestResponse',
412
- Payload: this.mapEventForInternalApiRequest(params),
413
- Qualifier: params.stage ?? this.stage
414
- });
415
- const client = new Lambda.LambdaClient();
416
- const { Payload } = await client.send(command);
417
- const payload = JSON.parse(Buffer.from(Payload).toString());
418
- const body = JSON.parse(payload.body);
419
- if (Number(payload.statusCode) !== 200)
420
- throw new Error(body.message);
421
- return body;
422
- }
423
- async invokeInternalAPIRequestWithEventBridge(params) {
424
- const request = {
425
- EventBusName: params.eventBridge.bus,
426
- Source: this.constructor.name,
427
- DetailType: params.eventBridge.target,
428
- Detail: this.mapEventForInternalApiRequest(params)
429
- };
430
- const client = new EventBridge.EventBridgeClient();
431
- const command = new EventBridge.PutEventsCommand({ Entries: [request] });
432
- return await client.send(command);
433
- }
434
- mapEventForInternalApiRequest(params) {
435
- const event = JSON.parse(JSON.stringify(this.event));
436
- // change only the event attributes we need; e.g. the authorization is unchanged
437
- if (!event.requestContext)
438
- event.requestContext = {};
439
- event.requestContext.stage = params.stage ?? this.stage;
440
- if (!event.requestContext.http)
441
- event.requestContext.http = {};
442
- event.requestContext.http.method = event.httpMethod = params.httpMethod;
443
- event.routeKey = event.resource = params.resource;
444
- event.pathParameters = params.pathParams ?? {};
445
- event.queryStringParameters = params.queryParams ?? {};
446
- event.body = JSON.stringify(params.body ?? {});
447
- event.rawPath = event.path = params.resource;
448
- for (const p in event.pathParameters)
449
- if (event.pathParameters[p])
450
- event.rawPath = event.path = event.path.replace(`{${p}}`, event.pathParameters[p]);
451
- // set a flag to make the invoked to recognise that is an internal request
452
- event.internalAPIRequest = true;
453
- return JSON.stringify(event);
454
- }
455
- /**
456
- * Whether the current request comes from an internal API request, i.e. it was invoked by another controller.
457
- * @deprecated don't run a Lambda from another Lambda (bad practice)
458
- */
459
- comesFromInternalRequest() {
460
- return Boolean(this.event.internalAPIRequest);
461
- }
462
- //
463
- // TRANSLATIONS
464
- //
465
- /**
466
- * Load the translations from the shared resources and set them with a fallback language.
467
- */
468
- loadTranslations(lang, defLang) {
469
- // check for the existance of the mandatory source file
470
- if (!this.sharedResourceExists(`i18n/${lang}.json`))
471
- return;
472
- // set the languages
473
- this.currentLang = lang;
474
- this.defaultLang = defLang ?? lang;
475
- this.translations = {};
476
- // load the translations in the chosen language
477
- this.translations[this.currentLang] = JSON.parse(this.loadSharedResource(`i18n/${this.currentLang}.json`).toString());
478
- // load the translations in the default language, if set and differ from the current
479
- if (this.defaultLang !== this.currentLang && this.sharedResourceExists(`i18n/${this.defaultLang}.json`))
480
- this.translations[this.defaultLang] = JSON.parse(this.loadSharedResource(`i18n/${this.defaultLang}.json`).toString());
481
- }
482
- /**
483
- * Get a translated term by key, optionally interpolating variables (e.g. `{{user}}`).
484
- * If the term doesn't exist in the current language, it is searched in the default language.
485
- */
486
- t(key, interpolateParams) {
487
- if (!this.translations || !this.currentLang)
488
- return;
489
- if (!this.isDefined(key) || !key.length)
490
- return;
491
- let res = this.interpolate(this.getValue(this.translations[this.currentLang], key), interpolateParams);
492
- if (res === undefined && this.defaultLang !== null && this.defaultLang !== this.currentLang)
493
- res = this.interpolate(this.getValue(this.translations[this.defaultLang], key), interpolateParams);
494
- return res;
495
- }
496
- /**
497
- * Interpolates a string to replace parameters.
498
- * `"This is a {{ key }}"` ==> `"This is a value", with params = { key: "value" }`.
499
- */
500
- interpolate(expr, params) {
501
- if (!params || !expr)
502
- return expr;
503
- return expr.replace(this.templateMatcher, (substring, b) => {
504
- const r = this.getValue(params, b);
505
- return this.isDefined(r) ? r : substring;
506
- });
507
- }
508
- /**
509
- * Gets a value from an object by composed key.
510
- * `getValue({ key1: { keyA: 'valueI' }}, 'key1.keyA')` ==> `'valueI'`.
511
- */
512
- getValue(target, key) {
513
- const keys = typeof key === 'string' ? key.split('.') : [key];
514
- key = '';
515
- do {
516
- key += keys.shift();
517
- if (this.isDefined(target) && this.isDefined(target[key]) && (typeof target[key] === 'object' || !keys.length)) {
518
- target = target[key];
519
- key = '';
520
- }
521
- else if (!keys.length)
522
- target = undefined;
523
- else
524
- key += '.';
525
- } while (keys.length);
526
- return target;
527
- }
528
- /**
529
- * Helper to quicly check if the value is defined.
530
- */
531
- isDefined(value) {
532
- return value !== undefined && value !== null;
533
- }
534
- }
535
- exports.ResourceController = ResourceController;