Skip to content

Commit 25bea00

Browse files
Merge pull request #137 from shiftcode/#136-improvements
#136 improvements
2 parents 334d204 + 5974560 commit 25bea00

28 files changed

+276
-84
lines changed

src/config/config.type.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,24 @@ import { SessionValidityEnsurer } from '../dynamo/session-validity-ensurer.type'
33
import { LogReceiver } from '../logger'
44
import { MapperForType } from '../mapper'
55

6+
/**
7+
* the global config object
8+
*/
69
export interface Config {
10+
/**
11+
* function receiving all the log statements
12+
*/
713
logReceiver: LogReceiver
14+
/**
15+
* mapper used for {@link DateProperty} decorated properties
16+
*/
817
dateMapper: MapperForType<any, any>,
18+
/**
19+
* function used to create the table names
20+
*/
921
tableNameResolver: TableNameResolver,
22+
/**
23+
* function called before calling dynamoDB api
24+
*/
1025
sessionValidityEnsurer: SessionValidityEnsurer,
1126
}

src/config/update-config.function.spec.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,37 @@ import { updateDynamoEasyConfig } from './update-config.function'
88
describe('updateDynamoEasyConfig', () => {
99
afterEach(resetDynamoEasyConfig)
1010

11-
it('should throw when providing invalid stuff', () => {
11+
it('should have defaults', () => {
12+
expect(dynamoEasyConfig.logReceiver).toBeDefined()
13+
expect(dynamoEasyConfig.dateMapper).toBeDefined()
14+
expect(dynamoEasyConfig.tableNameResolver).toBeDefined()
15+
expect(dynamoEasyConfig.sessionValidityEnsurer).toBeDefined()
16+
})
17+
18+
it('should throw when providing explicitly undefined, null, or a non-function value', () => {
19+
expect(() => updateDynamoEasyConfig({ logReceiver: undefined })).toThrow()
1220
expect(() => updateDynamoEasyConfig({ logReceiver: <any>null })).toThrow()
21+
expect(() => updateDynamoEasyConfig({ logReceiver: <any>'foo' })).toThrow()
22+
23+
expect(() => updateDynamoEasyConfig({ dateMapper: undefined })).toThrow()
1324
expect(() => updateDynamoEasyConfig({ dateMapper: <any>null })).toThrow()
25+
expect(() => updateDynamoEasyConfig({ dateMapper: <any>'foo' })).toThrow()
26+
27+
expect(() => updateDynamoEasyConfig({ tableNameResolver: undefined })).toThrow()
28+
expect(() => updateDynamoEasyConfig({ tableNameResolver: <any>null })).toThrow()
29+
expect(() => updateDynamoEasyConfig({ tableNameResolver: <any>'foo' })).toThrow()
30+
31+
expect(() => updateDynamoEasyConfig({ sessionValidityEnsurer: undefined })).toThrow()
32+
expect(() => updateDynamoEasyConfig({ sessionValidityEnsurer: <any>null })).toThrow()
33+
expect(() => updateDynamoEasyConfig({ sessionValidityEnsurer: <any>'foo' })).toThrow()
1434
})
1535

16-
it('should have defaults', () => {
36+
it('should not override default when updating but not providing a value', () => {
37+
updateDynamoEasyConfig({})
1738
expect(dynamoEasyConfig.logReceiver).toBeDefined()
1839
expect(dynamoEasyConfig.dateMapper).toBeDefined()
40+
expect(dynamoEasyConfig.tableNameResolver).toBeDefined()
41+
expect(dynamoEasyConfig.sessionValidityEnsurer).toBeDefined()
1942
})
2043

2144
it('should work when providing valid stuff', () => {

src/config/update-config.function.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import { Config } from './config.type'
22
import { dynamoEasyConfig } from './dynamo-easy-config'
33

4+
/**
5+
* update the global dynamoEasy configuration.
6+
* @see {@link Config}
7+
* @param config
8+
*/
49
export function updateDynamoEasyConfig(config: Partial<Config>): void {
5-
if (config.logReceiver !== undefined && typeof config.logReceiver !== 'function') {
10+
if ('logReceiver' in config && typeof config.logReceiver !== 'function') {
611
throw new Error('Config.logReceiver has to be a function')
712
}
8-
if (config.dateMapper !== undefined && (config.dateMapper === null || typeof config.dateMapper.toDb !== 'function' || typeof config.dateMapper.fromDb !== 'function')) {
13+
if ('dateMapper' in config && (!config.dateMapper || typeof config.dateMapper.toDb !== 'function' || typeof config.dateMapper.fromDb !== 'function')) {
914
throw new Error('Config.dateMapper must be an object of type MapperForType')
1015
}
11-
if (config.tableNameResolver !== undefined && (config.tableNameResolver === null || typeof config.tableNameResolver !== 'function')) {
16+
if ('tableNameResolver' in config && typeof config.tableNameResolver !== 'function') {
1217
throw new Error('Config.tableNameResolver must be function')
1318
}
14-
if (config.sessionValidityEnsurer !== undefined && (config.sessionValidityEnsurer === null || typeof config.sessionValidityEnsurer !== 'function')) {
19+
if ('sessionValidityEnsurer' in config && typeof config.sessionValidityEnsurer !== 'function') {
1520
throw new Error('Config.sessionValidityEnsurer must be function')
1621
}
1722
Object.assign(dynamoEasyConfig, config)

src/decorator/decorators.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// tslint:disable:max-classes-per-file
22
// tslint:disable:no-unnecessary-class
33
// tslint:disable:no-non-null-assertion
4+
// tslint:disable:no-unused-variable
45
import { getMetaDataProperty } from '../../test/helper'
56
import {
67
ComplexModel,
@@ -523,4 +524,19 @@ describe('Decorators should add correct metadata', () => {
523524
expect(meta).toBeDefined()
524525
})
525526
})
527+
528+
describe('should throw when more than one partitionKey was defined in a model', () => {
529+
expect(() => {
530+
@Model()
531+
class InvalidModel {
532+
@PartitionKey()
533+
partKeyA: string
534+
535+
@PartitionKey()
536+
partKeyB: string
537+
}
538+
539+
return new InvalidModel()
540+
}).toThrow()
541+
})
526542
})

src/decorator/impl/key/partition-key.decorator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function PartitionKey(): PropertyDecorator {
1313
if (properties.find(property => property.name === propertyKey)) {
1414
// just ignore this and go on, somehow the partition key gets defined
1515
// tslint:disable-next-line:no-console
16-
console.warn(`this is the second execution to define the paritionKey for propety ${propertyKey}`)
16+
console.warn(`this is the second execution to define the partitionKey for property ${propertyKey}`)
1717
} else {
1818
throw new Error(
1919
'only one partition key is allowed per model, if you want to define key for indexes use one of these decorators: ' +

src/decorator/impl/property/init-or-update-property.function.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ function createNewProperty(
4646
name: propertyKey,
4747
nameDb: propertyKey,
4848
typeInfo,
49-
// ...mapperOpts,
5049
...propertyOptions,
5150
}
5251

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import { Observable, of } from 'rxjs'
44
import { delay, map, mergeMap } from 'rxjs/operators'
55
import { DynamoRx } from '../dynamo-rx'
66

7+
/**
8+
* Function which executes batchGetItem operations until all given items (as params) are processed (fetched).
9+
* Between each follow-up request (in case of unprocessed items) a delay is interposed calculated by the given backoffTime and throttleTimeSlot.
10+
* @param dynamoRx
11+
* @param params containing the keys per table to create the batchGet operation
12+
* @param backoffTimer used to determine how many time slots the follow-up request should be delayed
13+
* @param throttleTimeSlot used to calculate the effective wait time
14+
*/
715
export function batchGetItemsFetchAll(
816
dynamoRx: DynamoRx,
917
params: DynamoDB.BatchGetItemInput,
@@ -14,16 +22,21 @@ export function batchGetItemsFetchAll(
1422
.pipe(
1523
mergeMap(response => {
1624
if (hasUnprocessedKeys(response)) {
25+
// in case of unprocessedItems do a follow-up requests
1726
return of(response.UnprocessedKeys)
1827
.pipe(
28+
// delay before doing the follow-up request
1929
delay(backoffTimer.next().value * throttleTimeSlot),
30+
2031
mergeMap((UnprocessedKeys: DynamoDB.BatchGetRequestMap) => {
2132
const nextParams = { ...params, RequestItems: UnprocessedKeys }
33+
// call recursively batchGetItemsFetchAll with the returned UnprocessedItems params
2234
return batchGetItemsFetchAll(dynamoRx, nextParams, backoffTimer, throttleTimeSlot)
2335
}),
2436
map(combineBatchGetResponses(response)),
2537
)
2638
}
39+
// no follow-up request necessary, return result
2740
return of(response)
2841
}),
2942
)

src/dynamo/batchwrite/batch-write-utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import { delay, mergeMap } from 'rxjs/operators'
44
import { DynamoRx } from '../dynamo-rx'
55

66

7+
/**
8+
* Function which executes batchWriteItem operations until all given items (as params) are processed (written).
9+
* Between each follow-up request (in case of unprocessed items) a delay is interposed calculated by the given backoffTime and throttleTimeSlot.
10+
* @param dynamoRx
11+
* @param params containing the items per table to create the batchWrite operation
12+
* @param backoffTimer used to determine how many time slots the follow-up request should be delayed
13+
* @param throttleTimeSlot used to calculate the effective wait time
14+
*/
715
export function batchWriteItemsWriteAll(
816
dynamoRx: DynamoRx,
917
params: DynamoDB.BatchWriteItemInput,
@@ -14,15 +22,21 @@ export function batchWriteItemsWriteAll(
1422
.pipe(
1523
mergeMap(response => {
1624
if (hasUnprocessedItems(response)) {
25+
// in case of unprocessedItems do a follow-up requests
1726
return of(response.UnprocessedItems)
1827
.pipe(
28+
// delay before doing the follow-up request
1929
delay(backoffTimer.next().value * throttleTimeSlot),
30+
2031
mergeMap((unprocessedKeys: DynamoDB.BatchWriteItemRequestMap) => {
2132
const nextParams: DynamoDB.BatchWriteItemInput = { ...params, RequestItems: unprocessedKeys }
33+
// call recursively batchWriteItemsWriteAll with the returned UnprocessedItems params
2234
return batchWriteItemsWriteAll(dynamoRx, nextParams, backoffTimer, throttleTimeSlot)
2335
}),
36+
// no combining of responses necessary, only the last response is returned
2437
)
2538
}
39+
// no follow-up request necessary, return result
2640
return of(response)
2741
}),
2842
)

src/dynamo/dynamo-rx.spec.ts

Lines changed: 87 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,115 @@
11
// tslint:disable:no-empty
22
// tslint:disable:no-unnecessary-callback-wrapper
33

4-
import { Config, Credentials, DynamoDB } from 'aws-sdk'
5-
import { EMPTY, Observable } from 'rxjs'
4+
import { Config, Credentials } from 'aws-sdk'
5+
import { of } from 'rxjs'
66
import { resetDynamoEasyConfig } from '../../test/helper/resetDynamoEasyConfig.function'
77
import { updateDynamoEasyConfig } from '../config'
88
import { DynamoRx } from './dynamo-rx'
9-
import { SessionValidityEnsurer } from './session-validity-ensurer.type'
109

1110
describe('dynamo rx', () => {
12-
describe('should call the validity ensurer before each call and return an observable', () => {
11+
describe('should call the validity ensurer before each call and call the correct dynamoDB method', () => {
1312
let dynamoRx: DynamoRx
14-
let spyValidityEnsurer: SessionValidityEnsurer
13+
let sessionValidityEnsurerSpy: jasmine.Spy
14+
let dynamoDbSpy: jasmine.Spy
15+
let pseudoParams: any
1516

1617
beforeEach(() => {
17-
spyValidityEnsurer = jasmine.createSpy().and.returnValue(EMPTY)
18-
updateDynamoEasyConfig({ sessionValidityEnsurer: spyValidityEnsurer })
18+
pseudoParams = { TableName: 'tableName', KeyConditionExpression: 'blub' }
19+
sessionValidityEnsurerSpy = jasmine.createSpy().and.returnValue(of(true))
20+
updateDynamoEasyConfig({ sessionValidityEnsurer: sessionValidityEnsurerSpy })
1921
dynamoRx = new DynamoRx()
2022
})
2123

22-
afterEach(resetDynamoEasyConfig)
24+
afterEach(() => {
25+
resetDynamoEasyConfig()
26+
expect(sessionValidityEnsurerSpy).toHaveBeenCalled()
27+
expect(dynamoDbSpy).toHaveBeenCalledTimes(1)
28+
expect(dynamoDbSpy).toHaveBeenCalledWith(pseudoParams)
29+
})
2330

24-
it('putItem', () => {
25-
expect(dynamoRx.putItem(<any>null) instanceof Observable).toBeTruthy()
26-
expect(spyValidityEnsurer).toHaveBeenCalled()
31+
it('putItem', async () => {
32+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'putItem').and.returnValue({ promise: () => Promise.resolve() })
33+
await dynamoRx.putItem(pseudoParams).toPromise()
2734
})
28-
it('getItem', () => {
29-
expect(dynamoRx.getItem(<any>null) instanceof Observable).toBeTruthy()
30-
expect(spyValidityEnsurer).toHaveBeenCalled()
35+
36+
it('getItem', async () => {
37+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'getItem').and.returnValue({ promise: () => Promise.resolve() })
38+
await dynamoRx.getItem(pseudoParams).toPromise()
3139
})
32-
it('updateItem', () => {
33-
expect(dynamoRx.updateItem(<any>null) instanceof Observable).toBeTruthy()
34-
expect(spyValidityEnsurer).toHaveBeenCalled()
40+
41+
it('updateItem', async () => {
42+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'updateItem').and.returnValue({ promise: () => Promise.resolve() })
43+
await dynamoRx.updateItem(pseudoParams).toPromise()
3544
})
36-
it('deleteItem', () => {
37-
expect(dynamoRx.deleteItem(<any>null) instanceof Observable).toBeTruthy()
38-
expect(spyValidityEnsurer).toHaveBeenCalled()
45+
46+
it('deleteItem', async () => {
47+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'deleteItem').and.returnValue({ promise: () => Promise.resolve() })
48+
await dynamoRx.deleteItem(pseudoParams).toPromise()
3949
})
40-
it('batchWriteItem', () => {
41-
expect(dynamoRx.batchWriteItem(<any>null) instanceof Observable).toBeTruthy()
42-
expect(spyValidityEnsurer).toHaveBeenCalled()
50+
51+
it('batchWriteItem', async () => {
52+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'batchWriteItem').and.returnValue({ promise: () => Promise.resolve() })
53+
await dynamoRx.batchWriteItem(pseudoParams).toPromise()
4354
})
44-
it('batchGetItems', () => {
45-
expect(dynamoRx.batchGetItems(<any>null) instanceof Observable).toBeTruthy()
46-
expect(spyValidityEnsurer).toHaveBeenCalled()
55+
56+
it('batchGetItems', async () => {
57+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'batchGetItem').and.returnValue({ promise: () => Promise.resolve() })
58+
await dynamoRx.batchGetItems(pseudoParams).toPromise()
4759
})
48-
it('transactWriteItem', () => {
49-
expect(dynamoRx.transactWriteItems(<any>null) instanceof Observable).toBeTruthy()
50-
expect(spyValidityEnsurer).toHaveBeenCalled()
60+
61+
it('transactWriteItems', async () => {
62+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'transactWriteItems').and.returnValue({ promise: () => Promise.resolve() })
63+
await dynamoRx.transactWriteItems(pseudoParams).toPromise()
5164
})
52-
it('transactGetItems', () => {
53-
expect(dynamoRx.transactGetItems(<any>null) instanceof Observable).toBeTruthy()
54-
expect(spyValidityEnsurer).toHaveBeenCalled()
65+
66+
it('transactGetItems', async () => {
67+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'transactGetItems').and.returnValue({ promise: () => Promise.resolve() })
68+
await dynamoRx.transactGetItems(pseudoParams).toPromise()
5569
})
56-
it('scan', () => {
57-
expect(dynamoRx.scan(<any>null) instanceof Observable).toBeTruthy()
58-
expect(spyValidityEnsurer).toHaveBeenCalled()
70+
71+
it('scan', async () => {
72+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'scan').and.returnValue({ promise: () => Promise.resolve() })
73+
await dynamoRx.scan(pseudoParams).toPromise()
5974
})
60-
it('query', () => {
61-
const params: DynamoDB.QueryInput = {
62-
TableName: 'tableName',
63-
}
64-
expect(dynamoRx.query({ ...params, KeyConditionExpression: 'blub' }) instanceof Observable).toBeTruthy()
65-
expect(spyValidityEnsurer).toHaveBeenCalled()
66-
expect(() => dynamoRx.query(params)).toThrow()
75+
76+
it('query', async () => {
77+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'query').and.returnValue({ promise: () => Promise.resolve() })
78+
await dynamoRx.query(pseudoParams).toPromise()
6779
})
68-
it('makeRequest', () => {
69-
expect(dynamoRx.makeRequest(<any>null) instanceof Observable).toBeTruthy()
70-
expect(spyValidityEnsurer).toHaveBeenCalled()
80+
})
81+
82+
describe('makeRequest', async () => {
83+
let dynamoRx: DynamoRx
84+
let sessionValidityEnsurerSpy: jasmine.Spy
85+
let dynamoDbSpy: jasmine.Spy
86+
let pseudoParams: any
87+
88+
beforeEach(() => {
89+
pseudoParams = { TableName: 'tableName', KeyConditionExpression: 'blub' }
90+
sessionValidityEnsurerSpy = jasmine.createSpy().and.returnValue(of(true))
91+
updateDynamoEasyConfig({ sessionValidityEnsurer: sessionValidityEnsurerSpy })
92+
dynamoRx = new DynamoRx()
93+
})
94+
95+
afterEach(() => {
96+
resetDynamoEasyConfig()
97+
expect(sessionValidityEnsurerSpy).toHaveBeenCalled()
98+
expect(dynamoDbSpy).toHaveBeenCalledTimes(1)
99+
expect(dynamoDbSpy).toHaveBeenCalledWith('pseudoOperation', pseudoParams)
100+
})
101+
102+
it('should call the validity ensurer before each call and call the correct dynamoDB method', async () => {
103+
dynamoDbSpy = spyOn(dynamoRx.dynamoDb, 'makeRequest').and.returnValue({ promise: () => Promise.resolve() })
104+
await dynamoRx.makeRequest('pseudoOperation', pseudoParams).toPromise()
105+
})
106+
})
107+
108+
describe('query', () => {
109+
beforeEach(() => {})
110+
it('should throw when no KeyConditionExpression was given', () => {
111+
const dynamoRx = new DynamoRx()
112+
expect(() => dynamoRx.query({ TableName: 'tableName' })).toThrow()
71113
})
72114
})
73115

src/dynamo/expression/condition-expression-builder.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { resolveAttributeNames } from './functions/attribute-names.function'
77
import { isFunctionOperator } from './functions/is-function-operator.function'
88
import { isNoParamFunctionOperator } from './functions/is-no-param-function-operator.function'
99
import { operatorParameterArity } from './functions/operator-parameter-arity.function'
10-
import { uniqAttributeValueName } from './functions/unique-attribute-value-name.function'
10+
import { uniqueAttributeValueName } from './functions/unique-attribute-value-name.function'
1111
import { ConditionOperator } from './type/condition-operator.type'
1212
import { Expression } from './type/expression.type'
1313
import { validateAttributeType } from './update-expression-builder'
@@ -114,7 +114,7 @@ export function buildFilterExpression(
114114
* person.age
115115
*/
116116
const resolvedAttributeNames = resolveAttributeNames(attributePath, propertyMetadata)
117-
const valuePlaceholder = uniqAttributeValueName(attributePath, existingValueNames)
117+
const valuePlaceholder = uniqueAttributeValueName(attributePath, existingValueNames)
118118

119119
/*
120120
* build the statement
@@ -204,7 +204,7 @@ function buildBetweenConditionExpression(
204204
[mappedValue1, mappedValue2]
205205
.forEach(mv => validateAttributeType('between', mv, 'S', 'N', 'B'))
206206

207-
const value2Placeholder = uniqAttributeValueName(attributePath, [valuePlaceholder].concat(existingValueNames || []))
207+
const value2Placeholder = uniqueAttributeValueName(attributePath, [valuePlaceholder].concat(existingValueNames || []))
208208

209209
const statement = `${namePlaceholder} BETWEEN ${valuePlaceholder} AND ${value2Placeholder}`
210210
attributeValues[valuePlaceholder] = mappedValue1

0 commit comments

Comments
 (0)