Skip to content

Commit 68e0150

Browse files
fix(batch-write-single-table-request): delete/put directly to params
same handling of params as in other requests. uses the limits aws dynamoDB uses (max 25 put or delete items) BREAKING CHANGE you cannot longer put or delete more than 25 items in one BatchWriteSingleTableRequest instance. create multiple request instances instead.
1 parent 1f3a501 commit 68e0150

File tree

5 files changed

+215
-123
lines changed

5 files changed

+215
-123
lines changed

src/dynamo/batchget/batch-get-utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ export function batchGetItemsFetchAll(
2929
)
3030
}
3131

32-
export type ResponseWithUnprocessedKeys = DynamoDB.BatchGetItemOutput & { UnprocessedKeys: BatchGetRequestMap }
32+
export type BatchGetItemOutputWithUnprocessedKeys =
33+
DynamoDB.BatchGetItemOutput
34+
& { UnprocessedKeys: BatchGetRequestMap }
3335

34-
export function hasUnprocessedKeys(response: DynamoDB.BatchGetItemOutput): response is ResponseWithUnprocessedKeys {
36+
export function hasUnprocessedKeys(response: DynamoDB.BatchGetItemOutput): response is BatchGetItemOutputWithUnprocessedKeys {
3537
if (!response.UnprocessedKeys) {
3638
return false
3739
}
Lines changed: 120 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// tslint:disable:no-unnecessary-class
2+
13
import * as DynamoDB from 'aws-sdk/clients/dynamodb'
24
import { of } from 'rxjs'
35
import { Organization } from '../../../../test/models'
@@ -7,38 +9,53 @@ import { BatchWriteSingleTableRequest } from './batch-write-single-table.request
79

810
describe('batch write single table request', () => {
911
const tableName = getTableName(Organization)
12+
const item: Organization = <Organization>{
13+
id: 'myId',
14+
createdAtDate: new Date(),
15+
name: 'myOrg',
16+
}
1017

11-
let item: Organization
1218
let dynamoRx: DynamoRx
1319
let request: BatchWriteSingleTableRequest<Organization>
1420

15-
let nextSpyFn: () => { value: number }
16-
const generatorMock = () => <any>{ next: nextSpyFn }
21+
describe('constructor', () => {
22+
it('should throw when no class was given', () => {
23+
expect(() => new BatchWriteSingleTableRequest(<any>null, <any>null)).toThrow()
24+
})
25+
it('should throw when class given is not @Model decorated', () => {
26+
class NoModel {}
27+
expect(() => new BatchWriteSingleTableRequest(<any>null, NoModel)).toThrow()
28+
})
1729

18-
beforeEach(() => {
19-
item = <any>{
20-
id: 'myId',
21-
createdAtDate: new Date(),
22-
name: 'myOrg',
23-
}
24-
nextSpyFn = jest.fn().mockImplementation(() => ({ value: 0 }))
30+
it('should initialize params', () => {
31+
request = new BatchWriteSingleTableRequest(<any>null, Organization)
32+
expect(request.params).toEqual({
33+
RequestItems: {
34+
[tableName]: [],
35+
},
36+
})
37+
})
2538
})
2639

2740
describe('correct params', () => {
2841
beforeEach(() => {
29-
dynamoRx = new DynamoRx()
3042
request = new BatchWriteSingleTableRequest(dynamoRx, Organization)
43+
})
44+
45+
it('returnConsumedCapacity', () => {
46+
request.returnConsumedCapacity('TOTAL')
47+
expect(request.params.ReturnConsumedCapacity).toBe('TOTAL')
48+
})
3149

32-
const output: DynamoDB.BatchWriteItemOutput = {}
33-
spyOn(dynamoRx, 'batchWriteItem').and.returnValue(of(output))
50+
it('returnItemCollectionMetrics', () => {
51+
request.returnItemCollectionMetrics('SIZE')
52+
expect(request.params.ReturnItemCollectionMetrics).toBe('SIZE')
3453
})
3554

36-
it('delete with complex primary key', async () => {
55+
it('delete with composite key', () => {
3756
request.delete([item])
38-
await request.exec(generatorMock).toPromise()
3957

40-
expect(dynamoRx.batchWriteItem).toHaveBeenCalledTimes(1)
41-
expect(dynamoRx.batchWriteItem).toHaveBeenCalledWith({
58+
expect(request.params).toEqual({
4259
RequestItems: {
4360
[tableName]: [
4461
{
@@ -52,15 +69,12 @@ describe('batch write single table request', () => {
5269
],
5370
},
5471
})
55-
expect(nextSpyFn).toHaveBeenCalledTimes(0)
5672
})
5773

5874
it('put object', async () => {
5975
request.put([item])
60-
await request.exec(generatorMock).toPromise()
6176

62-
expect(dynamoRx.batchWriteItem).toHaveBeenCalledTimes(1)
63-
expect(dynamoRx.batchWriteItem).toHaveBeenCalledWith({
77+
expect(request.params).toEqual({
6478
RequestItems: {
6579
[tableName]: [
6680
{
@@ -75,50 +89,103 @@ describe('batch write single table request', () => {
7589
],
7690
},
7791
})
78-
expect(nextSpyFn).toHaveBeenCalledTimes(0)
7992
})
8093

81-
it('delete >25 items in two requests', async () => {
82-
const twentyFiveItems = []
83-
for (let i = 0; i < 25; i++) {
84-
twentyFiveItems.push(item)
85-
}
86-
request.delete(twentyFiveItems)
94+
it('adding >25 items in first delete call throws', () => {
95+
const twentyFiveItems = new Array(30).map(() => item)
96+
expect(() => request.delete(twentyFiveItems)).toThrow()
97+
})
98+
99+
it('adding >25 items in second delete call throws', () => {
100+
const twentyFiveItems = new Array(25).map(() => item)
87101
request.delete(twentyFiveItems)
88-
await request.exec(generatorMock).toPromise()
89-
expect(dynamoRx.batchWriteItem).toHaveBeenCalledTimes(2)
90-
expect(nextSpyFn).toHaveBeenCalledTimes(0)
102+
expect(() => request.delete(twentyFiveItems)).toThrow()
103+
})
104+
105+
it('adding >25 items in first put call throws', () => {
106+
const twentyFiveItems = new Array(30).map(() => item)
107+
expect(() => request.put(twentyFiveItems)).toThrow()
108+
})
109+
110+
it('adding >25 items in second put call throws', () => {
111+
const twentyFiveItems = new Array(25).map(() => item)
112+
request.put(twentyFiveItems)
113+
expect(() => request.put(twentyFiveItems)).toThrow()
91114
})
92115
})
93116

94-
describe('correct backoff', () => {
117+
describe('Unprocessed items', () => {
118+
const output: DynamoDB.BatchWriteItemOutput = {
119+
UnprocessedItems: {
120+
[tableName]: [
121+
{
122+
PutRequest: {
123+
Item: {
124+
id: { S: 'myId' },
125+
createdAtDate: { S: item.createdAtDate.toISOString() },
126+
name: { S: 'myOrg' },
127+
},
128+
},
129+
},
130+
],
131+
},
132+
}
133+
134+
let generatorSpy: jasmine.Spy
135+
let nextFnSpy: jasmine.Spy
136+
let batchWriteItemSpy: jasmine.Spy
137+
95138
beforeEach(() => {
96-
dynamoRx = new DynamoRx()
139+
batchWriteItemSpy = jasmine.createSpy().and.returnValues(of(output), of(output), of({ MyResult: true }))
140+
nextFnSpy = jasmine.createSpy().and.returnValue({ value: 0 })
141+
dynamoRx = <DynamoRx>(<any>{ batchWriteItem: batchWriteItemSpy })
142+
generatorSpy = jasmine.createSpy().and.returnValue({ next: nextFnSpy })
143+
97144
request = new BatchWriteSingleTableRequest(dynamoRx, Organization)
145+
})
98146

99-
const output: DynamoDB.BatchWriteItemOutput = {
100-
UnprocessedItems: {
101-
[tableName]: [
102-
{
103-
PutRequest: {
104-
Item: {
105-
id: { S: 'myId' },
106-
createdAtDate: { S: item.createdAtDate.toISOString() },
107-
name: { S: 'myOrg' },
108-
},
109-
},
110-
},
111-
],
112-
},
113-
}
114-
spyOn(dynamoRx, 'batchWriteItem').and.returnValues(of(output), of({}))
147+
it('should retry when unprocessed items are returned', async () => {
148+
request.put([item])
149+
await request.exec(<any>generatorSpy).toPromise()
150+
151+
// only one instance of the generator should be created
152+
expect(generatorSpy).toHaveBeenCalledTimes(1)
153+
154+
expect(nextFnSpy).toHaveBeenCalledTimes(2)
155+
156+
expect(batchWriteItemSpy).toHaveBeenCalledTimes(3)
115157
})
116158

117-
it('should retry when capacity is exceeded', async () => {
159+
it('should keep other params in additional calls', async () => {
118160
request.put([item])
119-
await request.exec(generatorMock).toPromise()
120-
expect(dynamoRx.batchWriteItem).toHaveBeenCalledTimes(2)
121-
expect(nextSpyFn).toHaveBeenCalledTimes(1)
161+
request.returnConsumedCapacity('TOTAL')
162+
request.returnItemCollectionMetrics('SIZE')
163+
await request.exec(<any>generatorSpy).toPromise()
164+
165+
expect(batchWriteItemSpy).toHaveBeenCalledTimes(3)
166+
const paramsThirdCall = <DynamoDB.BatchWriteItemInput>batchWriteItemSpy.calls.all()[2].args[0]
167+
168+
expect(paramsThirdCall).toBeDefined()
169+
expect(paramsThirdCall.ReturnConsumedCapacity).toBe('TOTAL')
170+
expect(paramsThirdCall.ReturnItemCollectionMetrics).toBe('SIZE')
171+
})
172+
})
173+
174+
describe('exec / execFullResponse', () => {
175+
beforeEach(() => {
176+
dynamoRx = <DynamoRx>(<any>{ batchWriteItem: () => of({ myResponse: true }) })
177+
request = new BatchWriteSingleTableRequest(dynamoRx, Organization)
178+
request.delete([item])
179+
})
180+
181+
it('exec should return nothing', async () => {
182+
const response = await request.exec().toPromise()
183+
expect(response).toBeUndefined()
184+
})
185+
186+
it('execFullResponse should return BatchWriteItemOutput', async () => {
187+
const response = await request.execFullResponse().toPromise()
188+
expect(response).toEqual({ myResponse: true })
122189
})
123190
})
124191
})

0 commit comments

Comments
 (0)