Source: tree/rebuildList.js

// @ts-check
/**
 * @import {PublishEventOptions} from '../utils/events.js'
 */

import {query,transaction, HEX2uuid} from '@commtool/sql-query'

import { includeChangeability, includeVisibility, visibilityList, visibilityFilter } from './executeFilters/executeFilters.js'
import { matchObject } from '@commtool/object-filter'

import { addVisibility } from './matchObjects/matchObjects.js'
import { publishEvent } from '../utils/events.js'
import { errorLoggerUpdate } from '../utils/requestLogger.js'


export const personRebuildAccess=async (UID)=>
{
    try {
        
        // rebuild  all visibilities for none list/dlist/email/location the user with this UID has
        // delete all existing links for the user UID
        await transaction(async(connection)=>
        {
            //console.log('delete person visibility', HEX2uuid(UID))
        
            
            await connection.query(`DELETE Visible FROM Visible INNER JOIN ObjectBase 
                        ON (ObjectBase.UID=Visible.UID AND ObjectBase.Type NOT IN ('list','dlist','email','event','eventT','location')) 
                    
                        WHERE Visible.UIDUser=? AND Visible.Accounting IS NULL`,[UID])

            // now build access for all functions of the person
            // get all job filters and execute them
                const jobsFilter=await connection.query(`SELECT filter.UID AS UIDFilter, filter.Type, Jobs.UID AS UIDJob
                    FROM ObjectBase AS filter INNER JOIN Links ON (Links.UID=filter.UID AND Links.Type='list') 
                    INNER JOIN ObjectBase AS Jobs ON (Jobs.UID=Links.UIDTarget AND Jobs.Type='job') 
                    WHERE filter.Type IN ('visible','changeable') AND Jobs.UIDBelongsTo=? 
                    GROUP BY filter.UID`,[UID])
            if(jobsFilter.length)
            {
                for (const filter of jobsFilter)
                {
                    if(filter.Type==='visible')
                        await includeVisibility(filter.UIDFilter,filter.UIDJob,connection.connection)
                    else if(filter.Type==='changeable')
                        await includeChangeability(filter.UIDFilter,filter.UIDJob,connection.connection)
                }
            }
            // enable user to edit himself
            await connection.query(`INSERT INTO Visible (UID,Type,UIDUser) VALUES(?,'changeable',?) ON DUPLICATE KEY UPDATE Type='changeable'`,[UID,UID])
        
            

        },
        {
            beforeTransaction:async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
        })
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }


}


/**
 * Rebuilds accounting visibility (Visible.Accounting) for a person based on
 * their accountingVisible/accountingChangeable filter objects (rule-based access).
 *
 * Evaluates the filter rules and sets Visible.Accounting entries. Respects
 * hierarchy: admin > changeable > visible — never overwrites a higher level.
 *
 * This is a separate step from personRebuildAccess and should only be called
 * when accounting rules change (financialMaster toggle or accountingAccess/
 * accountingWriteAccess changes), NOT on every visibility rebuild.
 *
 * @param {Buffer|string} UID - hex or UUID of the person
 */
export const personRebuildAccountingAccess = async (UID) => {
    try {
        // Clear rule-based Accounting (keep 'admin' from financialMaster)
        await query(`UPDATE Visible SET Accounting=NULL
            WHERE Visible.UIDUser=? AND Accounting IN ('visible','changeable')`,[UID])

        const accountingFilters = await query(`SELECT filter.UID AS UIDFilter, filter.Type, Jobs.UID AS UIDJob
            FROM ObjectBase AS filter INNER JOIN Links ON (Links.UID=filter.UID AND Links.Type='list')
            INNER JOIN ObjectBase AS Jobs ON (Jobs.UID=Links.UIDTarget AND Jobs.Type='job')
            WHERE filter.Type IN ('accountingVisible','accountingChangeable') AND Jobs.UIDBelongsTo=?
            GROUP BY filter.UID`, [UID])

        for (const filter of accountingFilters) {
            const accountingLevel = filter.Type === 'accountingVisible' ? 'visible' : 'changeable'
            const [filterData] = await query(`SELECT Data AS filterJSON, UIDBelongsTo AS sourceUID
                FROM ObjectBase WHERE UID=?`, [filter.UIDFilter], {cast: ['json']})
            if (!filterData) continue

            const filtered = await visibilityFilter(filterData.filterJSON, filterData.sourceUID)
            if (filtered && filtered.length) {
                const [{user}] = await query(`SELECT UIDBelongsTo AS user FROM Objects WHERE UID=?`, [filter.UIDJob])
                await query(`INSERT INTO Visible (UID, Type, Accounting, UIDUser) VALUES (?,'visible',?,?)
                    ON DUPLICATE KEY UPDATE Accounting =
                        CASE
                            WHEN VALUES(Accounting) = 'admin' THEN 'admin'
                            WHEN Accounting = 'admin' THEN Accounting
                            WHEN VALUES(Accounting) = 'changeable' THEN 'changeable'
                            WHEN Accounting = 'changeable' THEN Accounting
                            ELSE VALUES(Accounting)
                        END`,
                    filtered.map(o => ([o.UID, accountingLevel, user])),
                    {batch: true})
            }
        }
    }
    catch(e) {
        errorLoggerUpdate(e)
    }
}



export const objectRebuildAccess=async (UIDobject,targets)=>
{
    try {
        // rebuild  all visibility links for an Object when it is removed from targets
        
    /* console.log('delete object visibility', HEX2uuid(UIDobject),targets.map(u=>HEX2uuid(u)))
        await query(`DELETE Visible FROM Visible 
            INNER JOIN ObjectBase AS Jobs ON (Jobs.UIDBelongsTo=Visible.UIDUser AND Jobs.Type='job' ) 
            INNER JOIN Links ON (Links.UID=Jobs.UID AND Links.Type IN ('member','memberA'))
            WHERE Visible.UID=? AND Links.UIDTarget IN (?)`,[UIDobject,targets])*/
        // now build access for all functions of the persons, which are not part of the target list
        // get all job filters which are still belonging to the object and execute them
        // rebuild access for actual targets its belonging to
        const sources= await query(`SELECT UID FROM Links WHERE UIDTarget=? AND Type IN ('member','memberA')`,[UIDobject])
        if(sources.length)
        await addVisibility(UIDobject,sources.map(s=>s.UID),{andRemove:true})
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }


}

export const listRebuildAccess=async (UIDtarget, UIDOrga)=>
{
    try {
        // rebuild  all visibility links for a specfic list,dlist,emeil,event,eventT,location supllied in UIDtarget
        // delete all existing links for the list
        const [{targetType}]=await query(`SELECT Type AS targetType FROM ObjectBase WHERE UID=? `,[UIDtarget])
        
        // Ensure UIDOrga is in hex format for publishEvent
        if (Buffer.isBuffer(UIDOrga)) {
            UIDOrga = HEX2uuid(UIDOrga);
        }

        const oldVisibility=await query(`SELECT Visible.UIDUser,Visible.Type 
            FROM Visible
            WHERE Visible.UID=?`,[UIDtarget])
        await transaction(async (connection)=>
        {
            await connection.query(`DELETE Visible FROM Visible WHERE UID=?`,[UIDtarget])
            const objectFilter=await query(`SELECT filter.UID AS UIDFilter,filter.Data AS Data, filter.UIDBelongsTo AS UIDSource
                    FROM ObjectBase AS filter INNER JOIN Links ON (Links.UID=filter.UID AND Links.Type IN ('list','dlist','email','event','eventT','location')) 
                    WHERE filter.Type IN ('visible','changeable') AND Links.UIDTarget=? `,[UIDtarget])
            if(objectFilter.length)
            {
                for (const filter of objectFilter)
                {
                    await visibilityList(filter.UIDFilter,UIDtarget,connection)
                }
            }
            // now add the owner of the list to the visibility
            await connection.query(`INSERT INTO Visible (UID,Type,UIDUser) 
                SELECT  Links.UID,'admin',Links.UIDTarget 
                    FROM Links 
                    INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UIDTarget AND ObjectBase.Type='person') 
                    WHERE Links.Type ='member' AND Links.UID=?
                    ON DUPLICATE KEY UPDATE  Type='admin'`,[UIDtarget])
            // always add the organization superadmin (memberSys extern) so it survives filter replacements
            await connection.query(`INSERT INTO Visible (UID,Type,UIDUser)
                SELECT ?, 'admin', Links.UID
                FROM Links
                INNER JOIN ObjectBase AS Admin ON (Admin.UID=Links.UID AND Admin.Type='extern')
                WHERE Links.Type='memberSys' AND Links.UIDTarget=U_UUID2BIN(?)
                ON DUPLICATE KEY UPDATE Type='admin'`,[UIDtarget, UIDOrga])
        },
        {
            beforeTransaction:async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
        })

        const newVisibility=await query(`SELECT Visible.UIDUser,Visible.Type 
            FROM Visible
            WHERE Visible.UID=?`,[UIDtarget])

        const remove=oldVisibility.filter(oldEl=>!newVisibility.find(el=>(oldEl.UIDUser.equals(el.UIDUser) && oldEl.Type===el.Type)))

        if(remove.length>0)
        {
            const admin=remove.filter(el=>el.Type==='admin')
            if(admin.length>0)
                publishEvent(`/remove/${targetType}/admin/${HEX2uuid(UIDtarget)}`, { organization: UIDOrga, data: admin.map(el=>HEX2uuid(el.UIDUser)) })
            const changeable=remove.filter(el=>el.Type==='changeable')
            if(changeable.length>0)
                publishEvent(`/remove/${targetType}/changeable/${HEX2uuid(UIDtarget)}`, { organization: UIDOrga, data: changeable.map(el=>HEX2uuid(el.UIDUser)) })
            const visible=remove.filter(el=>el.Type==='visible')
            if(visible.length>0)
                publishEvent(`/remove/${targetType}/visible/${HEX2uuid(UIDtarget)}`, { organization: UIDOrga, data: visible.map(el=>HEX2uuid(el.UIDUser)) })
        }
        const add= newVisibility.filter(newEl=>!oldVisibility.find(el=>(newEl.UIDUser.equals(el.UIDUser) && newEl.Type===el.Type)))
        if(add.length>0)
        {
            const admin=add.filter(el=>el.Type==='admin')
            if(admin.length>0)
                publishEvent(`/add/${targetType}/admin/${HEX2uuid(UIDtarget)}`, { organization: UIDOrga, data: admin.map(el=>HEX2uuid(el.UIDUser)) })
            const changeable=add.filter(el=>el.Type==='changeable')
            if(changeable.length>0)
                publishEvent(`/add/${targetType}/changeable/${HEX2uuid(UIDtarget)}`, { organization: UIDOrga, data: changeable.map(el=>HEX2uuid(el.UIDUser)) })
            const visible=add.filter(el=>el.Type==='visible')
            if(visible.length>0)
                publishEvent(`/add/${targetType}/visible/${HEX2uuid(UIDtarget)}`, { organization: UIDOrga, data: visible.map(el=>HEX2uuid(el.UIDUser)) })
        }
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }


    

}


export const personListRebuildAccess=  async (UID, UIDOrga)=>
{
    // rebuild  all visibilities for  list/email/dlist/event/eventT the user with this UID has
    // delete all exsiting links for the user UID
    try {
        // Ensure UIDOrga is in hex format for publishEvent
        if (Buffer.isBuffer(UIDOrga)) {
            UIDOrga = HEX2uuid(UIDOrga);
        }
        /** @type {any[]} */
        let oldVisibility = []
        /** @type {any[]} */
        let newVisibility = []

        // Read-only: get all filter pairs before the transaction to avoid long-lived locks
        const filterPairs=await query(`SELECT filter.UID,filter.Data AS filterData,list.UID AS listUID,job.Type AS ObjectType, job.Data AS ExtraData,Member.Data,
                Main.Data AS MainBaseData, list.Display,filter.Type
                FROM ObjectBase AS filter
                INNER JOIN Links AS gLink ON (gLink.UIDTarget=filter.UIDBelongsTo AND filter.Type IN ('visible','changeable' ) AND gLink.Type IN ('member','memberA','memberSys') )
                INNER JOIN Links AS lLink ON (lLink.UID=filter.UID AND lLink.Type='list')
                INNER JOIN ObjectBase AS list ON (list.UID=lLink.UIDTarget AND list.Type IN ('list','dlist','email','event','eventT','location'))
                INNER JOIN ObjectBase AS job ON (gLink.UID=job.UID AND job.Type IN ('person','guest','extern','job'))
                INNER JOIN Member ON (Member.UID=job.UIDBelongsTo)
                INNER JOIN ObjectBase AS Main ON (Main.UID=Member.UID) 
                WHERE job.UIDBelongsTo=?
                GROUP BY filter.UID,job.UID
            `,[UID])

        await transaction(async (connection)=>
        {
            //console.log('starte transaktion', HEX2uuid(UID)) 
            oldVisibility=await connection.query(`SELECT Visible.UID,Visible.Type,ObjectBase.Type AS ObjectType
                FROM Visible INNER JOIN ObjectBase 
                ON (ObjectBase.UID=Visible.UID AND ObjectBase.Type IN ('list','dlist','email','event','eventT','location')) 
                WHERE Visible.UIDUser=?`,[UID])
            await connection.query(`DELETE Visible FROM Visible INNER JOIN ObjectBase 
                        ON (ObjectBase.UID=Visible.UID AND ObjectBase.Type IN ('list','dlist','email','event','eventT','location')) 
                        WHERE Visible.UIDUser=?`,[UID])
            // now build pair of access of lists for all jobs of the person
            // now execute all filter object pairs
            const toBeAdded=filterPairs.map(el=>
                ({...el,filterData:JSON.parse(el.filterData),Data:JSON.parse(el.Data),ExtraData:JSON.parse(el.ExtraData),MainBaseData:JSON.parse(el.MainBaseData)})
            )
            .filter(fp=>(
                        
                        matchObject(
                                {UID:UID, Type:fp.ObjectType, Data:fp.Data, ExtraData: fp.ExtraData, MainBaseData: fp.MainBaseData}
                                
                            ,fp.filterData)
                    ))
            .reduce((result,actual)=>{
                const already=result.find(el=>el.listUID.equals(actual.listUID))
                if(already)
                {
                    if(actual.Type==='changeable')
                    {
                        if(actual.filterData.admin)
                            already.Type='admin'
                        else
                            already.Type='changeable'
                    }
                    return result
                }
                else
                {
                
                    const Type= actual.Type==='changeable' ? actual.filterData.admin ? 'admin' :'changeable' :'visible'
                    return [...result,{listUID:actual.listUID,Type:Type}]
                }
            },[])
            if(toBeAdded.length >0)
            {
                await connection.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))`,
                toBeAdded.map(el=>([el.listUID,el.Type,UID])),
                {batch:true})
            }
            newVisibility=await connection.query(`SELECT Visible.UID,Visible.Type,ObjectBase.Type AS ObjectType
                FROM Visible INNER JOIN ObjectBase 
                ON (ObjectBase.UID=Visible.UID AND ObjectBase.Type IN ('list','dlist','email','event','eventT','location')) 
                WHERE Visible.UIDUser=?`,[UID])
      //      console.log('beende transaktion', HEX2uuid(UID)) 
        },
        {
            beforeTransaction:async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
        })
        const remove=oldVisibility.filter(oldEl=>!newVisibility.find(el=>(oldEl.UID.equals(el.UID) && oldEl.Type===el.Type)))
        remove.forEach(el => {
            publishEvent(`/remove/${el.ObjectType}/${el.Type}/${HEX2uuid(el.UID)}`, { organization: UIDOrga, data: [HEX2uuid(UID)] })
        });
        const add= newVisibility.filter(newEl=>!oldVisibility.find(el=>(newEl.UID.equals(el.UID) && newEl.Type===el.Type)))
        add.forEach(el => {
            publishEvent(`/add/${el.ObjectType}/${el.Type}/${HEX2uuid(el.UID)}`, { organization: UIDOrga, data: [HEX2uuid(UID)] })
        });
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }


    

}