module import { Metadata } from '../../decorator/metadata/metadata' ; import { alterCollectionPropertyMetadataForSingleItem , PropertyMetadata , } from '../../decorator/metadata/property-metadata.model' ; import { curry } from '../../helper/curry.function' ; import { isPlainObject } from '../../helper/is-plain-object.function' ; import { toDbOne } from '../../mapper/mapper' ; import { Attribute , Attributes } from '../../mapper/type/attribute.type' ; import { typeOf } from '../../mapper/util' ; import { resolveAttributeNames } from './functions/attribute-names.function' ; import { isFunctionOperator } from './functions/is-function-operator.function' ; import { isNoParamFunctionOperator } from './functions/is-no-param-function-operator.function' ; import { operatorParameterArity } from './functions/operator-parameter-arity.function' ; import { uniqueAttributeValueName } from './functions/unique-attribute-value-name.function' ; import { ConditionOperator } from './type/condition-operator.type' ; import { Expression } from './type/expression.type' ; import { validateAttributeType } from './update-expression-builder' ; import { dynamicTemplate } from './util' ; hidden type BuildFilterFn = ( attributePath : string , namePlaceholder : string , valuePlaceholder : string , attributeNames : Record < string , string > , values : any [ ] , existingValueNames : string [ ] | undefined , propertyMetadata : PropertyMetadata < any > | undefined ) => Expression ; /**
 * see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html
 * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
 */ param obj param { ( value : any ) => boolean } filterFn returns { any } hidden export function deepFilter ( obj , filterFn ) { if ( Array . isArray ( obj ) ) { const returnArr = [ ] ; obj . forEach ( ( i ) => { const item = deepFilter ( i , filterFn ) ; if ( item !== null ) { returnArr . push ( item ) ; } } ) ; return returnArr . length ? returnArr : null ; } else if ( obj instanceof Set ) { const returnArr = [ ] ; Array . from ( obj ) . forEach ( ( i ) => { const item = deepFilter ( i , filterFn ) ; if ( item !== null ) { returnArr . push ( item ) ; } } ) ; return returnArr . length ? new Set ( returnArr ) : null ; } else if ( isPlainObject ( obj ) ) { const returnObj = { } ; for ( const key in obj ) { if ( obj . hasOwnProperty ( key ) ) { const value = obj [ key ] ; const item = deepFilter ( value , filterFn ) ; if ( item !== null ) { returnObj [ key ] = item ; } } } return Object . keys ( returnObj ) . length ? returnObj : null ; } else { if ( filterFn ( obj ) ) { return obj ; } else { return null ; } } } param { string } attributePath param { ConditionOperator } operator param { any [ ] } values param { string [ ] } existingValueNames param { Metadata < any > } metadata returns { Expression } hidden export function buildFilterExpression ( attributePath , operator , values , existingValueNames , metadata ) { values = deepFilter ( values , ( value ) => value !== undefined ) ; validateForOperator ( operator , values ) ; let propertyMetadata ; if ( metadata ) { propertyMetadata = metadata . forProperty ( attributePath ) ; } const resolvedAttributeNames = resolveAttributeNames ( attributePath , metadata ) ; const valuePlaceholder = uniqueAttributeValueName ( attributePath , existingValueNames ) ; let buildFilterFn ; switch ( operator ) { case 'IN' : buildFilterFn = buildInConditionExpression ; break ; case 'BETWEEN' : buildFilterFn = buildBetweenConditionExpression ; break ; default : buildFilterFn = curry ( buildDefaultConditionExpression ) ( operator ) ; } return buildFilterFn ( attributePath , resolvedAttributeNames . placeholder , valuePlaceholder , resolvedAttributeNames . attributeNames , values , existingValueNames , propertyMetadata ) ; } param { string } attributePath param { string [ ] } values param { string [ ] } existingValueNames param { PropertyMetadata < any > } propertyMetadata returns { Expression } hidden function buildInConditionExpression ( attributePath , namePlaceholder , valuePlaceholder , attributeNames , values , existingValueNames , propertyMetadata ) { const attributeValues = ( < any [ ] > values [ 0 ] ) . map ( ( value ) => toDbOne ( value , propertyMetadata ) ) . reduce ( ( result , mappedValue , index ) => { if ( mappedValue !== null ) { validateAttributeType ( 'IN condition' , mappedValue , 'S' , 'N' , 'B' ) ; result [ `${ valuePlaceholder }_${ index }` ] = mappedValue ; } return result ; } , < Attributes < any > > { } ) ; const inStatement = ( < any [ ] > values [ 0 ] ) . map ( ( value , index ) => `${ valuePlaceholder }_${ index }` ) . join ( ', ' ) ; return { statement : `${ namePlaceholder } IN (${ inStatement })` , attributeNames , attributeValues , } ; } hidden function buildBetweenConditionExpression ( attributePath , namePlaceholder , valuePlaceholder , attributeNames , values , existingValueNames , propertyMetadata ) { const attributeValues = { } ; const mappedValue1 = toDbOne ( values [ 0 ] , propertyMetadata ) ; const mappedValue2 = toDbOne ( values [ 1 ] , propertyMetadata ) ; if ( mappedValue1 === null || mappedValue2 === null ) { throw new Error ( 'make sure to provide an actual value for te BETWEEN operator' ) ; } ; [ mappedValue1 , mappedValue2 ] . forEach ( ( mv ) => validateAttributeType ( 'between' , mv , 'S' , 'N' , 'B' ) ) ; const value2Placeholder = uniqueAttributeValueName ( attributePath , [ valuePlaceholder ] . concat ( existingValueNames || [ ] ) ) ; const statement = `${ namePlaceholder } BETWEEN ${ valuePlaceholder } AND ${ value2Placeholder }` ; attributeValues [ valuePlaceholder ] = mappedValue1 ; attributeValues [ value2Placeholder ] = mappedValue2 ; return { statement , attributeNames , attributeValues , } ; } hidden function buildDefaultConditionExpression ( operator , attributePath , namePlaceholder , valuePlaceholder , attributeNames , values , existingValueNames , propertyMetadata ) { let statement ; let hasValue = true ; if ( isFunctionOperator ( operator ) ) { if ( isNoParamFunctionOperator ( operator ) ) { statement = `${ operator } (${ namePlaceholder })` ; hasValue = false ; } else { statement = `${ operator } (${ namePlaceholder }, ${ valuePlaceholder })` ; } } else { statement = [ namePlaceholder , operator , valuePlaceholder ] . join ( ' ' ) ; } const attributeValues = { } ; if ( hasValue ) { let attribute ; if ( operator === 'contains' || operator === 'not_contains' ) { attribute = toDbOne ( values [ 0 ] , alterCollectionPropertyMetadataForSingleItem ( propertyMetadata ) ) ; validateAttributeType ( `${ operator } condition` , attribute , 'N' , 'S' , 'B' ) ; } else { attribute = toDbOne ( values [ 0 ] , propertyMetadata ) ; switch ( operator ) { case 'begins_with' : validateAttributeType ( `${ operator } condition` , attribute , 'S' , 'B' ) ; break ; case '<' : case '<=' : case '>' : case '>=' : validateAttributeType ( `${ operator } condition` , attribute , 'N' , 'S' , 'B' ) ; break ; } } if ( attribute ) { attributeValues [ valuePlaceholder ] = attribute ; } } return { statement , attributeNames , attributeValues , } ; } param operator param values throws hidden function validateForOperator ( operator , values ? ) { validateArity ( operator , values ) ; if ( ! isFunctionOperator ( operator ) || ( isFunctionOperator ( operator ) && ! isNoParamFunctionOperator ( operator ) ) ) { if ( values && Array . isArray ( values ) && values . length ) { validateValues ( operator , values ) ; } else { throw new Error ( dynamicTemplate ( ERR_ARITY_DEFAULT , { parameterArity : operatorParameterArity ( operator ) , operator } ) ) ; } } } hidden 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)' ; hidden export const ERR_ARITY_DEFAULT = 'expected ${parameterArity} value(s) for operator ${operator}, this is not the right amount of method parameters for this operator' ; hidden function validateArity ( operator , values ? ) { if ( values === null || values === undefined ) { if ( isFunctionOperator ( operator ) && ! isNoParamFunctionOperator ( operator ) ) { throw new Error ( dynamicTemplate ( ERR_ARITY_DEFAULT , { parameterArity : operatorParameterArity ( operator ) , operator } ) ) ; } } else if ( values && Array . isArray ( values ) ) { const parameterArity = operatorParameterArity ( operator ) ; if ( values . length !== parameterArity ) { switch ( operator ) { case 'IN' : throw new Error ( dynamicTemplate ( ERR_ARITY_IN , { parameterArity , operator } ) ) ; default : throw new Error ( dynamicTemplate ( ERR_ARITY_DEFAULT , { parameterArity , operator } ) ) ; } } } } hidden export const ERR_VALUES_BETWEEN_TYPE = 'both values for operator BETWEEN must have the same type, got ${value1} and ${value2}' ; hidden export const ERR_VALUES_IN = 'the provided value for IN operator must be an array' ; hidden function validateValues ( operator , values ) { switch ( operator ) { case 'BETWEEN' : if ( typeOf ( values [ 0 ] ) !== typeOf ( values [ 1 ] ) ) { throw new Error ( dynamicTemplate ( ERR_VALUES_BETWEEN_TYPE , { value1 : typeOf ( values [ 0 ] ) , value2 : typeOf ( values [ 1 ] ) } ) ) ; } break ; case 'IN' : if ( ! Array . isArray ( values [ 0 ] ) ) { throw new Error ( ERR_VALUES_IN ) ; } } }