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)
}