// @ts-check
/**
* Extern Service — Pure Business Logic
*
* All database operations and data transformations for external members.
* No HTTP concerns (no req.params / res). Session values are passed
* as explicit parameters where needed; `req` is forwarded only to the
* shared personHelpers functions that still require it internally.
*/
import { query, transaction, UUID2hex } from '@commtool/sql-query';
import { phonetikArray } from '../../utils/compileTemplates.js';
import { queueAdd } from '../../tree/treeQueue/treeQueue.js';
import { addToTree } from './migratePerson.js';
import { encryptIbans } from '../../utils/crypto.js';
import {
updatePersonData,
handleGroupMembershipMigration,
updatePersonEmbedding,
fetchGroup,
fetchMemberExists,
fetchLatestObjectValidFrom,
rebuildMemberAccess,
} from './personHelpers.js';
// Re-export shared helpers so callers of externService have one import point
export { fetchGroup, fetchMemberExists as fetchExternExists, fetchLatestObjectValidFrom };
// ---------------------------------------------------------------------------
// Mutation helpers
// ---------------------------------------------------------------------------
/**
* Insert a brand-new extern member into ObjectBase, Member, Links, and Visible.
* Fires tree queue and starts a non-blocking AI-embedding job.
*
* @param {Object} object Rendered extern object (from renderObject)
* @param {Buffer} UIDgroup Target group UID
* @param {string} sessionUser req.session.user
* @param {string} sessionRoot req.session.root
* @param {number|undefined} timestamp Unix seconds backdate (or undefined)
*/
export const createNewExtern = async (object, UIDgroup, sessionUser, sessionRoot, timestamp) => {
await transaction(async (connection) => {
await connection.query(
`INSERT INTO ObjectBase(UID, UIDuser, Type, SortName, UIDBelongsTo, Title, hierarchie, stage, gender, Data)
VALUES (?, ?, 'extern', ?, ?, ?, ?, ?, ?, '{}')`,
[object.UID, UUID2hex(sessionUser), object.SortBase, object.UID, object.Title, 0, 0, object.gender]
);
await query(
`INSERT INTO Member(UID, Display, SortName, FullTextIndex, PhonetikIndex, Data)
VALUES (?, ?, ?, ?, ?, ?)`,
[
object.UID,
object.Display,
object.SortIndex,
object.FullTextIndex,
phonetikArray([object.Data.firstName, object.Data.lastName]),
JSON.stringify(object.Data),
]
);
await connection.query(
`INSERT IGNORE INTO Links(UID, Type, UIDTarget) VALUES (?, 'memberA', ?)`,
[object.UID, UIDgroup]
);
}, { backDate: timestamp });
await addToTree(object.UID, UIDgroup, timestamp);
queueAdd(UUID2hex(sessionRoot), UUID2hex(sessionUser), 'extern', object.UID, object.UID, null, UIDgroup, timestamp);
await query(
`INSERT INTO Visible (UID, Type, UIDUser) VALUES (?, 'changeable', ?)
ON DUPLICATE KEY UPDATE Type = 'changeable'`,
[object.UID, UUID2hex(sessionUser)]
);
// Non-blocking AI embedding — failure must not abort the request
updatePersonEmbedding(object.UID, object.Data, 'extern', UUID2hex(sessionRoot), UUID2hex(sessionUser))
.catch(err => console.error('Background embedding creation failed:', err));
};
/**
* Update an existing extern record, or migrate a person record to type 'extern'.
* Delegates shared update logic to personHelpers.
*
* Note: `req` is forwarded because updatePersonData / handleGroupMembershipMigration
* internally require session information from it.
*
* @param {Object} extern Existing DB record for the extern
* @param {Object} object Rendered extern object
* @param {Buffer} UIDgroup
* @param {Object} req Express request object
* @param {number|undefined} timestamp
*/
export const updateExistingExtern = async (extern, object, UIDgroup, req, timestamp) => {
object.Type = 'extern';
object.hierarchie = 0;
object.stage = 0;
await updatePersonData(object.UID, object.Data, object, req, timestamp);
if (!extern.MemberA || !extern.MemberA.equals(UIDgroup) || extern.Type === 'person') {
await handleGroupMembershipMigration(extern, object.UID, UIDgroup, req, timestamp, 'extern');
}
};
/**
* Delete an extern and all its child objects (guest, job, entry) plus associated links.
*
* @param {Buffer} UID
* @param {number|undefined} timestamp
* @returns {Promise<Buffer[]>} Binary UIDs of affected parent groups (for WebSocket updates)
*/
export const deleteExternById = async (UID, timestamp) => {
const update = await query(
`SELECT UIDTarget FROM Links WHERE UID = ? AND Links.Type IN ('member', 'memberA')`,
[UID]
);
await query(
`DELETE ObjectBase, Links
FROM ObjectBase
INNER JOIN Links ON (Links.UID = ObjectBase.UID OR Links.UIDTarget = ObjectBase.UID)
WHERE ObjectBase.UIDBelongsTo = ? AND ObjectBase.Type IN ('extern', 'guest', 'job', 'entry')`,
[UID],
{ backDate: timestamp }
);
return update.map(el => el.UIDTarget);
};
/**
* Rebuild visibility and list access for an extern and all its job objects.
* Delegates to the shared helper in personHelpers.
*
* @param {Buffer} UID
* @param {string} sessionRoot req.session.root
*/
export const rebuildExternAccess = rebuildMemberAccess;