Skip to content

Commit 464ebdc

Browse files
committed
Merge branch 'beta' into #13-more-robust-uuid-implementation
2 parents d47e0af + b292fa4 commit 464ebdc

34 files changed

+1695
-462
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// tslint:disable:no-empty
2+
3+
import { DateToNumberMapper } from '../mapper/custom'
4+
import { dynamoEasyConfig } from './dynamo-easy-config'
5+
import { updateDynamoEasyConfig } from './update-config.function'
6+
7+
describe('updateDynamoEasyConfig', () => {
8+
9+
it('should throw when providing invalid stuff', () => {
10+
expect(() => updateDynamoEasyConfig({ logReceiver: <any>null })).toThrow()
11+
expect(() => updateDynamoEasyConfig({ dateMapper: <any>null })).toThrow()
12+
})
13+
14+
it('should have defaults', () => {
15+
expect(dynamoEasyConfig.logReceiver).toBeDefined()
16+
expect(dynamoEasyConfig.dateMapper).toBeDefined()
17+
})
18+
19+
it('should work when providing valid stuff', () => {
20+
const myLogReceiver = () => {}
21+
const myDateMapper = { ...DateToNumberMapper }
22+
updateDynamoEasyConfig({
23+
logReceiver: myLogReceiver,
24+
dateMapper: myDateMapper,
25+
})
26+
expect(dynamoEasyConfig.logReceiver).toBe(myLogReceiver)
27+
expect(dynamoEasyConfig.dateMapper).toBe(myDateMapper)
28+
29+
})
30+
31+
})

src/config/update-config.function.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@ export function updateDynamoEasyConfig(config: Partial<Config>): void {
55
if (config.logReceiver !== undefined && typeof config.logReceiver !== 'function') {
66
throw new Error('Config.logReceiver has to be a function')
77
}
8+
if (config.dateMapper !== undefined && (config.dateMapper === null || typeof config.dateMapper.toDb !== 'function' || typeof config.dateMapper.fromDb !== 'function')) {
9+
throw new Error('Config.dateMapper must be an object of type MapperForType')
10+
}
811
Object.assign(dynamoEasyConfig, config)
912
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// tslint:disable:no-non-null-assertion
2+
import {
3+
ModelWithABunchOfIndexes, ModelWithAutogeneratedId,
4+
ModelWithGSI,
5+
ModelWithLSI,
6+
SimpleWithCompositePartitionKeyModel,
7+
SimpleWithPartitionKeyModel,
8+
} from '../../../test/models'
9+
import { INDEX_ACTIVE, INDEX_ACTIVE_CREATED_AT, INDEX_COUNT } from '../../../test/models/model-with-indexes.model'
10+
import { Metadata } from './metadata'
11+
12+
describe('metadata', () => {
13+
let metaDataPartitionKey: Metadata<SimpleWithPartitionKeyModel>
14+
let metaDataComposite: Metadata<SimpleWithCompositePartitionKeyModel>
15+
let metaDataLsi: Metadata<ModelWithLSI>
16+
let metaDataGsi: Metadata<ModelWithGSI>
17+
let metaDataIndexes: Metadata<ModelWithABunchOfIndexes>
18+
let metaDataUuid: Metadata<ModelWithAutogeneratedId>
19+
20+
beforeEach(() => {
21+
metaDataPartitionKey = new Metadata(SimpleWithPartitionKeyModel)
22+
metaDataComposite = new Metadata(SimpleWithCompositePartitionKeyModel)
23+
metaDataLsi = new Metadata(ModelWithLSI)
24+
metaDataGsi = new Metadata(ModelWithGSI)
25+
metaDataIndexes = new Metadata(ModelWithABunchOfIndexes)
26+
metaDataUuid = new Metadata(ModelWithAutogeneratedId)
27+
})
28+
29+
it('forProperty', () => {
30+
const forId = metaDataPartitionKey.forProperty('id')
31+
expect(forId).toBeDefined()
32+
expect(forId!.key).toBeDefined()
33+
expect(forId!.name).toBe('id')
34+
expect(forId!.typeInfo).toBeDefined()
35+
expect(forId!.typeInfo!.isCustom).toBeFalsy()
36+
})
37+
38+
it('getKeysWithUUID', () => {
39+
const uuid = metaDataUuid.getKeysWithUUID()
40+
expect(uuid.length).toBe(1)
41+
expect(uuid[0].key).toBeDefined()
42+
expect(uuid[0].key!.uuid).toBeTruthy()
43+
expect(uuid[0].name).toBe('id')
44+
45+
})
46+
47+
it('getPartitionKey', () => {
48+
expect(metaDataPartitionKey.getPartitionKey()).toEqual('id')
49+
expect(metaDataGsi.getPartitionKey(INDEX_ACTIVE)).toEqual('active')
50+
expect(metaDataIndexes.getPartitionKey(INDEX_COUNT)).toEqual('myId')
51+
expect(metaDataIndexes.getPartitionKey(INDEX_ACTIVE_CREATED_AT)).toEqual('active')
52+
53+
})
54+
55+
it('getSortKey', () => {
56+
expect(metaDataPartitionKey.getSortKey()).toBe(null)
57+
expect(metaDataComposite.getSortKey()).toBe('creationDate')
58+
expect(metaDataLsi.getSortKey(INDEX_ACTIVE)).toBe('active')
59+
expect(() => metaDataGsi.getSortKey(INDEX_ACTIVE)).toThrow()
60+
expect(metaDataIndexes.getSortKey(INDEX_ACTIVE_CREATED_AT)).toBe('createdAt')
61+
})
62+
63+
it('getIndexes', () => {
64+
expect(metaDataLsi.getIndexes()).toEqual([
65+
{ partitionKey: 'id', sortKey: 'active' },
66+
])
67+
expect(metaDataGsi.getIndexes()).toEqual([
68+
{ partitionKey: 'active' },
69+
])
70+
expect(metaDataIndexes.getIndexes()).toEqual([
71+
{ partitionKey: 'active', sortKey: 'createdAt' },
72+
{ partitionKey: 'myId', sortKey: 'count' },
73+
])
74+
})
75+
76+
it('getIndex', () => {
77+
expect(metaDataPartitionKey.getIndexes().length).toBe(0)
78+
expect(metaDataPartitionKey.getIndex('blub')).toBe(null)
79+
expect(metaDataIndexes.getIndex(INDEX_ACTIVE_CREATED_AT)).toEqual({
80+
partitionKey: 'active',
81+
sortKey: 'createdAt',
82+
})
83+
})
84+
85+
})

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
*/
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { DynamoDB } from 'aws-sdk'
2+
import { of } from 'rxjs'
3+
import { DynamoRx } from '../dynamo-rx'
4+
import { batchGetItemsFetchAll, combineBatchGetResponses, hasUnprocessedKeys } from './batch-get-utils'
5+
6+
describe('batch-get utils', () => {
7+
8+
describe('hasUnprocessedKeys', () => {
9+
it('should return bool according to given object', () => {
10+
expect(hasUnprocessedKeys({})).toBeFalsy()
11+
expect(hasUnprocessedKeys({ Responses: {} })).toBeFalsy()
12+
expect(hasUnprocessedKeys({ UnprocessedKeys: {} })).toBeFalsy()
13+
expect(hasUnprocessedKeys({ UnprocessedKeys: { 'aTableName': { Keys: [] } } })).toBeFalsy()
14+
expect(hasUnprocessedKeys({ UnprocessedKeys: { 'aTableName': { Keys: [{ id: { S: 'id' } }] } } })).toBeTruthy()
15+
})
16+
})
17+
18+
describe('combineBatchGetResponses', () => {
19+
const resp1: DynamoDB.BatchGetItemOutput = {
20+
Responses: {
21+
'tableA': [
22+
{ id: { S: 'id-a1' } },
23+
],
24+
'tableB': [
25+
{ id: { S: 'id-b' } },
26+
],
27+
},
28+
UnprocessedKeys: {
29+
'tableA:': { Keys: [{ id: { S: 'id-a2' } }] },
30+
'tableC:': { Keys: [{ id: { S: 'id-c' } }] },
31+
'tableD:': { Keys: [{ id: { S: 'id-d' } }] },
32+
},
33+
}
34+
const resp2: DynamoDB.BatchGetItemOutput = {
35+
Responses: {
36+
'tableA': [
37+
{ id: { S: 'id-a2' } },
38+
],
39+
'tableC': [
40+
{ id: { S: 'id-c' } },
41+
],
42+
},
43+
UnprocessedKeys: {
44+
'tableD:': { Keys: [{ id: { S: 'id-d' } }] },
45+
},
46+
}
47+
const expectedOutput: DynamoDB.BatchGetItemOutput = {
48+
Responses: {
49+
'tableA': [
50+
{ id: { S: 'id-a1' } },
51+
{ id: { S: 'id-a2' } },
52+
],
53+
'tableB': [
54+
{ id: { S: 'id-b' } },
55+
],
56+
'tableC': [
57+
{ id: { S: 'id-c' } },
58+
],
59+
},
60+
UnprocessedKeys: {
61+
'tableD:': { Keys: [{ id: { S: 'id-d' } }] },
62+
},
63+
}
64+
it('should combine correctly', () => {
65+
expect(combineBatchGetResponses(resp1)(resp2)).toEqual(expectedOutput)
66+
})
67+
68+
})
69+
70+
describe('batchGetItemsFetchAll', () => {
71+
let batchGetItemsSpy: jasmine.Spy
72+
let dynamoRx: DynamoRx
73+
let backoffTimerMock: { next: jasmine.Spy }
74+
75+
const output1: DynamoDB.BatchGetItemOutput = {
76+
Responses: {
77+
'tableA': [{ id: { S: 'id-A' } }],
78+
},
79+
UnprocessedKeys: {
80+
'tableA': {
81+
Keys: [{ id: { S: 'id-A' } }],
82+
},
83+
},
84+
}
85+
const output2: DynamoDB.BatchGetItemOutput = {
86+
Responses: {
87+
'tableA': [{ id: { S: 'id-A' } }],
88+
},
89+
}
90+
91+
beforeEach(async () => {
92+
batchGetItemsSpy = jasmine.createSpy().and.returnValues(of(output1), of(output2))
93+
dynamoRx = <any>{ batchGetItems: batchGetItemsSpy }
94+
backoffTimerMock = { next: jasmine.createSpy().and.returnValue({ value: 0 }) }
95+
96+
await batchGetItemsFetchAll(
97+
dynamoRx,
98+
<any>{},
99+
<IterableIterator<number>><any>backoffTimerMock,
100+
0,
101+
).toPromise()
102+
})
103+
104+
105+
it('should use UnprocessedKeys for next request', () => {
106+
expect(batchGetItemsSpy).toHaveBeenCalledTimes(2)
107+
expect(batchGetItemsSpy.calls.mostRecent().args[0]).toBeDefined()
108+
expect(batchGetItemsSpy.calls.mostRecent().args[0].RequestItems).toBeDefined()
109+
expect(batchGetItemsSpy.calls.mostRecent().args[0].RequestItems).toEqual(output1.UnprocessedKeys)
110+
})
111+
112+
it('should backoff when UnprocessedItems', () => {
113+
expect(backoffTimerMock.next).toHaveBeenCalledTimes(1)
114+
})
115+
116+
})
117+
118+
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { DynamoDB } from 'aws-sdk'
2+
import { BatchGetRequestMap } from 'aws-sdk/clients/dynamodb'
3+
import { Observable, of } from 'rxjs'
4+
import { delay, map, mergeMap } from 'rxjs/operators'
5+
import { DynamoRx } from '../dynamo-rx'
6+
7+
export function batchGetItemsFetchAll(
8+
dynamoRx: DynamoRx,
9+
params: DynamoDB.BatchGetItemInput,
10+
backoffTimer: IterableIterator<number>,
11+
throttleTimeSlot: number,
12+
): Observable<DynamoDB.BatchGetItemOutput> {
13+
return dynamoRx.batchGetItems(params)
14+
.pipe(
15+
mergeMap(response => {
16+
if (hasUnprocessedKeys(response)) {
17+
return of(response.UnprocessedKeys)
18+
.pipe(
19+
delay(backoffTimer.next().value * throttleTimeSlot),
20+
mergeMap((UnprocessedKeys: DynamoDB.BatchGetRequestMap) => {
21+
const nextParams = { ...params, RequestItems: UnprocessedKeys }
22+
return batchGetItemsFetchAll(dynamoRx, nextParams, backoffTimer, throttleTimeSlot)
23+
}),
24+
map(combineBatchGetResponses(response)),
25+
)
26+
}
27+
return of(response)
28+
}),
29+
)
30+
}
31+
32+
export type ResponseWithUnprocessedKeys = DynamoDB.BatchGetItemOutput & { UnprocessedKeys: BatchGetRequestMap }
33+
34+
export function hasUnprocessedKeys(response: DynamoDB.BatchGetItemOutput): response is ResponseWithUnprocessedKeys {
35+
if (!response.UnprocessedKeys) {
36+
return false
37+
}
38+
return Object.values(response.UnprocessedKeys)
39+
.some(t => !!t && t.Keys && t.Keys.length > 0)
40+
}
41+
42+
/**
43+
* combines a first with a second response. ConsumedCapacity is always from the latter.
44+
* @param response1
45+
*/
46+
export function combineBatchGetResponses(response1: DynamoDB.BatchGetItemOutput) {
47+
return (response2: DynamoDB.BatchGetItemOutput): DynamoDB.BatchGetItemOutput => {
48+
const tableNames: string[] = Object.keys(response1.Responses || {})
49+
50+
Object.keys(response2.Responses || {})
51+
.filter(tn => !tableNames.includes(tn))
52+
.forEach(tn => tableNames.push(tn))
53+
54+
const Responses = tableNames
55+
.reduce((u, tableName) => ({
56+
...u,
57+
[tableName]: [
58+
...(response1.Responses && response1.Responses[tableName] || []),
59+
...(response2.Responses && response2.Responses[tableName] || []),
60+
],
61+
}), {})
62+
return {
63+
...response2,
64+
Responses,
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)