@squiz/db-lib 1.71.2 → 1.72.0

Sign up to get free protection for your applications and to get access to all the features.
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