/**
* 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;
};