Source: RouterAddons/addons/service.js

/**
 * 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` }
}