Source: tree/matchObjects/checkEntries.js

// @ts-check

/**
 * @typedef {Object} EntryObject
 * @property {Buffer} UID - Entry unique identifier
 * @property {Buffer} UIDBelongsTo - Parent object identifier
 * @property {Buffer} UIDList - List identifier
 * @property {Buffer} UIDEntry - Entry identifier
 * @property {string} Type - Object type
 * @property {Object} Data - Entry data
 */

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

/**
 * Helper function to ensure UUIDs are converted to strings
 * @param {string|Buffer} uuid - UUID that might be Buffer or string
 * @returns {string} - UUID as string
 */
function toStringUuid(uuid) {
    if (Buffer.isBuffer(uuid)) {
        const result = HEX2uuid(uuid);
        return typeof result === 'string' ? result : '';
    }
    return uuid || '';
}


// checkEntries is checking all entries of this object, if they are still matching the criteria for there filters
// if not it updates the dlis accordingly and sends the appropriate add maessages
// ObjectUID can be a single object UID or an array of object UIDs

/**
 * Retrieves all filters and related data for objects that need to be checked for dynamic list membership.
 * This function queries the database to get filter information for objects and their associated lists.
 *
 * @async
 * @function getObjectFilters
 * @param {Buffer|Buffer[]} ObjectUID - Single object UID or array of object UIDs to get filters for
 * @returns {Promise<Array<any>|undefined>} Array of filter objects with complete filter and object information
 */
export const  getObjectFilters=async (ObjectUID)=>
{
    try {
        let myObjectFilters
        if(Array.isArray(ObjectUID))
        {


            myObjectFilters=await query( `SELECT  DISTINCT ObjectBase.UID,ObjectBase.UIDBelongsTo, Main.Type, Member.Data, Main.Data AS ExtraData,MainBase.Data AS MainBaseData,
                                    Main.dindex,Filter.UID AS UIDFilter,Filter.Data AS FilterData,Filter.Type AS FilterType, UNIX_TIMESTAMP(Filter.validFrom) AS FilterFrom,
                                    ListLinks.UIDTarget AS UIDList, Entries.UID AS UIDEntry
                                    

                                    FROM ObjectBase  INNER JOIN ObjectBase AS Main ON (Main.UIDBelongsTo=ObjectBase.UIDBelongsTo AND Main.Type IN ('person','guest','job','extern')) 
                                        INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
                                        INNER JOIN ObjectBase  AS MainBase ON (MainBase.UID=Member.UID)
                                        INNER JOIN ObjectBase  AS Entries ON (Entries.UIDBelongsTo=ObjectBase.UIDBelongsTo AND Entries.Type='entry')
                                        INNER JOIN Links AS ListLinks ON (ListLinks.UID=Entries.UID AND ListLinks.Type IN ('member','memberA','member0'))
                                        INNER JOIN Links AS FilterLinks ON (FilterLinks.UIDTarget=ListLinks.UIDTarget AND FilterLinks.Type='list')
                                        INNER JOIN ObjectBase  AS Filter ON (Filter.UID=FilterLinks.UID AND Filter.Type IN ('include','exclude','intersect'))
                                        WHERE ObjectBase.UID IN (?) AND ObjectBase.Type IN ('person','guest','extern','job','entry')
                                        ORDER BY UIDList,Entries.UID
                                    `,[ObjectUID],{cast:['json']} )
        }
        else
        {
            myObjectFilters=await query( `SELECT  DISTINCT ObjectBase.UID,ObjectBase.UIDBelongsTo, Main.Type, Member.Data, Main.Data AS ExtraData,MainBase.Data AS MainBaseData,
                Main.dindex,Filter.UID AS UIDFilter,Filter.Data AS FilterData,Filter.Type AS FilterType, UNIX_TIMESTAMP(Filter.validFrom) AS FilterFrom,
                ListLinks.UIDTarget AS UIDList, Entries.UID AS UIDEntry
            

                FROM ObjectBase  INNER JOIN ObjectBase AS Main ON (Main.UIDBelongsTo=ObjectBase.UIDBelongsTo AND Main.Type IN ('person','guest','job','extern')) 
                    INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
                    INNER JOIN ObjectBase  AS MainBase ON (MainBase.UID=Member.UID)
                    INNER JOIN ObjectBase  AS Entries ON (Entries.UIDBelongsTo=ObjectBase.UIDBelongsTo AND Entries.Type='entry')


                    INNER JOIN Links AS ListLinks ON (ListLinks.UID=Entries.UID AND ListLinks.Type IN ('member','memberA','member0'))
                    INNER JOIN Links AS FilterLinks ON (FilterLinks.UIDTarget=ListLinks.UIDTarget AND FilterLinks.Type='list')
                    INNER JOIN ObjectBase  AS Filter ON (Filter.UID=FilterLinks.UID AND Filter.Type IN ('include','exclude','intersect'))
                    WHERE ObjectBase.UID =? AND ObjectBase.Type IN ('person','guest','extern','job','entry')
                    ORDER BY UIDList,Entries.UID
                        `,[ObjectUID],{
                            cast:['json'],
                            log:false
                        } )
        }
        return myObjectFilters
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}


/**
 * Processes a single filter entry and groups it under the appropriate entry key in the ObjectFilter.
 * This function organizes filter data by entry UID, creating or updating filter collections
 * for include, exclude, and intersect filters, along with membership information.
 *
 * @async
 * @function processGroupEntry
 * @param {Record<string, any>} ObjectFilter - The object to store grouped filter data, keyed by entry UID
 * @param {any} of - Filter object containing entry data, filter information, and list details
 * @throws Will log an error if any operation fails during execution.
 */
export  const processGroupEntry=async(ObjectFilter,of)=>
{
    try {
  
        // UID key for this entry (we convert it into base64 from buffer for easier comparison)
        const key=of.UIDEntry.toString('base64')
        // store the relevant data for the later objetc filtereing over all filters
        const filtering={
                Data:  {
                    Data: of.Data,
                    ExtraData :of.ExtraData,
                    MainBaseData: of.MainBaseData,
                    Type: of.Type
                },
                Filter: of.FilterData,
                UIDFilter: of.UIDFilter,
                timestamp: of.FilterFrom
        } 
        // if not yet have an UID entry in the ObjectFilter, we are creating a new one    
        if(!ObjectFilter[key])
        {
            // retrieve the membership links for this entry
            const mLinks= await query(`SELECT Links.Type FROM Links WHERE UID=? AND Links.UIDTarget=? AND Links.Type IN ('member','memberA','member0')`,
                [of.UIDEntry,of.UIDList])
            // form the object to be added to ObjectFilter
            /** @type {any} */
            const newUID = {
                Filter: {include:[], exclude:[], intersect:[]},
                UIDList: of.UIDList,
                MemberType: mLinks.some(/** @param {any} l */ l=>l.Type==='member0') ? 'member0': mLinks.find(/** @param {any} l */ l=>l.Type==='member' || l.Type==='memberA').Type ,
                UIDEntry: of.UIDEntry,
                UIDBelongsTo: of.UIDBelongsTo,
                StillMember: mLinks.some(/** @param {any} l */ l=>l.Type==='member' || l.Type==='memberA')
            }
            // save this filter in the right filter category
            newUID.Filter[of.FilterType].push(filtering)
            // add the newly created oblect under the entry key in the ObjectFilter object
            ObjectFilter[key]=newUID

        }
        else
        {
            // just push the filter in the right category array
            /** @type {any} */ (ObjectFilter[key]).Filter[of.FilterType].push(filtering)
        
            
        }
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
        
}

/**
 * Groups filter data by entry UID and organizes filters into include/exclude/intersect categories.
 * This function processes raw filter data from the database and structures it for efficient
 * filtering operations, creating an object keyed by entry UIDs with complete filter information.
 *
 * @async
 * @function groupEntriesFilter
 * @param {Array<any>} myObjectFilters - Array of raw filter objects from the database query
 * @returns {Promise<Record<string, any>|undefined>} Object keyed by entry UIDs containing organized filter data
 * @throws Will log an error if any operation fails during execution.
 */
export const groupEntriesFilter=async (myObjectFilters)=>
{
    // forms from the retrieved lines an object with the entry UID as primary key
    // and stores in it the relevant data to be filtered and all filters with there list for this entry
    // this can be later easy processed
    try {
        const ObjectFilter={}
        // group by entries.
        for (const of of myObjectFilters)
            await processGroupEntry(ObjectFilter,of)
        return ObjectFilter  
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}


/**
 * Removes the exclusion link for an entry and publishes add events to notify about the re-inclusion.
 * This function is called when an entry that was previously excluded from a list should now be included.
 *
 * @async
 * @function unExclude
 * @param {any} entry - Entry object containing UIDEntry, UIDList, UIDBelongsTo
 * @param {string} UIDOrga - The unique identifier of the organization (required)
 * @throws Will log an error if any operation fails during execution.
 */
export const unExclude=async (entry, UIDOrga)=>
{
    try {
        // we have to 'un-exclude'
        await query(`DELETE FROM Links WHERE UID=? AND UIDTarget=? AND Type ='member0'`,[entry.UIDEntry,entry.UIDList])
                
        // we have to publish, that we have it again here (only, if it was not there before)
        // Ensure UIDOrga is string for publishEvent


        const listId = HEX2uuid(entry.UIDList);
        const belongsToId = HEX2uuid(entry.UIDBelongsTo);
        const entryId = HEX2uuid(entry.UIDEntry);

        /** @type {any} */ (publishEvent)(`/add/dlist/person/${listId}`, { organization: UIDOrga, data: [belongsToId] })
        /** @type {any} */ (publishEvent)(`/add/dlist/entry/${listId}`, { organization: UIDOrga, data: [entryId] })
        addUpdateList(entry.UIDList)
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}


/**
 * Creates an exclusion link for an entry and publishes remove events to notify about the exclusion.
 * This function is called when an entry should be excluded from a list due to filter matching.
 *
 * @async
 * @function exclude
 * @param {any} entry - Entry object containing UIDEntry, UIDList, UIDBelongsTo
 * @param {string} UIDOrga - The unique identifier of the organization (required)
 * @throws Will log an error if any operation fails during execution.
 */
export const exclude=async(entry, UIDOrga)=>
{
    try {
        // we have to 'exclude'
        await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'member0',?)`,
            [entry.UIDEntry,entry.UIDList],
        )
        // we have to publish, that we have  removed it
        // Ensure UIDOrga is string for publishEvent
  
    

        const listId = HEX2uuid(entry.UIDList);
        const belongsToId = HEX2uuid(entry.UIDBelongsTo);
        const entryId = HEX2uuid(entry.UIDEntry);

        /** @type {any} */ (publishEvent)(`/remove/dlist/person/${listId}`, { organization: UIDOrga, data: [belongsToId] })
        /** @type {any} */ (publishEvent)(`/remove/dlist/entry/${listId}`, { organization: UIDOrga, data: [entryId] })
        addUpdateList(entry.UIDList)
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }

}

/**
 * Checks a single entry against its filters and performs necessary add/remove operations.
 * This function evaluates whether an entry should be included or excluded from a list based on
 * the current filter state and publishes appropriate events for membership changes.
 *
 * @async
 * @function checkEntry
 * @param {any} entry - Entry object with filter information and membership data
 * @param {string} UIDOrga - The unique identifier of the organization (required)
 * @throws Will log an error if any operation fails during execution.
 */
export const checkEntry= async (entry, UIDOrga)=>
{
    try {
        let remains=false
        // we are first checking, if the object ist still matching at least one of the include filters of the dlist
        if(entry.StillMember!==null)
        {
            for(const Filter of entry.Filter.include)
            {
                if(matchObject(Filter.Data,Filter.Filter))
                {
                    remains=true
                    break
                }
            }
        }
        if(!remains)
        {
            // if it is no more includes, we remove the entry and the relevant links
            await transaction(/** @param {any} connection */ (connection)=>
            {
                connection.query(`DELETE ObjectBase,Links FROM ObjectBase INNER JOIN Links ON (Links.UID=ObjectBase.UID) 
                    WHERE ObjectBase.UID=?`,
                    [entry.UIDEntry])
            },
            {
                beforeTransaction: /** @type {function(any): Promise<any>} */ async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
            })

            // we are publishing the removal as well if it was member (did not have a member0 link)
            if(entry.MemberType!=='member0')
            {
                // Ensure UIDOrga is string for publishEvent


                const listId = HEX2uuid(entry.UIDList);
                const belongsToId = HEX2uuid(entry.UIDBelongsTo);
                const entryUid = HEX2uuid(entry.UID);

                /** @type {any} */ (publishEvent)(`/remove/dlist/person/${listId}`, { organization: UIDOrga, data: [belongsToId] })
                /** @type {any} */ (publishEvent)(`/remove/dlist/entry/${listId}`, { organization: UIDOrga, data: [entryUid] })
                // and update the list in the UID
                addUpdateList(entry.UIDList)
            }

            
        }
        if(remains)
        {
            // if the object is still included, we have to check the includers and the excluders
            let excludes=false
            // check if the excluders are still valid
            for(const Filter of entry.Filter.exclude)
            {
                if(Filter.Data && matchObject(Filter.Data,Filter.Filter))
                {  
                    excludes=true
                    // make sure, that we have the dynamic link for this entry to the exclude filter
                    await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'dynamic',?)`,[Filter.UIDFilter,entry.UIDEntry])
                    break
                }
            }
            for(const Filter of entry.Filter.intersect)
            {
                if(Filter.Data && !matchObject(Filter.Data,Filter.Filter))
                {
                    excludes=true
                    // make sure, that we have the dynamic link for this ebtry to the exclude filter
                    await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'dynamic',?)`,[Filter.UIDFilter,entry.UIDEntry])
                    break
                }
            }
            // if it is no more excluded and still masked by a member0 link, we have to unexclude
            if(excludes===false && entry.MemberType==='member0')
            {
                await unExclude(entry, UIDOrga)
            }
            else if((entry.Filter.exclude.length>0 || entry.Filter.intersect.length>0) && entry.MemberType!=='member0' && excludes)
            {
                await exclude(entry, UIDOrga)
            }
        }
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}


/**
 * Checks all entries for the given objects against their filters and performs necessary cleanup.
 * This function processes multiple objects, retrieves their filters, and checks each entry
 * to ensure they still match the current filter criteria, removing entries that no longer qualify.
 *
 * @async
 * @function checkEntries
 * @param {Buffer|Buffer[]} ObjectUID - Single object UID or array of object UIDs to check
 * @param {string} UIDOrga - The organization UUID for multi-tenant scoping (must be string)
 * @returns {Promise<void>}
 * @throws Will log an error if any operation fails during execution.
 */
export const checkEntries=async (ObjectUID, UIDOrga)=>
{
    
    try {
        // Type validation
        if (typeof UIDOrga !== 'string') {
            throw new TypeError(`UIDOrga must be a string, got ${typeof UIDOrga}`);
        }

        // Check if ObjectUID is a Buffer or an array of Buffers
        if (!Buffer.isBuffer(ObjectUID) && !Array.isArray(ObjectUID)) {
            throw new TypeError(`ObjectUID must be a Buffer or an array of Buffers, got ${typeof ObjectUID}`);
        }

        // check all filters for the entries of the supplied objects. 
        // if they are not longer valid, remove there entries from the list
        // get Object Data and Filters
        const myObjectFilters=await getObjectFilters(ObjectUID)
        
        // Add null safety check
        if (!myObjectFilters) {
            return;
        }

        const ObjectFilter= await groupEntriesFilter(myObjectFilters)   
    
        // Add null safety check for ObjectFilter
        if (!ObjectFilter) {
            return;
        }
    
        // now we go through all entries and check, if the filters are keeping the objects in the list
        for (const [uid,entry] of Object.entries(ObjectFilter))
        {
        await checkEntry(entry, UIDOrga)
        }
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}