1
import { useEnv } from '@directus/env';
2
import { InvalidQueryError } from '@directus/errors';
3
import type { Filter, Query } from '@directus/types';
5
import { isPlainObject, uniq } from 'lodash-es';
6
import { stringify } from 'wellknown';
7
import { calculateFieldDepth } from './calculate-field-depth.js';
11
const querySchema = Joi.object({
12
fields: Joi.array().items(Joi.string()),
13
group: Joi.array().items(Joi.string()),
14
sort: Joi.array().items(Joi.string()),
15
filter: Joi.object({}).unknown(),
17
'QUERY_LIMIT_MAX' in env && env['QUERY_LIMIT_MAX'] !== -1
21
.max(env['QUERY_LIMIT_MAX'] as number) // min should be 0
22
: Joi.number().integer().min(-1),
23
offset: Joi.number().integer().min(0),
24
page: Joi.number().integer().min(0),
25
meta: Joi.array().items(Joi.string().valid('total_count', 'filter_count')),
27
export: Joi.string().valid('csv', 'json', 'xml', 'yaml'),
28
version: Joi.string(),
29
versionRaw: Joi.boolean(),
30
aggregate: Joi.object(),
35
export function validateQuery(query: Query): Query {
36
const { error } = querySchema.validate(query);
38
if (query.filter && Object.keys(query.filter).length > 0) {
39
validateFilter(query.filter);
43
validateAlias(query.alias);
46
validateRelationalDepth(query);
49
throw new InvalidQueryError({ reason: error.message });
55
function validateFilter(filter: Filter) {
56
for (const [key, nested] of Object.entries(filter)) {
57
if (key === '_and' || key === '_or') {
58
nested.forEach(validateFilter);
59
} else if (key.startsWith('_')) {
67
validateList(value, key);
73
validateBoolean(value, key);
77
case '_intersects_bbox':
78
case '_nintersects_bbox':
79
validateGeometry(value, key);
83
validateFilter(nested);
92
case '_nistarts_with':
102
validateFilterPrimitive(value, key);
105
} else if (isPlainObject(nested)) {
106
validateFilter(nested);
107
} else if (Array.isArray(nested) === false) {
108
validateFilterPrimitive(nested, '_eq');
110
// @ts-ignore TODO Check which case this is supposed to cover
111
validateFilter(nested);
116
function validateFilterPrimitive(value: any, key: string) {
117
if (value === null) return true;
120
(typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value instanceof Date) ===
123
throw new InvalidQueryError({ reason: `The filter value for "${key}" has to be a string, number, or boolean` });
126
if (typeof value === 'number' && (Number.isNaN(value) || value > Number.MAX_SAFE_INTEGER)) {
127
throw new InvalidQueryError({ reason: `The filter value for "${key}" is not a valid number` });
130
if (typeof value === 'string' && value.length === 0) {
131
throw new InvalidQueryError({
132
reason: `You can't filter for an empty string in "${key}". Use "_empty" or "_nempty" instead`,
139
function validateList(value: any, key: string) {
140
if (Array.isArray(value) === false || value.length === 0) {
141
throw new InvalidQueryError({ reason: `"${key}" has to be an array of values` });
147
export function validateBoolean(value: any, key: string) {
148
if (value === null || value === '') return true;
150
if (typeof value !== 'boolean') {
151
throw new InvalidQueryError({ reason: `"${key}" has to be a boolean` });
157
export function validateGeometry(value: any, key: string) {
158
if (value === null || value === '') return true;
163
throw new InvalidQueryError({ reason: `"${key}" has to be a valid GeoJSON object` });
169
function validateAlias(alias: any) {
170
if (isPlainObject(alias) === false) {
171
throw new InvalidQueryError({ reason: `"alias" has to be an object` });
174
for (const [key, value] of Object.entries(alias)) {
175
if (typeof key !== 'string') {
176
throw new InvalidQueryError({ reason: `"alias" key has to be a string. "${typeof key}" given` });
179
if (typeof value !== 'string') {
180
throw new InvalidQueryError({ reason: `"alias" value has to be a string. "${typeof key}" given` });
183
if (key.includes('.') || value.includes('.')) {
184
throw new InvalidQueryError({ reason: `"alias" key/value can't contain a period character \`.\`` });
189
function validateRelationalDepth(query: Query) {
190
const maxRelationalDepth = Number(env['MAX_RELATIONAL_DEPTH']) > 2 ? Number(env['MAX_RELATIONAL_DEPTH']) : 2;
192
// Process the fields in the same way as api/src/utils/get-ast-from-query.ts
196
fields = query.fields;
200
* When using aggregate functions, you can't have any other regular fields
201
* selected. This makes sure you never end up in a non-aggregate fields selection error
203
if (Object.keys(query.aggregate || {}).length > 0) {
208
* Similarly, when grouping on a specific field, you can't have other non-aggregated fields.
209
* The group query will override the fields query
212
fields = query.group;
215
fields = uniq(fields);
217
for (const field of fields) {
218
if (field.split('.').length > maxRelationalDepth) {
219
throw new InvalidQueryError({ reason: 'Max relational depth exceeded' });
224
const filterRelationalDepth = calculateFieldDepth(query.filter);
226
if (filterRelationalDepth > maxRelationalDepth) {
227
throw new InvalidQueryError({ reason: 'Max relational depth exceeded' });
232
for (const sort of query.sort) {
233
if (sort.split('.').length > maxRelationalDepth) {
234
throw new InvalidQueryError({ reason: 'Max relational depth exceeded' });
240
const deepRelationalDepth = calculateFieldDepth(query.deep, ['_sort']);
242
if (deepRelationalDepth > maxRelationalDepth) {
243
throw new InvalidQueryError({ reason: 'Max relational depth exceeded' });