Source: tree/treeQueue/treeRemove.memberAction.js

// @ts-check
/**
 * @import {treeAction} from './../../types.js'
 */
import { query, transaction, HEX2uuid } from '@commtool/sql-query'
import { addUpdateList } from '../../server.ws.js'
import { checkEntries } from '../matchObjects/checkEntries.js'
import { personListRebuildAccess, personRebuildAccess, objectRebuildAccess } from '../rebuildList.js'
import { publishEvent } from '../../utils/events.js'
import { errorLoggerUpdate } from '../../utils/requestLogger.js'

/**
 * Handles tree-structural remove actions for objects leaving a group hierarchy.
 *
 * Supported types: `job`, `guest`, `groupGuest`, `group`, `person`, `extern`,
 * `achievement` (stub), `event`, `eventJob`.
 *
 * Steps:
 * 1. Resolve current memberships of the removed object.
 * 2. Delete `member` links for `job` and `guest`.
 * 3. Job-specific: delete visibility filters and rebuild the holding person's access.
 * 4. groupGuest: queue removal for all guest children; clean up memberG/memberGA links.
 * 5. guest: delete the guest ObjectBase row.
 * 6. group: cascade-delete member links; queue filter removals; re-check list entries.
 * 7. person/guest/extern/job: re-check list entries via `checkEntries`.
 * 8. event: rebuild object visibility.
 * 9. Publish removal events and trigger WebSocket updates.
 *
 * @param {treeAction} action
 * @returns {Promise<void>}
 */
export const removeMemberAction = 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})` : ''

    // ── 1. Resolve current memberships ───────────────────────────────────────
    const delta = await query(
        `SELECT Links.UIDTarget, Links.Type AS LinkType
         FROM Links ${asOf}
         INNER JOIN ObjectBase ON (Links.UIDTarget = ObjectBase.UID)
         WHERE Links.Type IN ('member','memberA','memberG','memberGA','memberS', 'memberSys') AND ObjectBase.Type IN ('group','ggroup') AND Links.UID=? `,
        [action.UIDObjectID],
    )
    const deltaPlus = delta.map(d => d.UIDTarget)
    if(!deltaPlus.some(d => d.equals(action.UIDoldTarget)))        deltaPlus.push(action.UIDoldTarget)

    const deltaGuest = delta.filter(d => d.LinkType === 'memberG').map(d => d.UIDTarget)

    // ── 2. Delete member links for job / guest ────────────────────────────────
    if (['job', 'guest'].includes(action.Type)) {
        if (delta.length > 0) {
            await query(
                `DELETE FROM Links WHERE UID=? AND UIDTarget IN (?) AND Type='member'`,
                [action.UIDObjectID, delta.map(gr => gr.UIDTarget)],
                { backDate: timestamp },
            )
        }
    }

    // ── 3. Job: delete visibility filters; rebuild person access ─────────────
    if (action.Type === 'job' && delta.length) {
        await transaction(
            async (connection) => {
                await connection.query(
                    `DELETE ObjectBase,Links FROM ObjectBase
                     INNER JOIN Links ON (ObjectBase.UID=Links.UID AND ObjectBase.Type='visible' AND Links.Type='list')
                     WHERE Links.UIDTarget=?`,
                    [action.UIDObjectID],
                )
            },
            {
                beforeTransaction: async (connection) =>
                    await connection.query(`SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;`),
            },
        )
        await Promise.all([
            personRebuildAccess(action.UIDBelongsTo),
            personListRebuildAccess(action.UIDBelongsTo, HEX2uuid(action.UIDroot)),
        ])
    }

    // achievement stub (logic commented out in original, preserved here)
    // if (action.Type === 'achievement') { /* TODO */ }

    // ── 4. groupGuest: queue guest removals; clean up memberG/memberGA links ──
    if (action.Type === 'groupGuest') {
        await query(
            `INSERT INTO TreeQueue (UIDRoot, UIDuser, Type, UIDObjectID, UIDBelongsTo, UIDoldTarget, UIDnewTarget)
             SELECT ?, ?, 'guest', ObjectBase.UID, ObjectBase.UIDBelongsTo, ?, NULL
             FROM ObjectBase
             INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type='memberA')
             WHERE Links.UIDTarget=?`,
            [action.UIDroot, action.UIDuser, action.UIDoldTarget, action.UIDoldTarget],
        )
        const depGroups = await query(
            `SELECT UID FROM Links WHERE Links.UIDTarget=? AND Links.Type IN ('member','memberA')`,
            [action.UIDObjectID],
        )
        await query(
            `DELETE FROM Links WHERE Type IN ('memberG','memberGA')
             AND UID IN (?) AND UIDTarget IN (?)`,
            [
                [...depGroups.map(g => g.UID), action.UIDObjectID],
                [...deltaGuest, action.UIDoldTarget],
            ],
        )
    }

    // ── 5. guest: delete guest ObjectBase row ────────────────────────────────
    if (action.Type === 'guest') {
        await query(
            `DELETE FROM ObjectBase WHERE UID=? AND Type='guest'`,
            [action.UIDObjectID],
        )
    }

    // ── 6. group: cascade-delete member links; queue filter removals ─────────
    if (action.Type === 'group') {
        await query(
            `DELETE Links FROM Links, Links AS LinksObject, ObjectBase
             WHERE ObjectBase.Type IN ('person','job','extern','guest','group','ggroup')
               AND Links.UID=ObjectBase.UID      AND Links.Type IN ('member','memberA')
               AND LinksObject.UID=ObjectBase.UID AND LinksObject.Type IN ('member','memberA')
               AND Links.UIDTarget=? AND LinksObject.UIDTarget=?`,
            [action.UIDoldTarget, action.UIDObjectID],
        )

        const filters = await query(
            `SELECT ObjectBase.UID, ObjectBase.Type FROM ObjectBase
             WHERE ObjectBase.UIDBelongsTo=? AND ObjectBase.Type IN ('visible','changeable')`,
            [action.UIDoldTarget],
        )
        if (filters.length) {
            await query(
                `INSERT INTO TreeQueue
                     (UIDRoot, UIDuser, Type, UIDObjectID, UIDBelongsTo, UIDoldTarget, UIDnewTarget)
                 VALUES (?, ?, ?, ?, ?, ?, ?)`,
                filters.map(f => [
                    action.UIDroot, action.UIDuser,
                    f.Type, f.UID, action.UIDObjectID,
                    null, f.UID,
                ]),
                { batch: true },
            )
        }

        const persons = await query(
            `SELECT ObjectBase.UID, ObjectBase.UIDBelongsTo FROM ObjectBase
             INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type IN ('member','memberA','memberS'))
             WHERE Links.UIDTarget=? AND ObjectBase.Type IN ('person','guest','extern','job')
             GROUP BY UIDBelongsTo`,
            [action.UIDObjectID],
        )
        if (persons.length)
            await checkEntries(persons.map(p => p.UIDBelongsTo), HEX2uuid(action.UIDroot))

        addUpdateList(action.UIDObjectID)
    }

    // ── 7. Re-check list entries for person-like types ───────────────────────
    if (['person', 'guest', 'extern', 'job'].includes(action.Type)) {
        await checkEntries(action.UIDBelongsTo, HEX2uuid(action.UIDroot))
    }

    // ── 8. event: rebuild object visibility ──────────────────────────────────
    if (action.Type === 'event') {
        objectRebuildAccess(action.UIDObjectID, deltaPlus)
    }

    
    // ── 9. Publish events + WebSocket updates ────────────────────────────────
    if (['guest', 'extern', 'job', 'eventJob','person'].includes(action.Type)) {
        for (const UIDgroup of deltaPlus) {
            publishEvent(`/remove/group/${action.Type}/${HEX2uuid(UIDgroup)}`, {
                organization: HEX2uuid(action.UIDroot),
                data: [HEX2uuid(action.UIDObjectID)],
                backDate: timestamp,
            })
        }
        addUpdateList(deltaPlus)
    }
    } catch (e) {
        errorLoggerUpdate(e || new Error('removeMemberAction: Unknown error occurred'))
    }
}