import isEqual from 'lodash/isEqual' ; import { Key , QueryInput , Converter } from 'aws-sdk/clients/dynamodb' ; import { Context } from '../context' ; import { DocumentWithId , WrappedDocument } from '../base/common' ; import { getCollection , unwrap , assembleIndexedValue , IndexedValue , } from '../base/util' ; import { Collection } from '../base/collection' ; import { InvalidQueryException , ConfigurationException , InvalidIndexedFieldValueException , } from '../base/exceptions' ; import { KeyPath , AccessPattern , AccessPatternOptions , } from '../base/access_pattern' ; import { SecondaryIndexLayout } from '../base/layout' ; import debugDynamo from '../debug/debugDynamo' ; import { CompositeCondition } from '../base/conditions' ; import { createNameMapper , createValueMapper } from '../base/mappers' ; import { parseCompositeCondition } from '../base/conditions_parser' ; /**
 * The query operator to use in the `KeyConditionExpression` to `QueryItem`
 * as called by [[find]]
 */ export type QueryOperator = 'match' | 'equals' | 'gte' | 'gt' | 'lte' | 'lt' | 'between' ; /**
 * A find query. This is a map of key paths (specified dot-separated)
 * to values to match.
 *
 * The query you specify must match an access pattern on the collection,
 * otherwise an [[IndexNotFoundException]] will be thrown by [[find]]
 */ export type FindQuery = { [ matchKey ] : string | [ string , string ] ; } ; /**
 * The results of a [[find]] operation.
 */ export type FindResults < DocumentType extends DocumentWithId > = { /** The items in this batch of results */ items : DocumentType [ ] ; /** The pagination token. This value is specified when
     * there are more results to fetch; pass it to another [[find]] call
     * to get the next batch. It will be `undefined` when there is no
     * more results.
     */ nextToken ? : Key ; } ; internal export const isEqualKey = ( lhs , rhs ) : boolean => { const normalisedLhs = typeof lhs === 'string' ? lhs . split ( '.' ) : lhs ; const normalisedRhs = typeof rhs === 'string' ? rhs . split ( '.' ) : rhs ; return isEqual ( normalisedLhs , normalisedRhs ) ; } ; internal export const findAccessPattern = ( collection , query ) : AccessPattern | undefined => { return ( collection . accessPatterns || [ ] ) . find ( ( ap ) => { const unmatchedQueryKeys = [ ... Object . keys ( query ) ] ; for ( const apKey of ap . partitionKeys ) { const matchingQueryKeyIndex = unmatchedQueryKeys . findIndex ( ( queryKey ) => isEqualKey ( apKey , queryKey ) ) ; if ( matchingQueryKeyIndex >= 0 ) { unmatchedQueryKeys . splice ( matchingQueryKeyIndex , 1 ) ; } else { return false ; } } if ( ap . sortKeys ) { for ( const apKey of ap . sortKeys ) { const matchingQueryKeyIndex = unmatchedQueryKeys . findIndex ( ( queryKey ) => isEqualKey ( apKey , queryKey ) ) ; if ( matchingQueryKeyIndex >= 0 ) { unmatchedQueryKeys . splice ( matchingQueryKeyIndex , 1 ) ; } else { break ; } } } return unmatchedQueryKeys . length === 0 ; } ) ; } ; internal export const findAccessPatternLayout = ( findKeys , ap ) : SecondaryIndexLayout | undefined => findKeys . find ( ( fk ) => fk . indexName === ap . indexName ) ; internal export const assembleQueryValue = ( type , collectionName , query , options , paths ? , separator ? ) : IndexedValue => { if ( paths ) { const values = [ ] ; for ( const path of paths ) { const keyPath = path . join ( '.' ) ; const pathValue = query [ keyPath ] ; if ( ! pathValue ) break ; const scalarPathValue = type === 'partition' || type === 'sort' ? Array . isArray ( pathValue ) ? pathValue [ 0 ] : pathValue : Array . isArray ( pathValue ) ? pathValue [ 1 ] : undefined ; if ( ! scalarPathValue ) throw new InvalidIndexedFieldValueException ( '$between queries must specify an array for sort key values' , { collection : collectionName , keyPath : path } ) ; const transformedValue = options . stringNormalizer ? options . stringNormalizer ( path , scalarPathValue ) : scalarPathValue ; values . push ( transformedValue ) ; } return assembleIndexedValue ( type === 'sort2' ? 'sort' : type , collectionName , values , separator ) ; } return undefined ; } ; internal const getQueryOperator = ( sortKeyName , sortValueName , sortValueName2 , qo ? ) : string => { switch ( qo ) { case 'lte' : return `${ sortKeyName } <= ${ sortValueName }` ; case 'lt' : return `${ sortKeyName } < ${ sortValueName }` ; case 'gte' : return `${ sortKeyName } >= ${ sortValueName }` ; case 'gt' : return `${ sortKeyName } > ${ sortValueName }` ; case 'equals' : return `${ sortKeyName } = ${ sortValueName }` ; case 'between' : return `${ sortKeyName } BETWEEN ${ sortValueName } AND ${ sortValueName2 }` ; default : case 'match' : return `begins_with(${ sortKeyName }, ${ sortValueName })` ; } throw new Error ( 'unreachable' ) ; } ; /**
 * The options for a [[find]] operation
 */ export type FindOptions = { /**
     * The sort key condition to use
     */ queryOperator ? : QueryOperator ; limit ? : number ; /**
     * `true` (default) to scan the index forward, `false` to scan it backward
     */ scanForward ? : boolean ; /**
     * An optional filter expression for the
     * find operation
     */ filter ? : CompositeCondition ; } ; param ctx param collectionName param query param nextToken param options param options . queryOperator param options . limit param options . scanForward param options . filter returns throws throws export async function find < DocumentType extends DocumentWithId > ( ctx , collectionName , query , nextToken ? , options = { } ) { const collection = getCollection ( ctx , collectionName ) ; const ap = findAccessPattern ( collection , query ) ; if ( ! ap ) { throw new InvalidQueryException ( 'Unable to find access pattern matching query' , { collection : collectionName , query , } ) ; } const layout = findAccessPatternLayout ( collection . layout ?. findKeys ?? [ ] , ap ) ; if ( ! layout ) { throw new ConfigurationException ( `Unable to find layout for index specified in access pattern` , { info : { collectionName , indexName : ap . indexName } , } ) ; } const partitionKeyValue = assembleQueryValue ( 'partition' , collection . name , query , ap . options || { } , ap . partitionKeys , collection . layout . indexKeySeparator ) ; const sortKeyValue = assembleQueryValue ( 'sort' , collection . name , query , ap . options || { } , ap . sortKeys , collection . layout . indexKeySeparator ) ; const sortKeyValue2 = options ?. queryOperator === 'between' ? assembleQueryValue ( 'sort2' , collection . name , query , ap . options || { } , ap . sortKeys , collection . layout . indexKeySeparator ) : undefined ; const nameMapper = createNameMapper ( ) ; const valueMapper = createValueMapper ( ) ; const sortKeyOp = sortKeyValue && getQueryOperator ( nameMapper . map ( layout . sortKey ! , '#indexSortKey' ) , valueMapper . map ( sortKeyValue ) , options ?. queryOperator === 'between' ? valueMapper . map ( sortKeyValue2 ) : undefined , options ?. queryOperator ) ; const keyConditionExpression = `${ nameMapper . map ( layout . partitionKey , '#indexPartitionKey' ) } = ${ valueMapper . map ( partitionKeyValue ) }${ sortKeyValue ? ` AND ${ sortKeyOp }` : '' }` ; let filterExpression ; if ( options ?. filter ) { filterExpression = parseCompositeCondition ( options . filter , { nameMapper , valueMapper , parsePath : [ ] , } ) ; } const queryRequest = { TableName : collection . layout . tableName , IndexName : ap . indexName , KeyConditionExpression : keyConditionExpression , ExpressionAttributeNames : nameMapper . get ( ) , ExpressionAttributeValues : valueMapper . get ( ) , ExclusiveStartKey : nextToken , Limit : options ?. limit , ScanIndexForward : options ?. scanForward ?? true , FilterExpression : filterExpression , } ; debugDynamo ( 'Query' , queryRequest ) ; const { Items : items , LastEvaluatedKey : lastEvaluatedKey } = await ctx . ddb . query ( queryRequest ) . promise ( ) ; const unwrappedItems = items ? items . map ( ( item ) => unwrap ( Converter . unmarshall ( item ) as WrappedDocument < DocumentType > ) ) : [ ] ; return { items : unwrappedItems , nextToken : lastEvaluatedKey , } ; }