@squiz/db-lib 1.71.3 → 1.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +62 -9
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.js +162 -19
- package/lib/dynamodb/AbstractDynamoDbRepository.js.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js +581 -31
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -1
- package/package.json +2 -2
- package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +642 -37
- package/src/dynamodb/AbstractDynamoDbRepository.ts +206 -31
- package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
|
|
1
|
-
import { AbstractDynamoDbRepository } from './AbstractDynamoDbRepository';
|
1
|
+
import { AbstractDynamoDbRepository, QueryOptions } from './AbstractDynamoDbRepository';
|
2
2
|
import {
|
3
3
|
DeleteCommand,
|
4
4
|
DeleteCommandInput,
|
@@ -14,6 +14,10 @@ import {
|
|
14
14
|
UpdateCommandInput,
|
15
15
|
TransactWriteCommand,
|
16
16
|
TransactWriteCommandInput,
|
17
|
+
BatchGetCommand,
|
18
|
+
BatchGetCommandInput,
|
19
|
+
BatchWriteCommand,
|
20
|
+
BatchWriteCommandInput,
|
17
21
|
} from '@aws-sdk/lib-dynamodb';
|
18
22
|
import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
|
19
23
|
import { DynamoDbManager, Transaction } from './DynamoDbManager';
|
@@ -245,13 +249,11 @@ describe('AbstractRepository', () => {
|
|
245
249
|
ConditionExpression: `attribute_exists(pk)`,
|
246
250
|
};
|
247
251
|
|
248
|
-
const partialItemWithKeyFields = {
|
249
|
-
name: 'foo',
|
250
|
-
};
|
251
252
|
const updateItem = {
|
253
|
+
name: 'foo',
|
252
254
|
country: 'au-updated',
|
253
255
|
};
|
254
|
-
const result = await repository.updateItem(
|
256
|
+
const result = await repository.updateItem(updateItem);
|
255
257
|
expect(ddbClientMock).toHaveReceivedNthCommandWith(2, UpdateCommand, input);
|
256
258
|
expect(result).toEqual(
|
257
259
|
new TestItem({
|
@@ -263,20 +265,49 @@ describe('AbstractRepository', () => {
|
|
263
265
|
);
|
264
266
|
});
|
265
267
|
|
266
|
-
it('should
|
268
|
+
it('should not trigger update request if the input attributes are same as in the existing item', async () => {
|
267
269
|
ddbClientMock.on(GetCommand).resolves({
|
268
270
|
$metadata: {
|
269
271
|
httpStatusCode: 200,
|
270
272
|
},
|
273
|
+
Item: {
|
274
|
+
name: 'foo',
|
275
|
+
age: 99,
|
276
|
+
country: 'au',
|
277
|
+
data: {},
|
278
|
+
},
|
271
279
|
});
|
280
|
+
ddbClientMock.on(UpdateCommand).rejects(new Error('updateItem() called when not expected'));
|
272
281
|
|
273
|
-
|
282
|
+
// update input attributes are same as in the existing item
|
283
|
+
const updateItem = {
|
274
284
|
name: 'foo',
|
285
|
+
country: 'au',
|
275
286
|
};
|
287
|
+
|
288
|
+
const result = await repository.updateItem(updateItem);
|
289
|
+
expect(result).toEqual(
|
290
|
+
new TestItem({
|
291
|
+
name: 'foo',
|
292
|
+
age: 99,
|
293
|
+
country: 'au',
|
294
|
+
data: {},
|
295
|
+
}),
|
296
|
+
);
|
297
|
+
});
|
298
|
+
|
299
|
+
it('should return undefined if item does does not exist', async () => {
|
300
|
+
ddbClientMock.on(GetCommand).resolves({
|
301
|
+
$metadata: {
|
302
|
+
httpStatusCode: 200,
|
303
|
+
},
|
304
|
+
});
|
305
|
+
|
276
306
|
const updateItem = {
|
307
|
+
name: 'foo',
|
277
308
|
country: 'au-updated',
|
278
309
|
};
|
279
|
-
const result = await repository.updateItem(
|
310
|
+
const result = await repository.updateItem(updateItem);
|
280
311
|
expect(result).toEqual(undefined);
|
281
312
|
});
|
282
313
|
|
@@ -300,13 +331,11 @@ describe('AbstractRepository', () => {
|
|
300
331
|
}),
|
301
332
|
);
|
302
333
|
|
303
|
-
const partialItemWithKeyFields = {
|
304
|
-
name: 'foo',
|
305
|
-
};
|
306
334
|
const updateItem = {
|
335
|
+
name: 'foo',
|
307
336
|
country: 'au-updated',
|
308
337
|
};
|
309
|
-
const result = await repository.updateItem(
|
338
|
+
const result = await repository.updateItem(updateItem);
|
310
339
|
expect(result).toEqual(undefined);
|
311
340
|
});
|
312
341
|
|
@@ -323,15 +352,13 @@ describe('AbstractRepository', () => {
|
|
323
352
|
},
|
324
353
|
});
|
325
354
|
|
326
|
-
const partialItemWithKeyFields = {
|
327
|
-
name: 'foo',
|
328
|
-
};
|
329
355
|
const updateItem = {
|
356
|
+
name: 'foo',
|
330
357
|
country: 61, // should be "string" type
|
331
358
|
};
|
332
|
-
await expect(
|
333
|
-
|
334
|
-
)
|
359
|
+
await expect(repository.updateItem(updateItem as unknown as Partial<TestItem>)).rejects.toEqual(
|
360
|
+
new Error('Invalid "country"'),
|
361
|
+
);
|
335
362
|
});
|
336
363
|
|
337
364
|
it('should throw error if excess column in input', async () => {
|
@@ -347,26 +374,22 @@ describe('AbstractRepository', () => {
|
|
347
374
|
},
|
348
375
|
});
|
349
376
|
|
350
|
-
const partialItemWithKeyFields = {
|
351
|
-
name: 'foo',
|
352
|
-
};
|
353
377
|
const updateItem = {
|
378
|
+
name: 'foo',
|
354
379
|
country: 'au-updated',
|
355
380
|
extra: '',
|
356
381
|
};
|
357
|
-
await expect(
|
358
|
-
|
359
|
-
)
|
382
|
+
await expect(repository.updateItem(updateItem as unknown as Partial<TestItem>)).rejects.toEqual(
|
383
|
+
new InvalidDbSchemaError('Excess properties in entity test-item-entity: extra'),
|
384
|
+
);
|
360
385
|
});
|
361
386
|
|
362
387
|
it('should throw error if input does not includes key field(s)', async () => {
|
363
|
-
const partialItemWithKeyFields = {
|
364
|
-
age: 99,
|
365
|
-
};
|
366
388
|
const updateItem = {
|
389
|
+
age: 99,
|
367
390
|
country: 'au-updated', // should be "string" type
|
368
391
|
};
|
369
|
-
await expect(repository.updateItem(
|
392
|
+
await expect(repository.updateItem(updateItem)).rejects.toEqual(
|
370
393
|
new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
|
371
394
|
);
|
372
395
|
});
|
@@ -473,6 +496,471 @@ describe('AbstractRepository', () => {
|
|
473
496
|
});
|
474
497
|
});
|
475
498
|
|
499
|
+
describe('getItems()', () => {
|
500
|
+
it('should use BatchGetItem to get result', async () => {
|
501
|
+
ddbClientMock.on(BatchGetCommand).resolves({
|
502
|
+
$metadata: {
|
503
|
+
httpStatusCode: 200,
|
504
|
+
},
|
505
|
+
Responses: {
|
506
|
+
[TABLE_NAME]: [
|
507
|
+
{
|
508
|
+
name: 'foo',
|
509
|
+
age: 99,
|
510
|
+
country: 'au',
|
511
|
+
data: {},
|
512
|
+
data2: '{"foo":"bar","num":123}',
|
513
|
+
},
|
514
|
+
{
|
515
|
+
name: 'foo2',
|
516
|
+
age: 999,
|
517
|
+
country: 'au',
|
518
|
+
data: {},
|
519
|
+
data2: '{"foo":"bar","num":123}',
|
520
|
+
},
|
521
|
+
],
|
522
|
+
},
|
523
|
+
});
|
524
|
+
const input: BatchGetCommandInput = {
|
525
|
+
RequestItems: {
|
526
|
+
[TABLE_NAME]: {
|
527
|
+
Keys: [
|
528
|
+
{ pk: 'test_item#foo', sk: '#meta' },
|
529
|
+
{ pk: 'test_item#foo2', sk: '#meta' },
|
530
|
+
],
|
531
|
+
},
|
532
|
+
},
|
533
|
+
};
|
534
|
+
|
535
|
+
const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
|
536
|
+
const result = await repository.getItems(requestItems);
|
537
|
+
expect(ddbClientMock).toHaveReceivedCommandWith(BatchGetCommand, input);
|
538
|
+
expect(result).toEqual([
|
539
|
+
new TestItem({
|
540
|
+
name: 'foo',
|
541
|
+
age: 99,
|
542
|
+
country: 'au',
|
543
|
+
data: {},
|
544
|
+
data2: {
|
545
|
+
foo: 'bar',
|
546
|
+
num: 123,
|
547
|
+
},
|
548
|
+
}),
|
549
|
+
new TestItem({
|
550
|
+
name: 'foo2',
|
551
|
+
age: 999,
|
552
|
+
country: 'au',
|
553
|
+
data: {},
|
554
|
+
data2: {
|
555
|
+
foo: 'bar',
|
556
|
+
num: 123,
|
557
|
+
},
|
558
|
+
}),
|
559
|
+
]);
|
560
|
+
});
|
561
|
+
|
562
|
+
it('should retry if unprocessed keys returned', async () => {
|
563
|
+
const input1: BatchGetCommandInput = {
|
564
|
+
RequestItems: {
|
565
|
+
[TABLE_NAME]: {
|
566
|
+
Keys: [
|
567
|
+
{ pk: 'test_item#foo', sk: '#meta' },
|
568
|
+
{ pk: 'test_item#foo2', sk: '#meta' },
|
569
|
+
],
|
570
|
+
},
|
571
|
+
},
|
572
|
+
};
|
573
|
+
const input2: BatchGetCommandInput = {
|
574
|
+
RequestItems: {
|
575
|
+
[TABLE_NAME]: {
|
576
|
+
Keys: [{ pk: 'test_item#foo2', sk: '#meta' }],
|
577
|
+
},
|
578
|
+
},
|
579
|
+
};
|
580
|
+
|
581
|
+
ddbClientMock.on(BatchGetCommand, input1).resolves({
|
582
|
+
$metadata: {
|
583
|
+
httpStatusCode: 200,
|
584
|
+
},
|
585
|
+
Responses: {
|
586
|
+
[TABLE_NAME]: [
|
587
|
+
{
|
588
|
+
name: 'foo',
|
589
|
+
age: 99,
|
590
|
+
country: 'au',
|
591
|
+
data: {},
|
592
|
+
data2: '{"foo":"bar","num":123}',
|
593
|
+
},
|
594
|
+
],
|
595
|
+
},
|
596
|
+
UnprocessedKeys: {
|
597
|
+
[TABLE_NAME]: {
|
598
|
+
Keys: [{ pk: 'test_item#foo2', sk: '#meta' }],
|
599
|
+
},
|
600
|
+
},
|
601
|
+
});
|
602
|
+
|
603
|
+
ddbClientMock.on(BatchGetCommand, input2).resolves({
|
604
|
+
$metadata: {
|
605
|
+
httpStatusCode: 200,
|
606
|
+
},
|
607
|
+
Responses: {
|
608
|
+
[TABLE_NAME]: [
|
609
|
+
{
|
610
|
+
name: 'foo2',
|
611
|
+
age: 999,
|
612
|
+
country: 'au',
|
613
|
+
data: {},
|
614
|
+
data2: '{"foo":"bar","num":123}',
|
615
|
+
},
|
616
|
+
],
|
617
|
+
},
|
618
|
+
UnprocessedKeys: {},
|
619
|
+
});
|
620
|
+
|
621
|
+
const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
|
622
|
+
const result = await repository.getItems(requestItems);
|
623
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchGetCommand, input1);
|
624
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchGetCommand, input2);
|
625
|
+
expect(result).toEqual([
|
626
|
+
new TestItem({
|
627
|
+
name: 'foo',
|
628
|
+
age: 99,
|
629
|
+
country: 'au',
|
630
|
+
data: {},
|
631
|
+
data2: {
|
632
|
+
foo: 'bar',
|
633
|
+
num: 123,
|
634
|
+
},
|
635
|
+
}),
|
636
|
+
new TestItem({
|
637
|
+
name: 'foo2',
|
638
|
+
age: 999,
|
639
|
+
country: 'au',
|
640
|
+
data: {},
|
641
|
+
data2: {
|
642
|
+
foo: 'bar',
|
643
|
+
num: 123,
|
644
|
+
},
|
645
|
+
}),
|
646
|
+
]);
|
647
|
+
});
|
648
|
+
|
649
|
+
it('should fail after max retries for unprocessed keys', async () => {
|
650
|
+
const input1: BatchGetCommandInput = {
|
651
|
+
RequestItems: {
|
652
|
+
[TABLE_NAME]: {
|
653
|
+
Keys: [
|
654
|
+
{ pk: 'test_item#foo', sk: '#meta' },
|
655
|
+
{ pk: 'test_item#foo2', sk: '#meta' },
|
656
|
+
],
|
657
|
+
},
|
658
|
+
},
|
659
|
+
};
|
660
|
+
const input2: BatchGetCommandInput = {
|
661
|
+
RequestItems: {
|
662
|
+
[TABLE_NAME]: {
|
663
|
+
Keys: [{ pk: 'test_item#foo2', sk: '#meta' }],
|
664
|
+
},
|
665
|
+
},
|
666
|
+
};
|
667
|
+
|
668
|
+
ddbClientMock.on(BatchGetCommand, input1).resolves({
|
669
|
+
$metadata: {
|
670
|
+
httpStatusCode: 200,
|
671
|
+
},
|
672
|
+
Responses: {
|
673
|
+
[TABLE_NAME]: [
|
674
|
+
{
|
675
|
+
name: 'foo',
|
676
|
+
age: 99,
|
677
|
+
country: 'au',
|
678
|
+
data: {},
|
679
|
+
data2: '{"foo":"bar","num":123}',
|
680
|
+
},
|
681
|
+
],
|
682
|
+
},
|
683
|
+
UnprocessedKeys: {
|
684
|
+
[TABLE_NAME]: {
|
685
|
+
Keys: [{ pk: 'test_item#foo2', sk: '#meta' }],
|
686
|
+
},
|
687
|
+
},
|
688
|
+
});
|
689
|
+
|
690
|
+
ddbClientMock.on(BatchGetCommand, input2).resolves({
|
691
|
+
$metadata: {
|
692
|
+
httpStatusCode: 200,
|
693
|
+
},
|
694
|
+
Responses: {},
|
695
|
+
UnprocessedKeys: {
|
696
|
+
[TABLE_NAME]: {
|
697
|
+
Keys: [{ pk: 'test_item#foo2', sk: '#meta' }],
|
698
|
+
},
|
699
|
+
},
|
700
|
+
});
|
701
|
+
|
702
|
+
const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
|
703
|
+
await expect(repository.getItems(requestItems)).rejects.toEqual(
|
704
|
+
new Error('Maximum allowed retries exceeded for unprocessed items'),
|
705
|
+
);
|
706
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchGetCommand, input1);
|
707
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchGetCommand, input2);
|
708
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(3, BatchGetCommand, input2);
|
709
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(4, BatchGetCommand, input2);
|
710
|
+
});
|
711
|
+
|
712
|
+
it('should request BatchGetItem in batch of 100 items to get result', async () => {
|
713
|
+
ddbClientMock.on(BatchGetCommand).resolves({
|
714
|
+
$metadata: {
|
715
|
+
httpStatusCode: 200,
|
716
|
+
},
|
717
|
+
});
|
718
|
+
|
719
|
+
const requestItems = [];
|
720
|
+
for (let i = 0; i < 120; i++) {
|
721
|
+
requestItems.push({ name: `foo${i}` });
|
722
|
+
}
|
723
|
+
// keys for first batch request
|
724
|
+
const keys1 = [];
|
725
|
+
for (let i = 0; i < 100; i++) {
|
726
|
+
keys1.push({ pk: `test_item#foo${i}`, sk: '#meta' });
|
727
|
+
}
|
728
|
+
// keys for second batch request
|
729
|
+
const keys2 = [];
|
730
|
+
for (let i = 100; i < 120; i++) {
|
731
|
+
keys2.push({ pk: `test_item#foo${i}`, sk: '#meta' });
|
732
|
+
}
|
733
|
+
|
734
|
+
const input1: BatchGetCommandInput = {
|
735
|
+
RequestItems: {
|
736
|
+
[TABLE_NAME]: {
|
737
|
+
Keys: keys1,
|
738
|
+
},
|
739
|
+
},
|
740
|
+
};
|
741
|
+
const input2: BatchGetCommandInput = {
|
742
|
+
RequestItems: {
|
743
|
+
[TABLE_NAME]: {
|
744
|
+
Keys: keys2,
|
745
|
+
},
|
746
|
+
},
|
747
|
+
};
|
748
|
+
|
749
|
+
await repository.getItems(requestItems);
|
750
|
+
expect(ddbClientMock).toHaveReceivedCommandTimes(BatchGetCommand, 2);
|
751
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchGetCommand, input1);
|
752
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchGetCommand, input2);
|
753
|
+
});
|
754
|
+
|
755
|
+
it('should throw error if any input item does not includes key field(s)', async () => {
|
756
|
+
const requestItems = [{ name: 'foo' }, { age: 22 }];
|
757
|
+
await expect(repository.getItems(requestItems)).rejects.toEqual(
|
758
|
+
new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
|
759
|
+
);
|
760
|
+
});
|
761
|
+
});
|
762
|
+
|
763
|
+
describe('deleteItems()', () => {
|
764
|
+
it('should use batchWrite() to get result', async () => {
|
765
|
+
ddbClientMock.on(BatchWriteCommand).resolves({
|
766
|
+
$metadata: {
|
767
|
+
httpStatusCode: 200,
|
768
|
+
},
|
769
|
+
ItemCollectionMetrics: {
|
770
|
+
[TABLE_NAME]: [{}],
|
771
|
+
},
|
772
|
+
});
|
773
|
+
const input: BatchWriteCommandInput = {
|
774
|
+
RequestItems: {
|
775
|
+
[TABLE_NAME]: [
|
776
|
+
{
|
777
|
+
DeleteRequest: {
|
778
|
+
Key: { pk: 'test_item#foo', sk: '#meta' },
|
779
|
+
},
|
780
|
+
},
|
781
|
+
{
|
782
|
+
DeleteRequest: {
|
783
|
+
Key: { pk: 'test_item#foo2', sk: '#meta' },
|
784
|
+
},
|
785
|
+
},
|
786
|
+
],
|
787
|
+
},
|
788
|
+
};
|
789
|
+
|
790
|
+
const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
|
791
|
+
await repository.deleteItems(requestItems);
|
792
|
+
expect(ddbClientMock).toHaveReceivedCommandWith(BatchWriteCommand, input);
|
793
|
+
});
|
794
|
+
|
795
|
+
it('should use re-try if unprocessed items returned', async () => {
|
796
|
+
const input1: BatchWriteCommandInput = {
|
797
|
+
RequestItems: {
|
798
|
+
[TABLE_NAME]: [
|
799
|
+
{
|
800
|
+
DeleteRequest: {
|
801
|
+
Key: { pk: 'test_item#foo', sk: '#meta' },
|
802
|
+
},
|
803
|
+
},
|
804
|
+
{
|
805
|
+
DeleteRequest: {
|
806
|
+
Key: { pk: 'test_item#foo2', sk: '#meta' },
|
807
|
+
},
|
808
|
+
},
|
809
|
+
],
|
810
|
+
},
|
811
|
+
};
|
812
|
+
const input2: BatchWriteCommandInput = {
|
813
|
+
RequestItems: {
|
814
|
+
[TABLE_NAME]: [
|
815
|
+
{
|
816
|
+
DeleteRequest: {
|
817
|
+
Key: { pk: 'test_item#foo2', sk: '#meta' },
|
818
|
+
},
|
819
|
+
},
|
820
|
+
],
|
821
|
+
},
|
822
|
+
};
|
823
|
+
|
824
|
+
ddbClientMock.on(BatchWriteCommand, input1).resolves({
|
825
|
+
$metadata: {
|
826
|
+
httpStatusCode: 200,
|
827
|
+
},
|
828
|
+
UnprocessedItems: {
|
829
|
+
[TABLE_NAME]: [
|
830
|
+
{
|
831
|
+
DeleteRequest: {
|
832
|
+
Key: { pk: 'test_item#foo2', sk: '#meta' },
|
833
|
+
},
|
834
|
+
},
|
835
|
+
],
|
836
|
+
},
|
837
|
+
});
|
838
|
+
ddbClientMock.on(BatchWriteCommand, input2).resolves({
|
839
|
+
$metadata: {
|
840
|
+
httpStatusCode: 200,
|
841
|
+
},
|
842
|
+
UnprocessedItems: {},
|
843
|
+
});
|
844
|
+
|
845
|
+
const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
|
846
|
+
await repository.deleteItems(requestItems);
|
847
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchWriteCommand, input1);
|
848
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchWriteCommand, input2);
|
849
|
+
});
|
850
|
+
|
851
|
+
it('should fail after max number of retries', async () => {
|
852
|
+
const input1: BatchWriteCommandInput = {
|
853
|
+
RequestItems: {
|
854
|
+
[TABLE_NAME]: [
|
855
|
+
{
|
856
|
+
DeleteRequest: {
|
857
|
+
Key: { pk: 'test_item#foo', sk: '#meta' },
|
858
|
+
},
|
859
|
+
},
|
860
|
+
{
|
861
|
+
DeleteRequest: {
|
862
|
+
Key: { pk: 'test_item#foo2', sk: '#meta' },
|
863
|
+
},
|
864
|
+
},
|
865
|
+
],
|
866
|
+
},
|
867
|
+
};
|
868
|
+
const input2: BatchWriteCommandInput = {
|
869
|
+
RequestItems: {
|
870
|
+
[TABLE_NAME]: [
|
871
|
+
{
|
872
|
+
DeleteRequest: {
|
873
|
+
Key: { pk: 'test_item#foo2', sk: '#meta' },
|
874
|
+
},
|
875
|
+
},
|
876
|
+
],
|
877
|
+
},
|
878
|
+
};
|
879
|
+
|
880
|
+
ddbClientMock.on(BatchWriteCommand).resolves({
|
881
|
+
$metadata: {
|
882
|
+
httpStatusCode: 200,
|
883
|
+
},
|
884
|
+
UnprocessedItems: {
|
885
|
+
[TABLE_NAME]: [
|
886
|
+
{
|
887
|
+
DeleteRequest: {
|
888
|
+
Key: { pk: 'test_item#foo2', sk: '#meta' },
|
889
|
+
},
|
890
|
+
},
|
891
|
+
],
|
892
|
+
},
|
893
|
+
});
|
894
|
+
|
895
|
+
const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
|
896
|
+
await expect(repository.deleteItems(requestItems)).rejects.toEqual(
|
897
|
+
new Error('Maximum allowed retries exceeded for unprocessed items'),
|
898
|
+
);
|
899
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchWriteCommand, input1);
|
900
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchWriteCommand, input2);
|
901
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(3, BatchWriteCommand, input2);
|
902
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(4, BatchWriteCommand, input2);
|
903
|
+
});
|
904
|
+
|
905
|
+
it('should request batchWrite in batch of 25 items to get result', async () => {
|
906
|
+
ddbClientMock.on(BatchWriteCommand).resolves({
|
907
|
+
$metadata: {
|
908
|
+
httpStatusCode: 200,
|
909
|
+
},
|
910
|
+
});
|
911
|
+
|
912
|
+
const requestItems = [];
|
913
|
+
for (let i = 0; i < 30; i++) {
|
914
|
+
requestItems.push({ name: `foo${i}` });
|
915
|
+
}
|
916
|
+
// keys for first batch request
|
917
|
+
const keys1 = [];
|
918
|
+
for (let i = 0; i < 25; i++) {
|
919
|
+
keys1.push({ pk: `test_item#foo${i}`, sk: '#meta' });
|
920
|
+
}
|
921
|
+
// keys for second batch request
|
922
|
+
const keys2 = [];
|
923
|
+
for (let i = 25; i < 30; i++) {
|
924
|
+
keys2.push({ pk: `test_item#foo${i}`, sk: '#meta' });
|
925
|
+
}
|
926
|
+
|
927
|
+
const input1: BatchWriteCommandInput = {
|
928
|
+
RequestItems: {
|
929
|
+
[TABLE_NAME]: keys1.map((key) => {
|
930
|
+
return {
|
931
|
+
DeleteRequest: {
|
932
|
+
Key: key,
|
933
|
+
},
|
934
|
+
};
|
935
|
+
}),
|
936
|
+
},
|
937
|
+
};
|
938
|
+
const input2: BatchWriteCommandInput = {
|
939
|
+
RequestItems: {
|
940
|
+
[TABLE_NAME]: keys2.map((key) => {
|
941
|
+
return {
|
942
|
+
DeleteRequest: {
|
943
|
+
Key: key,
|
944
|
+
},
|
945
|
+
};
|
946
|
+
}),
|
947
|
+
},
|
948
|
+
};
|
949
|
+
|
950
|
+
await repository.deleteItems(requestItems);
|
951
|
+
expect(ddbClientMock).toHaveReceivedCommandTimes(BatchWriteCommand, 2);
|
952
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchWriteCommand, input1);
|
953
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchWriteCommand, input2);
|
954
|
+
});
|
955
|
+
|
956
|
+
it('should throw error if any input item does not includes key field(s)', async () => {
|
957
|
+
const requestItems = [{ name: 'foo' }, { age: 22 }];
|
958
|
+
await expect(repository.deleteItems(requestItems)).rejects.toEqual(
|
959
|
+
new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
|
960
|
+
);
|
961
|
+
});
|
962
|
+
});
|
963
|
+
|
476
964
|
describe('queryItems()', () => {
|
477
965
|
it('should return the items if found', async () => {
|
478
966
|
ddbClientMock.on(QueryCommand).resolves({
|
@@ -555,8 +1043,7 @@ describe('AbstractRepository', () => {
|
|
555
1043
|
};
|
556
1044
|
|
557
1045
|
const partialItem = { name: 'foo' };
|
558
|
-
const
|
559
|
-
const _result = await repository.queryItems(partialItem, useSortKey);
|
1046
|
+
const _result = await repository.queryItems(partialItem, { useSortKey: true });
|
560
1047
|
expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
|
561
1048
|
});
|
562
1049
|
|
@@ -595,7 +1082,7 @@ describe('AbstractRepository', () => {
|
|
595
1082
|
|
596
1083
|
const partialItem = { country: 'au' };
|
597
1084
|
const useSortKey = false;
|
598
|
-
const result = await repository.queryItems(partialItem, useSortKey, index);
|
1085
|
+
const result = await repository.queryItems(partialItem, { useSortKey, index });
|
599
1086
|
expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
|
600
1087
|
expect(result).toEqual([
|
601
1088
|
new TestItem({
|
@@ -636,14 +1123,134 @@ describe('AbstractRepository', () => {
|
|
636
1123
|
|
637
1124
|
const partialItem = { age: 99, country: 'au' };
|
638
1125
|
const useSortKey = true;
|
639
|
-
const _result = await repository.queryItems(partialItem, useSortKey, index);
|
1126
|
+
const _result = await repository.queryItems(partialItem, { useSortKey, index });
|
1127
|
+
expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
|
1128
|
+
});
|
1129
|
+
|
1130
|
+
it('should set input query correctly when "filter - begins_with" query option is set', async () => {
|
1131
|
+
ddbClientMock.on(QueryCommand).resolves({
|
1132
|
+
$metadata: {
|
1133
|
+
httpStatusCode: 200,
|
1134
|
+
},
|
1135
|
+
});
|
1136
|
+
const input: QueryCommandInput = {
|
1137
|
+
TableName: TABLE_NAME,
|
1138
|
+
KeyConditionExpression: '#pkName = :pkValue AND begins_with(#skName, :skValue)',
|
1139
|
+
ExpressionAttributeNames: {
|
1140
|
+
'#pkName': 'pk',
|
1141
|
+
'#skName': 'sk',
|
1142
|
+
},
|
1143
|
+
ExpressionAttributeValues: {
|
1144
|
+
':pkValue': 'test_item#foo',
|
1145
|
+
':skValue': 'keyword-x',
|
1146
|
+
},
|
1147
|
+
};
|
1148
|
+
|
1149
|
+
const partialItem = { name: 'foo' };
|
1150
|
+
const queryOptions: QueryOptions = {
|
1151
|
+
filter: {
|
1152
|
+
type: 'begins_with',
|
1153
|
+
keyword: 'keyword-x',
|
1154
|
+
},
|
1155
|
+
};
|
1156
|
+
await repository.queryItems(partialItem, queryOptions);
|
1157
|
+
expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
|
1158
|
+
});
|
1159
|
+
|
1160
|
+
it('should throw error invalid "filter" query option is set', async () => {
|
1161
|
+
ddbClientMock.on(QueryCommand).resolves({
|
1162
|
+
$metadata: {
|
1163
|
+
httpStatusCode: 200,
|
1164
|
+
},
|
1165
|
+
});
|
1166
|
+
const partialItem = { name: 'foo' };
|
1167
|
+
const queryOptions = {
|
1168
|
+
filter: {
|
1169
|
+
type: 'invalid-type',
|
1170
|
+
keyword: 'keyword-x',
|
1171
|
+
},
|
1172
|
+
} as unknown as QueryOptions;
|
1173
|
+
await expect(repository.queryItems(partialItem, queryOptions)).rejects.toEqual(
|
1174
|
+
new Error(`Invalid query filter type: invalid-type`),
|
1175
|
+
);
|
1176
|
+
});
|
1177
|
+
|
1178
|
+
it('should set input query correctly when "limit" query option is set', async () => {
|
1179
|
+
ddbClientMock.on(QueryCommand).resolves({
|
1180
|
+
$metadata: {
|
1181
|
+
httpStatusCode: 200,
|
1182
|
+
},
|
1183
|
+
});
|
1184
|
+
const input: QueryCommandInput = {
|
1185
|
+
TableName: TABLE_NAME,
|
1186
|
+
KeyConditionExpression: '#pkName = :pkValue',
|
1187
|
+
ExpressionAttributeNames: {
|
1188
|
+
'#pkName': 'pk',
|
1189
|
+
},
|
1190
|
+
ExpressionAttributeValues: {
|
1191
|
+
':pkValue': 'test_item#foo',
|
1192
|
+
},
|
1193
|
+
Limit: 33,
|
1194
|
+
};
|
1195
|
+
|
1196
|
+
const partialItem = { name: 'foo' };
|
1197
|
+
const queryOptions = { limit: 33 };
|
1198
|
+
await repository.queryItems(partialItem, queryOptions);
|
1199
|
+
expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
|
1200
|
+
});
|
1201
|
+
|
1202
|
+
it('should set input query correctly when "order asc" query option is set', async () => {
|
1203
|
+
ddbClientMock.on(QueryCommand).resolves({
|
1204
|
+
$metadata: {
|
1205
|
+
httpStatusCode: 200,
|
1206
|
+
},
|
1207
|
+
});
|
1208
|
+
const input: QueryCommandInput = {
|
1209
|
+
TableName: TABLE_NAME,
|
1210
|
+
KeyConditionExpression: '#pkName = :pkValue',
|
1211
|
+
ExpressionAttributeNames: {
|
1212
|
+
'#pkName': 'pk',
|
1213
|
+
},
|
1214
|
+
ExpressionAttributeValues: {
|
1215
|
+
':pkValue': 'test_item#foo',
|
1216
|
+
},
|
1217
|
+
ScanIndexForward: true,
|
1218
|
+
};
|
1219
|
+
|
1220
|
+
const partialItem = { name: 'foo' };
|
1221
|
+
const queryOptions = { order: 'asc' } as unknown as QueryOptions;
|
1222
|
+
await repository.queryItems(partialItem, queryOptions);
|
1223
|
+
expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
|
1224
|
+
});
|
1225
|
+
|
1226
|
+
it('should set input query correctly when "order desc" query option is set', async () => {
|
1227
|
+
ddbClientMock.on(QueryCommand).resolves({
|
1228
|
+
$metadata: {
|
1229
|
+
httpStatusCode: 200,
|
1230
|
+
},
|
1231
|
+
});
|
1232
|
+
const input: QueryCommandInput = {
|
1233
|
+
TableName: TABLE_NAME,
|
1234
|
+
KeyConditionExpression: '#pkName = :pkValue',
|
1235
|
+
ExpressionAttributeNames: {
|
1236
|
+
'#pkName': 'pk',
|
1237
|
+
},
|
1238
|
+
ExpressionAttributeValues: {
|
1239
|
+
':pkValue': 'test_item#foo',
|
1240
|
+
},
|
1241
|
+
ScanIndexForward: false,
|
1242
|
+
};
|
1243
|
+
|
1244
|
+
const partialItem = { name: 'foo' };
|
1245
|
+
const queryOptions = { order: 'desc' } as unknown as QueryOptions;
|
1246
|
+
await repository.queryItems(partialItem, queryOptions);
|
640
1247
|
expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
|
641
1248
|
});
|
642
1249
|
|
643
1250
|
it('should throw error for invalid index query', async () => {
|
644
1251
|
const index = 'undefined-index';
|
645
1252
|
const partialItem = { age: 99, country: 'au' };
|
646
|
-
await expect(repository.queryItems(partialItem,
|
1253
|
+
await expect(repository.queryItems(partialItem, { index })).rejects.toEqual(
|
647
1254
|
new MissingKeyValuesError(`Table index '${index}' not defined on entity test-item-entity`),
|
648
1255
|
);
|
649
1256
|
});
|
@@ -659,7 +1266,7 @@ describe('AbstractRepository', () => {
|
|
659
1266
|
const partialItem = { name: 'foo' };
|
660
1267
|
const useSortKey = false;
|
661
1268
|
const index = 'gsi1_pk-gsi1_sk-index';
|
662
|
-
await expect(repository.queryItems(partialItem, useSortKey, index)).rejects.toEqual(
|
1269
|
+
await expect(repository.queryItems(partialItem, { useSortKey, index })).rejects.toEqual(
|
663
1270
|
new MissingKeyValuesError(`Key field "country" must be specified in the input item in entity test-item-entity`),
|
664
1271
|
);
|
665
1272
|
});
|
@@ -668,7 +1275,7 @@ describe('AbstractRepository', () => {
|
|
668
1275
|
const partialItem = { country: 'au' };
|
669
1276
|
const useSortKey = true;
|
670
1277
|
const index = 'gsi1_pk-gsi1_sk-index';
|
671
|
-
await expect(repository.queryItems(partialItem, useSortKey, index)).rejects.toEqual(
|
1278
|
+
await expect(repository.queryItems(partialItem, { useSortKey, index })).rejects.toEqual(
|
672
1279
|
new MissingKeyValuesError(`Key field "age" must be specified in the input item in entity test-item-entity`),
|
673
1280
|
);
|
674
1281
|
});
|
@@ -752,8 +1359,6 @@ describe('AbstractRepository', () => {
|
|
752
1359
|
await repository.updateItem(
|
753
1360
|
{
|
754
1361
|
name: 'foo2',
|
755
|
-
},
|
756
|
-
{
|
757
1362
|
age: 55,
|
758
1363
|
},
|
759
1364
|
transaction,
|