Source: tree/treeQueue/treeAdd.js


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

import {query, transaction, HEX2uuid, UUID2hex} from '@commtool/sql-query'
import {addUpdateList } from '../../server.ws.js'

import {addFilter} from '../executeFilters/executeFilters.js'
import {personRebuildAccess} from '../rebuildList.js'
import {addVisibility} from '../matchObjects/matchObjects.js';
import { matchObjectsLists } from '../matchObjects/matchObjectsLists.js';
import {match as matchAchievement} from '../../utils/objectfilter/filters/achievement.js'
import {requalify} from '../../Router/job/utilities.js'
import rebuildFees from '../rebuildFees.js'
import { publishEvent } from '../../utils/events.js'
import { addGroupGuest, addGuests } from '../../Router/guest.js'
import { queueAdd, queueAddArray} from './treeQueue.js'
import { listRebuildAccess } from '../rebuildList.js';
import { checkPersonListMember } from '../matchObjects/checkPersonListMember.js';
import { errorLoggerUpdate } from '../../utils/requestLogger.js';
import { familyAddress } from '../familyAddress.js';

/**
 * Handles various actions related to adding objects to a tree structure, such as filters, memberships, visibility, 
 * and other hierarchical updates. This function processes different types of actions and performs corresponding 
 * database operations and updates.
 *
 * @async
 * @function addAction

 * @param {treeAction} action - The action object containing details about the operation to be performed.
 * @throws {Error} Throws an error if any database operation or action processing fails.
 */
export const addAction= async (action)=>
{
    try {
            const timestamp = action.timestamp ? (typeof action.timestamp === 'string' ? parseFloat(action.timestamp) : action.timestamp) : null
            const asOf=timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : ''
            const since= timestamp ? `FOR SYSTEM_TIME BETWEEN FROM_UNIXTIME(${timestamp}) AND NOW()` : ''
            const after= timestamp ? `FOR SYSTEM_TIME FROM FROM_UNIXTIME(${timestamp-3600}) TO NOW()`:''
            /* performs the relevant actions, when a filter has been addes to a source (group or list)
            * the filter target is supplied as action.UIDnewTarget. It can be a dlist fo include/exclude/intersect filter or a user for visibility/changeability filter
            * the filter object is supplied as action.UIDObjectID
            * the action.Type is the type of filter, which has been added
            * the action.UIDBelongsTo is the source object, which is the group or list, acting as source for the filter
            * valid action.Types are:
            * - visible: the filter is a visibility filter, which will be used to extract visible objects from the source for a user, or it will define visibility for a list/dlist/email
            * - changeable: the filter is a changeability filter, which will be used to extract changeable objects from the source for a user or it will define visibility for a list/dlist/email
            *  - include: the filter is an include filter, which will be used to extract objects from the source for a dlist, which will be added to the dlist
            *  - exclude: the filter is an exclude filter, which will be used to extract objects from the source for a dlist, which will be removed from the dlist
            *  - intersect: the filter is an intersect filter, which will be used to keep only these objects in the dlist, which are also in the filtered source
            * */
            if (['visible','changeable','include','exclude','intersect'].includes(action.Type))
            {
                await addFilter(action.UIDObjectID,action.UIDnewTarget,/** @type {FilterAction} */(action.Type), HEX2uuid(action.UIDroot)) // UIDBelongsTo
                addUpdateList(action.UIDnewTarget)

            }
            /* evaluates to which dlists an object should belongs and which useres can see/modify an object, when it is newly created or changed
            * action.UIDUIDBelongsTo the base/Member object, which has to be evaluated
            * checks all filters, and changes dlists and updates visibility and changeability for the users or for lists,dlists and events
            * */
            else if(action.Type==='listMember') 
            {
                
                // match jobs, guest and person and extern
            
                const objects= await query(`SELECT UID FROM Objects ${asOf}
                    WHERE (UIDBelongsTo=? OR UID=?) AND Type IN ('job','person','guest','extern')`,
                    [action.UIDObjectID,action.UIDObjectID]
                    )
                if(objects.length)
                {
                    const objectUIDs=objects.map(o=>o.UID)
                    const groups=await query(`SELECT ObjectBase.UID FROM ObjectBase ${asOf}
                        INNER JOIN Links ${asOf} ON (Links.UIDTarget=ObjectBase.UID AND Links.Type IN ('member','memberA'))   
                        WHERE Links.UID IN(?) AND ObjectBase.Type = 'group'
                        GROUP BY ObjectBase.UID
                    `,[objectUIDs],
                )
                    const groupUIDs=groups.map(o=>o.UID)
                    
                    await addVisibility(objectUIDs,groupUIDs,{andRemove:true})
                    await checkPersonListMember(action.UIDBelongsTo, HEX2uuid(action.UIDroot))
                    
                }

            }
            /* * this is executed, when a visibility or changeability filter defining the visibility or changeability of list/dlist/email,evant,eventT has been modified/added
            * action.UIDnewTarget is the list/dlist, whichfilters have been modified
            * */ 
            else if(action.Type==='listVisibility')
            {
                await listRebuildAccess(action.UIDnewTarget, HEX2uuid(action.UIDroot))
            }
            /* * this is executed, when an entry has been added to a list
            * we have to match the object against the filters of the list and add it to their dlist
            * action.UIDObjectID is the entry object, which has been added to the list
            * action.UIDnewTarget is the list, which has received the entry
            * */
            else if(action.Type==='list')
            {       
                // this is executed if a person was added to a list
                // we have to execute the filters for the list
                matchObjectsLists(action.UIDObjectID,action.UIDnewTarget, HEX2uuid(action.UIDroot))

            }
            /* * this has to be executed, when a function has been created or modified according to its qualification requirements
            * it (re-)qualifies all jobs, which are based to this function template
            * action.UIDObjectID is the function template, which has been created or modified
            */
            else if(action.Type==='function')
            {
                // rebuild jobs for this funntion
                requalify(HEX2uuid(action.UIDroot), action.UIDObjectID)
            }
            /* * this has to be executed, when a function has been created or modified according to its visibility and changeability requirements
            * it rebuilds all visibility and changeability filters for the jobs, which are based to this function template
            * action.UIDObjectID is the function template, which has been created or modified
            * is the group, which has been added to the function
            * 
            *  */
            else if(action.Type==='functionV')
            {
                // adjust all visibilities of jobs based on this function 
                // find all jobs for this function
                const jobs=await query(`SELECT Jobs.UID AS UIDJob, Jobs.UIDBelongsTo AS UIDPerson, GLinks.UIDTarget AS UIDGroup
                    FROM ObjectBase AS Jobs INNER JOIN Links AS FLinks ON(FLinks.UIDTarget=Jobs.UID AND FLinks.Type='function' AND Jobs.Type='job')
                    INNER JOIN Links AS GLinks ON (GLinks.UID=Jobs.UID AND GLinks.Type='memberA') AND FLinks.UID=?
                `,[action.UIDObjectID])
            
                for await (const job of jobs)
                {
                

                await transaction(async(connection)=>
                {
                        // delete all visibility and changeability filters for this job
                        await connection.query(`DELETE ObjectBase,Links 
                                        FROM ObjectBase INNER JOIN Links ON(ObjectBase.UID=Links.UID AND ObjectBase.Type IN ('visible','changeable') AND Links.Type ='list') 
                                        WHERE Links.UIDTarget=?`,[job.UIDJob])
                
                        const functionTemplate= await connection.query(`SELECT ObjectBase.Data FROM ObjectBase WHERE UID=?`,
                            [action.UIDObjectID],
                            {cast:['json']})
                        const functionData=functionTemplate[0].Data
                        if(functionTemplate.length>0 && Object.keys(functionData.access).length>0)
                        {
                            
                            // add the visibility access includers for this job
                            // get the groups from the hierarchie of this job given by the key
                            const hGroups=await connection.query(`SELECT ObjectBase.UID,JSON_VALUE(Member.Data,'$.hierarchie') AS Hierarchie,
                                    JSON_VALUE(Member.Data,'$.gender') AS Gender,ObjectBase.Title
                                    FROM ObjectBase 
                                    INNER JOIN Member ON (Member.UID=ObjectBase.UID)
                                    INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID )
                                    WHERE (Links.UID=? OR  ObjectBase.UID=? ) AND ObjectBase.Type='group' 
                                    AND JSON_VALUE(Member.Data,'$.hierarchie') IN (?) GROUP BY ObjectBase.UID`,
                            [job.UIDGroup,job.UIDGroup,Object.keys(functionData.access)])
                            // add the includer to the groups which will later add the visibilty to the person
                            if(hGroups.length>0)
                            {
                                for(const hGroup of hGroups)
                                {
                                    // add only filteres to combined groups. if a group with this both hierarchie exists
                                    if(hGroup.Gender==='C' || !hGroups.find(hg=>hg.Hierarchie===hGroup.Hierarchie && hg.Gender==='C'))
                                    {
                                        // add filter to group for extraction of the visible objects and pointing to job
                                        const [{UID:filterUID}]= await query(`SELECT UIDV1() AS UID`,[])
                                        await connection.query(`
                                            INSERT INTO ObjectBase(UID,Type,UIDBelongsTo,Title,Display,SortName, FullTextIndex, dindex,Data)
                                            VALUES (?,'visible',?,?,?,'visible','',0,?)
                                            `,
                                            [
                                                
                                                filterUID, hGroup.UID, functionData.name,hGroup.Title,JSON.stringify(functionData.access[hGroup.Hierarchie])
                                            ]
                                        )
                                        // link filter to the person holding the job
                                        await connection.query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'list',?)`,[filterUID,job.UIDJob])
                                    }
                                }
                            }
                            // now add changeability filter
                            if(functionData.writeAccess)
                            {
                                const [{UID:filterUID}]= await query(`SELECT UIDV1() AS UID`,[])
                                await connection.query(`
                                    INSERT INTO ObjectBase(UID,Type,UIDBelongsTo,Title,Display,SortName, FullTextIndex, dindex,Data)
                                    VALUES (?,'changeable',?,?,?,'changeable','',0,?)
                                    `,
                                    [
                                        filterUID, job.UIDGroup, functionData.name,'',JSON.stringify(functionData.writeAccess)
                                    ]
                                
                                )
                                // link filter to the person holding the job
                                await connection.query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'list',?)`,[filterUID,job.UIDJob])

                            }
                            // rebuild visibility for the person holding the job
                        
                        }
                    },
                    {
                        beforeTransaction:async (connection)=> await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`)
                    })
                    await personRebuildAccess(job.UIDPerson)
                }
            }
            /* * this is executed, when a family member has been added to a family and the organisation has family based fees
            * action.UIDnewTarget is the family UID, which has been created or modified
            * this will calulate the new membership fees for the family and update the family indices
            * */
            else if(action.Type==='family' || action.Type==='familyB')
            {
                // The link has in this case already been created when the member was created
                // update family indices and calculate new membership fees
                // faked req.session
                rebuildFees(action.UIDnewTarget, HEX2uuid(action.UIDroot))
            }
            /* * syncs adresses, phone numbers, email adresses, accounts between family members
            * this has to be called, when a fsmily phone, family email or family address has been created or modified for a member
            * action.UIDBelongsTo is the family UID, which the member belongs to which has been created or modified
            * */
            else if(action.Type==='familySync' )
            {
                // snyc the data between family members (fmily maial, accounts, address...)
                const persons = await query(`SELECT Member.UID,Member.Data,Links.UIDTarget AS MemberA, 
                    ObjectBase.Data AS BaseData, ObjectBase.dindex, ObjectBase.Type,ObjectBase.SortName AS SortBase,
                    ObjectBase.ValidFrom, ObjectBase.ValidUntil,ObjectBase.dindex
                    FROM ObjectBase 
                    INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
                    INNER JOIN Links  ON(Links.Type='memberA' AND Links.UID=ObjectBase.UID) 
                    INNER JOIN ObjectBase  AS MainGroup ON (Links.UIDTarget=MainGroup.UID AND MainGroup.Type='group')
                    WHERE ObjectBase.UID=? AND ObjectBase.Type IN ('person','extern')`,
                    [action.UIDBelongsTo],
                    {
                        log:false,
                        cast:['json']
                    })
                if(persons.length>0)
                    familyAddress(/** @type {import('../familyAddress.js').FamilyMemberObject} */ (persons[0]), HEX2uuid(action.UIDroot))
            }



            
            /* * tree specific actions
            * these actions are maintaining the membership tree strcuture of none persons
            */

            else
            {   


                // get membership of the target group
                let delta=await query(`SELECT ObjectBase.UID AS UIDTarget,ObjectBase.Type, Member.Display, Links.Type AS LinkType
                                FROM Links 
                                INNER JOIN ObjectBase  ON (ObjectBase.UID=Links.UIDTarget
                                    AND Links.Type IN ('member','memberA','memberS') AND ObjectBase.Type IN('group','ggroup'))
                                INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
                                WHERE Links.UID=?
                                GROUP BY ObjectBase.UID`,[action.UIDnewTarget],{log:true})
        
                const deltaPlus=[...delta.map(d=>(d.UIDTarget)),action.UIDnewTarget]
                // in case of guest or guest groups, the obejct can already be member of a higher level group
                // in this case we do not want to add him again to the higher level group
                const secondLevel=await query(`SELECT ObjectBase.UID AS UIDTarget,ObjectBase.Type, Member.Display, Links.Type AS LinkType 
                        FROM Links
                    INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UIDTarget  
                        AND Links.Type IN ('member','memberA') AND ObjectBase.Type IN('group','ggroup'))
                    INNER JOIN Member ON (Member.UID=ObjectBase.UID)
                    WHERE Links.UID IN (?)`
                ,[deltaPlus])
                
                let deltaVisual

                if(secondLevel.length>0)
                {
                    deltaVisual=[...delta,...secondLevel].reduce((result,actual)=>
                    {
                        if(!result.find(el=>el.UIDTarget.equals(actual.UIDTarget)))
                        {
                            return [...result,actual]
                        }
                        else
                        {
                            return result
                        }
                    },[])
                }
                else
                {
                    deltaVisual=[...delta]
                }


                if (action.Type==='guest' || action.Type==='ggroup' )
                {
                    // remove from delta  already existing membership of groups or persons, which are allready part of the target groups
                    const existsDebug=           
                    `SELECT UIDTarget,ObjectBase.Type, ObjectBase.Display FROM Links  ${asOf}
                        INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UIDTarget)  
                            WHERE Links.Type IN ('member','memberA','memberS') AND Links.UID=? AND ObjectBase.Type IN ('guest','person') `
                    //const existsProd= `SELECT UIDTarget FROM Links  ${asOf}WHERE Type IN ('member','memberA', 'memberS') AND UID=? `
                    const exists=await query(existsDebug,[action.UIDBelongsTo])
                    delta=delta.filter(el=>(!exists.find(exist=>
                            (
                                exist.UIDTarget.equals(el.UIDTarget)
                            )
                    )))
                }
                if (action.Type==='group')
                {
                    // group migration is currently  supported for sister groups only 
                    // now we have to trigger the tree queue for all these insertions as well)
                    const members=await query(`SELECT ObjectBase.UID,ObjectBase.Type, ObjectBase.UIDBelongsTo
                        FROM Links 
                        INNER JOIN ObjectBase ON (Links.UID=ObjectBase.UID AND Links.Type IN ('member','memberA'))
                        WHERE ObjectBase.Type IN ('job','guest','extern','person') AND Links.UIDTarget=?`,
                        [action.UIDObjectID])
                    
                    if(members.length>0)
                    {
                        // we have to add delta membership for the persons before the treeQueue action
                        const persons=members.filter(m=>m.Type==='extern' || m.Type==='person')
                        if(persons.length>0 && delta.length>0)
                        {
                                // add delta membership
                                const ADD= persons.flatMap(p=>
                                    delta.map(gr=>([p.UID,gr.UIDTarget]))
                                    )
                                
                                await query(`INSERT IGNORE INTO Links (UID, Type, UIDTarget) VALUES (?,'member',?)`,
                                ADD,{backDate:timestamp,batch:true})

                        }

                        await query(`INSERT INTO TreeQueue(UIDRoot,UIDuser,Type,UIDObjectID,UIDBelongsTo,UIDoldTarget,UIDnewTarget,timestamp) 
                            VALUES (?,?,?,?,?,?,?,?)`, members.map(m=>([action.UIDroot,action.UIDuser,m.Type,m.UID,m.UIDBelongsTo,null,action.UIDnewTarget,timestamp])), 
                            {batch:true})
                    }
                    
                    addUpdateList(action.UIDObjectID)
                }
                
                if (action.Type==='person')
                    await transaction(async (connection)=>
                    {
                

                        if(delta.length)
                        {
                            // delete possible old guest memberships, which would be duplications
                            connection.query(`DELETE Links FROM Links 
                                    INNER JOIN ObjectBase ON (ObjectBase.UIDBelongsTo=? AND ObjectBase.Type='guest' AND ObjectBase.UID=Links.UID)
                                    WHERE Links.UIDTarget IN (?)`,
                                    [action.UIDObjectID,delta.map(el=>(el.UIDTarget))],
                                )
                        }
                        // add guests to groups linked by memberG Link (guest Groups)
                        // find the group, we have to add the guest as MemberA to (the group with link Type memberGA)
                        const groupAdmin= delta.find(d=>d.LinkType==='memberGA')
                        if(groupAdmin)
                        {
                            // create guests
                            const [guest]=await addGuests([action.UIDBelongsTo],groupAdmin.UIDTarget,'guest',HEX2uuid(action.UIDroot))
                            // now link it to all memberG groups
                            await connection.query(`INSERT INTO Links (UID,Type,UIDTarget) VALUES (?,'member',?) `,
                            [deltaVisual.map(UID=>([guest.UID,UID]))],
                            {batch:true})
                        }
                    },{backDate:timestamp})

                if(action.Type==='job')
                {
                    const functionTemplate= await query(`SELECT ObjectBase.Data FROM ObjectBase 
                        INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type='function') 
                        WHERE Links.UIDTarget=?`,
                        [action.UIDObjectID],
                        {cast:['json']}
                    )
                    const functionData=functionTemplate[0].Data
                    // add visibility filter
                    if(functionData.access && Object.keys(functionData.access).length>0)
                    {
                        // add the visibility access includers for this job
                        // get the groups from the hierarchie of this job given by the key
                        const hGroups=await query(`SELECT ObjectBase.UID,JSON_VALUE(Member.Data,'$.hierarchie') AS Hierarchie,
                                JSON_VALUE(Member.Data,'$.gender') AS Gender,ObjectBase.Title
                                FROM ObjectBase
                                INNER JOIN Member ON (Member.UID=ObjectBase.UID)
                                INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID )
                                WHERE (Links.UID=? OR  ObjectBase.UID=? ) AND ObjectBase.Type='group' 
                                AND JSON_VALUE(Member.Data,'$.hierarchie') IN (?) GROUP BY ObjectBase.UID`,
                        [action.UIDnewTarget,action.UIDnewTarget,Object.keys(functionData.access)])
                        // add the includer to the groups which will later add the visibilty to the person
                        if(hGroups.length>0)
                        {
                            for(const hGroup of hGroups)
                            {
                                // add only filteres to combined groups. if a group with this both hierarchie exists
                                if(hGroup.Gender==='C' || !hGroups.find(hg=>hg.Hierarchie===hGroup.Hierarchie && hg.Gender==='C'))
                                {
                                    // add filter to group for extraction of the visible objects and pointing to job
                                    const [{UID:filterUID}]= await query(`SELECT UIDV1() AS UID`,[])
                                    await query(`
                                        INSERT INTO ObjectBase(UID,Type,UIDBelongsTo,Title,Display,SortName, FullTextIndex, dindex,Data)
                                        VALUES (?,'visible',?,?,?,'visible','',0,?)
                                        `,
                                        [
                                            filterUID, hGroup.UID, functionData.name,hGroup.Title,JSON.stringify(functionData.access[hGroup.Hierarchie])
                                        ]
                                    )
                                    queueAdd(action.UIDroot, action.UIDuser, 'visible', filterUID,hGroup.UID,null,action.UIDObjectID)
                                }
                            }
                        }
                        else
                        {
                            console.error('invalid hgroups in treeAdd.js')
                        }
                    }
                    // add changeability filter
                    if(functionData.writeAccess)
                    {
                        const [{UID:filterUID}]= await query(`SELECT UIDV1() AS UID`,[])
                        await query(`
                            INSERT INTO ObjectBase(UID,Type,UIDBelongsTo,Title,Display,SortName, FullTextIndex, dindex,Data)
                            VALUES (?,'changeable',?,?,?,'changeable','',0,?)
                            `,
                            [
                                filterUID, action.UIDnewTarget, functionData.name,'',JSON.stringify(functionData.writeAccess)
                            ]
                        )
                        queueAdd(action.UIDroot, action.UIDuser, 'changeable', filterUID,action.UIDnewTarget,null,action.UIDObjectID)
                    }
                }

                if(['person','guest','ggroup','extern','job','group'/*,'list' most likely obsolete?*/].includes(action.Type))
                {
                    const  sourceDelta=[action.UIDnewTarget,...deltaVisual.map(el=>(el.UIDTarget))]
                    await addVisibility(action.UIDObjectID,sourceDelta)
                    if(action.Type !=='group' && action.Type !=='event' && action.Type !=='eventT' && action.Type!=='ggroup')
                    {
                        matchObjectsLists(action.UIDObjectID,sourceDelta, HEX2uuid(action.UIDroot))
                    }
                }

            
            
                // add delta membership
                if(delta.length && !['person','extern', 'guest'].includes(action.Type))
                {
                        // add delta membership
                    const ADD= delta.map(gr=>([action.UIDObjectID,gr.UIDTarget]))
                    await query(`INSERT IGNORE INTO Links (UID, Type, UIDTarget) VALUES (?,'member',?)`,
                    ADD,{backDate:timestamp,batch:true})
                }

                // guest group membership
                if(['person','group','guest','ggroup','job'].includes(action.Type))
                {
                    // get ggroups of target
                    const ggroups= await query(`SELECT ObjectBase.UID,UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom
                        FROM ObjectBase  ${asOf} WHERE ObjectBase.Type='ggroup' AND ObjectBase.UIDBelongsTo=?
                        `,
                        [action.UIDnewTarget])
                    if(ggroups.length)
                    {   
                        if(action.Type==='person' || action.Type==='guest')
                        {
                            for(const group of ggroups)
                            {
                                const guest=await addGuests([action.UIDBelongsTo],group.UID,'guest',HEX2uuid(action.UIDroot),Math.max(action.timestamp,group.validFrom),action.UIDuser)
                                if(guest.length)
                                    queueAdd(action.UIDroot,action.UIDuser,'guest',guest[0].UID,guest[0].UIDBelongsTo,null,group.UID,timestamp)
                            }
                        }
                        else if(action.Type==='group' || action.Type==='ggroup')
                        {
                            for(const group of ggroups)
                                addGroupGuest({session:{root:action.UIDroot, user: action.UIDuser}},
                                    action.UIDObjectID,group.UID,action.timestamp)
                        }
                    }
                }
            


                // sending events for membership migrations to all groups where a membership change happened
                if(['person','group','guest','extern','job','ggroup','eventJob'].includes(action.Type))
                {
                        // now fire the events
                    for(const group of deltaPlus)
                        publishEvent(`/add/group/${action.Type}/${HEX2uuid(group)}`, {
                            data: [HEX2uuid(action.UIDObjectID)],
                            backDate: timestamp
                        })
                }
               
                // add mutation group as well
                deltaVisual.push({UIDTarget:action.UIDnewTarget})
                // update websockets ( for person and extern this has already been performed by migratePerson  storage)
                if(!['person','extern'].includes(action.Type))
                    addUpdateList(deltaVisual.map(el=>el.UIDTarget))
            }
    }
    catch(e)
    {
        errorLoggerUpdate(e || new Error('treeAdd: Unknown error occurred'))
    }


}