UNPKG

@directus/api

Version:

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

214 lines (212 loc) 8.34 kB
import { splitFieldPath } from "../../../utils/split-field-path.js"; import { fetchPolicies } from "../../../permissions/lib/fetch-policies.js"; import { fetchPermissions } from "../../../permissions/lib/fetch-permissions.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"; import { getRelation, getRelationType, parseFilterFunctionPath } from "@directus/utils"; import { isEmpty } from "lodash-es"; import { REGEX_BETWEEN_PARENS } from "@directus/constants"; //#region src/database/get-ast-from-query/lib/parse-fields.ts 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) { if (name in options.query.alias) { alias = true; name = options.query.alias[fieldKey]; } } name = parseFilterFunctionPath(name); const isFunctionCall = name.includes("(") && name.includes(")"); if (isFunctionCall) { const functionName = name.split("(")[0]; const columnName = name.match(REGEX_BETWEEN_PARENS)[1]; const foundField = context.schema.collections[options.parentCollection].fields[columnName]; if (functionName === "count" && 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 (functionName === "json") { children.push({ type: "functionField", name, fieldKey, query: {}, relatedCollection: options.parentCollection, whenCase: [], cases: [] }); continue; } } const isRelationalFunctionCall = isFunctionCall && name.includes(".") && name.indexOf(".") < name.indexOf("("); if ((!isFunctionCall || isRelationalFunctionCall) && (name.includes(".") || !!context.schema.relations.find((relation) => relation.related_collection === options.parentCollection && relation.meta?.one_field === name))) { const parts = splitFieldPath(isFunctionCall ? name : fieldKey); let rootField = parts[0]; let collectionScope = null; 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(":")) { 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, useA2O: true }); 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, 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, relation, cases: {}, whenCase: [] }; for (const relatedCollection$1 of allowedCollections) { const collectionDeep = options.deep?.[`${fieldKey}:${relatedCollection$1}`]; const childQuery = { ...options.query }; const deepAlias = getDeepQuery(collectionDeep || {})?.["alias"]; childQuery.alias = isEmpty(deepAlias) ? {} : deepAlias; child.children[relatedCollection$1] = await parseFields({ parentCollection: relatedCollection$1, fields: Array.isArray(nestedFields) ? nestedFields : nestedFields[relatedCollection$1] || [], query: childQuery, deep: collectionDeep, accountability: options.accountability }, { ...context, parentRelation: relation }); child.query[relatedCollection$1] = getDeepQuery(collectionDeep || {}); child.relatedKey[relatedCollection$1] = context.schema.collections[relatedCollection$1].primary; } } else if (relatedCollection) { if (options.accountability && options.accountability.admin === false && policies) { if ((await fetchPermissions({ action: "read", collections: [relatedCollection], policies, accountability: options.accountability }, context)).length === 0) continue; } const childQuery = { ...options.query }; const deepAlias = getDeepQuery(options.deep?.[fieldKey] || {})?.["alias"]; childQuery.alias = isEmpty(deepAlias) ? {} : deepAlias; child = { type: relationType, name: relatedCollection, fieldKey, parentKey: context.schema.collections[options.parentCollection].primary, relatedKey: context.schema.collections[relatedCollection].primary, 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) child.query.group.unshift(relation.field); } if (child) children.push(child); } 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; }); } function isO2MNode(node) { return !!node && node.type === "o2m"; } //#endregion export { isO2MNode, parseFields };