idea-aws 4.4.1 → 4.4.2

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.
Files changed (48) hide show
  1. package/dist/src/attachments.d.ts +27 -0
  2. package/dist/src/attachments.js +39 -0
  3. package/dist/src/cognito.d.ts +177 -0
  4. package/dist/src/cognito.js +412 -0
  5. package/dist/src/comprehend.d.ts +34 -0
  6. package/dist/src/comprehend.js +58 -0
  7. package/dist/src/dynamoDB.d.ts +108 -0
  8. package/dist/src/dynamoDB.js +296 -0
  9. package/dist/src/genericController.d.ts +60 -0
  10. package/dist/src/genericController.js +89 -0
  11. package/dist/src/lambdaLogger.d.ts +13 -0
  12. package/dist/src/lambdaLogger.js +43 -0
  13. package/dist/src/logger.d.ts +10 -0
  14. package/dist/src/logger.js +16 -0
  15. package/dist/src/metrics.d.ts +31 -0
  16. package/dist/src/metrics.js +45 -0
  17. package/dist/src/resourceController.d.ts +232 -0
  18. package/dist/src/resourceController.js +561 -0
  19. package/dist/src/s3.d.ts +225 -0
  20. package/dist/src/s3.js +180 -0
  21. package/dist/src/secretsManager.d.ts +15 -0
  22. package/dist/src/secretsManager.js +48 -0
  23. package/dist/src/ses.d.ts +161 -0
  24. package/dist/src/ses.js +196 -0
  25. package/dist/src/sns.d.ts +60 -0
  26. package/dist/src/sns.js +94 -0
  27. package/dist/src/ssm.d.ts +22 -0
  28. package/dist/src/ssm.js +54 -0
  29. package/dist/src/streamController.d.ts +11 -0
  30. package/dist/src/streamController.js +20 -0
  31. package/dist/src/translate.d.ts +61 -0
  32. package/dist/src/translate.js +155 -0
  33. package/package.json +2 -2
  34. package/src/attachments.ts +41 -0
  35. package/src/cognito.ts +511 -0
  36. package/src/comprehend.ts +52 -0
  37. package/src/dynamoDB.ts +311 -0
  38. package/src/genericController.ts +103 -0
  39. package/src/lambdaLogger.ts +39 -0
  40. package/src/metrics.ts +45 -0
  41. package/src/resourceController.ts +645 -0
  42. package/src/s3.ts +334 -0
  43. package/src/secretsManager.ts +24 -0
  44. package/src/ses.ts +313 -0
  45. package/src/sns.ts +118 -0
  46. package/src/ssm.ts +33 -0
  47. package/src/streamController.ts +25 -0
  48. package/src/translate.ts +174 -0
@@ -0,0 +1,645 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import * as Lambda from '@aws-sdk/client-lambda';
3
+ import * as EventBridge from '@aws-sdk/client-eventbridge';
4
+ import { Tracer } from '@aws-lambda-powertools/tracer';
5
+ import { APIGatewayProxyEventV2, APIGatewayProxyEvent, Callback } from 'aws-lambda';
6
+ import { APIRequestLog, CognitoUser, Auth0User } from 'idea-toolbox';
7
+
8
+ import { CloudWatchMetrics } from './metrics';
9
+ import { GenericController, HandledError, UnhandledError } from './genericController';
10
+ import { DynamoDB } from './dynamoDB';
11
+
12
+ const ENV = process?.env ?? {};
13
+ const { PROJECT, STAGE, RESOURCE } = ENV;
14
+ ENV.POWERTOOLS_SERVICE_NAME = [PROJECT, STAGE, RESOURCE].filter(x => x).join('_');
15
+
16
+ /**
17
+ * An abstract class to inherit to manage API requests (AWS API Gateway) in an AWS Lambda function.
18
+ */
19
+ export abstract class ResourceController extends GenericController {
20
+ protected event: APIGatewayProxyEventV2 | APIGatewayProxyEvent;
21
+ protected callback: Callback;
22
+
23
+ protected initError = false;
24
+
25
+ protected authorization: string;
26
+ protected claims: any;
27
+ protected principalId: string;
28
+ protected cognitoUser: CognitoUser;
29
+ protected auth0User: Auth0User;
30
+
31
+ protected project = PROJECT;
32
+ protected stage = STAGE;
33
+ protected httpMethod: string;
34
+ protected body: any;
35
+ protected queryParams: any;
36
+ protected resourcePath: string;
37
+ protected path: string;
38
+ protected pathParameters: any;
39
+ protected resource = RESOURCE;
40
+ protected resourceId: string;
41
+
42
+ protected clientVersion = '?';
43
+ protected clientPlatform = '?';
44
+ protected clientBundle: string = null;
45
+
46
+ protected returnStatusCode?: number;
47
+
48
+ protected logRequestsWithKey: string;
49
+
50
+ protected metrics: CloudWatchMetrics;
51
+
52
+ protected tracer: Tracer;
53
+
54
+ protected currentLang: string;
55
+ protected defaultLang: string;
56
+ protected translations: any;
57
+ protected templateMatcher = /{{\s?([^{}\s]*)\s?}}/g;
58
+
59
+ constructor(
60
+ event: APIGatewayProxyEventV2 | APIGatewayProxyEvent,
61
+ callback: Callback,
62
+ options: ResourceControllerOptions = {}
63
+ ) {
64
+ super(event, callback);
65
+
66
+ this.event = event;
67
+ this.callback = callback;
68
+
69
+ try {
70
+ if ((event as APIGatewayProxyEventV2).version === '2.0')
71
+ this.initFromEventV2(event as APIGatewayProxyEventV2, options);
72
+ else this.initFromEventV1(event as APIGatewayProxyEvent, options);
73
+
74
+ this.logRequestsWithKey = options.logRequestsWithKey;
75
+
76
+ this.tracer = options.tracer;
77
+
78
+ // acquire some info about the client, if available
79
+ if (this.queryParams['_v']) {
80
+ this.clientVersion = this.queryParams['_v'];
81
+ delete this.queryParams['_v'];
82
+ }
83
+ if (this.queryParams['_p']) {
84
+ this.clientPlatform = this.queryParams['_p'];
85
+ delete this.queryParams['_p'];
86
+ }
87
+ if (this.queryParams['_b']) {
88
+ this.clientBundle = this.queryParams['_b'];
89
+ delete this.queryParams['_b'];
90
+ }
91
+
92
+ if (options.useMetrics) this.prepareMetrics();
93
+ } catch (err) {
94
+ this.initError = true;
95
+ this.done(this.handleControllerError(err, 'INIT-ERROR', 'Malformed request'));
96
+ }
97
+ }
98
+ private initFromEventV2(event: APIGatewayProxyEventV2, options: ResourceControllerOptions): void {
99
+ this.authorization = event.headers.authorization;
100
+ const authorizer = (event.requestContext as any)?.authorizer ?? {};
101
+ const contextFromAuthorizer = authorizer.lambda ?? authorizer.jwt?.claims ?? {};
102
+ this.principalId = contextFromAuthorizer.principalId ?? contextFromAuthorizer.sub ?? null;
103
+ this.cognitoUser = authorizer.jwt?.claims ? new CognitoUser(authorizer.jwt?.claims) : null;
104
+ this.auth0User = contextFromAuthorizer.auth0User ? new Auth0User(contextFromAuthorizer.auth0User) : null;
105
+
106
+ this.stage = this.stage ?? event.requestContext.stage;
107
+ this.httpMethod = event.requestContext.http.method;
108
+ this.resourcePath = event.routeKey.replace('+', ''); // {proxy+} -> {proxy}
109
+ this.path = event.rawPath;
110
+ this.pathParameters = {};
111
+ for (const param in event.pathParameters)
112
+ this.pathParameters[param] = event.pathParameters[param] ? decodeURIComponent(event.pathParameters[param]) : null;
113
+ this.resourceId = this.pathParameters[options.resourceId || 'proxy'];
114
+ this.queryParams = event.queryStringParameters || {};
115
+ try {
116
+ this.body = (event.body ? JSON.parse(event.body) : {}) || {};
117
+ } catch (error) {
118
+ throw new HandledError('Malformed body');
119
+ }
120
+ }
121
+ private initFromEventV1(event: APIGatewayProxyEvent, options: ResourceControllerOptions): void {
122
+ this.authorization = event.headers.Authorization;
123
+ this.claims = event.requestContext.authorizer?.claims || {};
124
+ this.principalId = this.claims.sub;
125
+ this.cognitoUser = this.principalId ? new CognitoUser(this.claims) : null;
126
+ this.auth0User = null;
127
+
128
+ this.stage = this.stage ?? event.requestContext.stage;
129
+ this.httpMethod = event.httpMethod;
130
+ this.resourcePath = event.resource.replace('+', ''); // {proxy+} -> {proxy}
131
+ this.path = event.path;
132
+ this.pathParameters = {};
133
+ for (const param in event.pathParameters)
134
+ this.pathParameters[param] = event.pathParameters[param] ? decodeURIComponent(event.pathParameters[param]) : null;
135
+ this.resourceId = this.pathParameters[options.resourceId || 'proxy'];
136
+ this.queryParams = event.queryStringParameters || {};
137
+ try {
138
+ this.body = (event.body ? JSON.parse(event.body) : {}) || {};
139
+ } catch (error) {
140
+ throw new HandledError('Malformed body');
141
+ }
142
+ }
143
+ protected getEventSummary(): Record<string, any> {
144
+ return {
145
+ httpMethod: this.httpMethod,
146
+ path: this.path,
147
+ principalId: this.principalId,
148
+ queryParams: this.queryParams,
149
+ body: this.body,
150
+ version: this.clientVersion,
151
+ platform: this.clientPlatform,
152
+ bundle: this.clientBundle
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Force the parsing of a query parameter as an array of strings.
158
+ */
159
+ protected getQueryParamAsArray(paramName: string): string[] {
160
+ if (!this.queryParams[paramName]) return [];
161
+ else if (Array.isArray(this.queryParams[paramName])) return this.queryParams[paramName];
162
+ else return String(this.queryParams[paramName]).split(',');
163
+ }
164
+
165
+ ///
166
+ /// REQUEST HANDLERS
167
+ ///
168
+
169
+ handleRequest = async (): Promise<void> => {
170
+ if (this.initError) return;
171
+
172
+ this.logger.info('START', { event: this.getEventSummary() });
173
+
174
+ let lambdaSegment, rcSegment;
175
+ if (this.tracer) {
176
+ lambdaSegment = this.tracer.getSegment();
177
+ if (lambdaSegment) {
178
+ rcSegment = lambdaSegment.addNewSubsegment('RC');
179
+ this.tracer.setSegment(rcSegment);
180
+ }
181
+ this.tracer.annotateColdStart();
182
+ this.tracer.addServiceNameAnnotation();
183
+ this.tracer.putMetadata('START', { event: this.getEventSummary() });
184
+ }
185
+
186
+ try {
187
+ await this.checkAuthBeforeRequest();
188
+
189
+ try {
190
+ let response;
191
+ if (this.resourceId) {
192
+ switch (this.httpMethod) {
193
+ // resource/{resourceId}
194
+ case 'GET':
195
+ response = await this.getResource();
196
+ break;
197
+ case 'POST':
198
+ response = await this.postResource();
199
+ break;
200
+ case 'PUT':
201
+ response = await this.putResource();
202
+ break;
203
+ case 'DELETE':
204
+ response = await this.deleteResource();
205
+ break;
206
+ case 'PATCH':
207
+ response = await this.patchResource();
208
+ break;
209
+ case 'HEAD':
210
+ response = await this.headResource();
211
+ break;
212
+ default:
213
+ this.done(new HandledError('Unsupported method'));
214
+ }
215
+ } else {
216
+ switch (this.httpMethod) {
217
+ // resource
218
+ case 'GET':
219
+ response = await this.getResources();
220
+ break;
221
+ case 'POST':
222
+ response = await this.postResources();
223
+ break;
224
+ case 'PUT':
225
+ response = await this.putResources();
226
+ break;
227
+ case 'DELETE':
228
+ response = await this.deleteResources();
229
+ break;
230
+ case 'PATCH':
231
+ response = await this.patchResources();
232
+ break;
233
+ case 'HEAD':
234
+ response = await this.headResources();
235
+ break;
236
+ default:
237
+ this.done(new HandledError('Unsupported method'));
238
+ }
239
+ }
240
+
241
+ this.done(null, response);
242
+ } catch (err) {
243
+ this.done(this.handleControllerError(err, 'HANDLER-ERROR', 'Operation failed'));
244
+ }
245
+ } catch (err) {
246
+ this.done(this.handleControllerError(err, 'AUTH-CHECK-ERROR', 'Forbidden'));
247
+ } finally {
248
+ if (this.tracer && lambdaSegment && rcSegment) {
249
+ rcSegment.close();
250
+ this.tracer.setSegment(lambdaSegment);
251
+ }
252
+ }
253
+ };
254
+ protected done(
255
+ error?: Error | any,
256
+ rawResult?: any,
257
+ statusCode = this.returnStatusCode ?? (error ? 400 : 200)
258
+ ): void {
259
+ const result = error ? { message: error.message } : rawResult ?? {};
260
+
261
+ const responseTrace = { result: Array.isArray(result) ? { array: result.length } : result };
262
+ this.logger.debug('END-DETAIL', responseTrace);
263
+ if (this.tracer) this.tracer.addResponseAsMetadata(responseTrace, 'END-DETAIL');
264
+
265
+ const finalLogContent = { statusCode, event: this.getEventSummary() };
266
+ if (error) {
267
+ if ((error as UnhandledError).unhandled) this.logger.error('END-FAILED', error, finalLogContent);
268
+ else this.logger.warn('END-FAILED', error, finalLogContent);
269
+ if (this.tracer) this.tracer.addErrorAsMetadata(error);
270
+ } else this.logger.info('END-SUCCESS', finalLogContent);
271
+
272
+ if (this.logRequestsWithKey) this.storeLog(!error);
273
+
274
+ if (this.metrics) this.publishMetrics(statusCode, error);
275
+
276
+ this.callback(null, {
277
+ statusCode: String(statusCode),
278
+ body: JSON.stringify(result),
279
+ headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }
280
+ });
281
+ }
282
+
283
+ /**
284
+ * To @override
285
+ */
286
+ protected async checkAuthBeforeRequest(): Promise<void> {
287
+ return;
288
+ }
289
+ /**
290
+ * To @override
291
+ */
292
+ protected async getResource(): Promise<any> {
293
+ throw new HandledError('Unsupported method');
294
+ }
295
+ /**
296
+ * To @override
297
+ */
298
+ protected async postResource(): Promise<any> {
299
+ throw new HandledError('Unsupported method');
300
+ }
301
+ /**
302
+ * To @override
303
+ */
304
+ protected async putResource(): Promise<any> {
305
+ throw new HandledError('Unsupported method');
306
+ }
307
+ /**
308
+ * To @override
309
+ */
310
+ protected async deleteResource(): Promise<any> {
311
+ throw new HandledError('Unsupported method');
312
+ }
313
+ /**
314
+ * To @override
315
+ */
316
+ protected async headResource(): Promise<any> {
317
+ throw new HandledError('Unsupported method');
318
+ }
319
+ /**
320
+ * To @override
321
+ */
322
+ protected async getResources(): Promise<any> {
323
+ throw new HandledError('Unsupported method');
324
+ }
325
+ /**
326
+ * To @override
327
+ */
328
+ protected async postResources(): Promise<any> {
329
+ throw new HandledError('Unsupported method');
330
+ }
331
+ /**
332
+ * To @override
333
+ */
334
+ protected async putResources(): Promise<any> {
335
+ throw new HandledError('Unsupported method');
336
+ }
337
+ /**
338
+ * To @override
339
+ */
340
+ protected async patchResource(): Promise<any> {
341
+ throw new HandledError('Unsupported method');
342
+ }
343
+ /**
344
+ * To @override
345
+ */
346
+ protected async patchResources(): Promise<any> {
347
+ throw new HandledError('Unsupported method');
348
+ }
349
+ /**
350
+ * To @override
351
+ */
352
+ protected async deleteResources(): Promise<any> {
353
+ throw new HandledError('Unsupported method');
354
+ }
355
+ /**
356
+ * To @override
357
+ */
358
+ protected async headResources(): Promise<any> {
359
+ throw new HandledError('Unsupported method');
360
+ }
361
+
362
+ ///
363
+ /// HELPERS
364
+ ///
365
+
366
+ /**
367
+ * Store the log associated to the request (no response/error handling).
368
+ */
369
+ protected async storeLog(succeeded: boolean): Promise<void> {
370
+ const log = new APIRequestLog({
371
+ logId: this.logRequestsWithKey,
372
+ userId: this.principalId,
373
+ resource: this.resourcePath,
374
+ path: this.path,
375
+ resourceId: this.resourceId,
376
+ method: this.httpMethod,
377
+ succeeded
378
+ });
379
+
380
+ // optionally add a track of the action
381
+ if (this.httpMethod === 'PATCH' && this.body && this.body.action) log.action = this.body.action;
382
+
383
+ try {
384
+ await new DynamoDB().put({ TableName: 'idea_logs', Item: log });
385
+ } catch (error) {
386
+ // ignore
387
+ }
388
+ }
389
+ /**
390
+ * Check whether shared resource exists in the back-end (translation, template, etc.).
391
+ * Search for the specified file path in both the Lambda function's main folder and the layers folder.
392
+ */
393
+ protected sharedResourceExists(filePath: string): boolean {
394
+ return existsSync(`assets/${filePath}`) || existsSync(`/opt/nodejs/assets/${filePath}`);
395
+ }
396
+ /**
397
+ * Load a shared resource in the back-end (translation, template, etc.).
398
+ * Search for the specified file path in both the Lambda function's main folder and the layers folder.
399
+ */
400
+ protected loadSharedResource(filePath: string): string {
401
+ let path: string = null;
402
+
403
+ if (existsSync(`assets/${filePath}`)) path = `assets/${filePath}`;
404
+ else if (existsSync(`/opt/nodejs/assets/${filePath}`)) path = `/opt/nodejs/assets/${filePath}`;
405
+
406
+ return path ? readFileSync(path, { encoding: 'utf-8' }) : null;
407
+ }
408
+
409
+ /**
410
+ * Prepare the CloudWatch metrics at the beginning of a request.
411
+ */
412
+ protected prepareMetrics(): void {
413
+ this.metrics = new CloudWatchMetrics({ project: this.project });
414
+ this.metrics.addDimension('stage', this.stage);
415
+ this.metrics.addDimension('resource', this.resource);
416
+ this.metrics.addDimension('method', this.httpMethod);
417
+ this.metrics.addDimension('target', this.resourceId ? 'id' : 'list');
418
+ this.metrics.addDimension('action', this.body?.action);
419
+ this.metrics.addDimension('userId', this.principalId);
420
+ this.metrics.addDimension('clientVersion', this.clientVersion);
421
+ this.metrics.addDimension('clientPlatform', this.clientPlatform);
422
+ this.metrics.addDimension('clientBundle', this.clientBundle ?? '-');
423
+ this.metrics.addMetadata('resourceId', this.resourceId);
424
+ }
425
+ /**
426
+ * Publish the CloudWatch metrics (default and custom-defined) at the end of a reqeust.
427
+ */
428
+ protected publishMetrics(statusCode: number, error?: any): void {
429
+ if (!this.metrics) return;
430
+ this.metrics.addMetric('request');
431
+ this.metrics.addMetric('statusCode', statusCode);
432
+ if (error) {
433
+ this.metrics.addMetric('failed');
434
+ this.metrics.addMetadata('error', error.name);
435
+ } else this.metrics.addMetric('success');
436
+ this.metrics.publishStoredMetrics();
437
+ }
438
+
439
+ ///
440
+ /// MANAGE INTERNAL API REQUESTS (lambda invokes masked as API requests)
441
+ ///
442
+
443
+ /**
444
+ * Simulate an internal API request, invoking directly the lambda and therefore saving resources.
445
+ * @return the body of the response
446
+ * @deprecated don't run a Lambda from another Lambda (bad practice)
447
+ */
448
+ async invokeInternalAPIRequest(params: InternalAPIRequestParams): Promise<any> {
449
+ if (params.lambda) return await this.invokeInternalAPIRequestWithLambda(params);
450
+ if (params.eventBridge) return await this.invokeInternalAPIRequestWithEventBridge(params);
451
+ throw new Error('Either "lambda" or "eventBus" parameters must be set.');
452
+ }
453
+ private async invokeInternalAPIRequestWithLambda(params: InternalAPIRequestParams): Promise<any> {
454
+ const command = new Lambda.InvokeCommand({
455
+ FunctionName: params.lambda,
456
+ InvocationType: 'RequestResponse',
457
+ Payload: this.mapEventForInternalApiRequest(params),
458
+ Qualifier: params.stage ?? this.stage
459
+ });
460
+ const client = new Lambda.LambdaClient();
461
+ const { Payload } = await client.send(command);
462
+ const payload = JSON.parse(Buffer.from(Payload).toString());
463
+ const body = JSON.parse(payload.body);
464
+ if (Number(payload.statusCode) !== 200) throw new Error(body.message);
465
+ return body;
466
+ }
467
+ private async invokeInternalAPIRequestWithEventBridge(
468
+ params: InternalAPIRequestParams
469
+ ): Promise<EventBridge.PutEventsCommandOutput> {
470
+ const request = {
471
+ EventBusName: params.eventBridge.bus,
472
+ Source: this.constructor.name,
473
+ DetailType: params.eventBridge.target,
474
+ Detail: this.mapEventForInternalApiRequest(params)
475
+ };
476
+ const client = new EventBridge.EventBridgeClient();
477
+ const command = new EventBridge.PutEventsCommand({ Entries: [request] });
478
+ return await client.send(command);
479
+ }
480
+ private mapEventForInternalApiRequest(params: InternalAPIRequestParams): string {
481
+ const event = JSON.parse(JSON.stringify(this.event));
482
+
483
+ // change only the event attributes we need; e.g. the authorization is unchanged
484
+ if (!event.requestContext) event.requestContext = {};
485
+ event.requestContext.stage = params.stage ?? this.stage;
486
+ if (!event.requestContext.http) event.requestContext.http = {};
487
+ event.requestContext.http.method = event.httpMethod = params.httpMethod;
488
+ event.routeKey = event.resource = params.resource;
489
+ event.pathParameters = params.pathParams ?? {};
490
+ event.queryStringParameters = params.queryParams ?? {};
491
+ event.body = JSON.stringify(params.body ?? {});
492
+ event.rawPath = event.path = params.resource;
493
+ for (const p in event.pathParameters)
494
+ if (event.pathParameters[p]) event.rawPath = event.path = event.path.replace(`{${p}}`, event.pathParameters[p]);
495
+ // set a flag to make the invoked to recognise that is an internal request
496
+ event.internalAPIRequest = true;
497
+
498
+ return JSON.stringify(event);
499
+ }
500
+ /**
501
+ * Whether the current request comes from an internal API request, i.e. it was invoked by another controller.
502
+ * @deprecated don't run a Lambda from another Lambda (bad practice)
503
+ */
504
+ comesFromInternalRequest(): boolean {
505
+ return Boolean((this.event as any).internalAPIRequest);
506
+ }
507
+
508
+ //
509
+ // TRANSLATIONS
510
+ //
511
+
512
+ /**
513
+ * Load the translations from the shared resources and set them with a fallback language.
514
+ */
515
+ loadTranslations(lang: string, defLang?: string): void {
516
+ // check for the existance of the mandatory source file
517
+ if (!this.sharedResourceExists(`i18n/${lang}.json`)) return;
518
+ // set the languages
519
+ this.currentLang = lang;
520
+ this.defaultLang = defLang ?? lang;
521
+ this.translations = {};
522
+ // load the translations in the chosen language
523
+ this.translations[this.currentLang] = JSON.parse(
524
+ this.loadSharedResource(`i18n/${this.currentLang}.json`).toString()
525
+ );
526
+ // load the translations in the default language, if set and differ from the current
527
+ if (this.defaultLang !== this.currentLang && this.sharedResourceExists(`i18n/${this.defaultLang}.json`))
528
+ this.translations[this.defaultLang] = JSON.parse(
529
+ this.loadSharedResource(`i18n/${this.defaultLang}.json`).toString()
530
+ );
531
+ }
532
+ /**
533
+ * Get a translated term by key, optionally interpolating variables (e.g. `{{user}}`).
534
+ * If the term doesn't exist in the current language, it is searched in the default language.
535
+ */
536
+ t(key: string, interpolateParams?: any): string {
537
+ if (!this.translations || !this.currentLang) return;
538
+ if (!this.isDefined(key) || !key.length) return;
539
+ let res = this.interpolate(this.getValue(this.translations[this.currentLang], key), interpolateParams);
540
+ if (res === undefined && this.defaultLang !== null && this.defaultLang !== this.currentLang)
541
+ res = this.interpolate(this.getValue(this.translations[this.defaultLang], key), interpolateParams);
542
+ return res;
543
+ }
544
+ /**
545
+ * Interpolates a string to replace parameters.
546
+ * `"This is a {{ key }}"` ==> `"This is a value", with params = { key: "value" }`.
547
+ */
548
+ private interpolate(expr: string, params?: any): string {
549
+ if (!params || !expr) return expr;
550
+ return expr.replace(this.templateMatcher, (substring: string, b: string) => {
551
+ const r = this.getValue(params, b);
552
+ return this.isDefined(r) ? r : substring;
553
+ });
554
+ }
555
+ /**
556
+ * Gets a value from an object by composed key.
557
+ * `getValue({ key1: { keyA: 'valueI' }}, 'key1.keyA')` ==> `'valueI'`.
558
+ */
559
+ private getValue(target: any, key: string): any {
560
+ const keys = typeof key === 'string' ? key.split('.') : [key];
561
+ key = '';
562
+ do {
563
+ key += keys.shift();
564
+ if (this.isDefined(target) && this.isDefined(target[key]) && (typeof target[key] === 'object' || !keys.length)) {
565
+ target = target[key];
566
+ key = '';
567
+ } else if (!keys.length) target = undefined;
568
+ else key += '.';
569
+ } while (keys.length);
570
+ return target;
571
+ }
572
+ /**
573
+ * Helper to quicly check if the value is defined.
574
+ */
575
+ private isDefined(value: any): boolean {
576
+ return value !== undefined && value !== null;
577
+ }
578
+ }
579
+
580
+ /**
581
+ * The initial options for a constructor of class ResourceController.
582
+ */
583
+ export interface ResourceControllerOptions {
584
+ /**
585
+ * The resourceId of the API request, to specify if different from "proxy".
586
+ */
587
+ resourceId?: string;
588
+ /**
589
+ * If set, the logs of the API requests on this resource will be stored (using this key).
590
+ */
591
+ logRequestsWithKey?: string;
592
+ /**
593
+ * Whether to automatically store usage metrics on CloudWatch.
594
+ */
595
+ useMetrics?: boolean;
596
+ /**
597
+ * The instance of the tracer to use in case of advanced monitoring.
598
+ */
599
+ tracer?: Tracer;
600
+ }
601
+
602
+ /**
603
+ * The parameters needed to invoke an internal API request.
604
+ * @deprecated don't run a Lambda from another Lambda (bad practice).
605
+ */
606
+ export interface InternalAPIRequestParams {
607
+ /**
608
+ * The name of the Lambda function receiving the request; e.g. `project_memberships`.
609
+ * Note: the invocation is always syncronous.
610
+ * Either this attribute or `eventBus` must be set.
611
+ */
612
+ lambda?: string;
613
+ /**
614
+ * The EventBridge destination of the request.
615
+ * If the bus name or ARN isn't specified, the default one is used.
616
+ * The `target` maps into the `DetailType` of the event.
617
+ * Note: the invocation is always asyncronous.
618
+ * Either this attribute or `lambda` must be set.
619
+ */
620
+ eventBridge?: { bus?: string; target?: string };
621
+ /**
622
+ * The alias of the lambda function to invoke. Default: the value of the current API stage.
623
+ */
624
+ stage?: string;
625
+ /**
626
+ * The http method to use.
627
+ */
628
+ httpMethod: string;
629
+ /**
630
+ * The path (in the internal API) to the resource we need; e.g. `teams/{teamId}/memberships/{userId}`.
631
+ */
632
+ resource: string;
633
+ /**
634
+ * The parameters to substitute in the path.
635
+ */
636
+ pathParams?: { [index: string]: string | number };
637
+ /**
638
+ * The parameters to substitute in the path.
639
+ */
640
+ queryParams?: { [index: string]: string | number };
641
+ /**
642
+ * The body of the request.
643
+ */
644
+ body?: any;
645
+ }