idea-aws 4.4.3 → 4.4.4
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 -1
- package/.prettierrc +0 -9
- package/.vscode/settings.json +0 -26
- package/HOW-TO-RELEASE.md +0 -8
- package/build.sh +0 -21
- package/dist/src/attachments.d.ts +0 -27
- package/dist/src/attachments.js +0 -39
- package/dist/src/cognito.d.ts +0 -177
- package/dist/src/cognito.js +0 -412
- package/dist/src/comprehend.d.ts +0 -34
- package/dist/src/comprehend.js +0 -58
- package/dist/src/dynamoDB.d.ts +0 -108
- package/dist/src/dynamoDB.js +0 -296
- package/dist/src/genericController.d.ts +0 -60
- package/dist/src/genericController.js +0 -89
- package/dist/src/lambdaLogger.d.ts +0 -13
- package/dist/src/lambdaLogger.js +0 -43
- package/dist/src/metrics.d.ts +0 -31
- package/dist/src/metrics.js +0 -45
- package/dist/src/resourceController.d.ts +0 -232
- package/dist/src/resourceController.js +0 -561
- package/dist/src/s3.d.ts +0 -225
- package/dist/src/s3.js +0 -180
- package/dist/src/secretsManager.d.ts +0 -15
- package/dist/src/secretsManager.js +0 -48
- package/dist/src/ses.d.ts +0 -161
- package/dist/src/ses.js +0 -196
- package/dist/src/sns.d.ts +0 -60
- package/dist/src/sns.js +0 -94
- package/dist/src/ssm.d.ts +0 -22
- package/dist/src/ssm.js +0 -54
- package/dist/src/streamController.d.ts +0 -11
- package/dist/src/streamController.js +0 -20
- package/dist/src/translate.d.ts +0 -61
- package/dist/src/translate.js +0 -155
- package/docs/.nojekyll +0 -1
- package/docs/assets/custom.css +0 -4
- package/docs/assets/highlight.css +0 -50
- package/docs/assets/main.js +0 -58
- package/docs/assets/search.js +0 -1
- package/docs/assets/style.css +0 -1367
- package/docs/classes/Attachments.html +0 -201
- package/docs/classes/Cognito.html +0 -713
- package/docs/classes/Comprehend.html +0 -176
- package/docs/classes/DynamoDB.html +0 -584
- package/docs/classes/GenericController.html +0 -262
- package/docs/classes/HandledError.html +0 -219
- package/docs/classes/LambdaLogger.html +0 -220
- package/docs/classes/ResourceController.html +0 -957
- package/docs/classes/S3.html +0 -391
- package/docs/classes/SES.html +0 -335
- package/docs/classes/SNS.html +0 -185
- package/docs/classes/SecretsManager.html +0 -159
- package/docs/classes/StreamController.html +0 -284
- package/docs/classes/SystemsManager.html +0 -184
- package/docs/classes/Translate.html +0 -239
- package/docs/classes/UnhandledError.html +0 -252
- package/docs/functions/cleanFilename.html +0 -93
- package/docs/index.html +0 -95
- package/docs/interfaces/BasicEmailData.html +0 -145
- package/docs/interfaces/CognitoGroup.html +0 -122
- package/docs/interfaces/CognitoUserGeneric.html +0 -125
- package/docs/interfaces/CopyObjectOptions.html +0 -132
- package/docs/interfaces/CreateDownloadURLFromDataOptions.html +0 -163
- package/docs/interfaces/CreateUserOptions.html +0 -122
- package/docs/interfaces/DeleteObjectOptions.html +0 -122
- package/docs/interfaces/DetectSentimentParameters.html +0 -121
- package/docs/interfaces/EmailAttachment.html +0 -176
- package/docs/interfaces/EmailData.html +0 -188
- package/docs/interfaces/GetObjectOptions.html +0 -133
- package/docs/interfaces/HeadObjectOptions.html +0 -132
- package/docs/interfaces/InternalAPIRequestParams.html +0 -207
- package/docs/interfaces/ListObjectsOptions.html +0 -122
- package/docs/interfaces/PutObjectOptions.html +0 -173
- package/docs/interfaces/ResourceControllerOptions.html +0 -142
- package/docs/interfaces/SESParams.html +0 -152
- package/docs/interfaces/SNSCreateEndpointParams.html +0 -132
- package/docs/interfaces/SNSPublishParams.html +0 -142
- package/docs/interfaces/SignedURLOptions.html +0 -123
- package/docs/interfaces/TemplatedEmailData.html +0 -186
- package/docs/interfaces/TranslateParameters.html +0 -140
- package/docs/modules.html +0 -130
- package/docs/variables/LOG_LEVELS_PRIORITY.html +0 -81
- package/docs.style.css +0 -4
- package/src/attachments.ts +0 -41
- package/src/cognito.ts +0 -511
- package/src/comprehend.ts +0 -52
- package/src/dynamoDB.ts +0 -311
- package/src/genericController.ts +0 -103
- package/src/lambdaLogger.ts +0 -39
- package/src/metrics.ts +0 -45
- package/src/resourceController.ts +0 -645
- package/src/s3.ts +0 -334
- package/src/secretsManager.ts +0 -24
- package/src/ses.ts +0 -313
- package/src/sns.ts +0 -118
- package/src/ssm.ts +0 -33
- package/src/streamController.ts +0 -25
- package/src/translate.ts +0 -174
- package/tsconfig.json +0 -10
|
@@ -1,645 +0,0 @@
|
|
|
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
|
-
}
|