@squiz/db-lib 1.64.0 → 1.66.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.
@@ -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
+ });