import Joi from 'joi' ; import { isPlainObject } from 'lodash' ; import { InvalidQueryException } from '../exceptions' ; import { Query } from '@directus/shared/types' ; import { stringify } from 'wellknown' ; const querySchema = Joi . object ( { fields : Joi . array ( ) . items ( Joi . string ( ) ) , group : Joi . array ( ) . items ( Joi . string ( ) ) , sort : Joi . array ( ) . items ( Joi . string ( ) ) , filter : Joi . object ( { } ) . unknown ( ) , limit : Joi . number ( ) , offset : Joi . number ( ) , page : Joi . number ( ) , meta : Joi . array ( ) . items ( Joi . string ( ) . valid ( 'total_count' , 'filter_count' ) ) , search : Joi . string ( ) , export : Joi . string ( ) . valid ( 'json' , 'csv' , 'xml' ) , aggregate : Joi . object ( ) , deep : Joi . object ( ) , alias : Joi . object ( ) , } ) . id ( 'query' ) ; export function validateQuery ( query ) { const { error } = querySchema . validate ( query ) ; if ( query . filter && Object . keys ( query . filter ) . length > 0 ) { validateFilter ( query . filter ) ; } if ( query . alias ) { validateAlias ( query . alias ) ; } if ( error ) { throw new InvalidQueryException ( error . message ) ; } return query ; } function validateFilter ( filter ) { if ( ! filter ) throw new InvalidQueryException ( 'Invalid filter object' ) ; for ( const [ key , nested ] of Object . entries ( filter ) ) { if ( key === '_and' || key === '_or' ) { nested . forEach ( validateFilter ) ; } else if ( key . startsWith ( '_' ) ) { const value = nested ; switch ( key ) { case '_eq' : case '_neq' : case '_contains' : case '_ncontains' : case '_starts_with' : case '_nstarts_with' : case '_ends_with' : case '_nends_with' : case '_gt' : case '_gte' : case '_lt' : case '_lte' : default : validateFilterPrimitive ( value , key ) ; break ; case '_in' : case '_nin' : case '_between' : case '_nbetween' : validateList ( value , key ) ; break ; case '_null' : case '_nnull' : case '_empty' : case '_nempty' : validateBoolean ( value , key ) ; break ; case '_intersects' : case '_nintersects' : case '_intersects_bbox' : case '_nintersects_bbox' : validateGeometry ( value , key ) ; break ; } } else if ( isPlainObject ( nested ) ) { validateFilter ( nested ) ; } else if ( Array . isArray ( nested ) === false ) { validateFilterPrimitive ( nested , '_eq' ) ; } else { validateFilter ( nested ) ; } } } function validateFilterPrimitive ( value , key ) { if ( value === null ) return true ; if ( ( typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value instanceof Date ) === false ) { throw new InvalidQueryException ( `The filter value for "${ key }" has to be a string, number, or boolean` ) ; } if ( typeof value === 'number' && ( Number . isNaN ( value ) || ! Number . isSafeInteger ( value ) ) ) { throw new InvalidQueryException ( `The filter value for "${ key }" is not a valid number` ) ; } if ( typeof value === 'string' && value . length === 0 ) { throw new InvalidQueryException ( `You can't filter for an empty string in "${ key }". Use "_empty" or "_nempty" instead` ) ; } return true ; } function validateList ( value , key ) { if ( Array . isArray ( value ) === false || value . length === 0 ) { throw new InvalidQueryException ( `"${ key }" has to be an array of values` ) ; } return true ; } function validateBoolean ( value , key ) { if ( value === null ) return true ; if ( typeof value !== 'boolean' ) { throw new InvalidQueryException ( `"${ key }" has to be a boolean` ) ; } return true ; } function validateGeometry ( value , key ) { if ( value === null ) return true ; try { stringify ( value ) ; } catch { throw new InvalidQueryException ( `"${ key }" has to be a valid GeoJSON object` ) ; } return true ; } function validateAlias ( alias ) { if ( isPlainObject ( alias ) === false ) { throw new InvalidQueryException ( `"alias" has to be an object` ) ; } for ( const [ key , value ] of Object . entries ( alias ) ) { if ( typeof key !== 'string' ) { throw new InvalidQueryException ( `"alias" key has to be a string. "${ typeof key }" given.` ) ; } if ( typeof value !== 'string' ) { throw new InvalidQueryException ( `"alias" value has to be a string. "${ typeof key }" given.` ) ; } if ( key . includes ( '.' ) || value . includes ( '.' ) ) { throw new InvalidQueryException ( `"alias" key/value can't contain a period character \`.\`` ) ; } } }