Source: Router/person/migratePerson.js

/**
 * Person Migration Module
 * 
 * Handles the migration of persons between groups and management of group membership trees.
 * This module manages the complex relationships between persons and groups, including:
 * - Adding persons to group hierarchies
 * - Migrating persons between groups
 * - Managing guest memberships
 * - Publishing migration events
 * - Handling temporal data with timestamps
 * 
 * @module person/migratePerson
 */

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

import {addUpdateList } from '../../server.ws.js'
import { errorLoggerUpdate } from '../../utils/requestLogger.js'



/**
 * Migrates a person from one group to another, managing all membership links
 * 
 * Handles the complete migration process including:
 * - Removing old group memberships
 * - Adding new group memberships
 * - Publishing migration events (exit/add)
 * - Updating WebSocket clients
 * The event messaging will be done in the trriggerQueue function after the migration is processed.
 * 
 * @param {Buffer} UID - Person UID to migrate
 * @param {Buffer} UIDoldTarget - Old group UID to migrate from
 * @param {Buffer} UIDnewTarget - New group UID to migrate to
 * @param {number|null} timestamp - Optional timestamp for backdating
 * @returns {Promise<void>}
 */
export const migratePerson = async (UID, UIDoldTarget, UIDnewTarget, timestamp) => 
{
    try {
    // const asOf=timestamp? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : ''
        const after= timestamp ? `FOR SYSTEM_TIME FROM FROM_UNIXTIME(${timestamp}) TO NOW()`:''
        const asOf=timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : ''

        // get delta between old and new membership
        const diffOld = await query(`SELECT Links.UIDTarget, ObjectBase.Title, Member.Display, Links.TUIDTarget
                    FROM Links 
                    INNER JOIN ObjectBase ${after} ON (Links.UIDTarget=ObjectBase.UID AND ObjectBase.Type IN ('group','ggroup'))
                    INNER JOIN Member ON (Member.UID=ObjectBase.UID)
                    WHERE Links.Type IN ('member','memberA','memberS') AND Links.UID=?
                    AND  Links.UIDTarget NOT IN 
                    (SELECT Links.UIDTarget FROM Links ${asOf}  WHERE Links.Type IN ('member','memberA','memberS') AND Links.UID=? )
                    GROUP BY ObjectBase.UID`,
                [UIDoldTarget, UIDnewTarget],{log:false})
            


        const deltaOld = diffOld.map(ob => ob.UIDTarget).filter(el => !UIDoldTarget.equals(el))
        // add mutation group as well
        const deltaOldPlus = [...deltaOld, UIDoldTarget]

        // get delta between new and old membership
        const diffNew = await query(`
                SELECT UIDTarget,ObjectBase.Title, Member.Display 
                FROM Links  ${after}
                INNER JOIN ObjectBase ${after} ON (Links.UIDTarget=ObjectBase.UID AND ObjectBase.Type IN ('group','ggroup'))
                INNER JOIN Member ON (Member.UID=ObjectBase.UID)
                WHERE Links.Type IN ('member','memberA','memberS') AND Links.UID=?
                AND  Links.UIDTarget NOT IN 
                (SELECT UIDTarget FROM Links  ${asOf} 
                WHERE Type IN ('member','memberA','memberS') AND UID=?
                AND UIDTarget NOT IN (?)
                )
                GROUP BY UIDTarget`,
            [UIDnewTarget, UIDoldTarget, deltaOldPlus])

        const deltaNew = diffNew.map(ob => ob.UIDTarget).filter(el => !UIDnewTarget.equals(el))


        await transaction(async (connection) => {
            // delete deltaOld membership
            if(deltaOld.length > 0) {
                await connection.query(`DELETE FROM Links 
                WHERE UIDTarget IN(?)  AND UID = ? AND Type IN ('member','memberA')`,
                    [deltaOldPlus, UID])
                
            }

            
            
            // add deltaNew membership
            // await connection.query(`SET @@timestamp = ${parseFloat(timestamp + 1)}`)


            if (deltaNew.length > 0)
            {
                
                await connection.query(
                    `INSERT IGNORE INTO Links (UID, Type, UIDTarget) VALUES (?,'member',?)`,
                    deltaNew.map(el => ([UID, el])),
                    { batch: true })
               
            }
            // add main membership
            
            const setResult = await connection.query(
                `INSERT IGNORE INTO Links (UID, Type, UIDTarget) VALUES (?,'memberA',?)`,
                [UID, UIDnewTarget])
            
            if (!setResult) {
                //console.log('error link', HEX2uuid(UID), HEX2uuid(UIDnewTarget), deltaNew, deltaOld, timestamp)
            }
        }, { backDate: timestamp })
        const updateNew=await query(`SELECT UIDTarget FROM Links WHERE UID=? AND Links.Type IN ('member','memberA')`,[UID])
        addUpdateList(updateNew.map(u=>u.UIDTarget))
        addUpdateList(UIDnewTarget)
        addUpdateList(deltaOldPlus)
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }


}

/**
 * Checks and adjusts timestamp to ensure it's not before the last change
 * 
 * @param {Buffer} targetUID - Target UID to check
 * @param {number|null} timestamp - Proposed timestamp
 * @returns {Promise<number|null>} Adjusted timestamp (may be earlier than supplied if conflicts exist)
 */
export const timestampCheck = async (targetUID, timestamp) => {


    const result = await query(`SELECT MAX(UNIX_TIMESTAMP(ObjectBase.validFrom)) AS validFrom
        FROM ObjectBase FOR SYSTEM_TIME ALL
        WHERE UID=?`, [targetUID]);

    
    const validFrom = result[0]?.validFrom || 0;


    if (timestamp && timestamp < validFrom) {
        return validFrom;
    }
    return timestamp;
}

/**
 * Adds a person to a group hierarchy tree
 * 
 * When a person is added to a group, they automatically become members of all parent
 * groups in the hierarchy. This function handles that propagation.
 * 
 * @param {Buffer} UID - Person UID to add
 * @param {Buffer} UIDnewTarget - Group UID to add person to
 * @param {number|null} timestamp - Optional timestamp for backdating
 * @returns {Promise<void>}
 */
export const addToTree=async (UID,UIDnewTarget,timestamp)=>
{
    try {
        const after= timestamp ? `FOR SYSTEM_TIME FROM FROM_UNIXTIME(${timestamp-3600}) TO NOW()`:''
    //    const asOf=timestamp? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : ''

        let delta=await query(`SELECT ObjectBase.UID AS UIDTarget,ObjectBase.Type, Member.Display, Links.Type AS LinkType,Links.validFrom 
                FROM Links ${after}
            INNER JOIN ObjectBase  ${after} 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.UID)
            WHERE Links.UID=?
            GROUP BY ObjectBase.UID`,[UIDnewTarget ])
        if(delta.length )
        {
            // timestamp darf nicht vor dem maximum aller Links.validFrom liegen
            const maxValidFrom=delta.reduce((max,el)=>(el.validFrom && el.validFrom>max ? el.validFrom : max), timestamp || 0)
            
            // add delta membership
            const ADD= delta.map(gr=>([UID,gr.UIDTarget]))
            await query(`INSERT IGNORE INTO Links (UID, Type, UIDTarget) VALUES (?,'member',?)`,
            ADD,{backDate:timestamp,batch:true})
        }
        addUpdateList(delta.map(el=>el.UIDTarget))
        addUpdateList(UIDnewTarget)
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}
/**
 * Adds a guest to group hierarchy
 * 
 * Similar to addToTree but specifically for guest members. Guests belong to a person
 * (UIDBelongsTo) and need to be added to all groups that person belongs to.
 * 
 * @param {Buffer} UID - Guest UID to add
 * @param {Buffer} UIDBelongsTo - Person UID that the guest belongs to
 * @param {Buffer} UIDnewTarget - Group UID to add guest to
 * @param {number|null} timestamp - Optional timestamp for backdating
 * @returns {Promise<void>}
 */
export const addGuest=async (UID,UIDBelongsTo,UIDnewTarget,timestamp)=>
{
    try {
        const after= timestamp ? `FOR SYSTEM_TIME FROM FROM_UNIXTIME(${timestamp}) TO NOW()`:''
    //    const asOf=timestamp? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : ''

        let delta=await query(`SELECT ObjectBase.UID AS UIDTarget,ObjectBase.Type, Member.Display, Links.Type AS LinkType 
                FROM Links ${after}
            INNER JOIN ObjectBase  ${after} 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.UID)
            WHERE Links.UID=?
            GROUP BY ObjectBase.UID`,[UIDnewTarget ])
        if(delta.length )
        {
            // 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  ${after}
                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  ${after}WHERE Type IN ('member','memberA', 'memberS') AND UID=? `
            const exists=await query(existsDebug, [UIDBelongsTo])
            delta=delta.filter(el=>(!exists.find(exist=>
                    (
                        exist.UIDTarget.equals(el.UIDTarget)
                    )
            )))
           
            // add delta membership
            const ADD= delta.map(gr=>([UID,gr.UIDTarget]))
            await query(`INSERT IGNORE INTO Links (UID, Type, UIDTarget) VALUES (?,'member',?)`,
            ADD,{backDate:timestamp,batch:true})
        }
        addUpdateList(delta.map(el=>el.UIDTarget))
        addUpdateList(UIDnewTarget)
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}

/**
 * Removes a person from a group hierarchy tree
 * 
 * Removes all membership links for a person from groups.
 * 
 * @param {Buffer} UID - Person UID to remove
 * @param {Buffer} UIDTarget - Group UID to remove person from  
 * @param {number|null} timestamp - Optional timestamp for backdating
 * @returns {Promise<void>}
 */
export const removeFromTree= async(UID, UIDTarget, timestamp) =>
{
    try {
        //    const asOf=timestamp? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : ''
        const after= timestamp ? `FOR SYSTEM_TIME FROM FROM_UNIXTIME(${timestamp}) TO NOW()`:''


        const delta=await query(`
            SELECT UIDTarget,Links.Type AS LinkType 
            FROM Links ${after}
            WHERE Type IN ('member','memberA') AND UID=?`,
        [UID])
        // add mutation group as well
        //delta.push({UIDTarget:action.UIDoldTarget})
        // delete delta membership
        const deltaPlus=delta.map(gr=>(gr.UIDTarget))
        await query(`DELETE FROM Links WHERE UID= ?  AND UIDTarget IN (?) AND Type ='member'`,
            [UID, deltaPlus.map(gr=>(gr.UIDTarget))],
            {backDate:timestamp})
        addUpdateList(deltaPlus)
    }
    catch(e)
    {
        errorLoggerUpdate(e)
    }
}