@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.
- package/CHANGELOG.md +11 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +49 -9
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.js +108 -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 +257 -31
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -1
- package/package.json +2 -2
- package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +289 -37
- package/src/dynamodb/AbstractDynamoDbRepository.ts +140 -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,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(
|
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
|
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
|
-
|
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(
|
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(
|
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
|
-
|
334
|
-
)
|
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
|
-
|
359
|
-
)
|
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(
|
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
|
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,
|
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,
|