124 lines
5.0 KiB
JavaScript
124 lines
5.0 KiB
JavaScript
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<object|null>} 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<object|null>}
|
|
*/
|
|
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)
|
|
}
|