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