import { Knex } from 'knex' ; import { clone , cloneDeep , pick , uniq , merge } from 'lodash' ; import { PayloadService } from '../services/payload' ; import { Item , SchemaOverview } from '../types' ; import { AST , FieldNode , NestedCollectionNode , M2ONode } from '../types/ast' ; import { applyFunctionToColumnName } from '../utils/apply-function-to-column-name' ; import applyQuery from '../utils/apply-query' ; import { getColumn } from '../utils/get-column' ; import { stripFunction } from '../utils/strip-function' ; import { toArray } from '@directus/shared/utils' ; import { Query } from '@directus/shared/types' ; import getDatabase from '.' ; import { getHelpers } from '../database/helpers' ; type RunASTOptions = { /**
     * Query override for the current level
     */ query ? : AST [ 'query' ] ; /**
     * Knex instance
     */ knex ? : Knex ; /**
     * Whether or not the current execution is a nested dataset in another AST
     */ nested ? : boolean ; /**
     * Whether or not to strip out non-requested required fields automatically (eg IDs / FKs)
     */ stripNonRequested ? : boolean ; } ; /**
 * Execute a given AST using Knex. Returns array of items based on requested AST.
 */ export default async function runAST ( originalAST , schema , options ? ) { const ast = cloneDeep ( originalAST ) ; const knex = options ?. knex || getDatabase ( ) ; if ( ast . type === 'a2o' ) { const results = { } ; for ( const collection of ast . names ) { results [ collection ] = await run ( collection , ast . children [ collection ] , ast . query [ collection ] ) ; } return results ; } else { return await run ( ast . name , ast . children , options ?. query || ast . query ) ; } async function run ( collection , children , query ) { const { fieldNodes , primaryKeyField , nestedCollectionNodes } = await parseCurrentLevel ( schema , collection , children , query ) ; const dbQuery = getDBQuery ( schema , knex , collection , fieldNodes , query ) ; const rawItems = await dbQuery ; if ( ! rawItems ) return null ; const payloadService = new PayloadService ( collection , { knex , schema } ) ; let items = await payloadService . processValues ( 'read' , rawItems ) ; if ( ! items || items . length === 0 ) return items ; const nestedNodes = applyParentFilters ( schema , nestedCollectionNodes , items ) ; for ( const nestedNode of nestedNodes ) { const nestedItems = await runAST ( nestedNode , schema , { knex , nested : true } ) ; if ( nestedItems ) { items = mergeWithParentItems ( schema , nestedItems , items , nestedNode ) ; } } if ( options ?. nested !== true && options ?. stripNonRequested !== false ) { items = removeTemporaryFields ( schema , items , originalAST , primaryKeyField ) ; } return items ; } } async function parseCurrentLevel ( schema , collection , children , query ) { const primaryKeyField = schema . collections [ collection ] . primary ; const columnsInCollection = Object . keys ( schema . collections [ collection ] . fields ) ; const columnsToSelectInternal = [ ] ; const nestedCollectionNodes = [ ] ; for ( const child of children ) { if ( child . type === 'field' ) { const fieldKey = stripFunction ( child . name ) ; if ( columnsInCollection . includes ( fieldKey ) || fieldKey === '*' ) { columnsToSelectInternal . push ( child . name ) ; if ( query . alias ) { columnsToSelectInternal . push ( ... Object . entries ( query . alias ) . filter ( ( [ _key , value ] ) => value === child . name ) . map ( ( [ key ] ) => key ) ) ; } } continue ; } if ( ! child . relation ) continue ; if ( child . type === 'm2o' ) { columnsToSelectInternal . push ( child . fieldKey ) ; } if ( child . type === 'a2o' ) { columnsToSelectInternal . push ( child . relation . field ) ; columnsToSelectInternal . push ( child . relation . meta ! . one_collection_field ! ) ; } nestedCollectionNodes . push ( child ) ; } const isAggregate = ( query . group || ( query . aggregate && Object . keys ( query . aggregate ) . length > 0 ) ) ?? false ; /** Always fetch primary key in case there's a nested relation that needs it. Aggregate payloads
     * can't have nested relational fields
     */ if ( isAggregate === false && columnsToSelectInternal . includes ( primaryKeyField ) === false ) { columnsToSelectInternal . push ( primaryKeyField ) ; } /** Make sure select list has unique values */ const columnsToSelect = [ ... new Set ( columnsToSelectInternal ) ] ; const fieldNodes = columnsToSelect . map ( ( column ) => children . find ( ( childNode ) => childNode . type === 'field' && childNode . fieldKey === column ) ?? { type : 'field' , name : column , fieldKey : column , } ) as FieldNode [ ] ; return { fieldNodes , nestedCollectionNodes , primaryKeyField } ; } function getColumnPreprocessor ( knex , schema , table ) { const helpers = getHelpers ( knex ) ; return function ( fieldNode ) : Knex . Raw < string > { let field ; if ( fieldNode . type === 'field' ) { field = schema . collections [ table ] . fields [ stripFunction ( fieldNode . name ) ] ; } else { field = schema . collections [ fieldNode . relation . collection ] . fields [ fieldNode . relation . field ] ; } let alias = undefined ; if ( fieldNode . name !== fieldNode . fieldKey ) { alias = fieldNode . fieldKey ; } if ( field . type . startsWith ( 'geometry' ) ) { return helpers . st . asText ( table , field . field ) ; } return getColumn ( knex , table , fieldNode . name , alias ) ; } ; } function getDBQuery ( schema , knex , table , fieldNodes , query ) { const preProcess = getColumnPreprocessor ( knex , schema , table ) ; const dbQuery = knex . select ( fieldNodes . map ( preProcess ) ) . from ( table ) ; const queryCopy = clone ( query ) ; queryCopy . limit = typeof queryCopy . limit === 'number' ? queryCopy . limit : 100 ; return applyQuery ( knex , table , dbQuery , queryCopy , schema ) ; } function applyParentFilters ( schema , nestedCollectionNodes , parentItem ) { const parentItems = toArray ( parentItem ) ; for ( const nestedNode of nestedCollectionNodes ) { if ( ! nestedNode . relation ) continue ; if ( nestedNode . type === 'm2o' ) { const foreignField = schema . collections [ nestedNode . relation . related_collection ! ] . primary ; const foreignIds = uniq ( parentItems . map ( ( res ) => res [ nestedNode . relation . field ] ) ) . filter ( ( id ) => id ) ; const limit = nestedNode . query . limit ; if ( limit === - 1 ) { merge ( nestedNode , { query : { filter : { [ foreignField ] : { _in : foreignIds } } } } ) ; } else { nestedNode . query . union = [ foreignField , foreignIds ] ; } } else if ( nestedNode . type === 'o2m' ) { const relatedM2OisFetched = ! ! nestedNode . children . find ( ( child ) => { return child . type === 'field' && child . name === nestedNode . relation . field ; } ) ; if ( relatedM2OisFetched === false ) { nestedNode . children . push ( { type : 'field' , name : nestedNode . relation . field , fieldKey : nestedNode . relation . field , } ) ; } if ( nestedNode . relation . meta ?. sort_field ) { nestedNode . children . push ( { type : 'field' , name : nestedNode . relation . meta . sort_field , fieldKey : nestedNode . relation . meta . sort_field , } ) ; } const foreignField = nestedNode . relation . field ; const foreignIds = uniq ( parentItems . map ( ( res ) => res [ nestedNode . parentKey ] ) ) . filter ( ( id ) => id ) ; const limit = nestedNode . query . limit ; if ( limit === - 1 ) { merge ( nestedNode , { query : { filter : { [ foreignField ] : { _in : foreignIds } } } } ) ; } else { nestedNode . query . union = [ foreignField , foreignIds ] ; } } else if ( nestedNode . type === 'a2o' ) { const keysPerCollection = { } ; for ( const parentItem of parentItems ) { const collection = parentItem [ nestedNode . relation . meta ! . one_collection_field ! ] ; if ( ! keysPerCollection [ collection ] ) keysPerCollection [ collection ] = [ ] ; keysPerCollection [ collection ] . push ( parentItem [ nestedNode . relation . field ] ) ; } for ( const relatedCollection of nestedNode . names ) { const foreignField = nestedNode . relatedKey [ relatedCollection ] ; const foreignIds = uniq ( keysPerCollection [ relatedCollection ] ) ; const limit = nestedNode . query [ relatedCollection ] . limit ; if ( limit === - 1 ) { merge ( nestedNode , { query : { [ relatedCollection ] : { filter : { [ foreignField ] : { _in : foreignIds } } } } } ) ; } else { nestedNode . query [ relatedCollection ] . union = [ foreignField , foreignIds ] ; } } } } return nestedCollectionNodes ; } function mergeWithParentItems ( schema , nestedItem , parentItem , nestedNode ) { const nestedItems = toArray ( nestedItem ) ; const parentItems = clone ( toArray ( parentItem ) ) ; if ( nestedNode . type === 'm2o' ) { for ( const parentItem of parentItems ) { const itemChild = nestedItems . find ( ( nestedItem ) => { return ( nestedItem [ schema . collections [ nestedNode . relation . related_collection ! ] . primary ] == parentItem [ nestedNode . fieldKey ] ) ; } ) ; parentItem [ nestedNode . fieldKey ] = itemChild || null ; } } else if ( nestedNode . type === 'o2m' ) { for ( const parentItem of parentItems ) { const itemChildren = nestedItems . filter ( ( nestedItem ) => { if ( nestedItem === null ) return false ; if ( Array . isArray ( nestedItem [ nestedNode . relation . field ] ) ) return true ; return ( nestedItem [ nestedNode . relation . field ] == parentItem [ schema . collections [ nestedNode . relation . related_collection ! ] . primary ] || nestedItem [ nestedNode . relation . field ] ?. [ schema . collections [ nestedNode . relation . related_collection ! ] . primary ] == parentItem [ schema . collections [ nestedNode . relation . related_collection ! ] . primary ] ) ; } ) . sort ( ( a , b ) => { const sortField = nestedNode . query . sort ! [ 0 ] ! ; let column = sortField ; let order = 'asc' ; if ( sortField . startsWith ( '-' ) ) { column = sortField . substring ( 1 ) ; order = 'desc' ; } if ( a [ column ] === b [ column ] ) return 0 ; if ( a [ column ] === null ) return 1 ; if ( b [ column ] === null ) return - 1 ; if ( order === 'asc' ) { return a [ column ] < b [ column ] ? - 1 : 1 ; } else { return a [ column ] < b [ column ] ? 1 : - 1 ; } } ) ; parentItem [ nestedNode . fieldKey ] = itemChildren . length > 0 ? itemChildren : [ ] ; } } else if ( nestedNode . type === 'a2o' ) { for ( const parentItem of parentItems ) { if ( ! nestedNode . relation . meta ?. one_collection_field ) { parentItem [ nestedNode . fieldKey ] = null ; continue ; } const relatedCollection = parentItem [ nestedNode . relation . meta . one_collection_field ] ; if ( ! ( nestedItem as Record < string , any [ ] > ) [ relatedCollection ] ) { parentItem [ nestedNode . fieldKey ] = null ; continue ; } const itemChild = ( nestedItem as Record < string , any [ ] > ) [ relatedCollection ] . find ( ( nestedItem ) => { return nestedItem [ nestedNode . relatedKey [ relatedCollection ] ] == parentItem [ nestedNode . fieldKey ] ; } ) ; parentItem [ nestedNode . fieldKey ] = itemChild || null ; } } return Array . isArray ( parentItem ) ? parentItems : parentItems [ 0 ] ; } function removeTemporaryFields ( schema , rawItem , ast , primaryKeyField , parentItem ? ) { const rawItems = cloneDeep ( toArray ( rawItem ) ) ; const items = [ ] ; if ( ast . type === 'a2o' ) { const fields = { } ; const nestedCollectionNodes = { } ; for ( const relatedCollection of ast . names ) { if ( ! fields [ relatedCollection ] ) fields [ relatedCollection ] = [ ] ; if ( ! nestedCollectionNodes [ relatedCollection ] ) nestedCollectionNodes [ relatedCollection ] = [ ] ; for ( const child of ast . children [ relatedCollection ] ) { if ( child . type === 'field' ) { fields [ relatedCollection ] . push ( child . name ) ; } else { fields [ relatedCollection ] . push ( child . fieldKey ) ; nestedCollectionNodes [ relatedCollection ] . push ( child ) ; } } } for ( const rawItem of rawItems ) { const relatedCollection = parentItem ?. [ ast . relation . meta ! . one_collection_field ! ] ; if ( rawItem === null || rawItem === undefined ) return rawItem ; let item = rawItem ; for ( const nestedNode of nestedCollectionNodes [ relatedCollection ] ) { item [ nestedNode . fieldKey ] = removeTemporaryFields ( schema , item [ nestedNode . fieldKey ] , nestedNode , schema . collections [ nestedNode . relation . collection ] . primary , item ) ; } item = fields [ relatedCollection ] . length > 0 ? pick ( rawItem , fields [ relatedCollection ] ) : rawItem [ primaryKeyField ] ; items . push ( item ) ; } } else { const fields = [ ] ; const nestedCollectionNodes = [ ] ; for ( const child of ast . children ) { fields . push ( child . fieldKey ) ; if ( child . type !== 'field' ) { nestedCollectionNodes . push ( child ) ; } } if ( ast . query ?. aggregate ) { for ( const [ operation , aggregateFields ] of Object . entries ( ast . query . aggregate ) ) { if ( ! fields ) continue ; if ( operation === 'count' && aggregateFields . includes ( '*' ) ) fields . push ( 'count' ) ; fields . push ( ... aggregateFields . map ( ( field ) => `${ operation }.${ field }` ) ) ; } } for ( const rawItem of rawItems ) { if ( rawItem === null || rawItem === undefined ) return rawItem ; let item = rawItem ; for ( const nestedNode of nestedCollectionNodes ) { item [ nestedNode . fieldKey ] = removeTemporaryFields ( schema , item [ nestedNode . fieldKey ] , nestedNode , nestedNode . type === 'm2o' ? schema . collections [ nestedNode . relation . related_collection ! ] . primary : schema . collections [ nestedNode . relation . collection ] . primary , item ) ; } const fieldsWithFunctionsApplied = fields . map ( ( field ) => applyFunctionToColumnName ( field ) ) ; item = fields . length > 0 ? pick ( rawItem , fieldsWithFunctionsApplied ) : rawItem [ primaryKeyField ] ; items . push ( item ) ; } } return Array . isArray ( rawItem ) ? items : items [ 0 ] ; }