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