import { Injectable, Logger } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { ActivityAction, EntityType, Prisma, ActivityLog } from "@prisma/client"; import type { CreateActivityLogInput, PaginatedActivityLogs, ActivityLogResult, } from "./interfaces/activity.interface"; import type { QueryActivityLogDto } from "./dto"; /** * Service for managing activity logs and audit trails */ @Injectable() export class ActivityService { private readonly logger = new Logger(ActivityService.name); constructor(private readonly prisma: PrismaService) {} /** * Create a new activity log entry (fire-and-forget) * * Activity logging failures are logged but never propagate to callers. * This ensures activity logging never breaks primary operations. * * @returns The created ActivityLog or null if logging failed */ async logActivity(input: CreateActivityLogInput): Promise { try { return await this.prisma.activityLog.create({ data: input as unknown as Prisma.ActivityLogCreateInput, }); } catch (error) { // Log the error but don't propagate - activity logging is fire-and-forget this.logger.error( `Failed to log activity: action=${input.action} entityType=${input.entityType} entityId=${input.entityId}`, error instanceof Error ? error.stack : String(error) ); return null; } } /** * Get paginated activity logs with filters */ async findAll(query: QueryActivityLogDto): Promise { const page = query.page ?? 1; const limit = query.limit ?? 50; const skip = (page - 1) * limit; // Build where clause const where: Prisma.ActivityLogWhereInput = {}; if (query.workspaceId !== undefined) { where.workspaceId = query.workspaceId; } if (query.userId) { where.userId = query.userId; } if (query.action) { where.action = query.action; } if (query.entityType) { where.entityType = query.entityType; } if (query.entityId) { where.entityId = query.entityId; } if (query.startDate ?? query.endDate) { where.createdAt = {}; if (query.startDate) { where.createdAt.gte = query.startDate; } if (query.endDate) { where.createdAt.lte = query.endDate; } } // Execute queries in parallel const [data, total] = await Promise.all([ this.prisma.activityLog.findMany({ where, include: { user: { select: { id: true, name: true, email: true, }, }, }, orderBy: { createdAt: "desc", }, skip, take: limit, }), this.prisma.activityLog.count({ where }), ]); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; } /** * Get a single activity log by ID */ async findOne(id: string, workspaceId: string): Promise { return await this.prisma.activityLog.findUnique({ where: { id, workspaceId, }, include: { user: { select: { id: true, name: true, email: true, }, }, }, }); } /** * Get audit trail for a specific entity */ async getAuditTrail( workspaceId: string, entityType: EntityType, entityId: string ): Promise { return await this.prisma.activityLog.findMany({ where: { workspaceId, entityType, entityId, }, include: { user: { select: { id: true, name: true, email: true, }, }, }, orderBy: { createdAt: "asc", }, }); } // ============================================ // HELPER METHODS FOR COMMON ACTIVITY TYPES // ============================================ /** * Log task creation */ async logTaskCreated( workspaceId: string, userId: string, taskId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.CREATED, entityType: EntityType.TASK, entityId: taskId, ...(details && { details }), }); } /** * Log task update */ async logTaskUpdated( workspaceId: string, userId: string, taskId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.UPDATED, entityType: EntityType.TASK, entityId: taskId, ...(details && { details }), }); } /** * Log task deletion */ async logTaskDeleted( workspaceId: string, userId: string, taskId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.DELETED, entityType: EntityType.TASK, entityId: taskId, ...(details && { details }), }); } /** * Log task completion */ async logTaskCompleted( workspaceId: string, userId: string, taskId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.COMPLETED, entityType: EntityType.TASK, entityId: taskId, ...(details && { details }), }); } /** * Log task assignment */ async logTaskAssigned( workspaceId: string, userId: string, taskId: string, assigneeId: string ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.ASSIGNED, entityType: EntityType.TASK, entityId: taskId, details: { assigneeId }, }); } /** * Log event creation */ async logEventCreated( workspaceId: string, userId: string, eventId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.CREATED, entityType: EntityType.EVENT, entityId: eventId, ...(details && { details }), }); } /** * Log event update */ async logEventUpdated( workspaceId: string, userId: string, eventId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.UPDATED, entityType: EntityType.EVENT, entityId: eventId, ...(details && { details }), }); } /** * Log event deletion */ async logEventDeleted( workspaceId: string, userId: string, eventId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.DELETED, entityType: EntityType.EVENT, entityId: eventId, ...(details && { details }), }); } /** * Log project creation */ async logProjectCreated( workspaceId: string, userId: string, projectId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.CREATED, entityType: EntityType.PROJECT, entityId: projectId, ...(details && { details }), }); } /** * Log project update */ async logProjectUpdated( workspaceId: string, userId: string, projectId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.UPDATED, entityType: EntityType.PROJECT, entityId: projectId, ...(details && { details }), }); } /** * Log project deletion */ async logProjectDeleted( workspaceId: string, userId: string, projectId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.DELETED, entityType: EntityType.PROJECT, entityId: projectId, ...(details && { details }), }); } /** * Log workspace creation */ async logWorkspaceCreated( workspaceId: string, userId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.CREATED, entityType: EntityType.WORKSPACE, entityId: workspaceId, ...(details && { details }), }); } /** * Log workspace update */ async logWorkspaceUpdated( workspaceId: string, userId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.UPDATED, entityType: EntityType.WORKSPACE, entityId: workspaceId, ...(details && { details }), }); } /** * Log workspace member added */ async logWorkspaceMemberAdded( workspaceId: string, userId: string, memberId: string, role: string ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.CREATED, entityType: EntityType.WORKSPACE, entityId: workspaceId, details: { memberId, role }, }); } /** * Log workspace member removed */ async logWorkspaceMemberRemoved( workspaceId: string, userId: string, memberId: string ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.DELETED, entityType: EntityType.WORKSPACE, entityId: workspaceId, details: { memberId }, }); } /** * Log user profile update */ async logUserUpdated( workspaceId: string, userId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.UPDATED, entityType: EntityType.USER, entityId: userId, ...(details && { details }), }); } /** * Log domain creation */ async logDomainCreated( workspaceId: string, userId: string, domainId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.CREATED, entityType: EntityType.DOMAIN, entityId: domainId, ...(details && { details }), }); } /** * Log domain update */ async logDomainUpdated( workspaceId: string, userId: string, domainId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.UPDATED, entityType: EntityType.DOMAIN, entityId: domainId, ...(details && { details }), }); } /** * Log domain deletion */ async logDomainDeleted( workspaceId: string, userId: string, domainId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.DELETED, entityType: EntityType.DOMAIN, entityId: domainId, ...(details && { details }), }); } /** * Log idea creation */ async logIdeaCreated( workspaceId: string, userId: string, ideaId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.CREATED, entityType: EntityType.IDEA, entityId: ideaId, ...(details && { details }), }); } /** * Log idea update */ async logIdeaUpdated( workspaceId: string, userId: string, ideaId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.UPDATED, entityType: EntityType.IDEA, entityId: ideaId, ...(details && { details }), }); } /** * Log idea deletion */ async logIdeaDeleted( workspaceId: string, userId: string, ideaId: string, details?: Prisma.JsonValue ): Promise { return this.logActivity({ workspaceId, userId, action: ActivityAction.DELETED, entityType: EntityType.IDEA, entityId: ideaId, ...(details && { details }), }); } }