Skip to content

Commit 6f2f338

Browse files
feat(updateRequest): add support for fluent condition expression api in update request
1 parent 357b3b1 commit 6f2f338

28 files changed

+543
-80
lines changed

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { isFunctionOperator } from './functions/is-function-operator.function'
1010
import { isNoParamFunctionOperator } from './functions/is-no-param-function-operator.function'
1111
import { operatorParameterArity } from './functions/operator-parameter-arity.function'
1212
import { uniqAttributeValueName } from './functions/unique-attribute-value-name.function'
13-
import { ConditionExpression } from './type/condition-expression.type'
1413
import { ConditionOperator } from './type/condition-operator.type'
14+
import { Expression } from './type/expression.type'
1515

1616
/**
1717
* TODO complete doc
@@ -27,15 +27,15 @@ export class ConditionExpressionBuilder {
2727
* @param {any[]} values Depending on the operator the amount of values differs
2828
* @param {string[]} existingValueNames If provided the existing names are used to make sure we have a unique name for the current attributePath
2929
* @param {Metadata<any>} metadata If provided we use the metadata to define the attribute name and use it to map the given value(s) to attributeValue(s)
30-
* @returns {ConditionExpression}
30+
* @returns {Expression}
3131
*/
3232
static buildFilterExpression(
3333
attributePath: string,
3434
operator: ConditionOperator,
3535
values: any[],
3636
existingValueNames: string[] | undefined,
3737
metadata: Metadata<any> | undefined
38-
): ConditionExpression {
38+
): Expression {
3939
// TODO investigate is there a use case for undefined desired to be a value
4040
// get rid of undefined values
4141
values = values.filter(value => value !== undefined)
@@ -94,7 +94,7 @@ export class ConditionExpressionBuilder {
9494
* @param {string[]} values
9595
* @param {string[]} existingValueNames
9696
* @param {PropertyMetadata<any>} propertyMetadata
97-
* @returns {ConditionExpression}
97+
* @returns {Expression}
9898
*/
9999
private static buildInConditionExpression(
100100
attributePath: string,
@@ -104,7 +104,7 @@ export class ConditionExpressionBuilder {
104104
values: any[],
105105
existingValueNames: string[] | undefined,
106106
propertyMetadata: PropertyMetadata<any> | undefined
107-
): ConditionExpression {
107+
): Expression {
108108
const mappedValues = Mapper.toDbOne(values[0], propertyMetadata)
109109

110110
const attributeValues: AttributeMap = {}
@@ -125,7 +125,7 @@ export class ConditionExpressionBuilder {
125125
values: string[],
126126
existingValueNames: string[] | undefined,
127127
propertyMetadata: PropertyMetadata<any> | undefined
128-
): ConditionExpression {
128+
): Expression {
129129
const attributeValues: AttributeMap = {}
130130
const mappedValue1 = Mapper.toDbOne(values[0], propertyMetadata)
131131
const mappedValue2 = Mapper.toDbOne(values[1], propertyMetadata)
@@ -156,7 +156,7 @@ export class ConditionExpressionBuilder {
156156
existingValueNames: string[] | undefined,
157157
propertyMetadata: PropertyMetadata<any> | undefined,
158158
operator: ConditionOperator
159-
): ConditionExpression {
159+
): Expression {
160160
let statement: string
161161
let hasValue = true
162162
if (isFunctionOperator(operator)) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ConditionExpressionDefinitionFunction } from '../type/condition-expression-definition-function'
2-
import { ConditionExpression } from '../type/condition-expression.type'
2+
import { Expression } from '../type/expression.type'
33
import { mergeConditions } from './merge-conditions.function'
44

55
export function and(

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import { ExpressionAttributeValueMap } from 'aws-sdk/clients/dynamodb'
2-
import { curry } from 'lodash'
3-
import { Metadata } from '../../../decorator/metadata/metadata'
4-
import { PropertyMetadata } from '../../../decorator/metadata/property-metadata.model'
5-
import { AttributeType } from '../../../mapper/type/attribute.type'
6-
import { ConditionExpressionBuilder } from '../condition-expression-builder'
71
import { RequestExpressionBuilder } from '../request-expression-builder'
8-
import { ConditionExpressionChain } from '../type/condition-expression-chain'
92
import { ConditionExpressionDefinitionChain } from '../type/condition-expression-definition-chain'
10-
import { ConditionExpressionDefinitionFunction } from '../type/condition-expression-definition-function'
11-
import { ConditionExpression } from '../type/condition-expression.type'
12-
import { ConditionOperator } from '../type/condition-operator.type'
133

4+
/**
5+
* Use this method when accesing a top level attribute of a model
6+
*/
147
export function attribute<T>(attributePath: keyof T): ConditionExpressionDefinitionChain
8+
9+
/**
10+
* Use this method when accessing a nested attribute of a model
11+
*/
1512
export function attribute(attributePath: string): ConditionExpressionDefinitionChain
1613

1714
export function attribute<T>(attributePath: keyof T): ConditionExpressionDefinitionChain {

src/dynamo/expression/logical-operator/merge-conditions.function.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,14 @@ import { Metadata } from '../../../decorator/metadata/metadata'
33
import { ConditionExpressionBuilder } from '../condition-expression-builder'
44
import { uniqAttributeValueName } from '../functions/unique-attribute-value-name.function'
55
import { ConditionExpressionDefinitionFunction } from '../type/condition-expression-definition-function'
6-
import { ConditionExpression } from '../type/condition-expression.type'
6+
import { Expression } from '../type/expression.type'
77

88
export function mergeConditions(
99
operator: 'AND' | 'OR',
1010
conditionDefinitionFns: ConditionExpressionDefinitionFunction[]
1111
): ConditionExpressionDefinitionFunction {
12-
return (
13-
expressionAttributeValues: string[] | undefined,
14-
metadata: Metadata<any> | undefined
15-
): ConditionExpression => {
16-
const mergedCondition: ConditionExpression = {
12+
return (expressionAttributeValues: string[] | undefined, metadata: Metadata<any> | undefined): Expression => {
13+
const mergedCondition: Expression = {
1714
statement: '',
1815
attributeNames: {},
1916
attributeValues: {},

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { Metadata } from '../../../decorator/metadata/metadata'
22
import { ConditionExpressionDefinitionFunction } from '../type/condition-expression-definition-function'
3-
import { ConditionExpression } from '../type/condition-expression.type'
3+
import { Expression } from '../type/expression.type'
44
import { mergeConditions } from './merge-conditions.function'
55

66
export function not(
77
conditionDefinitionFn: ConditionExpressionDefinitionFunction
88
): ConditionExpressionDefinitionFunction {
9-
return (
10-
expressionAttributeValues: string[] | undefined,
11-
metadata: Metadata<any> | undefined
12-
): ConditionExpression => {
9+
return (expressionAttributeValues: string[] | undefined, metadata: Metadata<any> | undefined): Expression => {
1310
const condition = conditionDefinitionFn(expressionAttributeValues, metadata)
1411
condition.statement = `NOT ${condition.statement}`
1512
return condition

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ConditionExpressionDefinitionFunction } from '../type/condition-expression-definition-function'
2-
import { ConditionExpression } from '../type/condition-expression.type'
2+
import { Expression } from '../type/expression.type'
33
import { mergeConditions } from './merge-conditions.function'
44

55
export function or(
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Use this method when accesing a top level attribute of a model
3+
*/
4+
import { RequestExpressionBuilder } from '../request-expression-builder'
5+
import { UpdateExpressionDefinitionChain } from '../type/update-expression-definition-chain'
6+
7+
export function update<T>(attributePath: keyof T): UpdateExpressionDefinitionChain
8+
9+
/**
10+
* Use this method when accessing a nested attribute of a model
11+
*/
12+
export function update(attributePath: string): UpdateExpressionDefinitionChain
13+
14+
export function update<T>(attributePath: keyof T): UpdateExpressionDefinitionChain {
15+
return RequestExpressionBuilder.updateDefinitionFunction<T>(attributePath)
16+
}

src/dynamo/expression/param-util.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import {
33
ExpressionAttributeValueMap,
44
QueryInput,
55
ScanInput,
6+
UpdateItemInput,
67
} from 'aws-sdk/clients/dynamodb'
78
import { isEmpty, isString } from 'lodash'
8-
import { ConditionExpression } from './type/condition-expression.type'
9+
import { UpdateRequest } from '../request/update/update.request'
10+
import { Expression } from './type/expression.type'
911

1012
export class ParamUtil {
1113
static addExpression(
1214
expressionType: 'ConditionExpression' | 'KeyConditionExpression' | 'FilterExpression',
13-
condition: ConditionExpression,
15+
condition: Expression,
1416
params: QueryInput | ScanInput
1517
) {
1618
const expressionAttributeNames = <ExpressionAttributeNameMap>{
@@ -41,4 +43,35 @@ export class ParamUtil {
4143
;(<any>params)[expressionType] = condition.statement
4244
}
4345
}
46+
47+
static addUpdateExpression(updateExpression: Expression, params: UpdateItemInput) {
48+
const expressionType = 'UpdateExpression'
49+
// FIXME refactor remove duplicate code
50+
const expressionAttributeNames = <ExpressionAttributeNameMap>{
51+
...updateExpression.attributeNames,
52+
...params.ExpressionAttributeNames,
53+
}
54+
55+
const expressionAttributeValues = <ExpressionAttributeValueMap>{
56+
...updateExpression.attributeValues,
57+
...params.ExpressionAttributeValues,
58+
}
59+
60+
if (!isEmpty(expressionAttributeNames)) {
61+
params.ExpressionAttributeNames = expressionAttributeNames
62+
}
63+
64+
if (!isEmpty(expressionAttributeValues)) {
65+
params.ExpressionAttributeValues = expressionAttributeValues
66+
}
67+
68+
const expression = Reflect.get(params, expressionType)
69+
if (isString(expression) && expression !== '') {
70+
throw new Error(
71+
'params.UpdateExpression is not empty, please use the UpdateRequest.operations() method to define all the update operations'
72+
)
73+
} else {
74+
;(<any>params)[expressionType] = updateExpression.statement
75+
}
76+
}
4477
}

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import { curry } from 'lodash'
22
import { Metadata } from '../../decorator/metadata/metadata'
3-
import { PropertyMetadata } from '../../decorator/metadata/property-metadata.model'
43
import { BaseRequest } from '../request/base.request'
54
import { ConditionExpressionBuilder } from './condition-expression-builder'
65
import { ParamUtil } from './param-util'
76
import { ConditionExpressionDefinitionChain } from './type/condition-expression-definition-chain'
87
import { ConditionExpressionDefinitionFunction } from './type/condition-expression-definition-function'
9-
import { ConditionExpression } from './type/condition-expression.type'
108
import { OperatorAlias } from './type/condition-operator-alias.type'
119
import { OPERATOR_TO_ALIAS_MAP } from './type/condition-operator-to-alias-map.const'
1210
import { ConditionOperator } from './type/condition-operator.type'
1311
import { ExpressionType } from './type/expression-type.type'
12+
import { Expression } from './type/expression.type'
1413
import { RequestConditionFunction } from './type/request-condition-function'
1514
import { RequestSortKeyConditionFunction } from './type/sort-key-condition-function'
15+
import { UpdateAction, UpdateActionDef } from './type/update-action.type'
16+
import { UpdateExpressionDefinitionChain } from './type/update-expression-definition-chain'
17+
import { UpdateExpressionDefinitionFunction } from './type/update-expression-definition-function'
18+
import { UpdateExpression } from './type/update-expression.type'
19+
import { UpdateExpressionBuilder } from './update-expression-builder'
1620

1721
/**
1822
*
@@ -98,11 +102,35 @@ export class RequestExpressionBuilder {
98102
return RequestExpressionBuilder.addSortKeyCondition(keyName, request, metadata).equals(keyValue)
99103
}
100104

105+
// FIXME attributePath could also be a simple string for nested paths
106+
static updateDefinitionFunction(attributePath: string): UpdateExpressionDefinitionChain
107+
static updateDefinitionFunction<T>(attributePath: keyof T): UpdateExpressionDefinitionChain
108+
109+
static updateDefinitionFunction<T>(attributePath: keyof T): UpdateExpressionDefinitionChain {
110+
const f = (operation: UpdateActionDef) => {
111+
return (...values: any[]): UpdateExpressionDefinitionFunction => {
112+
const copy = [...values]
113+
const curried = curry<
114+
string,
115+
UpdateActionDef,
116+
any[],
117+
string[] | undefined,
118+
Metadata<any> | undefined,
119+
UpdateExpression
120+
>(UpdateExpressionBuilder.buildUpdateExpression)
121+
122+
return curried(attributePath, operation, copy)
123+
}
124+
}
125+
126+
return RequestExpressionBuilder.createUpdateFunctions<UpdateExpressionDefinitionChain>(f)
127+
}
128+
101129
static propertyDefinitionFunction<T>(attributePath: keyof T): ConditionExpressionDefinitionChain {
102130
const f = (operator: ConditionOperator) => {
103131
return (...values: any[]): ConditionExpressionDefinitionFunction => {
104132
const copy = [...values]
105-
const curried = curry<string, ConditionOperator, any[], string[], Metadata<any>, ConditionExpression>(
133+
const curried = curry<string, ConditionOperator, any[], string[], Metadata<any>, Expression>(
106134
ConditionExpressionBuilder.buildFilterExpression
107135
)
108136
return curried(attributePath, operator, copy)
@@ -112,6 +140,30 @@ export class RequestExpressionBuilder {
112140
return RequestExpressionBuilder.createConditionFunctions<ConditionExpressionDefinitionChain>(f)
113141
}
114142

143+
/**
144+
* Creates an object which contains callable functions for all update operations defined in update-operation type
145+
* for all the values included in operators
146+
*
147+
* @param {(operator: ConditionOperator) => any} impl The function which is called with the operator and returns a function which expects the value
148+
* for the condition. when executed the implementation defines what todo with the condition, just return it for example or add the condition to the request
149+
* parameters as another example
150+
*/
151+
private static createUpdateFunctions<T>(impl: (operation: UpdateActionDef) => any): T {
152+
// FIXME add all operators
153+
return <T>[
154+
new UpdateActionDef('SET', 'incrementBy'),
155+
new UpdateActionDef('SET', 'decrementBy'),
156+
new UpdateActionDef('SET', 'set'),
157+
].reduce(
158+
(result: T, updateActionDef: UpdateActionDef) => {
159+
Reflect.set(<any>result, updateActionDef.action, impl(updateActionDef))
160+
161+
return result
162+
},
163+
<T>{}
164+
)
165+
}
166+
115167
/**
116168
* Creates an object which contains callable functions for all aliases defined in CONDITION_OPERATOR_ALIAS or if operators parameter is defined,
117169
* for all the values included in operators
Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { AttributeType } from '../../../mapper/type/attribute.type'
2-
import { ConditionExpression } from './condition-expression.type'
2+
import { Expression } from './expression.type'
33

44
export interface ConditionExpressionChain {
5-
equals: (value: any) => ConditionExpression
6-
eq: (value: any) => ConditionExpression
7-
ne: (value: any) => ConditionExpression
8-
lte: (value: any) => ConditionExpression
9-
lt: (value: any) => ConditionExpression
10-
gte: (value: any) => ConditionExpression
11-
gt: (value: any) => ConditionExpression
12-
null: () => ConditionExpression
13-
notNull: () => ConditionExpression
14-
contains: (value: any) => ConditionExpression
15-
notContains: (value: any) => ConditionExpression
16-
type: (value: AttributeType) => ConditionExpression
17-
in: (value: any[]) => ConditionExpression
18-
beginsWith: (value: any) => ConditionExpression
19-
between: (value1: any, value2: any) => ConditionExpression
5+
equals: (value: any) => Expression
6+
eq: (value: any) => Expression
7+
ne: (value: any) => Expression
8+
lte: (value: any) => Expression
9+
lt: (value: any) => Expression
10+
gte: (value: any) => Expression
11+
gt: (value: any) => Expression
12+
null: () => Expression
13+
notNull: () => Expression
14+
contains: (value: any) => Expression
15+
notContains: (value: any) => Expression
16+
type: (value: AttributeType) => Expression
17+
in: (value: any[]) => Expression
18+
beginsWith: (value: any) => Expression
19+
between: (value1: any, value2: any) => Expression
2020
}

0 commit comments

Comments
 (0)