UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

213 lines (212 loc) 7.73 kB
import { useEnv } from '@directus/env'; import { InvalidQueryError } from '@directus/errors'; import Joi from 'joi'; import { isPlainObject, uniq } from 'lodash-es'; import { stringify } from 'wellknown'; import { calculateFieldDepth } from './calculate-field-depth.js'; const env = useEnv(); 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: 'QUERY_LIMIT_MAX' in env && env['QUERY_LIMIT_MAX'] !== -1 ? Joi.number() .integer() .min(-1) .max(env['QUERY_LIMIT_MAX']) // min should be 0 : Joi.number().integer().min(-1), offset: Joi.number().integer().min(0), page: Joi.number().integer().min(0), meta: Joi.array().items(Joi.string().valid('total_count', 'filter_count')), search: Joi.string(), export: Joi.string().valid('csv', 'json', 'xml', 'yaml'), version: Joi.string(), versionRaw: Joi.boolean(), aggregate: Joi.object(), deep: Joi.object(), alias: Joi.object(), backlink: Joi.boolean(), }).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); } validateRelationalDepth(query); if (error) { throw new InvalidQueryError({ reason: error.message }); } return query; } function validateFilter(filter) { 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 '_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; case '_none': case '_some': validateFilter(nested); break; case '_eq': case '_neq': case '_contains': case '_ncontains': case '_starts_with': case '_nstarts_with': case '_istarts_with': case '_nistarts_with': case '_ends_with': case '_nends_with': case '_iends_with': case '_niends_with': case '_gt': case '_gte': case '_lt': case '_lte': default: validateFilterPrimitive(value, key); break; } } else if (isPlainObject(nested)) { validateFilter(nested); } else if (Array.isArray(nested) === false) { validateFilterPrimitive(nested, '_eq'); } else { // @ts-ignore TODO Check which case this is supposed to cover 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 InvalidQueryError({ reason: `The filter value for "${key}" has to be a string, number, or boolean` }); } if (typeof value === 'number' && (Number.isNaN(value) || value > Number.MAX_SAFE_INTEGER)) { throw new InvalidQueryError({ reason: `The filter value for "${key}" is not a valid number` }); } if (typeof value === 'string' && value.length === 0) { throw new InvalidQueryError({ reason: `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 InvalidQueryError({ reason: `"${key}" has to be an array of values` }); } return true; } export function validateBoolean(value, key) { if (value === null || value === '') return true; if (typeof value !== 'boolean') { throw new InvalidQueryError({ reason: `"${key}" has to be a boolean` }); } return true; } export function validateGeometry(value, key) { if (value === null || value === '') return true; try { stringify(value); } catch { throw new InvalidQueryError({ reason: `"${key}" has to be a valid GeoJSON object` }); } return true; } function validateAlias(alias) { if (isPlainObject(alias) === false) { throw new InvalidQueryError({ reason: `"alias" has to be an object` }); } for (const [key, value] of Object.entries(alias)) { if (typeof key !== 'string') { throw new InvalidQueryError({ reason: `"alias" key has to be a string. "${typeof key}" given` }); } if (typeof value !== 'string') { throw new InvalidQueryError({ reason: `"alias" value has to be a string. "${typeof key}" given` }); } if (key.includes('.') || value.includes('.')) { throw new InvalidQueryError({ reason: `"alias" key/value can't contain a period character \`.\`` }); } } } function validateRelationalDepth(query) { const maxRelationalDepth = Number(env['MAX_RELATIONAL_DEPTH']) > 2 ? Number(env['MAX_RELATIONAL_DEPTH']) : 2; // Process the fields in the same way as api/src/utils/get-ast-from-query.ts let fields = ['*']; if (query.fields) { fields = query.fields; } /** * When using aggregate functions, you can't have any other regular fields * selected. This makes sure you never end up in a non-aggregate fields selection error */ if (Object.keys(query.aggregate || {}).length > 0) { fields = []; } /** * Similarly, when grouping on a specific field, you can't have other non-aggregated fields. * The group query will override the fields query */ if (query.group) { fields = query.group; } fields = uniq(fields); for (const field of fields) { if (field.split('.').length > maxRelationalDepth) { throw new InvalidQueryError({ reason: 'Max relational depth exceeded' }); } } if (query.filter) { const filterRelationalDepth = calculateFieldDepth(query.filter); if (filterRelationalDepth > maxRelationalDepth) { throw new InvalidQueryError({ reason: 'Max relational depth exceeded' }); } } if (query.sort) { for (const sort of query.sort) { if (sort.split('.').length > maxRelationalDepth) { throw new InvalidQueryError({ reason: 'Max relational depth exceeded' }); } } } if (query.deep) { const deepRelationalDepth = calculateFieldDepth(query.deep, ['_sort']); if (deepRelationalDepth > maxRelationalDepth) { throw new InvalidQueryError({ reason: 'Max relational depth exceeded' }); } } }