@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
223 lines (222 loc) • 10.6 kB
JavaScript
import { REGEX_BETWEEN_PARENS } from '@directus/constants';
import { getRelation } from '@directus/utils';
import { isEmpty } from 'lodash-es';
import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
import { getRelationType } from '../../../utils/get-relation-type.js';
import { getAllowedSort } from '../utils/get-allowed-sort.js';
import { getDeepQuery } from '../utils/get-deep-query.js';
import { getRelatedCollection } from '../utils/get-related-collection.js';
import { convertWildcards } from './convert-wildcards.js';
export async function parseFields(options, context) {
let { fields } = options;
if (!fields)
return [];
fields = await convertWildcards({
fields,
collection: options.parentCollection,
alias: options.query.alias,
accountability: options.accountability,
backlink: options.query.backlink,
}, context);
if (!fields || !Array.isArray(fields))
return [];
const children = [];
const policies = options.accountability && options.accountability.admin === false
? await fetchPolicies(options.accountability, context)
: null;
const relationalStructure = Object.create(null);
for (const fieldKey of fields) {
let alias = false;
let name = fieldKey;
if (options.query.alias) {
// check for field alias (is one of the key)
if (name in options.query.alias) {
alias = true;
name = options.query.alias[fieldKey];
}
}
const isRelational = name.includes('.') ||
// We'll always treat top level o2m fields as a related item. This is an alias field, otherwise it won't return
// anything
!!context.schema.relations.find((relation) => relation.related_collection === options.parentCollection && relation.meta?.one_field === name);
if (isRelational) {
// field is relational
const parts = fieldKey.split('.');
let rootField = parts[0];
let collectionScope = null;
// a2o related collection scoped field selector `fields=sections.section_id:headings.title`
if (rootField.includes(':')) {
const [key, scope] = rootField.split(':');
rootField = key;
collectionScope = scope;
}
if (rootField in relationalStructure === false) {
if (collectionScope) {
relationalStructure[rootField] = { [collectionScope]: [] };
}
else {
relationalStructure[rootField] = [];
}
}
if (parts.length > 1) {
const childKey = parts.slice(1).join('.');
if (collectionScope) {
if (collectionScope in relationalStructure[rootField] === false) {
relationalStructure[rootField][collectionScope] = [];
}
relationalStructure[rootField][collectionScope].push(childKey);
}
else {
relationalStructure[rootField].push(childKey);
}
}
}
else {
if (name.includes('(') && name.includes(')')) {
const columnName = name.match(REGEX_BETWEEN_PARENS)[1];
const foundField = context.schema.collections[options.parentCollection].fields[columnName];
if (foundField && foundField.type === 'alias') {
const foundRelation = context.schema.relations.find((relation) => relation.related_collection === options.parentCollection && relation.meta?.one_field === columnName);
if (foundRelation) {
children.push({
type: 'functionField',
name,
fieldKey,
query: {},
relatedCollection: foundRelation.collection,
whenCase: [],
cases: [],
});
continue;
}
}
}
if (name.includes(':')) {
const [key, scope] = name.split(':');
if (key in relationalStructure === false) {
relationalStructure[key] = { [scope]: [] };
}
else if (scope in relationalStructure[key] === false) {
relationalStructure[key][scope] = [];
}
continue;
}
children.push({ type: 'field', name, fieldKey, whenCase: [], alias });
}
}
for (const [fieldKey, nestedFields] of Object.entries(relationalStructure)) {
let fieldName = fieldKey;
if (options.query.alias && fieldKey in options.query.alias) {
fieldName = options.query.alias[fieldKey];
}
const relatedCollection = getRelatedCollection(context.schema, options.parentCollection, fieldName);
const relation = getRelation(context.schema.relations, options.parentCollection, fieldName);
if (!relation)
continue;
const relationType = getRelationType({
relation,
collection: options.parentCollection,
field: fieldName,
});
if (!relationType)
continue;
let child = null;
if (relationType === 'a2o') {
let allowedCollections = relation.meta.one_allowed_collections;
if (options.accountability && options.accountability.admin === false && policies) {
const permissions = await fetchPermissions({
action: 'read',
collections: allowedCollections,
policies: policies,
accountability: options.accountability,
}, context);
allowedCollections = allowedCollections.filter((collection) => permissions.some((permission) => permission.collection === collection));
}
child = {
type: 'a2o',
names: allowedCollections,
children: {},
query: {},
relatedKey: {},
parentKey: context.schema.collections[options.parentCollection].primary,
fieldKey: fieldKey,
relation: relation,
cases: {},
whenCase: [],
};
for (const relatedCollection of allowedCollections) {
child.children[relatedCollection] = await parseFields({
parentCollection: relatedCollection,
fields: Array.isArray(nestedFields)
? nestedFields
: nestedFields[relatedCollection] || [],
query: options.query,
deep: options.deep?.[`${fieldKey}:${relatedCollection}`],
accountability: options.accountability,
}, { ...context, parentRelation: relation });
child.query[relatedCollection] = getDeepQuery(options.deep?.[`${fieldKey}:${relatedCollection}`] || {});
child.relatedKey[relatedCollection] = context.schema.collections[relatedCollection].primary;
}
}
else if (relatedCollection) {
if (options.accountability && options.accountability.admin === false && policies) {
const permissions = await fetchPermissions({
action: 'read',
collections: [relatedCollection],
policies: policies,
accountability: options.accountability,
}, context);
// Skip related collection if no permissions
if (permissions.length === 0) {
continue;
}
}
const childQuery = { ...options.query };
// update query alias for children parseFields
const deepAlias = getDeepQuery(options.deep?.[fieldKey] || {})?.['alias'];
// reset alias to empty if none are present
childQuery.alias = isEmpty(deepAlias) ? {} : deepAlias;
child = {
type: relationType,
name: relatedCollection,
fieldKey: fieldKey,
parentKey: context.schema.collections[options.parentCollection].primary,
relatedKey: context.schema.collections[relatedCollection].primary,
relation: relation,
query: getDeepQuery(options.deep?.[fieldKey] || {}),
children: await parseFields({
parentCollection: relatedCollection,
fields: nestedFields,
query: childQuery,
deep: options.deep?.[fieldKey] || {},
accountability: options.accountability,
}, { ...context, parentRelation: relation }),
cases: [],
whenCase: [],
};
if (isO2MNode(child) && !child.query.sort) {
child.query.sort = await getAllowedSort({ collection: relation.collection, relation, accountability: options.accountability }, context);
}
if (isO2MNode(child) && child.query.group && child.query.group[0] !== relation.field) {
// If a group by is used, the result needs to be grouped by the foreign key of the relation first, so results
// are correctly grouped under the foreign key when extracting the grouped results from the nested queries.
child.query.group.unshift(relation.field);
}
}
if (child) {
children.push(child);
}
}
// Deduplicate any children fields that are included both as a regular field, and as a nested m2o field
const nestedCollectionNodes = children.filter((childNode) => childNode.type !== 'field');
return children.filter((childNode) => {
const existsAsNestedRelational = !!nestedCollectionNodes.find((nestedCollectionNode) => childNode.fieldKey === nestedCollectionNode.fieldKey);
if (childNode.type === 'field' && existsAsNestedRelational)
return false;
return true;
});
}
export function isO2MNode(node) {
return !!node && node.type === 'o2m';
}