import { Repository, ILike, FindOptionsWhere } from 'typeorm'; import { SystemSetting } from '../entities'; import { UpdateSystemSettingDto, CreateSystemSettingDto, SystemSettingsFiltersDto } from '../dto'; import { logger } from '../../../shared/utils/logger.js'; /** * Cache entry for system settings */ interface CacheEntry { value: any; expiresAt: number; } /** * SystemSettingsService * * Manages global system settings that apply across all tenants. * Implements in-memory caching for performance optimization. * * Features: * - CRUD operations for system settings * - In-memory cache with TTL * - Category-based filtering * - Reset to default values */ export class SystemSettingsService { private cache: Map = new Map(); private cacheTtlMs: number = 5 * 60 * 1000; // 5 minutes default constructor(private readonly settingRepository: Repository) {} /** * Sets the cache TTL in milliseconds */ setCacheTtl(ttlMs: number): void { this.cacheTtlMs = ttlMs; } /** * Clears the entire cache */ clearCache(): void { this.cache.clear(); logger.debug('System settings cache cleared'); } /** * Clears a specific key from cache */ clearCacheKey(key: string): void { this.cache.delete(key); this.cache.delete(`all:${key}`); logger.debug('System settings cache key cleared', { key }); } /** * Get all system settings with optional filtering */ async getAll(filters: SystemSettingsFiltersDto = {}): Promise { const { category, isPublic, isEditable, search } = filters; const where: FindOptionsWhere[] = []; const baseWhere: FindOptionsWhere = {}; if (category) { baseWhere.category = category; } if (isPublic !== undefined) { baseWhere.isPublic = isPublic; } if (isEditable !== undefined) { baseWhere.isEditable = isEditable; } if (search) { where.push( { ...baseWhere, key: ILike(`%${search}%`) }, { ...baseWhere, description: ILike(`%${search}%`) } ); } else { where.push(baseWhere); } const settings = await this.settingRepository.find({ where: where.length > 0 ? where : undefined, order: { category: 'ASC', key: 'ASC' }, }); return settings; } /** * Get all public system settings (for unauthenticated access) */ async getPublicSettings(): Promise { const cacheKey = 'public:all'; const cached = this.getFromCache(cacheKey); if (cached !== undefined) { return cached; } const settings = await this.settingRepository.find({ where: { isPublic: true }, order: { category: 'ASC', key: 'ASC' }, }); this.setCache(cacheKey, settings); return settings; } /** * Get settings by category */ async getByCategory(category: string): Promise { const cacheKey = `category:${category}`; const cached = this.getFromCache(cacheKey); if (cached !== undefined) { return cached; } const settings = await this.settingRepository.find({ where: { category }, order: { key: 'ASC' }, }); this.setCache(cacheKey, settings); return settings; } /** * Get a single setting by key */ async getByKey(key: string): Promise { const cacheKey = `key:${key}`; const cached = this.getFromCache(cacheKey); if (cached !== undefined) { return cached; } const setting = await this.settingRepository.findOne({ where: { key }, }); if (setting) { this.setCache(cacheKey, setting); } return setting; } /** * Get the value of a setting by key */ async getValue(key: string): Promise { const setting = await this.getByKey(key); return setting?.value ?? null; } /** * Update a system setting by key */ async update( key: string, dto: UpdateSystemSettingDto, updatedBy?: string ): Promise { const setting = await this.settingRepository.findOne({ where: { key } }); if (!setting) { logger.warn('System setting not found for update', { key }); return null; } if (!setting.isEditable) { logger.warn('Attempted to update non-editable setting', { key }); throw new Error(`Setting '${key}' is not editable`); } Object.assign(setting, { ...dto, updatedBy, }); const updated = await this.settingRepository.save(setting); this.clearCacheKey(key); this.cache.delete('public:all'); this.cache.delete(`category:${setting.category}`); logger.info('System setting updated', { key, updatedBy }); return updated; } /** * Reset a setting to its default value */ async reset(key: string, updatedBy?: string): Promise { const setting = await this.settingRepository.findOne({ where: { key } }); if (!setting) { logger.warn('System setting not found for reset', { key }); return null; } if (setting.defaultValue === null || setting.defaultValue === undefined) { logger.warn('Setting has no default value', { key }); throw new Error(`Setting '${key}' has no default value`); } setting.value = setting.defaultValue; setting.updatedBy = updatedBy ?? null; const updated = await this.settingRepository.save(setting); this.clearCacheKey(key); this.cache.delete('public:all'); this.cache.delete(`category:${setting.category}`); logger.info('System setting reset to default', { key, updatedBy }); return updated; } /** * Create a new system setting (admin only) */ async create(dto: CreateSystemSettingDto, createdBy?: string): Promise { const existing = await this.settingRepository.findOne({ where: { key: dto.key } }); if (existing) { throw new Error(`Setting with key '${dto.key}' already exists`); } const setting = this.settingRepository.create({ ...dto, updatedBy: createdBy, }); const created = await this.settingRepository.save(setting); this.clearCache(); logger.info('System setting created', { key: dto.key, createdBy }); return created; } /** * Get all unique categories */ async getCategories(): Promise { const cacheKey = 'categories:all'; const cached = this.getFromCache(cacheKey); if (cached !== undefined) { return cached; } const result = await this.settingRepository .createQueryBuilder('setting') .select('DISTINCT setting.category', 'category') .orderBy('setting.category', 'ASC') .getRawMany(); const categories = result.map((r: { category: string }) => r.category); this.setCache(cacheKey, categories); return categories; } /** * Cache helper: get from cache if not expired */ private getFromCache(key: string): any { const entry = this.cache.get(key); if (entry && entry.expiresAt > Date.now()) { return entry.value; } if (entry) { this.cache.delete(key); } return undefined; } /** * Cache helper: set value in cache with TTL */ private setCache(key: string, value: any): void { this.cache.set(key, { value, expiresAt: Date.now() + this.cacheTtlMs, }); } }