@baadal-sdk/dapi 0.28.4 → 0.31.0

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/src/aws/db.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Examples:
3
+ * Ref: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-examples.html
4
+ *
2
5
  * Partition key vs Composite primary key:
3
6
  * Ref: https://aws.amazon.com/premiumsupport/knowledge-center/primary-key-dynamodb-table/
4
7
  */
@@ -8,8 +11,12 @@ import {
8
11
  DynamoDBDocumentClient,
9
12
  GetCommand,
10
13
  GetCommandInput,
14
+ BatchGetCommand,
15
+ BatchGetCommandInput,
11
16
  PutCommand,
12
17
  PutCommandInput,
18
+ BatchWriteCommand,
19
+ BatchWriteCommandInput,
13
20
  UpdateCommand,
14
21
  UpdateCommandInput,
15
22
  QueryCommand,
@@ -20,86 +27,141 @@ import {
20
27
  DeleteCommandInput,
21
28
  } from '@aws-sdk/lib-dynamodb';
22
29
  import short from 'short-uuid';
30
+ import { chunkifyArray } from '@baadal-sdk/utils';
23
31
 
24
- import dbClient from './db-client';
32
+ import { dbClient } from './client';
25
33
  import { StringIndexable } from '../common/common.model';
26
34
  import { CustomError } from '../common/error';
35
+ import { warn, error } from '../common/logger';
36
+ import { BATCH_SIZE, CHUNK_SIZE } from '../common/const';
27
37
 
28
38
  const DynamoDBError = (msg: string) => new CustomError(msg, { name: 'DynamoDBError' });
29
39
 
40
+ /** @internal */
30
41
  export const init = (region: string) => {
31
- const dydbClient = new DynamoDBClient({ region });
32
-
33
42
  // Ref: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_lib_dynamodb.html#configuration
34
- if (!dbClient.dbDocClient) {
35
- dbClient.dbDocClient = DynamoDBDocumentClient.from(dydbClient);
43
+ if (!dbClient.client) {
44
+ const dydbClient = new DynamoDBClient({ region }); // may also pass `credentials`
45
+ dbClient.client = DynamoDBDocumentClient.from(dydbClient);
36
46
  dbClient.id = short.uuid();
37
47
  return true;
38
48
  }
39
49
  return false;
40
50
  };
41
51
 
52
+ /** @internal */
42
53
  export const status = () => dbClient.id;
43
54
 
44
- const tryInit = () => {
45
- if (status()) return;
55
+ const tryInit = (silent = false) => {
56
+ if (dbClient.client) return;
46
57
  const region = process.env.AWS_REGION || '';
47
58
  if (region) {
48
- if (init(region)) return;
59
+ if (init(region)) {
60
+ // console.log('Auto-initialization of DynamoDB successful');
61
+ return;
62
+ }
63
+ }
64
+ if (!silent) {
65
+ // throw DynamoDBError('DynamoDB is possibly uninitialized!');
66
+ throw DynamoDBError('Could not auto-initialize DynamoDB!');
49
67
  }
50
- // throw DynamoDBError('DynamoDB is possibly uninitialized!');
51
- throw DynamoDBError('Could not auto-initialize DynamoDB!');
52
68
  };
53
69
 
54
70
  // auto-initialize on load
55
- // tryInit();
71
+ tryInit(true);
56
72
 
57
- const writeItemForceHelper = async <T = any>(table: string, data: T, key: string, i: number): Promise<T | null> => {
58
- if (!dbClient.dbDocClient) tryInit();
59
- if (!dbClient.dbDocClient) return null;
60
- if (!table || !data) return null;
73
+ const writeItemForceHelper = async <T = any>(table: string, item: T, key: string, i: number): Promise<T | null> => {
74
+ if (!dbClient.client) tryInit();
75
+ if (!dbClient.client) return null;
76
+ if (!table || !item) return null;
61
77
 
62
- if (!(data as any)[key]) {
63
- (data as any)[key] = short.uuid();
78
+ if (!(item as any)[key]) {
79
+ (item as any)[key] = short.uuid();
64
80
  }
65
- const cmdParams: PutCommandInput = { TableName: table, Item: data, ConditionExpression: `attribute_not_exists(${key})` };
81
+ const cmdParams: PutCommandInput = { TableName: table, Item: item, ConditionExpression: `attribute_not_exists(${key})` };
66
82
  const command = new PutCommand(cmdParams);
67
83
  const numberOfAttempts = 3;
68
84
 
69
85
  try {
70
- await dbClient.dbDocClient.send(command);
86
+ await dbClient.client.send(command);
71
87
  } catch (err: any) {
72
- // console.error('PutCommandInput:', cmdParams);
73
- // console.error(err);
74
88
  if (err.name === 'ConditionalCheckFailedException') {
75
89
  if (i < numberOfAttempts - 1) {
76
- (data as any)[key] = short.uuid(); // new primary key
77
- const ret: T | null = await writeItemForceHelper(table, data, key, i + 1);
90
+ (item as any)[key] = short.uuid(); // new primary key
91
+ const ret: T | null = await writeItemForceHelper(table, item, key, i + 1);
78
92
  return ret;
79
93
  }
80
94
  console.error('PutCommandInput:', cmdParams);
81
- console.error('[ERROR] Maximum attempts overflow!');
95
+ error('[ERROR] Maximum attempts overflow!');
82
96
  }
83
97
  return null;
84
98
  }
85
99
 
86
- return data;
100
+ return item;
87
101
  };
88
102
 
89
- export const writeItemForce = async <T = any>(table: string, data: T, key = 'id'): Promise<T | null> => {
90
- return writeItemForceHelper<T>(table, data, key, 0);
103
+ export interface WriteItemForceInput<T = any> {
104
+ table: string;
105
+ item: T;
106
+ key?: string;
107
+ }
108
+
109
+ /**
110
+ * Write an item to a DynamoDB table, retry in case of key conflict
111
+ * @param input input command object
112
+ * @returns the created item, null in case of error
113
+ *
114
+ * ```js
115
+ * writeItemForce({
116
+ * table: 'lesson_list',
117
+ * item: { title: 'My Lesson' },
118
+ * key: 'id',
119
+ * });
120
+ *
121
+ * interface WriteItemForceInput<T = any> {
122
+ * table: string;
123
+ * item: T;
124
+ * key?: string; // default: `id`
125
+ * }
126
+ * ```
127
+ */
128
+ export const writeItemForce = async <T = any>(input: WriteItemForceInput<T>): Promise<T | null> => {
129
+ const key = input.key || 'id';
130
+ return writeItemForceHelper<T>(input.table, input.item, key, 0);
91
131
  };
92
132
 
93
- export const writeItem = async (table: string, data: StringIndexable) => {
94
- if (!dbClient.dbDocClient) tryInit();
95
- if (!dbClient.dbDocClient) return null;
96
- if (!table || !data) return null;
133
+ export interface WriteItemInput {
134
+ table: string;
135
+ item: StringIndexable;
136
+ }
97
137
 
98
- const cmdParams: PutCommandInput = { TableName: table, Item: data };
138
+ /**
139
+ * Write an item to a DynamoDB table
140
+ * @param input input command object
141
+ * @returns true if successful, null in case of error
142
+ *
143
+ * ```js
144
+ * writeItem({
145
+ * table: 'lesson_list',
146
+ * item: { id: 'id_001', title: 'My Lesson' },
147
+ * });
148
+ *
149
+ * interface WriteItemInput {
150
+ * table: string;
151
+ * item: StringIndexable;
152
+ * }
153
+ * ```
154
+ */
155
+ export const writeItem = async (input: WriteItemInput) => {
156
+ if (!dbClient.client) tryInit();
157
+ if (!dbClient.client) return null;
158
+ if (!input.table || !input.item) return null;
159
+
160
+ const cmdParams: PutCommandInput = { TableName: input.table, Item: input.item };
99
161
  const command = new PutCommand(cmdParams);
100
162
 
101
163
  try {
102
- await dbClient.dbDocClient.send(command);
164
+ await dbClient.client.send(command);
103
165
  } catch (err) {
104
166
  console.error('PutCommandInput:', cmdParams);
105
167
  console.error(err);
@@ -110,28 +172,127 @@ export const writeItem = async (table: string, data: StringIndexable) => {
110
172
  return true;
111
173
  };
112
174
 
113
- export const updateItem = async (
114
- table: string,
115
- key: StringIndexable,
116
- update: string,
117
- attr: StringIndexable,
118
- attrNames?: StringIndexable
119
- ) => {
120
- if (!dbClient.dbDocClient) tryInit();
121
- if (!dbClient.dbDocClient) return null;
122
- if (!table || !key || !update || !attr) return null;
175
+ // Ref: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-table-read-write-batch.html
176
+ const batchWriteItems = async (table: string, items: StringIndexable[]) => {
177
+ if (!dbClient.client) tryInit();
178
+ if (!dbClient.client) return null;
179
+ if (!table || !items || !Array.isArray(items)) return null;
180
+ if (!items.length) return false;
181
+
182
+ const reqList = items.map(item => ({ PutRequest: { Item: item } }));
183
+ const cmdParams: BatchWriteCommandInput = {
184
+ RequestItems: {
185
+ [table]: reqList,
186
+ },
187
+ };
188
+
189
+ const command = new BatchWriteCommand(cmdParams);
190
+
191
+ try {
192
+ await dbClient.client.send(command);
193
+ } catch (err) {
194
+ console.error('BatchWriteCommandInput:', cmdParams);
195
+ console.error(err);
196
+ return null;
197
+ // throw err;
198
+ }
199
+
200
+ return true;
201
+ };
202
+
203
+ export interface WriteItemsAllInput {
204
+ table: string;
205
+ items: StringIndexable[];
206
+ }
207
+
208
+ /**
209
+ * Write an list of items to a DynamoDB table
210
+ * @param input input command object
211
+ * @returns true if successful, null in case of error
212
+ *
213
+ * ```js
214
+ * writeItemsAll({
215
+ * table: 'lesson_list',
216
+ * items: [{ id: 'id_001', title: 'My Lesson' }, { id: 'id_002', title: 'My Lesson 2' }],
217
+ * });
218
+ *
219
+ * interface WriteItemInput {
220
+ * table: string;
221
+ * items: StringIndexable[];
222
+ * }
223
+ * ```
224
+ */
225
+ export const writeItemsAll = async (input: WriteItemsAllInput) => {
226
+ if (!dbClient.client) tryInit();
227
+ if (!dbClient.client) return null;
228
+ if (!input.table || !input.items || !Array.isArray(input.items)) return null;
229
+ if (!input.items.length) return false;
230
+
231
+ let errFlag = false;
232
+
233
+ const batchedItems = chunkifyArray(input.items, BATCH_SIZE);
234
+ const chunkedItems = chunkifyArray(batchedItems, CHUNK_SIZE);
235
+
236
+ for (let i = 0; i < chunkedItems.length; i += 1) {
237
+ const bchunks = chunkedItems[i];
238
+
239
+ const brlist = bchunks.map(iItems => batchWriteItems(input.table, iItems));
240
+ const bslist = await Promise.all(brlist); // eslint-disable-line no-await-in-loop
241
+
242
+ const isSuccess = bslist.every(e => e === true);
243
+ if (!isSuccess) errFlag = true;
244
+ }
245
+
246
+ return errFlag ? null : true;
247
+ };
248
+
249
+ export interface UpdateItemInput {
250
+ table: string;
251
+ key: StringIndexable;
252
+ update: string;
253
+ attr: StringIndexable;
254
+ attrNames?: StringIndexable;
255
+ }
256
+
257
+ /**
258
+ * Update an item in DynamoDB table
259
+ * @param input input command object
260
+ * @returns true if successful, null in case of error
261
+ *
262
+ * ```js
263
+ * updateItem({
264
+ * table: 'lesson_list',
265
+ * key: { id: 'id_001' },
266
+ * update: 'SET status = :status, #rev = 10',
267
+ * attr: { ':status': 'completed' },
268
+ * attrNames: { '#rev': 'revision' },
269
+ * });
270
+ *
271
+ * interface UpdateItemInput {
272
+ * table: string;
273
+ * key: StringIndexable;
274
+ * update: string;
275
+ * attr: StringIndexable;
276
+ * attrNames?: StringIndexable;
277
+ * }
278
+ * ```
279
+ */
280
+ export const updateItem = async (input: UpdateItemInput) => {
281
+ if (!dbClient.client) tryInit();
282
+ if (!dbClient.client) return null;
283
+ if (!input.table || !input.key || !input.update || !input.attr) return null;
123
284
 
124
285
  let cmdParams: UpdateCommandInput = {
125
- TableName: table,
126
- Key: key,
127
- UpdateExpression: update,
128
- ExpressionAttributeValues: attr,
286
+ TableName: input.table,
287
+ Key: input.key,
288
+ UpdateExpression: input.update,
289
+ ExpressionAttributeValues: input.attr,
129
290
  };
130
- if (attrNames) cmdParams = { ...cmdParams, ExpressionAttributeNames: attrNames };
291
+ if (input.attrNames) cmdParams = { ...cmdParams, ExpressionAttributeNames: input.attrNames };
131
292
  const command = new UpdateCommand(cmdParams);
132
293
 
133
294
  try {
134
- await dbClient.dbDocClient.send(command);
295
+ await dbClient.client.send(command);
135
296
  } catch (err) {
136
297
  console.error('UpdateCommandInput:', cmdParams);
137
298
  console.error(err);
@@ -142,27 +303,49 @@ export const updateItem = async (
142
303
  return true;
143
304
  };
144
305
 
145
- export const readItem = async <T = any>(
146
- table: string,
147
- key: StringIndexable,
148
- projection?: string,
149
- attrNames?: StringIndexable
150
- ) => {
151
- if (!dbClient.dbDocClient) tryInit();
152
- if (!dbClient.dbDocClient) return null;
153
- if (!table || !key) return null;
306
+ export interface ReadItemInput {
307
+ table: string;
308
+ key: StringIndexable;
309
+ projection?: string;
310
+ attrNames?: StringIndexable;
311
+ }
312
+
313
+ /**
314
+ * Read an item from DynamoDB table
315
+ * @param input input command object
316
+ * @returns item contents, null in case of error
317
+ *
318
+ * ```js
319
+ * readItem({
320
+ * table: 'lesson_list',
321
+ * key: { id: 'id_001' },
322
+ * projection: 'id, lesson, status',
323
+ * });
324
+ *
325
+ * interface ReadItemInput {
326
+ * table: string;
327
+ * key: StringIndexable;
328
+ * projection?: string;
329
+ * attrNames?: StringIndexable;
330
+ * }
331
+ * ```
332
+ */
333
+ export const readItem = async <T = any>(input: ReadItemInput) => {
334
+ if (!dbClient.client) tryInit();
335
+ if (!dbClient.client) return null;
336
+ if (!input.table || !input.key) return null;
154
337
 
155
338
  let contents: T | null = null;
156
- let cmdParams: GetCommandInput = { TableName: table, Key: key };
339
+ let cmdParams: GetCommandInput = { TableName: input.table, Key: input.key };
157
340
 
158
341
  // Ref: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html
159
- if (projection) cmdParams = { ...cmdParams, ProjectionExpression: projection };
160
- if (attrNames) cmdParams = { ...cmdParams, ExpressionAttributeNames: attrNames };
342
+ if (input.projection) cmdParams = { ...cmdParams, ProjectionExpression: input.projection };
343
+ if (input.attrNames) cmdParams = { ...cmdParams, ExpressionAttributeNames: input.attrNames };
161
344
 
162
345
  const command = new GetCommand(cmdParams);
163
346
 
164
347
  try {
165
- const results = await dbClient.dbDocClient.send(command);
348
+ const results = await dbClient.client.send(command);
166
349
  const item = results.Item;
167
350
 
168
351
  if (item) {
@@ -171,44 +354,180 @@ export const readItem = async <T = any>(
171
354
  } catch (err) {
172
355
  console.error('GetCommandInput:', cmdParams);
173
356
  console.error(err);
357
+ return null;
174
358
  // throw err;
175
359
  }
176
360
 
177
361
  return contents;
178
362
  };
179
363
 
180
- export const queryItems = async (
364
+ // Ref: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-table-read-write-batch.html
365
+ const batchReadItems = async <T = any>(
181
366
  table: string,
182
- indexName: string,
183
- cond: string,
184
- attr: StringIndexable,
185
- projection = '',
186
- desc = false
367
+ keys: StringIndexable[],
368
+ projection?: string,
369
+ attrNames?: StringIndexable
187
370
  ) => {
188
- if (!dbClient.dbDocClient) tryInit();
189
- if (!dbClient.dbDocClient) return null;
190
- if (!table || !cond || !attr) return null;
371
+ if (!dbClient.client) tryInit();
372
+ if (!dbClient.client) return null;
373
+ if (!table || !keys || !Array.isArray(keys)) return null;
374
+ if (!keys.length) return [];
375
+
376
+ let contents: StringIndexable<T>[] = [];
377
+
378
+ let reqParams: any = { Keys: keys };
379
+ if (projection) reqParams = { ...reqParams, ProjectionExpression: projection };
380
+ if (attrNames) reqParams = { ...reqParams, ExpressionAttributeNames: attrNames };
191
381
 
192
- let contents: StringIndexable[] | null = null;
382
+ const cmdParams: BatchGetCommandInput = {
383
+ RequestItems: {
384
+ [table]: reqParams,
385
+ },
386
+ };
387
+
388
+ const command = new BatchGetCommand(cmdParams);
389
+
390
+ try {
391
+ const results = await dbClient.client.send(command);
392
+ const items = results.Responses;
193
393
 
394
+ if (items && items[table]) {
395
+ contents = items[table];
396
+ }
397
+ } catch (err) {
398
+ console.error('BatchGetCommandInput:', cmdParams);
399
+ console.error(err);
400
+ return null;
401
+ // throw err;
402
+ }
403
+
404
+ return contents;
405
+ };
406
+
407
+ export interface ReadItemsAllInput {
408
+ table: string;
409
+ keys: StringIndexable[];
410
+ projection?: string;
411
+ attrNames?: StringIndexable;
412
+ }
413
+
414
+ /**
415
+ * Read a list of items from DynamoDB table
416
+ * Note: ordering of items in result may not be same as that in `keys`
417
+ * @param input input command object
418
+ * @returns list of contents for items, null in case of error
419
+ *
420
+ * ```js
421
+ * readItemsAll({
422
+ * table: 'lesson_list',
423
+ * keys: [{ id: 'id_001' }, { id: 'id_002' }],
424
+ * projection: 'id, lesson, status',
425
+ * });
426
+ *
427
+ * interface ReadItemsAllInput {
428
+ * table: string;
429
+ * keys: StringIndexable[];
430
+ * projection?: string;
431
+ * attrNames?: StringIndexable;
432
+ * }
433
+ * ```
434
+ */
435
+ export const readItemsAll = async <T = any>(input: ReadItemsAllInput) => {
436
+ if (!dbClient.client) tryInit();
437
+ if (!dbClient.client) return null;
438
+ if (!input.table || !input.keys || !Array.isArray(input.keys)) return null;
439
+ if (!input.keys.length) return [];
440
+
441
+ let contents: StringIndexable<T>[] = [];
442
+ let errFlag = false;
443
+
444
+ const batchedKeys = chunkifyArray(input.keys, BATCH_SIZE);
445
+ const chunkedKeys = chunkifyArray(batchedKeys, CHUNK_SIZE);
446
+
447
+ for (let i = 0; i < chunkedKeys.length; i += 1) {
448
+ const bchunks = chunkedKeys[i];
449
+
450
+ const brlist = bchunks.map(ikeys => batchReadItems(input.table, ikeys, input.projection, input.attrNames));
451
+ const bslist = await Promise.all(brlist); // eslint-disable-line no-await-in-loop
452
+
453
+ const icontents = bslist.flat();
454
+ const isError = icontents.find(e => e === null) === null;
455
+ if (isError) {
456
+ errFlag = true;
457
+ return null;
458
+ }
459
+ if (!errFlag) {
460
+ contents = contents.concat(icontents as StringIndexable[]);
461
+ }
462
+ }
463
+
464
+ return contents;
465
+ };
466
+
467
+ export interface QueryItemsInput {
468
+ table: string;
469
+ indexName?: string;
470
+ cond: string;
471
+ attr: StringIndexable;
472
+ attrNames?: StringIndexable;
473
+ projection?: string;
474
+ desc?: boolean;
475
+ }
476
+
477
+ /**
478
+ * Query items from a DynamoDB table based on some condition
479
+ * @param input input command object
480
+ * @returns query results array, null in case of error
481
+ *
482
+ * ```js
483
+ * dbQueryItems({
484
+ * table: 'lesson_list',
485
+ * indexName: 'status-revision-index',
486
+ * cond: 'status = :comp AND #rev >= :rev',
487
+ * attr: { ':comp': 'completed', ':rev': 9 },
488
+ * attrNames: { '#rev': 'revision' },
489
+ * projection: 'id, lesson, status, revision',
490
+ * });
491
+ *
492
+ * interface QueryItemsInput {
493
+ * table: string;
494
+ * indexName?: string;
495
+ * cond: string;
496
+ * attr: StringIndexable;
497
+ * attrNames?: StringIndexable;
498
+ * projection?: string;
499
+ * desc?: boolean;
500
+ * }
501
+ * ```
502
+ */
503
+ export const queryItems = async (input: QueryItemsInput) => {
504
+ if (!dbClient.client) tryInit();
505
+ if (!dbClient.client) return null;
506
+ if (!input.table || !input.cond || !input.attr) return null;
507
+
508
+ let contents: StringIndexable[] = [];
509
+ const desc = input.desc || false;
510
+
511
+ // Ref: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html
194
512
  // Ref: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html
195
513
  // Ref: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-query-scan.html
196
514
  // Ref: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SQLtoNoSQL.Indexes.QueryAndScan.html#SQLtoNoSQL.Indexes.QueryAndScan.DynamoDB
197
515
  let cmdParams: QueryCommandInput = {
198
- TableName: table,
199
- KeyConditionExpression: cond,
200
- ExpressionAttributeValues: attr,
516
+ TableName: input.table,
517
+ KeyConditionExpression: input.cond,
518
+ ExpressionAttributeValues: input.attr,
201
519
  // FilterExpression: "contains (category_id, :cid)",
202
520
  };
203
521
 
204
- if (indexName) cmdParams = { ...cmdParams, IndexName: indexName };
205
- if (projection) cmdParams = { ...cmdParams, ProjectionExpression: projection };
522
+ if (input.indexName) cmdParams = { ...cmdParams, IndexName: input.indexName };
523
+ if (input.attrNames) cmdParams = { ...cmdParams, ExpressionAttributeNames: input.attrNames };
524
+ if (input.projection) cmdParams = { ...cmdParams, ProjectionExpression: input.projection };
206
525
  if (desc) cmdParams = { ...cmdParams, ScanIndexForward: false };
207
526
 
208
527
  const command = new QueryCommand(cmdParams);
209
528
 
210
529
  try {
211
- const results = await dbClient.dbDocClient.send(command);
530
+ const results = await dbClient.client.send(command);
212
531
  const items = results.Items;
213
532
 
214
533
  if (items) {
@@ -217,77 +536,187 @@ export const queryItems = async (
217
536
  } catch (err) {
218
537
  console.error('QueryCommandInput:', command.input);
219
538
  console.error(err);
539
+ return null;
220
540
  // throw err;
221
541
  }
222
542
 
223
543
  return contents;
224
544
  };
225
545
 
226
- export const scanItems = async (table: string, projection = '') => {
227
- if (!dbClient.dbDocClient) tryInit();
228
- if (!dbClient.dbDocClient) return null;
229
- if (!table) return null;
546
+ export interface ScanItemsInput {
547
+ table: string;
548
+ projection?: string;
549
+ }
550
+
551
+ /**
552
+ * Scan all items in a DynamoDB table
553
+ * Note: avoid using this method in favour of `queryItems` method due to performance reasons
554
+ * @param input input command object
555
+ * @returns results of the scan query, null in case of error
556
+ *
557
+ * ```js
558
+ * scanItems({
559
+ * table: 'lesson_list',
560
+ * projection: 'id, status',
561
+ * });
562
+ *
563
+ * interface ScanItemsInput {
564
+ * table: string;
565
+ * projection?: string;
566
+ * }
567
+ * ```
568
+ */
569
+ export const scanItems = async (input: ScanItemsInput) => {
570
+ if (!dbClient.client) tryInit();
571
+ if (!dbClient.client) return null;
572
+ if (!input.table) return null;
230
573
 
231
- let contents: StringIndexable[] | null = null;
574
+ let contents: StringIndexable[] = [];
232
575
 
233
576
  // Ref: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-query-scan.html
234
577
  let cmdParams: ScanCommandInput = {
235
- TableName: table,
578
+ TableName: input.table,
236
579
  };
237
580
 
238
- if (projection) cmdParams = { ...cmdParams, ProjectionExpression: projection };
581
+ if (input.projection) cmdParams = { ...cmdParams, ProjectionExpression: input.projection };
239
582
 
240
583
  const command = new ScanCommand(cmdParams);
241
584
 
242
585
  try {
243
- const results = await dbClient.dbDocClient.send(command);
586
+ const results = await dbClient.client.send(command);
244
587
  const items = results.Items;
245
588
 
589
+ // Ref: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Scan.html#Scan.Pagination
590
+ if (results.LastEvaluatedKey) {
591
+ warn('[scanItems] Partial results obtained! Consider pagination.');
592
+ }
593
+
246
594
  if (items) {
247
595
  contents = items;
248
596
  }
249
597
  } catch (err) {
250
598
  console.error('ScanCommandInput:', cmdParams);
251
599
  console.error(err);
600
+ return null;
252
601
  // throw err;
253
602
  }
254
603
 
255
604
  return contents;
256
605
  };
257
606
 
258
- export const deleteItem = async (table: string, key: StringIndexable) => {
259
- if (!dbClient.dbDocClient) tryInit();
260
- if (!dbClient.dbDocClient) return null;
261
- if (!table || !key) return null;
607
+ export interface DeleteItemInput {
608
+ table: string;
609
+ key: StringIndexable;
610
+ }
611
+
612
+ /**
613
+ * Delete an item in a DynamoDB table
614
+ * @param input input command object
615
+ * @returns true if successful, null in case of error
616
+ *
617
+ * ```js
618
+ * deleteItem({
619
+ * table: 'lesson_list',
620
+ * key: { id: 'id_001' },
621
+ * });
622
+ *
623
+ * interface DeleteItemInput {
624
+ * table: string;
625
+ * key: StringIndexable;
626
+ * }
627
+ * ```
628
+ */
629
+ export const deleteItem = async (input: DeleteItemInput) => {
630
+ if (!dbClient.client) tryInit();
631
+ if (!dbClient.client) return null;
632
+ if (!input.table || !input.key) return null;
262
633
 
263
- const cmdParams: DeleteCommandInput = { TableName: table, Key: key };
634
+ const cmdParams: DeleteCommandInput = { TableName: input.table, Key: input.key };
264
635
  const command = new DeleteCommand(cmdParams);
265
636
 
266
637
  try {
267
- await dbClient.dbDocClient.send(command);
638
+ await dbClient.client.send(command);
268
639
  } catch (err) {
269
640
  console.error('DeleteCommandInput:', cmdParams);
270
641
  console.error(err);
642
+ return null;
271
643
  // throw err;
272
- return false;
273
644
  }
274
645
 
275
646
  return true;
276
647
  };
277
648
 
278
- // ----------------
649
+ // Ref: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-table-read-write-batch.html
650
+ // Ref: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-dynamodb/interfaces/batchwriteitemcommandinput.html#requestitems
651
+ const batchDeleteItems = async (table: string, keys: StringIndexable[]) => {
652
+ if (!dbClient.client) tryInit();
653
+ if (!dbClient.client) return null;
654
+ if (!table || !keys || !Array.isArray(keys)) return null;
655
+ if (!keys.length) return false;
656
+
657
+ const reqList = keys.map(key => ({ DeleteRequest: { Key: key } }));
658
+ const cmdParams: BatchWriteCommandInput = {
659
+ RequestItems: {
660
+ [table]: reqList,
661
+ },
662
+ };
279
663
 
280
- // await dbWriteItem('lesson_list', { id: 'id_001', title: 'My Lesson' });
664
+ const command = new BatchWriteCommand(cmdParams);
281
665
 
282
- // const contents = await dbReadItem('lesson_list', { id: 'id_001' });
283
- // console.log(contents);
666
+ try {
667
+ await dbClient.client.send(command);
668
+ } catch (err) {
669
+ console.error('BatchWriteCommandInput:', cmdParams);
670
+ console.error(err);
671
+ return null;
672
+ // throw err;
673
+ }
674
+
675
+ return true;
676
+ };
677
+
678
+ export interface DeleteItemsAllInput {
679
+ table: string;
680
+ keys: StringIndexable[];
681
+ }
682
+
683
+ /**
684
+ * Delete a list of items in a DynamoDB table
685
+ * @param input input command object
686
+ * @returns true if successful, null in case of error
687
+ *
688
+ * ```js
689
+ * deleteItemsAll({
690
+ * table: 'lesson_list',
691
+ * keys: [{ id: 'id_001' }, { id: 'id_002' }],
692
+ * });
693
+ *
694
+ * interface DeleteItemsAllInput {
695
+ * table: string;
696
+ * keys: StringIndexable[];
697
+ * }
698
+ * ```
699
+ */
700
+ export const deleteItemsAll = async (input: DeleteItemsAllInput) => {
701
+ if (!dbClient.client) tryInit();
702
+ if (!dbClient.client) return null;
703
+ if (!input.table || !input.keys || !Array.isArray(input.keys)) return null;
704
+ if (!input.keys.length) return false;
705
+
706
+ let errFlag = false;
707
+
708
+ const batchedItems = chunkifyArray(input.keys, BATCH_SIZE);
709
+ const chunkedItems = chunkifyArray(batchedItems, CHUNK_SIZE);
710
+
711
+ for (let i = 0; i < chunkedItems.length; i += 1) {
712
+ const bchunks = chunkedItems[i];
284
713
 
285
- // await dbUpdateItem(
286
- // 'lesson_list',
287
- // { id: 'id_001' },
288
- // 'set #a = :a, #b = :b',
289
- // { ':a': 'abhi@raj.me', ':b': 'Abhishek Raj' },
290
- // { '#a': 'email', '#b': 'name' }
291
- // );
714
+ const brlist = bchunks.map(ikeys => batchDeleteItems(input.table, ikeys));
715
+ const bslist = await Promise.all(brlist); // eslint-disable-line no-await-in-loop
292
716
 
293
- // ----------------
717
+ const isSuccess = bslist.every(e => e === true);
718
+ if (!isSuccess) errFlag = true;
719
+ }
720
+
721
+ return errFlag ? null : true;
722
+ };