import { getUserApps, getUserById, getUserDataGeneration, getUserPermissions, type AppDb, type UserRow, } from '@authportal/db' import { allPermissionKeys, normalizePermissionKeys } from '@authportal/shared' import { TtlCache } from './cache.js' export type UserProfile = { row: UserRow /** Сырые app_id из БД. */ apps: string[] /** Нормализованные права; у админа — полный каталог. */ permissions: string[] } type Entry = { profile: UserProfile; generation: number } /** Страховочный TTL: основная инвалидация — по счётчику поколения из @authportal/db. */ const PROFILE_TTL_MS = 10_000 const PROFILE_MAX = 500 const caches = new WeakMap>() function cacheFor(db: AppDb): TtlCache { let c = caches.get(db) if (!c) { c = new TtlCache({ max: PROFILE_MAX, ttlMs: PROFILE_TTL_MS }) caches.set(db, c) } return c } export function loadUserProfile(db: AppDb, id: string): UserProfile | undefined { const row = getUserById(db, id) if (!row) return undefined return profileFromRow(db, row) } function profileFromRow(db: AppDb, row: UserRow): UserProfile { const apps = getUserApps(db, row.id) const permissions = row.isAdmin ? allPermissionKeys() : normalizePermissionKeys(getUserPermissions(db, row.id)) return { row, apps, permissions } } /** * Профиль пользователя (строка + приложения + права) с кэшем: 3 SELECT на * запрос → 0 пока данные пользователей не менялись. */ export function getUserProfile( db: AppDb, id: string, ): UserProfile | undefined { const cache = cacheFor(db) const generation = getUserDataGeneration() const hit = cache.get(id) if (hit && hit.generation === generation) return hit.profile const profile = loadUserProfile(db, id) if (!profile) { cache.delete(id) return undefined } cache.set(id, { profile, generation }) return profile }