2
* Generate an AST based on a given collection and query
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';
13
accountability?: Accountability | null;
14
action?: PermissionsAction;
19
[collectionScope: string]: string[];
22
export default async function getASTFromQuery(
25
schema: SchemaOverview,
26
options?: GetASTOptions,
28
query = cloneDeep(query);
30
const accountability = options?.accountability;
31
const action = options?.action || 'read';
34
accountability && accountability.admin !== true
35
? accountability?.permissions?.filter((permission) => {
36
return permission.action === action;
50
fields = query.fields;
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
57
if (Object.keys(query.aggregate || {}).length > 0) {
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
69
fields = uniq(fields);
71
const deep = query.deep || {};
73
// Prevent fields/deep from showing up in the query object in further use
78
// We'll default to the primary key for the standard sort output
79
let sortField = schema.collections[collection]!.primary;
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;
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];
91
query.sort = [sortField];
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]) {
100
ast.children = await parseFields(collection, fields, deep);
104
async function parseFields(parentCollection: string, fields: string[] | null, deep?: Record<string, any>) {
105
if (!fields) return [];
107
fields = await convertWildcards(parentCollection, fields);
109
if (!fields || !Array.isArray(fields)) return [];
111
const children: (NestedCollectionNode | FieldNode | FunctionFieldNode)[] = [];
113
const relationalStructure: Record<string, string[] | anyNested> = Object.create(null);
115
for (const fieldKey of fields) {
119
// check for field alias (is one of the key)
120
if (name in query.alias) {
121
name = query.alias[fieldKey]!;
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
129
!!schema.relations.find(
130
(relation) => relation.related_collection === parentCollection && relation.meta?.one_field === name,
134
// field is relational
135
const parts = fieldKey.split('.');
137
let rootField = parts[0]!;
138
let collectionScope: string | null = null;
140
// a2o related collection scoped field selector `fields=sections.section_id:headings.title`
141
if (rootField.includes(':')) {
142
const [key, scope] = rootField.split(':');
144
collectionScope = scope!;
147
if (rootField in relationalStructure === false) {
148
if (collectionScope) {
149
relationalStructure[rootField] = { [collectionScope]: [] };
151
relationalStructure[rootField] = [];
155
if (parts.length > 1) {
156
const childKey = parts.slice(1).join('.');
158
if (collectionScope) {
159
if (collectionScope in relationalStructure[rootField]! === false) {
160
(relationalStructure[rootField] as anyNested)[collectionScope] = [];
163
(relationalStructure[rootField] as anyNested)[collectionScope]!.push(childKey);
165
(relationalStructure[rootField] as string[]).push(childKey);
169
if (fieldKey.includes('(') && fieldKey.includes(')')) {
170
const columnName = fieldKey.match(REGEX_BETWEEN_PARENS)![1]!;
171
const foundField = schema.collections[parentCollection]!.fields[columnName];
173
if (foundField && foundField.type === 'alias') {
174
const foundRelation = schema.relations.find(
175
(relation) => relation.related_collection === parentCollection && relation.meta?.one_field === columnName,
180
type: 'functionField',
184
relatedCollection: foundRelation.collection,
192
children.push({ type: 'field', name, fieldKey });
196
for (const [fieldKey, nestedFields] of Object.entries(relationalStructure)) {
197
let fieldName = fieldKey;
199
if (query.alias && fieldKey in query.alias) {
200
fieldName = query.alias[fieldKey]!;
203
const relatedCollection = getRelatedCollection(parentCollection, fieldName);
204
const relation = getRelation(parentCollection, fieldName);
206
if (!relation) continue;
208
const relationType = getRelationType({
210
collection: parentCollection,
214
if (!relationType) continue;
216
let child: NestedCollectionNode | null = null;
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);
226
names: allowedCollections,
230
parentKey: schema.collections[parentCollection]!.primary,
235
for (const relatedCollection of allowedCollections) {
236
child.children[relatedCollection] = await parseFields(
238
Array.isArray(nestedFields) ? nestedFields : (nestedFields as anyNested)[relatedCollection] || [],
239
deep?.[`${fieldKey}:${relatedCollection}`],
242
child.query[relatedCollection] = getDeepQuery(deep?.[`${fieldKey}:${relatedCollection}`] || {});
244
child.relatedKey[relatedCollection] = schema.collections[relatedCollection]!.primary;
246
} else if (relatedCollection) {
247
if (permissions && permissions.some((permission) => permission.collection === relatedCollection) === false) {
251
// update query alias for children parseFields
252
const deepAlias = getDeepQuery(deep?.[fieldKey] || {})?.['alias'];
253
if (!isEmpty(deepAlias)) query.alias = deepAlias;
257
name: relatedCollection,
259
parentKey: schema.collections[parentCollection]!.primary,
260
relatedKey: schema.collections[relatedCollection]!.primary,
262
query: getDeepQuery(deep?.[fieldKey] || {}),
263
children: await parseFields(relatedCollection, nestedFields as string[], deep?.[fieldKey] || {}),
266
if (relationType === 'o2m' && !child!.query.sort) {
267
child!.query.sort = [relation.meta?.sort_field || schema.collections[relation.collection]!.primary];
272
children.push(child);
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');
279
return children.filter((childNode) => {
280
const existsAsNestedRelational = !!nestedCollectionNodes.find(
281
(nestedCollectionNode) => childNode.fieldKey === nestedCollectionNode.fieldKey,
284
if (childNode.type === 'field' && existsAsNestedRelational) return false;
290
async function convertWildcards(parentCollection: string, fields: string[]) {
291
fields = cloneDeep(fields);
293
const fieldsInCollection = Object.entries(schema.collections[parentCollection]!.fields).map(([name]) => name);
295
let allowedFields: string[] | null = fieldsInCollection;
298
const permittedFields = permissions.find((permission) => parentCollection === permission.collection)?.fields;
299
if (permittedFields !== undefined) allowedFields = permittedFields;
302
if (!allowedFields || allowedFields.length === 0) return [];
304
// In case of full read permissions
305
if (allowedFields[0] === '*') allowedFields = fieldsInCollection;
307
for (let index = 0; index < fields.length; index++) {
308
const fieldKey = fields[index]!;
310
if (fieldKey.includes('*') === false) continue;
312
if (fieldKey === '*') {
313
const aliases = Object.keys(query.alias ?? {});
315
// Set to all fields in collection
316
if (allowedFields.includes('*')) {
317
fields.splice(index, 1, ...fieldsInCollection, ...aliases);
319
// Set to all allowed fields
320
const allowedAliases = aliases.filter((fieldKey) => {
321
const name = query.alias![fieldKey]!;
322
return allowedFields!.includes(name);
325
fields.splice(index, 1, ...allowedFields, ...allowedAliases);
329
// Swap *.* case for *,<relational-field>.*,<another-relational>.*
330
if (fieldKey.includes('.') && fieldKey.split('.')[0] === '*') {
331
const parts = fieldKey.split('.');
333
const relationalFields = allowedFields.includes('*')
337
relation.collection === parentCollection || relation.related_collection === parentCollection,
340
const isMany = relation.collection === parentCollection;
341
return isMany ? relation.field : relation.meta?.one_field;
343
: allowedFields.filter((fieldKey) => !!getRelation(parentCollection, fieldKey));
345
const nonRelationalFields = allowedFields.filter((fieldKey) => relationalFields.includes(fieldKey) === false);
347
const aliasFields = Object.keys(query.alias ?? {}).map((fieldKey) => {
348
const name = query.alias![fieldKey];
350
if (relationalFields.includes(name)) {
351
return `${fieldKey}.${parts.slice(1).join('.')}`;
361
...relationalFields.map((relationalField) => {
362
return `${relationalField}.${parts.slice(1).join('.')}`;
364
...nonRelationalFields,
374
function getRelation(collection: string, field: string) {
375
const relation = schema.relations.find((relation) => {
377
(relation.collection === collection && relation.field === field) ||
378
(relation.related_collection === collection && relation.meta?.one_field === field)
385
function getRelatedCollection(collection: string, field: string): string | null {
386
const relation = getRelation(collection, field);
388
if (!relation) return null;
390
if (relation.collection === collection && relation.field === field) {
391
return relation.related_collection || null;
394
if (relation.related_collection === collection && relation.meta?.one_field === field) {
395
return relation.collection || null;
402
function getDeepQuery(query: Record<string, any>) {
404
omitBy(query, (_value, key) => key.startsWith('_') === false),
405
(_value, key) => key.substring(1),