directus

Форк
0
/
validate-query.ts 
246 строк · 6.5 Кб
1
import { useEnv } from '@directus/env';
2
import { InvalidQueryError } from '@directus/errors';
3
import type { Filter, Query } from '@directus/types';
4
import Joi from 'joi';
5
import { isPlainObject, uniq } from 'lodash-es';
6
import { stringify } from 'wellknown';
7
import { calculateFieldDepth } from './calculate-field-depth.js';
8

9
const env = useEnv();
10

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(),
16
	limit:
17
		'QUERY_LIMIT_MAX' in env && env['QUERY_LIMIT_MAX'] !== -1
18
			? Joi.number()
19
					.integer()
20
					.min(-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')),
26
	search: Joi.string(),
27
	export: Joi.string().valid('csv', 'json', 'xml', 'yaml'),
28
	version: Joi.string(),
29
	versionRaw: Joi.boolean(),
30
	aggregate: Joi.object(),
31
	deep: Joi.object(),
32
	alias: Joi.object(),
33
}).id('query');
34

35
export function validateQuery(query: Query): Query {
36
	const { error } = querySchema.validate(query);
37

38
	if (query.filter && Object.keys(query.filter).length > 0) {
39
		validateFilter(query.filter);
40
	}
41

42
	if (query.alias) {
43
		validateAlias(query.alias);
44
	}
45

46
	validateRelationalDepth(query);
47

48
	if (error) {
49
		throw new InvalidQueryError({ reason: error.message });
50
	}
51

52
	return query;
53
}
54

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('_')) {
60
			const value = nested;
61

62
			switch (key) {
63
				case '_in':
64
				case '_nin':
65
				case '_between':
66
				case '_nbetween':
67
					validateList(value, key);
68
					break;
69
				case '_null':
70
				case '_nnull':
71
				case '_empty':
72
				case '_nempty':
73
					validateBoolean(value, key);
74
					break;
75
				case '_intersects':
76
				case '_nintersects':
77
				case '_intersects_bbox':
78
				case '_nintersects_bbox':
79
					validateGeometry(value, key);
80
					break;
81
				case '_none':
82
				case '_some':
83
					validateFilter(nested);
84
					break;
85
				case '_eq':
86
				case '_neq':
87
				case '_contains':
88
				case '_ncontains':
89
				case '_starts_with':
90
				case '_nstarts_with':
91
				case '_istarts_with':
92
				case '_nistarts_with':
93
				case '_ends_with':
94
				case '_nends_with':
95
				case '_iends_with':
96
				case '_niends_with':
97
				case '_gt':
98
				case '_gte':
99
				case '_lt':
100
				case '_lte':
101
				default:
102
					validateFilterPrimitive(value, key);
103
					break;
104
			}
105
		} else if (isPlainObject(nested)) {
106
			validateFilter(nested);
107
		} else if (Array.isArray(nested) === false) {
108
			validateFilterPrimitive(nested, '_eq');
109
		} else {
110
			// @ts-ignore TODO Check which case this is supposed to cover
111
			validateFilter(nested);
112
		}
113
	}
114
}
115

116
function validateFilterPrimitive(value: any, key: string) {
117
	if (value === null) return true;
118

119
	if (
120
		(typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value instanceof Date) ===
121
		false
122
	) {
123
		throw new InvalidQueryError({ reason: `The filter value for "${key}" has to be a string, number, or boolean` });
124
	}
125

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` });
128
	}
129

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`,
133
		});
134
	}
135

136
	return true;
137
}
138

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` });
142
	}
143

144
	return true;
145
}
146

147
export function validateBoolean(value: any, key: string) {
148
	if (value === null || value === '') return true;
149

150
	if (typeof value !== 'boolean') {
151
		throw new InvalidQueryError({ reason: `"${key}" has to be a boolean` });
152
	}
153

154
	return true;
155
}
156

157
export function validateGeometry(value: any, key: string) {
158
	if (value === null || value === '') return true;
159

160
	try {
161
		stringify(value);
162
	} catch {
163
		throw new InvalidQueryError({ reason: `"${key}" has to be a valid GeoJSON object` });
164
	}
165

166
	return true;
167
}
168

169
function validateAlias(alias: any) {
170
	if (isPlainObject(alias) === false) {
171
		throw new InvalidQueryError({ reason: `"alias" has to be an object` });
172
	}
173

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` });
177
		}
178

179
		if (typeof value !== 'string') {
180
			throw new InvalidQueryError({ reason: `"alias" value has to be a string. "${typeof key}" given` });
181
		}
182

183
		if (key.includes('.') || value.includes('.')) {
184
			throw new InvalidQueryError({ reason: `"alias" key/value can't contain a period character \`.\`` });
185
		}
186
	}
187
}
188

189
function validateRelationalDepth(query: Query) {
190
	const maxRelationalDepth = Number(env['MAX_RELATIONAL_DEPTH']) > 2 ? Number(env['MAX_RELATIONAL_DEPTH']) : 2;
191

192
	// Process the fields in the same way as api/src/utils/get-ast-from-query.ts
193
	let fields = ['*'];
194

195
	if (query.fields) {
196
		fields = query.fields;
197
	}
198

199
	/**
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
202
	 */
203
	if (Object.keys(query.aggregate || {}).length > 0) {
204
		fields = [];
205
	}
206

207
	/**
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
210
	 */
211
	if (query.group) {
212
		fields = query.group;
213
	}
214

215
	fields = uniq(fields);
216

217
	for (const field of fields) {
218
		if (field.split('.').length > maxRelationalDepth) {
219
			throw new InvalidQueryError({ reason: 'Max relational depth exceeded' });
220
		}
221
	}
222

223
	if (query.filter) {
224
		const filterRelationalDepth = calculateFieldDepth(query.filter);
225

226
		if (filterRelationalDepth > maxRelationalDepth) {
227
			throw new InvalidQueryError({ reason: 'Max relational depth exceeded' });
228
		}
229
	}
230

231
	if (query.sort) {
232
		for (const sort of query.sort) {
233
			if (sort.split('.').length > maxRelationalDepth) {
234
				throw new InvalidQueryError({ reason: 'Max relational depth exceeded' });
235
			}
236
		}
237
	}
238

239
	if (query.deep) {
240
		const deepRelationalDepth = calculateFieldDepth(query.deep, ['_sort']);
241

242
		if (deepRelationalDepth > maxRelationalDepth) {
243
			throw new InvalidQueryError({ reason: 'Max relational depth exceeded' });
244
		}
245
	}
246
}
247

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.