Source: tree/treeQueue/treeMigrate.personAction.js

// @ts-check
/**
 * @import {treeAction} from './../../types.js'
 */
import { query, HEX2uuid } from '@commtool/sql-query'
import { addVisibility } from '../matchObjects/matchObjects.js'
import { checkEntries } from '../matchObjects/checkEntries.js'
import { matchObjectsLists } from '../matchObjects/matchObjectsLists.js'
import { publishEvent } from '../../utils/events.js'
import { errorLoggerUpdate } from '../../utils/requestLogger.js'

/**
 * Normalises a raw action timestamp into a numeric value and the
 * corresponding `FOR SYSTEM_TIME AS OF …` SQL clause.
 *
 * @param {string | number | null | undefined} raw
 * @returns {{ timestamp: number | null, asOf: string }}
 */
export const parseTimestamp = (raw) => {
    const timestamp = raw
        ? (typeof raw === 'string' ? parseFloat(raw) : raw)
        : null
    const asOf = timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : ''
    return { timestamp, asOf }
}

/**
 * Returns the groups that belong to `UIDoldTarget` but **not** to
 * `UIDnewTarget`, together with the derived delta arrays.
 *
 * @param {Buffer} UIDoldTarget
 * @param {Buffer} UIDnewTarget
 * @param {string} asOf  – temporal SQL clause (may be empty string)
 * @returns {Promise<{ deltaOld: Buffer[], deltaOldPlus: Buffer[] }>}
 */
export const queryLostGroups = async (UIDoldTarget, UIDnewTarget, asOf) => {
    const diffOld = await query(
        `SELECT Links.UIDTarget, ObjectBase.Title, Member.Display, Links.TUIDTarget
         FROM Links ${asOf}
         INNER JOIN ObjectBase ${asOf} ON (Links.UIDTarget=ObjectBase.UID AND ObjectBase.Type IN ('group','ggroup'))
         INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
         WHERE Links.Type IN ('member','memberA') AND Links.UID=?
           AND Links.UIDTarget NOT IN (
               SELECT Links.UIDTarget FROM Links ${asOf}
               WHERE Links.Type IN ('member','memberA') AND Links.UID=?
           )
         GROUP BY ObjectBase.UID`,
        [UIDoldTarget, UIDnewTarget],
    )
    /** @type {Buffer[]} */
    const deltaOld = diffOld.map(ob => ob.UIDTarget).filter(el => !UIDoldTarget.equals(el))
    /** @type {Buffer[]} */
    const deltaOldPlus = [...deltaOld, UIDoldTarget]
    return { deltaOld, deltaOldPlus }
}

/**
 * Returns the groups that belong to `UIDnewTarget` but are **not**
 * exclusively shared with `UIDoldTarget`, together with the derived delta arrays.
 *
 * @param {Buffer}   UIDnewTarget
 * @param {Buffer}   UIDoldTarget
 * @param {Buffer[]} deltaOldPlus  – result from {@link queryLostGroups}
 * @param {string}   asOf          – temporal SQL clause (may be empty string)
 * @returns {Promise<{ deltaNew: Buffer[], deltaNewPlus: Buffer[] }>}
 */
export const queryGainedGroups = async (UIDnewTarget, UIDoldTarget, deltaOldPlus, asOf) => {
    const diffNew = await query(
        `SELECT UIDTarget, ObjectBase.Title, Member.Display
         FROM Links ${asOf}
         INNER JOIN ObjectBase ${asOf} ON (Links.UIDTarget=ObjectBase.UID AND ObjectBase.Type IN ('group','ggroup'))
         INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
         WHERE Links.Type IN ('member','memberA') AND Links.UID=?
           AND Links.UIDTarget NOT IN (
               SELECT UIDTarget FROM Links ${asOf}
               WHERE Type IN ('member','memberA') AND UID=?
                 AND UIDTarget NOT IN (?)
           )
         GROUP BY UIDTarget`,
        [UIDnewTarget, UIDoldTarget, deltaOldPlus],
    )
    /** @type {Buffer[]} */
    const deltaNew = diffNew.map(ob => ob.UIDTarget).filter(el => !UIDnewTarget.equals(el))
    /** @type {Buffer[]} */
    const deltaNewPlus = [...deltaNew, UIDnewTarget]
    return { deltaNew, deltaNewPlus }
}

/**
 * Returns every group `UIDTarget` is currently a member of.
 *
 * @param {Buffer} UIDTarget
 * @param {string} asOf  – temporal SQL clause (may be empty string)
 * @returns {Promise<Buffer[]>}
 */
export const queryAllGroupsOf = async (UIDTarget, asOf) => {
    const rows = await query(
        `SELECT UIDTarget FROM Links ${asOf}
         WHERE Type IN ('member','memberA') AND Links.UID=?`,
        [UIDTarget],
    )
    return rows.map(el => el.UIDTarget)
}

/**
 * Publishes `/add/group/…` or `/remove/group/…` events for each group in the
 * provided list.
 *
 * @param {'add' | 'remove'} verb
 * @param {Buffer[]}         groups
 * @param {string}           type          – action.Type ('person' | 'extern')
 * @param {Buffer}           UIDObjectID
 * @param {Buffer}           UIDroot
 * @param {number | null}    timestamp
 * @returns {void}
 */
export const publishGroupEvents = (verb, groups, type, UIDObjectID, UIDroot, timestamp) => {
    for (const group of groups) {
        publishEvent(`/${verb}/group/${type}/${HEX2uuid(group)}`, {
            organization: HEX2uuid(UIDroot),
            data: [HEX2uuid(UIDObjectID)],
            backDate: timestamp,
        })
    }
}

/**
 * Handles `person` and `extern` migration actions.
 *
 * Computes the symmetric difference between the old and new group memberships,
 * then:
 * - Rebuilds visibility for the groups gained by the move.
 * - Re-evaluates list membership for the person in all new groups.
 * - Re-checks list entries for the person.
 * - Fires `/add/group/…` events for gained groups and `/remove/group/…` for lost ones.
 *
 * @param {treeAction} action
 * @returns {Promise<void>}
 */
export const migratePersonAction = async (action) => {
    try {
        const { timestamp, asOf } = parseTimestamp(action.timestamp)

        const { deltaOldPlus } = await queryLostGroups(action.UIDoldTarget, action.UIDnewTarget, asOf)
        const { deltaNewPlus } = await queryGainedGroups(action.UIDnewTarget, action.UIDoldTarget, deltaOldPlus, asOf)

        // ── Rebuild visibility, list membership and entries ──────────────────
        await addVisibility(action.UIDObjectID, deltaNewPlus)

        const allGroupUIDs = await queryAllGroupsOf(action.UIDnewTarget, asOf)
        await matchObjectsLists(
            action.UIDObjectID,
            [action.UIDnewTarget, ...allGroupUIDs],
            HEX2uuid(action.UIDroot),
        )
        await checkEntries(action.UIDObjectID, HEX2uuid(action.UIDroot))

        // ── Publish events ───────────────────────────────────────────────────
        publishGroupEvents('add',    deltaNewPlus, action.Type, action.UIDObjectID, action.UIDroot, timestamp)
        publishGroupEvents('remove', deltaOldPlus, action.Type, action.UIDObjectID, action.UIDroot, timestamp)
    }
    catch (e) {
        errorLoggerUpdate(e || new Error('treeMigrate.personAction: Unknown error occurred'))
    }
}