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 { publishVisibilityDiff, resolveVisibilityOrganization } from './visibilityEvents.js'
import { errorLoggerUpdate } from '../utils/requestLogger.js'
import { ACCOUNTING_TARGETS, GENERIC_VISIBLE_TARGETS, typesIn } from './visibilityTargets.js'
import { NORMALIZED_VISIBLE_SCOPES } from '../config/featureFlags.js'

// Object types whose visibility is rebuilt by the generic list/visibility path.
const GENERIC_TARGET_TYPES_IN = typesIn([...GENERIC_VISIBLE_TARGETS]);
/**
 * Was der Personen-Rebuild **loeschen** darf.
 *
 * Muss genau die Menge sein, die `personListRebuildAccess` danach wieder
 * aufbaut — sonst loescht der eine Lauf, was der andere nicht zurueckholt.
 * Genau das war der Fehler: die Loesch-Menge stand auf den Legacy-Typen fest,
 * waehrend die Wiederaufbau-Menge um `project` und die Share-Typen gewachsen
 * ist. Ein Projekt/ein Share verlor damit seine Personen-Zeile, ohne dass sie
 * neu entstand.
 *
 * Accounting-Ziele sind ausgenommen, solange die normalisierte Ueberfuehrung
 * laeuft — sie werden getrennt gerechnet.
 */
const REBUILD_CLEANUP_EXCLUDE = NORMALIZED_VISIBLE_SCOPES
    ? `ObjectBase.Type NOT IN ${typesIn([...GENERIC_VISIBLE_TARGETS, ...ACCOUNTING_TARGETS])}`
    : `ObjectBase.Type NOT IN ${GENERIC_TARGET_TYPES_IN}`;


export const personRebuildAccess=async (UID,UIDOrga=null)=>
{
    try {
        
        // rebuild  all visibilities for none list/dlist/email/location the user with this UID has
        // delete all existing links for the user UID
        //
        // Der Lauf meldet **als Ganzes**: er loescht erst alle Zeilen dieses Typs
        // und baut sie dann neu auf. Deshalb derselbe Ausschnitt vor und nach der
        // Transaktion — und die Job-Filter darin melden nicht selbst
        // (`report=false`), sonst kaeme dasselbe Recht zweimal an.
        const scopeSql=`SELECT Visible.UID, Visible.Type, Visible.UIDUser, ObjectBase.Type AS ObjectType
            FROM Visible INNER JOIN ObjectBase
            ON (ObjectBase.UID=Visible.UID AND ${REBUILD_CLEANUP_EXCLUDE})
            WHERE Visible.UIDUser=? AND Visible.Accounting IS NULL`

        const oldVisibility=await query(scopeSql,[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 ${REBUILD_CLEANUP_EXCLUDE}) 
                    
                        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,null,false)
                    else if(filter.Type==='changeable')
                        await includeChangeability(filter.UIDFilter,filter.UIDJob,connection.connection,null,false)
                }
            }
            // 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;`)
        })

        const newVisibility=await query(scopeSql,[UID])
        publishVisibilityDiff(oldVisibility,newVisibility,UIDOrga ?? await resolveVisibilityOrganization(UID))
    }
    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,organization=null)=>
{
    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,organization})
    }
    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);
        }

        // `ObjectType` wird nicht gelesen, sondern gesetzt: alle Zeilen haengen an
        // `UIDtarget`, dessen Typ oben schon feststeht. `publishVisibilityDiff`
        // braucht das Feld, weil es den Event-Key daraus baut.
        const oldVisibility=(await query(`SELECT Visible.UIDUser,Visible.Type 
            FROM Visible
            WHERE Visible.UID=?`,[UIDtarget])).map(el=>({...el,UID:UIDtarget,ObjectType:targetType}))
        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)
                {
                    // Die Organisation muss durchgereicht werden: `visibilityList`
                    // laeuft sonst auf seinen Default (Null-Organisation) und die
                    // von ihm gemeldeten /add|remove-Events tragen die falsche
                    // Organisation im Payload, obwohl die Zeile am richtigen
                    // Objekt landet.
                    await visibilityList(filter.UIDFilter,UIDtarget,connection,UIDOrga)
                }
            }
            // 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])).map(el=>({...el,UID:UIDtarget,ObjectType:targetType}))

        // Der Vertrag lebt in `visibilityEvents` — nicht mehr als Kopie der
        // Schleife hier (siehe 057 §16).
        publishVisibilityDiff(oldVisibility,newVisibility,UIDOrga)
    }
    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 ${GENERIC_TARGET_TYPES_IN})
                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,Visible.UIDUser,ObjectBase.Type AS ObjectType
                FROM Visible INNER JOIN ObjectBase 
                ON (ObjectBase.UID=Visible.UID AND ObjectBase.Type IN ${GENERIC_TARGET_TYPES_IN}) 
                WHERE Visible.UIDUser=?`,[UID])
            await connection.query(`DELETE Visible FROM Visible INNER JOIN ObjectBase 
                        ON (ObjectBase.UID=Visible.UID AND ObjectBase.Type IN ${GENERIC_TARGET_TYPES_IN}) 
                        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})
            }
            // Die Owner-Zeile (`member`-Link am Ziel) entsteht sonst nur beim
            // **Ziel**-Rebuild (`listRebuildAccess`). Dieser Lauf loescht sie mit
            // (sie liegt im generischen Ziel-Set) und kann sie nicht aus Filtern
            // gewinnen — der Eigentuemer verloere sein `admin` bei jeder
            // Personen-Aenderung. Deshalb hier dieselbe Zeile zurueckholen.
            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')
                    INNER JOIN ObjectBase AS Target ON (Target.UID=Links.UID AND Target.Type IN ${GENERIC_TARGET_TYPES_IN})
                    WHERE Links.Type= 'member' AND Links.UIDTarget=?
                    ON DUPLICATE KEY UPDATE Type='admin'`,[UID])
            newVisibility=await connection.query(`SELECT Visible.UID,Visible.Type,Visible.UIDUser,ObjectBase.Type AS ObjectType
                FROM Visible INNER JOIN ObjectBase 
                ON (ObjectBase.UID=Visible.UID AND ObjectBase.Type IN ${GENERIC_TARGET_TYPES_IN}) 
                WHERE Visible.UIDUser=?`,[UID])
      //      console.log('beende transaktion', HEX2uuid(UID)) 
        },
        {
            beforeTransaction:async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
        })
        // Ein Vertrag fuer alle Meldewege (siehe 057 §16): die Zeilen tragen hier
        // bereits `ObjectType` aus dem Join.
        publishVisibilityDiff(oldVisibility,newVisibility,UIDOrga)
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }


    

}