UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

223 lines (222 loc) 10.6 kB
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'; }