inlaweb-lib-dynamodb 1.0.1 → 1.0.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/.vscode/launch.json +22 -0
- package/.vscode/task.json +13 -0
- package/coverage/clover.xml +172 -0
- package/coverage/coverage-final.json +2 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +116 -0
- package/coverage/lcov-report/index.ts.html +1132 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov.info +270 -0
- package/{index.js → dist/index.js} +13 -7
- package/jest.config.js +9 -0
- package/package.json +1 -1
- package/readme.MD +8 -0
- package/src/commons/types.ts +71 -0
- package/src/index.ts +350 -0
- package/tests/index.test.ts +249 -0
- package/tsconfig.json +12 -0
- /package/{commons → dist/commons}/types.js +0 -0
- /package/{types → dist/types}/commons/types.d.ts +0 -0
- /package/{types → dist/types}/index.d.ts +0 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
2
|
+
import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, UpdateCommand, BatchWriteCommand, TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
|
|
3
|
+
import { Logger } from "@aws-lambda-powertools/logger"
|
|
4
|
+
import { ParamsPut, ParamsGet, ParamsQuery, ParamsUpdate, ProjectionExpression, SearchParameters, KeyValue, InputUpdate, Operation } from "./commons/types";
|
|
5
|
+
import moment from "moment-timezone";
|
|
6
|
+
|
|
7
|
+
const templatesUpdateExpresion = {
|
|
8
|
+
SET: (atribute: string) => `#${atribute} = :${atribute},`,
|
|
9
|
+
ADD: (atribute: string) => `#${atribute} :${atribute},`,
|
|
10
|
+
REMOVE: (atribute: string) => `#${atribute},`,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class DynamoLib {
|
|
14
|
+
private readonly client: DynamoDBClient;
|
|
15
|
+
private readonly docClient: DynamoDBDocumentClient;
|
|
16
|
+
private readonly logger: Logger;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this.client = new DynamoDBClient({});
|
|
20
|
+
this.docClient = DynamoDBDocumentClient.from(this.client);
|
|
21
|
+
this.logger = new Logger();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async putItem(tableName: string, item: KeyValue): Promise<void> {
|
|
25
|
+
const params: ParamsPut = {
|
|
26
|
+
TableName: tableName,
|
|
27
|
+
Item: item,
|
|
28
|
+
ReturnItemCollectionMetrics: "SIZE",
|
|
29
|
+
ReturnConsumedCapacity: "TOTAL",
|
|
30
|
+
};
|
|
31
|
+
this.logger.debug(`PutItem: ${JSON.stringify(params)}`);
|
|
32
|
+
const result = await this.docClient.send(new PutCommand(params));
|
|
33
|
+
this.logger.debug(`PutItem Result: ${JSON.stringify(result)}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getItem<T>(tableName: string, key: KeyValue, projectionExpression?: string): Promise<T | undefined> {
|
|
37
|
+
const params: ParamsGet = {
|
|
38
|
+
TableName: tableName,
|
|
39
|
+
Key: key,
|
|
40
|
+
ReturnConsumedCapacity: "TOTAL",
|
|
41
|
+
};
|
|
42
|
+
const projection = this.buildProjectionExpression(projectionExpression);
|
|
43
|
+
if(projection) {
|
|
44
|
+
params.ProjectionExpression = projection.projectionExpression;
|
|
45
|
+
params.ExpressionAttributeNames = projection.expressionAttributeNames;
|
|
46
|
+
}
|
|
47
|
+
this.logger.debug(`GetItem: ${JSON.stringify(params)}`);
|
|
48
|
+
const response = await this.docClient.send(new GetCommand(params));
|
|
49
|
+
this.logger.debug(`GetItem Result: ${JSON.stringify(response)}`);
|
|
50
|
+
if (response.Item) {
|
|
51
|
+
return response.Item as T;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async updateItem(params: InputUpdate): Promise<boolean> {
|
|
57
|
+
const itemUpdate = this.createUpdateParams(params);
|
|
58
|
+
this.logger.debug(`UpdateItem: ${JSON.stringify(itemUpdate.Update)}`);
|
|
59
|
+
const result = await this.docClient.send(new UpdateCommand(itemUpdate.Update));
|
|
60
|
+
this.logger.debug(`UpdateItem Result: ${JSON.stringify(result)}`);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async query<T>(params: {tableName: string, searchParameters: SearchParameters, permission?: string, username?: string}): Promise<T[] | undefined> {
|
|
65
|
+
try {
|
|
66
|
+
const parameters: ParamsQuery = this.buildQueryParams(params);
|
|
67
|
+
this.logger.debug(`Query: ${JSON.stringify(parameters)}`);
|
|
68
|
+
const response = await this.docClient.send(new QueryCommand(parameters));
|
|
69
|
+
this.logger.debug(`Query Result: ${JSON.stringify(response)}`);
|
|
70
|
+
if(response.Items) {
|
|
71
|
+
return response.Items as T[];
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.logger.error((error as Error).message);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private buildQueryParams(params: {tableName: string, searchParameters: SearchParameters, permission?: string, username?: string}): ParamsQuery {
|
|
82
|
+
const projection = this.buildProjectionExpression(params.searchParameters.projectionExpression);
|
|
83
|
+
|
|
84
|
+
const parameters: ParamsQuery = {
|
|
85
|
+
TableName: params.tableName,
|
|
86
|
+
KeyConditionExpression: "",
|
|
87
|
+
ExpressionAttributeNames: projection?.expressionAttributeNames || {},
|
|
88
|
+
ExpressionAttributeValues: {}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (params.permission && params.permission === "OWNS") {
|
|
92
|
+
params.searchParameters.parameters.push({
|
|
93
|
+
name: "creationUser",
|
|
94
|
+
value: params.username,
|
|
95
|
+
operator: "FILTER",
|
|
96
|
+
filterOperator: "=",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
params.searchParameters.parameters.forEach((searchParameter, index) => {
|
|
101
|
+
this.buildExpressions(searchParameter, index, parameters);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return parameters;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private buildExpressions(
|
|
108
|
+
searchParameter: KeyValue,
|
|
109
|
+
index: number,
|
|
110
|
+
params: ParamsQuery
|
|
111
|
+
): void {
|
|
112
|
+
if (index !== 0 && searchParameter.operator !== "FILTER") params.KeyConditionExpression += " and ";
|
|
113
|
+
|
|
114
|
+
switch (searchParameter.operator) {
|
|
115
|
+
case "begins_with":
|
|
116
|
+
params.KeyConditionExpression += `${searchParameter.operator}(#key${index}, :value${index})`;
|
|
117
|
+
break;
|
|
118
|
+
case "BETWEEN":
|
|
119
|
+
params.KeyConditionExpression += `#key${index} ${searchParameter.operator} :value${index} AND :value1${index}`;
|
|
120
|
+
params.ExpressionAttributeValues[`:value1${index}`] = searchParameter.value1;
|
|
121
|
+
break;
|
|
122
|
+
case "FILTER":
|
|
123
|
+
this.buildFilterExpression(searchParameter, index, params);
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
params.KeyConditionExpression += `#key${index} ${searchParameter.operator} :value${index}`;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
params.ExpressionAttributeNames[`#key${index}`] = searchParameter.name;
|
|
131
|
+
params.ExpressionAttributeValues[`:value${index}`] = searchParameter.value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private buildFilterExpression(
|
|
135
|
+
searchParameter: KeyValue,
|
|
136
|
+
index: number,
|
|
137
|
+
params: ParamsQuery
|
|
138
|
+
): void {
|
|
139
|
+
if (params.FilterExpression) params.FilterExpression += ' and ';
|
|
140
|
+
else params.FilterExpression = '';
|
|
141
|
+
|
|
142
|
+
switch (searchParameter.filterOperator) {
|
|
143
|
+
case 'contains':
|
|
144
|
+
params.FilterExpression += `contains (#key${index}, :value${index})`;
|
|
145
|
+
break;
|
|
146
|
+
case 'begins_with':
|
|
147
|
+
params.FilterExpression += `${searchParameter.filterOperator}(#key${index}, :value${index})`;
|
|
148
|
+
break;
|
|
149
|
+
case "attribute_not_exists":
|
|
150
|
+
params.FilterExpression += `attribute_not_exists (#key${index})`;
|
|
151
|
+
break;
|
|
152
|
+
case "attribute_exists":
|
|
153
|
+
params.FilterExpression += `attribute_exists (#key${index})`;
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
params.FilterExpression += `#key${index} ${searchParameter.filterOperator} :value${index}`;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private buildProjectionExpression(projectionExpression: string | undefined): ProjectionExpression | undefined {
|
|
162
|
+
if (!projectionExpression) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const fields = projectionExpression.split(",");
|
|
166
|
+
if(fields.length > 0) {
|
|
167
|
+
let projectionExpression = "";
|
|
168
|
+
const expressionAttributeNames: KeyValue = {};
|
|
169
|
+
fields.forEach((field) => {
|
|
170
|
+
projectionExpression += `#${field},`;
|
|
171
|
+
expressionAttributeNames[`#${field}`] = field;
|
|
172
|
+
});
|
|
173
|
+
projectionExpression = projectionExpression.slice(0, -1);
|
|
174
|
+
return { projectionExpression, expressionAttributeNames };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async saveBatch(table: string, items: KeyValue[]): Promise<boolean> {
|
|
179
|
+
const executions = this.createBatch(items);
|
|
180
|
+
for (let i = 0; i < executions.length; i++) {
|
|
181
|
+
const params = {
|
|
182
|
+
RequestItems: {
|
|
183
|
+
[table]: executions[i] as any
|
|
184
|
+
},
|
|
185
|
+
ReturnConsumedCapacity: "TOTAL" as const,
|
|
186
|
+
};
|
|
187
|
+
this.logger.debug(`BatchWrite: ${JSON.stringify(params)}`);
|
|
188
|
+
const response = await this.docClient.send(new BatchWriteCommand(params));
|
|
189
|
+
this.logger.debug(`BatchWrite Result: ${JSON.stringify(response)}`);
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private createBatch(items: KeyValue[]): KeyValue[] {
|
|
195
|
+
const executions: KeyValue[] = [];
|
|
196
|
+
const numberOfBacthInsertions = Math.ceil(items.length / 25);
|
|
197
|
+
for (let i = 0; i < numberOfBacthInsertions; i++) {
|
|
198
|
+
executions[i] = [];
|
|
199
|
+
}
|
|
200
|
+
items.forEach((item, index) => {
|
|
201
|
+
const currentIndex = Math.ceil((index + 1) / 25);
|
|
202
|
+
executions[currentIndex - 1].push({ PutRequest: { Item: item } });
|
|
203
|
+
});
|
|
204
|
+
return executions;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async writeTransactions(items: KeyValue[], limit: number): Promise<boolean> {
|
|
208
|
+
const executions = this.createWriteTransactions(items, limit);
|
|
209
|
+
for (let i = 0; i < executions.length; i++) {
|
|
210
|
+
const params = {
|
|
211
|
+
TransactItems: executions[i],
|
|
212
|
+
ReturnConsumedCapacity: "TOTAL" as const,
|
|
213
|
+
};
|
|
214
|
+
this.logger.debug(`TransactWrite: ${JSON.stringify(params)}`);
|
|
215
|
+
const response = await this.docClient.send(new TransactWriteCommand(params));
|
|
216
|
+
this.logger.debug(`TransactWrite Result: ${JSON.stringify(response)}`);
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
createWriteTransactions(items: KeyValue[], limit: number): KeyValue[][] {
|
|
222
|
+
const executions: KeyValue[][] = [];
|
|
223
|
+
const numberOfBacthInsertions = Math.ceil(items.length / limit);
|
|
224
|
+
for (let i = 0; i < numberOfBacthInsertions; i++) {
|
|
225
|
+
executions[i] = [];
|
|
226
|
+
}
|
|
227
|
+
items.forEach((item, index) => {
|
|
228
|
+
const currentIndex = Math.ceil((index + 1) / limit);
|
|
229
|
+
executions[currentIndex - 1].push({ Put: { Item: item } });
|
|
230
|
+
});
|
|
231
|
+
return executions;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
createUpdateParams(params: InputUpdate): KeyValue {
|
|
235
|
+
const { table, PK, SK, item, setAttributes, addAttributes, removeAttributes, username } = params;
|
|
236
|
+
// Se agregan los datos de auditoría para la actualización
|
|
237
|
+
item.updateUser = username;
|
|
238
|
+
item.updateDate = moment.tz(new Date(), "America/Bogota").format("YYYY-MM-DD");
|
|
239
|
+
if (!setAttributes.includes("updateUser")) setAttributes.push("updateUser");
|
|
240
|
+
if (!setAttributes.includes("updateDate")) setAttributes.push("updateDate");
|
|
241
|
+
// Se crea el objeto de la operación
|
|
242
|
+
let itemUpdate = {
|
|
243
|
+
Update: {
|
|
244
|
+
TableName: table,
|
|
245
|
+
Key: { PK, SK },
|
|
246
|
+
UpdateExpression: ``,
|
|
247
|
+
ExpressionAttributeNames: {},
|
|
248
|
+
ExpressionAttributeValues: {},
|
|
249
|
+
ReturnConsumedCapacity: "TOTAL",
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
// Se agregan los campos a modificar
|
|
253
|
+
if (Array.isArray(setAttributes) && setAttributes.length) {
|
|
254
|
+
this.formatUpdateExpression("SET", itemUpdate, item, setAttributes);
|
|
255
|
+
}
|
|
256
|
+
// Se agragan los campos nuevos del item
|
|
257
|
+
if (Array.isArray(addAttributes) && addAttributes.length) {
|
|
258
|
+
this.formatUpdateExpression("ADD", itemUpdate, item, addAttributes);
|
|
259
|
+
}
|
|
260
|
+
// Se agregan los campos que deben ser borrados
|
|
261
|
+
if (Array.isArray(removeAttributes) && removeAttributes.length) {
|
|
262
|
+
this.formatUpdateExpressionRemove(itemUpdate, removeAttributes);
|
|
263
|
+
}
|
|
264
|
+
// Se remueve el espacio al final de la expresión
|
|
265
|
+
itemUpdate.Update.UpdateExpression = itemUpdate.Update.UpdateExpression.slice(0, -1);
|
|
266
|
+
return itemUpdate;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private formatUpdateExpression(operation: Operation, updateObject: KeyValue, item: KeyValue, atributes: string[]): void {
|
|
270
|
+
// Se agrega la operación.
|
|
271
|
+
updateObject.Update.UpdateExpression += `${operation} `;
|
|
272
|
+
// Se obtiene el templete de acuerdo a la operación.
|
|
273
|
+
let operationTemplate = templatesUpdateExpresion[operation];
|
|
274
|
+
// Se añaden los items afectados por la operación.
|
|
275
|
+
for (let atribute of atributes) {
|
|
276
|
+
if (typeof item[atribute] === "boolean" || typeof item[atribute] === "number" || item[atribute]) {
|
|
277
|
+
updateObject.Update.UpdateExpression += operationTemplate(atribute);
|
|
278
|
+
updateObject.Update.ExpressionAttributeNames[`#${atribute}`] = atribute;
|
|
279
|
+
updateObject.Update.ExpressionAttributeValues[`:${atribute}`] = item[atribute];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Se remueve la coma final para evitar errores con el SDK.
|
|
283
|
+
updateObject.Update.UpdateExpression = updateObject.Update.UpdateExpression.slice(0, -1);
|
|
284
|
+
// Se añade un espacio para poder agregar nuevas operaciones.
|
|
285
|
+
updateObject.Update.UpdateExpression += " ";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private formatUpdateExpressionRemove(updateObject: KeyValue, atributes: string[]): void {
|
|
289
|
+
let operation: Operation = "REMOVE";
|
|
290
|
+
// Se agrega la operación.
|
|
291
|
+
updateObject.Update.UpdateExpression += `${operation} `;
|
|
292
|
+
// Se obtiene el templete de acuerdo a la operación.
|
|
293
|
+
let operationTemplate = templatesUpdateExpresion[operation];
|
|
294
|
+
// Se añaden los items afectados por la operación.
|
|
295
|
+
for (let atribute of atributes) {
|
|
296
|
+
updateObject.Update.UpdateExpression += operationTemplate(atribute);
|
|
297
|
+
updateObject.Update.ExpressionAttributeNames[`#${atribute}`] = atribute;
|
|
298
|
+
}
|
|
299
|
+
// Se remueve la coma final para evitar errores con el SDK.
|
|
300
|
+
updateObject.Update.UpdateExpression = updateObject.Update.UpdateExpression.slice(0, -1);
|
|
301
|
+
// Se añade un espacio para poder agregar nuevas operaciones.
|
|
302
|
+
updateObject.Update.UpdateExpression += " ";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async getId(table: string, entity: string, username: string, quantity?: number): Promise<number> {
|
|
306
|
+
let item = await this.getItem<KeyValue>(table, {PK: "COUN", SK: entity}, "order");
|
|
307
|
+
if (!item) {
|
|
308
|
+
item = {
|
|
309
|
+
PK: "COUN",
|
|
310
|
+
SK: entity,
|
|
311
|
+
entity: "COUN",
|
|
312
|
+
order: 0,
|
|
313
|
+
creationUser: username,
|
|
314
|
+
creationDate: moment.tz(new Date(), "America/Bogota").format("YYYY-MM-DD"),
|
|
315
|
+
};
|
|
316
|
+
await this.putItem(table, item);
|
|
317
|
+
}
|
|
318
|
+
const params: ParamsUpdate = {
|
|
319
|
+
TableName: table,
|
|
320
|
+
Key: { PK: "COUN", SK: entity },
|
|
321
|
+
UpdateExpression: `SET #id = #id + :increment`,
|
|
322
|
+
ExpressionAttributeNames: {"#id": "order"},
|
|
323
|
+
ExpressionAttributeValues: {":increment": quantity || 1 },
|
|
324
|
+
ReturnValues: "UPDATED_OLD"
|
|
325
|
+
};
|
|
326
|
+
this.logger.debug(`GetId: ${JSON.stringify(params)}`);
|
|
327
|
+
const result = await this.docClient.send(new UpdateCommand(params));
|
|
328
|
+
this.logger.debug(`GetId Result: ${JSON.stringify(result)}`);
|
|
329
|
+
if(result.Attributes) return result.Attributes.order + 1;
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async validatePermission(table: string, entity: string, action: string, profile: string): Promise<string | undefined> {
|
|
334
|
+
let result: KeyValue;
|
|
335
|
+
|
|
336
|
+
let searchParameters = {
|
|
337
|
+
indexName: "GSI2",
|
|
338
|
+
parameters: [
|
|
339
|
+
{ name: "entity", value: "PERM", operator: "=" },
|
|
340
|
+
{ name: "relation2", value: `${profile}|${entity}|${action}`, operator: "=" },
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
let permissionQuery = await this.query<KeyValue>({tableName: table, searchParameters});
|
|
344
|
+
if (permissionQuery && permissionQuery.length > 0) {
|
|
345
|
+
result = permissionQuery[0];
|
|
346
|
+
return result.type === "ALL" ? "ALL" : "OWNS";
|
|
347
|
+
}
|
|
348
|
+
throw new Error("UNAUTHORIZE_REQUEST");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
2
|
+
import { DynamoDBDocumentClient, GetCommand, PutCommand, UpdateCommand, QueryCommand, BatchWriteCommand, TransactWriteCommand } from "@aws-sdk/lib-dynamodb"
|
|
3
|
+
import { DynamoLib } from '../src/index';
|
|
4
|
+
import { ParamsQuery, SearchParameters } from "../src/commons/types";
|
|
5
|
+
import moment, { tz } from "moment-timezone";
|
|
6
|
+
|
|
7
|
+
const mockSend = jest.fn();
|
|
8
|
+
|
|
9
|
+
jest.mock("@aws-sdk/lib-dynamodb", () => ({
|
|
10
|
+
DynamoDBDocumentClient: {
|
|
11
|
+
from: () => ({
|
|
12
|
+
send: mockSend,
|
|
13
|
+
}),
|
|
14
|
+
},
|
|
15
|
+
GetCommand: jest.fn().mockImplementation((params) => params),
|
|
16
|
+
PutCommand: jest.fn(),
|
|
17
|
+
UpdateCommand: jest.fn().mockImplementation((params) => params),
|
|
18
|
+
QueryCommand: jest.fn().mockImplementation((params) => params),
|
|
19
|
+
BatchWriteCommand: jest.fn().mockImplementation((params) => params),
|
|
20
|
+
TransactWriteCommand: jest.fn().mockImplementation((params) => params),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.mock("moment-timezone", () => {
|
|
24
|
+
return{
|
|
25
|
+
tz: jest.fn().mockReturnValue({
|
|
26
|
+
format: jest.fn().mockReturnValue("2025-02-25"),
|
|
27
|
+
}),
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('DynamoLib', () => {
|
|
32
|
+
let dynamoLib: DynamoLib;
|
|
33
|
+
let tableName: string;
|
|
34
|
+
let item: { [key: string]: any };
|
|
35
|
+
let items: { [key: string]: any }[];
|
|
36
|
+
let key: { [key: string]: any };
|
|
37
|
+
let projection: string | undefined;
|
|
38
|
+
let searchParameters: SearchParameters = { indexName: "", parameters: [] };
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
tableName = 'testTable';
|
|
43
|
+
item = { id: '1', name: 'test' };
|
|
44
|
+
items = [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }];
|
|
45
|
+
key = { id: '1' };
|
|
46
|
+
projection = "key,name";
|
|
47
|
+
searchParameters.indexName = "GSI2";
|
|
48
|
+
searchParameters.parameters = [
|
|
49
|
+
{ name: "entity", value: "entity", operator: "=" },
|
|
50
|
+
];
|
|
51
|
+
dynamoLib = new DynamoLib();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
jest.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('putItem', () => {
|
|
59
|
+
it('should put an item into the table', async () => {
|
|
60
|
+
|
|
61
|
+
await dynamoLib.putItem(tableName, item);
|
|
62
|
+
|
|
63
|
+
expect(PutCommand).toHaveBeenCalledWith({"Item": {"id": "1", "name": "test"}, "TableName": "testTable", "ReturnConsumedCapacity": "TOTAL", "ReturnItemCollectionMetrics": "SIZE"});
|
|
64
|
+
expect(mockSend).toHaveBeenCalledWith({});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('getItem', () => {
|
|
69
|
+
|
|
70
|
+
it('should get an item from the table', async () => {
|
|
71
|
+
mockSend.mockResolvedValue({ Item: item });
|
|
72
|
+
|
|
73
|
+
const result = await dynamoLib.getItem(tableName, key, projection);
|
|
74
|
+
|
|
75
|
+
expect(GetCommand).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#key": "key", "#name": "name"}, "Key": {"id": "1"}, "ProjectionExpression": "#key,#name", "TableName": "testTable", "ReturnConsumedCapacity": "TOTAL"});
|
|
76
|
+
expect(mockSend).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#key": "key", "#name": "name"}, "Key": {"id": "1"}, "ProjectionExpression": "#key,#name", "TableName": "testTable", "ReturnConsumedCapacity": "TOTAL"});
|
|
77
|
+
expect(result).toEqual(item);
|
|
78
|
+
});
|
|
79
|
+
it('should return undefined if item is not found', async () => {
|
|
80
|
+
|
|
81
|
+
mockSend.mockResolvedValue({});
|
|
82
|
+
|
|
83
|
+
const result = await dynamoLib.getItem(tableName, key);
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual(undefined);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('updateItem', () => {
|
|
90
|
+
it('should update an item', async () => {
|
|
91
|
+
|
|
92
|
+
await dynamoLib.updateItem({table: tableName, PK: "PK", SK: "SK", username: "username", item, setAttributes: ["name"]});
|
|
93
|
+
|
|
94
|
+
expect(UpdateCommand).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#name": "name", "#updateDate": "updateDate", "#updateUser": "updateUser"}, "ExpressionAttributeValues": {":name": "test", ":updateDate": "2025-02-25", ":updateUser": "username"}, "Key": {"PK": "PK", "SK": "SK"}, "TableName": "testTable", "UpdateExpression": "SET #name = :name,#updateUser = :updateUser,#updateDate = :updateDate", "ReturnConsumedCapacity": "TOTAL"});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('query', () => {
|
|
99
|
+
it('should get items of the table', async () => {
|
|
100
|
+
searchParameters.parameters.push({ name: "filter1", value: "filter1", operator: "="});
|
|
101
|
+
mockSend.mockResolvedValue({ Items: items });
|
|
102
|
+
|
|
103
|
+
const result = await dynamoLib.query({tableName, searchParameters});
|
|
104
|
+
|
|
105
|
+
expect(QueryCommand).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#key0": "entity", "#key1": "filter1"}, "ExpressionAttributeValues": {":value0": "entity", ":value1": "filter1"}, "KeyConditionExpression": "#key0 = :value0 and #key1 = :value1", "TableName": "testTable"});
|
|
106
|
+
expect(mockSend).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#key0": "entity", "#key1": "filter1"}, "ExpressionAttributeValues": {":value0": "entity", ":value1": "filter1"}, "KeyConditionExpression": "#key0 = :value0 and #key1 = :value1", "TableName": "testTable"});
|
|
107
|
+
expect(result).toEqual(items);
|
|
108
|
+
});
|
|
109
|
+
it('should call QueryCommand with filterexpressions', async () => {
|
|
110
|
+
searchParameters.parameters.push({ name: "filter1", value: "filter1", operator: "FILTER", filterOperator: "="});
|
|
111
|
+
searchParameters.parameters.push({ name: "filter2", value: "filter", operator: "FILTER", filterOperator: "contains"});
|
|
112
|
+
searchParameters.parameters.push({ name: "filter3", value: "filter3", operator: "FILTER", filterOperator: "begins_with"});
|
|
113
|
+
searchParameters.parameters.push({ name: "filter4", value: "filter4", operator: "FILTER", filterOperator: "attribute_exists"});
|
|
114
|
+
searchParameters.parameters.push({ name: "filter5", value: "filter5", operator: "FILTER", filterOperator: "attribute_not_exists"});
|
|
115
|
+
mockSend.mockResolvedValue({ Items: items });
|
|
116
|
+
|
|
117
|
+
await dynamoLib.query({tableName, searchParameters});
|
|
118
|
+
|
|
119
|
+
expect(QueryCommand).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#key0": "entity", "#key1": "filter1", "#key2": "filter2", "#key3": "filter3", "#key4": "filter4", "#key5": "filter5"}, "ExpressionAttributeValues": {":value0": "entity", ":value1": "filter1", ":value2": "filter", ":value3": "filter3", ":value4": "filter4", ":value5": "filter5"}, "FilterExpression": "#key1 = :value1 and contains (#key2, :value2) and begins_with(#key3, :value3) and attribute_exists (#key4) and attribute_not_exists (#key5)", "KeyConditionExpression": "#key0 = :value0", "TableName": "testTable"});
|
|
120
|
+
});
|
|
121
|
+
it('should call QueryCommand with filterexpression for own registers', async () => {
|
|
122
|
+
const permission = 'OWNS';
|
|
123
|
+
const username = 'testUser';
|
|
124
|
+
mockSend.mockResolvedValue({ Items: items });
|
|
125
|
+
|
|
126
|
+
await dynamoLib.query({tableName, searchParameters, permission, username});
|
|
127
|
+
|
|
128
|
+
expect(QueryCommand).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#key0": "entity", "#key1": "creationUser"}, "ExpressionAttributeValues": {":value0": "entity", ":value1": "testUser"}, "FilterExpression": "#key1 = :value1", "KeyConditionExpression": "#key0 = :value0", "TableName": "testTable"});
|
|
129
|
+
});
|
|
130
|
+
it('should call QueryCommand with keyCondition begins_with', async () => {
|
|
131
|
+
searchParameters.parameters[0].operator = "begins_with";
|
|
132
|
+
mockSend.mockResolvedValue({ Items: items });
|
|
133
|
+
|
|
134
|
+
await dynamoLib.query({tableName, searchParameters});
|
|
135
|
+
|
|
136
|
+
expect(QueryCommand).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#key0": "entity"}, "ExpressionAttributeValues": {":value0": "entity"}, "KeyConditionExpression": "begins_with(#key0, :value0)", "TableName": "testTable"});
|
|
137
|
+
});
|
|
138
|
+
it('should throw error', async () => {
|
|
139
|
+
mockSend.mockRejectedValue(new Error("error"));
|
|
140
|
+
|
|
141
|
+
await expect(dynamoLib.query({tableName, searchParameters})).rejects.toThrow('error');
|
|
142
|
+
});
|
|
143
|
+
it('should call QueryCommand with keyCondition between', async () => {
|
|
144
|
+
searchParameters.parameters[0].operator = "BETWEEN";
|
|
145
|
+
searchParameters.parameters[0].value = "2025-01-01";
|
|
146
|
+
searchParameters.parameters[0].value1 = "2025-01-31";
|
|
147
|
+
mockSend.mockResolvedValue({ Items: items });
|
|
148
|
+
|
|
149
|
+
await dynamoLib.query({tableName, searchParameters});
|
|
150
|
+
|
|
151
|
+
expect(QueryCommand).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#key0": "entity"}, "ExpressionAttributeValues": {":value0": "2025-01-01", ":value10": "2025-01-31"}, "KeyConditionExpression": "#key0 BETWEEN :value0 AND :value10", "TableName": "testTable"});
|
|
152
|
+
});
|
|
153
|
+
it('should return undefined if items is not found', async () => {
|
|
154
|
+
|
|
155
|
+
mockSend.mockResolvedValue({});
|
|
156
|
+
|
|
157
|
+
const result = await dynamoLib.query({tableName, searchParameters});
|
|
158
|
+
|
|
159
|
+
expect(result).toEqual(undefined);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('saveBatch', () => {
|
|
164
|
+
it('should save batch of items', async () => {
|
|
165
|
+
|
|
166
|
+
await dynamoLib.saveBatch(tableName, items);
|
|
167
|
+
|
|
168
|
+
expect(BatchWriteCommand).toHaveBeenCalledWith({"RequestItems": {"testTable": [{"PutRequest": {"Item": {"id": "1", "name": "test"}}}, {"PutRequest": {"Item": {"id": "2", "name": "test2"}}}]}, "ReturnConsumedCapacity": "TOTAL"});
|
|
169
|
+
expect(mockSend).toHaveBeenCalledWith({"RequestItems": {"testTable": [{"PutRequest": {"Item": {"id": "1", "name": "test"}}}, {"PutRequest": {"Item": {"id": "2", "name": "test2"}}}]}, "ReturnConsumedCapacity": "TOTAL"});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('writeTransaction', () => {
|
|
174
|
+
it('should save transaction', async () => {
|
|
175
|
+
|
|
176
|
+
await dynamoLib.writeTransactions(items, 24);
|
|
177
|
+
|
|
178
|
+
expect(TransactWriteCommand).toHaveBeenCalledWith({"ReturnConsumedCapacity": "TOTAL", "TransactItems": [{"Put": {"Item": {"id": "1", "name": "test"}}}, {"Put": {"Item": {"id": "2", "name": "test2"}}}]});
|
|
179
|
+
expect(mockSend).toHaveBeenCalledWith({"ReturnConsumedCapacity": "TOTAL", "TransactItems": [{"Put": {"Item": {"id": "1", "name": "test"}}}, {"Put": {"Item": {"id": "2", "name": "test2"}}}]});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('createUpdateParams', () => {
|
|
184
|
+
it('should create item formatted to update', async () => {
|
|
185
|
+
|
|
186
|
+
const result = dynamoLib.createUpdateParams({table: tableName, PK: "PK", SK: "SK", username: "username", item, setAttributes: ["name"], addAttributes: ["id"], removeAttributes: ["address"]});
|
|
187
|
+
|
|
188
|
+
expect(result).toEqual({"Update": {"ExpressionAttributeNames": {"#address": "address", "#id": "id", "#name": "name", "#updateDate": "updateDate", "#updateUser": "updateUser"}, "ExpressionAttributeValues": {":id": "1", ":name": "test", ":updateDate": "2025-02-25", ":updateUser": "username"}, "Key": {"PK": "PK", "SK": "SK"}, "ReturnConsumedCapacity": "TOTAL", "TableName": "testTable", "UpdateExpression": "SET #name = :name,#updateUser = :updateUser,#updateDate = :updateDate ADD #id :id REMOVE #address"}});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('getId', () => {
|
|
193
|
+
it('should return next id to use', async () => {
|
|
194
|
+
const spyGetItem = jest.spyOn(dynamoLib, 'getItem');
|
|
195
|
+
spyGetItem.mockImplementation((table, key) => Promise.resolve(undefined));
|
|
196
|
+
const spyPutItem = jest.spyOn(dynamoLib, 'putItem');
|
|
197
|
+
spyPutItem.mockImplementation((table, item) => Promise.resolve());
|
|
198
|
+
mockSend.mockResolvedValue({ Attributes: { order: 0 }});
|
|
199
|
+
|
|
200
|
+
const result = await dynamoLib.getId(tableName, "entity", "username");
|
|
201
|
+
|
|
202
|
+
expect(spyGetItem).toHaveBeenCalledWith("testTable", {"PK": "COUN", "SK": "entity"}, "order");
|
|
203
|
+
expect(spyPutItem).toHaveBeenCalledWith("testTable", {"PK": "COUN", "SK": "entity", "creationDate": "2025-02-25", "creationUser": "username", "entity": "COUN", "order": 0});
|
|
204
|
+
expect(UpdateCommand).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#id": "order"}, "ExpressionAttributeValues": {":increment": 1}, "Key": {"PK": "COUN", "SK": "entity"}, "ReturnValues": "UPDATED_OLD", "TableName": "testTable", "UpdateExpression": "SET #id = #id + :increment"});
|
|
205
|
+
expect(mockSend).toHaveBeenCalledWith({"ExpressionAttributeNames": {"#id": "order"}, "ExpressionAttributeValues": {":increment": 1}, "Key": {"PK": "COUN", "SK": "entity"}, "ReturnValues": "UPDATED_OLD", "TableName": "testTable", "UpdateExpression": "SET #id = #id + :increment"});
|
|
206
|
+
expect(result).toEqual(1);
|
|
207
|
+
});
|
|
208
|
+
it('should return 1 if element not exists', async () => {
|
|
209
|
+
const spyGetItem = jest.spyOn(dynamoLib, 'getItem');
|
|
210
|
+
spyGetItem.mockImplementation((table, key) => Promise.resolve(undefined));
|
|
211
|
+
const spyPutItem = jest.spyOn(dynamoLib, 'putItem');
|
|
212
|
+
spyPutItem.mockImplementation((table, item) => Promise.resolve());
|
|
213
|
+
mockSend.mockResolvedValue({});
|
|
214
|
+
|
|
215
|
+
const result = await dynamoLib.getId(tableName, "entity", "username");
|
|
216
|
+
|
|
217
|
+
expect(result).toEqual(1);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('validatePermission', () => {
|
|
222
|
+
it('should return ALL', async () => {
|
|
223
|
+
const spyQuery = jest.spyOn(dynamoLib, 'query');
|
|
224
|
+
spyQuery.mockImplementation((params) => Promise.resolve([{type: "ALL"}]));
|
|
225
|
+
mockSend.mockResolvedValue({ Attributes: { order: 0 }});
|
|
226
|
+
|
|
227
|
+
const result = await dynamoLib.validatePermission(tableName, "entity", "INSERT", "PROF1");
|
|
228
|
+
|
|
229
|
+
expect(spyQuery).toHaveBeenCalledWith({"searchParameters": {"indexName": "GSI2", "parameters": [{"name": "entity", "operator": "=", "value": "PERM"}, {"name": "relation2", "operator": "=", "value": "PROF1|entity|INSERT"}]}, "tableName": "testTable"});
|
|
230
|
+
expect(result).toEqual("ALL");
|
|
231
|
+
});
|
|
232
|
+
it('should return OWNS', async () => {
|
|
233
|
+
const spyQuery = jest.spyOn(dynamoLib, 'query');
|
|
234
|
+
spyQuery.mockImplementation((params) => Promise.resolve([{type: "OWNS"}]));
|
|
235
|
+
mockSend.mockResolvedValue({ Attributes: { order: 0 }});
|
|
236
|
+
|
|
237
|
+
const result = await dynamoLib.validatePermission(tableName, "entity", "INSERT", "PROF1");
|
|
238
|
+
|
|
239
|
+
expect(spyQuery).toHaveBeenCalledWith({"searchParameters": {"indexName": "GSI2", "parameters": [{"name": "entity", "operator": "=", "value": "PERM"}, {"name": "relation2", "operator": "=", "value": "PROF1|entity|INSERT"}]}, "tableName": "testTable"});
|
|
240
|
+
expect(result).toEqual("OWNS");
|
|
241
|
+
});
|
|
242
|
+
it('should trow error if permission not exists', async () => {
|
|
243
|
+
const spyQuery = jest.spyOn(dynamoLib, 'query');
|
|
244
|
+
spyQuery.mockImplementation((params) => Promise.resolve([]));
|
|
245
|
+
|
|
246
|
+
await expect(dynamoLib.validatePermission(tableName, "entity", "INSERT", "PROF1")).rejects.toThrow("UNAUTHORIZE_REQUEST");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
package/tsconfig.json
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|