@squiz/db-lib 1.71.3 → 1.73.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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(partialItemWithKeyFields, 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 undefined if item does does not exist', async () => {
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
- const partialItemWithKeyFields = {
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(partialItemWithKeyFields, 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(partialItemWithKeyFields, 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
- repository.updateItem(partialItemWithKeyFields, updateItem as unknown as Partial<TestItem>),
334
- ).rejects.toEqual(new Error('Invalid "country"'));
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
- repository.updateItem(partialItemWithKeyFields, updateItem as unknown as Partial<TestItem>),
359
- ).rejects.toEqual(new InvalidDbSchemaError('Excess properties in entity test-item-entity: extra'));
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(partialItemWithKeyFields, updateItem)).rejects.toEqual(
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 useSortKey = true;
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, false, index)).rejects.toEqual(
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,