inlaweb-lib-dynamodb 1.0.15 → 1.0.16

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 (3) hide show
  1. package/index.js +15 -4
  2. package/index.ts +528 -0
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -81,10 +81,21 @@ class DynamoLib {
81
81
  async query(params) {
82
82
  try {
83
83
  const parameters = this.buildQueryParams(params);
84
- this.logger.debug(`Query: ${JSON.stringify(parameters)}`);
85
- const response = await this.docClient.send(new lib_dynamodb_1.QueryCommand(parameters));
86
- this.logger.debug(`Query Result: ${JSON.stringify(response)}`);
87
- return response.Items;
84
+ let allItems = [];
85
+ let lastEvaluatedKey;
86
+ do {
87
+ if (lastEvaluatedKey) {
88
+ parameters.ExclusiveStartKey = lastEvaluatedKey;
89
+ }
90
+ this.logger.debug(`Query: ${JSON.stringify(parameters)}`);
91
+ const response = await this.docClient.send(new lib_dynamodb_1.QueryCommand(parameters));
92
+ this.logger.debug(`Query Result: ${JSON.stringify(response)}`);
93
+ if (response.Items) {
94
+ allItems = allItems.concat(response.Items);
95
+ }
96
+ lastEvaluatedKey = response.LastEvaluatedKey;
97
+ } while (lastEvaluatedKey);
98
+ return allItems;
88
99
  }
89
100
  catch (error) {
90
101
  this.logger.error(error.message);
package/index.ts ADDED
@@ -0,0 +1,528 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import {
3
+ DynamoDBDocumentClient,
4
+ GetCommand,
5
+ PutCommand,
6
+ QueryCommand,
7
+ UpdateCommand,
8
+ BatchWriteCommand,
9
+ TransactWriteCommand,
10
+ DeleteCommand,
11
+ } from "@aws-sdk/lib-dynamodb";
12
+ import { Logger } from "@aws-lambda-powertools/logger";
13
+ import {
14
+ ParamsPut,
15
+ ParamsGet,
16
+ ParamsQuery,
17
+ ParamsUpdate,
18
+ ProjectionExpression,
19
+ SearchParameters,
20
+ KeyValue,
21
+ InputUpdate,
22
+ Operation,
23
+ } from "./commons/types";
24
+ import moment from "moment-timezone";
25
+
26
+ const templatesUpdateExpresion = {
27
+ SET: (atribute: string) => `#${atribute} = :${atribute},`,
28
+ ADD: (atribute: string) => `#${atribute} :${atribute},`,
29
+ REMOVE: (atribute: string) => `#${atribute},`,
30
+ };
31
+
32
+ export class DynamoLib {
33
+ private readonly client: DynamoDBClient;
34
+ private readonly docClient: DynamoDBDocumentClient;
35
+ private readonly logger: Logger;
36
+
37
+ constructor() {
38
+ this.client = new DynamoDBClient({
39
+ customUserAgent: "lib-dynamodb",
40
+ disableHostPrefix: true,
41
+ userAgentAppId: "lib-dynamodb",
42
+ });
43
+ this.docClient = DynamoDBDocumentClient.from(this.client);
44
+ this.logger = new Logger({
45
+ serviceName: "lib-dynamodb",
46
+ logLevel: "DEBUG",
47
+ });
48
+ }
49
+
50
+ async putItem(tableName: string, item: KeyValue): Promise<void> {
51
+ try {
52
+ const params: ParamsPut = {
53
+ TableName: tableName,
54
+ Item: item,
55
+ ReturnItemCollectionMetrics: "SIZE",
56
+ ReturnConsumedCapacity: "TOTAL",
57
+ };
58
+ this.logger.debug(`PutItem: ${JSON.stringify(params)}`);
59
+ const result = await this.docClient.send(new PutCommand(params));
60
+ this.logger.debug(`PutItem Result: ${JSON.stringify(result)}`);
61
+ } catch (error) {
62
+ this.logger.error((error as Error).message);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ async getItem<T>(
68
+ tableName: string,
69
+ key: KeyValue,
70
+ projectionExpression?: string,
71
+ ): Promise<T> {
72
+ try {
73
+ const params: ParamsGet = {
74
+ TableName: tableName,
75
+ Key: key,
76
+ ReturnConsumedCapacity: "TOTAL",
77
+ };
78
+ const projection = this.buildProjectionExpression(projectionExpression);
79
+ if (projection) {
80
+ params.ProjectionExpression = projection.projectionExpression;
81
+ params.ExpressionAttributeNames = projection.expressionAttributeNames;
82
+ }
83
+ this.logger.debug(`GetItem: ${JSON.stringify(params)}`);
84
+ const response = await this.docClient.send(new GetCommand(params));
85
+ this.logger.debug(`GetItem Result: ${JSON.stringify(response)}`);
86
+ return response.Item as T;
87
+ } catch (error) {
88
+ this.logger.error((error as Error).message);
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ async updateItem<T>(params: InputUpdate): Promise<T> {
94
+ try {
95
+ const itemUpdate = this.createUpdateParams(params);
96
+ this.logger.debug(`UpdateItem: ${JSON.stringify(itemUpdate.Update)}`);
97
+ const result = await this.docClient.send(
98
+ new UpdateCommand(itemUpdate.Update),
99
+ );
100
+ this.logger.debug(`UpdateItem Result: ${JSON.stringify(result)}`);
101
+ return result.Attributes as T;
102
+ } catch (error) {
103
+ this.logger.error((error as Error).message);
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ async query<T>(params: {
109
+ tableName: string;
110
+ searchParameters: SearchParameters;
111
+ permission?: string;
112
+ username?: string;
113
+ }): Promise<T[]> {
114
+ try {
115
+ const parameters: ParamsQuery = this.buildQueryParams(params);
116
+ let allItems: T[] = [];
117
+ let lastEvaluatedKey: KeyValue | undefined;
118
+
119
+ do {
120
+ if (lastEvaluatedKey) {
121
+ parameters.ExclusiveStartKey = lastEvaluatedKey;
122
+ }
123
+
124
+ this.logger.debug(`Query: ${JSON.stringify(parameters)}`);
125
+ const response = await this.docClient.send(
126
+ new QueryCommand(parameters),
127
+ );
128
+ this.logger.debug(`Query Result: ${JSON.stringify(response)}`);
129
+
130
+ if (response.Items) {
131
+ allItems = allItems.concat(response.Items as T[]);
132
+ }
133
+
134
+ lastEvaluatedKey = response.LastEvaluatedKey;
135
+ } while (lastEvaluatedKey);
136
+
137
+ return allItems;
138
+ } catch (error) {
139
+ this.logger.error((error as Error).message);
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ async deleteItem(tableName: string, key: KeyValue): Promise<void> {
145
+ try {
146
+ const params: ParamsGet = {
147
+ TableName: tableName,
148
+ Key: key,
149
+ ReturnConsumedCapacity: "TOTAL",
150
+ };
151
+ this.logger.debug(`DeleteItem: ${JSON.stringify(params)}`);
152
+ const response = await this.docClient.send(new DeleteCommand(params));
153
+ this.logger.debug(`DeleteItem Result: ${JSON.stringify(response)}`);
154
+ } catch (error) {
155
+ this.logger.error((error as Error).message);
156
+ throw error;
157
+ }
158
+ }
159
+
160
+ private buildQueryParams(params: {
161
+ tableName: string;
162
+ searchParameters: SearchParameters;
163
+ permission?: string;
164
+ username?: string;
165
+ }): ParamsQuery {
166
+ const projection = this.buildProjectionExpression(
167
+ params.searchParameters.projectionExpression,
168
+ );
169
+
170
+ const parameters: ParamsQuery = {
171
+ TableName: params.tableName,
172
+ KeyConditionExpression: "",
173
+ ExpressionAttributeNames: projection?.expressionAttributeNames || {},
174
+ ExpressionAttributeValues: {},
175
+ };
176
+
177
+ if (projection) {
178
+ parameters.ProjectionExpression = projection.projectionExpression;
179
+ }
180
+ if (params.searchParameters.exclusiveStartKey) {
181
+ parameters.ExclusiveStartKey = params.searchParameters.exclusiveStartKey;
182
+ }
183
+ if (params.searchParameters.limit) {
184
+ parameters.Limit = params.searchParameters.limit;
185
+ }
186
+ if (params.searchParameters.scanIndexForward) {
187
+ parameters.ScanIndexForward = params.searchParameters.scanIndexForward;
188
+ }
189
+ if (params.searchParameters.indexName !== "") {
190
+ parameters.IndexName = params.searchParameters.indexName;
191
+ }
192
+
193
+ if (params.permission && params.permission === "OWNS") {
194
+ params.searchParameters.parameters.push({
195
+ name: "creationUser",
196
+ value: params.username,
197
+ operator: "FILTER",
198
+ filterOperator: "=",
199
+ });
200
+ }
201
+
202
+ params.searchParameters.parameters.forEach((searchParameter, index) => {
203
+ this.buildExpressions(searchParameter, index, parameters);
204
+ });
205
+
206
+ return parameters;
207
+ }
208
+
209
+ private buildExpressions(
210
+ searchParameter: KeyValue,
211
+ index: number,
212
+ params: ParamsQuery,
213
+ ): void {
214
+ if (index !== 0 && searchParameter.operator !== "FILTER")
215
+ params.KeyConditionExpression += " and ";
216
+
217
+ switch (searchParameter.operator) {
218
+ case "begins_with":
219
+ params.KeyConditionExpression += `${searchParameter.operator}(#key${index}, :value${index})`;
220
+ break;
221
+ case "BETWEEN":
222
+ params.KeyConditionExpression += `#key${index} ${searchParameter.operator} :value${index} AND :value1${index}`;
223
+ params.ExpressionAttributeValues[`:value1${index}`] =
224
+ searchParameter.value1;
225
+ break;
226
+ case "FILTER":
227
+ this.buildFilterExpression(searchParameter, index, params);
228
+ break;
229
+ default:
230
+ params.KeyConditionExpression += `#key${index} ${searchParameter.operator} :value${index}`;
231
+ break;
232
+ }
233
+
234
+ params.ExpressionAttributeNames[`#key${index}`] = searchParameter.name;
235
+ params.ExpressionAttributeValues[`:value${index}`] = searchParameter.value;
236
+ }
237
+
238
+ private buildFilterExpression(
239
+ searchParameter: KeyValue,
240
+ index: number,
241
+ params: ParamsQuery,
242
+ ): void {
243
+ if (params.FilterExpression) params.FilterExpression += " and ";
244
+ else params.FilterExpression = "";
245
+
246
+ switch (searchParameter.filterOperator) {
247
+ case "contains":
248
+ params.FilterExpression += `contains (#key${index}, :value${index})`;
249
+ break;
250
+ case "begins_with":
251
+ params.FilterExpression += `${searchParameter.filterOperator}(#key${index}, :value${index})`;
252
+ break;
253
+ case "attribute_not_exists":
254
+ params.FilterExpression += `attribute_not_exists (#key${index})`;
255
+ break;
256
+ case "attribute_exists":
257
+ params.FilterExpression += `attribute_exists (#key${index})`;
258
+ break;
259
+ case "BETWEEN":
260
+ params.FilterExpression += `#key${index} ${searchParameter.filterOperator} :value${index} AND :value1${index}`;
261
+ params.ExpressionAttributeValues[`:value1${index}`] =
262
+ searchParameter.value1;
263
+ break;
264
+ default:
265
+ params.FilterExpression += `#key${index} ${searchParameter.filterOperator} :value${index}`;
266
+ break;
267
+ }
268
+ }
269
+
270
+ private buildProjectionExpression(
271
+ projectionExpression: string | undefined,
272
+ ): ProjectionExpression | undefined {
273
+ if (!projectionExpression) {
274
+ return undefined;
275
+ }
276
+ const fields = projectionExpression.split(",");
277
+ if (fields.length > 0) {
278
+ let projectionExpression = "";
279
+ const expressionAttributeNames: KeyValue = {};
280
+ fields.forEach((field) => {
281
+ projectionExpression += `#${field},`;
282
+ expressionAttributeNames[`#${field}`] = field;
283
+ });
284
+ projectionExpression = projectionExpression.slice(0, -1);
285
+ return { projectionExpression, expressionAttributeNames };
286
+ }
287
+ }
288
+
289
+ async saveBatch(table: string, items: KeyValue[]): Promise<boolean> {
290
+ try {
291
+ const executions = this.createBatch(items);
292
+ for (let i = 0; i < executions.length; i++) {
293
+ const params = {
294
+ RequestItems: {
295
+ [table]: executions[i] as any,
296
+ },
297
+ ReturnConsumedCapacity: "TOTAL" as const,
298
+ };
299
+ this.logger.debug(`BatchWrite: ${JSON.stringify(params)}`);
300
+ const response = await this.docClient.send(
301
+ new BatchWriteCommand(params),
302
+ );
303
+ this.logger.debug(`BatchWrite Result: ${JSON.stringify(response)}`);
304
+ }
305
+ return true;
306
+ } catch (error) {
307
+ this.logger.error((error as Error).message);
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ private createBatch(items: KeyValue[]): KeyValue[] {
313
+ const executions: KeyValue[] = [];
314
+ const numberOfBacthInsertions = Math.ceil(items.length / 25);
315
+ for (let i = 0; i < numberOfBacthInsertions; i++) {
316
+ executions[i] = [];
317
+ }
318
+ items.forEach((item, index) => {
319
+ const currentIndex = Math.ceil((index + 1) / 25);
320
+ executions[currentIndex - 1].push({ PutRequest: { Item: item } });
321
+ });
322
+ return executions;
323
+ }
324
+
325
+ async writeTransactions(items: KeyValue[], limit: number): Promise<boolean> {
326
+ try {
327
+ const executions = this.createWriteTransactions(items, limit);
328
+ for (let i = 0; i < executions.length; i++) {
329
+ const params = {
330
+ TransactItems: executions[i],
331
+ ReturnConsumedCapacity: "TOTAL" as const,
332
+ };
333
+ this.logger.debug(`TransactWrite: ${JSON.stringify(params)}`);
334
+ const response = await this.docClient.send(
335
+ new TransactWriteCommand(params),
336
+ );
337
+ this.logger.debug(`TransactWrite Result: ${JSON.stringify(response)}`);
338
+ }
339
+ return true;
340
+ } catch (error) {
341
+ this.logger.error((error as Error).message);
342
+ throw error;
343
+ }
344
+ }
345
+
346
+ createWriteTransactions(items: KeyValue[], limit: number): KeyValue[][] {
347
+ const executions: KeyValue[][] = [];
348
+ const numberOfBacthInsertions = Math.ceil(items.length / limit);
349
+ for (let i = 0; i < numberOfBacthInsertions; i++) {
350
+ executions[i] = [];
351
+ }
352
+ items.forEach((item, index) => {
353
+ const currentIndex = Math.ceil((index + 1) / limit);
354
+ executions[currentIndex - 1].push(item);
355
+ });
356
+ return executions;
357
+ }
358
+
359
+ createUpdateParams(params: InputUpdate): KeyValue {
360
+ const {
361
+ table,
362
+ PK,
363
+ SK,
364
+ item,
365
+ setAttributes,
366
+ addAttributes,
367
+ removeAttributes,
368
+ username,
369
+ } = params;
370
+ // Se crea el objeto de la operación
371
+ let itemUpdate = {
372
+ Update: {
373
+ TableName: table,
374
+ Key: { PK, SK },
375
+ UpdateExpression: ``,
376
+ ExpressionAttributeNames: {},
377
+ ExpressionAttributeValues: {},
378
+ ReturnConsumedCapacity: "TOTAL",
379
+ ReturnValues: "ALL_NEW",
380
+ },
381
+ };
382
+ // Se agregan los campos a modificar
383
+ if (Array.isArray(setAttributes) && setAttributes.length) {
384
+ this.formatUpdateExpression("SET", itemUpdate, item, setAttributes);
385
+ }
386
+ // Se agragan los campos nuevos del item
387
+ if (Array.isArray(addAttributes) && addAttributes.length) {
388
+ this.formatUpdateExpression("ADD", itemUpdate, item, addAttributes);
389
+ }
390
+ // Se agregan los campos que deben ser borrados
391
+ if (Array.isArray(removeAttributes) && removeAttributes.length) {
392
+ this.formatUpdateExpressionRemove(itemUpdate, removeAttributes);
393
+ }
394
+ // Se remueve el espacio al final de la expresión
395
+ itemUpdate.Update.UpdateExpression =
396
+ itemUpdate.Update.UpdateExpression.slice(0, -1);
397
+ return itemUpdate;
398
+ }
399
+
400
+ private formatUpdateExpression(
401
+ operation: Operation,
402
+ updateObject: KeyValue,
403
+ item: KeyValue,
404
+ atributes: string[],
405
+ ): void {
406
+ // Se agrega la operación.
407
+ updateObject.Update.UpdateExpression += `${operation} `;
408
+ // Se obtiene el templete de acuerdo a la operación.
409
+ let operationTemplate = templatesUpdateExpresion[operation];
410
+ // Se añaden los items afectados por la operación.
411
+ for (let atribute of atributes) {
412
+ if (
413
+ typeof item[atribute] === "boolean" ||
414
+ typeof item[atribute] === "number" ||
415
+ item[atribute]
416
+ ) {
417
+ updateObject.Update.UpdateExpression += operationTemplate(atribute);
418
+ updateObject.Update.ExpressionAttributeNames[`#${atribute}`] = atribute;
419
+ updateObject.Update.ExpressionAttributeValues[`:${atribute}`] =
420
+ item[atribute];
421
+ }
422
+ }
423
+ // Se remueve la coma final para evitar errores con el SDK.
424
+ updateObject.Update.UpdateExpression =
425
+ updateObject.Update.UpdateExpression.slice(0, -1);
426
+ // Se añade un espacio para poder agregar nuevas operaciones.
427
+ updateObject.Update.UpdateExpression += " ";
428
+ }
429
+
430
+ private formatUpdateExpressionRemove(
431
+ updateObject: KeyValue,
432
+ atributes: string[],
433
+ ): void {
434
+ let operation: Operation = "REMOVE";
435
+ // Se agrega la operación.
436
+ updateObject.Update.UpdateExpression += `${operation} `;
437
+ // Se obtiene el templete de acuerdo a la operación.
438
+ let operationTemplate = templatesUpdateExpresion[operation];
439
+ // Se añaden los items afectados por la operación.
440
+ for (let atribute of atributes) {
441
+ updateObject.Update.UpdateExpression += operationTemplate(atribute);
442
+ updateObject.Update.ExpressionAttributeNames[`#${atribute}`] = atribute;
443
+ }
444
+ // Se remueve la coma final para evitar errores con el SDK.
445
+ updateObject.Update.UpdateExpression =
446
+ updateObject.Update.UpdateExpression.slice(0, -1);
447
+ // Se añade un espacio para poder agregar nuevas operaciones.
448
+ updateObject.Update.UpdateExpression += " ";
449
+ }
450
+
451
+ async getId(
452
+ table: string,
453
+ entity: string,
454
+ username: string,
455
+ quantity?: number,
456
+ ): Promise<number> {
457
+ try {
458
+ let item = await this.getItem<KeyValue>(
459
+ table,
460
+ { PK: "COUN", SK: entity },
461
+ "order",
462
+ );
463
+ if (!item) {
464
+ item = {
465
+ PK: "COUN",
466
+ SK: entity,
467
+ entity: "COUN",
468
+ order: 0,
469
+ creationUser: username,
470
+ creationDate: moment
471
+ .tz(new Date(), "America/Bogota")
472
+ .format("YYYY-MM-DD"),
473
+ };
474
+ await this.putItem(table, item);
475
+ }
476
+ const params: ParamsUpdate = {
477
+ TableName: table,
478
+ Key: { PK: "COUN", SK: entity },
479
+ UpdateExpression: `SET #id = #id + :increment`,
480
+ ExpressionAttributeNames: { "#id": "order" },
481
+ ExpressionAttributeValues: { ":increment": quantity || 1 },
482
+ ReturnValues: "UPDATED_OLD",
483
+ };
484
+ this.logger.debug(`GetId: ${JSON.stringify(params)}`);
485
+ const result = await this.docClient.send(new UpdateCommand(params));
486
+ this.logger.debug(`GetId Result: ${JSON.stringify(result)}`);
487
+ if (result.Attributes) return result.Attributes.order + 1;
488
+ return 1;
489
+ } catch (error) {
490
+ this.logger.error((error as Error).message);
491
+ throw error;
492
+ }
493
+ }
494
+
495
+ async validatePermission(
496
+ table: string,
497
+ entity: string,
498
+ action: string,
499
+ profile: string,
500
+ ): Promise<string | undefined> {
501
+ try {
502
+ let result: KeyValue;
503
+ let searchParameters = {
504
+ indexName: "GSI2",
505
+ parameters: [
506
+ { name: "entity", value: "PERM", operator: "=" },
507
+ {
508
+ name: "relation2",
509
+ value: `${profile}|${entity}|${action}`,
510
+ operator: "=",
511
+ },
512
+ ],
513
+ };
514
+ let permissionQuery = await this.query<KeyValue>({
515
+ tableName: table,
516
+ searchParameters,
517
+ });
518
+ if (permissionQuery && permissionQuery.length > 0) {
519
+ result = permissionQuery[0];
520
+ return result.type === "ALL" ? "ALL" : "OWNS";
521
+ }
522
+ throw new Error("UNAUTHORIZE_REQUEST");
523
+ } catch (error) {
524
+ this.logger.error((error as Error).message);
525
+ throw error;
526
+ }
527
+ }
528
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inlaweb-lib-dynamodb",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/types/index.d.ts",
6
6
  "scripts": {