@squiz/db-lib 1.71.3 → 1.72.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,8 @@ import {
14
14
  UpdateCommandInput,
15
15
  TransactWriteCommand,
16
16
  TransactWriteCommandInput,
17
+ BatchGetCommand,
18
+ BatchGetCommandInput,
17
19
  } from '@aws-sdk/lib-dynamodb';
18
20
  import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
19
21
  import { DynamoDbManager, Transaction } from './DynamoDbManager';
@@ -245,13 +247,11 @@ describe('AbstractRepository', () => {
245
247
  ConditionExpression: `attribute_exists(pk)`,
246
248
  };
247
249
 
248
- const partialItemWithKeyFields = {
249
- name: 'foo',
250
- };
251
250
  const updateItem = {
251
+ name: 'foo',
252
252
  country: 'au-updated',
253
253
  };
254
- const result = await repository.updateItem(partialItemWithKeyFields, updateItem);
254
+ const result = await repository.updateItem(updateItem);
255
255
  expect(ddbClientMock).toHaveReceivedNthCommandWith(2, UpdateCommand, input);
256
256
  expect(result).toEqual(
257
257
  new TestItem({
@@ -263,20 +263,49 @@ describe('AbstractRepository', () => {
263
263
  );
264
264
  });
265
265
 
266
- it('should undefined if item does does not exist', async () => {
266
+ it('should not trigger update request if the input attributes are same as in the existing item', async () => {
267
267
  ddbClientMock.on(GetCommand).resolves({
268
268
  $metadata: {
269
269
  httpStatusCode: 200,
270
270
  },
271
+ Item: {
272
+ name: 'foo',
273
+ age: 99,
274
+ country: 'au',
275
+ data: {},
276
+ },
271
277
  });
278
+ ddbClientMock.on(UpdateCommand).rejects(new Error('updateItem() called when not expected'));
272
279
 
273
- const partialItemWithKeyFields = {
280
+ // update input attributes are same as in the existing item
281
+ const updateItem = {
274
282
  name: 'foo',
283
+ country: 'au',
275
284
  };
285
+
286
+ const result = await repository.updateItem(updateItem);
287
+ expect(result).toEqual(
288
+ new TestItem({
289
+ name: 'foo',
290
+ age: 99,
291
+ country: 'au',
292
+ data: {},
293
+ }),
294
+ );
295
+ });
296
+
297
+ it('should return undefined if item does does not exist', async () => {
298
+ ddbClientMock.on(GetCommand).resolves({
299
+ $metadata: {
300
+ httpStatusCode: 200,
301
+ },
302
+ });
303
+
276
304
  const updateItem = {
305
+ name: 'foo',
277
306
  country: 'au-updated',
278
307
  };
279
- const result = await repository.updateItem(partialItemWithKeyFields, updateItem);
308
+ const result = await repository.updateItem(updateItem);
280
309
  expect(result).toEqual(undefined);
281
310
  });
282
311
 
@@ -300,13 +329,11 @@ describe('AbstractRepository', () => {
300
329
  }),
301
330
  );
302
331
 
303
- const partialItemWithKeyFields = {
304
- name: 'foo',
305
- };
306
332
  const updateItem = {
333
+ name: 'foo',
307
334
  country: 'au-updated',
308
335
  };
309
- const result = await repository.updateItem(partialItemWithKeyFields, updateItem);
336
+ const result = await repository.updateItem(updateItem);
310
337
  expect(result).toEqual(undefined);
311
338
  });
312
339
 
@@ -323,15 +350,13 @@ describe('AbstractRepository', () => {
323
350
  },
324
351
  });
325
352
 
326
- const partialItemWithKeyFields = {
327
- name: 'foo',
328
- };
329
353
  const updateItem = {
354
+ name: 'foo',
330
355
  country: 61, // should be "string" type
331
356
  };
332
- await expect(
333
- repository.updateItem(partialItemWithKeyFields, updateItem as unknown as Partial<TestItem>),
334
- ).rejects.toEqual(new Error('Invalid "country"'));
357
+ await expect(repository.updateItem(updateItem as unknown as Partial<TestItem>)).rejects.toEqual(
358
+ new Error('Invalid "country"'),
359
+ );
335
360
  });
336
361
 
337
362
  it('should throw error if excess column in input', async () => {
@@ -347,26 +372,22 @@ describe('AbstractRepository', () => {
347
372
  },
348
373
  });
349
374
 
350
- const partialItemWithKeyFields = {
351
- name: 'foo',
352
- };
353
375
  const updateItem = {
376
+ name: 'foo',
354
377
  country: 'au-updated',
355
378
  extra: '',
356
379
  };
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'));
380
+ await expect(repository.updateItem(updateItem as unknown as Partial<TestItem>)).rejects.toEqual(
381
+ new InvalidDbSchemaError('Excess properties in entity test-item-entity: extra'),
382
+ );
360
383
  });
361
384
 
362
385
  it('should throw error if input does not includes key field(s)', async () => {
363
- const partialItemWithKeyFields = {
364
- age: 99,
365
- };
366
386
  const updateItem = {
387
+ age: 99,
367
388
  country: 'au-updated', // should be "string" type
368
389
  };
369
- await expect(repository.updateItem(partialItemWithKeyFields, updateItem)).rejects.toEqual(
390
+ await expect(repository.updateItem(updateItem)).rejects.toEqual(
370
391
  new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
371
392
  );
372
393
  });
@@ -473,6 +494,120 @@ describe('AbstractRepository', () => {
473
494
  });
474
495
  });
475
496
 
497
+ describe('getItems()', () => {
498
+ it('should use BatchGetItem to get result', async () => {
499
+ ddbClientMock.on(BatchGetCommand).resolves({
500
+ $metadata: {
501
+ httpStatusCode: 200,
502
+ },
503
+ Responses: {
504
+ [TABLE_NAME]: [
505
+ {
506
+ name: 'foo',
507
+ age: 99,
508
+ country: 'au',
509
+ data: {},
510
+ data2: '{"foo":"bar","num":123}',
511
+ },
512
+ {
513
+ name: 'foo2',
514
+ age: 999,
515
+ country: 'au',
516
+ data: {},
517
+ data2: '{"foo":"bar","num":123}',
518
+ },
519
+ ],
520
+ },
521
+ });
522
+ const input: BatchGetCommandInput = {
523
+ RequestItems: {
524
+ [TABLE_NAME]: {
525
+ Keys: [
526
+ { pk: 'test_item#foo', sk: '#meta' },
527
+ { pk: 'test_item#foo2', sk: '#meta' },
528
+ ],
529
+ },
530
+ },
531
+ };
532
+
533
+ const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
534
+ const result = await repository.getItems(requestItems);
535
+ expect(ddbClientMock).toHaveReceivedCommandWith(BatchGetCommand, input);
536
+ expect(result).toEqual([
537
+ new TestItem({
538
+ name: 'foo',
539
+ age: 99,
540
+ country: 'au',
541
+ data: {},
542
+ data2: {
543
+ foo: 'bar',
544
+ num: 123,
545
+ },
546
+ }),
547
+ new TestItem({
548
+ name: 'foo2',
549
+ age: 999,
550
+ country: 'au',
551
+ data: {},
552
+ data2: {
553
+ foo: 'bar',
554
+ num: 123,
555
+ },
556
+ }),
557
+ ]);
558
+ });
559
+
560
+ it('should request BatchGetItem in batch of 100 items to get result', async () => {
561
+ ddbClientMock.on(BatchGetCommand).resolves({
562
+ $metadata: {
563
+ httpStatusCode: 200,
564
+ },
565
+ });
566
+
567
+ const requestItems = [];
568
+ for (let i = 0; i < 120; i++) {
569
+ requestItems.push({ name: `foo${i}` });
570
+ }
571
+ // keys for first batch request
572
+ const keys1 = [];
573
+ for (let i = 0; i < 100; i++) {
574
+ keys1.push({ pk: `test_item#foo${i}`, sk: '#meta' });
575
+ }
576
+ // keys for second batch request
577
+ const keys2 = [];
578
+ for (let i = 100; i < 120; i++) {
579
+ keys2.push({ pk: `test_item#foo${i}`, sk: '#meta' });
580
+ }
581
+
582
+ const input1: BatchGetCommandInput = {
583
+ RequestItems: {
584
+ [TABLE_NAME]: {
585
+ Keys: keys1,
586
+ },
587
+ },
588
+ };
589
+ const input2: BatchGetCommandInput = {
590
+ RequestItems: {
591
+ [TABLE_NAME]: {
592
+ Keys: keys2,
593
+ },
594
+ },
595
+ };
596
+
597
+ await repository.getItems(requestItems);
598
+ expect(ddbClientMock).toHaveReceivedCommandTimes(BatchGetCommand, 2);
599
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchGetCommand, input1);
600
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchGetCommand, input2);
601
+ });
602
+
603
+ it('should throw error if any input item does not includes key field(s)', async () => {
604
+ const requestItems = [{ name: 'foo' }, { age: 22 }];
605
+ await expect(repository.getItems(requestItems)).rejects.toEqual(
606
+ new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
607
+ );
608
+ });
609
+ });
610
+
476
611
  describe('queryItems()', () => {
477
612
  it('should return the items if found', async () => {
478
613
  ddbClientMock.on(QueryCommand).resolves({
@@ -555,8 +690,7 @@ describe('AbstractRepository', () => {
555
690
  };
556
691
 
557
692
  const partialItem = { name: 'foo' };
558
- const useSortKey = true;
559
- const _result = await repository.queryItems(partialItem, useSortKey);
693
+ const _result = await repository.queryItems(partialItem, { useSortKey: true });
560
694
  expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
561
695
  });
562
696
 
@@ -595,7 +729,7 @@ describe('AbstractRepository', () => {
595
729
 
596
730
  const partialItem = { country: 'au' };
597
731
  const useSortKey = false;
598
- const result = await repository.queryItems(partialItem, useSortKey, index);
732
+ const result = await repository.queryItems(partialItem, { useSortKey, index });
599
733
  expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
600
734
  expect(result).toEqual([
601
735
  new TestItem({
@@ -636,14 +770,134 @@ describe('AbstractRepository', () => {
636
770
 
637
771
  const partialItem = { age: 99, country: 'au' };
638
772
  const useSortKey = true;
639
- const _result = await repository.queryItems(partialItem, useSortKey, index);
773
+ const _result = await repository.queryItems(partialItem, { useSortKey, index });
774
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
775
+ });
776
+
777
+ it('should set input query correctly when "filter - begins_with" query option is set', async () => {
778
+ ddbClientMock.on(QueryCommand).resolves({
779
+ $metadata: {
780
+ httpStatusCode: 200,
781
+ },
782
+ });
783
+ const input: QueryCommandInput = {
784
+ TableName: TABLE_NAME,
785
+ KeyConditionExpression: '#pkName = :pkValue AND begins_with(#skName, :skValue)',
786
+ ExpressionAttributeNames: {
787
+ '#pkName': 'pk',
788
+ '#skName': 'sk',
789
+ },
790
+ ExpressionAttributeValues: {
791
+ ':pkValue': 'test_item#foo',
792
+ ':skValue': 'keyword-x',
793
+ },
794
+ };
795
+
796
+ const partialItem = { name: 'foo' };
797
+ const queryOptions: QueryOptions = {
798
+ filter: {
799
+ type: 'begins_with',
800
+ keyword: 'keyword-x',
801
+ },
802
+ };
803
+ await repository.queryItems(partialItem, queryOptions);
804
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
805
+ });
806
+
807
+ it('should throw error invalid "filter" query option is set', async () => {
808
+ ddbClientMock.on(QueryCommand).resolves({
809
+ $metadata: {
810
+ httpStatusCode: 200,
811
+ },
812
+ });
813
+ const partialItem = { name: 'foo' };
814
+ const queryOptions = {
815
+ filter: {
816
+ type: 'invalid-type',
817
+ keyword: 'keyword-x',
818
+ },
819
+ } as unknown as QueryOptions;
820
+ await expect(repository.queryItems(partialItem, queryOptions)).rejects.toEqual(
821
+ new Error(`Invalid query filter type: invalid-type`),
822
+ );
823
+ });
824
+
825
+ it('should set input query correctly when "limit" query option is set', async () => {
826
+ ddbClientMock.on(QueryCommand).resolves({
827
+ $metadata: {
828
+ httpStatusCode: 200,
829
+ },
830
+ });
831
+ const input: QueryCommandInput = {
832
+ TableName: TABLE_NAME,
833
+ KeyConditionExpression: '#pkName = :pkValue',
834
+ ExpressionAttributeNames: {
835
+ '#pkName': 'pk',
836
+ },
837
+ ExpressionAttributeValues: {
838
+ ':pkValue': 'test_item#foo',
839
+ },
840
+ Limit: 33,
841
+ };
842
+
843
+ const partialItem = { name: 'foo' };
844
+ const queryOptions = { limit: 33 };
845
+ await repository.queryItems(partialItem, queryOptions);
846
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
847
+ });
848
+
849
+ it('should set input query correctly when "order asc" query option is set', async () => {
850
+ ddbClientMock.on(QueryCommand).resolves({
851
+ $metadata: {
852
+ httpStatusCode: 200,
853
+ },
854
+ });
855
+ const input: QueryCommandInput = {
856
+ TableName: TABLE_NAME,
857
+ KeyConditionExpression: '#pkName = :pkValue',
858
+ ExpressionAttributeNames: {
859
+ '#pkName': 'pk',
860
+ },
861
+ ExpressionAttributeValues: {
862
+ ':pkValue': 'test_item#foo',
863
+ },
864
+ ScanIndexForward: true,
865
+ };
866
+
867
+ const partialItem = { name: 'foo' };
868
+ const queryOptions = { order: 'asc' } as unknown as QueryOptions;
869
+ await repository.queryItems(partialItem, queryOptions);
870
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
871
+ });
872
+
873
+ it('should set input query correctly when "order desc" query option is set', async () => {
874
+ ddbClientMock.on(QueryCommand).resolves({
875
+ $metadata: {
876
+ httpStatusCode: 200,
877
+ },
878
+ });
879
+ const input: QueryCommandInput = {
880
+ TableName: TABLE_NAME,
881
+ KeyConditionExpression: '#pkName = :pkValue',
882
+ ExpressionAttributeNames: {
883
+ '#pkName': 'pk',
884
+ },
885
+ ExpressionAttributeValues: {
886
+ ':pkValue': 'test_item#foo',
887
+ },
888
+ ScanIndexForward: false,
889
+ };
890
+
891
+ const partialItem = { name: 'foo' };
892
+ const queryOptions = { order: 'desc' } as unknown as QueryOptions;
893
+ await repository.queryItems(partialItem, queryOptions);
640
894
  expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
641
895
  });
642
896
 
643
897
  it('should throw error for invalid index query', async () => {
644
898
  const index = 'undefined-index';
645
899
  const partialItem = { age: 99, country: 'au' };
646
- await expect(repository.queryItems(partialItem, false, index)).rejects.toEqual(
900
+ await expect(repository.queryItems(partialItem, { index })).rejects.toEqual(
647
901
  new MissingKeyValuesError(`Table index '${index}' not defined on entity test-item-entity`),
648
902
  );
649
903
  });
@@ -659,7 +913,7 @@ describe('AbstractRepository', () => {
659
913
  const partialItem = { name: 'foo' };
660
914
  const useSortKey = false;
661
915
  const index = 'gsi1_pk-gsi1_sk-index';
662
- await expect(repository.queryItems(partialItem, useSortKey, index)).rejects.toEqual(
916
+ await expect(repository.queryItems(partialItem, { useSortKey, index })).rejects.toEqual(
663
917
  new MissingKeyValuesError(`Key field "country" must be specified in the input item in entity test-item-entity`),
664
918
  );
665
919
  });
@@ -668,7 +922,7 @@ describe('AbstractRepository', () => {
668
922
  const partialItem = { country: 'au' };
669
923
  const useSortKey = true;
670
924
  const index = 'gsi1_pk-gsi1_sk-index';
671
- await expect(repository.queryItems(partialItem, useSortKey, index)).rejects.toEqual(
925
+ await expect(repository.queryItems(partialItem, { useSortKey, index })).rejects.toEqual(
672
926
  new MissingKeyValuesError(`Key field "age" must be specified in the input item in entity test-item-entity`),
673
927
  );
674
928
  });
@@ -752,8 +1006,6 @@ describe('AbstractRepository', () => {
752
1006
  await repository.updateItem(
753
1007
  {
754
1008
  name: 'foo2',
755
- },
756
- {
757
1009
  age: 55,
758
1010
  },
759
1011
  transaction,