@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.
@@ -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,