/**
* Helper functions for Action Template operations
*/
/**
* Recursively extract defaultValue entries from UIaction form field definitions.
* Returns a flat object mapping field name → defaultValue.
*
* @param {Object} node - A UIaction node (e.g. { "Form.Input": { paras: { name, defaultValue } } })
* @returns {Object} Map of field name → default value
*/
export const extractUIDefaults = (node) => {
const defaults = {};
if (!node || typeof node !== 'object') return defaults;
for (const [comp, config] of Object.entries(node)) {
if (config?.paras?.name && config.paras.defaultValue != null) {
defaults[config.paras.name] = config.paras.defaultValue;
}
if (Array.isArray(config?.content)) {
config.content.forEach(child => Object.assign(defaults, extractUIDefaults(child)));
}
if (Array.isArray(config?.children)) {
config.children.forEach(child => Object.assign(defaults, extractUIDefaults(child)));
}
}
return defaults;
};
/**
* Recursively builds a translation object by extracting translatable properties
* (content, label, placeholder, html) from a serialized input object and its nested
* children. Ensures that existing entries in the translation object are not overridden.
*
* @param {Object} serialized - The input object containing components and their properties.
* @param {Object} translateObject - The object to store translations, mapping keys to values.
*
* @example
* const serialized = {
* component1: {
* paras: { content: "Hello", label: "Greeting", html: "<p>Welcome</p>" },
* children: [
* { component2: { paras: { content: "World", label: "Planet" } } }
* ]
* }
* };
* const translateObject = {};
* buildTranslateObject(serialized, translateObject);
* console.log(translateObject);
* // { Hello: "Hello", Greeting: "Greeting", "<p>Welcome</p>": "<p>Welcome</p>", World: "World", Planet: "Planet" }
*/
export const buildTranslateObject = (serialized, translateObject) => {
const [[component, componentObject]] = Object.entries(serialized);
if (componentObject.paras) {
const paras = componentObject.paras;
if (paras.content)
translateObject[paras.content] = translateObject[paras.content] ? translateObject[paras.content] : paras.content; // do not override existing entries
if (paras.label)
translateObject[paras.label] = translateObject[paras.label] ? translateObject[paras.label] : paras.label; // do not override existing entries
if (paras.placeholder)
translateObject[paras.placeholder] = translateObject[paras.placeholder] ? translateObject[paras.placeholder] : paras.placeholder; // do not override existing entries
if (paras.html)
translateObject[paras.html] = translateObject[paras.html] ? translateObject[paras.html] : paras.html; // do not override existing entries
}
if (componentObject.content) {
const children = componentObject.content;
// now call it for further content
if (Array.isArray(children)) {
children.forEach((c, Index) => {
buildTranslateObject(c, translateObject);
});
}
else if (typeof children === 'object')
buildTranslateObject(children, translateObject);
}
};
/**
* Validates input for register endpoint
* @param {Object} body - Request body
* @returns {Object} Validation result with success flag and message
*/
export const validateRegisterInput = (body) => {
const { botUID, organizationUIDs = [], template = {} } = body;
if (!botUID || !botUID.match(/^UUID\-[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[1-5][0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}/)) {
return {
success: false,
message: 'botUID is required in the UUID format'
};
}
if (!Array.isArray(organizationUIDs) ) {
return {
success: false,
message: 'organizationUIDs must be an array'
};
}
if (organizationUIDs.length === 0) {
return {
success: false,
message: 'organizationUIDs array cannot be empty'
};
}
if (!template || (typeof template === 'object' && Object.keys(template).length === 0)) {
return {
success: false,
message: 'template cannot be empty'
};
}
return { success: true };
};
/**
* Merges existing template data with new data, preserving customizations.
*
* Default values support optional version-based enforcement:
* - The bot may supply a `defaultVersions` object alongside `defaults`,
* mapping each key to a monotonically increasing version number.
* - On re-registration a key is overwritten only when the bot's version is
* strictly greater than the stored version.
* - Admin-edited values are preserved as long as the bot does not bump the
* version for that key.
* - Templates that do not supply `defaultVersions` continue to work as
* before: existing values are always preserved (backwards-compatible).
* - `defaultsOriginal` is always overwritten with the bot's current defaults
* so the admin UI can offer a "reset to bot default" action at any time.
*
* @param {Object} templateData - New template data (from the bot)
* @param {Object} existingData - Existing template data (from the database)
* @returns {Object} Merged data
*/
export const mergeTemplateData = (templateData, existingData) => {
// Guard against undefined inputs
if (!templateData || !existingData) {
return templateData || existingData || {};
}
// Start with new template data
const Data = { ...templateData };
// Preserve existing translate only (admin customization)
// name and description come from the bot and should be updated on re-registration
if (existingData.translate !== undefined) {
Data.translate = existingData.translate;
}
// Merge defaults with version-based conflict resolution.
// For each key:
// botVersion > storedVersion → bot value wins (forced update)
// botVersion <= storedVersion → existing value preserved (admin customization)
if (templateData.defaults) {
const botVersions = templateData.defaultVersions || {};
const storedVersions = existingData.defaultVersions || {};
const mergedDefaults = { ...(existingData.defaults || {}) };
const mergedVersions = { ...storedVersions };
const storedOriginals = existingData.defaultsOriginal || {};
for (const [key, value] of Object.entries(templateData.defaults)) {
const botVersion = botVersions[key] ?? 0;
const storedVersion = storedVersions[key] ?? 0;
const isNewKey = !(key in mergedDefaults);
// Admin has not customised this key if the stored value still matches
// what the bot originally wrote (tracked in defaultsOriginal).
const adminUntouched = !isNewKey && (key in storedOriginals) && mergedDefaults[key] === storedOriginals[key];
if (isNewKey || adminUntouched || botVersion > storedVersion) {
// • New key → always add
// • Admin untouched → bot can update freely (no version bump needed)
// • Version bumped → bot forces overwrite of admin customisation
mergedDefaults[key] = value;
mergedVersions[key] = botVersion;
}
// Ensure version is tracked even when not forcing
if (mergedVersions[key] === undefined) {
mergedVersions[key] = botVersion;
}
}
// Remove stale defaults — keys the bot no longer ships are deleted
for (const key of Object.keys(mergedDefaults)) {
if (!(key in templateData.defaults)) {
delete mergedDefaults[key];
delete mergedVersions[key];
}
}
Data.defaults = mergedDefaults;
Data.defaultVersions = mergedVersions;
// Always store the bot's original defaults so admins can reset their customisations
Data.defaultsOriginal = { ...templateData.defaults };
} else if (existingData.defaults) {
// No new defaults from the bot — keep all existing
Data.defaults = existingData.defaults;
if (existingData.defaultVersions !== undefined) {
Data.defaultVersions = existingData.defaultVersions;
}
if (existingData.defaultsOriginal !== undefined) {
Data.defaultsOriginal = existingData.defaultsOriginal;
}
}
// Prune stale translation keys — entries whose source string no longer
// appears in the bot's UIaction are removed from the stored translate map.
// Only runs when the bot sends a UIaction (guards against partial updates).
if (Data.translate && templateData.UIaction && Object.keys(templateData.UIaction).length > 0) {
const validKeys = {};
buildTranslateObject(templateData.UIaction, validKeys);
for (const key of Object.keys(Data.translate)) {
if (!(key in validKeys)) {
delete Data.translate[key];
}
}
}
return Data;
};