import { CommonFieldComparisonBetweenType , FilterComparisonOperators } from '@nestjs-query/core' ; import escapeRegExp from 'lodash.escaperegexp' ; import { BadRequestException } from '@nestjs/common' ; import { ReturnModelType , mongoose } from '@typegoose/typegoose' ; import { getSchemaKey } from './helpers' ; internal export type EntityComparisonField < Entity , F extends keyof Entity > = Entity [ F ] | Entity [ F ] [ ] | CommonFieldComparisonBetweenType < Entity [ F ] > | true | false | null ; internal export class ComparisonBuilder < Entity > { static DEFAULT_COMPARISON_MAP : Record < string , string > = { eq : '$eq' , neq : '$ne' , gt : '$gt' , gte : '$gte' , lt : '$lt' , lte : '$lte' , in : '$in' , notin : '$nin' , is : '$eq' , isnot : '$ne' , } ; constructor ( readonly Model , readonly comparisonMap = ComparisonBuilder . DEFAULT_COMPARISON_MAP ) { } param field param cmp param val build < F extends keyof Entity > ( field , cmp , val ) { const schemaKey = getSchemaKey ( `${ String ( field ) }` ) ; const normalizedCmp = ( cmp as string ) . toLowerCase ( ) ; let querySelector ; if ( this . comparisonMap [ normalizedCmp ] ) { querySelector = { [ this . comparisonMap [ normalizedCmp ] ] : this . convertQueryValue ( field , val as Entity [ F ] ) , } as mongoose . FilterQuery < Entity [ F ] > ; } if ( normalizedCmp . includes ( 'like' ) ) { querySelector = this . likeComparison ( normalizedCmp , val ) ; } if ( normalizedCmp . includes ( 'between' ) ) { querySelector = this . betweenComparison ( normalizedCmp , field , val ) ; } if ( ! querySelector ) { throw new BadRequestException ( `unknown operator ${ JSON . stringify ( cmp ) }` ) ; } return { [ schemaKey ] : querySelector } as mongoose . FilterQuery < Entity > ; } private betweenComparison < F extends keyof Entity > ( cmp , field , val ) { if ( ! this . isBetweenVal ( val ) ) { throw new Error ( `Invalid value for ${ cmp } expected {lower: val, upper: val} got ${ JSON . stringify ( val ) }` ) ; } if ( cmp === 'notbetween' ) { return { $lt : this . convertQueryValue ( field , val . lower ) , $gt : this . convertQueryValue ( field , val . upper ) } ; } return { $gte : this . convertQueryValue ( field , val . lower ) , $lte : this . convertQueryValue ( field , val . upper ) } ; } private isBetweenVal < F extends keyof Entity > ( val ) { return val !== null && typeof val === 'object' && 'lower' in val && 'upper' in val ; } private likeComparison < F extends keyof Entity > ( cmp , val ) { const regExpStr = escapeRegExp ( `${ String ( val ) }` ) . replace ( /%/g , '.*' ) ; const regExp = new RegExp ( regExpStr , cmp . includes ( 'ilike' ) ? 'i' : undefined ) ; if ( cmp . startsWith ( 'not' ) ) { return { $not : { $regex : regExp } } ; } return { $regex : regExp } ; } private convertQueryValue < F extends keyof Entity > ( field , val ) { const schemaType = this . Model . schema . path ( getSchemaKey ( field as string ) ) ; if ( ! schemaType ) { throw new BadRequestException ( `unknown comparison field ${ String ( field ) }` ) ; } if ( schemaType instanceof mongoose . Schema . Types . ObjectId ) { return this . convertToObjectId ( val ) as Entity [ F ] ; } return val ; } private convertToObjectId ( val ) { if ( Array . isArray ( val ) ) { return val . map ( ( v ) => this . convertToObjectId ( v ) ) ; } if ( typeof val === 'string' || typeof val === 'number' ) { if ( mongoose . Types . ObjectId . isValid ( val ) ) { return mongoose . Types . ObjectId ( val ) ; } } return val ; } }