Activity logging now catches and logs errors without propagating them. This ensures activity logging failures never break primary operations. Updated return type to ActivityLog | null to indicate potential failure. Refs #339 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
585 lines
13 KiB
TypeScript
585 lines
13 KiB
TypeScript
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<ActivityLog | null> {
|
|
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<PaginatedActivityLogs> {
|
|
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<ActivityLogResult | null> {
|
|
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<ActivityLogResult[]> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
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<ActivityLog | null> {
|
|
return this.logActivity({
|
|
workspaceId,
|
|
userId,
|
|
action: ActivityAction.DELETED,
|
|
entityType: EntityType.IDEA,
|
|
entityId: ideaId,
|
|
...(details && { details }),
|
|
});
|
|
}
|
|
}
|