import { IntrospectionInputObjectType , IntrospectionInputTypeRef , } from "graphql" ; import upperFirst from "lodash/upperFirst" ; import isObject from "lodash/isObject" ; import isArray from "lodash/isArray" ; import isEmpty from "lodash/isEmpty" ; import { CheckComparisonQueryResult , IntrospectionResult , Resource , } from "./constants/interfaces" ; import { OurOptions } from "./types" ; const getStringFilter = ( key , value , hasCaseInsensitive = false , comparator = "contains" ) => { if ( hasCaseInsensitive ) { return { [ key ] : { [ comparator ] : value , mode : "insensitive" , } , } ; } const OR = [ { [ key ] : { [ comparator ] : value , } , } , ] ; const valueLowerCased = value . toLowerCase ( ) ; if ( valueLowerCased !== value ) { OR . push ( { [ key ] : { [ comparator ] : valueLowerCased , } , } ) ; } const valueUpperFirst = upperFirst ( value ) ; if ( valueUpperFirst !== value ) { OR . push ( { [ key ] : { [ comparator ] : valueUpperFirst , } , } ) ; } return { OR } ; } ; const getIntFilter = ( key , value , comparator = "equals" ) => { const asNumber = parseInt ( value , 10 ) ; if ( isNaN ( asNumber ) ) { return { } ; } return { [ key ] : { [ comparator ] : asNumber , } , } ; } ; const getFloatFilter = ( key , value , comparator = "equals" ) => { const asFloat = parseFloat ( value ) ; if ( isNaN ( asFloat ) ) { return { } ; } return { [ key ] : { [ comparator ] : asFloat , } , } ; } ; const getEnumFilter = ( key , value , comparator = "equals" ) => ( { [ key ] : { [ comparator ] : value , } , } ) ; const getBooleanFilter = ( key , value ) => { return { [ key ] : { equals : Boolean ( value ) , } , } ; } ; const getDateTimeFilter = ( key , value , comparator = "equals" ) => { return { [ key ] : { [ comparator ] : new Date ( value ) , } , } ; } ; const isDateTimeFilter = ( type ) => type . kind === "INPUT_OBJECT" && [ "DateTimeFilter" , "DateTimeNullableFilter" , "NestedDateTimeFilter" , "NestedDateTimeNullableFilter" , ] . indexOf ( type . name ) !== - 1 ; const isBooleanFilter = ( type ) => type . kind === "INPUT_OBJECT" && [ "BoolFilter" , "BoolNullableFilter" , "NestedBoolFilter" , "NestedBoolNullableFilter" , ] . indexOf ( type . name ) !== - 1 ; const isStringFilter = ( type ) => type . kind === "INPUT_OBJECT" && [ "StringFilter" , "StringNullableFilter" ] . indexOf ( type . name ) !== - 1 ; const isNestedStringFilter = ( type ) => type . kind === "INPUT_OBJECT" && [ "NestedStringFilter" , "NestedStringNullableFilter" ] . indexOf ( type . name ) !== - 1 ; const isIntFilter = ( type ) => type . kind === "INPUT_OBJECT" && [ "IntFilter" , "IntNullableFilter" , "NestedIntFilter" , "NestedIntNullableFilter" , ] . indexOf ( type . name ) !== - 1 ; const isFloatFilter = ( type ) => type . kind === "INPUT_OBJECT" && [ "FloatFilter" , "FloatNullableFilter" , "NestedFloatFilter" , "NestedFloatNullableFilter" , ] . indexOf ( type . name ) !== - 1 ; const isEnumFilter = ( type ) => type . kind === "INPUT_OBJECT" && type . name . startsWith ( "Enum" ) ; const supportsCaseInsensitive = ( introspectionResults ) : boolean => { const queryModeExists = introspectionResults . types . find ( ( type ) => type . kind === "ENUM" && type . name === "QueryMode" ) ; const stringFilterType = introspectionResults . types . find ( ( type ) => type . kind === "INPUT_OBJECT" && type . name === "StringFilter" ) as IntrospectionInputObjectType ; const usesQueryMode = stringFilterType ?. inputFields . find ( ( field ) => field . name === "mode" && field . type . kind === "ENUM" && field . type . name === "QueryMode" ) ; return ! ! queryModeExists && ! ! usesQueryMode ; } ; const processKey = ( originalKey ) => { const keyRegex = /^(.+?)(_(gt|gte|lt|lte|equals|contains|startsWith|endsWith))?$/ ; const matches = originalKey . match ( keyRegex ) ; const key = matches [ 1 ] ; const comparator = matches [ 3 ] ; return { originalKey , key , comparator , } ; } ; const processComparisonQuery = ( field , value , comparison , whereType , introspectionResults ) : CheckComparisonQueryResult => { if ( ! comparison ) { return { comparisonPossible : false } ; } const comparisonFieldType = whereType . inputFields . find ( ( f ) => f . name === field ) ?. type as IntrospectionInputObjectType ; if ( comparisonFieldType ) { const filterName = comparisonFieldType . name ; const filter = introspectionResults . types . find ( ( f ) => f . name === filterName ) as IntrospectionInputObjectType ; const comparatorField = filter . inputFields . find ( ( f ) => f . name === comparison ) ; if ( comparatorField ) { if ( ! isObject ( value ) && ! isArray ( value ) ) { return { comparisonPossible : true , comparisonFieldType : filter } ; } } } return { comparisonPossible : false } ; } ; const getFilters = ( _key , value , whereType , introspectionResults , options ) => { const hasCaseInsensitive = supportsCaseInsensitive ( introspectionResults ) ; const { originalKey , key , comparator } = processKey ( _key ) ; if ( options . filters ?. [ originalKey ] ) { return options . filters [ originalKey ] ( value ) ?? { } ; } if ( key === "NOT" || key === "OR" || key === "AND" ) { return { [ key ] : value === null ? null : value . map ( ( f ) => buildWhereWithType ( f , introspectionResults , options , whereType ) ) , } ; } const { comparisonPossible , comparisonFieldType } = processComparisonQuery ( key , value , comparator , whereType , introspectionResults ) ; if ( comparisonPossible ) { if ( isBooleanFilter ( comparisonFieldType ) ) { return getBooleanFilter ( key , value ) ; } if ( isStringFilter ( comparisonFieldType ) ) { return getStringFilter ( key , value , hasCaseInsensitive , comparator ) ; } if ( isNestedStringFilter ( comparisonFieldType ) ) { return getStringFilter ( key , value , false , comparator ) ; } if ( isDateTimeFilter ( comparisonFieldType ) ) { return getDateTimeFilter ( key , value , comparator ) ; } if ( isIntFilter ( comparisonFieldType ) ) { return getIntFilter ( key , value , comparator ) ; } if ( isFloatFilter ( comparisonFieldType ) ) { return getFloatFilter ( key , value , comparator ) ; } if ( isEnumFilter ( comparisonFieldType ) ) { return getEnumFilter ( key , value , comparator ) ; } } const fieldType = whereType . inputFields . find ( ( f ) => f . name === originalKey ) ?. type as IntrospectionInputObjectType ; if ( ! fieldType ) { if ( originalKey === "q" ) { const AND = value . split ( " " ) . map ( ( part ) => ( { OR : whereType . inputFields . map ( ( f ) => { if ( f . name !== "id" ) { if ( isStringFilter ( f . type ) ) { return getStringFilter ( f . name , part . trim ( ) , hasCaseInsensitive ) ; } if ( isNestedStringFilter ( f . type ) ) { return getStringFilter ( f . name , part . trim ( ) , false ) ; } if ( isIntFilter ( f . type ) ) { const numberValue = parseInt ( part . trim ( ) ) ; const valueIsInt32 = - ( 2 ** 31 ) <= numberValue && numberValue <= 2 ** 31 - 1 ; return valueIsInt32 ? getIntFilter ( f . name , part . trim ( ) ) : null ; } } return null ; } ) . filter ( ( f ) => ! isEmpty ( f ) ) , } ) ) ; return { AND } ; } else { return { } ; } } if ( ! isObject ( value ) && ! isArray ( value ) ) { if ( isBooleanFilter ( fieldType ) ) { return getBooleanFilter ( originalKey , value ) ; } if ( isStringFilter ( fieldType ) ) { return getStringFilter ( originalKey , value , hasCaseInsensitive ) ; } if ( isNestedStringFilter ( fieldType ) ) { return getStringFilter ( originalKey , value , false ) ; } if ( isDateTimeFilter ( fieldType ) ) { return getDateTimeFilter ( originalKey , value ) ; } if ( isIntFilter ( fieldType ) ) { return getIntFilter ( originalKey , value ) ; } if ( isFloatFilter ( fieldType ) ) { return getFloatFilter ( originalKey , value ) ; } if ( isEnumFilter ( fieldType ) ) { return getEnumFilter ( originalKey , value ) ; } } if ( isArray ( value ) ) { if ( isStringFilter ( fieldType ) ) { return { OR : value . map ( ( v ) => getStringFilter ( originalKey , v , hasCaseInsensitive ) ) , } ; } if ( isNestedStringFilter ( fieldType ) ) { return { OR : value . map ( ( v ) => getStringFilter ( originalKey , v , false ) ) , } ; } if ( isIntFilter ( fieldType ) ) { return { OR : value . map ( ( v ) => getIntFilter ( originalKey , v ) ) } ; } if ( isDateTimeFilter ( fieldType ) ) { return { OR : value . map ( ( v ) => getDateTimeFilter ( originalKey , v ) ) } ; } if ( isFloatFilter ( fieldType ) ) { return { OR : value . map ( ( v ) => getFloatFilter ( originalKey , v ) ) } ; } if ( isEnumFilter ( fieldType ) ) { return { OR : value . map ( ( v ) => getEnumFilter ( originalKey , v ) ) } ; } } if ( fieldType . kind === "INPUT_OBJECT" ) { const inputObjectType = introspectionResults . types . find ( ( t ) => t . name === fieldType . name ) as IntrospectionInputObjectType ; if ( ! isObject ( value ) ) { const hasSomeFilter = inputObjectType . inputFields . some ( ( s ) => s . name === "some" ) ; if ( hasSomeFilter ) { return { [ originalKey ] : { some : { id : { equals : value , } , } , } , } ; } return { [ originalKey ] : { id : { equals : value , } , } , } ; } if ( isArray ( value ) ) { const hasSomeFilter = inputObjectType . inputFields . some ( ( s ) => s . name === "some" ) ; if ( hasSomeFilter ) { return { [ originalKey ] : { some : { id : { in : value , } , } , } , } ; } return { [ originalKey ] : { id : { in : value , } , } , } ; } else { const where = buildWhereWithType ( value , introspectionResults , options , inputObjectType ) ; return { [ originalKey ] : where } ; } } return { [ originalKey ] : value } ; } ; type Filter = { NOT ? : Filter ; OR ? : Filter [ ] ; AND ? : Filter [ ] ; } & { [ key ] : any ; } ; const buildWhereWithType = ( filter , introspectionResults , options , whereType ) => { const hasAnd = whereType . inputFields . some ( ( i ) => i . name === "AND" ) ; const where = hasAnd ? Object . keys ( filter ?? { } ) . reduce ( ( acc , key ) => { const filters = getFilters ( key , filter [ key ] , whereType , introspectionResults , options ) ; return { ... acc , AND : [ ... acc . AND , filters ] } ; } , { AND : [ ] } ) : Object . keys ( filter ?? { } ) . reduce ( ( acc , key ) => { const filters = getFilters ( key , filter [ key ] , whereType , introspectionResults , options ) ; return { ... acc , ... filters } ; } , { } ) ; if ( where . AND ?. length === 0 ) { delete where . AND ; } if ( where . AND ?. length === 1 ) { const singleAnd = where . AND [ 0 ] ; delete where . AND ; return { ... where , ... singleAnd , } ; } return where ; } ; export const buildWhere = ( filter , resource , introspectionResults , options ) => { const whereType = introspectionResults . types . find ( ( t ) => t . name === `${ resource . type . name }WhereInput` ) as IntrospectionInputObjectType ; return buildWhereWithType ( filter , introspectionResults , options , whereType ) ; } ;