import { NameMapper } from './mappers' ; import { CompositeCondition , ConditionValue , AndCondition , OrCondition , NotCondition , KeyPathsAndClause , } from './conditions' ; import { InvalidCompositeConditionException } from './exceptions' ; import { ParseElement , ConditionParseContext } from './conditions_types' ; internal const isConditionKey = ( key ) : boolean => [ '$or' , '$and' , '$eq' , '$neq' , '$gt' , '$gte' , '$lt' , '$lte' , '$between' , '$in' , '$exists' , '$type' , '$beginsWith' , '$contains' , ] . includes ( key ) ; internal const mapKeyPath = ( key , nameMapper ) : string => [ nameMapper . map ( 'value' , '#value' ) , ... key . split ( '.' ) . map ( ( pathElement ) => nameMapper . map ( pathElement ) ) , ] . join ( '.' ) ; internal param clause param context returns export const parseCompositeCondition = ( clause , context ) : string => { if ( Object . keys ( clause ) . length === 1 ) { if ( '$and' in clause ) { return parseAndCondition ( clause as AndCondition , context ) ; } else if ( '$or' in clause ) { return parseOrCondition ( clause as OrCondition , context ) ; } else if ( '$not' in clause ) { return parseNotCondition ( clause as NotCondition , context ) ; } } return parseKeyPathsAndClause ( clause as KeyPathsAndClause , context ) ; } ; internal const parseAndCondition = ( clause , context ) : string => { const parsePath = [ ... context . parsePath , { type : 'object' , key : '$and' } , ] ; const updatedContextForIndex = ( index ) : ConditionParseContext => ( { ... context , parsePath : [ ... parsePath , { type : 'array' , index } ] , } ) ; const subclauses = clause . $and . map ( ( clause , index ) => parseCompositeCondition ( clause , updatedContextForIndex ( index ) ) ) ; return subclauses . map ( ( clause ) => `(${ clause })` ) . join ( ' AND ' ) ; } ; internal const parseOrCondition = ( clause , context ) : string => { const parsePath = [ ... context . parsePath , { type : 'object' , key : '$or' } , ] ; const updatedContextForIndex = ( index ) : ConditionParseContext => ( { ... context , parsePath : [ ... parsePath , { type : 'array' , index } ] , } ) ; const subclauses = clause . $or . map ( ( clause , index ) => parseCompositeCondition ( clause , updatedContextForIndex ( index ) ) ) ; return subclauses . map ( ( clause ) => `(${ clause })` ) . join ( ' OR ' ) ; } ; internal const parseNotCondition = ( clause , context ) : string => { const parsePath = [ ... context . parsePath , { type : 'object' , key : '$not' } , ] ; const updatedContext = { ... context , parsePath } ; const subclause = parseCompositeCondition ( clause . $not , updatedContext ) ; return `NOT ${ subclause }` ; } ; internal const parseKeyPathsAndClause = ( clause , context ) : string => { const { nameMapper , valueMapper , parsePath } = context ; const paths = Object . keys ( clause ) ; if ( paths . length < 1 ) { throw new InvalidCompositeConditionException ( 'expected at least one key path with operator' , parsePath ) ; } const conditionKey = paths . find ( isConditionKey ) ; if ( conditionKey ) { const keyParsePath = [ ... parsePath , { type : 'object' , key : conditionKey } , ] ; throw new InvalidCompositeConditionException ( `unexpected condition key` , keyParsePath ) ; } const clauses = [ ] ; Object . entries ( clause ) . forEach ( ( [ path , condition ] ) => { const keyParsePath = [ ... parsePath , { type : 'object' , key : path } , ] ; let clauseString ; const simpleClause = ( operator , value ) : string => { const valueName = valueMapper . map ( value ) ; return `${ mapKeyPath ( path , nameMapper ) } ${ operator } ${ valueName }` ; } ; if ( '$eq' in condition ) { const value = condition . $eq ; clauseString = simpleClause ( '=' , value ) ; } else if ( '$neq' in condition ) { const value = condition . $neq ; clauseString = simpleClause ( '<>' , value ) ; } else if ( '$gt' in condition ) { const value = condition . $gt ; clauseString = simpleClause ( '>' , value ) ; } else if ( '$gte' in condition ) { const value = condition . $gte ; clauseString = simpleClause ( '>=' , value ) ; } else if ( '$lt' in condition ) { const value = condition . $lt ; clauseString = simpleClause ( '<' , value ) ; } else if ( '$lte' in condition ) { const value = condition . $lte ; clauseString = simpleClause ( '<=' , value ) ; } else if ( '$between' in condition ) { const value = condition . $between ; if ( ! value || typeof value !== 'object' || typeof value . $lte === 'undefined' || typeof value . $gte === 'undefined' ) { throw new InvalidCompositeConditionException ( '$between must be an object with values for $lte and $gte' , keyParsePath ) ; } const { $lte , $gte } = condition . $between ; const value1 = valueMapper . map ( $gte ) ; const value2 = valueMapper . map ( $lte ) ; clauseString = `${ mapKeyPath ( path , nameMapper ) } BETWEEN ${ value1 } AND ${ value2 }` ; } else if ( '$in' in condition ) { if ( ! Array . isArray ( condition . $in ) ) { throw new InvalidCompositeConditionException ( '$in must be an array of values' , keyParsePath ) ; } const values = condition . $in ; if ( values . length > 100 ) { throw new InvalidCompositeConditionException ( '$in condition has too many values' , keyParsePath ) ; } else if ( values . length === 0 ) { throw new InvalidCompositeConditionException ( '$in condition must have at least one value' , keyParsePath ) ; } const valueNames = values . map ( ( value ) => valueMapper . map ( value ) ) ; clauseString = `${ mapKeyPath ( path , nameMapper ) } IN (${ valueNames . join ( ',' ) })` ; } else if ( '$exists' in condition ) { const fn = condition . $exists ? 'attribute_exists' : 'attribute_not_exists' ; clauseString = `${ fn }(${ mapKeyPath ( path , nameMapper ) })` ; } else if ( '$type' in condition ) { const valueName = valueMapper . map ( condition . $type ) ; clauseString = `attribute_type(${ mapKeyPath ( path , nameMapper ) },${ valueName })` ; } else if ( '$beginsWith' in condition ) { const valueName = valueMapper . map ( condition . $beginsWith ) ; clauseString = `begins_with(${ mapKeyPath ( path , nameMapper ) },${ valueName })` ; } else if ( '$contains' in condition ) { const valueName = valueMapper . map ( condition . $contains ) ; clauseString = `contains(${ mapKeyPath ( path , nameMapper ) },${ valueName })` ; } else throw new InvalidCompositeConditionException ( 'unknown operator' , keyParsePath ) ; clauses . push ( clauseString ) ; } ) ; return clauses . join ( ' AND ' ) ; } ;