Source: utils/compileTemplates.js


import { UUID2hex, query} from '@commtool/sql-query';
import YAML from 'yaml'
import { getConfigFile, getConfigFiles } from '../config/configFile.js';
import _ from 'lodash'
import { doubleMetaphone } from 'double-metaphone';
import { isValidUID } from './UUIDs.js'
import { triggerConfigUpdates } from '../Router/configRoute/service.js';

export const Templates={}
export const BannerRules={}
export const configs={}
export const mergedConfigs={}


export const readConfig=async (file,app)=>
{

    //console.log(file)
    const configFile=await getConfigFile(file)
    let config
    try
    {
        config=YAML.parse(configFile)
    }
    catch (e)
    {
        return {success: false, message: e.message}
    }
    if(!config.UID)
        return {success: false, message: 'missing UID declaration  for the organisation in config file, you have to supply an UID under the label UID'}
    
    if(!isValidUID(config.UID))
        return {success: false, message: 'the supplied UID is not following our UID schmema regex: /^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}$/'}
    const UIDroot= config.UID 
    if(!configs[app])
        configs[app]={}
    configs[app][UIDroot]= config
    Templates[UIDroot]= _.merge(Templates[UIDroot],config.ObjectTemplates)
    if(app==='db' && configs.admin[UIDroot].Hierarchie)
    {
        const hierarchies=configs.admin[UIDroot].Hierarchie.length
        BannerRules[UIDroot]=config.BannerRules ? config.BannerRules : {group:Array(hierarchies).fill([]),member:[]}
    }
    // if app is admin, read extraConfig from the orgas database entry
    if(app==='admin')
    {
        const orgaData= await query(`SELECT Member.Data
            FROM Member
            INNER JOIN ObjectBase ON (Member.UID=ObjectBase.UIDBelongsTo AND ObjectBase.Type='Group')
            WHERE ObjectBase.UID=?`,
            [UUID2hex(UIDroot)],
            {cast:['json']})

        if(orgaData.length>0 && orgaData[0].Data && orgaData[0].Data.extraConfig)
        {
            configs.admin[UIDroot].UIconfig=orgaData[0].Data.extraConfig
        }
    }
    return {success: true, config}
}


/**
 * Vollstaendiger Config-Durchlauf: liest alle Orga-YAMLs (admin + app-spezifisch) und
 * baut `configs`/`mergedConfigs` von Grund auf neu auf.
 *
 * ACHTUNG: Setzt unterwegs `configs[app] = {}` zurueck und ist deshalb *nicht*
 * nebenlaeufig ausfuehrbar. Immer ueber `readAllConfigs()` bzw. `ensureConfigsLoaded()`
 * aufrufen — niemals direkt.
 */
const loadAllConfigs=async()=> {
    // Use default 'db' if env variable not set
    const currentApps = process.env.apiApps ? process.env.apiApps.split('|').filter(app => app !== 'admin') : ['db']; 
    const orgas= []
    const files =await getConfigFiles('admin')
    for(const file of files)
    {
        if(file.match(/config\/admin\/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}\.yaml$/) )
        {
            const myOrga= file.replace('config/admin/','').replace('.yaml','')
            const result= await readConfig(file,'admin')
            if(!result.success)
            {
                console.error(`[readAllConfigs] Failed to load admin config '${file}': ${result.message} — skipping org ${myOrga}`)
                continue
            }
            orgas.push(myOrga)
            // store the sysUser for this orga, if already availlable
            const sysUser= await query(`SELECT ObjectBase.UID, SysUser.Data,  SysUser.Display
                FROM  Member AS SysUser
                INNER JOIN ObjectBase ON (SysUser.UID=ObjectBase.UIDBelongsTo AND ObjectBase.Type='extern')
                INNER JOIN Links ON (ObjectBase.UID= Links.UID AND Links.UIDTarget=? AND Links.Type = 'memberSys')
                `,
                [UUID2hex(myOrga)],
                {cast:['json','UUID']})
            if(sysUser.length>0)
                configs.admin[myOrga].sysUser=sysUser[0].UID
            // store the loginOrga for this orga, if already availlable
            const loginOrga= await query(`SELECT Links.UID
                FROM Links
                WHERE Links.Type = 'identifyer' AND Links.UIDTarget=?
                `,
                [UUID2hex(myOrga)],
                {cast:['json','UUID']})
            if(loginOrga.length>0)
                configs.admin[myOrga].loginOrga=loginOrga[0].UID
        }
        else
        {
            // Sonst verschwindet eine Config lautlos, ohne dass irgendwo steht warum.
            console.warn(`[readAllConfigs] Ignoring admin config '${file}': file name is not config/admin/UUID-<valid uuid>.yaml`)
        }
    }
    
    for(const app of currentApps)
    {
        const files =await getConfigFiles(app)
        configs[app]={}
        for(const orga of orgas)
        {
            if(files.includes('config/'+app+'/'+orga+'.yaml'))
            {
                const result= await readConfig('config/'+app+'/'+orga+'.yaml',app)
                if(!result.success)
                    console.error(`[readAllConfigs] Failed to load '${app}' config for org ${orga}: ${result.message}`)
            }
            else
            {
                configs[app][orga]=null
            }
        }
    }
    // now create the merged configs
    for(const app of currentApps)
    {
        mergedConfigs[app]={}
        for(const orga of orgas)
        {
            remergeConfig(app,orga)
        }
    }
    return orgas
}

/**
 * Gemeinsames Init-Promise: `loadAllConfigs()` darf nur einmal gleichzeitig laufen.
 *
 * Vorher guardete `getConfig()` nur mit `if(!configs.admin)` (Check-then-Act). Da ein
 * Durchlauf Dateien aus S3 liest und DB-Queries fahrt, sahen parallele Requests denselben
 * unfertigen Zustand, starteten jeweils einen eigenen Durchlauf, und der zweite Lauf
 * leerte `configs[app] = {}` mitten im ersten. Ergebnis waren sporadische "Table 'Member'
 * doesn't exist" bzw. unvollstaendige `mergedConfigs`.
 *
 * Das Promise wird memoisiert: Boot (server.js) und Lazy-Init aus `getConfig()` teilen
 * sich genau einen Durchlauf. Nach Erfolg bleibt es bestehen — ein erneuter Voll-Durchlauf
 * ist nicht noetig, Config-Aenderungen laufen ueber `remergeConfig()` (siehe
 * `triggerConfigUpdates`). Nur nach einem Fehler wird zurueckgesetzt, damit ein Retry
 * moeglich bleibt.
 */
let configLoadPromise = null
let configsLoaded = false

export const readAllConfigs=()=> {
    if(!configLoadPromise)
        configLoadPromise = loadAllConfigs()
            .then(orgas => { configsLoaded = true; return orgas })
            .catch(err => { configLoadPromise = null; throw err })
    return configLoadPromise
}

/**
 * Wartet, bis der Config-Bestand vollstaendig geladen ist. Race-frei und idempotent:
 * parallele Aufrufe haengen sich an denselben Durchlauf, spaetere Aufrufe sind ein No-Op.
 * @returns {Promise<void>}
 */
export const ensureConfigsLoaded=()=> {
    if(configsLoaded)
        return Promise.resolve()
    return readAllConfigs().then(() => undefined)
}

export const remergeConfig=(app,orga)=>
{
    if(configs[app][orga]) 
    {
        mergedConfigs[app][orga]=_.merge({},configs['admin'][orga] ,configs[app][orga])
    }  
}

export const getConfig=async (app,req)=>
{
    // this should have req or root
    let root= req.session?.root ? req.session.root : req.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}$/) ? req : null
    if(!root) {
        console.warn(`[getConfig] Could not find root in request for app '${app}'`)
        return null
    }
    
    try {
        // `configs[app]` ist erst nach dem *kompletten* Durchlauf vollstaendig — `configs.admin`
        // wird dagegen schon nach der ersten Orga truthy. Deshalb am Promise haengen, nicht am Objekt.
        await ensureConfigsLoaded()
        
        if(app==='admin') {
            if(!configs.admin[root]) {
                console.warn(`[getConfig] No admin config found for org ${root}`)
                return null
            }
            return configs.admin[root]
        }
        else {
            if(!configs[app] || !configs[app][root]) {
                if(configs.admin && configs.admin[root]) {
                    console.warn(`[getConfig] No '${app}' config found for org ${root}, falling back to admin config`)
                    return configs.admin[root]
                } else {
                    console.warn(`[getConfig] No '${app}' config found for org ${root}`);
                    return null;
                }
            }
            // make sure admin can not override root organisation UID in the app specific config, which would break the auth flow and cause security issues. Also make sure, that the orgaUser is always taken from the admin config, so that the UI shows the correct name and permissions, even if the app specific config is missing these fields    
            mergedConfigs.UID=root
            return mergedConfigs[app][root]
        }
    }
    catch(e) {
        console.error(`[getConfig] Error loading config for app '${app}', org '${root}':`, e)
        return null
    }
}




export const storeTemplates=(UIDroot,req)=>
{
    if (!Templates[UIDroot])
    {
        Templates[UIDroot]={...configs.db[UIDroot].ObjectTemplates, ...configs.locations[UIDroot].ObjectTemplates,...configs.events[UIDroot].ObjectTemplates}
    }
    if (!BannerRules[UIDroot])
    {
        const myConfig=configs.db[UIDroot]
        const hierarchies=myConfig.Hierarchie.length
        BannerRules[UIDroot]=myConfig.BannerRules ? myConfig.BannerRules : {group:Array(hierarchies).fill([]),member:[]}
    }
   
}




export const phonetik=(str)=>
{
    if(str && typeof str==='string')
    {
        const results=str.split(/\s/)
        const result=results.map(s=>doubleMetaphone(s.replace('ß','ss')).join(' ')).join(' ')
        return result
    }
    else
    {
        return ''
    }
}

export const phonetikArray=(strArray)=>
{
    return strArray.map(str=>phonetik(str)).join(' ')

}

// add extra configs to admin app orgaData
export const addExtraConfigToOrgaData=(UIDorga,extraConfig)=>
{
   configs.admin[UIDorga].UIconfig= extraConfig
   // trigger the UIS config update across apps
   triggerConfigUpdates('admin', UIDorga)
   
}