Skip to content

Commit a76befa

Browse files
feat(expression-builder): cleanup validation and other
- split validation into two functions one for arity check and one for values, to be ready to add more rules in a clearer scope - add util to build templates on runtime, to build error messages with ${varName} syntax - remove unused comparator-operators.const.ts
1 parent 09f6ed7 commit a76befa

13 files changed

+158
-73
lines changed

src/dynamo/expression/comparator-operators.const.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

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

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@ import { has } from 'lodash'
22
import { ComplexModel } from '../../../test/models'
33
import { Model, PartitionKey, Property } from '../../decorator/impl'
44
import { metadataForClass } from '../../decorator/metadata'
5-
import { buildFilterExpression, deepFilter } from './condition-expression-builder'
5+
import { typeOf } from '../../mapper'
6+
import {
7+
buildFilterExpression,
8+
deepFilter,
9+
ERR_ARITY_DEFAULT,
10+
ERR_ARITY_IN,
11+
ERR_VALUES_BETWEEN_TYPE,
12+
ERR_VALUES_IN,
13+
} from './condition-expression-builder'
14+
import { operatorParameterArity } from './functions/operator-parameter-arity.function'
15+
import { ConditionOperator } from './type'
16+
import { dynamicTemplate } from './util'
617

718
@Model()
819
class MyModel {
@@ -264,24 +275,6 @@ describe('expressions', () => {
264275
expect(has(condition.attributeValues, ':creationDate_2')).toBeTruthy()
265276
expect(condition.attributeValues[':creationDate_2']).toEqual({ S: date2.toISOString() })
266277
})
267-
268-
it('should throw error for wrong value arity', () => {
269-
expect(() => buildFilterExpression('age', 'attribute_type', [], undefined, undefined)).toThrowError(
270-
'expected 1 value(s) for operator attribute_type, this is not the right amount of method parameters for this operator',
271-
)
272-
})
273-
274-
it('should throw error for wrong value arity', () => {
275-
expect(() => buildFilterExpression('age', 'attribute_type', [undefined], undefined, undefined)).toThrowError(
276-
'expected 1 value(s) for operator attribute_type, this is not the right amount of method parameters for this operator',
277-
)
278-
})
279-
280-
it('should throw error for wrong value type', () => {
281-
expect(() => buildFilterExpression('age', 'IN', ['myValue', 'mySecondValue'], undefined, undefined)).toThrowError(
282-
'expected 1 value(s) for operator IN, this is not the right amount of method parameters for this operator (IN operator requires one value of array type)',
283-
)
284-
})
285278
})
286279

287280
describe('operator nested attributes', () => {
@@ -312,4 +305,38 @@ describe('expressions', () => {
312305
expect(condition.attributeValues).toEqual({ ':person__birthdays_at_5__year': { N: '2016' } })
313306
})
314307
})
308+
309+
describe('validation', () => {
310+
describe('arity', () => {
311+
it('should throw default error for wrong arity', () => {
312+
const operator: ConditionOperator = 'attribute_type'
313+
expect(() => buildFilterExpression('age', operator, [], undefined, undefined)).toThrow(
314+
dynamicTemplate(ERR_ARITY_DEFAULT, { parameterArity: operatorParameterArity(operator), operator }),
315+
)
316+
})
317+
318+
it('should throw error for wrong IN arity', () => {
319+
const operator: ConditionOperator = 'IN'
320+
expect(() =>
321+
buildFilterExpression('age', operator, ['myValue', 'mySecondValue'], undefined, undefined),
322+
).toThrowError(dynamicTemplate(ERR_ARITY_IN, { parameterArity: operatorParameterArity(operator), operator }))
323+
})
324+
})
325+
326+
describe('operator values', () => {
327+
it('should throw error for wrong IN values', () => {
328+
const operator: ConditionOperator = 'IN'
329+
expect(() => buildFilterExpression('age', operator, ['myValue'], undefined, undefined)).toThrowError(
330+
ERR_VALUES_IN,
331+
)
332+
})
333+
334+
it('should throw error for wrong value type', () => {
335+
const operator: ConditionOperator = 'BETWEEN'
336+
expect(() => buildFilterExpression('age', operator, ['myValue', 2], undefined, undefined)).toThrowError(
337+
dynamicTemplate(ERR_VALUES_BETWEEN_TYPE, { value1: typeOf('myValue'), value2: typeOf(2) }),
338+
)
339+
})
340+
})
341+
})
315342
})

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

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { operatorParameterArity } from './functions/operator-parameter-arity.fun
1010
import { uniqAttributeValueName } from './functions/unique-attribute-value-name.function'
1111
import { ConditionOperator } from './type/condition-operator.type'
1212
import { Expression } from './type/expression.type'
13-
import { validateAttributeValue } from './update-expression-builder'
13+
import { validateAttributeType } from './update-expression-builder'
14+
import { dynamicTemplate } from './util'
1415

1516
type BuildFilterFn = (
1617
attributePath: string,
@@ -96,7 +97,7 @@ export function buildFilterExpression(
9697
values = deepFilter(values, value => value !== undefined)
9798

9899
// check if provided values are valid for given operator
99-
validateValues(operator, values)
100+
validateForOperator(operator, values)
100101

101102
// load property metadata if model metadata was provided
102103
let propertyMetadata: PropertyMetadata<any> | undefined
@@ -165,7 +166,7 @@ function buildInConditionExpression(
165166
.reduce(
166167
(result, mappedValue: Attribute | null, index: number) => {
167168
if (mappedValue !== null) {
168-
validateAttributeValue('IN condition', mappedValue, 'S', 'N', 'B')
169+
validateAttributeType('IN condition', mappedValue, 'S', 'N', 'B')
169170
result[`${valuePlaceholder}_${index}`] = mappedValue
170171
}
171172
return result
@@ -201,7 +202,7 @@ function buildBetweenConditionExpression(
201202
throw new Error('make sure to provide an actual value for te BETWEEN operator')
202203
}
203204
[mappedValue1, mappedValue2]
204-
.forEach(mv => validateAttributeValue('between', mv, 'S', 'N', 'B'))
205+
.forEach(mv => validateAttributeType('between', mv, 'S', 'N', 'B'))
205206

206207
const value2Placeholder = uniqAttributeValueName(attributePath, [valuePlaceholder].concat(existingValueNames || []))
207208

@@ -244,14 +245,14 @@ function buildDefaultConditionExpression(
244245
const attribute: Attribute | null = toDbOne(values[0], propertyMetadata)
245246
switch (operator) {
246247
case 'begins_with':
247-
validateAttributeValue(`${operator} condition`, attribute, 'S', 'B')
248+
validateAttributeType(`${operator} condition`, attribute, 'S', 'B')
248249
break
249250
case 'contains':
250251
case '<':
251252
case '<=':
252253
case '>':
253254
case '>=':
254-
validateAttributeValue(`${operator} condition`, attribute, 'N', 'S', 'B')
255+
validateAttributeType(`${operator} condition`, attribute, 'N', 'S', 'B')
255256
break
256257
}
257258

@@ -271,51 +272,85 @@ function buildDefaultConditionExpression(
271272
* Every operator requires a predefined arity of parameters, this method checks for the correct arity and throws an Error
272273
* if not correct
273274
*
274-
* @param {any[]} values The values which will be applied to the operator function implementation
275+
* @param operator
276+
* @param values The values which will be applied to the operator function implementation, not every operator requires values
275277
* @throws {Error} error Throws an error if the amount of values won't match the operator function parameter arity or
276278
* the given values is not an array
277279
*/
278-
function validateValues(operator: ConditionOperator, values?: any[]) {
279-
const parameterArity = operatorParameterArity(operator)
280+
function validateForOperator(operator: ConditionOperator, values?: any[]) {
281+
validateArity(operator, values)
282+
283+
/*
284+
* validate values if operator supports values
285+
*/
286+
if (!isFunctionOperator(operator) || isFunctionOperator(operator) && !isNoParamFunctionOperator(operator)) {
287+
if (values && Array.isArray(values) && values.length) {
288+
validateValues(operator, values)
289+
} else {
290+
// TODO
291+
throw new Error('blub')
292+
}
293+
}
294+
}
295+
296+
// tslint:disable:no-invalid-template-strings
297+
/*
298+
* error messages for arity issues
299+
*/
300+
export const ERR_ARITY_IN = 'expected ${parameterArity} value(s) for operator ${operator}, this is not the right amount of method parameters for this operator (IN operator requires one value of array type)'
301+
export const ERR_ARITY_DEFAULT = 'expected ${parameterArity} value(s) for operator ${operator}, this is not the right amount of method parameters for this operator'
302+
303+
// tslint:enable:no-invalid-template-strings
304+
305+
function validateArity(operator: ConditionOperator, values?: any[]) {
280306
if (values === null || values === undefined) {
281307
if (isFunctionOperator(operator) && !isNoParamFunctionOperator(operator)) {
282308
// the operator needs some values to work
283-
throw new Error(
284-
`expected ${parameterArity} value(s) for operator ${operator}, this is not the right amount of method parameters for this operator`,
285-
)
309+
throw new Error(dynamicTemplate(ERR_ARITY_DEFAULT, {parameterArity: operatorParameterArity(operator), operator}))
286310
}
287311
} else if (values && Array.isArray(values)) {
312+
const parameterArity = operatorParameterArity(operator)
288313
// check for correct amount of values
289314
if (values.length !== parameterArity) {
290315
switch (operator) {
291316
case 'IN':
292-
throw new Error(
293-
`expected ${parameterArity} value(s) for operator ${operator}, this is not the right amount of method parameters for this operator (IN operator requires one value of array type)`,
294-
)
317+
throw new Error(dynamicTemplate(ERR_ARITY_IN, { parameterArity, operator }))
295318
default:
296-
throw new Error(
297-
`expected ${parameterArity} value(s) for operator ${operator}, this is not the right amount of method parameters for this operator`,
298-
)
319+
throw new Error(dynamicTemplate(ERR_ARITY_DEFAULT, { parameterArity, operator }))
299320
}
300321
}
322+
}
323+
}
301324

302-
// some additional operator dependent validation
303-
switch (operator) {
304-
case 'BETWEEN':
305-
// values must be the same type
306-
if (typeOf(values[0]) !== typeOf(values[1])) {
307-
throw new Error(
308-
`both values for operator BETWEEN must have the same type, got ${typeOf(values[0])} and ${typeOf(
309-
values[1],
310-
)}`,
311-
)
312-
}
313-
break
314-
case 'IN':
315-
if (!Array.isArray(values[0])) {
316-
throw new Error('the provided value for IN operator must be an array')
317-
}
318-
}
325+
326+
/*
327+
* error message for wrong operator values
328+
*/
329+
// tslint:disable:no-invalid-template-strings
330+
export const ERR_VALUES_BETWEEN_TYPE = 'both values for operator BETWEEN must have the same type, got ${value1} and ${value2}'
331+
export const ERR_VALUES_IN = 'the provided value for IN operator must be an array'
332+
// tslint:enable:no-invalid-template-strings
333+
334+
/**
335+
* Every operator has some constraints about the values it supports, this method makes sure everything is fine for given
336+
* operator and values
337+
*/
338+
function validateValues(operator: ConditionOperator, values: any[]) {
339+
// some additional operator dependent validation
340+
switch (operator) {
341+
case 'BETWEEN':
342+
// values must be the same type
343+
if (typeOf(values[0]) !== typeOf(values[1])) {
344+
throw new Error(dynamicTemplate(
345+
ERR_VALUES_BETWEEN_TYPE,
346+
{ value1: typeOf(values[0]), value2: typeOf(values[1]) }
347+
))
348+
}
349+
break
350+
case 'IN':
351+
if (!Array.isArray(values[0])) {
352+
throw new Error(ERR_VALUES_IN)
353+
}
319354
}
320355
}
321356

src/dynamo/expression/functions/operator-parameter-arity.function.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { isFunctionOperator } from './is-function-operator.function'
33
import { isNoParamFunctionOperator } from './is-no-param-function-operator.function'
44

55
/**
6-
* Every expression condition operator has a predefined arity (amount) of function paramers, this method
6+
* Every expression condition operator has a predefined arity (amount) of function parameters, this method
77
* returns this value
88
*
99
* @returns {number} The amount of required method parameters when calling an operator function
@@ -27,7 +27,7 @@ export function operatorParameterArity(operator: ConditionOperator): number {
2727
case 'BETWEEN':
2828
return 2
2929
default:
30-
throw new Error(`no parameter arity defined for opererator ${operator}`)
30+
throw new Error(`no parameter arity defined for operator ${operator}`)
3131
}
3232
}
3333
}

src/dynamo/expression/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './logical-operator'
22
export * from './type'
3+
export * from './util'

src/dynamo/expression/type/update-action-def.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import { UpdateActionKeyword } from './update-action-keyword.type'
22
import { UpdateAction } from './update-action.type'
33

44
export class UpdateActionDef {
5-
actionKeyword: UpdateActionKeyword
6-
action: UpdateAction
7-
8-
constructor(actionKeyWord: UpdateActionKeyword, action: UpdateAction) {
9-
this.actionKeyword = actionKeyWord
10-
this.action = action
5+
constructor(public actionKeyword: UpdateActionKeyword, public action: UpdateAction) {
116
}
127
}

src/dynamo/expression/type/update-action.type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
*
33
* update expressions support these 4 base operations:
4+
* https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html
45
*
56
* update-expression ::=
67
* [ SET action [, action] ... ]

src/dynamo/expression/type/update-expression-definition-chain.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ export interface UpdateFunctions<T, R> {
2525
* samples:
2626
* - persons.age
2727
* - places[0].address.street
28+
*
29+
* specify ifNotExists to only execute if the property does not exist
2830
*/
2931
set: (value: T, ifNotExists?: boolean) => R
3032

3133
/**
32-
* appends one or more values to the start or end of a list, value must be of type L(ist)
34+
* appends one or more values to the start or end of a list, value must map to ListAttribute
3335
*/
3436
appendToList: (value: T | Array<ExtractListType<T>> | Set<ExtractListType<T>>, position?: 'START' | 'END') => R
3537

@@ -45,7 +47,7 @@ export interface UpdateFunctions<T, R> {
4547

4648
/**
4749
* adds or manipulates a value to an attribute of type N(umber) or S(et), manipulation behaviour differs based on attribute type
48-
* for numbers AWS generally recommends to use SET rather than ADD. See incrementBy and decrementBy
50+
* for numbers AWS generally recommends to use SET rather than ADD. See incrementBy and decrementBy.
4951
*
5052
* @param values {multiple values as Array | Set}
5153
*

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('buildUpdateExpression', () => {
3030
expect(() => buildUpdateExpression('age', op, ['notANumber'], [], metaDataS)).toThrow()
3131
})
3232
})
33+
3334
describe('decrementBy', () => {
3435
const op = new UpdateActionDef('SET', 'decrementBy')
3536
it('should build expression', () => {
@@ -80,6 +81,7 @@ describe('buildUpdateExpression', () => {
8081
it('should throw when not number or a set value', () => {
8182
expect(() => buildUpdateExpression('age', op, ['notANumber'], [], metaDataS)).toThrow()
8283
})
84+
8385
it('should throw when no value for attributeValue was given', () => {
8486
expect(() => buildUpdateExpression('age', op, [], [], metaDataS)).toThrow()
8587
})

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,15 @@ function buildDefaultExpression(
8181
}
8282
}
8383

84+
// see update-expression-definition-chain.ts for action definitions
8485
let statement: string
8586
switch (operator.action) {
8687
case 'incrementBy':
87-
validateAttributeValue(operator.action, attribute, 'N')
88+
validateAttributeType(operator.action, attribute, 'N')
8889
statement = `${namePlaceholder} = ${namePlaceholder} + ${valuePlaceholder}`
8990
break
9091
case 'decrementBy':
91-
validateAttributeValue(operator.action, attribute, 'N')
92+
validateAttributeType(operator.action, attribute, 'N')
9293
statement = `${namePlaceholder} = ${namePlaceholder} - ${valuePlaceholder}`
9394
break
9495
case 'set':
@@ -112,11 +113,11 @@ function buildDefaultExpression(
112113
statement = values.map(pos => `${namePlaceholder}[${pos}]`).join(', ')
113114
break
114115
case 'add':
115-
validateAttributeValue(operator.action, attribute, 'N', 'SS', 'NS', 'BS')
116+
validateAttributeType(operator.action, attribute, 'N', 'SS', 'NS', 'BS')
116117
statement = `${namePlaceholder} ${valuePlaceholder}`
117118
break
118119
case 'removeFromSet':
119-
validateAttributeValue(operator.action, attribute, 'SS', 'NS', 'BS')
120+
validateAttributeType(operator.action, attribute, 'SS', 'NS', 'BS')
120121
statement = `${namePlaceholder} ${valuePlaceholder}`
121122
break
122123
default:
@@ -139,12 +140,12 @@ function isNoAttributeValueAction(action: UpdateAction) {
139140
)
140141
}
141142

142-
export function validateAttributeValue(name: string, attributeValue: Attribute | null, ...allowedTypes: AttributeType[]) {
143-
if (attributeValue === null || attributeValue === undefined) {
143+
export function validateAttributeType(name: string, attribute: Attribute | null, ...allowedTypes: AttributeType[]) {
144+
if (attribute === null || attribute === undefined) {
144145
throw new Error(`${name} requires an attributeValue of ${allowedTypes.join(', ')} but non was given`)
145146
}
146-
const key = <AttributeType>Object.keys(attributeValue)[0]
147+
const key = <AttributeType>Object.keys(attribute)[0]
147148
if (!allowedTypes.includes(key)) {
148-
throw new Error(`Type ${key} of ${JSON.stringify(attributeValue)} is not allowed for ${name}. Valid types are: ${allowedTypes.join('. ')}`)
149+
throw new Error(`Type ${key} of ${JSON.stringify(attribute)} is not allowed for ${name}. Valid types are: ${allowedTypes.join('. ')}`)
149150
}
150151
}

0 commit comments

Comments
 (0)