Source: RouterEvents/eventGroup/service.js

/**
 * Event Group Service Layer
 * 
 * This service manages the association between events and groups (gentries).
 * Groups can participate in events with dedicated participant lists and access control.
 * 
 * Key concepts:
 * - gentries: Group entries that link groups to events
 * - Participant Lists: Each group gets a dedicated list for event participants
 * - Filters: Consolidate group lists into main event participant list
 * - Visibility: Manages access rights for group members to event
 * 
 * Database structure:
 * - ObjectBase: Stores gentry objects linking groups to events
 * - Links: Junction table for gentry-event relationships
 * - Lists: Participant lists per group
 * - Filters: Include filters for aggregating participant data
 * 
 * @module EventGroupService
 */

// @ts-check
/**
 * @import {ExpressRequestAuthorized} from './../../types.js'
 */

import { query, pool, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { addUpdateEntry } from '../../server.ws.js';
import { isObjectAdmin } from '../../utils/authChecks.js';
import { insertList, deleteList } from '../../Router/list.js';
import { addFilter, deleteFilter } from '../../Router/filter.js';
import { addGroupVisibility, rebuildEventAccess } from '../shared/eventVisibility.js';

/**
 * Add a participant list for a group in an event
 * 
 * Creates a dedicated participant list for the group and links it to the
 * main event participant list via an include filter.
 * 
 * @param {Object} event - Event object with template data
 * @param {Object} group - Group object
 * @param {Object} session - User session object
 * @returns {Promise<void>}
 * 
 * Creates:
 * - Participant list for the group
 * - Include filter to main event participant list
 */
export const addParticipantList = async (event, group, session) => {
    const result = await insertList(
        {
            query: { user: event.UID },
            body: {
                name: `${event.templateData.groupListPrefix} ${event.templateDisplay} ${event.Display}`,
                tag: ['participant', 'group', 'event'],
                description: `<p>${group.Title} ${group.Display}</p>`
            },
            session
        },
        group.UID,
        'list'
    );
    const list = result.result;

    // Get main participant list
    const lists = await query(
        `SELECT ObjectBase.UID, Member.Data, owner.UIDTarget AS UIDowner
         FROM ObjectBase
         INNER JOIN Member ON (Member.UID=ObjectBase.UID)
         INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type='memberA')
         INNER JOIN ObjectBase AS event ON (event.UID=Links.UIDTarget AND event.Type='event')
         INNER JOIN Links AS owner ON (owner.UID=ObjectBase.UID AND owner.Type='memberA')
         WHERE ObjectBase.Type IN ('list','dlist') AND Links.UIDTarget=?`,
        [event.UID],
        { cast: ['json', 'UUID'] }
    );

    let mainList = lists.find(l => l.Data.tag.includes('all'));
    if (!mainList) {
        // Create main list if it doesn't exist
        const result = await insertList(
            {
                query: { private: true },
                body: {
                    name: `${event.templateData.participantListPrefix} ${event.templateDisplay} ${event.Display}`,
                    tag: ['participant', 'event', 'all']
                },
                session
            },
            event.UID,
            'dlist'
        );
        mainList = result.result;
    }

    // Add filter to consolidate participants
    await addFilter({
        params: {
            target: mainList.UID,
            source: list.UID,
            type: 'include'
        },
        body: { person: { all: null }, extern: { all: null } },
        session: session
    });
};

/**
 * Add a group to an event
 * 
 * Creates a gentry object linking the group to the event,
 * sets up visibility, and creates participant lists.
 * 
 * @param {Buffer} UIDevent - Event UID in hex format
 * @param {string} UIDgroupStr - Group UID as string
 * @param {Object} session - User session object
 * @param {ExpressRequestAuthorized} req - Full request object
 * @returns {Promise<Object>} Result with success status and created gentry
 * @throws {Error} When validation fails or user is not authorized
 * 
 * Security features:
 * - Validates user has admin rights to the event
 * - Checks for duplicate group associations
 * - Creates visibility entries for group members
 */
export const addEventGroup = async (UIDevent, UIDgroupStr, session, req) => {
    if (!isObjectAdmin(req, UIDevent)) {
        throw new Error('you are not authorized to add an event to this group');
    }

    const UIDgroup = UUID2hex(UIDgroupStr);

    // Get event data
    const events = await query(
        `SELECT Member.UID, Member.Display, Member.Data, Template.Display AS templateDisplay,
                Template.Data AS templateData 
         FROM ObjectBase 
         INNER JOIN Member ON (Member.UID=ObjectBase.UID) 
         INNER JOIN Links AS ALink ON (ALink.UID=ObjectBase.UID AND ALink.Type='event')
         INNER JOIN ObjectBase AS Template 
            ON (Template.UID=ALink.UIDTarget AND Template.Type='eventT')
         WHERE ObjectBase.Type='event' AND ObjectBase.UID=?`,
        [UIDevent],
        { cast: ['json'] }
    );

    if (!events || events.length === 0) {
        throw new Error('invalid event UID');
    }

    // Check if group already associated
    const already = await query(
        `SELECT ObjectBase.UID FROM 
         ObjectBase 
         INNER JOIN Links ON(Links.UID=ObjectBase.UID AND Links.Type='memberA')
         WHERE ObjectBase.Type='gentry' AND ObjectBase.UIDBelongsTo=? AND Links.UIDTarget=?`,
        [UIDgroup, UIDevent]
    );

    if (already.length > 0) {
        throw new Error('this group belongs already to this event');
    }

    const event = events[0];

    // Get group data
    const groups = await query(
        `SELECT Member.Display, ObjectBase.Title, Member.SortName, ObjectBase.hierarchie, ObjectBase.stage, 
                ObjectBase.gender, ObjectBase.dindex, ObjectBase.UID, ObjectBase.UIDBelongsTo, UIDV1() AS UIDgentry
         FROM ObjectBase 
         INNER JOIN Member ON (Member.UID=ObjectBase.UID)
         WHERE ObjectBase.Type IN('group','gentry') AND ObjectBase.UID=?`,
        [UIDgroup]
    );

    if (!groups || groups.length === 0) {
        throw new Error('invalid group parameter UID in body');
    }

    const group = groups[0];

    // Create gentry object
    await query(
        `INSERT INTO ObjectBase (UID,Type,UIDBelongsTo,Title,SortName,stage,gender,hierarchie,dindex)
         VALUES (?,'gentry',?,?,?,?,?,?,?)`,
        [
            group.UIDgentry, group.UID, group.Title, group.SortName,
            group.stage, group.gender, group.hierarchie, group.dindex
        ]
    );

    // Link gentry to event
    await query(
        `INSERT INTO Links(UID,Type,UIDTarget) VALUES(?,'memberA',?)`,
        [group.UIDgentry, UIDevent]
    );

    // Set up visibility
    addGroupVisibility(UIDevent, group.UID, req);

    // Create participant list
    await addParticipantList(event, group, session);

    addUpdateEntry(UIDevent, {
        addGroup: { group: { ...groups[0], UID: HEX2uuid(groups[0].UIDgentry) } }
    });

    return { success: true, gentry: { ...groups[0], UID: HEX2uuid(groups[0].UIDgentry) } };
};

/**
 * Delete a group from an event
 * 
 * Removes the gentry association, deletes participant list and filters,
 * and rebuilds event access control.
 * 
 * @param {Buffer} UIDevent - Event UID in hex format
 * @param {Buffer} UIDgroup - Group UID in hex format
 * @param {ExpressRequestAuthorized} req - Full request object
 * @returns {Promise<boolean>} Success status
 * @throws {Error} When event or group not found
 * 
 * Cleanup operations:
 * - Deletes gentry object and links
 * - Removes participant list for the group
 * - Deletes include filter from main list
 * - Rebuilds event access control
 */
export const deleteEventGroup = async (UIDevent, UIDgroup, req) => {
    // Validate event
    const events = await query(
        `SELECT Member.Data FROM 
         ObjectBase
         INNER JOIN Member ON (Member.UID=ObjectBase.UID)
         WHERE ObjectBase.Type='event' AND ObjectBase.UID=?`,
        [UIDevent]
    );

    if (!events || events.length === 0) {
        throw new Error('invalid event UID');
    }

    // Validate group
    const groups = await query(
        `SELECT UIDBelongsTo FROM ObjectBase WHERE Type IN ('group','gentry') AND UID=?`,
        [UIDgroup]
    );

    if (!groups || groups.length === 0) {
        throw new Error('invalid group or parameter UIDgroup');
    }

    // Delete gentry and links
    await query(
        `DELETE ObjectBase, Links FROM
         ObjectBase 
         INNER JOIN Links ON(Links.UID=ObjectBase.UID AND Links.Type='memberA')
         WHERE Links.UIDTarget=? AND ObjectBase.UIDBelongsTo=? AND ObjectBase.Type='gentry'`,
        [UIDevent, groups[0].UIDBelongsTo]
    );

    // Delete group participant list
    const lists = await query(
        `SELECT ObjectBase.UID, Member.Data, owner.UIDTarget AS UIDowner
         FROM ObjectBase
         INNER JOIN Member ON (Member.UID=ObjectBase.UID)
         INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type IN ('memberA','member'))
         INNER JOIN ObjectBase AS event ON (event.UID=Links.UIDTarget AND event.Type='event')
         INNER JOIN Links AS owner ON (owner.UID=ObjectBase.UID AND owner.Type='memberA')
         WHERE ObjectBase.Type IN ('list','dlist') AND Links.UIDTarget=?`,
        [UIDevent],
        { cast: ['json'] }
    );

    if (lists.length > 0) {
        const list = lists.find(l => l.UIDowner.equals(groups[0].UIDBelongsTo));
        if (list) {
            const UIDlist = list.UID;
            const listT = lists.find(l => l.Data?.tag?.includes('all'));
            const UIDtarget = listT.UID;

            // Delete participant list
            await deleteList(
                {
                    query: { force: true },
                    params: { UID: UIDlist }
                },
                'list'
            );

            // Delete filter
            await deleteFilter({
                params: {
                    type: 'include',
                    source: UIDlist,
                    target: UIDtarget
                },
                session: req.session
            });
        }
    }

    // Rebuild event access
    rebuildEventAccess(UIDevent, req);

    addUpdateEntry(UIDevent, { deleteGroup: { UID: HEX2uuid(UIDgroup) } });

    return true;
};

/**
 * Get list of groups associated with an event
 * 
 * Retrieves all gentries (group entries) for an event with optional
 * custom data fields via JSON paths.
 * 
 * @param {ExpressRequestAuthorized} req - Request object with query parameters
 * @returns {Promise<Array>} Array of group entry objects
 * 
 * Query parameters:
 * - Data: 'all'/'full' for complete data, or JSON array of field paths
 * - __page: For pagination (handled by controller)
 * 
 * Returns group data including:
 * - UID, Type, Title, Display
 * - Parent group information
 * - Hierarchical data (stage, gender, hierarchie)
 * - Custom data fields if requested
 */
export const getGroupsListing = async (req) => {
    let dataFields = '';

    if (req.query.Data && req.query.Data !== 'full' && req.query.Data !== 'all') {
        let fields = [];
        try {
            fields = req.query.Data ? JSON.parse(String(req.query.Data)) : null;
        } catch (e) {
            fields[0] = [req.query.Data];
        }
        for (const field of fields) {
            dataFields += `,JSON_VALUE(Member.Data,${pool.escape(field.path)}) AS ${pool.escape(field.alias)}`;
        }
    }

    if (req.query.Data === 'full' || req.query.Data === 'all') {
        dataFields += ',Member.Data';
    }

    const result = await query(
        `SELECT
            Main.UID, Main.Type, Main.UIDBelongsTo, Main.Title, Member.Display, Member.SortName, 
            pgroup.UID AS UIDgroup, 
            CONCAT(pgroup.Title,' ',pMember.Display) AS pGroup, Main.hierarchie, Main.stage, 
            Main.gender, Main.dindex
            ${dataFields}
         FROM
            ObjectBase Main
            LEFT JOIN (ObjectBase AS pgroup 
                INNER JOIN Links AS GLink ON (GLink.UIDTarget = pgroup.UID)
                INNER JOIN Member AS pMember ON (pMember.UID=pgroup.UID)
            )
            ON (GLink.UID=Main.UID AND GLink.Type='memberA') 
            INNER JOIN Member ON (Member.UID=Main.UIDBelongsTo)
            INNER JOIN Links ON (Links.UID=Main.UID AND Links.Type='memberA')
            INNER JOIN Visible ON (Visible.UID=Main.UIDBelongsTo)
         WHERE
            Links.UIDTarget=? AND Main.Type='gentry'
         GROUP BY
            Main.UID
         ORDER BY
            Main.SortName`,
        [UUID2hex(req.params.UID)],
        {
            cast: ['UUID', 'json'],
            log: false
        }
    );

    return result;
};