Source: sub-folder/controller.js

/**
 * Action Controller
 * 
 * Contains all the business logic for action operations:
 * - Creating actions from templates and triggers
 * - Updating existing actions
 * - Deleting actions
 * - Retrieving actions by various criteria
 * - Managing missed action events
 * 
 * Actions are instances of action templates that are triggered by specific events.
 * They link templates to triggers and contain the actual execution data.
 */

import { query, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates } from '../../utils/compileTemplates.js';
import { getUID, isValidUID } from '../../utils/UUIDs.js';
import { addUpdateEntry } from '../../server.ws.js';
import { publishEvent } from '../../utils/events.js';
import { isAdmin } from '../../utils/authChecks.js';
import { errorLoggerRead, errorLoggerUpdate } from '../../utils/requestLogger.js';
import _ from 'lodash';

/**
 * Create or update an action from a template and trigger
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const createAction = async (req, res) => {
    try {
        let UID = await getUID(req);
        const UIDtrigger = UUID2hex(req.params.UIDTrigger);
        const UIDtemplate = UUID2hex(req.params.UIDTemplate);
        const triggers = await query(`SELECT Type,Data FROM ObjectBase WHERE UID=?`, [UIDtrigger]);

        if (triggers.length === 0) {
            res.json({ success: false, message: 'invalid trigger UID supplied' });
            return;
        }
        const trigger = triggers[0];
        const aTemplates = await query(`SELECT Data FROM ObjectBase WHERE UID=?`, [UIDtemplate]);
        if (aTemplates.length === 0) {
            res.json({ success: false, message: 'invalid template UID supplied' });
            return;
        }
        const aTemplate = JSON.parse(aTemplates[0].Data);

        // Check the unique requirements of the template and delete the old one if required
        if (aTemplate.unique) {
            if (aTemplate.unique === 'system') {
                // Delete existing actions for this template
                const exist = await query(`SELECT ObjectBase.UID, ObjectBase.Data, ObjectBase.UIDBelongsTo AS UIDTrigger,
                    Links.UID AS UIDtemplate,TriggerT.Type AS TriggerType
                    FROM ObjectBase
                    INNER JOIN ObjectBase AS TriggerT ON (ObjectBase.UIDBelongsTo=TriggerT.UID)
                    INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='action')
                    WHERE  ObjectBase.Type='action'  AND Links.UID=?`, [UIDtemplate], { cast: ['json', 'UUID'] });
                
                if (exist.length > 0) {
                    await query(`DELETE ObjectBase,Links 
                    FROM ObjectBase
                    INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='action')
                    WHERE  ObjectBase.Type='action' AND ObjectBase.UID IN (?)`, [exist.map(a => UUID2hex(a.UID))]);
                    
                    for (const action of exist) {
                        publishEvent(`/remove/actionT/action/${req.params.UIDTemplate}`, {
                            UID: action.UID,
                            action: action
                        });
                    }
                }
            }
            
            if (aTemplate.unique === 'trigger') {
                // Find existing action for this template and trigger, reuse UID
                const exists = await query(`Select ObjectBase.UID
                FROM ObjectBase
                INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='action')
                WHERE ObjectBase.UIDBelongsTo=? AND ObjectBase.Type='action' AND Links.UID=?`, [UIDtrigger, UIDtemplate]);
                if (exists.length > 0) {
                    UID = exists[0].UID;
                }
            }
        }
        
        if (aTemplate.onlyAdmin && !await isAdmin(req.session)) {
            res.json({ success: false, message: 'user not authorized for this action template. Has to be admin' });
            return;
        }
        
        req.body.TriggerType = trigger.Type;
        req.body.UIDTrigger = req.params.UIDTrigger;
        const template = Templates[req.session.root].action;
        const object = await renderObject(template, { ...req.body, action: { ...aTemplate, targetType: trigger.Type } }, req);
        
        await query(`
                INSERT INTO ObjectBase(UID,UIDBelongsTo,Type,Title,Display,SortName,FullTextIndex,dindex,Data)
                VALUES (?,?,'action',?,?,?,?,?,?) ON DUPLICATE KEY UPDATE Data=VALUE(Data)`,
            [object.UID, UIDtrigger, object.Title, object.Display, object.SortIndex, object.FullTextIndex, object.dindex, JSON.stringify({ ...aTemplate.defaults, ...req.body, UID: undefined })]);
        
        await query(`INSERT INTO Links (UID,Type,UIDTarget) VALUES(?,'action',?) ON DUPLICATE KEY UPDATE Type='action'`, [UIDtemplate, UID]);
        
        addUpdateEntry(UIDtrigger, { addAction: { UID: HEX2uuid(UID), Display: object.Display, Data: req.body, UIDtemplate: req.params.UIDTemplate } });

        publishEvent(`/add/actionT/action/${req.params.UIDTemplate}`, HEX2uuid(UID));
        res.json({ success: true, result: { Data: req.body, template: aTemplates[0] } });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Update an existing action
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const updateAction = async (req, res) => {
    try {
        const UID = UUID2hex(req.params.UID);
        const result = await query(`SELECT ObjectBase.Data,Template.Data AS TemplateData,Template.UID AS UIDtemplate,
            TriggerObject.Type AS triggerType, TriggerObject.UID AS UIDtrigger FROM ObjectBase 
            INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='action')
            INNER JOIN ObjectBase AS Template ON (Template.UID=Links.UID)
            INNER JOIN ObjectBase AS TriggerObject ON (ObjectBase.UIDBelongsTo=TriggerObject.UID)
            WHERE ObjectBase.UID=? AND ObjectBase.Type='action'`, [UID], { cast: ['json'] });
        
        if (!result || result.length === 0) {
            res.json({ success: false, message: 'invalid action UUID' });
            return;
        }
        
        const oldData = result[0].Data;
        const aTemplate = result[0].TemplateData;
        const triggerType = result[0].triggerType;
        const UIDtrigger = result[0].UIDtrigger;
        const UIDtemplate = result[0].UIDtemplate;

        const data = { ...oldData, ...req.body };
        if (req.body.recipients) {
            data.recipients = req.body.recipients;
        }

        const template = Templates[req.session.root].action;
        const object = await renderObject(template, { ...data, action: { ...aTemplate, targetType: triggerType } }, req);
        
        await query(`UPDATE ObjectBase SET Data=?, Display=?,FullTextIndex=?,dindex=? WHERE UID=?`,
            [JSON.stringify({ ...data, UID: undefined }), object.Display, object.FullTextIndex, object.dindex, UID]);
        
        addUpdateEntry(HEX2uuid(UIDtrigger), { changeAction: { UID: req.params.UID, Display: object.Display, Data: data, UIDtemplate: HEX2uuid(UIDtemplate) } });
        
        // Publish an event, if the data has been updated (this prevents an infinite update loop with the bot)
        if (!_.isEqual(oldData, data)) {
            publishEvent(`/change/actionT/action/${HEX2uuid(UIDtemplate)}`, HEX2uuid(UID));
        }
        res.json({ success: true, result: data });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Delete an action
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const deleteAction = async (req, res) => {
    try {
        const UID = UUID2hex(req.params.action);
        const result = await query(`SELECT TriggerT.UID AS UIDTrigger,Links.UID AS UIDtemplate, ObjectBase.Data, 
                TriggerT.Type AS TriggerType
                FROM ObjectBase 
                INNER JOIN ObjectBase AS TriggerT ON (ObjectBase.UIDBelongsTo=TriggerT.UID)
                LEFT JOIN Links ON(Links.UIDTarget=ObjectBase.UID AND Links.Type='action')
                WHERE ObjectBase.UID=? AND ObjectBase.Type ='action'`, [UID], { cast: ['UUID', 'json'] });

        if (!result.length > 0) {
            res.json({ success: false, message: 'invalid action UID supplied' });
            return;
        }
        
        const action = result[0];
        await query(`DELETE ObjectBase,Links 
                    FROM ObjectBase 
                    LEFT JOIN Links 
                    ON (Links.UID=ObjectBase.UID AND Links.Type='action') 
                    WHERE ObjectBase.UID=? AND ObjectBase.Type='action'`, [UID]);
        
        res.json({ success: true });
        publishEvent(`/remove/actionT/action/${HEX2uuid(action.UIDtemplate)}`, {
            UID: req.params.action,
            action: action
        });
        addUpdateEntry(action.UIDTrigger, { removeAction: { UID: req.params.action } });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get actions by template
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getActionsByTemplate = async (req, res) => {
    try {
        const UIDtemplate = UUID2hex(req.params.UIDtemplate);
        const result = await query(`SELECT ObjectBase.UID,ObjectBase.Data,
            ETrigger.UID AS UIDTrigger,ETrigger.Type AS TriggerType 
            FROM ObjectBase 
            INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='action' AND ObjectBase.Type='action')
            INNER JOIN ObjectBase AS ETrigger ON (ETrigger.UIDBelongsTo=ObjectBase.UIDBelongsTo AND ETrigger.UID<>ObjectBase.UID)  
            WHERE Links.UID=?
            GROUP BY ObjectBase.UID`, [UIDtemplate], { cast: ['UUID', 'json'] });
        
        res.json({ success: true, result });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get actions by trigger
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getActionsByTrigger = async (req, res) => {
    try {
        const UIDtrigger = UUID2hex(req.params.UIDtrigger);
        let result;
        
        if (UIDtrigger) {
            result = await query(`SELECT ObjectBase.UID,ObjectBase.Data, ObjectBase.Display, Links.UID AS UIDtemplate
                FROM ObjectBase 
                INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='action')    
                WHERE ObjectBase.UIDBelongsTo=? AND ObjectBase.Type='action'`, [UIDtrigger], { cast: ['json', 'UUID'] });
        } else {
            result = [];
        }
        res.json({ success: true, result: result });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get a specific action by UID
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getAction = async (req, res) => {
    try {
        const UID = UUID2hex(req.params.UID);
        const result = await query(`SELECT ObjectBase.UID,ObjectBase.Data,
            ETrigger.UID AS UIDTrigger, ETrigger.Type AS TriggerType 
            FROM ObjectBase 
            INNER JOIN ObjectBase AS ETrigger ON (ETrigger.UID=ObjectBase.UIDBelongsTo)
            WHERE ObjectBase.UID=?`, [UID], { cast: ['UUID', 'json'] });
        
        res.json({ success: true, result: result[0] });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get multiple actions by UIDs
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getMultipleActions = async (req, res) => {
    try {
        // Get the actions for the UIDs posted
        const UIDs = Array.isArray(req.body) ? req.body.filter(UID => isValidUID(UID)).map(UID => UUID2hex(UID)) : [];
        let result = [];
        
        if (UIDs.length > 0) {
            result = await query(`SELECT ObjectBase.UID, ObjectBase.Data,ObjectBase.Display,
                    ETrigger.UID AS UIDTrigger, ETrigger.Type AS TriggerType, ETrigger.Title AS TriggerTitle,
                    Member.Display AS TriggerDisplay, ETrigger.Type AS TriggerType
                    FROM ObjectBase 
                    INNER JOIN ObjectBase AS ETrigger ON (ETrigger.UID=ObjectBase.UIDBelongsTo)
                    INNER JOIN Member ON (ETrigger.UID=Member.UID)
                    WHERE ObjectBase.UID IN (?)`, [UIDs], { cast: ['UUID', 'json'] });
        }
        res.json({ success: true, result });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get missed events by key and timestamp
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getMissedEventsByKey = async (req, res) => {
    try {
        const result = await query(`SELECT UNIX_TIMESTAMP(Timestamp) AS Timestamp, Data FROM eventLog WHERE UNIX_TIMESTAMP(Timestamp)>? AND  EventKey= ?`,
            [req.params.timestamp / 1000, req.params.key], {});
        res.json({ success: true, result });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get missed events by multiple keys and timestamp
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getMissedEventsByKeys = async (req, res) => {
    try {
        // Retrieves all actions since the last timestamp
        if (Array.isArray(req.body) && req.body.length > 0) {
            const result = await query(`
                    SELECT UNIX_TIMESTAMP(Timestamp) AS Timestamp,Data, EventKey FROM eventLog 
                    WHERE UNIX_TIMESTAMP(Timestamp)>? AND  EventKey IN (?)
                    ORDER BY Timestamp`,
                [req.params.timestamp / 1000, req.body], {});

            res.json({ success: true, result });
        } else {
            res.json({ success: false, message: 'invalid or empty body supplied' });
        }
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get missed events by template and timestamp
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getMissedEventsByTemplate = async (req, res) => {
    try {
        // Retrieves all action for a template
        const resultA = await query(`SELECT action.Data,actionT.Data AS TData 
            FROM ObjectBase AS actionT
            INNER JOIN Links ON (actionT.UID=Links.UID AND Links.Type='action')
            INNER JOIN ObjectBase AS action ON (action.UID=Links.UIDTarget AND action.Type='action' )
            WHERE actionT.UID=?
            ORDER BY Timestamp`, [req.params.UIDtemplate], {
            cast: ['UUID', 'json'],
            castParas: true,
            log: false
        });
        
        const keys = resultA.reduce((result, current) => {
            const resultA = current.Data.entries.map(e => `/${current.TData.action}/${current.Data.TriggerType}/${e}/${current.Data.UIDTrigger}`);
            if (resultA && resultA.length > 0)
                return [...result, ...resultA];
            else
                return result;
        }, []);
        
        if (keys.length > 0) {
            const result = await query(`
                    SELECT UNIX_TIMESTAMP(Timestamp) AS Timestamp,Data, EventKey FROM eventLog 
                    WHERE UNIX_TIMESTAMP(Timestamp)>? AND  EventKey IN (?)
                    ORDER BY EventKey`,
                [req.params.timestamp / 1000, keys], { log: false });
            
            if (result) {
                res.json({ success: true, result: result });
                return;
            }
        }
        res.json({ success: true, result: [] });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get missed events by pattern and timestamp
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getMissedEventsByPattern = async (req, res) => {
    try {
        // Retrieves all missed actions for a given pattern
        const searchPattern = decodeURIComponent(req.params.pattern).replace('*', '%').replace('?', '_');

        const result = await query(`
                SELECT UNIX_TIMESTAMP(Timestamp) AS Timestamp,Data, EventKey FROM eventLog 
                WHERE UNIX_TIMESTAMP(Timestamp)>? AND  EventKey LIKE ? 
                ORDER BY EventKey`,
            [req.params.timestamp / 1000, searchPattern], { log: false });

        res.json({ success: true, result });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};