import { BaseEntityService } from '../BaseEntityService'; import { IndexedDBContext } from '../IndexedDBContext'; import { IAuditEntry } from '../../types/AuditTypes'; import { EntityType, IEventBus } from '../../types/CalendarTypes'; import { CoreEvents } from '../../constants/CoreEvents'; import { IEntitySavedPayload, IEntityDeletedPayload, IAuditLoggedPayload } from '../../types/EventTypes'; /** * AuditService - Entity service for audit entries * * RESPONSIBILITIES: * - Store audit entries in IndexedDB * - Listen for ENTITY_SAVED/ENTITY_DELETED events * - Create audit entries for all entity changes * - Emit AUDIT_LOGGED after saving (for SyncManager to listen) * * OVERRIDE PATTERN: * - Overrides save() to NOT emit events (prevents infinite loops) * - AuditService saves audit entries without triggering more audits * * EVENT CHAIN: * Entity change → ENTITY_SAVED/DELETED → AuditService → AUDIT_LOGGED → SyncManager */ export class AuditService extends BaseEntityService { readonly storeName = 'audit'; readonly entityType: EntityType = 'Audit'; // Hardcoded userId for now - will come from session later private static readonly DEFAULT_USER_ID = '00000000-0000-0000-0000-000000000001'; constructor(context: IndexedDBContext, eventBus: IEventBus) { super(context, eventBus); this.setupEventListeners(); } /** * Setup listeners for ENTITY_SAVED and ENTITY_DELETED events */ private setupEventListeners(): void { // Listen for entity saves (create/update) this.eventBus.on(CoreEvents.ENTITY_SAVED, (event: Event) => { const detail = (event as CustomEvent).detail; this.handleEntitySaved(detail); }); // Listen for entity deletes this.eventBus.on(CoreEvents.ENTITY_DELETED, (event: Event) => { const detail = (event as CustomEvent).detail; this.handleEntityDeleted(detail); }); } /** * Handle ENTITY_SAVED event - create audit entry */ private async handleEntitySaved(payload: IEntitySavedPayload): Promise { // Don't audit audit entries (prevent infinite loops) if (payload.entityType === 'Audit') return; const auditEntry: IAuditEntry = { id: crypto.randomUUID(), entityType: payload.entityType, entityId: payload.entityId, operation: payload.operation, userId: AuditService.DEFAULT_USER_ID, timestamp: payload.timestamp, changes: payload.changes, synced: false, syncStatus: 'pending' }; await this.save(auditEntry); } /** * Handle ENTITY_DELETED event - create audit entry */ private async handleEntityDeleted(payload: IEntityDeletedPayload): Promise { // Don't audit audit entries (prevent infinite loops) if (payload.entityType === 'Audit') return; const auditEntry: IAuditEntry = { id: crypto.randomUUID(), entityType: payload.entityType, entityId: payload.entityId, operation: 'delete', userId: AuditService.DEFAULT_USER_ID, timestamp: payload.timestamp, changes: { id: payload.entityId }, // For delete, just store the ID synced: false, syncStatus: 'pending' }; await this.save(auditEntry); } /** * Override save to NOT trigger ENTITY_SAVED event * Instead, emits AUDIT_LOGGED for SyncManager to listen * * This prevents infinite loops: * - BaseEntityService.save() emits ENTITY_SAVED * - AuditService listens to ENTITY_SAVED and creates audit * - If AuditService.save() also emitted ENTITY_SAVED, it would loop */ async save(entity: IAuditEntry): Promise { const serialized = this.serialize(entity); return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); const request = store.put(serialized); request.onsuccess = () => { // Emit AUDIT_LOGGED instead of ENTITY_SAVED const payload: IAuditLoggedPayload = { auditId: entity.id, entityType: entity.entityType, entityId: entity.entityId, operation: entity.operation, timestamp: entity.timestamp }; this.eventBus.emit(CoreEvents.AUDIT_LOGGED, payload); resolve(); }; request.onerror = () => { reject(new Error(`Failed to save audit entry ${entity.id}: ${request.error}`)); }; }); } /** * Override delete to NOT trigger ENTITY_DELETED event * Audit entries should never be deleted (compliance requirement) */ async delete(_id: string): Promise { throw new Error('Audit entries cannot be deleted (compliance requirement)'); } /** * Get pending audit entries (for sync) */ async getPendingAudits(): Promise { return this.getBySyncStatus('pending'); } /** * Get audit entries for a specific entity */ async getByEntityId(entityId: string): Promise { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); const index = store.index('entityId'); const request = index.getAll(entityId); request.onsuccess = () => { const entries = request.result as IAuditEntry[]; resolve(entries); }; request.onerror = () => { reject(new Error(`Failed to get audit entries for entity ${entityId}: ${request.error}`)); }; }); } }