/**
 * Generate an AST based on a given collection and query
 */ import { Knex } from 'knex' ; import { cloneDeep , mapKeys , omitBy , uniq } from 'lodash' ; import { Accountability } from '@directus/shared/types' ; import { AST , FieldNode , NestedCollectionNode , SchemaOverview } from '../types' ; import { Query , PermissionsAction } from '@directus/shared/types' ; import { getRelationType } from '../utils/get-relation-type' ; type GetASTOptions = { accountability ? : Accountability | null ; action ? : PermissionsAction ; knex ? : Knex ; } ; type anyNested = { [ collectionScope ] : string [ ] ; } ; export default async function getASTFromQuery ( collection , query , schema , options ? ) { query = cloneDeep ( query ) ; const accountability = options ?. accountability ; const action = options ?. action || 'read' ; const permissions = accountability && accountability . admin !== true ? accountability ?. permissions ?. filter ( ( permission ) => { return permission . action === action ; } ) ?? [ ] : null ; const ast = { type : 'root' , name : collection , query : query , children : [ ] , } ; let fields = [ '*' ] ; if ( query . fields ) { fields = query . fields ; } /**
     * When using aggregate functions, you can't have any other regular fields
     * selected. This makes sure you never end up in a non-aggregate fields selection error
     */ if ( Object . keys ( query . aggregate || { } ) . length > 0 ) { fields = [ ] ; } /**
     * Similarly, when grouping on a specific field, you can't have other non-aggregated fields.
     * The group query will override the fields query
     */ if ( query . group ) { fields = query . group ; } fields = uniq ( fields ) ; const deep = query . deep || { } ; delete query . fields ; delete query . deep ; if ( ! query . sort ) { let sortField = schema . collections [ collection ] . primary ; if ( schema . collections [ collection ] ?. sortField ) { sortField = schema . collections [ collection ] . sortField as string ; } if ( query . group ?. [ 0 ] ) { sortField = query . group [ 0 ] ; } query . sort = [ sortField ] ; } if ( query . aggregate && Object . keys ( query . aggregate ) . length && ! query . group ?. [ 0 ] ) { delete query . sort ; } ast . children = await parseFields ( collection , fields , deep ) ; return ast ; async function parseFields ( parentCollection , fields , deep ? ) { if ( ! fields ) return [ ] ; fields = await convertWildcards ( parentCollection , fields ) ; if ( ! fields ) return [ ] ; const children = [ ] ; const relationalStructure = { } ; for ( const fieldKey of fields ) { let name = fieldKey ; const isAlias = ( query . alias && name in query . alias ) ?? false ; if ( isAlias ) { name = query . alias ! [ fieldKey ] ; } const isRelational = name . includes ( '.' ) || ! ! schema . relations . find ( ( relation ) => relation . related_collection === parentCollection && relation . meta ?. one_field === name ) ; if ( isRelational ) { const parts = name . split ( '.' ) ; 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 ] as anyNested ) [ collectionScope ] = [ ] ; } ( relationalStructure [ rootField ] as anyNested ) [ collectionScope ] . push ( childKey ) ; } else { ( relationalStructure [ rootField ] as string [ ] ) . push ( childKey ) ; } } } else { children . push ( { type : 'field' , name , fieldKey } ) ; } } for ( const [ fieldKey , nestedFields ] of Object . entries ( relationalStructure ) ) { let fieldName = fieldKey ; if ( query . alias && fieldKey in query . alias ) { fieldName = query . alias [ fieldKey ] ; } const relatedCollection = getRelatedCollection ( parentCollection , fieldName ) ; const relation = getRelation ( parentCollection , fieldName ) ; if ( ! relation ) continue ; const relationType = getRelationType ( { relation , collection : parentCollection , field : fieldName , } ) ; if ( ! relationType ) continue ; let child = null ; if ( relationType === 'a2o' ) { const allowedCollections = relation . meta ! . one_allowed_collections ! . filter ( ( collection ) => { if ( ! permissions ) return true ; return permissions . some ( ( permission ) => permission . collection === collection ) ; } ) ; child = { type : 'a2o' , names : allowedCollections , children : { } , query : { } , relatedKey : { } , parentKey : schema . collections [ parentCollection ] . primary , fieldKey : fieldKey , relation : relation , } ; for ( const relatedCollection of allowedCollections ) { child . children [ relatedCollection ] = await parseFields ( relatedCollection , Array . isArray ( nestedFields ) ? nestedFields : ( nestedFields as anyNested ) [ relatedCollection ] || [ '*' ] , deep ?. [ `${ fieldKey }:${ relatedCollection }` ] ) ; child . query [ relatedCollection ] = getDeepQuery ( deep ?. [ `${ fieldKey }:${ relatedCollection }` ] || { } ) ; child . relatedKey [ relatedCollection ] = schema . collections [ relatedCollection ] . primary ; } } else if ( relatedCollection ) { if ( permissions && permissions . some ( ( permission ) => permission . collection === relatedCollection ) === false ) { continue ; } child = { type : relationType , name : relatedCollection , fieldKey : fieldKey , parentKey : schema . collections [ parentCollection ] . primary , relatedKey : schema . collections [ relatedCollection ] . primary , relation : relation , query : getDeepQuery ( deep ?. [ fieldKey ] || { } ) , children : await parseFields ( relatedCollection , nestedFields as string [ ] , deep ?. [ fieldKey ] || { } ) , } ; if ( relationType === 'o2m' && ! child ! . query . sort ) { child ! . query . sort = [ relation . meta ?. sort_field || schema . collections [ relation . collection ] . primary ] ; } } 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 ; } ) ; } async function convertWildcards ( parentCollection , fields ) { fields = cloneDeep ( fields ) ; const fieldsInCollection = Object . entries ( schema . collections [ parentCollection ] . fields ) . map ( ( [ name ] ) => name ) ; let allowedFields = fieldsInCollection ; if ( permissions ) { const permittedFields = permissions . find ( ( permission ) => parentCollection === permission . collection ) ?. fields ; if ( permittedFields !== undefined ) allowedFields = permittedFields ; } if ( ! allowedFields || allowedFields . length === 0 ) return [ ] ; if ( allowedFields [ 0 ] === '*' ) allowedFields = fieldsInCollection ; for ( let index = 0 ; index < fields . length ; index ++ ) { const fieldKey = fields [ index ] ; if ( fieldKey . includes ( '*' ) === false ) continue ; if ( fieldKey === '*' ) { const aliases = Object . keys ( query . alias ?? { } ) ; if ( allowedFields . includes ( '*' ) ) { fields . splice ( index , 1 , ... fieldsInCollection , ... aliases ) ; } else { const allowedAliases = aliases . filter ( ( fieldKey ) => { const name = query . alias ! [ fieldKey ] ; return allowedFields ! . includes ( name ) ; } ) ; fields . splice ( index , 1 , ... allowedFields , ... allowedAliases ) ; } } if ( fieldKey . includes ( '.' ) && fieldKey . split ( '.' ) [ 0 ] === '*' ) { const parts = fieldKey . split ( '.' ) ; const relationalFields = allowedFields . includes ( '*' ) ? schema . relations . filter ( ( relation ) => relation . collection === parentCollection || relation . related_collection === parentCollection ) . map ( ( relation ) => { const isMany = relation . collection === parentCollection ; return isMany ? relation . field : relation . meta ?. one_field ; } ) : allowedFields . filter ( ( fieldKey ) => ! ! getRelation ( parentCollection , fieldKey ) ) ; const nonRelationalFields = allowedFields . filter ( ( fieldKey ) => relationalFields . includes ( fieldKey ) === false ) ; const aliasFields = Object . keys ( query . alias ?? { } ) . map ( ( fieldKey ) => { const name = query . alias ! [ fieldKey ] ; if ( relationalFields . includes ( name ) ) { return `${ fieldKey }.${ parts . slice ( 1 ) . join ( '.' ) }` ; } return fieldKey ; } ) ; fields . splice ( index , 1 , ... [ ... relationalFields . map ( ( relationalField ) => { return `${ relationalField }.${ parts . slice ( 1 ) . join ( '.' ) }` ; } ) , ... nonRelationalFields , ... aliasFields , ] ) ; } } return fields ; } function getRelation ( collection , field ) { const relation = schema . relations . find ( ( relation ) => { return ( ( relation . collection === collection && relation . field === field ) || ( relation . related_collection === collection && relation . meta ?. one_field === field ) ) ; } ) ; return relation ; } function getRelatedCollection ( collection , field ) { const relation = getRelation ( collection , field ) ; if ( ! relation ) return null ; if ( relation . collection === collection && relation . field === field ) { return relation . related_collection || null ; } if ( relation . related_collection === collection && relation . meta ?. one_field === field ) { return relation . collection || null ; } return null ; } } function getDeepQuery ( query ) { return mapKeys ( omitBy ( query , ( value , key ) => key . startsWith ( '_' ) === false ) , ( value , key ) => key . substring ( 1 ) ) ; }