import { config } from '../../config.js' import { getDocument, listDocuments, Query } from '../appwriteAdmin.js' import { getFeatureDoc } from '../siteFeatures.js' import { unterkunftFuerKunde, unterkunftModus } from './schalter.js' import { ladeEinstellungen } from './speicher.js' /** * Kontext eines Buchungskalenders (Funktionsschluessel 'unterkunft'). * * ctx = { featureId, customerId, projectId, siteKey, state, besitzerEmail, einstellungen, project } * featureId ID des portalFeatures-Dokuments (zugleich ID der ukEinstellungen) * state Zustand der Funktion ('active' fuer die oeffentliche Schnittstelle) * besitzerEmail E-Mail des Kunden (leer, wenn der Kunde fehlt) * einstellungen ukEinstellungen (geparst) oder null, solange der Kunde noch nichts gespeichert hat * project websiteProjects-Dokument oder null * Ein ctx aus unterkunftPerSiteKey ist nur zum Lesen: innerhalb von 30 Sekunden kommt dasselbe Objekt zurueck. Der Schalter * (off | pilot | on) gilt bei jedem Aufruf, nicht erst nach Ablauf des Speichers. */ const FUNKTION = 'unterkunft' export const SITE_KEY_RE = /^[a-z2-7]{16}$/ const CACHE_MS = 30 * 1000 const CACHE_MAX = 10000 const cache = new Map() /** Wird mit jedem Leeren erhoeht: eine Abfrage, die davor begann, darf ihr Ergebnis nicht mehr merken. */ let generation = 0 /** 404 "Dokument fehlt" (nicht: Collection oder Datenbank fehlt). */ const dokumentFehlt = (err) => err?.status === 404 && err.type !== 'collection_not_found' && err.type !== 'database_not_found' async function holeOderNull(collectionId, id) { if (typeof id !== 'string' || id === '') return null try { return await getDocument(collectionId, id) } catch (err) { if (dokumentFehlt(err)) return null throw err } } async function bauKontext(doc) { const [kunde, project, einstellungen] = await Promise.all([ holeOderNull(config.collections.customers, doc.customerId), holeOderNull(config.collections.websiteProjects, doc.projectId), ladeEinstellungen(doc.$id), ]) return { featureId: doc.$id, customerId: doc.customerId ?? '', projectId: doc.projectId ?? '', siteKey: doc.siteKey ?? '', state: doc.state ?? '', besitzerEmail: String(kunde?.email ?? ''), einstellungen, project, } } /** * Kontext fuer die oeffentliche Schnittstelle: Funktion 'unterkunft' mit diesem siteKey im Zustand active. Modus off => null; * pilot => nur, wenn die Besitzer-E-Mail in der Pilotliste steht. Treffer und Fehlschlaege werden 30 Sekunden je siteKey gemerkt; * leereCache leert den Speicher. * @param {string} siteKey * @param {{ now?: Date }} [opt] now: Zeitpunkt fuer den Speicher (Tests) * @returns {Promise} null bei ungueltigem oder unbekanntem siteKey, anderem Zustand als active, Modus off und * Besitzer ausserhalb der Pilotliste */ export async function unterkunftPerSiteKey(siteKey, { now = new Date() } = {}) { if (typeof siteKey !== 'string' || !SITE_KEY_RE.test(siteKey)) return null const modus = unterkunftModus() if (modus === 'off') return null const nowMs = now instanceof Date ? now.getTime() : Number(now) if (!Number.isFinite(nowMs)) throw new TypeError('unterkunftPerSiteKey: now ist kein gueltiger Zeitpunkt') let ctx const gemerkt = cache.get(siteKey) if (gemerkt && gemerkt.bis > nowMs) { ctx = gemerkt.ctx } else { const stand = generation const [doc] = await listDocuments(config.collections.portalFeatures, [ Query.equal('siteKey', siteKey), Query.equal('featureKey', FUNKTION), Query.equal('state', 'active'), Query.limit(1), ]) ctx = doc && doc.state === 'active' ? await bauKontext(doc) : null if (stand === generation) merke(siteKey, ctx, nowMs) } if (!ctx) return null if (modus === 'pilot' && !unterkunftFuerKunde(ctx.besitzerEmail)) return null return ctx } /** * Kontext fuer das Portal: Funktion des Projekts in jedem Zustand ausser off (bestellt, Einrichtung, aktiv, abgeschaltet). * Immer frisch gelesen (kein Speicher). null, wenn das Projekt nicht dem Kunden gehoert, es keine Funktion gibt oder sie off ist. * @returns {Promise} */ export async function ladeFuerProjekt(customerId, projectId) { const project = await holeOderNull(config.collections.websiteProjects, projectId) if (!project || !customerId || project.customerId !== customerId) return null const doc = await getFeatureDoc(customerId, projectId, FUNKTION) if (!doc || doc.state === 'off') return null const ctx = await bauKontext(doc) return { ...ctx, project } } function merke(siteKey, ctx, nowMs) { if (cache.size >= CACHE_MAX) { for (const [key, eintrag] of cache) if (eintrag.bis <= nowMs) cache.delete(key) if (cache.size >= CACHE_MAX) cache.delete(cache.keys().next().value) } cache.set(siteKey, { bis: nowMs + CACHE_MS, ctx }) } /** Leert den Speicher von unterkunftPerSiteKey: fuer einen siteKey oder (ohne Angabe) ganz. Laufende Abfragen merken ihr Ergebnis nicht mehr. */ export function leereCache(siteKey) { generation++ if (siteKey === undefined) cache.clear() else cache.delete(siteKey) }