// @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'))
}
}