Source: tree/executeFilters/executeFilters.js

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





import {query,transaction, HEX2uuid, getConnection, UUID2hex} from '@commtool/sql-query'
import {addUpdateList } from '../../server.ws.js'
import { filterObjects, matchObject} from '../../utils/objectfilter/filters/index.js';
import { publishEvent } from '../../utils/events.js';
import { execDListFilter } from './dlistFilter.js';
import { errorLoggerUpdate } from '../../utils/requestLogger.js';

// excute an includer filter

//UIDFilter: (the UID of the filter)
// target: UID of the target list, we have to add the filtered objects to

/**
 * Adds a filter to a target and executes the filtering logic
 * @param {Buffer|String} UIDFilter - The UID of the filter object
 * @param {Buffer|String} target - The UID of the target list/object
 * @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 addFilter=async  (UIDFilter,target,action, organization, options={virtual:false, connection: null})=>
{
    try {
        // Type validation
        UIDFilter=UUID2hex(UIDFilter)
        target=UUID2hex(target)
        if (typeof organization !== 'string') {
            throw new TypeError(`organization must be a string, got ${typeof organization}`);
        }
        if (!['visible', 'changeable', 'include', 'exclude', 'intersect'].includes(action)) {
            throw new TypeError(`action must be a valid FilterAction, got ${action}`);
        }
        
        // get filter Type
        // make the Filter linked to the target
        const {connection, virtual}= options
        if(!virtual)
        {
            // virtual is supplied, this is ignored as we assume, that this is a virtual add after a removal
            await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) 
                VALUES(?,'list',?)`,[UIDFilter,target],
            )
            
        }
        if(action==='visible' || action==='changeable')
        {
            // get the target type
            const types=await query(`SELECT Type FROM ObjectBase WHERE UID=? `,[target])
            if(types && types.length>0)
            {
                const Type=types[0].Type

                if(Type==='job' || Type==='eventJob')
                {
                    if(action==='visible')
                        await includeVisibility(UIDFilter,target,connection)
                    else
                        await includeChangeability(UIDFilter, target,connection)

                }
                else if(['list','dlist','email','location','event', 'eventT'].includes(Type))
                    await visibilityList(UIDFilter,target,connection,organization)
            }
            
        }
        else
        {
            await execDListFilter(UIDFilter,target,action,organization,{connection,virtual})
        }
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}



/**
 * Visibility filter function
 * @param {any} actFilter - The filter object
 * @param {Buffer} sourceUID - The source UID
 * @param {any} [connection=null] - Database connection
 * @returns {Promise<any[]|undefined>} Filtered results
 */
export const visibilityFilter =async (actFilter,sourceUID,connection=null)=>
{
    try {
        let filtered
        //if(!connection)
            //console.log(HEX2uuid(sourceUID))
        const all=await query(`
            (
                SELECT ObjectBase.Type, ObjectBase.UID AS UID, ObjectBase.UIDBelongsTo AS UIDBelongsTo,ObjectBase.Data AS ExtraData,Main.Data,
                ObjectBase.Title,Main.Display,Main.SortName
                FROM ObjectBase
                INNER JOIN Links
                    ON (ObjectBase.UID = Links.UID AND Links.Type IN ('member','memberA','memberS','memberSys') AND ObjectBase.Type NOT IN ('filter','list','dlist','email','event','location','eventT'))
                INNER JOIN Member AS Main ON (Main.UID=ObjectBase.UIDBelongsTo)
                WHERE (Links.UIDTarget=? )  AND ObjectBase.Type IN('person','extern','job','guest','group','event')
                GROUP BY ObjectBase.UID
            ) 
            UNION
            (
                SELECT 'person' AS Type, ObjectBase.UID AS UID, ObjectBase.UIDBelongsTo AS UIDBelongsTo,ObjectBase.Data AS ExtraData,Main.Data,
                ObjectBase.Title,Main.Display,Main.SortName
                FROM ObjectBase
                INNER JOIN Links
                    ON (ObjectBase.UID = Links.UID AND Links.Type IN ('member','memberA') AND ObjectBase.Type NOT IN ('filter','list','dlist','emeil','event','location','eventT'))
                INNER JOIN Member AS Main ON (Main.UID=ObjectBase.UIDBelongsTo)
                WHERE (Links.UIDTarget=? )  AND ObjectBase.Type ='entry'
                GROUP BY ObjectBase.UID

            ) 
            UNION
            (
                SELECT 'group' AS Type, ObjectBase.UID AS UID, ObjectBase.UIDBelongsTo AS UIDBelongsTo,ObjectBase.Data AS ExtraData,Main.Data,
                ObjectBase.Title,Main.Display,Main.SortName
                FROM ObjectBase
                INNER JOIN Member AS Main ON (Main.UID=ObjectBase.UID)
                WHERE ObjectBase.UID=?
    
            )
        

        
            `,[sourceUID,sourceUID,sourceUID,sourceUID,sourceUID,sourceUID],{connection})

        filtered=all
        if(Object.keys(actFilter).length)
        {
            filtered=all.map(o=>{
                try
                {
                    return {...o,Data:JSON.parse(o.Data),ExtraData:JSON.parse(o.ExtraData)}
                }
                catch(e)
                {
                    //console.log(e,o)
                }

            })
            filtered=filterObjects(filtered,actFilter)
        } 
        return filtered
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}


/**
 * Include visibility filter
 * @param {Buffer} UIDFilter - Filter UID
 * @param {Buffer} target - Target UID
 * @param {any} [connection=null] - Database connection
 * @returns {Promise<void>}
 */
export const includeVisibility=async (UIDFilter,target,connection=null)=>
{
 
    try
    {
        //const myConnection=connection ? connection : await getConnection()
        
        const filters=await query (`SELECT Data AS filterJSON, UIDBelongsTo AS sourceUID
            FROM ObjectBase WHERE UID=? AND Type='visible'`,[UIDFilter],{cast:['json'],connection})

        if(filters.length>0)
        {
            const [{filterJSON,sourceUID}]=filters
            const filtered=await visibilityFilter(filterJSON,sourceUID,connection)
            if(filtered && filtered.length)
            {
                // visibilty has to be added to the person, not to the job
                const result = await query(`SELECT UIDBelongsTo AS user FROM Objects WHERE UID=?`,[target],{connection})
                if(result.length>0)
                {
                    const [{user}]=result
                        //console.log('includeVisibility',HEX2uuid(user))
                    await query(`INSERT INTO Visible (UID,Type,UIDUser) VALUES (?,'visible',?)
                        ON DUPLICATE KEY UPDATE UID=VALUE(UID)`, filtered.map(/** @param {any} o */ o=>([o.UID,user])),
                        {batch:true, connection}) 
                

                    // update the clients
                    addUpdateList(filtered.filter(/** @param {any} o */ o=>(o.Type==='group')).map(/** @param {any} el */ el=>el.UID))
                }
                   
            }
           
        }
    
    }
     catch(e)
    {
        console.log(e)
    }
}

/**
 * Include changeability filter
 * @param {Buffer} UIDFilter - Filter UID
 * @param {Buffer} target - Target UID
 * @param {any} [connection=null] - Database connection
 * @returns {Promise<void>}
 */
export const includeChangeability=async (UIDFilter,target, connection=null)=>
{
 
    try
    {
        //const myConnection=connection ? connection : await getConnection()

        const filters= await query (`SELECT Data AS filterJSON, UIDBelongsTo AS sourceUID
            FROM ObjectBase WHERE UID=? AND Type='changeable'`,[UIDFilter],{cast:['json'],connection})
        if(filters.length>0)
        {
            const [{filterJSON,sourceUID}]=filters

            const filtered=await visibilityFilter(filterJSON,sourceUID,connection)
            if(filtered && filtered.length)
            {
                // visibilty has to be added to the person, not to the job, so we get the UIDBelongsTo
                const [{user}]= await query(`SELECT UIDBelongsTo AS user FROM Objects WHERE UID=?`,[target],{connection})
                //console.log('includeVisibility',HEX2uuid(user))
                
                    await query(`INSERT INTO Visible (UID,Type,UIDUser) VALUES (?,'changeable',?)
                        ON DUPLICATE KEY UPDATE Type='changeable'`,
                        filtered.map(/** @param {any} o */ o=>([o.UID,user])), 
                        {batch:true,connection}
                    )
              

            }
            

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

/**
 * Visibility list filter
 * @param {Buffer} UIDFilter - Filter UID
 * @param {Buffer} target - Target UID
 * @param {any} [connection=null] - Database connection
 * @param {string} [organization='UUID-00000000-0000-0000-0000-000000000000'] - Organization UUID for events
 * @returns {Promise<void>}
 */
export const visibilityList=async (UIDFilter,target,connection=null,organization='UUID-00000000-0000-0000-0000-000000000000')=>
{
    try
    {
        //const myConnection=connection ? connection : await getConnection()

        const filters=await query (`SELECT Data AS filterJSON, UIDBelongsTo AS sourceUID, Type
            FROM ObjectBase WHERE UID=? AND Type IN('visible','changeable')`,[UIDFilter])
        const tresult=await query(`SELECT Type  FROM ObjectBase WHERE UID=?`,[target])
        const targetType=tresult[0].Type
        if(filters.length>0)
        {
            const [{filterJSON,sourceUID,Type}]= filters
            if(filterJSON)
            {
                const filterData= JSON.parse(filterJSON)
                const filtered=await visibilityFilter(filterData,sourceUID)
                const myType = Type==='visible' ? 'visible' : filterData.admin ? 'admin' : 'changeable'
                if(filtered && filtered.length)
                {
                    const paras=filtered.map(/** @param {any} o */ o=>([target,myType,o.UIDBelongsTo]))
                    await query(`INSERT INTO Visible (UID,Type,UIDUser) VALUES (?,?,?)   
                            ON DUPLICATE KEY UPDATE 
                                Type=IF(VALUE(Type)='admin','admin',IF(Type<>'admin' 
                                AND VALUE(Type)='changeable','changeable',Type))`,
                            paras,
                            {batch:true, connection: connection}) 
                    
                    const myKey=`/add/${targetType}/${myType}/${HEX2uuid(target)}`
                    publishEvent(myKey, { organization, data: filtered.map(/** @param {any} o */ o=>HEX2uuid(o.UIDBelongsTo)) })
                }
            }
        }
        addUpdateList(target)
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}

/**
 * Delete links for filters
 * @param {Object} params - Parameters object
 * @param {any[]} params.lists - List of objects
 * @param {Buffer} params.UIDFilter - Filter UID
 * @param {Buffer} params.target - Target UID
 * @param {any} connection - Database connection
 * @returns {Promise<void>}
 */
export  const deleteLinks= async ({lists,UIDFilter,target},connection)=>
{
    try {
        if(lists.length>0 && lists[0].Type==='include')
        {
            await query(`DELETE Links FROM Links
                    LEFT JOIN (
                        Links AS lViolettOrange 
                        INNER JOIN ObjectBase AS Filter ON (Filter.UID = lViolettOrange.UIDTarget AND Filter.Type ='include')
                        INNER JOIN Links AS lBlue ON (Filter.UID=lBlue.UID AND lBlue.UIDTarget=?)
                    )
                    ON (Links.UID=lViolettOrange.UID AND lViolettOrange.Type='dynamic' AND lViolettOrange.UIDTarget<> ? ) 
                    WHERE  lViolettOrange.UIDTarget IS NULL AND Links.Type IN ('memberA','member') AND Links.UIDTarget=? `
                ,[target,UIDFilter,target],
                {connection}
            )
        }
        if(lists.length>0 && lists[0].Type!=='include')
        {
            await query(`DELETE Links FROM Links
                    LEFT JOIN (
                        Links AS lViolettOrange 
                        INNER JOIN ObjectBase AS Filter ON (Filter.UID = lViolettOrange.UIDTarget AND Filter.Type =?)
                        INNER JOIN Links AS lBlue ON (Filter.UID=lBlue.UID AND lBlue.UIDTarget=?)
                    )
                    ON (Links.UID=lViolettOrange.UID AND lViolettOrange.Type='dynamic' AND lViolettOrange.UIDTarget<> ? ) 
                    WHERE  lViolettOrange.UIDTarget IS NULL AND Links.Type ='member0' AND Links.UIDTarget=? `,
                [lists[0].Type,target,UIDFilter,target],
                {connection}
            )
        }
    
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }

}

/**
 * Delete entries function
 * @param {any[]} remove - Array of entries to remove
 * @param {any} connection - Database connection
 * @returns {Promise<void>}
 */
export  const deleteEntries=async (remove,connection)=>
{
    try {
        await query(`DELETE Entries,DelLinks FROM ObjectBase AS Entries 
                LEFT JOIN Links AS DelLinks ON (DelLinks.UID=Entries.UID) 
                WHERE Entries.Type ='entry' AND Entries.UID IN (?)
        `,[remove.map(/** @param {any} el */ el=>el.UID)],
        {connection})
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}

/**
 * Removes a filter from a target and cleans up associated entries
 * @param {string|Buffer} UIDFilter - The UID of the filter to remove
 * @param {string|Buffer} target - The UID of the target list/object
 * @param {string} organization - The organization UUID for multi-tenant scoping
 * @param {FilterOptions} [options={}] - Additional options for the operation
 * @returns {Promise<void>}
 */
export const removeFilter=async  (UIDFilter,target,organization,options={connection:null,virtual: false})=>
{
    // this is called, when a filter is being removed 
    // get the type of filter
    try {
        // Type validation
        UIDFilter=UUID2hex(UIDFilter)
        target=UUID2hex(target)
        if (typeof organization !== 'string') {
            throw new TypeError(`organization must be a string, got ${typeof organization}`);
        }
        
        const {connection, virtual}= options

        const lists=await query (`SELECT ObjectBase.Type FROM ObjectBase  WHERE UID=?`,[UIDFilter],{connection})

    
        await deleteLinks({lists,UIDFilter,target},connection)
    
    
        // collect the entries without member or memberA links
        const remove= await query(`
            SELECT Entries.UID, Entries.UIDBelongsTo
            FROM ObjectBase  AS Entries
            INNER JOIN Links  AS DynamicLinks ON (DynamicLinks.UID=Entries.UID AND DynamicLinks.Type='dynamic' )
            INNER JOIN ObjectBase  AS Filter ON (Filter.UID=DynamicLinks.UIDTarget)
            INNER JOIN Links  AS ListLink ON (ListLink.UID=DynamicLinks.UIDTarget)
            LEFT JOIN Links  ON (Links.UID=Entries.UID AND Links.Type IN ('member','memberA')) 
            WHERE Links.UID IS NULL AND  ListLink.UIDTarget=?
        `,[target],{connection, log: false}
        )
    


        // delete the entries without dynamic links and there membership links
        if(remove.length>0)
        {
            if(!connection)
            {
                await transaction(async (/** @type {any} */ connection)=>
                {
                    await deleteEntries(remove,connection)
                    // delete the dynamic Links belonging to the filter
                    await query(`DELETE FROM Links WHERE UIDTarget=? AND Links.Type='dynamic'`,[UIDFilter], {connection})
                    // publish removal
                    publishEvent(`/remove/dlist/person/${HEX2uuid(target)}`, { organization, data: remove.map(/** @param {any} o */ o=>HEX2uuid(o.UIDBelongsTo)) })
                })

            }
            else
            {
                
                await deleteEntries(remove,connection)
                if(!virtual)
                {
                    await query(`DELETE FROM Links WHERE UIDTarget=? AND Links.Type='dynamic'`,[UIDFilter],{ connection})
                    // publish removal
                    publishEvent(`/remove/dlist/person/${HEX2uuid(target)}`, { organization, data: remove.map(o=>HEX2uuid(o.UIDBelongsTo)) })

                }
            }

        }
        if(!virtual)
        {
            // virtual is supplied, this is ignored as we assume, that this is a virtual add after a removal
            await query(`DELETE FROM Links WHERE Type='List' AND UID=?  AND UIDTarget=? `,[UIDFilter,target],{connection})
            
        }
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}