Source: tree/executeFilters/dlistFilter.js


// @ts-check
/**
 * @import {FilterAction,FilterOptions} from './../../types.js'
 */



import {query, transaction, UUID2hex, HEX2uuid}from "@commtool/sql-query"
import { matchObject } from "../../utils/objectfilter/filters/index.js"
import { publishEvent } from "../../utils/events.js"
import { addUpdateList } from "../../server.ws.js"
import { connection } from "@commtool/sql-query"
import { errorLoggerUpdate } from "../../utils/requestLogger.js"

// processes the addition of include/exclude or intersect filter to a dynamic list (dlist)

/**
 * Gets filter data and source information for a filter
 * @param {Buffer} UIDFilter - The UID of the filter
 * @param {FilterAction} action - The filter action type
 * @param {any} [connection=null] - Database connection
 * @returns {Promise<any[]|undefined>} Filter data with source information, or undefined if error occurs
 */
export const getFilterData= async (UIDFilter,action,connection=null)=>
{
    try {
        return await query (`
            SELECT ObjectBase.Data AS filter, ObjectBase.UIDBelongsTo AS sourceUID, Source.Type AS sourceType
                FROM ObjectBase 
                INNER JOIN ObjectBase  AS Source ON (Source.UID=ObjectBase.UIDBelongsTo AND Source.Type IN ('list','group'))
                WHERE ObjectBase.UID=? AND ObjectBase.Type=?`,
            [UIDFilter,action],{cast:['json'], connection})
    }
    catch(e)
    {
        errorLoggerUpdate(e)
        return undefined
    }
}

/**
 * Determines what types of objects to select based on source type and filter
 * @param {string} sourceType - The type of source ('list' or 'group')
 * @param {Object} filter - The filter configuration object
 * @returns {string[]} Array of object types to select
 */
export const getTypes=(sourceType,filter)=>
(
        // which type of data do we have to select?
        // for list sources it is 'entry' for other sources it is defined in the filter
     sourceType==='list' ? ['entry'] : Object.keys(filter).filter(el=>['person','guest','job','extern'].includes(el))
)

/**
 * Gets objects that match include filter criteria
 * @param {Object} filter - The filter configuration
 * @param {Buffer} sourceUID - Source object UID
 * @param {string} sourceType - Source type ('list' or 'group')
 * @param {Buffer} target - Target list UID
 * @param {string[]} types - Object types to include
 * @param {any} connection - Database connection
 * @returns {Promise<any[]|undefined>} Filtered objects for inclusion, or undefined if error occurs
 */
export const getIncludeObjects= async(filter,sourceUID,sourceType,target,types, connection)=>
{
    try {
    // will return the UID of an exsiting entry in this list in UIDentry, will as well provide a newly generated UIDentry for later usage in newEntry
     // this function is already filtering the data, while it is  streamed from the database (only whne the source is not a list)
    return await query(`SELECT ObjectBase.Type, ObjectBase.UIDBelongsTo AS UIDBelongsTo,ObjectBase.UID,
            ObjectBase.Data AS ExtraData,Main.Data,MainBase.Data AS MainBaseData,
            ObjectBase.Title,Main.Display,Main.SortName,Entry.UID AS UIDEntry, UIDV1() AS newEntry
           

            FROM ObjectBase
            INNER JOIN Links 
            ON (ObjectBase.UID = Links.UID AND Links.Type IN ('member','memberA') AND ObjectBase.Type NOT IN ('filter','list','achievement'))
            INNER JOIN Member AS Main ON (Main.UID=ObjectBase.UIDBelongsTo)
            INNER JOIN ObjectBase  AS MainBase ON (Main.UID=MainBase.UID)
            LEFT JOIN (
                SELECT Entry.UID, Entry.UIDBelongsTo 
                FROM ObjectBase  AS Entry 
                INNER JOIN Links AS ELink
                ON (Entry.UID=ELink.UID)
                AND ELink.UIDTarget=? AND Entry.Type='entry'
            ) AS Entry ON (Entry.UIDBelongsTo=ObjectBase.UIDBelongsTo)
            WHERE Links.UIDTarget=?  AND ObjectBase.Type IN (?)
            GROUP BY ObjectBase.UID
        `,[target,sourceUID, types],
        {
            cast:['json'],
            // filter, when there are filter keys and the source is not a list
            filter:   (
                Object.keys(filter).length  && sourceType!=='list'? 
                    /** @param {any} row */ (row)=>(matchObject(row,filter)) :null
            ),
            log: false,
            connection
        })      
    }
    catch(e)
    {
        errorLoggerUpdate(e)
        return undefined
    }
} 

/**
 * Gets objects that match exclude/intersect filter criteria
 * @param {Object} filter - The filter configuration
 * @param {FilterAction} action - Filter action ('exclude' or 'intersect')
 * @param {Buffer} sourceUID - Source object UID
 * @param {string} sourceType - Source type
 * @param {Buffer} target - Target list UID
 * @param {string[]} types - Object types to process
 * @param {any} [connection] - Database connection
 * @returns {Promise<any[]|undefined>} Filtered objects for exclusion/intersection, or undefined if error occurs
 */
export const getExcludeObjects= async (filter,action,sourceUID,sourceType,target,types,connection)=>
{
    try {
        return await query(`SELECT ObjectBase.Type, ObjectBase.UIDBelongsTo AS UIDBelongsTo,ObjectBase.UID,ObjectBase.Data AS ExtraData,Main.Data,MainBase.Data AS MainBaseData,
                                ObjectBase.Title,Main.Display,Main.SortName,Entry.UID AS UIDEntry, UIDV1() AS newEntry,ObjectBase.Title
                        FROM ObjectBase
                        INNER JOIN Links 
                            ON (ObjectBase.UID = Links.UID AND Links.Type IN ('member','memberA') AND ObjectBase.Type NOT IN ('filter','list','achievement'))
                        INNER JOIN Member AS Main ON (Main.UID=ObjectBase.UIDBelongsTo)
                        INNER JOIN ObjectBase  AS MainBase ON (Main.UID=MainBase.UID)
                        INNER JOIN (
                            SELECT Entry.UID, Entry.UIDBelongsTo 
                            FROM ObjectBase AS Entry 
                            INNER JOIN Links  AS ELink
                            ON (Entry.UID=ELink.UID)
                            AND ELink.UIDTarget=? AND Entry.Type='entry'
                        ) AS Entry ON (Entry.UIDBelongsTo=ObjectBase.UIDBelongsTo)
                        WHERE Links.UIDTarget=?  AND ObjectBase.Type IN (?)
                        GROUP BY ObjectBase.UID
                        `,[target,sourceUID,types],{
                            cast:['json'],
                            filter:  (
                                Object.keys(filter).length && sourceType!=='list' ?
                                     action==='exclude' ? 
                                        /** @param {any} row */ (row)=>(matchObject(row,filter)) : 
                                        /** @param {any} row */ (row)=>(!matchObject(row,filter)) :null
                            )
                            ,connection
                        })
                    }
    catch(e)
    {
        errorLoggerUpdate(e)
        return undefined
    }
}

/**
 * Reduces filtered results to unique entries by UIDBelongsTo
 * @param {any[]} filtered - Array of filtered objects
 * @returns {any[]} Reduced array with unique entries
 */
export const reduceFiltered=(filtered)=>
(
    // make unique UIDBelongsTo
   filtered.reduce((result,current)=>(result.find(/** @param {any} el */ el=>el.UIDBelongsTo.equals(current.UIDBelongsTo))
            ? result: [...result,current]),[])
)

/**
 * Creates database links for included objects
 * @param {string} sourceType - Source type ('group' or 'list')
 * @param {Buffer} UIDFilter - Filter UID
 * @param {any[]} filtered - Filtered objects to link
 * @param {Buffer} target - Target list UID
 * @param {any} execConnection - Database connection
 * @returns {Promise<void>}
 */
const linkIncludes=async (sourceType,UIDFilter,filtered,target,execConnection)=>
{
    try {
        // link the entries via dynamik link to the filter
        await query(
            `INSERT IGNORE INTO Links (UID,Type,UIDTarget)
                VALUES (?,?,?)`,
                filtered.map(o=>([o.UID,'dynamic',UIDFilter])),
                {batch:true, connection: execConnection})
        const memberType= sourceType==='group' ? 'memberA' : 'member'
        await query(
            `INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES (?,?,?)`,
            filtered.map(o=>[o.UID,memberType,target]),
            { batch:true,connection:execConnection})
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}

/**
 * Processes include filter operations
 * @param {Buffer} UIDFilter - Filter UID
 * @param {any[]} filtered - Filtered objects to include
 * @param {string} sourceType - Source type
 * @param {Buffer} target - Target list UID
 * @param {string} organization - Organization UUID for events
 * @param {FilterOptions} options - Operation options
 * @returns {Promise<void>}
 */
export const processInclude= async (UIDFilter,filtered, sourceType,target,organization,{connection,virtual})=>
{
    
    return new Promise(async (fullfill,reject)=>
    {
        try {
            let toBeAdded=[]
            // generate entry objetcs, if source is a group, if is a list, reuse the entries
            if(sourceType==='group' )
            {
                // for a group the UIDEntry  does not yet exist, if it is not already member
                toBeAdded = filtered.filter(o=>o.UIDEntry===null)
                // use generted uid's for entry  objects, if they are already praesent
                filtered.forEach(fo=>{fo.UIDEntry ? fo.UID=fo.UIDEntry : fo.UID=fo.newEntry})
            }
            else
            {
                // for a list the 
            }
            // list entries are passed through to the dlist 
            if(filtered.length)
            {
                    
                if(!virtual)
                {
                    // generate List entries for objects which have not yet an entry
                    if(toBeAdded.length>0)
                    {
                        await query(`INSERT INTO ObjectBase (UID,Type,UIDBelongsTo,Title) VALUES (?,?,?,?)`,
                                toBeAdded.map(o=>([o.UID,'entry',o.UIDBelongsTo,o.Title])),
                                {batch:true,connection}
                        )
                        // only publish events, if it is a none virtual add/remove
                        publishEvent(`/add/dlist/person/${HEX2uuid(target)}`, { organization, data: toBeAdded.map(o=>HEX2uuid(o.UIDBelongsTo)) })
                    }
                    publishEvent(`/add/dlist/entry/${HEX2uuid(target)}`, { organization, data: toBeAdded.map(o=>HEX2uuid(o.UID)) })
                    //matchExcludersList(toBeAdded,target)
                    await transaction(async (/** @type {any} */ connection)=>
                    {
                       await linkIncludes(sourceType,UIDFilter,filtered,target,connection)    
                    })
                    addUpdateList(target)
                }
                else
                {
                    // generate List entries for objects which have not yet an entry
                    if(toBeAdded.length>0)
                    {
                        await query(`INSERT INTO ObjectBase (UID,Type,UIDBelongsTo,Title) VALUES (?,?,?,?)`,
                            toBeAdded.map(o=>([o.UID,'entry',o.UIDBelongsTo,o.Title])),
                            {batch:true,connection}
                        )
                    }
                    // virtual addFilter within remove/add transaction
                    await linkIncludes(sourceType,UIDFilter,filtered,target,connection)
                }
            }
            fullfill()
        }
        catch(e)
        {
            reject(e)
            errorLoggerUpdate(e)
        }
    }) 
}

/**
 * Creates database links for excluded/intersected objects
 * @param {Buffer} UIDFilter - Filter UID
 * @param {any[]} filtered - Filtered objects to exclude/intersect
 * @param {Buffer} target - Target list UID
 * @param {any} connection - Database connection
 * @returns {Promise<void>}
 */
const linkExcludeIntersect=async (UIDFilter,filtered,target,  connection)=>
{
    try {
        // link the entries via dynamik link to the filter
        await query(
            `INSERT INTO Links (UID,Type,UIDTarget)
                VALUES (?,'dynamic',?) ON DUPLICATE KEY UPDATE Type='dynamic'`,
                filtered.map(o=>[o.UIDEntry,UIDFilter]),
                {batch:true,connection})
        await query(
            `INSERT INTO Links (UID,Type,UIDTarget) VALUES (?,'member0',?)
                ON DUPLICATE KEY UPDATE Type=VALUE(Type)`,
                filtered.map(o=>[o.UIDEntry,target]),
                {batch:true, connection})
            
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}

/**
 * Processes exclude filter operations
 * @param {Buffer} UIDFilter - Filter UID
 * @param {any[]} filtered - Filtered objects to exclude
 * @param {Buffer} target - Target list UID
 * @param {string} organization - Organization UUID for events
 * @param {FilterOptions} options - Operation options
 * @returns {Promise<void>}
 */
export const processExclude=async(UIDFilter,filtered,target,organization,{connection,virtual})=>
{
    try {
        if(!virtual)
        {
            await transaction(async(/** @type {any} */ connection)=>
            {
                linkExcludeIntersect(UIDFilter,filtered,target,connection)
            })
            publishEvent(`/remove/dlist/person/${HEX2uuid(target)}`, { organization, data: filtered.map(o=>HEX2uuid(o.UIDBelongsTo)) })
            publishEvent(`/remove/dlist/entry/${HEX2uuid(target)}`, { organization, data: filtered.map(o=>HEX2uuid(o.UID)) })
            addUpdateList(target)

        }
        else
        {
            // virtual addFilter within remove/add transaction
            await linkExcludeIntersect(UIDFilter,filtered,target,connection)
        }
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}






/**
 * Executes a dynamic list filter (include/exclude/intersect) on a target list
 * @param {Buffer|string} UIDFilter - The UID of the filter to execute
 * @param {Buffer|string} target - The UID of the target dynamic list
 * @param {FilterAction} action - The type of filter action
 * @param {string} organization - The organization UUID for multi-tenant scoping
 * @param {FilterOptions} options - Additional options for the operation
 * @returns {Promise<void>}
 */
export const execDListFilter=async (UIDFilter,target,action,organization,{connection,virtual})=>
{
      
    // executes the filter UIDFilter, taking data form its source UIDBelongsTo 
    // includes/excludes/intersects (defined by action) the filtered data of its source into the target list
    
    // Type validation
    UIDFilter = UUID2hex(UIDFilter)
    target = UUID2hex(target)
    if (typeof organization !== 'string') {
        throw new TypeError(`organization must be a string, got ${typeof organization}`);
    }
    if (!['include', 'exclude', 'intersect'].includes(action)) {
        throw new TypeError(`action must be a valid DListFilterAction, got ${action}`);
    }
    
    // UIDFilter: the UID of the filter to be acecuted
    // target: the UID of the target dlist
    // action: the type of the filter (include,exclude,intersect)
    // myConnection: a database connection. This has to be set, when you would like to execute the filter in a 
    // seperate transaction
    
    // get the data of the filter
    try {
        const filters= await getFilterData(UIDFilter,action)
        if(!filters || filters.length===0)
            return
        const [{filter,sourceUID, sourceType}]= filters
        
        if(filter)
        {
            // which type of data do we have to select?
            // for list sources it is 'entry' for other sources it is defined in the filter
            const types= getTypes(sourceType,filter)
            let filtered 
            if(action==='include')
            {
                // will contain all filtered objects of the source
                // get everything belonging to the group to be filtered ( we are storing UIDbelongsTo in the list)
                // this means, we are bypassing guest, external or job avatars and generate later an entry avatar, which is directly linked to the person
            
                filtered=await getIncludeObjects(filter,sourceUID,sourceType, target, types, connection)
            }
            if(action==='exclude' || action==='intersect')
            {
                // get all entries in the dlist for that source filtered by the filter
                
                
                filtered=await getExcludeObjects(filter,action,sourceUID,sourceType, target, types, connection)

            }
            filtered=reduceFiltered(filtered || [])
            if(filtered.length)
            {
                if(action==='include')
                {

                    await processInclude(UIDFilter,filtered, sourceType, target, organization, {connection, virtual})
                }
            
                
                if(action==='exclude' || action==='intersect')
                {
                    await processExclude(UIDFilter, filtered, target, organization, {connection,virtual})
                    

                }

            }
        }
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
 }