Skip to content

Commit 048ea01

Browse files
committed
feat(batch-get.request): handle UnprocessedItems
BREAKING CHANGE: method `forModel` now takes Partial<T> instead of string or object with PartitionKey+SortKey
1 parent 5826b7f commit 048ea01

File tree

4 files changed

+226
-109
lines changed

4 files changed

+226
-109
lines changed

src/dynamo/batchget/batch-get-full.response.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface BatchGetFullResponse {
66
/**
77
* A map of table name to a list of items. Each object in Responses consists of a table name, along with a map of attribute data consisting of the data type and attribute value.
88
*/
9-
Responses?: BatchGetResponse
9+
Responses: BatchGetResponse
1010
/**
1111
* A map of tables and their respective keys that were not processed with the current response. The UnprocessedKeys value is in the same form as RequestItems, so the value can be provided directly to a subsequent BatchGetItem operation. For more information, see RequestItems in the Request Parameters section. Each element consists of: Keys - An array of primary key attribute values that define specific items in the table. ProjectionExpression - One or more attributes to be retrieved from the table or index. By default, all attributes are returned. If a requested attribute is not found, it does not appear in the result. ConsistentRead - The consistency of a read operation. If set to true, then a strongly consistent read is used; otherwise, an eventually consistent read is used. If there are no unprocessed keys remaining, the response contains an empty UnprocessedKeys map.
1212
*/

src/dynamo/batchget/batch-get.request.spec.ts

Lines changed: 126 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1+
// tslint:disable:no-non-null-assertion
12
import * as DynamoDB from 'aws-sdk/clients/dynamodb'
23
import { of } from 'rxjs'
34
import { getTableName } from '../../../test/helper'
4-
import { SimpleWithCompositePartitionKeyModel, SimpleWithPartitionKeyModel } from '../../../test/models'
5-
import { Organization } from '../../../test/models/organization.model'
6-
import { Attributes } from '../../mapper'
5+
import { Organization, SimpleWithCompositePartitionKeyModel, SimpleWithPartitionKeyModel } from '../../../test/models'
6+
import { Attributes, toDb } from '../../mapper'
77
import { DynamoRx } from '../dynamo-rx'
88
import { BatchGetRequest } from './batch-get.request'
99

1010
describe('batch get', () => {
1111
let request: BatchGetRequest
1212

13-
1413
describe('params', () => {
1514

1615
beforeEach(() => request = new BatchGetRequest())
@@ -21,39 +20,150 @@ describe('batch get', () => {
2120
})
2221

2322
it('key', () => {
24-
request.forModel(Organization, ['idValue'])
23+
const o: Partial<Organization> = {
24+
id: 'idValue',
25+
createdAtDate: new Date(),
26+
}
27+
request.forModel(Organization, [o])
2528
const params = request.params
2629
expect(params.RequestItems).toBeDefined()
2730
expect(params.RequestItems.Organization).toBeDefined()
28-
expect(params.RequestItems.Organization).toEqual({ Keys: [{ id: { S: 'idValue' } }] })
31+
expect(params.RequestItems.Organization.Keys).toBeDefined()
32+
expect(params.RequestItems.Organization.Keys).toEqual([{
33+
id: { S: 'idValue' },
34+
createdAtDate: { S: o.createdAtDate!.toISOString() },
35+
}])
2936
})
3037
})
3138

32-
3339
describe('forModel', () => {
3440
beforeEach(() => request = new BatchGetRequest())
3541

3642
it('should throw when same table is used 2 times', () => {
37-
request.forModel(SimpleWithPartitionKeyModel, ['idVal'])
38-
expect(() => request.forModel(SimpleWithPartitionKeyModel, ['otherVal'])).toThrow()
43+
request.forModel(SimpleWithPartitionKeyModel, [{ id: 'idVal' }])
44+
expect(() => request.forModel(SimpleWithPartitionKeyModel, [{ id: 'otherVal' }])).toThrow()
45+
})
46+
47+
it('should throw when sortKey is missing but necessary', () => {
48+
expect(() => request.forModel(SimpleWithCompositePartitionKeyModel, [{ id: 'idVal' }]))
49+
})
50+
51+
it('should throw when modelClazz is not @Model decorated', () => {
52+
class X {id: string}
53+
54+
expect(() => request.forModel(X, [{ id: 'ok' }])).toThrow()
3955
})
4056

4157
it('should throw when providing null value ', () => {
4258
expect(() => request.forModel(SimpleWithPartitionKeyModel, [<any>null])).toThrow()
4359
})
4460

45-
it('should throw when sortKey is missing', () => {
46-
expect(() => request.forModel(SimpleWithCompositePartitionKeyModel, [{ partitionKey: 'idVal' }]))
61+
it('should allow ConsistentRead', () => {
62+
request.forModel(SimpleWithPartitionKeyModel, [{ id: 'myId' }], true)
63+
expect(request.params).toBeDefined()
64+
expect(request.params.RequestItems).toBeDefined()
65+
const keysOfTable = request.params.RequestItems[getTableName(SimpleWithPartitionKeyModel)]
66+
expect(keysOfTable).toBeDefined()
67+
expect(keysOfTable.ConsistentRead).toBeTruthy()
4768
})
4869

49-
it('should throw when partitionKey is neither string nor object', () => {
50-
expect(() => request.forModel(SimpleWithCompositePartitionKeyModel, [<any>78]))
51-
expect(() => request.forModel(SimpleWithCompositePartitionKeyModel, [<any>true]))
52-
expect(() => request.forModel(SimpleWithCompositePartitionKeyModel, [<any>new Date()]))
70+
it('should throw when more than 100 items are added', () => {
71+
const items55: Array<Partial<SimpleWithPartitionKeyModel>> = new Array(55)
72+
.map((x, i) => ({ id: `id-${i}` }))
73+
const items60: Array<Partial<Organization>> = new Array(60)
74+
.map((x, i) => ({ id: `id-${i}`, createdAtDate: new Date() }))
75+
76+
// at once
77+
expect(() => request.forModel(SimpleWithPartitionKeyModel, [...items55, ...items55])).toThrow()
78+
79+
// in two steps
80+
expect(() => {
81+
request.forModel(SimpleWithPartitionKeyModel, items55)
82+
request.forModel(Organization, items60)
83+
}).toThrow()
5384
})
5485

5586
})
5687

88+
describe('execNoMap, execFullResponse, exec', () => {
89+
const jsItem1: SimpleWithPartitionKeyModel = { id: 'id-1', age: 21 }
90+
const jsItem2: SimpleWithPartitionKeyModel = { id: 'id-2', age: 22 }
91+
92+
const output1: DynamoDB.BatchGetItemOutput = {
93+
Responses: {
94+
[getTableName(SimpleWithPartitionKeyModel)]: [toDb(jsItem1, SimpleWithPartitionKeyModel)],
95+
},
96+
UnprocessedKeys: {
97+
[getTableName(SimpleWithPartitionKeyModel)]: {
98+
Keys: [toDb(jsItem1, SimpleWithPartitionKeyModel)],
99+
},
100+
},
101+
}
102+
const output2: DynamoDB.BatchGetItemOutput = {
103+
Responses: {
104+
[getTableName(SimpleWithPartitionKeyModel)]: [toDb(jsItem2, SimpleWithPartitionKeyModel)],
105+
},
106+
}
107+
108+
let batchGetItemsSpy: jasmine.Spy
109+
let nextSpyFn: () => { value: number }
110+
111+
const generatorMock = () => <any>{ next: nextSpyFn }
112+
113+
beforeEach(() => {
114+
request = new BatchGetRequest()
115+
request.forModel(SimpleWithPartitionKeyModel, [jsItem1, jsItem2])
116+
117+
batchGetItemsSpy = jasmine.createSpy().and.returnValues(of(output1), of(output2))
118+
const dynamoRx: DynamoRx = <any>{ batchGetItems: batchGetItemsSpy }
119+
120+
Object.assign(request, { dynamoRx })
121+
122+
nextSpyFn = jest.fn().mockImplementation(() => ({ value: 0 }))
123+
})
124+
125+
it('[execNoMap] should backoff and retry when UnprocessedItems are returned', async () => {
126+
const result = await request.execNoMap(generatorMock).toPromise()
127+
expect(nextSpyFn).toHaveBeenCalledTimes(1)
128+
expect(batchGetItemsSpy).toHaveBeenCalledTimes(2)
129+
expect(result).toBeDefined()
130+
expect(result.Responses).toBeDefined()
131+
132+
const resultItems = result.Responses![getTableName(SimpleWithPartitionKeyModel)]
133+
expect(resultItems).toBeDefined()
134+
expect(resultItems.length).toBe(2)
135+
expect(resultItems[0]).toEqual(toDb(jsItem1, SimpleWithPartitionKeyModel))
136+
expect(resultItems[1]).toEqual(toDb(jsItem2, SimpleWithPartitionKeyModel))
137+
})
138+
139+
it('[execFullResponse] should backoff and retry when UnprocessedItems are returned', async () => {
140+
const result = await request.execFullResponse(generatorMock).toPromise()
141+
expect(nextSpyFn).toHaveBeenCalledTimes(1)
142+
expect(batchGetItemsSpy).toHaveBeenCalledTimes(2)
143+
expect(result).toBeDefined()
144+
expect(result.Responses).toBeDefined()
145+
146+
const resultItems = result.Responses![getTableName(SimpleWithPartitionKeyModel)]
147+
expect(resultItems).toBeDefined()
148+
expect(resultItems.length).toBe(2)
149+
expect(resultItems[0]).toEqual(jsItem1)
150+
expect(resultItems[1]).toEqual(jsItem2)
151+
})
152+
153+
it('[exec] should backoff and retry when UnprocessedItems are returned', async () => {
154+
const result = await request.exec(generatorMock).toPromise()
155+
expect(nextSpyFn).toHaveBeenCalledTimes(1)
156+
expect(batchGetItemsSpy).toHaveBeenCalledTimes(2)
157+
expect(result).toBeDefined()
158+
159+
const resultItems = result[getTableName(SimpleWithPartitionKeyModel)]
160+
expect(resultItems).toBeDefined()
161+
expect(resultItems.length).toBe(2)
162+
expect(resultItems[0]).toEqual(jsItem1)
163+
expect(resultItems[1]).toEqual(jsItem2)
164+
})
165+
166+
})
57167

58168
describe('should map the result items', () => {
59169
let batchGetItemsSpy: jasmine.Spy
@@ -74,7 +184,7 @@ describe('batch get', () => {
74184
const dynamoRx: DynamoRx = <any>{ batchGetItems: batchGetItemsSpy }
75185
request = new BatchGetRequest()
76186
Object.assign(request, { dynamoRx })
77-
request.forModel(SimpleWithPartitionKeyModel, ['idVal'])
187+
request.forModel(SimpleWithPartitionKeyModel, [{ id: 'idVal' }])
78188
})
79189

80190
it('exec', async () => {
@@ -93,5 +203,4 @@ describe('batch get', () => {
93203
})
94204

95205
})
96-
97206
})

0 commit comments

Comments
 (0)