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