@squiz/db-lib 1.77.0 → 1.77.2
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 +12 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +7 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.js +29 -5
- package/lib/dynamodb/AbstractDynamoDbRepository.js.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js +162 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -1
- package/lib/externalized/ExternalizedDynamoDbRepository.d.ts +15 -0
- package/lib/externalized/ExternalizedDynamoDbRepository.d.ts.map +1 -1
- package/lib/externalized/ExternalizedDynamoDbRepository.js +93 -12
- package/lib/externalized/ExternalizedDynamoDbRepository.js.map +1 -1
- package/lib/externalized/ExternalizedDynamoDbRepository.spec.js +372 -133
- package/lib/externalized/ExternalizedDynamoDbRepository.spec.js.map +1 -1
- package/package.json +1 -1
- package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +179 -1
- package/src/dynamodb/AbstractDynamoDbRepository.ts +39 -5
- package/src/externalized/ExternalizedDynamoDbRepository.spec.ts +453 -151
- package/src/externalized/ExternalizedDynamoDbRepository.ts +104 -12
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1465,7 +1465,11 @@ describe('AbstractRepository', () => {
|
|
|
1465
1465
|
httpStatusCode: 200,
|
|
1466
1466
|
},
|
|
1467
1467
|
});
|
|
1468
|
+
|
|
1469
|
+
// Limit does not appear in the input query.
|
|
1470
|
+
// Splitting/pagination is now handled in the queryFullPartition sub function.
|
|
1468
1471
|
const input: QueryCommandInput = {
|
|
1472
|
+
ExclusiveStartKey: undefined,
|
|
1469
1473
|
TableName: TABLE_NAME,
|
|
1470
1474
|
KeyConditionExpression: '#pkName = :pkValue',
|
|
1471
1475
|
ExpressionAttributeNames: {
|
|
@@ -1474,7 +1478,6 @@ describe('AbstractRepository', () => {
|
|
|
1474
1478
|
ExpressionAttributeValues: {
|
|
1475
1479
|
':pkValue': 'test_item#foo',
|
|
1476
1480
|
},
|
|
1477
|
-
Limit: 33,
|
|
1478
1481
|
};
|
|
1479
1482
|
|
|
1480
1483
|
const partialItem = { name: 'foo' };
|
|
@@ -1563,6 +1566,181 @@ describe('AbstractRepository', () => {
|
|
|
1563
1566
|
new MissingKeyValuesError(`Key field "age" must be specified in the input item in entity test-item-entity`),
|
|
1564
1567
|
);
|
|
1565
1568
|
});
|
|
1569
|
+
|
|
1570
|
+
describe('dynamo pagination', () => {
|
|
1571
|
+
const mockItems = [
|
|
1572
|
+
{
|
|
1573
|
+
name: 'foo',
|
|
1574
|
+
age: 10,
|
|
1575
|
+
country: 'au',
|
|
1576
|
+
data: {},
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
name: 'fox',
|
|
1580
|
+
age: 11,
|
|
1581
|
+
country: 'au',
|
|
1582
|
+
data: {},
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
name: 'bar',
|
|
1586
|
+
age: 12,
|
|
1587
|
+
country: 'au',
|
|
1588
|
+
data: {},
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
name: 'baz',
|
|
1592
|
+
age: 13,
|
|
1593
|
+
country: 'au',
|
|
1594
|
+
data: {},
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
name: 'qux',
|
|
1598
|
+
age: 14,
|
|
1599
|
+
country: 'au',
|
|
1600
|
+
data: {},
|
|
1601
|
+
},
|
|
1602
|
+
];
|
|
1603
|
+
|
|
1604
|
+
it('should return all items when there is only 1 page of results', async () => {
|
|
1605
|
+
ddbClientMock.on(QueryCommand).resolves({
|
|
1606
|
+
Items: [mockItems[0], mockItems[1], mockItems[2]],
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
const input: QueryCommandInput = {
|
|
1610
|
+
TableName: TABLE_NAME,
|
|
1611
|
+
KeyConditionExpression: '#pkName = :pkValue',
|
|
1612
|
+
ExpressionAttributeNames: {
|
|
1613
|
+
'#pkName': 'pk',
|
|
1614
|
+
},
|
|
1615
|
+
ExpressionAttributeValues: {
|
|
1616
|
+
':pkValue': 'test_item#foo',
|
|
1617
|
+
},
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
const partialItem = { name: 'foo' };
|
|
1621
|
+
const result = await repository.queryItems(partialItem);
|
|
1622
|
+
expect(ddbClientMock).toHaveReceivedCommandTimes(QueryCommand, 1);
|
|
1623
|
+
expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
|
|
1624
|
+
expect(result).toEqual([new TestItem(mockItems[0]), new TestItem(mockItems[1]), new TestItem(mockItems[2])]);
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
it('should return all items when there are more than 1 page of results', async () => {
|
|
1628
|
+
const mockLastEvaluatedKey = { pk: { S: 'test_item#foo' }, sk: { S: 'name#bar' } };
|
|
1629
|
+
|
|
1630
|
+
ddbClientMock
|
|
1631
|
+
.on(QueryCommand)
|
|
1632
|
+
.resolvesOnce({
|
|
1633
|
+
Items: [mockItems[0], mockItems[1], mockItems[2]],
|
|
1634
|
+
LastEvaluatedKey: mockLastEvaluatedKey,
|
|
1635
|
+
})
|
|
1636
|
+
.resolvesOnce({
|
|
1637
|
+
Items: [mockItems[3], mockItems[4]],
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
const input: QueryCommandInput = {
|
|
1641
|
+
TableName: TABLE_NAME,
|
|
1642
|
+
KeyConditionExpression: '#pkName = :pkValue',
|
|
1643
|
+
ExpressionAttributeNames: {
|
|
1644
|
+
'#pkName': 'pk',
|
|
1645
|
+
},
|
|
1646
|
+
ExpressionAttributeValues: {
|
|
1647
|
+
':pkValue': 'test_item#foo',
|
|
1648
|
+
},
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
const partialItem = { name: 'foo' };
|
|
1652
|
+
const result = await repository.queryItems(partialItem);
|
|
1653
|
+
expect(ddbClientMock).toHaveReceivedCommandTimes(QueryCommand, 2);
|
|
1654
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(1, QueryCommand, input);
|
|
1655
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(2, QueryCommand, {
|
|
1656
|
+
...input,
|
|
1657
|
+
ExclusiveStartKey: mockLastEvaluatedKey,
|
|
1658
|
+
});
|
|
1659
|
+
expect(result).toEqual([
|
|
1660
|
+
new TestItem(mockItems[0]),
|
|
1661
|
+
new TestItem(mockItems[1]),
|
|
1662
|
+
new TestItem(mockItems[2]),
|
|
1663
|
+
new TestItem(mockItems[3]),
|
|
1664
|
+
new TestItem(mockItems[4]),
|
|
1665
|
+
]);
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
it('should return items but only up to the provided limit when limit is within first page', async () => {
|
|
1669
|
+
const mockLimit = 2;
|
|
1670
|
+
const mockLastEvaluatedKey = { pk: { S: 'test_item#foo' }, sk: { S: 'name#bar' } };
|
|
1671
|
+
|
|
1672
|
+
ddbClientMock
|
|
1673
|
+
.on(QueryCommand)
|
|
1674
|
+
.resolvesOnce({
|
|
1675
|
+
Items: [mockItems[0], mockItems[1], mockItems[2]],
|
|
1676
|
+
LastEvaluatedKey: mockLastEvaluatedKey,
|
|
1677
|
+
})
|
|
1678
|
+
.resolvesOnce({
|
|
1679
|
+
Items: [mockItems[3], mockItems[4]],
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
const input: QueryCommandInput = {
|
|
1683
|
+
TableName: TABLE_NAME,
|
|
1684
|
+
KeyConditionExpression: '#pkName = :pkValue',
|
|
1685
|
+
ExpressionAttributeNames: {
|
|
1686
|
+
'#pkName': 'pk',
|
|
1687
|
+
},
|
|
1688
|
+
ExpressionAttributeValues: {
|
|
1689
|
+
':pkValue': 'test_item#foo',
|
|
1690
|
+
},
|
|
1691
|
+
};
|
|
1692
|
+
|
|
1693
|
+
// Only expect first two items from the first page of results.
|
|
1694
|
+
const partialItem = { name: 'foo' };
|
|
1695
|
+
const result = await repository.queryItems(partialItem, { limit: mockLimit });
|
|
1696
|
+
expect(ddbClientMock).toHaveReceivedCommandTimes(QueryCommand, 1);
|
|
1697
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(1, QueryCommand, input);
|
|
1698
|
+
expect(result).toEqual([new TestItem(mockItems[0]), new TestItem(mockItems[1])]);
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
it('should return items up to the limit when limit spans multiple pages', async () => {
|
|
1702
|
+
const mockLimit = 4;
|
|
1703
|
+
const mockLastEvaluatedKey = { pk: { S: 'test_item#foo' }, sk: { S: 'name#bar' } };
|
|
1704
|
+
|
|
1705
|
+
ddbClientMock
|
|
1706
|
+
.on(QueryCommand)
|
|
1707
|
+
.resolvesOnce({
|
|
1708
|
+
Items: [mockItems[0], mockItems[1], mockItems[2]],
|
|
1709
|
+
LastEvaluatedKey: mockLastEvaluatedKey,
|
|
1710
|
+
})
|
|
1711
|
+
.resolvesOnce({
|
|
1712
|
+
Items: [mockItems[3], mockItems[4]],
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
const input: QueryCommandInput = {
|
|
1716
|
+
TableName: TABLE_NAME,
|
|
1717
|
+
KeyConditionExpression: '#pkName = :pkValue',
|
|
1718
|
+
ExpressionAttributeNames: {
|
|
1719
|
+
'#pkName': 'pk',
|
|
1720
|
+
},
|
|
1721
|
+
ExpressionAttributeValues: {
|
|
1722
|
+
':pkValue': 'test_item#foo',
|
|
1723
|
+
},
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
// Expect only 4 results in total across both pages.
|
|
1727
|
+
// Expect all 3 results from first page, but only 1 result from second page.
|
|
1728
|
+
const partialItem = { name: 'foo' };
|
|
1729
|
+
const result = await repository.queryItems(partialItem, { limit: mockLimit });
|
|
1730
|
+
expect(ddbClientMock).toHaveReceivedCommandTimes(QueryCommand, 2);
|
|
1731
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(1, QueryCommand, input);
|
|
1732
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(2, QueryCommand, {
|
|
1733
|
+
...input,
|
|
1734
|
+
ExclusiveStartKey: mockLastEvaluatedKey,
|
|
1735
|
+
});
|
|
1736
|
+
expect(result).toEqual([
|
|
1737
|
+
new TestItem(mockItems[0]),
|
|
1738
|
+
new TestItem(mockItems[1]),
|
|
1739
|
+
new TestItem(mockItems[2]),
|
|
1740
|
+
new TestItem(mockItems[3]),
|
|
1741
|
+
]);
|
|
1742
|
+
});
|
|
1743
|
+
});
|
|
1566
1744
|
});
|
|
1567
1745
|
|
|
1568
1746
|
describe('deleteItem()', () => {
|
|
@@ -304,12 +304,8 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
|
304
304
|
if (options?.order !== undefined) {
|
|
305
305
|
queryCommandInput.ScanIndexForward = options.order === 'asc';
|
|
306
306
|
}
|
|
307
|
-
if (options?.limit !== undefined) {
|
|
308
|
-
queryCommandInput.Limit = options.limit;
|
|
309
|
-
}
|
|
310
307
|
|
|
311
|
-
|
|
312
|
-
return !output.Items ? [] : output.Items.map((item) => this.hydrateItem(item));
|
|
308
|
+
return await this.queryFullPartition(queryCommandInput, options?.limit);
|
|
313
309
|
}
|
|
314
310
|
|
|
315
311
|
/**
|
|
@@ -326,6 +322,44 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
|
326
322
|
throw new Error(`Invalid query filter type: ${(filter as any)?.type}`);
|
|
327
323
|
}
|
|
328
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Get the list of objects by given params, paginating until all results are returned for a full partition.
|
|
327
|
+
* @param params - The DynamoDB query params.
|
|
328
|
+
* @param limit - The maximum number of items to return.
|
|
329
|
+
* @returns Promise of array of items.
|
|
330
|
+
*/
|
|
331
|
+
private async queryFullPartition(params: QueryCommandInput, limit?: number): Promise<DATA_CLASS[]> {
|
|
332
|
+
const items: Array<DATA_CLASS> = [];
|
|
333
|
+
let exclusiveStartKey: QueryCommandInput['ExclusiveStartKey'] = undefined;
|
|
334
|
+
|
|
335
|
+
do {
|
|
336
|
+
const queryInput: QueryCommandInput = {
|
|
337
|
+
...params,
|
|
338
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const result = await this.client.query(queryInput);
|
|
342
|
+
|
|
343
|
+
exclusiveStartKey = result.LastEvaluatedKey;
|
|
344
|
+
|
|
345
|
+
if (result.Items && result.Items.length) {
|
|
346
|
+
const resultItems = result.Items.map((item) => this.hydrateItem(item));
|
|
347
|
+
|
|
348
|
+
const itemsToAdd = limit === undefined ? resultItems : resultItems.slice(0, limit - items.length);
|
|
349
|
+
|
|
350
|
+
items.push(...itemsToAdd);
|
|
351
|
+
|
|
352
|
+
const hasReachedLimit = limit !== undefined && items.length >= limit;
|
|
353
|
+
|
|
354
|
+
if (hasReachedLimit) {
|
|
355
|
+
exclusiveStartKey = undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} while (exclusiveStartKey);
|
|
359
|
+
|
|
360
|
+
return items;
|
|
361
|
+
}
|
|
362
|
+
|
|
329
363
|
/**
|
|
330
364
|
* Update the existing item matching the key fields value
|
|
331
365
|
* in the passed newValue
|