/**
* Addon Service — Business-Logik für Bot-registrierte UI-Addons
*
* Bots registrieren ihre Addons zur Laufzeit via PUT /api/addons.
* Die Addons werden NUR im Runtime-Config-Cache (`configs[app][orga].addons`)
* gespeichert — nicht in der statischen YAML-Datei in MinIO.
*
* Bei jedem Config-Reload (Backend-Restart, Admin-Upload) werden die
* Addons aus dem Cache gelöscht. Der Bot muss sie via onStartup (Bot-Restart)
* oder extraPatterns/onExtraPattern (Config-Change) neu registrieren.
*
* @module AddonService
*/
import { configs, remergeConfig } from '../../utils/compileTemplates.js'
import { updateConfig } from '../../server.ws.js'
import { publishEvent } from '../../utils/events.js'
/** Array-Felder, die von mehreren Bots befüllt werden können (Union) */
const ARRAY_FIELDS = ['menus', 'listContext', 'objectTypes']
/**
* Addon registrieren oder überschreiben (Upsert per name)
*
* @param {Object} params
* @param {string} params.url
* @param {string} params.name
* @param {string} [params.moduleName] - Name des Bot-Moduls (für RemoteAddon)
* @param {string} [params.modulePath] - Subpath für HMR-Import (z.B. "balance")
* @param {boolean} [params.remoteAddon] - true = RemoteAddon (Script-Load), false/undefined = Iframe-Addon
* @param {string[]} [params.menus]
* @param {string} [params.icon]
* @param {string} [params.mailLink]
* @param {string[]} [params.listContext]
* @param {string[]} [params.objectTypes]
* @param {string} params.UIDorga
* @param {string} params.app
* @param {boolean} [params.overwrite=false]
* @returns {{success: boolean, addon: Object}}
*/
export const upsertAddon = async ({ url, name, moduleName, modulePath, remoteAddon, menus, icon, mailLink, listContext, objectTypes, UIDorga, app, overwrite = false }) => {
// Config-Cache sicherstellen
if (!configs[app]) {
configs[app] = {}
}
if (!configs[app][UIDorga]) {
configs[app][UIDorga] = {}
}
// addons-Array initialisieren
if (!configs[app][UIDorga].addons) {
configs[app][UIDorga].addons = []
}
const existingIndex = configs[app][UIDorga].addons.findIndex(a => a.name === name)
if (existingIndex >= 0) {
if (overwrite) {
// Komplett ersetzen
configs[app][UIDorga].addons[existingIndex] = {
name,
url,
...(moduleName && { moduleName }),
...(modulePath && { modulePath }),
...(remoteAddon !== undefined && { remoteAddon }),
...(menus && { menus }),
...(icon && { icon }),
...(mailLink && { mailLink }),
...(listContext && { listContext }),
...(objectTypes && { objectTypes }),
}
} else {
// Merge: Bestehenden Eintrag erweitern statt ersetzen
const existing = configs[app][UIDorga].addons[existingIndex]
// Skalare Felder: nur überschreiben wenn explizit gesetzt
if (url) existing.url = url
if (moduleName !== undefined) existing.moduleName = moduleName
if (modulePath !== undefined) existing.modulePath = modulePath
if (remoteAddon !== undefined) existing.remoteAddon = remoteAddon
if (icon !== undefined) existing.icon = icon
if (mailLink !== undefined) existing.mailLink = mailLink
// Array-Felder: Union (keine Duplikate)
const arrayValues = { menus, listContext, objectTypes }
for (const field of ARRAY_FIELDS) {
const incoming = arrayValues[field]
if (Array.isArray(incoming)) {
if (!Array.isArray(existing[field])) {
existing[field] = [...incoming]
} else {
existing[field] = [...new Set([...existing[field], ...incoming])]
}
}
}
}
} else {
// Neuer Eintrag
const addonEntry = {
name,
url,
...(moduleName && { moduleName }),
...(modulePath && { modulePath }),
...(remoteAddon !== undefined && { remoteAddon }),
...(menus && { menus }),
...(icon && { icon }),
...(mailLink && { mailLink }),
...(listContext && { listContext }),
...(objectTypes && { objectTypes }),
}
configs[app][UIDorga].addons.push(addonEntry)
}
const addonEntry = configs[app][UIDorga].addons.find(a => a.name === name)
// Config im Frontend aktualisieren (WebSocket-Notify + Redis-Event)
remergeConfig(app, UIDorga)
updateConfig(app, UIDorga)
publishEvent(`/add/addon/${UIDorga}`, {
organization: UIDorga,
data: { addon: addonEntry }
})
return { success: true, addon: addonEntry }
}
/**
* Addon aus dem Config-Cache entfernen
*
* @param {Object} params
* @param {string} params.name
* @param {string} params.UIDorga
* @param {string} params.app
* @returns {{success: boolean, message?: string}}
*/
export const removeAddon = async ({ name, UIDorga, app }) => {
if (!configs[app] || !configs[app][UIDorga] || !configs[app][UIDorga].addons) {
return { success: false, message: `Keine addons für orga ${UIDorga} gefunden` }
}
const initialLength = configs[app][UIDorga].addons.length
configs[app][UIDorga].addons = configs[app][UIDorga].addons.filter(a => a.name !== name)
if (configs[app][UIDorga].addons.length === initialLength) {
return { success: false, message: `Addon '${name}' nicht gefunden` }
}
// Config im Frontend aktualisieren (WebSocket-Notify + Redis-Event)
remergeConfig(app, UIDorga)
updateConfig(app, UIDorga)
publishEvent(`/remove/addon/${UIDorga}`, {
organization: UIDorga,
data: { name }
})
return { success: true, message: `Addon '${name}' entfernt` }
}