import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { PaginatedResponse } from '@edr/types'; import { AuditLog } from './entities/audit-log.entity'; import { AuditLogRepository } from './audit-log.repository'; import { AuditLogQueryDto } from './dto/audit-log-query.dto'; import { buildPaginationMeta, normalizePagination, } from '../../common/utils/pagination.util'; @Injectable() export class AuditService { private readonly logger = new Logger(AuditService.name); constructor(private readonly auditLogRepository: AuditLogRepository) {} /** * Persist one audit row, swallowing any failure. * * An audit write must never turn a successful business action into an error * for the user: if this table is full, misconfigured or mid-migration, * contract approvals still need to work. Failures are logged so the gap is * visible in application logs rather than silent. */ async record(entry: Partial): Promise { try { await this.auditLogRepository.record(entry); } catch (error) { this.logger.error( `Failed to write audit log for ${entry.method} ${entry.routePath}: ${ error instanceof Error ? error.message : String(error) }`, ); } } /** Paginated, filtered audit history, newest first. */ async search(query: AuditLogQueryDto): Promise> { const { page, pageSize, skip, take } = normalizePagination(query); const from = query.from ? new Date(query.from) : undefined; const to = query.to ? new Date(query.to) : undefined; // A reversed range silently returns zero rows, which reads as "nothing // happened" rather than "your filter is wrong" — reject it explicitly. if (from && to && from > to) { throw new BadRequestException('`from` must be earlier than `to`'); } const [items, total] = await this.auditLogRepository.search({ type: query.type, userId: query.userId, method: query.method, resourceId: query.resourceId, isSuccess: query.isSuccess === undefined ? undefined : query.isSuccess === 'true', from, to, skip, take, }); return { items, meta: buildPaginationMeta(total, page, pageSize) }; } /** Distinct entity types, for the filter dropdown on the audit screen. */ async listTypes(): Promise { return this.auditLogRepository.distinctTypes(); } }