Files
directus/api/src/utils/validate-query.ts
Oreille 0425809db1 Fix string filter unexpectedly being casted to number (#10138)
* Fix string unexpectedly being casted to numbers

* Parse filter with multiple keys into `_and` block.

* Fixed filter structure in tests
2021-12-01 13:55:54 -05:00

166 lines
4.2 KiB
TypeScript

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: Query): 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: Query['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: any, key: string) {
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: any, key: string) {
if (Array.isArray(value) === false || value.length === 0) {
throw new InvalidQueryException(`"${key}" has to be an array of values`);
}
return true;
}
function validateBoolean(value: any, key: string) {
if (value === null) return true;
if (typeof value !== 'boolean') {
throw new InvalidQueryException(`"${key}" has to be a boolean`);
}
return true;
}
function validateGeometry(value: any, key: string) {
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: any) {
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 \`.\``);
}
}
}