Skip to content

Commit fdf6f85

Browse files
feat(expressionBuilder): update filtering to be deep and walk the object tree to filter out undefine
1 parent d48e679 commit fdf6f85

23 files changed

+173
-202
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Dynamo-Easy
22
[![Travis](https://img.shields.io/travis/shiftcode/dynamo-easy.svg)](https://travis-ci.org/shiftcode/dynamo-easy)
3-
[![Coverage Status](https://img.shields.io/coveralls/jekyll/jekyll.svg)](https://coveralls.io/github/shiftcode/dynamo-easy?branch=master)
43
[![Coverage Status](https://coveralls.io/repos/github/shiftcode/dynamo-easy/badge.svg?branch=master)](https://coveralls.io/github/shiftcode/dynamo-easy?branch=master)
54
[![Dev Dependencies](https://img.shields.io/david/expressjs/express.svg)](https://david-dm.org/michaelwittwer/dynamo-easy?type=dev)
65
[![Greenkeeper badge](https://badges.greenkeeper.io/alexjoverm/typescript-library-starter.svg)](https://greenkeeper.io/)
@@ -153,7 +152,7 @@ Enum values are persisted as Numbers (index of enum).
153152

154153
# Request API
155154
To start making requests create an instance of [DynamoStore](https://shiftcode.github.io/dynamo-easy/classes/_dynamo_dynamo_store_.dynamostore.html) and execute the desired operation using the provided api.
156-
We support all the common dynamodb operations:
155+
We support the following dynamodb operations with a fluent api:
157156

158157
- Put
159158
- Get

src/decorator/impl/property/property.decorator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function initOrUpdateIndex(indexType: IndexType, indexData: IndexData, ta
5555

5656
function initOrUpdateGSI(indexes: { [key: string]: KeyType }, indexData: IndexData): Partial<PropertyMetadata<any>> {
5757
if (indexes[indexData.name]) {
58-
// TODO when we throw an error we have a problem where multiple different classes extend one base class, this will be executed by multiple times
58+
// TODO LOW:INVESTIGATE when we throw an error we have a problem where multiple different classes extend one base class, this will be executed by multiple times
5959
// throw new Error(
6060
// 'the property with name is already registered as key for index - one property can only define one key per index'
6161
// )
@@ -145,7 +145,7 @@ function createNewProperty(
145145
}
146146

147147
/**
148-
* TODO BINARY make sure to implement the context dependant details of Binary (Buffer vs. Uint8Array)
148+
* TODO LOW:BINARY make sure to implement the context dependant details of Binary (Buffer vs. Uint8Array)
149149
* @returns {boolean} true if the type cannot be mapped by dynamo document client
150150
*/
151151
function isCustomType(type: AttributeModelTypes): boolean {

src/decorator/metadata/property-metadata.model.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { ModelConstructor } from '../../model/model-constructor'
55

66
export interface TypeInfo {
77
type: ModelConstructor<any>
8-
// TODO define what custom means, maybe remove it
98
// true if we use a non native type for dynamo document client
109
isCustom?: boolean
1110
genericType?: ModelConstructor<any>

src/dynamo-easy.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
//
22
// Reflect Metadata
33
//
4-
//
5-
// MomentJs locales
6-
//
7-
// TODO MOMENT we should import other locals (should we just import all locales for now?)
8-
import 'moment/locale/de-ch'
94
import 'reflect-metadata'
105
//
116
// RxJs

src/dynamo/dynamo-store.spec.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,30 @@ class DynamoStoreModel {}
1010
class DynamoStoreModel2 {}
1111

1212
describe('dynamo store', () => {
13-
it('correct table name (default)', () => {
14-
const dynamoStore = new DynamoStore(DynamoStoreModel)
13+
describe('table name', () => {
14+
it('correct table name (default)', () => {
15+
const dynamoStore = new DynamoStore(DynamoStoreModel)
1516

16-
expect(dynamoStore.tableName).toBe('dynamo-store-models')
17-
})
17+
expect(dynamoStore.tableName).toBe('dynamo-store-models')
18+
})
1819

19-
it('correct table name ()', () => {
20-
const dynamoStore = new DynamoStore(DynamoStoreModel2)
20+
it('correct table name ()', () => {
21+
const dynamoStore = new DynamoStore(DynamoStoreModel2)
2122

22-
expect(dynamoStore.tableName).toBe('myTableName')
23-
})
23+
expect(dynamoStore.tableName).toBe('myTableName')
24+
})
2425

25-
it('correct table name ()', () => {
26-
const dynamoStore = new DynamoStore(DynamoStoreModel2, tableName => `${tableName}-with-special-thing`)
26+
it('correct table name ()', () => {
27+
const dynamoStore = new DynamoStore(DynamoStoreModel2, tableName => `${tableName}-with-special-thing`)
2728

28-
expect(dynamoStore.tableName).toBe('myTableName-with-special-thing')
29-
})
29+
expect(dynamoStore.tableName).toBe('myTableName-with-special-thing')
30+
})
3031

31-
it('throw error because table name is invalid', () => {
32-
expect(() => {
33-
// tslint:disable-next-line:no-unused-expression
34-
new DynamoStore(DynamoStoreModel2, tableName => `${tableName}$`)
35-
}).toThrowError()
32+
it('throw error because table name is invalid', () => {
33+
expect(() => {
34+
// tslint:disable-next-line:no-unused-expression
35+
new DynamoStore(DynamoStoreModel2, tableName => `${tableName}$`)
36+
}).toThrowError()
37+
})
3638
})
3739
})

src/dynamo/dynamo-store.ts

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -73,53 +73,37 @@ export class DynamoStore<T> {
7373
return this.dynamoRx.makeRequest(operation, params)
7474
}
7575

76-
/*
77-
* some methods which simplify calls which are usually often used
78-
* TODO review the methods
79-
*/
80-
/**
81-
* executes a dynamoDB.batchGetItem for multiple keys or a operation
82-
*/
83-
byKeys(keys: any[]): Observable<T[]> {
84-
return this.findByMultipleKeys(keys)
85-
}
86-
87-
findAll(): Observable<T[]> {
88-
return this.scan().exec()
89-
}
90-
91-
// TODO how does this work when we work with composite primary key
92-
private findByMultipleKeys(keys: any[]): Observable<T[]> {
93-
const requestItems: { [nameDb: string]: { Keys: DynamoDB.AttributeMap[] } } = {}
94-
const attributeMaps: DynamoDB.AttributeMap[] = []
95-
keys.forEach(id => {
96-
// TODO add support for secondary index
97-
const idOb: DynamoDB.AttributeMap = {}
98-
const value = Mapper.toDbOne(id)
99-
if (value === null) {
100-
throw Error('please provide an actual value for partition key')
101-
}
102-
103-
idOb[MetadataHelper.get(this.modelClazz).getPartitionKey()] = value
104-
attributeMaps.push(idOb)
105-
})
106-
107-
requestItems[this.tableName] = {
108-
Keys: attributeMaps,
109-
}
110-
111-
const params: DynamoDB.BatchGetItemInput = {
112-
RequestItems: requestItems,
113-
}
114-
115-
return this.dynamoRx.batchGetItems(params).map(response => {
116-
if (response.Responses && Object.keys(response.Responses).length) {
117-
return response.Responses[this.tableName].map(attributeMap => Mapper.fromDb(attributeMap, this.modelClazz))
118-
} else {
119-
return []
120-
}
121-
})
122-
}
76+
// TODO implement BatchGetItem request (think about support for secondary indexes)
77+
// batchGetItems(keys: any[]): Observable<T[]> {
78+
// const requestItems: { [nameDb: string]: { Keys: DynamoDB.AttributeMap[] } } = {}
79+
// const attributeMaps: DynamoDB.AttributeMap[] = []
80+
// keys.forEach(id => {
81+
// const idOb: DynamoDB.AttributeMap = {}
82+
// const value = Mapper.toDbOne(id)
83+
// if (value === null) {
84+
// throw Error('please provide an actual value for partition key')
85+
// }
86+
//
87+
// idOb[MetadataHelper.get(this.modelClazz).getPartitionKey()] = value
88+
// attributeMaps.push(idOb)
89+
// })
90+
//
91+
// requestItems[this.tableName] = {
92+
// Keys: attributeMaps,
93+
// }
94+
//
95+
// const params: DynamoDB.BatchGetItemInput = {
96+
// RequestItems: requestItems,
97+
// }
98+
//
99+
// return this.dynamoRx.batchGetItems(params).map(response => {
100+
// if (response.Responses && Object.keys(response.Responses).length) {
101+
// return response.Responses[this.tableName].map(attributeMap => Mapper.fromDb(attributeMap, this.modelClazz))
102+
// } else {
103+
// return []
104+
// }
105+
// })
106+
// }
123107

124108
private createBaseParams(): { TableName: string } {
125109
const params: { TableName: string } = {

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ class MyModel {
1919
}
2020

2121
describe('expressions', () => {
22+
it('deep filter', () => {
23+
const arr = [5, 'bla', undefined]
24+
const obj = [
25+
{ street: 'street', zip: 1524 },
26+
undefined,
27+
[undefined, { name: undefined, age: 25 }],
28+
[undefined, undefined, {}],
29+
{},
30+
[],
31+
{ blub: undefined, other: undefined },
32+
new Set(arr),
33+
]
34+
35+
const filteredObj = ConditionExpressionBuilder.deepFilter(obj, item => item !== undefined)
36+
expect(filteredObj).toEqual([{ street: 'street', zip: 1524 }, [{ age: 25 }], new Set([arr[0], arr[1]])])
37+
})
38+
2239
it('use property metadata', () => {
2340
const condition = ConditionExpressionBuilder.buildFilterExpression(
2441
'prop',

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

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AttributeMap, AttributeValue } from 'aws-sdk/clients/dynamodb'
2-
import { curryRight } from 'lodash'
2+
import { curryRight, forEach, isPlainObject } from 'lodash'
33
import { Metadata } from '../../decorator/metadata/metadata'
44
import { PropertyMetadata } from '../../decorator/metadata/property-metadata.model'
55
import { Mapper } from '../../mapper/mapper'
@@ -17,6 +17,53 @@ import { Expression } from './type/expression.type'
1717
* see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html
1818
*/
1919
export class ConditionExpressionBuilder {
20+
/**
21+
* Will walk the object tree recursively and removes all items which do not satisfy the filterFn
22+
* @param obj
23+
* @param {(value: any) => boolean} filterFn
24+
* @returns {any}
25+
*/
26+
static deepFilter(obj: any, filterFn: (value: any) => boolean): any | null {
27+
if (Array.isArray(obj)) {
28+
const returnArr: any[] = []
29+
obj.forEach(i => {
30+
const item = ConditionExpressionBuilder.deepFilter(i, filterFn)
31+
if (item !== null) {
32+
returnArr.push(item)
33+
}
34+
})
35+
36+
return returnArr.length ? returnArr : null
37+
} else if (obj instanceof Set) {
38+
const returnArr: any[] = []
39+
Array.from(<Set<any>>obj).forEach(i => {
40+
const item = ConditionExpressionBuilder.deepFilter(i, filterFn)
41+
if (item !== null) {
42+
returnArr.push(item)
43+
}
44+
})
45+
46+
return returnArr.length ? new Set(returnArr) : null
47+
} else if (isPlainObject(obj)) {
48+
const returnObj: { [key: string]: any } = {}
49+
50+
forEach(obj, (value: any, key: string) => {
51+
const item = ConditionExpressionBuilder.deepFilter(value, filterFn)
52+
if (item !== null) {
53+
returnObj[key] = item
54+
}
55+
})
56+
57+
return Object.keys(returnObj).length ? returnObj : null
58+
} else {
59+
if (filterFn(obj)) {
60+
return obj
61+
} else {
62+
return null
63+
}
64+
}
65+
}
66+
2067
/**
2168
* Will create a condition which can be added to a request using the param object.
2269
* It will create the expression statement and the attribute names and values.
@@ -35,10 +82,9 @@ export class ConditionExpressionBuilder {
3582
existingValueNames: string[] | undefined,
3683
metadata: Metadata<any> | undefined
3784
): Expression {
38-
// TODO investigate is there a use case for undefined desired to be a value
85+
// TODO LOW:INVESTIGATE is there a use case for undefined desired to be a value
3986
// get rid of undefined values
40-
// TODO should this not be a deep filter?
41-
values = values.filter(value => value !== undefined)
87+
values = ConditionExpressionBuilder.deepFilter(values, value => value !== undefined)
4288

4389
// check if provided values are valid for given operator
4490
ConditionExpressionBuilder.validateValues(operator, values)
@@ -202,9 +248,16 @@ export class ConditionExpressionBuilder {
202248
* the given values is not an array
203249
*/
204250
private static validateValues(operator: ConditionOperator, values?: any[]) {
205-
if (values && Array.isArray(values)) {
251+
const parameterArity = operatorParameterArity(operator)
252+
if (values === null || values === undefined) {
253+
if (!isNoParamFunctionOperator(operator)) {
254+
// the operator needs some values to work
255+
throw new Error(
256+
`expected ${parameterArity} value(s) for operator ${operator}, this is not the right amount of method parameters for this operator`
257+
)
258+
}
259+
} else if (values && Array.isArray(values)) {
206260
// check for correct amount of values
207-
const parameterArity = operatorParameterArity(operator)
208261
if (values.length !== parameterArity) {
209262
switch (operator) {
210263
case 'IN':
@@ -231,8 +284,6 @@ export class ConditionExpressionBuilder {
231284
}
232285
break
233286
}
234-
} else {
235-
throw new Error('values must be of type Array')
236287
}
237288
}
238289

src/dynamo/expression/logical-operator/update.function.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
/**
2-
* Use this method when accesing a top level attribute of a model
3-
*/
41
import { RequestExpressionBuilder } from '../request-expression-builder'
52
import { UpdateExpressionDefinitionChain } from '../type/update-expression-definition-chain'
63

4+
/**
5+
* Use this method when accesing a top level attribute of a model
6+
*/
77
export function update<T>(attributePath: keyof T): UpdateExpressionDefinitionChain
88

99
/**

0 commit comments

Comments
 (0)