@squiz/db-lib 1.65.0 → 1.66.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,804 @@
1
+ import { AbstractDynamoDbRepository } from './AbstractDynamoDbRepository';
2
+ import {
3
+ DeleteCommand,
4
+ DeleteCommandInput,
5
+ DynamoDBDocument,
6
+ DynamoDBDocumentClient,
7
+ GetCommand,
8
+ GetCommandInput,
9
+ PutCommand,
10
+ PutCommandInput,
11
+ QueryCommand,
12
+ QueryCommandInput,
13
+ UpdateCommand,
14
+ UpdateCommandInput,
15
+ TransactWriteCommand,
16
+ TransactWriteCommandInput,
17
+ } from '@aws-sdk/lib-dynamodb';
18
+ import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
19
+ import { DynamoDbManager, Transaction } from './DynamoDbManager';
20
+ import { MissingKeyValuesError, InvalidDbSchemaError } from '..';
21
+
22
+ import { mockClient } from 'aws-sdk-client-mock';
23
+ import 'aws-sdk-client-mock-jest';
24
+ import { DynamoDB } from '@aws-sdk/client-dynamodb';
25
+ import { DuplicateItemError } from '../error/DuplicateItemError';
26
+ import crypto from 'crypto';
27
+
28
+ const ddbClientMock = mockClient(DynamoDBDocumentClient);
29
+ const ddbDoc = DynamoDBDocument.from(new DynamoDB({}));
30
+
31
+ interface ITestItem {
32
+ name: string;
33
+ age: number;
34
+ country: string;
35
+ data?: object;
36
+ }
37
+
38
+ class TestItem implements ITestItem {
39
+ public name: string;
40
+ public age: number;
41
+ public country: string;
42
+ public data: object;
43
+
44
+ constructor(data: Partial<ITestItem> = {}) {
45
+ this.name = data.name ?? 'default name';
46
+ this.age = data.age ?? 0;
47
+ this.country = data.country ?? 'default country';
48
+ this.data = data.data ?? {};
49
+
50
+ if (typeof this.name !== 'string') {
51
+ throw Error('Invalid "name"');
52
+ }
53
+ if (typeof this.age !== 'number') {
54
+ throw Error('Invalid "age"');
55
+ }
56
+ if (typeof this.country !== 'string') {
57
+ throw Error('Invalid "country"');
58
+ }
59
+ if (typeof this.data !== 'object' || Array.isArray(this.data)) {
60
+ throw Error('Invalid "data"');
61
+ }
62
+ }
63
+ }
64
+
65
+ export type TestRepositories = {
66
+ testItem: TestItemRepository;
67
+ };
68
+ export type TestDbManager = DynamoDbManager<TestRepositories>;
69
+
70
+ const TABLE_NAME = 'test-table';
71
+
72
+ const TEST_ITEM_ENTITY_NAME = 'test-item-entity';
73
+ const TEST_ITEM_ENTITY_DEFINITION = {
74
+ keys: {
75
+ pk: {
76
+ format: 'test_item#{name}',
77
+ attributeName: 'pk',
78
+ },
79
+ sk: {
80
+ format: '#meta',
81
+ attributeName: 'sk',
82
+ },
83
+ },
84
+ indexes: {
85
+ 'gsi1_pk-gsi1_sk-index': {
86
+ pk: {
87
+ format: 'country#{country}',
88
+ attributeName: 'gsi1_pk',
89
+ },
90
+ sk: {
91
+ format: 'age#{age}',
92
+ attributeName: 'gsi1_sk',
93
+ },
94
+ },
95
+ },
96
+ };
97
+
98
+ class TestItemRepository extends AbstractDynamoDbRepository<ITestItem, TestItem> {
99
+ constructor(tableName: string, dbManager: TestDbManager) {
100
+ super(tableName, dbManager, TEST_ITEM_ENTITY_NAME, TEST_ITEM_ENTITY_DEFINITION, TestItem);
101
+ }
102
+ }
103
+
104
+ const ddbManager = new DynamoDbManager<TestRepositories>(ddbDoc, (dbManager: TestDbManager) => {
105
+ return {
106
+ testItem: new TestItemRepository(TABLE_NAME, dbManager),
107
+ };
108
+ });
109
+
110
+ // Test start ////////////////////////////////////
111
+
112
+ describe('AbstractRepository', () => {
113
+ let repository: TestItemRepository;
114
+ beforeEach(() => {
115
+ ddbClientMock.reset();
116
+ repository = new TestItemRepository(TABLE_NAME, ddbManager);
117
+ });
118
+
119
+ describe('createItem()', () => {
120
+ it('should create and return the item object if valid input', async () => {
121
+ ddbClientMock.on(PutCommand).resolves({
122
+ $metadata: {
123
+ httpStatusCode: 200,
124
+ },
125
+ });
126
+ const input: PutCommandInput = {
127
+ TableName: TABLE_NAME,
128
+ Item: {
129
+ pk: 'test_item#foo',
130
+ sk: '#meta',
131
+ gsi1_pk: 'country#au',
132
+ gsi1_sk: 'age#99',
133
+
134
+ name: 'foo',
135
+ age: 99,
136
+ country: 'au',
137
+ data: {},
138
+ },
139
+ ConditionExpression: `attribute_not_exists(pk)`,
140
+ };
141
+
142
+ const item = {
143
+ name: 'foo',
144
+ age: 99,
145
+ country: 'au',
146
+ data: {},
147
+ };
148
+ const result = await repository.createItem(item);
149
+ expect(ddbClientMock).toHaveReceivedCommandWith(PutCommand, input);
150
+ expect(result).toEqual(
151
+ new TestItem({
152
+ name: 'foo',
153
+ age: 99,
154
+ country: 'au',
155
+ data: {},
156
+ }),
157
+ );
158
+ });
159
+
160
+ it('should throw error if invalid input', async () => {
161
+ const item = {
162
+ name: 'foo',
163
+ age: 99,
164
+ country: 'au',
165
+ data: [], // should be non-array object
166
+ };
167
+ await expect(repository.createItem(item)).rejects.toEqual(new Error('Invalid "data"'));
168
+ });
169
+
170
+ it('should throw error if item already exists input', async () => {
171
+ ddbClientMock.on(PutCommand).rejects(
172
+ new ConditionalCheckFailedException({
173
+ $metadata: {},
174
+ message: 'already exists',
175
+ }),
176
+ );
177
+
178
+ const item = {
179
+ name: 'foo',
180
+ age: 99,
181
+ country: 'au',
182
+ data: {},
183
+ };
184
+ await expect(repository.createItem(item)).rejects.toEqual(new DuplicateItemError('Item already exists'));
185
+ });
186
+
187
+ it('should throw error if excess column in input', async () => {
188
+ const item = {
189
+ name: 'foo',
190
+ age: 99,
191
+ country: 'au',
192
+ data: {},
193
+ extraColumn: '123',
194
+ extraColumn2: '',
195
+ };
196
+ await expect(repository.createItem(item)).rejects.toEqual(
197
+ new InvalidDbSchemaError('Excess properties in entity test-item-entity: extraColumn, extraColumn2'),
198
+ );
199
+ });
200
+
201
+ it('should throw error if input does not includes key field(s)', async () => {
202
+ const partialItem = {
203
+ age: 99,
204
+ country: 'au',
205
+ data: {},
206
+ };
207
+ await expect(repository.createItem(partialItem as unknown as TestItem)).rejects.toEqual(
208
+ new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
209
+ );
210
+ });
211
+ });
212
+
213
+ describe('updateItem()', () => {
214
+ it('should update and return the item object if valid input', async () => {
215
+ ddbClientMock.on(GetCommand).resolves({
216
+ $metadata: {
217
+ httpStatusCode: 200,
218
+ },
219
+ Item: {
220
+ name: 'foo',
221
+ age: 99,
222
+ country: 'au',
223
+ data: {},
224
+ },
225
+ });
226
+ ddbClientMock.on(UpdateCommand).resolves({
227
+ $metadata: {
228
+ httpStatusCode: 200,
229
+ },
230
+ Attributes: {
231
+ name: 'foo',
232
+ age: 99,
233
+ country: 'au-updated',
234
+ data: {},
235
+ },
236
+ });
237
+ const input: UpdateCommandInput = {
238
+ TableName: TABLE_NAME,
239
+ Key: { pk: 'test_item#foo', sk: '#meta' },
240
+ UpdateExpression: 'SET #country = :country',
241
+ ExpressionAttributeNames: {
242
+ '#country': 'country',
243
+ },
244
+ ExpressionAttributeValues: {
245
+ ':country': 'au-updated',
246
+ },
247
+ ConditionExpression: `attribute_exists(pk)`,
248
+ };
249
+
250
+ const partialItemWithKeyFields = {
251
+ name: 'foo',
252
+ };
253
+ const updateItem = {
254
+ country: 'au-updated',
255
+ };
256
+ const result = await repository.updateItem(partialItemWithKeyFields, updateItem);
257
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, UpdateCommand, input);
258
+ expect(result).toEqual(
259
+ new TestItem({
260
+ name: 'foo',
261
+ age: 99,
262
+ country: 'au-updated',
263
+ data: {},
264
+ }),
265
+ );
266
+ });
267
+
268
+ it('should undefined if item does does not exist', async () => {
269
+ ddbClientMock.on(GetCommand).resolves({
270
+ $metadata: {
271
+ httpStatusCode: 200,
272
+ },
273
+ });
274
+
275
+ const partialItemWithKeyFields = {
276
+ name: 'foo',
277
+ };
278
+ const updateItem = {
279
+ country: 'au-updated',
280
+ };
281
+ const result = await repository.updateItem(partialItemWithKeyFields, updateItem);
282
+ expect(result).toEqual(undefined);
283
+ });
284
+
285
+ it('should return undefined if update cmd conditional check fails', async () => {
286
+ ddbClientMock.on(GetCommand).resolves({
287
+ $metadata: {
288
+ httpStatusCode: 200,
289
+ },
290
+ Item: {
291
+ name: 'foo',
292
+ age: 99,
293
+ country: 'au',
294
+ data: {},
295
+ },
296
+ });
297
+
298
+ ddbClientMock.on(UpdateCommand).rejects(
299
+ new ConditionalCheckFailedException({
300
+ $metadata: {},
301
+ message: 'not found',
302
+ }),
303
+ );
304
+
305
+ const partialItemWithKeyFields = {
306
+ name: 'foo',
307
+ };
308
+ const updateItem = {
309
+ country: 'au-updated',
310
+ };
311
+ const result = await repository.updateItem(partialItemWithKeyFields, updateItem);
312
+ expect(result).toEqual(undefined);
313
+ });
314
+
315
+ it('should throw error update data has invalid data', async () => {
316
+ ddbClientMock.on(GetCommand).resolves({
317
+ $metadata: {
318
+ httpStatusCode: 200,
319
+ },
320
+ Item: {
321
+ name: 'foo',
322
+ age: 99,
323
+ country: 'au',
324
+ data: {},
325
+ },
326
+ });
327
+
328
+ const partialItemWithKeyFields = {
329
+ name: 'foo',
330
+ };
331
+ const updateItem = {
332
+ country: 61, // should be "string" type
333
+ };
334
+ await expect(
335
+ repository.updateItem(partialItemWithKeyFields, updateItem as unknown as Partial<TestItem>),
336
+ ).rejects.toEqual(new Error('Invalid "country"'));
337
+ });
338
+
339
+ it('should throw error if excess column in input', async () => {
340
+ ddbClientMock.on(GetCommand).resolves({
341
+ $metadata: {
342
+ httpStatusCode: 200,
343
+ },
344
+ Item: {
345
+ name: 'foo',
346
+ age: 99,
347
+ country: 'au',
348
+ data: {},
349
+ },
350
+ });
351
+
352
+ const partialItemWithKeyFields = {
353
+ name: 'foo',
354
+ };
355
+ const updateItem = {
356
+ country: 'au-updated',
357
+ extra: '',
358
+ };
359
+ await expect(
360
+ repository.updateItem(partialItemWithKeyFields, updateItem as unknown as Partial<TestItem>),
361
+ ).rejects.toEqual(new InvalidDbSchemaError('Excess properties in entity test-item-entity: extra'));
362
+ });
363
+
364
+ it('should throw error if input does not includes key field(s)', async () => {
365
+ const partialItemWithKeyFields = {
366
+ age: 99,
367
+ };
368
+ const updateItem = {
369
+ country: 'au-updated', // should be "string" type
370
+ };
371
+ await expect(repository.updateItem(partialItemWithKeyFields, updateItem)).rejects.toEqual(
372
+ new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
373
+ );
374
+ });
375
+ });
376
+
377
+ describe('getItem()', () => {
378
+ it('should return the item object if found', async () => {
379
+ ddbClientMock.on(GetCommand).resolves({
380
+ $metadata: {
381
+ httpStatusCode: 200,
382
+ },
383
+ Item: {
384
+ name: 'foo',
385
+ age: 99,
386
+ country: 'au',
387
+ data: {},
388
+ },
389
+ });
390
+ const input: GetCommandInput = {
391
+ TableName: TABLE_NAME,
392
+ Key: { pk: 'test_item#foo', sk: '#meta' },
393
+ };
394
+
395
+ const partialItem = { name: 'foo' };
396
+ const result = await repository.getItem(partialItem);
397
+ expect(ddbClientMock).toHaveReceivedCommandWith(GetCommand, input);
398
+ expect(result).toEqual(
399
+ new TestItem({
400
+ name: 'foo',
401
+ age: 99,
402
+ country: 'au',
403
+ data: {},
404
+ }),
405
+ );
406
+ });
407
+
408
+ it('should return undefined if item not found', async () => {
409
+ ddbClientMock.on(GetCommand).resolves({
410
+ $metadata: {
411
+ httpStatusCode: 200,
412
+ },
413
+ });
414
+ const input: GetCommandInput = {
415
+ TableName: TABLE_NAME,
416
+ Key: { pk: 'test_item#foo', sk: '#meta' },
417
+ };
418
+
419
+ const partialItem = { name: 'foo' };
420
+ const result = await repository.getItem(partialItem);
421
+ expect(ddbClientMock).toHaveReceivedCommandWith(GetCommand, input);
422
+ expect(result).toEqual(undefined);
423
+ });
424
+
425
+ it('should throw error if item schema validation fails', async () => {
426
+ ddbClientMock.on(GetCommand).resolves({
427
+ $metadata: {
428
+ httpStatusCode: 200,
429
+ },
430
+ Item: {
431
+ name: 'foo',
432
+ age: '99', // should be number
433
+ country: 'au',
434
+ data: {},
435
+ },
436
+ });
437
+
438
+ const partialItem = { name: 'foo' };
439
+ await expect(repository.getItem(partialItem)).rejects.toEqual(new Error('Invalid "age"'));
440
+ });
441
+
442
+ it('should throw error if input does not includes key field(s)', async () => {
443
+ const partialItem = { age: 99 };
444
+ await expect(repository.getItem(partialItem)).rejects.toEqual(
445
+ new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
446
+ );
447
+ });
448
+ });
449
+
450
+ describe('queryItems()', () => {
451
+ it('should return the items if found', async () => {
452
+ ddbClientMock.on(QueryCommand).resolves({
453
+ $metadata: {
454
+ httpStatusCode: 200,
455
+ },
456
+ Items: [
457
+ {
458
+ name: 'foo',
459
+ age: 99,
460
+ country: 'au',
461
+ data: {},
462
+ },
463
+ ],
464
+ });
465
+ const input: QueryCommandInput = {
466
+ TableName: TABLE_NAME,
467
+ KeyConditionExpression: '#pkName = :pkValue',
468
+ ExpressionAttributeNames: {
469
+ '#pkName': 'pk',
470
+ },
471
+ ExpressionAttributeValues: {
472
+ ':pkValue': 'test_item#foo',
473
+ },
474
+ };
475
+
476
+ const partialItem = { name: 'foo' };
477
+ const result = await repository.queryItems(partialItem);
478
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
479
+ expect(result).toEqual([
480
+ new TestItem({
481
+ name: 'foo',
482
+ age: 99,
483
+ country: 'au',
484
+ data: {},
485
+ }),
486
+ ]);
487
+ });
488
+
489
+ it('should return empty array if no items found', async () => {
490
+ ddbClientMock.on(QueryCommand).resolves({
491
+ $metadata: {
492
+ httpStatusCode: 200,
493
+ },
494
+ });
495
+ const input: QueryCommandInput = {
496
+ TableName: TABLE_NAME,
497
+ KeyConditionExpression: '#pkName = :pkValue',
498
+ ExpressionAttributeNames: {
499
+ '#pkName': 'pk',
500
+ },
501
+ ExpressionAttributeValues: {
502
+ ':pkValue': 'test_item#foo',
503
+ },
504
+ };
505
+
506
+ const partialItem = { name: 'foo' };
507
+ const result = await repository.queryItems(partialItem);
508
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
509
+ expect(result).toEqual([]);
510
+ });
511
+
512
+ it('should use sort key in query if "useSortKey" param is true', async () => {
513
+ ddbClientMock.on(QueryCommand).resolves({
514
+ $metadata: {
515
+ httpStatusCode: 200,
516
+ },
517
+ });
518
+ const input: QueryCommandInput = {
519
+ TableName: TABLE_NAME,
520
+ KeyConditionExpression: '#pkName = :pkValue AND #skName = :skValue',
521
+ ExpressionAttributeNames: {
522
+ '#pkName': 'pk',
523
+ '#skName': 'sk',
524
+ },
525
+ ExpressionAttributeValues: {
526
+ ':pkValue': 'test_item#foo',
527
+ ':skValue': '#meta',
528
+ },
529
+ };
530
+
531
+ const partialItem = { name: 'foo' };
532
+ const useSortKey = true;
533
+ const _result = await repository.queryItems(partialItem, useSortKey);
534
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
535
+ });
536
+
537
+ it('should return the items if found when using gsi index', async () => {
538
+ ddbClientMock.on(QueryCommand).resolves({
539
+ $metadata: {
540
+ httpStatusCode: 200,
541
+ },
542
+ Items: [
543
+ {
544
+ name: 'foo',
545
+ age: 99,
546
+ country: 'au',
547
+ data: {},
548
+ },
549
+ {
550
+ name: 'fox',
551
+ age: 11,
552
+ country: 'au',
553
+ data: {},
554
+ },
555
+ ],
556
+ });
557
+ const index = 'gsi1_pk-gsi1_sk-index';
558
+ const input: QueryCommandInput = {
559
+ TableName: TABLE_NAME,
560
+ IndexName: index,
561
+ KeyConditionExpression: '#pkName = :pkValue',
562
+ ExpressionAttributeNames: {
563
+ '#pkName': 'gsi1_pk',
564
+ },
565
+ ExpressionAttributeValues: {
566
+ ':pkValue': 'country#au',
567
+ },
568
+ };
569
+
570
+ const partialItem = { country: 'au' };
571
+ const useSortKey = false;
572
+ const result = await repository.queryItems(partialItem, useSortKey, index);
573
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
574
+ expect(result).toEqual([
575
+ new TestItem({
576
+ name: 'foo',
577
+ age: 99,
578
+ country: 'au',
579
+ data: {},
580
+ }),
581
+ new TestItem({
582
+ name: 'fox',
583
+ age: 11,
584
+ country: 'au',
585
+ data: {},
586
+ }),
587
+ ]);
588
+ });
589
+
590
+ it('should use sort key in query if "useSortKey" param is true when using gsi index', async () => {
591
+ ddbClientMock.on(QueryCommand).resolves({
592
+ $metadata: {
593
+ httpStatusCode: 200,
594
+ },
595
+ });
596
+ const index = 'gsi1_pk-gsi1_sk-index';
597
+ const input: QueryCommandInput = {
598
+ TableName: TABLE_NAME,
599
+ IndexName: index,
600
+ KeyConditionExpression: '#pkName = :pkValue AND #skName = :skValue',
601
+ ExpressionAttributeNames: {
602
+ '#pkName': 'gsi1_pk',
603
+ '#skName': 'gsi1_sk',
604
+ },
605
+ ExpressionAttributeValues: {
606
+ ':pkValue': 'country#au',
607
+ ':skValue': 'age#99',
608
+ },
609
+ };
610
+
611
+ const partialItem = { age: 99, country: 'au' };
612
+ const useSortKey = true;
613
+ const _result = await repository.queryItems(partialItem, useSortKey, index);
614
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
615
+ });
616
+
617
+ it('should throw error for invalid index query', async () => {
618
+ const index = 'undefined-index';
619
+ const partialItem = { age: 99, country: 'au' };
620
+ await expect(repository.queryItems(partialItem, false, index)).rejects.toEqual(
621
+ new MissingKeyValuesError(`Table index '${index}' not defined on entity test-item-entity`),
622
+ );
623
+ });
624
+
625
+ it('should throw error for missing key fields', async () => {
626
+ const partialItem = { age: 99, country: 'au' };
627
+ await expect(repository.queryItems(partialItem)).rejects.toEqual(
628
+ new MissingKeyValuesError(`Key field "name" must be specified in the input item in entity test-item-entity`),
629
+ );
630
+ });
631
+
632
+ it('should throw error for missing key fields when using index', async () => {
633
+ const partialItem = { name: 'foo' };
634
+ const useSortKey = false;
635
+ const index = 'gsi1_pk-gsi1_sk-index';
636
+ await expect(repository.queryItems(partialItem, useSortKey, index)).rejects.toEqual(
637
+ new MissingKeyValuesError(`Key field "country" must be specified in the input item in entity test-item-entity`),
638
+ );
639
+ });
640
+
641
+ it('should throw error for missing key fields with "useSortKey" param true when using index', async () => {
642
+ const partialItem = { country: 'au' };
643
+ const useSortKey = true;
644
+ const index = 'gsi1_pk-gsi1_sk-index';
645
+ await expect(repository.queryItems(partialItem, useSortKey, index)).rejects.toEqual(
646
+ new MissingKeyValuesError(`Key field "age" must be specified in the input item in entity test-item-entity`),
647
+ );
648
+ });
649
+ });
650
+
651
+ describe('deleteItem()', () => {
652
+ it('should return 1 when item is found and deleted', async () => {
653
+ ddbClientMock.on(DeleteCommand).resolves({
654
+ $metadata: {
655
+ httpStatusCode: 200,
656
+ },
657
+ });
658
+ const input: DeleteCommandInput = {
659
+ TableName: TABLE_NAME,
660
+ Key: { pk: 'test_item#foo', sk: '#meta' },
661
+ };
662
+
663
+ const partialItem = { name: 'foo' };
664
+ const result = await repository.deleteItem(partialItem);
665
+ expect(ddbClientMock).toHaveReceivedCommandWith(DeleteCommand, input);
666
+ expect(result).toBe(1);
667
+ });
668
+
669
+ it('should return 0 when item is not found', async () => {
670
+ ddbClientMock.on(DeleteCommand).rejects(
671
+ new ConditionalCheckFailedException({
672
+ $metadata: {},
673
+ message: 'not found',
674
+ }),
675
+ );
676
+ const input: DeleteCommandInput = {
677
+ TableName: TABLE_NAME,
678
+ Key: { pk: 'test_item#foo', sk: '#meta' },
679
+ };
680
+
681
+ const partialItem = { name: 'foo' };
682
+ const result = await repository.deleteItem(partialItem);
683
+ expect(ddbClientMock).toHaveReceivedCommandWith(DeleteCommand, input);
684
+ expect(result).toBe(0);
685
+ });
686
+
687
+ it('should throw error if request fails', async () => {
688
+ const partialItem = { name: 'foo' };
689
+ ddbClientMock.on(DeleteCommand).rejects('some other error');
690
+ await expect(repository.deleteItem(partialItem)).rejects.toEqual(new Error('some other error'));
691
+ });
692
+
693
+ it('should throw error if input does not includes key field(s)', async () => {
694
+ const partialItem = { age: 99 };
695
+ await expect(repository.deleteItem(partialItem)).rejects.toEqual(
696
+ new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
697
+ );
698
+ });
699
+ });
700
+
701
+ describe('Writer fns with transaction - DynamoDbManager', () => {
702
+ it('should execute the multiple transaction write request in a single request', async () => {
703
+ const spy = jest.spyOn(crypto, 'randomUUID');
704
+ spy.mockImplementation(() => 'some-token' as any);
705
+
706
+ ddbClientMock.on(GetCommand).resolves({
707
+ $metadata: {
708
+ httpStatusCode: 200,
709
+ },
710
+ Item: {
711
+ name: 'foo2',
712
+ age: 99,
713
+ country: 'au',
714
+ data: {},
715
+ },
716
+ });
717
+
718
+ ddbClientMock.on(TransactWriteCommand).resolves({
719
+ $metadata: {
720
+ httpStatusCode: 200,
721
+ },
722
+ });
723
+
724
+ const result = await ddbManager.executeInTransaction(async (transaction: Transaction) => {
725
+ await repository.deleteItem({ name: 'foo' }, transaction);
726
+ await repository.updateItem(
727
+ {
728
+ name: 'foo2',
729
+ },
730
+ {
731
+ age: 55,
732
+ },
733
+ transaction,
734
+ );
735
+ return await repository.createItem(
736
+ {
737
+ name: 'foo3',
738
+ age: 11,
739
+ country: 'au',
740
+ data: {},
741
+ },
742
+ transaction,
743
+ );
744
+ });
745
+
746
+ const input: TransactWriteCommandInput = {
747
+ ClientRequestToken: 'some-token',
748
+ TransactItems: [
749
+ {
750
+ Delete: {
751
+ ConditionExpression: 'attribute_exists(pk)',
752
+ Key: {
753
+ pk: 'test_item#foo',
754
+ sk: '#meta',
755
+ },
756
+ TableName: 'test-table',
757
+ },
758
+ },
759
+ {
760
+ Update: {
761
+ ConditionExpression: 'attribute_exists(pk)',
762
+ ExpressionAttributeNames: {
763
+ '#age': 'age',
764
+ },
765
+ ExpressionAttributeValues: {
766
+ ':age': 55,
767
+ },
768
+ Key: {
769
+ pk: 'test_item#foo2',
770
+ sk: '#meta',
771
+ },
772
+ TableName: 'test-table',
773
+ UpdateExpression: 'SET #age = :age',
774
+ },
775
+ },
776
+ {
777
+ Put: {
778
+ ConditionExpression: 'attribute_not_exists(pk)',
779
+ Item: {
780
+ age: 11,
781
+ country: 'au',
782
+ data: {},
783
+ gsi1_pk: 'country#au',
784
+ gsi1_sk: 'age#11',
785
+ name: 'foo3',
786
+ pk: 'test_item#foo3',
787
+ sk: '#meta',
788
+ },
789
+ TableName: 'test-table',
790
+ },
791
+ },
792
+ ],
793
+ };
794
+
795
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, TransactWriteCommand, input);
796
+ expect(result).toEqual({
797
+ name: 'foo3',
798
+ age: 11,
799
+ country: 'au',
800
+ data: {},
801
+ });
802
+ });
803
+ });
804
+ });