idea-aws 3.6.2 → 3.7.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.
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DynamoDB = void 0;
4
+ const aws_sdk_1 = require("aws-sdk");
5
+ const uuid_1 = require("uuid");
6
+ const nanoid_1 = require("nanoid");
7
+ const NanoID = (0, nanoid_1.customAlphabet)('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 25);
8
+ const shortid_1 = require("shortid");
9
+ const idea_toolbox_1 = require("idea-toolbox");
10
+ // declare libs as global vars to be reused in warm starts by the Lambda function
11
+ let ideaWarmStart_ddb = null;
12
+ /**
13
+ * A wrapper for AWS DynamoDB.
14
+ */
15
+ class DynamoDB {
16
+ constructor() {
17
+ if (!ideaWarmStart_ddb)
18
+ ideaWarmStart_ddb = new aws_sdk_1.DynamoDB.DocumentClient();
19
+ this.dynamo = ideaWarmStart_ddb;
20
+ }
21
+ /**
22
+ * Convert a JSON object from dynamoDB format to simple JSON.
23
+ */
24
+ unmarshall(data, options) {
25
+ return aws_sdk_1.DynamoDB.Converter.unmarshall(data, options);
26
+ }
27
+ /**
28
+ * Returns an IUID: IDEA's Unique IDentifier, which is an id unique through all IDEA's projects.
29
+ * Note: there's no need of an authorization check for extrernal uses: the permissions depend
30
+ * from the context in which it's executed.
31
+ * @deprecated use IUNID instead (nano version)
32
+ * @param project project code
33
+ * @return the IUID
34
+ */
35
+ async IUID(project) {
36
+ const MAX_ATTEMPTS = 3;
37
+ if (!project)
38
+ throw new Error('Missing project');
39
+ return await this.identifiersGeneratorHelper(project, 'IUID', 0, MAX_ATTEMPTS);
40
+ }
41
+ /**
42
+ * Returns an IUNID: IDEA's Unique Nano IDentifier, which is an id unique through all IDEA's projects.
43
+ * Note: no need of an auth check for external uses: the permissions depend from the context in which it's executed.
44
+ * @param project project code
45
+ * @return the IUNID
46
+ */
47
+ async IUNID(project) {
48
+ const MAX_ATTEMPTS = 3;
49
+ if (!project)
50
+ throw new Error('Missing project');
51
+ return await this.identifiersGeneratorHelper(project, 'IUNID', 0, MAX_ATTEMPTS);
52
+ }
53
+ /**
54
+ * Returns an ISID: IDEA's Short IDentifier, which is a short, unique id through a single project.
55
+ * Note: there's no need of an authorization check for extrernal uses: the permissions depend
56
+ * from the context in which it's executed.
57
+ * @param project project code
58
+ * @return the ISID
59
+ */
60
+ async ISID(project) {
61
+ const MAX_ATTEMPTS = 3;
62
+ if (!project)
63
+ throw new Error('Missing project');
64
+ return await this.identifiersGeneratorHelper(project, 'ISID', 0, MAX_ATTEMPTS);
65
+ }
66
+ async identifiersGeneratorHelper(project, type, attempt, maxAttempts) {
67
+ if (attempt > maxAttempts)
68
+ throw new Error('Operation failed');
69
+ let id, result;
70
+ switch (type) {
71
+ case 'IUNID':
72
+ id = NanoID();
73
+ result = `${project}_${id}`;
74
+ break;
75
+ case 'IUID':
76
+ id = (0, uuid_1.v4)();
77
+ result = `${project}_${id}`;
78
+ break;
79
+ case 'ISID':
80
+ // avoid _ characters (to avoid concatenation problems with ids) -- it must be anyway 64 chars-long
81
+ (0, shortid_1.characters)('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-@');
82
+ id = (0, shortid_1.generate)();
83
+ result = id;
84
+ break;
85
+ }
86
+ try {
87
+ await this.put({
88
+ TableName: 'idea_'.concat(type),
89
+ Item: { project, id },
90
+ ConditionExpression: 'NOT (#p = :project AND #id = :id)',
91
+ ExpressionAttributeNames: { '#p': 'project', '#id': 'id' },
92
+ ExpressionAttributeValues: { ':project': project, ':id': id }
93
+ });
94
+ return result;
95
+ }
96
+ catch (err) {
97
+ // ID exists, try again
98
+ await this.identifiersGeneratorHelper(project, type, attempt + 1, maxAttempts);
99
+ }
100
+ }
101
+ /**
102
+ * Manage atomic counters (atomic autoincrement values) in IDEA's projects.
103
+ * They key of an atomic counter should be composed as the following: `DynamoDBTableName_uniqueKey`.
104
+ * @param key the key of the counter
105
+ */
106
+ async getAtomicCounterByKey(key) {
107
+ (0, idea_toolbox_1.logger)(`GET ATOMIC COUNTER FOR ${key}`);
108
+ const result = await this.update({
109
+ TableName: 'idea_atomicCounters',
110
+ Key: { key },
111
+ UpdateExpression: 'ADD atomicCounter :increment',
112
+ ExpressionAttributeValues: { ':increment': 1 },
113
+ ReturnValues: 'UPDATED_NEW'
114
+ });
115
+ if (!result?.Attributes?.atomicCounter)
116
+ throw new Error('Operation failed');
117
+ else
118
+ return result.Attributes.atomicCounter;
119
+ }
120
+ /**
121
+ * Get an item of a DynamoDB table.
122
+ */
123
+ async get(params) {
124
+ (0, idea_toolbox_1.logger)(`GET ${params.TableName}`);
125
+ const result = await this.dynamo.get(params).promise();
126
+ if (!result?.Item)
127
+ throw new Error('Not found');
128
+ return result.Item;
129
+ }
130
+ /**
131
+ * Put an item in a DynamoDB table.
132
+ */
133
+ async put(params) {
134
+ (0, idea_toolbox_1.logger)(`PUT ${params.TableName}`);
135
+ return await this.dynamo.put(params).promise();
136
+ }
137
+ /**
138
+ * Update an item of a DynamoDB table.
139
+ */
140
+ async update(params) {
141
+ (0, idea_toolbox_1.logger)(`UPDATE ${params.TableName}`);
142
+ return await this.dynamo.update(params).promise();
143
+ }
144
+ /**
145
+ * Delete an item of a DynamoDB table.
146
+ */
147
+ async delete(params) {
148
+ (0, idea_toolbox_1.logger)(`DELETE ${params.TableName}`);
149
+ return await this.dynamo.delete(params).promise();
150
+ }
151
+ /**
152
+ * Get group of items based on their keys from DynamoDb table, avoiding the limits of DynamoDB's BatchGetItem.
153
+ * @param ignoreErr if set, ignore the errors and continue the bulk op.
154
+ */
155
+ async batchGet(table, keys, ignoreErr) {
156
+ if (!keys.length) {
157
+ (0, idea_toolbox_1.logger)(`BATCH GET ${table}`, null, 'No elements to get');
158
+ return [];
159
+ }
160
+ await this.batchGetHelper(table, keys, [], Boolean(ignoreErr));
161
+ }
162
+ async batchGetHelper(table, keys, resultElements, ignoreErr, currentChunk = 0, chunkSize = 100) {
163
+ const batch = { RequestItems: {} };
164
+ batch.RequestItems[table] = { Keys: [] };
165
+ batch.RequestItems[table].Keys = keys.slice(currentChunk, currentChunk + chunkSize);
166
+ (0, idea_toolbox_1.logger)(`BATCH GET ${table}`, null, `${currentChunk} of ${keys.length}`);
167
+ let result;
168
+ try {
169
+ result = await this.dynamo.batchGet(batch).promise();
170
+ }
171
+ catch (err) {
172
+ if (!ignoreErr)
173
+ throw err;
174
+ }
175
+ if (result)
176
+ resultElements = resultElements.concat(result.Responses[table]);
177
+ // if there are still chunks to manage, go on recursively
178
+ if (currentChunk + chunkSize < keys.length)
179
+ await this.batchGetHelper(table, keys, resultElements, ignoreErr, currentChunk + chunkSize, chunkSize);
180
+ // no more chunks to manage: we're done
181
+ return resultElements;
182
+ }
183
+ /**
184
+ * Put an array of items in a DynamoDb table, avoiding the limits of DynamoDB's BatchWriteItem.
185
+ * In case of errors, it will retry with a random back-off mechanism until the timeout.
186
+ * Therefore, in case of timeout, there may be some elements written and some not.
187
+ */
188
+ async batchPut(table, items) {
189
+ if (!items.length)
190
+ return (0, idea_toolbox_1.logger)(`BATCH WRITE (PUT) ${table}`, null, 'No elements to write');
191
+ await this.batchWriteHelper(table, items, true);
192
+ }
193
+ /**
194
+ * Delete an array of items from a DynamoDb table, avoiding the limits of DynamoDB's BatchWriteItem.
195
+ * In case of errors, it will retry with a random back-off mechanism until the timeout.
196
+ * Therefore, in case of timeout, there may be some elements deleted and some not.
197
+ */
198
+ async batchDelete(table, keys) {
199
+ if (!keys.length)
200
+ return (0, idea_toolbox_1.logger)(`BATCH WRITE (DELETE) ${table}`, null, 'No elements to write');
201
+ await this.batchWriteHelper(table, keys, false);
202
+ }
203
+ async batchWriteHelper(table, itemsOrKeys, isPut, currentChunk = 0, chunkSize = 25) {
204
+ (0, idea_toolbox_1.logger)(`BATCH WRITE (${isPut ? 'PUT' : 'DELETE'}) ${table}`, null, `${currentChunk} of ${itemsOrKeys.length}`);
205
+ let requests;
206
+ if (isPut)
207
+ requests = itemsOrKeys.slice(currentChunk, currentChunk + chunkSize).map(i => ({ PutRequest: { Item: i } }));
208
+ // isDelete
209
+ else
210
+ requests = itemsOrKeys.slice(currentChunk, currentChunk + chunkSize).map(k => ({ DeleteRequest: { Key: k } }));
211
+ const batch = { RequestItems: { [table]: requests } };
212
+ await this.batchWriteChunkWithRetries(table, batch);
213
+ // if there are still chunks to manage, go on recursively
214
+ if (currentChunk + chunkSize < itemsOrKeys.length)
215
+ await this.batchWriteHelper(table, itemsOrKeys, isPut, currentChunk + chunkSize, chunkSize);
216
+ }
217
+ async batchWriteChunkWithRetries(table, params) {
218
+ const getRandomInt = (max) => Math.floor(Math.random() * max);
219
+ const wait = (seconds) => new Promise(x => setTimeout(() => x(), seconds * 1000));
220
+ let attempts = 0;
221
+ do {
222
+ const response = await this.dynamo.batchWrite(params).promise();
223
+ if (response.UnprocessedItems &&
224
+ response.UnprocessedItems[table] &&
225
+ response.UnprocessedItems[table].length > 0) {
226
+ params.RequestItems = response.UnprocessedItems;
227
+ attempts++;
228
+ const waitSeconds = getRandomInt(attempts * 5);
229
+ (0, idea_toolbox_1.logger)('BATCH WRITE THROTTLED', null, `Waiting ${waitSeconds} seconds to retry`);
230
+ await wait(waitSeconds);
231
+ }
232
+ else {
233
+ params.RequestItems = null;
234
+ }
235
+ } while (params.RequestItems);
236
+ }
237
+ /**
238
+ * Query a DynamoDb table, avoiding the limits of DynamoDB's Query.
239
+ * @param params the params to apply to DynamoDB's function
240
+ */
241
+ async query(params) {
242
+ (0, idea_toolbox_1.logger)(`Query ${params.TableName}`);
243
+ const result = await this.queryScanHelper(params, [], true);
244
+ (0, idea_toolbox_1.logger)(`\tResults query ${params.TableName}`, null, result?.length || 0);
245
+ return result;
246
+ }
247
+ /**
248
+ * Scan a DynamoDb table, avoiding the limits of DynamoDB's Query.
249
+ * @param params the params to apply to DynamoDB's function
250
+ */
251
+ async scan(params) {
252
+ (0, idea_toolbox_1.logger)(`Scan ${params.TableName}`);
253
+ const result = await this.queryScanHelper(params, [], false);
254
+ (0, idea_toolbox_1.logger)(`\tResults scan ${params.TableName}`, null, result?.length || 0);
255
+ return result;
256
+ }
257
+ async queryScanHelper(params, items, isQuery) {
258
+ let result;
259
+ if (isQuery)
260
+ result = await this.dynamo.query(params).promise();
261
+ else
262
+ result = await this.dynamo.scan(params).promise();
263
+ items = items.concat(result.Items);
264
+ if (result.LastEvaluatedKey) {
265
+ params.ExclusiveStartKey = result.LastEvaluatedKey;
266
+ await this.queryScanHelper(params, items, isQuery);
267
+ }
268
+ else
269
+ return items;
270
+ }
271
+ /**
272
+ * Query a DynamoDb table in the traditional way (no pagination or data mapping).
273
+ * @param params the params to apply to DynamoDB's function
274
+ */
275
+ async queryClassic(params) {
276
+ (0, idea_toolbox_1.logger)(`Query classic ${params.TableName}`);
277
+ const result = await this.dynamo.query(params).promise();
278
+ (0, idea_toolbox_1.logger)(`\tResults query classic ${params.TableName}`, null, result?.Items?.length || 0);
279
+ return result;
280
+ }
281
+ /**
282
+ * Scan a DynamoDb table in the traditional way (no pagination or data mapping).
283
+ * @param params the params to apply to DynamoDB's function
284
+ */
285
+ async scanClassic(params) {
286
+ (0, idea_toolbox_1.logger)(`Scan classic ${params.TableName}`);
287
+ const result = await this.dynamo.scan(params).promise();
288
+ (0, idea_toolbox_1.logger)(`\tResults scan classic ${params.TableName}`, null, result?.Items?.length || 0);
289
+ return result;
290
+ }
291
+ /**
292
+ * Execute a series of max 10 write operations in a single transaction.
293
+ * @param ops the operations to execute in the transaction
294
+ */
295
+ async transactWrites(ops) {
296
+ if (!ops.length)
297
+ return (0, idea_toolbox_1.logger)('TRANSACTION WRITES', null, 'No elements to write');
298
+ (0, idea_toolbox_1.logger)('TRANSACTION WRITES');
299
+ await this.dynamo.transactWrite({ TransactItems: ops.slice(0, 10) }).promise();
300
+ }
301
+ /**
302
+ * Creates a set of elements (DynamoDB format) inferring the type of set from the type of the first element.
303
+ */
304
+ createSet(array, options) {
305
+ return this.dynamo.createSet(array, options);
306
+ }
307
+ }
308
+ exports.DynamoDB = DynamoDB;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GenericController = void 0;
4
+ const idea_toolbox_1 = require("idea-toolbox");
5
+ const dynamoDB_1 = require("./dynamoDB");
6
+ const cognito_1 = require("./cognito");
7
+ const s3_1 = require("./s3");
8
+ const ses_1 = require("./ses");
9
+ const sns_1 = require("./sns");
10
+ const translate_1 = require("./translate");
11
+ const comprehend_1 = require("./comprehend");
12
+ const attachments_1 = require("./attachments");
13
+ /**
14
+ * An abstract class to inherit to manage some resources with an AWS Lambda function.
15
+ */
16
+ class GenericController {
17
+ /**
18
+ * Initialize a new GenericController helper object.
19
+ * @param event the event that invoked the AWS lambda function
20
+ * @param callback the callback to resolve or reject the execution
21
+ */
22
+ constructor(event, callback, options) {
23
+ options = options || {};
24
+ this.event = event;
25
+ this.callback = callback;
26
+ this.tables = options.tables || {};
27
+ // set the logs to print objects deeper
28
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
29
+ require('util').inspect.defaultOptions.depth = null;
30
+ }
31
+ /**
32
+ * Default callback for the Lambda.
33
+ */
34
+ done(err, res) {
35
+ (0, idea_toolbox_1.logger)(err ? 'DONE WITH ERRORS' : 'DONE', err, res, true);
36
+ this.callback(err, res);
37
+ }
38
+ ///
39
+ /// AWS SERVICES
40
+ ///
41
+ get dynamoDB() {
42
+ if (!this._dynamoDB)
43
+ this._dynamoDB = new dynamoDB_1.DynamoDB();
44
+ return this._dynamoDB;
45
+ }
46
+ set dynamoDB(dynamoDB) {
47
+ this._dynamoDB = dynamoDB;
48
+ }
49
+ get cognito() {
50
+ if (!this._cognito)
51
+ this._cognito = new cognito_1.Cognito();
52
+ return this._cognito;
53
+ }
54
+ set cognito(cognito) {
55
+ this._cognito = cognito;
56
+ }
57
+ get s3() {
58
+ if (!this._s3)
59
+ this._s3 = new s3_1.S3();
60
+ return this._s3;
61
+ }
62
+ set s3(s3) {
63
+ this._s3 = s3;
64
+ }
65
+ get ses() {
66
+ if (!this._ses)
67
+ this._ses = new ses_1.SES();
68
+ return this._ses;
69
+ }
70
+ set ses(ses) {
71
+ this._ses = ses;
72
+ }
73
+ get sns() {
74
+ if (!this._sns)
75
+ this._sns = new sns_1.SNS();
76
+ return this._sns;
77
+ }
78
+ set sns(sns) {
79
+ this._sns = sns;
80
+ }
81
+ get translate() {
82
+ if (!this._translate)
83
+ this._translate = new translate_1.Translate();
84
+ return this._translate;
85
+ }
86
+ set translate(translate) {
87
+ this._translate = translate;
88
+ }
89
+ get comprehend() {
90
+ if (!this._comprehend)
91
+ this._comprehend = new comprehend_1.Comprehend();
92
+ return this._comprehend;
93
+ }
94
+ set comprehend(comprehend) {
95
+ this._comprehend = comprehend;
96
+ }
97
+ ///
98
+ /// HELPERS
99
+ ///
100
+ /**
101
+ * Manage attachments (through SignedURLs).
102
+ */
103
+ get attachments() {
104
+ if (!this._attachments)
105
+ this._attachments = new attachments_1.Attachments();
106
+ return this._attachments;
107
+ }
108
+ set attachments(attachments) {
109
+ this._attachments = attachments;
110
+ }
111
+ }
112
+ exports.GenericController = GenericController;
@@ -1,3 +1,4 @@
1
+ import { CognitoUser } from 'idea-toolbox';
1
2
  import { GenericController, GenericControllerOptions } from './genericController';
2
3
  /**
3
4
  * An abstract class to inherit to manage API requests (AWS API Gateway) in an AWS Lambda function.
@@ -6,6 +7,7 @@ export declare abstract class ResourceController extends GenericController {
6
7
  protected authorization: string;
7
8
  protected claims: any;
8
9
  protected principalId: string;
10
+ protected user: CognitoUser;
9
11
  protected stage: string;
10
12
  protected httpMethod: string;
11
13
  body: any;
@@ -19,8 +21,8 @@ export declare abstract class ResourceController extends GenericController {
19
21
  protected translations: any;
20
22
  protected templateMatcher: RegExp;
21
23
  constructor(event: any, callback: any, options?: ResourceControllerOptions);
22
- handleRequest: () => void;
23
- protected done(err: Error | null, res?: any): void;
24
+ handleRequest: () => Promise<void>;
25
+ protected done(err: Error | null, res?: any, statusCode?: number): void;
24
26
  /**
25
27
  * To @override
26
28
  */
@@ -83,16 +85,17 @@ export declare abstract class ResourceController extends GenericController {
83
85
  sharedResourceExists(path: string): boolean;
84
86
  /**
85
87
  * Load a shared resource in the back-end (translation, template, etc.).
86
- * @param encoding default: `utf-8`
87
88
  */
88
- loadSharedResource(path: string, encoding?: string): string;
89
+ loadSharedResource(path: string): string;
89
90
  /**
90
91
  * Simulate an internal API request, invoking directly the lambda and therefore saving resources.
91
92
  * @return the body of the response
93
+ * @deprecated don't run a Lambda from another Lambda (bad practice)
92
94
  */
93
95
  invokeInternalAPIRequest(params: InternalAPIRequestParams): Promise<any>;
94
96
  /**
95
97
  * Whether the current request comes from an internal API request, i.e. it was invoked by another controller.
98
+ * @deprecated don't run a Lambda from another Lambda (bad practice)
96
99
  */
97
100
  comesFromInternalRequest(): boolean;
98
101
  /**
@@ -134,6 +137,7 @@ export interface ResourceControllerOptions extends GenericControllerOptions {
134
137
  }
135
138
  /**
136
139
  * The parameters needed to invoke an internal API request.
140
+ * @deprecated don't run a Lambda from another Lambda (bad practice)
137
141
  */
138
142
  export interface InternalAPIRequestParams {
139
143
  /**