directus

Форк
0
/
get-ast-from-query.ts 
407 строк · 11.9 Кб
1
/**
2
 * Generate an AST based on a given collection and query
3
 */
4

5
import { REGEX_BETWEEN_PARENS } from '@directus/constants';
6
import type { Accountability, PermissionsAction, Query, SchemaOverview } from '@directus/types';
7
import type { Knex } from 'knex';
8
import { cloneDeep, isEmpty, mapKeys, omitBy, uniq } from 'lodash-es';
9
import type { AST, FieldNode, FunctionFieldNode, NestedCollectionNode } from '../types/index.js';
10
import { getRelationType } from './get-relation-type.js';
11

12
type GetASTOptions = {
13
	accountability?: Accountability | null;
14
	action?: PermissionsAction;
15
	knex?: Knex;
16
};
17

18
type anyNested = {
19
	[collectionScope: string]: string[];
20
};
21

22
export default async function getASTFromQuery(
23
	collection: string,
24
	query: Query,
25
	schema: SchemaOverview,
26
	options?: GetASTOptions,
27
): Promise<AST> {
28
	query = cloneDeep(query);
29

30
	const accountability = options?.accountability;
31
	const action = options?.action || 'read';
32

33
	const permissions =
34
		accountability && accountability.admin !== true
35
			? accountability?.permissions?.filter((permission) => {
36
					return permission.action === action;
37
			  }) ?? []
38
			: null;
39

40
	const ast: AST = {
41
		type: 'root',
42
		name: collection,
43
		query: query,
44
		children: [],
45
	};
46

47
	let fields = ['*'];
48

49
	if (query.fields) {
50
		fields = query.fields;
51
	}
52

53
	/**
54
	 * When using aggregate functions, you can't have any other regular fields
55
	 * selected. This makes sure you never end up in a non-aggregate fields selection error
56
	 */
57
	if (Object.keys(query.aggregate || {}).length > 0) {
58
		fields = [];
59
	}
60

61
	/**
62
	 * Similarly, when grouping on a specific field, you can't have other non-aggregated fields.
63
	 * The group query will override the fields query
64
	 */
65
	if (query.group) {
66
		fields = query.group;
67
	}
68

69
	fields = uniq(fields);
70

71
	const deep = query.deep || {};
72

73
	// Prevent fields/deep from showing up in the query object in further use
74
	delete query.fields;
75
	delete query.deep;
76

77
	if (!query.sort) {
78
		// We'll default to the primary key for the standard sort output
79
		let sortField = schema.collections[collection]!.primary;
80

81
		// If a custom manual sort field is configured, use that
82
		if (schema.collections[collection]?.sortField) {
83
			sortField = schema.collections[collection]!.sortField as string;
84
		}
85

86
		// When group by is used, default to the first column provided in the group by clause
87
		if (query.group?.[0]) {
88
			sortField = query.group[0];
89
		}
90

91
		query.sort = [sortField];
92
	}
93

94
	// When no group by is supplied, but an aggregate function is used, only a single row will be
95
	// returned. In those cases, we'll ignore the sort field altogether
96
	if (query.aggregate && Object.keys(query.aggregate).length && !query.group?.[0]) {
97
		delete query.sort;
98
	}
99

100
	ast.children = await parseFields(collection, fields, deep);
101

102
	return ast;
103

104
	async function parseFields(parentCollection: string, fields: string[] | null, deep?: Record<string, any>) {
105
		if (!fields) return [];
106

107
		fields = await convertWildcards(parentCollection, fields);
108

109
		if (!fields || !Array.isArray(fields)) return [];
110

111
		const children: (NestedCollectionNode | FieldNode | FunctionFieldNode)[] = [];
112

113
		const relationalStructure: Record<string, string[] | anyNested> = Object.create(null);
114

115
		for (const fieldKey of fields) {
116
			let name = fieldKey;
117

118
			if (query.alias) {
119
				// check for field alias (is one of the key)
120
				if (name in query.alias) {
121
					name = query.alias[fieldKey]!;
122
				}
123
			}
124

125
			const isRelational =
126
				name.includes('.') ||
127
				// We'll always treat top level o2m fields as a related item. This is an alias field, otherwise it won't return
128
				// anything
129
				!!schema.relations.find(
130
					(relation) => relation.related_collection === parentCollection && relation.meta?.one_field === name,
131
				);
132

133
			if (isRelational) {
134
				// field is relational
135
				const parts = fieldKey.split('.');
136

137
				let rootField = parts[0]!;
138
				let collectionScope: string | null = null;
139

140
				// a2o related collection scoped field selector `fields=sections.section_id:headings.title`
141
				if (rootField.includes(':')) {
142
					const [key, scope] = rootField.split(':');
143
					rootField = key!;
144
					collectionScope = scope!;
145
				}
146

147
				if (rootField in relationalStructure === false) {
148
					if (collectionScope) {
149
						relationalStructure[rootField] = { [collectionScope]: [] };
150
					} else {
151
						relationalStructure[rootField] = [];
152
					}
153
				}
154

155
				if (parts.length > 1) {
156
					const childKey = parts.slice(1).join('.');
157

158
					if (collectionScope) {
159
						if (collectionScope in relationalStructure[rootField]! === false) {
160
							(relationalStructure[rootField] as anyNested)[collectionScope] = [];
161
						}
162

163
						(relationalStructure[rootField] as anyNested)[collectionScope]!.push(childKey);
164
					} else {
165
						(relationalStructure[rootField] as string[]).push(childKey);
166
					}
167
				}
168
			} else {
169
				if (fieldKey.includes('(') && fieldKey.includes(')')) {
170
					const columnName = fieldKey.match(REGEX_BETWEEN_PARENS)![1]!;
171
					const foundField = schema.collections[parentCollection]!.fields[columnName];
172

173
					if (foundField && foundField.type === 'alias') {
174
						const foundRelation = schema.relations.find(
175
							(relation) => relation.related_collection === parentCollection && relation.meta?.one_field === columnName,
176
						);
177

178
						if (foundRelation) {
179
							children.push({
180
								type: 'functionField',
181
								name,
182
								fieldKey,
183
								query: {},
184
								relatedCollection: foundRelation.collection,
185
							});
186

187
							continue;
188
						}
189
					}
190
				}
191

192
				children.push({ type: 'field', name, fieldKey });
193
			}
194
		}
195

196
		for (const [fieldKey, nestedFields] of Object.entries(relationalStructure)) {
197
			let fieldName = fieldKey;
198

199
			if (query.alias && fieldKey in query.alias) {
200
				fieldName = query.alias[fieldKey]!;
201
			}
202

203
			const relatedCollection = getRelatedCollection(parentCollection, fieldName);
204
			const relation = getRelation(parentCollection, fieldName);
205

206
			if (!relation) continue;
207

208
			const relationType = getRelationType({
209
				relation,
210
				collection: parentCollection,
211
				field: fieldName,
212
			});
213

214
			if (!relationType) continue;
215

216
			let child: NestedCollectionNode | null = null;
217

218
			if (relationType === 'a2o') {
219
				const allowedCollections = relation.meta!.one_allowed_collections!.filter((collection) => {
220
					if (!permissions) return true;
221
					return permissions.some((permission) => permission.collection === collection);
222
				});
223

224
				child = {
225
					type: 'a2o',
226
					names: allowedCollections,
227
					children: {},
228
					query: {},
229
					relatedKey: {},
230
					parentKey: schema.collections[parentCollection]!.primary,
231
					fieldKey: fieldKey,
232
					relation: relation,
233
				};
234

235
				for (const relatedCollection of allowedCollections) {
236
					child.children[relatedCollection] = await parseFields(
237
						relatedCollection,
238
						Array.isArray(nestedFields) ? nestedFields : (nestedFields as anyNested)[relatedCollection] || [],
239
						deep?.[`${fieldKey}:${relatedCollection}`],
240
					);
241

242
					child.query[relatedCollection] = getDeepQuery(deep?.[`${fieldKey}:${relatedCollection}`] || {});
243

244
					child.relatedKey[relatedCollection] = schema.collections[relatedCollection]!.primary;
245
				}
246
			} else if (relatedCollection) {
247
				if (permissions && permissions.some((permission) => permission.collection === relatedCollection) === false) {
248
					continue;
249
				}
250

251
				// update query alias for children parseFields
252
				const deepAlias = getDeepQuery(deep?.[fieldKey] || {})?.['alias'];
253
				if (!isEmpty(deepAlias)) query.alias = deepAlias;
254

255
				child = {
256
					type: relationType,
257
					name: relatedCollection,
258
					fieldKey: fieldKey,
259
					parentKey: schema.collections[parentCollection]!.primary,
260
					relatedKey: schema.collections[relatedCollection]!.primary,
261
					relation: relation,
262
					query: getDeepQuery(deep?.[fieldKey] || {}),
263
					children: await parseFields(relatedCollection, nestedFields as string[], deep?.[fieldKey] || {}),
264
				};
265

266
				if (relationType === 'o2m' && !child!.query.sort) {
267
					child!.query.sort = [relation.meta?.sort_field || schema.collections[relation.collection]!.primary];
268
				}
269
			}
270

271
			if (child) {
272
				children.push(child);
273
			}
274
		}
275

276
		// Deduplicate any children fields that are included both as a regular field, and as a nested m2o field
277
		const nestedCollectionNodes = children.filter((childNode) => childNode.type !== 'field');
278

279
		return children.filter((childNode) => {
280
			const existsAsNestedRelational = !!nestedCollectionNodes.find(
281
				(nestedCollectionNode) => childNode.fieldKey === nestedCollectionNode.fieldKey,
282
			);
283

284
			if (childNode.type === 'field' && existsAsNestedRelational) return false;
285

286
			return true;
287
		});
288
	}
289

290
	async function convertWildcards(parentCollection: string, fields: string[]) {
291
		fields = cloneDeep(fields);
292

293
		const fieldsInCollection = Object.entries(schema.collections[parentCollection]!.fields).map(([name]) => name);
294

295
		let allowedFields: string[] | null = fieldsInCollection;
296

297
		if (permissions) {
298
			const permittedFields = permissions.find((permission) => parentCollection === permission.collection)?.fields;
299
			if (permittedFields !== undefined) allowedFields = permittedFields;
300
		}
301

302
		if (!allowedFields || allowedFields.length === 0) return [];
303

304
		// In case of full read permissions
305
		if (allowedFields[0] === '*') allowedFields = fieldsInCollection;
306

307
		for (let index = 0; index < fields.length; index++) {
308
			const fieldKey = fields[index]!;
309

310
			if (fieldKey.includes('*') === false) continue;
311

312
			if (fieldKey === '*') {
313
				const aliases = Object.keys(query.alias ?? {});
314

315
				// Set to all fields in collection
316
				if (allowedFields.includes('*')) {
317
					fields.splice(index, 1, ...fieldsInCollection, ...aliases);
318
				} else {
319
					// Set to all allowed fields
320
					const allowedAliases = aliases.filter((fieldKey) => {
321
						const name = query.alias![fieldKey]!;
322
						return allowedFields!.includes(name);
323
					});
324

325
					fields.splice(index, 1, ...allowedFields, ...allowedAliases);
326
				}
327
			}
328

329
			// Swap *.* case for *,<relational-field>.*,<another-relational>.*
330
			if (fieldKey.includes('.') && fieldKey.split('.')[0] === '*') {
331
				const parts = fieldKey.split('.');
332

333
				const relationalFields = allowedFields.includes('*')
334
					? schema.relations
335
							.filter(
336
								(relation) =>
337
									relation.collection === parentCollection || relation.related_collection === parentCollection,
338
							)
339
							.map((relation) => {
340
								const isMany = relation.collection === parentCollection;
341
								return isMany ? relation.field : relation.meta?.one_field;
342
							})
343
					: allowedFields.filter((fieldKey) => !!getRelation(parentCollection, fieldKey));
344

345
				const nonRelationalFields = allowedFields.filter((fieldKey) => relationalFields.includes(fieldKey) === false);
346

347
				const aliasFields = Object.keys(query.alias ?? {}).map((fieldKey) => {
348
					const name = query.alias![fieldKey];
349

350
					if (relationalFields.includes(name)) {
351
						return `${fieldKey}.${parts.slice(1).join('.')}`;
352
					}
353

354
					return fieldKey;
355
				});
356

357
				fields.splice(
358
					index,
359
					1,
360
					...[
361
						...relationalFields.map((relationalField) => {
362
							return `${relationalField}.${parts.slice(1).join('.')}`;
363
						}),
364
						...nonRelationalFields,
365
						...aliasFields,
366
					],
367
				);
368
			}
369
		}
370

371
		return fields;
372
	}
373

374
	function getRelation(collection: string, field: string) {
375
		const relation = schema.relations.find((relation) => {
376
			return (
377
				(relation.collection === collection && relation.field === field) ||
378
				(relation.related_collection === collection && relation.meta?.one_field === field)
379
			);
380
		});
381

382
		return relation;
383
	}
384

385
	function getRelatedCollection(collection: string, field: string): string | null {
386
		const relation = getRelation(collection, field);
387

388
		if (!relation) return null;
389

390
		if (relation.collection === collection && relation.field === field) {
391
			return relation.related_collection || null;
392
		}
393

394
		if (relation.related_collection === collection && relation.meta?.one_field === field) {
395
			return relation.collection || null;
396
		}
397

398
		return null;
399
	}
400
}
401

402
function getDeepQuery(query: Record<string, any>) {
403
	return mapKeys(
404
		omitBy(query, (_value, key) => key.startsWith('_') === false),
405
		(_value, key) => key.substring(1),
406
	);
407
}
408

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

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

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

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