@squiz/db-lib 1.71.2 → 1.72.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/lib/AbstractRepository.d.ts +2 -0
  3. package/lib/AbstractRepository.d.ts.map +1 -0
  4. package/lib/AbstractRepository.integration.spec.d.ts +1 -0
  5. package/lib/AbstractRepository.integration.spec.d.ts.map +1 -0
  6. package/lib/AbstractRepository.integration.spec.js +118 -0
  7. package/lib/AbstractRepository.integration.spec.js.map +1 -0
  8. package/lib/AbstractRepository.js +187 -0
  9. package/lib/AbstractRepository.js.map +1 -0
  10. package/lib/ConnectionManager.d.ts +1 -0
  11. package/lib/ConnectionManager.d.ts.map +1 -0
  12. package/lib/ConnectionManager.js +58 -0
  13. package/lib/ConnectionManager.js.map +1 -0
  14. package/lib/Migrator.d.ts +1 -0
  15. package/lib/Migrator.d.ts.map +1 -0
  16. package/lib/Migrator.js +160 -0
  17. package/lib/Migrator.js.map +1 -0
  18. package/lib/PostgresErrorCodes.d.ts +1 -0
  19. package/lib/PostgresErrorCodes.d.ts.map +1 -0
  20. package/lib/PostgresErrorCodes.js +274 -0
  21. package/lib/PostgresErrorCodes.js.map +1 -0
  22. package/lib/Repositories.d.ts +1 -0
  23. package/lib/Repositories.d.ts.map +1 -0
  24. package/lib/Repositories.js +3 -0
  25. package/lib/Repositories.js.map +1 -0
  26. package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +50 -9
  27. package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -0
  28. package/lib/dynamodb/AbstractDynamoDbRepository.js +456 -0
  29. package/lib/dynamodb/AbstractDynamoDbRepository.js.map +1 -0
  30. package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts +1 -0
  31. package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts.map +1 -0
  32. package/lib/dynamodb/AbstractDynamoDbRepository.spec.js +924 -0
  33. package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -0
  34. package/lib/dynamodb/DynamoDbManager.d.ts +1 -0
  35. package/lib/dynamodb/DynamoDbManager.d.ts.map +1 -0
  36. package/lib/dynamodb/DynamoDbManager.js +66 -0
  37. package/lib/dynamodb/DynamoDbManager.js.map +1 -0
  38. package/lib/dynamodb/getDynamoDbOptions.d.ts +1 -0
  39. package/lib/dynamodb/getDynamoDbOptions.d.ts.map +1 -0
  40. package/lib/dynamodb/getDynamoDbOptions.js +15 -0
  41. package/lib/dynamodb/getDynamoDbOptions.js.map +1 -0
  42. package/lib/error/DuplicateItemError.d.ts +1 -0
  43. package/lib/error/DuplicateItemError.d.ts.map +1 -0
  44. package/lib/error/DuplicateItemError.js +12 -0
  45. package/lib/error/DuplicateItemError.js.map +1 -0
  46. package/lib/error/InvalidDataFormatError.d.ts +1 -0
  47. package/lib/error/InvalidDataFormatError.d.ts.map +1 -0
  48. package/lib/error/InvalidDataFormatError.js +12 -0
  49. package/lib/error/InvalidDataFormatError.js.map +1 -0
  50. package/lib/error/InvalidDbSchemaError.d.ts +1 -0
  51. package/lib/error/InvalidDbSchemaError.d.ts.map +1 -0
  52. package/lib/error/InvalidDbSchemaError.js +12 -0
  53. package/lib/error/InvalidDbSchemaError.js.map +1 -0
  54. package/lib/error/MissingKeyValuesError.d.ts +1 -0
  55. package/lib/error/MissingKeyValuesError.d.ts.map +1 -0
  56. package/lib/error/MissingKeyValuesError.js +12 -0
  57. package/lib/error/MissingKeyValuesError.js.map +1 -0
  58. package/lib/error/TransactionError.d.ts +1 -0
  59. package/lib/error/TransactionError.d.ts.map +1 -0
  60. package/lib/error/TransactionError.js +12 -0
  61. package/lib/error/TransactionError.js.map +1 -0
  62. package/lib/getConnectionInfo.d.ts +1 -0
  63. package/lib/getConnectionInfo.d.ts.map +1 -0
  64. package/lib/getConnectionInfo.js +30 -0
  65. package/lib/getConnectionInfo.js.map +1 -0
  66. package/lib/index.d.ts +1 -0
  67. package/lib/index.d.ts.map +1 -0
  68. package/lib/index.js +33 -70416
  69. package/lib/index.js.map +1 -7
  70. package/package.json +5 -5
  71. package/src/AbstractRepository.ts +26 -20
  72. package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +289 -37
  73. package/src/dynamodb/AbstractDynamoDbRepository.ts +140 -31
  74. package/src/dynamodb/getDynamoDbOptions.ts +1 -1
  75. package/tsconfig.json +5 -2
  76. package/tsconfig.tsbuildinfo +1 -1
  77. package/build.js +0 -31
@@ -0,0 +1,924 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const AbstractDynamoDbRepository_1 = require("./AbstractDynamoDbRepository");
7
+ const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
8
+ const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
9
+ const DynamoDbManager_1 = require("./DynamoDbManager");
10
+ const __1 = require("..");
11
+ const aws_sdk_client_mock_1 = require("aws-sdk-client-mock");
12
+ require("aws-sdk-client-mock-jest");
13
+ const client_dynamodb_2 = require("@aws-sdk/client-dynamodb");
14
+ const crypto_1 = __importDefault(require("crypto"));
15
+ const InvalidDataFormatError_1 = require("../error/InvalidDataFormatError");
16
+ const ddbClientMock = (0, aws_sdk_client_mock_1.mockClient)(lib_dynamodb_1.DynamoDBDocumentClient);
17
+ const ddbDoc = lib_dynamodb_1.DynamoDBDocument.from(new client_dynamodb_2.DynamoDB({}));
18
+ class TestItem {
19
+ constructor(data = {}) {
20
+ var _a, _b, _c, _d, _e;
21
+ this.name = (_a = data.name) !== null && _a !== void 0 ? _a : 'default name';
22
+ this.age = (_b = data.age) !== null && _b !== void 0 ? _b : 0;
23
+ this.country = (_c = data.country) !== null && _c !== void 0 ? _c : 'default country';
24
+ this.data = (_d = data.data) !== null && _d !== void 0 ? _d : {};
25
+ this.data2 = (_e = data.data2) !== null && _e !== void 0 ? _e : {};
26
+ if (typeof this.name !== 'string') {
27
+ throw Error('Invalid "name"');
28
+ }
29
+ if (typeof this.age !== 'number') {
30
+ throw Error('Invalid "age"');
31
+ }
32
+ if (typeof this.country !== 'string') {
33
+ throw Error('Invalid "country"');
34
+ }
35
+ if (typeof this.data !== 'object' || Array.isArray(this.data)) {
36
+ throw Error('Invalid "data"');
37
+ }
38
+ }
39
+ }
40
+ const TABLE_NAME = 'test-table';
41
+ const TEST_ITEM_ENTITY_NAME = 'test-item-entity';
42
+ const TEST_ITEM_ENTITY_DEFINITION = {
43
+ keys: {
44
+ pk: {
45
+ format: 'test_item#{name}',
46
+ attributeName: 'pk',
47
+ },
48
+ sk: {
49
+ format: '#meta',
50
+ attributeName: 'sk',
51
+ },
52
+ },
53
+ indexes: {
54
+ 'gsi1_pk-gsi1_sk-index': {
55
+ pk: {
56
+ format: 'country#{country}',
57
+ attributeName: 'gsi1_pk',
58
+ },
59
+ sk: {
60
+ format: 'age#{age}',
61
+ attributeName: 'gsi1_sk',
62
+ },
63
+ },
64
+ },
65
+ // field to be stored as JSON string
66
+ fieldsAsJsonString: ['data2'],
67
+ };
68
+ class TestItemRepository extends AbstractDynamoDbRepository_1.AbstractDynamoDbRepository {
69
+ constructor(tableName, dbManager) {
70
+ super(tableName, dbManager, TEST_ITEM_ENTITY_NAME, TEST_ITEM_ENTITY_DEFINITION, TestItem);
71
+ }
72
+ }
73
+ const ddbManager = new DynamoDbManager_1.DynamoDbManager(ddbDoc, (dbManager) => {
74
+ return {
75
+ testItem: new TestItemRepository(TABLE_NAME, dbManager),
76
+ };
77
+ });
78
+ // Test start ////////////////////////////////////
79
+ describe('AbstractRepository', () => {
80
+ let repository;
81
+ beforeEach(() => {
82
+ ddbClientMock.reset();
83
+ repository = new TestItemRepository(TABLE_NAME, ddbManager);
84
+ });
85
+ describe('createItem()', () => {
86
+ it('should create and return the item object if valid input', async () => {
87
+ ddbClientMock.on(lib_dynamodb_1.PutCommand).resolves({
88
+ $metadata: {
89
+ httpStatusCode: 200,
90
+ },
91
+ });
92
+ const input = {
93
+ TableName: TABLE_NAME,
94
+ Item: {
95
+ pk: 'test_item#foo',
96
+ sk: '#meta',
97
+ gsi1_pk: 'country#au',
98
+ gsi1_sk: 'age#99',
99
+ name: 'foo',
100
+ age: 99,
101
+ country: 'au',
102
+ data: {},
103
+ // "data2" property is defined to be stored as JSON string
104
+ data2: '{"foo":"bar","num":123}',
105
+ },
106
+ ConditionExpression: `attribute_not_exists(pk)`,
107
+ };
108
+ const item = {
109
+ name: 'foo',
110
+ age: 99,
111
+ country: 'au',
112
+ data: {},
113
+ data2: {
114
+ foo: 'bar',
115
+ num: 123,
116
+ },
117
+ };
118
+ const result = await repository.createItem(item);
119
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.PutCommand, input);
120
+ expect(result).toEqual(new TestItem({
121
+ name: 'foo',
122
+ age: 99,
123
+ country: 'au',
124
+ data: {},
125
+ data2: {
126
+ foo: 'bar',
127
+ num: 123,
128
+ },
129
+ }));
130
+ });
131
+ it('should throw error if invalid input', async () => {
132
+ const item = {
133
+ name: 'foo',
134
+ age: 99,
135
+ country: 'au',
136
+ data: [], // should be non-array object
137
+ };
138
+ await expect(repository.createItem(item)).rejects.toEqual(new Error('Invalid "data"'));
139
+ });
140
+ it('should throw error if excess column in input', async () => {
141
+ const item = {
142
+ name: 'foo',
143
+ age: 99,
144
+ country: 'au',
145
+ data: {},
146
+ extraColumn: '123',
147
+ extraColumn2: '',
148
+ };
149
+ await expect(repository.createItem(item)).rejects.toEqual(new __1.InvalidDbSchemaError('Excess properties in entity test-item-entity: extraColumn, extraColumn2'));
150
+ });
151
+ it('should throw error if input does not includes key field(s)', async () => {
152
+ const partialItem = {
153
+ age: 99,
154
+ country: 'au',
155
+ data: {},
156
+ };
157
+ await expect(repository.createItem(partialItem)).rejects.toEqual(new __1.MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'));
158
+ });
159
+ });
160
+ describe('updateItem()', () => {
161
+ it('should update and return the item object if valid input', async () => {
162
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
163
+ $metadata: {
164
+ httpStatusCode: 200,
165
+ },
166
+ Item: {
167
+ name: 'foo',
168
+ age: 99,
169
+ country: 'au',
170
+ data: {},
171
+ },
172
+ });
173
+ ddbClientMock.on(lib_dynamodb_1.UpdateCommand).resolves({
174
+ $metadata: {
175
+ httpStatusCode: 200,
176
+ },
177
+ Attributes: {
178
+ name: 'foo',
179
+ age: 99,
180
+ country: 'au-updated',
181
+ data: {},
182
+ },
183
+ });
184
+ const input = {
185
+ TableName: TABLE_NAME,
186
+ Key: { pk: 'test_item#foo', sk: '#meta' },
187
+ UpdateExpression: 'SET #country = :country',
188
+ ExpressionAttributeNames: {
189
+ '#country': 'country',
190
+ },
191
+ ExpressionAttributeValues: {
192
+ ':country': 'au-updated',
193
+ },
194
+ ConditionExpression: `attribute_exists(pk)`,
195
+ };
196
+ const updateItem = {
197
+ name: 'foo',
198
+ country: 'au-updated',
199
+ };
200
+ const result = await repository.updateItem(updateItem);
201
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, lib_dynamodb_1.UpdateCommand, input);
202
+ expect(result).toEqual(new TestItem({
203
+ name: 'foo',
204
+ age: 99,
205
+ country: 'au-updated',
206
+ data: {},
207
+ }));
208
+ });
209
+ it('should not trigger update request if the input attributes are same as in the existing item', async () => {
210
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
211
+ $metadata: {
212
+ httpStatusCode: 200,
213
+ },
214
+ Item: {
215
+ name: 'foo',
216
+ age: 99,
217
+ country: 'au',
218
+ data: {},
219
+ },
220
+ });
221
+ ddbClientMock.on(lib_dynamodb_1.UpdateCommand).rejects(new Error('updateItem() called when not expected'));
222
+ // update input attributes are same as in the existing item
223
+ const updateItem = {
224
+ name: 'foo',
225
+ country: 'au',
226
+ };
227
+ const result = await repository.updateItem(updateItem);
228
+ expect(result).toEqual(new TestItem({
229
+ name: 'foo',
230
+ age: 99,
231
+ country: 'au',
232
+ data: {},
233
+ }));
234
+ });
235
+ it('should return undefined if item does does not exist', async () => {
236
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
237
+ $metadata: {
238
+ httpStatusCode: 200,
239
+ },
240
+ });
241
+ const updateItem = {
242
+ name: 'foo',
243
+ country: 'au-updated',
244
+ };
245
+ const result = await repository.updateItem(updateItem);
246
+ expect(result).toEqual(undefined);
247
+ });
248
+ it('should return undefined if update cmd conditional check fails', async () => {
249
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
250
+ $metadata: {
251
+ httpStatusCode: 200,
252
+ },
253
+ Item: {
254
+ name: 'foo',
255
+ age: 99,
256
+ country: 'au',
257
+ data: {},
258
+ },
259
+ });
260
+ ddbClientMock.on(lib_dynamodb_1.UpdateCommand).rejects(new client_dynamodb_1.ConditionalCheckFailedException({
261
+ $metadata: {},
262
+ message: 'not found',
263
+ }));
264
+ const updateItem = {
265
+ name: 'foo',
266
+ country: 'au-updated',
267
+ };
268
+ const result = await repository.updateItem(updateItem);
269
+ expect(result).toEqual(undefined);
270
+ });
271
+ it('should throw error update data has invalid data', async () => {
272
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
273
+ $metadata: {
274
+ httpStatusCode: 200,
275
+ },
276
+ Item: {
277
+ name: 'foo',
278
+ age: 99,
279
+ country: 'au',
280
+ data: {},
281
+ },
282
+ });
283
+ const updateItem = {
284
+ name: 'foo',
285
+ country: 61, // should be "string" type
286
+ };
287
+ await expect(repository.updateItem(updateItem)).rejects.toEqual(new Error('Invalid "country"'));
288
+ });
289
+ it('should throw error if excess column in input', async () => {
290
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
291
+ $metadata: {
292
+ httpStatusCode: 200,
293
+ },
294
+ Item: {
295
+ name: 'foo',
296
+ age: 99,
297
+ country: 'au',
298
+ data: {},
299
+ },
300
+ });
301
+ const updateItem = {
302
+ name: 'foo',
303
+ country: 'au-updated',
304
+ extra: '',
305
+ };
306
+ await expect(repository.updateItem(updateItem)).rejects.toEqual(new __1.InvalidDbSchemaError('Excess properties in entity test-item-entity: extra'));
307
+ });
308
+ it('should throw error if input does not includes key field(s)', async () => {
309
+ const updateItem = {
310
+ age: 99,
311
+ country: 'au-updated', // should be "string" type
312
+ };
313
+ await expect(repository.updateItem(updateItem)).rejects.toEqual(new __1.MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'));
314
+ });
315
+ });
316
+ describe('getItem()', () => {
317
+ it('should return the item object if found', async () => {
318
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
319
+ $metadata: {
320
+ httpStatusCode: 200,
321
+ },
322
+ Item: {
323
+ name: 'foo',
324
+ age: 99,
325
+ country: 'au',
326
+ data: {},
327
+ data2: '{"foo":"bar","num":123}',
328
+ },
329
+ });
330
+ const input = {
331
+ TableName: TABLE_NAME,
332
+ Key: { pk: 'test_item#foo', sk: '#meta' },
333
+ };
334
+ const partialItem = { name: 'foo' };
335
+ const result = await repository.getItem(partialItem);
336
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.GetCommand, input);
337
+ expect(result).toEqual(new TestItem({
338
+ name: 'foo',
339
+ age: 99,
340
+ country: 'au',
341
+ data: {},
342
+ data2: {
343
+ foo: 'bar',
344
+ num: 123,
345
+ },
346
+ }));
347
+ });
348
+ it('should return undefined if item not found', async () => {
349
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
350
+ $metadata: {
351
+ httpStatusCode: 200,
352
+ },
353
+ });
354
+ const input = {
355
+ TableName: TABLE_NAME,
356
+ Key: { pk: 'test_item#foo', sk: '#meta' },
357
+ };
358
+ const partialItem = { name: 'foo' };
359
+ const result = await repository.getItem(partialItem);
360
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.GetCommand, input);
361
+ expect(result).toEqual(undefined);
362
+ });
363
+ it('should throw error if item schema validation fails', async () => {
364
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
365
+ $metadata: {
366
+ httpStatusCode: 200,
367
+ },
368
+ Item: {
369
+ name: 'foo',
370
+ age: '99',
371
+ country: 'au',
372
+ data: {},
373
+ },
374
+ });
375
+ const partialItem = { name: 'foo' };
376
+ await expect(repository.getItem(partialItem)).rejects.toEqual(new Error('Invalid "age"'));
377
+ });
378
+ it('should throw error if input does not includes key field(s)', async () => {
379
+ const partialItem = { age: 99 };
380
+ await expect(repository.getItem(partialItem)).rejects.toEqual(new __1.MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'));
381
+ });
382
+ it('should throw error if JSON string field has non-string data', async () => {
383
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
384
+ $metadata: {
385
+ httpStatusCode: 200,
386
+ },
387
+ Item: {
388
+ name: 'foo',
389
+ age: 99,
390
+ country: 'au',
391
+ data: {},
392
+ data2: {
393
+ foo: 'bar',
394
+ num: 123,
395
+ },
396
+ },
397
+ });
398
+ const partialItem = { name: 'foo' };
399
+ await expect(repository.getItem(partialItem)).rejects.toEqual(new InvalidDataFormatError_1.InvalidDataFormatError(`Field 'data2' defined as JSON String has a non-string data`));
400
+ });
401
+ });
402
+ describe('getItems()', () => {
403
+ it('should use BatchGetItem to get result', async () => {
404
+ ddbClientMock.on(lib_dynamodb_1.BatchGetCommand).resolves({
405
+ $metadata: {
406
+ httpStatusCode: 200,
407
+ },
408
+ Responses: {
409
+ [TABLE_NAME]: [
410
+ {
411
+ name: 'foo',
412
+ age: 99,
413
+ country: 'au',
414
+ data: {},
415
+ data2: '{"foo":"bar","num":123}',
416
+ },
417
+ {
418
+ name: 'foo2',
419
+ age: 999,
420
+ country: 'au',
421
+ data: {},
422
+ data2: '{"foo":"bar","num":123}',
423
+ },
424
+ ],
425
+ },
426
+ });
427
+ const input = {
428
+ RequestItems: {
429
+ [TABLE_NAME]: {
430
+ Keys: [
431
+ { pk: 'test_item#foo', sk: '#meta' },
432
+ { pk: 'test_item#foo2', sk: '#meta' },
433
+ ],
434
+ },
435
+ },
436
+ };
437
+ const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
438
+ const result = await repository.getItems(requestItems);
439
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.BatchGetCommand, input);
440
+ expect(result).toEqual([
441
+ new TestItem({
442
+ name: 'foo',
443
+ age: 99,
444
+ country: 'au',
445
+ data: {},
446
+ data2: {
447
+ foo: 'bar',
448
+ num: 123,
449
+ },
450
+ }),
451
+ new TestItem({
452
+ name: 'foo2',
453
+ age: 999,
454
+ country: 'au',
455
+ data: {},
456
+ data2: {
457
+ foo: 'bar',
458
+ num: 123,
459
+ },
460
+ }),
461
+ ]);
462
+ });
463
+ it('should request BatchGetItem in batch of 100 items to get result', async () => {
464
+ ddbClientMock.on(lib_dynamodb_1.BatchGetCommand).resolves({
465
+ $metadata: {
466
+ httpStatusCode: 200,
467
+ },
468
+ });
469
+ const requestItems = [];
470
+ for (let i = 0; i < 120; i++) {
471
+ requestItems.push({ name: `foo${i}` });
472
+ }
473
+ // keys for first batch request
474
+ const keys1 = [];
475
+ for (let i = 0; i < 100; i++) {
476
+ keys1.push({ pk: `test_item#foo${i}`, sk: '#meta' });
477
+ }
478
+ // keys for second batch request
479
+ const keys2 = [];
480
+ for (let i = 100; i < 120; i++) {
481
+ keys2.push({ pk: `test_item#foo${i}`, sk: '#meta' });
482
+ }
483
+ const input1 = {
484
+ RequestItems: {
485
+ [TABLE_NAME]: {
486
+ Keys: keys1,
487
+ },
488
+ },
489
+ };
490
+ const input2 = {
491
+ RequestItems: {
492
+ [TABLE_NAME]: {
493
+ Keys: keys2,
494
+ },
495
+ },
496
+ };
497
+ await repository.getItems(requestItems);
498
+ expect(ddbClientMock).toHaveReceivedCommandTimes(lib_dynamodb_1.BatchGetCommand, 2);
499
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(1, lib_dynamodb_1.BatchGetCommand, input1);
500
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, lib_dynamodb_1.BatchGetCommand, input2);
501
+ });
502
+ it('should throw error if any input item does not includes key field(s)', async () => {
503
+ const requestItems = [{ name: 'foo' }, { age: 22 }];
504
+ await expect(repository.getItems(requestItems)).rejects.toEqual(new __1.MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'));
505
+ });
506
+ });
507
+ describe('queryItems()', () => {
508
+ it('should return the items if found', async () => {
509
+ ddbClientMock.on(lib_dynamodb_1.QueryCommand).resolves({
510
+ $metadata: {
511
+ httpStatusCode: 200,
512
+ },
513
+ Items: [
514
+ {
515
+ name: 'foo',
516
+ age: 99,
517
+ country: 'au',
518
+ data: {},
519
+ },
520
+ ],
521
+ });
522
+ const input = {
523
+ TableName: TABLE_NAME,
524
+ KeyConditionExpression: '#pkName = :pkValue',
525
+ ExpressionAttributeNames: {
526
+ '#pkName': 'pk',
527
+ },
528
+ ExpressionAttributeValues: {
529
+ ':pkValue': 'test_item#foo',
530
+ },
531
+ };
532
+ const partialItem = { name: 'foo' };
533
+ const result = await repository.queryItems(partialItem);
534
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.QueryCommand, input);
535
+ expect(result).toEqual([
536
+ new TestItem({
537
+ name: 'foo',
538
+ age: 99,
539
+ country: 'au',
540
+ data: {},
541
+ }),
542
+ ]);
543
+ });
544
+ it('should return empty array if no items found', async () => {
545
+ ddbClientMock.on(lib_dynamodb_1.QueryCommand).resolves({
546
+ $metadata: {
547
+ httpStatusCode: 200,
548
+ },
549
+ });
550
+ const input = {
551
+ TableName: TABLE_NAME,
552
+ KeyConditionExpression: '#pkName = :pkValue',
553
+ ExpressionAttributeNames: {
554
+ '#pkName': 'pk',
555
+ },
556
+ ExpressionAttributeValues: {
557
+ ':pkValue': 'test_item#foo',
558
+ },
559
+ };
560
+ const partialItem = { name: 'foo' };
561
+ const result = await repository.queryItems(partialItem);
562
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.QueryCommand, input);
563
+ expect(result).toEqual([]);
564
+ });
565
+ it('should use sort key in query if "useSortKey" param is true', async () => {
566
+ ddbClientMock.on(lib_dynamodb_1.QueryCommand).resolves({
567
+ $metadata: {
568
+ httpStatusCode: 200,
569
+ },
570
+ });
571
+ const input = {
572
+ TableName: TABLE_NAME,
573
+ KeyConditionExpression: '#pkName = :pkValue AND #skName = :skValue',
574
+ ExpressionAttributeNames: {
575
+ '#pkName': 'pk',
576
+ '#skName': 'sk',
577
+ },
578
+ ExpressionAttributeValues: {
579
+ ':pkValue': 'test_item#foo',
580
+ ':skValue': '#meta',
581
+ },
582
+ };
583
+ const partialItem = { name: 'foo' };
584
+ const _result = await repository.queryItems(partialItem, { useSortKey: true });
585
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.QueryCommand, input);
586
+ });
587
+ it('should return the items if found when using gsi index', async () => {
588
+ ddbClientMock.on(lib_dynamodb_1.QueryCommand).resolves({
589
+ $metadata: {
590
+ httpStatusCode: 200,
591
+ },
592
+ Items: [
593
+ {
594
+ name: 'foo',
595
+ age: 99,
596
+ country: 'au',
597
+ data: {},
598
+ },
599
+ {
600
+ name: 'fox',
601
+ age: 11,
602
+ country: 'au',
603
+ data: {},
604
+ },
605
+ ],
606
+ });
607
+ const index = 'gsi1_pk-gsi1_sk-index';
608
+ const input = {
609
+ TableName: TABLE_NAME,
610
+ IndexName: index,
611
+ KeyConditionExpression: '#pkName = :pkValue',
612
+ ExpressionAttributeNames: {
613
+ '#pkName': 'gsi1_pk',
614
+ },
615
+ ExpressionAttributeValues: {
616
+ ':pkValue': 'country#au',
617
+ },
618
+ };
619
+ const partialItem = { country: 'au' };
620
+ const useSortKey = false;
621
+ const result = await repository.queryItems(partialItem, { useSortKey, index });
622
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.QueryCommand, input);
623
+ expect(result).toEqual([
624
+ new TestItem({
625
+ name: 'foo',
626
+ age: 99,
627
+ country: 'au',
628
+ data: {},
629
+ }),
630
+ new TestItem({
631
+ name: 'fox',
632
+ age: 11,
633
+ country: 'au',
634
+ data: {},
635
+ }),
636
+ ]);
637
+ });
638
+ it('should use sort key in query if "useSortKey" param is true when using gsi index', async () => {
639
+ ddbClientMock.on(lib_dynamodb_1.QueryCommand).resolves({
640
+ $metadata: {
641
+ httpStatusCode: 200,
642
+ },
643
+ });
644
+ const index = 'gsi1_pk-gsi1_sk-index';
645
+ const input = {
646
+ TableName: TABLE_NAME,
647
+ IndexName: index,
648
+ KeyConditionExpression: '#pkName = :pkValue AND #skName = :skValue',
649
+ ExpressionAttributeNames: {
650
+ '#pkName': 'gsi1_pk',
651
+ '#skName': 'gsi1_sk',
652
+ },
653
+ ExpressionAttributeValues: {
654
+ ':pkValue': 'country#au',
655
+ ':skValue': 'age#99',
656
+ },
657
+ };
658
+ const partialItem = { age: 99, country: 'au' };
659
+ const useSortKey = true;
660
+ const _result = await repository.queryItems(partialItem, { useSortKey, index });
661
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.QueryCommand, input);
662
+ });
663
+ it('should set input query correctly when "filter - begins_with" query option is set', async () => {
664
+ ddbClientMock.on(lib_dynamodb_1.QueryCommand).resolves({
665
+ $metadata: {
666
+ httpStatusCode: 200,
667
+ },
668
+ });
669
+ const input = {
670
+ TableName: TABLE_NAME,
671
+ KeyConditionExpression: '#pkName = :pkValue AND begins_with(#skName, :skValue)',
672
+ ExpressionAttributeNames: {
673
+ '#pkName': 'pk',
674
+ '#skName': 'sk',
675
+ },
676
+ ExpressionAttributeValues: {
677
+ ':pkValue': 'test_item#foo',
678
+ ':skValue': 'keyword-x',
679
+ },
680
+ };
681
+ const partialItem = { name: 'foo' };
682
+ const queryOptions = {
683
+ filter: {
684
+ type: 'begins_with',
685
+ keyword: 'keyword-x',
686
+ },
687
+ };
688
+ await repository.queryItems(partialItem, queryOptions);
689
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.QueryCommand, input);
690
+ });
691
+ it('should throw error invalid "filter" query option is set', async () => {
692
+ ddbClientMock.on(lib_dynamodb_1.QueryCommand).resolves({
693
+ $metadata: {
694
+ httpStatusCode: 200,
695
+ },
696
+ });
697
+ const partialItem = { name: 'foo' };
698
+ const queryOptions = {
699
+ filter: {
700
+ type: 'invalid-type',
701
+ keyword: 'keyword-x',
702
+ },
703
+ };
704
+ await expect(repository.queryItems(partialItem, queryOptions)).rejects.toEqual(new Error(`Invalid query filter type: invalid-type`));
705
+ });
706
+ it('should set input query correctly when "limit" query option is set', async () => {
707
+ ddbClientMock.on(lib_dynamodb_1.QueryCommand).resolves({
708
+ $metadata: {
709
+ httpStatusCode: 200,
710
+ },
711
+ });
712
+ const input = {
713
+ TableName: TABLE_NAME,
714
+ KeyConditionExpression: '#pkName = :pkValue',
715
+ ExpressionAttributeNames: {
716
+ '#pkName': 'pk',
717
+ },
718
+ ExpressionAttributeValues: {
719
+ ':pkValue': 'test_item#foo',
720
+ },
721
+ Limit: 33,
722
+ };
723
+ const partialItem = { name: 'foo' };
724
+ const queryOptions = { limit: 33 };
725
+ await repository.queryItems(partialItem, queryOptions);
726
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.QueryCommand, input);
727
+ });
728
+ it('should set input query correctly when "order asc" query option is set', async () => {
729
+ ddbClientMock.on(lib_dynamodb_1.QueryCommand).resolves({
730
+ $metadata: {
731
+ httpStatusCode: 200,
732
+ },
733
+ });
734
+ const input = {
735
+ TableName: TABLE_NAME,
736
+ KeyConditionExpression: '#pkName = :pkValue',
737
+ ExpressionAttributeNames: {
738
+ '#pkName': 'pk',
739
+ },
740
+ ExpressionAttributeValues: {
741
+ ':pkValue': 'test_item#foo',
742
+ },
743
+ ScanIndexForward: true,
744
+ };
745
+ const partialItem = { name: 'foo' };
746
+ const queryOptions = { order: 'asc' };
747
+ await repository.queryItems(partialItem, queryOptions);
748
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.QueryCommand, input);
749
+ });
750
+ it('should set input query correctly when "order desc" query option is set', async () => {
751
+ ddbClientMock.on(lib_dynamodb_1.QueryCommand).resolves({
752
+ $metadata: {
753
+ httpStatusCode: 200,
754
+ },
755
+ });
756
+ const input = {
757
+ TableName: TABLE_NAME,
758
+ KeyConditionExpression: '#pkName = :pkValue',
759
+ ExpressionAttributeNames: {
760
+ '#pkName': 'pk',
761
+ },
762
+ ExpressionAttributeValues: {
763
+ ':pkValue': 'test_item#foo',
764
+ },
765
+ ScanIndexForward: false,
766
+ };
767
+ const partialItem = { name: 'foo' };
768
+ const queryOptions = { order: 'desc' };
769
+ await repository.queryItems(partialItem, queryOptions);
770
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.QueryCommand, input);
771
+ });
772
+ it('should throw error for invalid index query', async () => {
773
+ const index = 'undefined-index';
774
+ const partialItem = { age: 99, country: 'au' };
775
+ await expect(repository.queryItems(partialItem, { index })).rejects.toEqual(new __1.MissingKeyValuesError(`Table index '${index}' not defined on entity test-item-entity`));
776
+ });
777
+ it('should throw error for missing key fields', async () => {
778
+ const partialItem = { age: 99, country: 'au' };
779
+ await expect(repository.queryItems(partialItem)).rejects.toEqual(new __1.MissingKeyValuesError(`Key field "name" must be specified in the input item in entity test-item-entity`));
780
+ });
781
+ it('should throw error for missing key fields when using index', async () => {
782
+ const partialItem = { name: 'foo' };
783
+ const useSortKey = false;
784
+ const index = 'gsi1_pk-gsi1_sk-index';
785
+ await expect(repository.queryItems(partialItem, { useSortKey, index })).rejects.toEqual(new __1.MissingKeyValuesError(`Key field "country" must be specified in the input item in entity test-item-entity`));
786
+ });
787
+ it('should throw error for missing key fields with "useSortKey" param true when using index', async () => {
788
+ const partialItem = { country: 'au' };
789
+ const useSortKey = true;
790
+ const index = 'gsi1_pk-gsi1_sk-index';
791
+ await expect(repository.queryItems(partialItem, { useSortKey, index })).rejects.toEqual(new __1.MissingKeyValuesError(`Key field "age" must be specified in the input item in entity test-item-entity`));
792
+ });
793
+ });
794
+ describe('deleteItem()', () => {
795
+ it('should return 1 when item is found and deleted', async () => {
796
+ ddbClientMock.on(lib_dynamodb_1.DeleteCommand).resolves({
797
+ $metadata: {
798
+ httpStatusCode: 200,
799
+ },
800
+ });
801
+ const input = {
802
+ TableName: TABLE_NAME,
803
+ Key: { pk: 'test_item#foo', sk: '#meta' },
804
+ };
805
+ const partialItem = { name: 'foo' };
806
+ const result = await repository.deleteItem(partialItem);
807
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.DeleteCommand, input);
808
+ expect(result).toBe(1);
809
+ });
810
+ it('should return 0 when item is not found', async () => {
811
+ ddbClientMock.on(lib_dynamodb_1.DeleteCommand).rejects(new client_dynamodb_1.ConditionalCheckFailedException({
812
+ $metadata: {},
813
+ message: 'not found',
814
+ }));
815
+ const input = {
816
+ TableName: TABLE_NAME,
817
+ Key: { pk: 'test_item#foo', sk: '#meta' },
818
+ };
819
+ const partialItem = { name: 'foo' };
820
+ const result = await repository.deleteItem(partialItem);
821
+ expect(ddbClientMock).toHaveReceivedCommandWith(lib_dynamodb_1.DeleteCommand, input);
822
+ expect(result).toBe(0);
823
+ });
824
+ it('should throw error if request fails', async () => {
825
+ const partialItem = { name: 'foo' };
826
+ ddbClientMock.on(lib_dynamodb_1.DeleteCommand).rejects('some other error');
827
+ await expect(repository.deleteItem(partialItem)).rejects.toEqual(new Error('some other error'));
828
+ });
829
+ it('should throw error if input does not includes key field(s)', async () => {
830
+ const partialItem = { age: 99 };
831
+ await expect(repository.deleteItem(partialItem)).rejects.toEqual(new __1.MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'));
832
+ });
833
+ });
834
+ describe('Writer fns with transaction - DynamoDbManager', () => {
835
+ it('should execute the multiple transaction write request in a single request', async () => {
836
+ const spy = jest.spyOn(crypto_1.default, 'randomUUID');
837
+ spy.mockImplementation(() => 'some-token');
838
+ ddbClientMock.on(lib_dynamodb_1.GetCommand).resolves({
839
+ $metadata: {
840
+ httpStatusCode: 200,
841
+ },
842
+ Item: {
843
+ name: 'foo2',
844
+ age: 99,
845
+ country: 'au',
846
+ data: {},
847
+ },
848
+ });
849
+ ddbClientMock.on(lib_dynamodb_1.TransactWriteCommand).resolves({
850
+ $metadata: {
851
+ httpStatusCode: 200,
852
+ },
853
+ });
854
+ const result = await ddbManager.executeInTransaction(async (transaction) => {
855
+ await repository.deleteItem({ name: 'foo' }, transaction);
856
+ await repository.updateItem({
857
+ name: 'foo2',
858
+ age: 55,
859
+ }, transaction);
860
+ return await repository.createItem({
861
+ name: 'foo3',
862
+ age: 11,
863
+ country: 'au',
864
+ data: {},
865
+ }, transaction);
866
+ });
867
+ const input = {
868
+ ClientRequestToken: 'some-token',
869
+ TransactItems: [
870
+ {
871
+ Delete: {
872
+ Key: {
873
+ pk: 'test_item#foo',
874
+ sk: '#meta',
875
+ },
876
+ TableName: 'test-table',
877
+ },
878
+ },
879
+ {
880
+ Update: {
881
+ ConditionExpression: 'attribute_exists(pk)',
882
+ ExpressionAttributeNames: {
883
+ '#age': 'age',
884
+ },
885
+ ExpressionAttributeValues: {
886
+ ':age': 55,
887
+ },
888
+ Key: {
889
+ pk: 'test_item#foo2',
890
+ sk: '#meta',
891
+ },
892
+ TableName: 'test-table',
893
+ UpdateExpression: 'SET #age = :age',
894
+ },
895
+ },
896
+ {
897
+ Put: {
898
+ ConditionExpression: 'attribute_not_exists(pk)',
899
+ Item: {
900
+ age: 11,
901
+ country: 'au',
902
+ data: {},
903
+ gsi1_pk: 'country#au',
904
+ gsi1_sk: 'age#11',
905
+ name: 'foo3',
906
+ pk: 'test_item#foo3',
907
+ sk: '#meta',
908
+ },
909
+ TableName: 'test-table',
910
+ },
911
+ },
912
+ ],
913
+ };
914
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, lib_dynamodb_1.TransactWriteCommand, input);
915
+ expect(result).toEqual({
916
+ name: 'foo3',
917
+ age: 11,
918
+ country: 'au',
919
+ data: {},
920
+ });
921
+ });
922
+ });
923
+ });
924
+ //# sourceMappingURL=AbstractDynamoDbRepository.spec.js.map