/**
* 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)
}
}