/**
* Action Template Service Layer
*
* This service manages action templates in the CommTool system. Action templates
* define the structure and behavior of actions that can be executed by bots.
*
* Key concepts:
* - Action Templates: Reusable templates that define action structure and UI
* - Organizations: Templates belong to specific organizations
* - Bots: Can be linked to action templates to execute actions
* - Multi-tenancy: Each organization has isolated templates
*
* Database structure:
* - ObjectBase: Main table storing templates with Type='actionT'
* - Links: Junction table linking bots to action templates
*
* @module ActionTemplateService
*/
import { query, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates } from '../../utils/compileTemplates.js';
import { getUID } from '../../utils/UUIDs.js';
import { updateTemplate } from '../../server.ws.js';
import { publishEvent } from '../../utils/events.js';
import { buildTranslateObject, mergeTemplateData } from './helpers.js';
import { errorLoggerRead, errorLoggerUpdate } from '../../utils/requestLogger.js';
/**
* Get a single action template by UID
*
* Retrieves a specific action template with its complete data structure.
* Used for template editing, viewing details, and bot execution contexts.
*
* @param {string} uid - Action template UUID
* @param {string} orgUID - Organization UUID (for security validation)
* @returns {Promise<Object>} Response object with success flag and template data
*
* Security features:
* - Validates organization ownership (UIDBelongsTo=orgUID)
* - Prevents cross-organization template access
*
* Returns complete template data including:
* - Metadata (UID, Display, SortName, Title)
* - Template structure and configuration (Data)
* - Organization context
*/
export const getActionTemplate = async (uid, orgUID) => {
try {
// Query specific action template with organization validation
// Security check ensures template belongs to the requesting organization
const result = await query(`
SELECT
UID,
Display,
SortName,
Title,
Data,
dindex
FROM
ObjectBase
WHERE
UID=? AND Type='actionT' AND UIDBelongsTo=?
`, [UUID2hex(uid), UUID2hex(orgUID)], { cast: ['UUID', 'json'] });
if (result.length === 0) {
return { success: false, error: 'Action template not found or access denied' };
}
return { success: true, result: result[0] };
} catch (e) {
errorLoggerRead(e);
return { success: false, error: e.message };
}
};
/**
* Get all action templates for a specific organization
*
* Retrieves action templates that belong to an organization. These templates
* define the structure and behavior of actions that can be created within
* the organization's context.
*
* @param {string} orgUID - Organization UUID
* @param {Object} req - Express request object (for query parameters)
* @returns {Promise<Object>} Response object with success flag and results
*
* Query parameters:
* - includeData: 'true' to include template data, otherwise only metadata
*
* Database schema:
* - ObjectBase.Type='actionT' for action templates
* - UIDBelongsTo links template to organization
* - Ordered by dindex for consistent display order
*/
export const getActionTemplatesForOrg = async (orgUID, req) => {
try {
// Conditionally include template data based on query parameter
// Template data can be large, so only include when explicitly requested
let mainData = '';
if (req.query.includeData === 'true') {
mainData = ', Main.Data';
}
// Query organization-specific action templates
// Uses GROUP BY to ensure unique templates (in case of duplicates)
const result = await query(`
SELECT
Main.UID,
Main.Display,
Main.SortName
${mainData}
FROM
ObjectBase AS Main
WHERE
Main.UIDBelongsTo=? AND Main.Type='actionT'
GROUP BY
Main.UID
ORDER BY
Main.dindex
`, [UUID2hex(orgUID)], { cast: ['UUID', 'json'] });
return { success: true, result };
} catch (e) {
// Log read errors for monitoring and debugging
errorLoggerRead(e);
return { success: false, error: e.message };
}
};
/**
* Get all action templates associated with a specific bot
*
* Retrieves action templates that are linked to a bot through the Links table.
* This enables bots to discover which action templates they can execute.
* Includes organization information for multi-tenant context.
*
* @param {string} botUID - Bot UUID
* @param {Object} req - Express request object (for query parameters)
* @returns {Promise<Object>} Response object with success flag and results
*
* Database relationships:
* - Links.UID = botUID, Links.UIDTarget = actionTemplateUID
* - Links.Type = 'actionT' identifies action template links
* - Joins with organization data for context
*
* Results include:
* - Template metadata (UID, Display, SortName)
* - Optional template data (if includeData=true)
* - Organization context (OrgUID, OrgDisplay)
*/
export const getActionTemplatesForBot = async (botUID, req) => {
try {
// Conditionally include template data - can be large
let mainData = '';
if (req.query.includeData === 'true') {
mainData = ', ActionT.Data';
}
// Query bot-linked action templates with organization context
// INNER JOIN with Links ensures only linked templates are returned
// LEFT JOIN with Org provides organization context (may be null)
const result = await query(`
SELECT
ActionT.UID,
ActionT.Display,
ActionT.SortName
${mainData},
ActionT.UIDBelongsTo AS OrgUID,
OrgMember.Display AS OrgDisplay,
gid.UID AS OrgLoginUID
FROM ObjectBase AS ActionT
INNER JOIN Links ON (Links.UIDTarget = ActionT.UID AND Links.Type = 'actionT')
INNER JOIN Member AS OrgMember ON (OrgMember.UID=ActionT.UIDBelongsTo)
LEFT JOIN Links AS gid ON (gid.UIDTarget=OrgMember.UID AND gid.Type='identifyer')
WHERE Links.UID = ? AND ActionT.Type = 'actionT'
ORDER BY OrgMember.Display, ActionT.Display
`, [UUID2hex(botUID)], { cast: ['UUID', 'json'] });
return { success: true, result };
} catch (e) {
errorLoggerRead(e);
return { success: false, error: e.message };
}
};
/**
* Get organizations by their UIDs
*
* Utility function to retrieve organization details for multiple organizations.
* Used for batch operations and providing organization context in templates.
*
* @param {string[]} organizationUIDs - Array of organization UUIDs
* @returns {Promise<Object>} Response object with organization details
*
* Returns organization data including:
* - UID: Organization identifier
* - Display: Human-readable organization name
* - Data: Organization-specific configuration (JSON)
*/
export const getOrganizationsByUIDs = async (organizationUIDs) => {
try {
// Convert UUIDs to hex format for database query
const orgUIDs = organizationUIDs.map(uid => UUID2hex(uid));
// Query multiple organizations by UID
// Type='orga' filters for organization records
const result = await query(`
SELECT ObjectBase.UID, Member.Display, Member.Data, gid.UID AS OrgLoginUID
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='memberSys' AND Links.UID=ObjectBase.UID)
LEFT JOIN Links AS gid ON (gid.UIDTarget=ObjectBase.UID AND gid.Type='identifyer')
WHERE ObjectBase.Type='group' AND ObjectBase.UID IN (?)
`, [orgUIDs], { cast: ['UUID', 'json'] });
return { success: true, result };
} catch (e) {
errorLoggerRead(e);
return { success: false, error: e.message };
}
};
/**
* Create or update an action template
*
* This is the core function for managing action templates. It handles both
* creation of new templates and updates to existing ones. The function
* processes template data, applies organization-specific rendering, and
* manages internationalization support.
*
* @param {Object} req - Express request object (contains session and query data)
* @param {Object} templateData - Template configuration and structure
* @returns {Promise<Object>} Response with created/updated template data
*
* Process flow:
* 1. Generate or retrieve template UID
* 2. Check if template exists (for updates vs creation)
* 3. Merge new data with existing data (if updating)
* 4. Render template with organization context
* 5. Build internationalization structure
* 6. Save to database with upsert operation
* 7. Notify UI of template changes via WebSocket
*
* Query parameters:
* - initialize: If undefined, always use new data; if defined, merge with existing
*
* Features:
* - Template rendering with organization context
* - Data merging for incremental updates
* - Internationalization support (translate object)
* - Real-time UI updates via WebSocket notifications
* - Full-text indexing for search functionality
*/
export const createOrUpdateActionTemplate = async (req, templateData) => {
try {
// check if a an action template of this type is already registered for this organization
const existingTemplate = await query(`SELECT Links.UIDTarget
FROM Links
INNER JOIN ObjectBase ON (Links.UIDTarget=ObjectBase.UID)
WHERE Links.Type='actionT' AND ObjectBase.Type='actionT' AND Links.UID=?`,
[templateData.UID ? UUID2hex(templateData.UID) : ''],{ cast: ['UUID'], log: true});
let UIDaction;
if (existingTemplate.length > 0) {
// If a template exists, we can update it
UIDaction = existingTemplate[0].UIDTarget;
}
else {
// If no template exists, we create a new one
[{UIDaction}] = await query(`SELECT UIDV1() AS UIDaction`, []);
}
// Get the template renderer for the current organization
const template = Templates[req.session.root].actionTemplate;
// Check if template already exists in database
const action = await query(`SELECT Data FROM ObjectBase WHERE UID=? `, [UIDaction]);
let aData = {};
// Parse existing template data if found
if (action.length === 1) {
try {
aData = JSON.parse(action[0].Data);
} catch (e) {
console.log('action without data');
// Template exists but has no valid JSON data
}
}
let Data;
// Determine data merge strategy based on query parameters and existing data
if (req.query.initialize === undefined || action.length === 0) {
// Use new template data completely (creation or full replacement)
Data = templateData;
} else {
// Merge new template data with existing data (incremental update)
Data = mergeTemplateData(templateData, aData);
}
// Render template with organization-specific context and data
const object = await renderObject(template, Data, req);
// Prepare data for database storage (remove UID from data to avoid duplication)
object.Data = { ...Data, UID: undefined };
// Set up translation structure for internationalization
if (object.Data.UIaction) {
// Initialize translation object if not present
object.Data.translate = object.Data.translate || {};
// Build translation keys from UI action structure
buildTranslateObject(object.Data.UIaction, object.Data.translate);
}
// Insert template to database
// Uses ON DUPLICATE KEY UPDATE for atomic create-or-update operation
await query(`
INSERT INTO ObjectBase(UID,Type,UIDBelongsTo,Title,Display,SortName,FullTextIndex,dindex,Data)
VALUES (?,'actionT',?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE Title=VALUE(Title),Display=VALUE(Display),SortName=VALUE(SortName),FullTextIndex=VALUE(FullTextIndex),dindex=VALUE(dindex),Data=VALUE(Data)
`, [UUID2hex(UIDaction), UUID2hex(req.session.root), object.Title, object.Display, object.SortIndex, object.FullTextIndex, object.dindex, JSON.stringify(object.Data)]);
// Notify UI clients that templates have been updated for this organization
// Uses WebSocket to provide real-time updates to connected clients
updateTemplate(req.session.root);
return {
success: true,
result: { ...object, UID: HEX2uuid(UIDaction) },
UIDaction
};
} catch (e) {
errorLoggerUpdate(e);
return { success: false, error: e.message };
}
};
/**
* Create a link between a bot and an action template
*
* This function establishes the relationship that allows a bot to execute
* actions based on a specific action template. The relationship is stored
* in the Links table and enables the bot discovery system.
*
* @param {string} botUID - Bot UUID that will be linked to the template
* @param {string} actionTemplateUID - Action template UUID to link
* @returns {Promise<Object>} Response object with success status
*
* Database operation:
* - Uses ON DUPLICATE KEY UPDATE to handle existing links gracefully
* - Links.UID = botUID (the bot that can execute actions)
* - Links.UIDTarget = actionTemplateUID (the template that defines the action)
* - Links.Type = 'actionT' (identifies this as an action template link)
*
* Use cases:
* - Bot registration: When a bot starts up and registers its capabilities
* - Template assignment: When administrators assign templates to bots
* - Multi-tenant isolation: Bots can only see templates they're linked to
*/
export const createBotLink = async (botUID, actionTemplateUID) => {
try {
// Create or update bot-to-action-template link
// ON DUPLICATE KEY UPDATE ensures idempotent operation
await query(`
INSERT IGNORE INTO Links (UID, Type, UIDTarget)
VALUES (?, 'actionT', ?)
`, [UUID2hex(botUID), UUID2hex(actionTemplateUID)]);
return { success: true };
} catch (e) {
errorLoggerUpdate(e);
return { success: false, error: e.message };
}
};
/**
* Delete an action template and all associated data
*
* This function performs a cascading delete of an action template, removing
* all related data to maintain database integrity. It's a comprehensive
* cleanup operation that affects multiple tables.
*
* @param {string} uid - Action template UUID to delete
* @param {string} orgUID - Organization UUID (for security validation)
* @returns {Promise<Object>} Response object with success status
*
* Cascading delete operations:
* 1. Delete the action template from ObjectBase
* 2. Remove bot links to this template from Links table
* 3. Delete all active actions created from this template
* 4. Notify UI clients of template changes
*
* Security measures:
* - Validates organization ownership (UIDBelongsTo=orgUID)
* - Prevents cross-organization template deletion
*
* Side effects:
* - All bots lose access to this template
* - All active actions from this template are removed
* - UI clients receive real-time update notifications
*/
export const deleteActionTemplate = async (uid, orgUID) => {
try {
const UID = UUID2hex(uid);
// Delete the action template from ObjectBase
// Security check: Only delete if template belongs to the specified organization
await query(`DELETE FROM ObjectBase WHERE UID=? AND Type='actionT'
AND UIDBelongsTo=?`, [UID, UUID2hex(orgUID)]);
// Remove bot links to this action template
// This prevents bots from trying to execute actions from deleted templates
await query(`DELETE FROM Links WHERE Type='actionT' AND UIDTarget=?`, [UID]);
// Delete all active actions that were created from this template
// Complex join operation to find and delete related action instances
query(`DELETE Links,ObjectBase
FROM Links
INNER JOIN ObjectBase ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='action' AND ObjectBase.Type='action')
WHERE Links.UID=?`, [UID]);
// Notify UI clients that templates have been updated for this organization
updateTemplate(orgUID);
return { success: true };
} catch (e) {
errorLoggerUpdate(e);
return { success: false, error: e.message };
}
};
/**
* Restart an action template
*
* This function sends a restart signal to the bots that defined
* the specified action template. It's used to update call the onStartup function
* of the bot part without requiring a full bot restart.
*
* @param {string} uid - Action template UUID to restart
* @returns {Promise<Object>} Response object with success status
*
* Operation mechanism:
* - Publishes a restart event to the Redis event system
* - Event format: `/restart/actionT/{templateUID}`
* - All subscribed bots receive the restart signal
*
* Bot behavior upon receiving restart signal:
* - Bots executed the onStartup function for the specified template
*
* Use cases:
* - Template logic has been modified
* - Template parameters have changed
* - Template permissions have been updated
* - Forcing bot cleanup related to the template
*/
export const restartActionTemplate = async (uid) => {
try {
// Publish restart event to Redis for all subscribed bots
// Event path follows pattern: /restart/actionT/{templateUID}
publishEvent(`/restart/actionT/${uid}`, {});
return { success: true };
} catch (e) {
errorLoggerUpdate(e);
return { success: false, error: e.message };
}
};