refactor(api): extract audit log router helpers
This commit is contained in:
@@ -0,0 +1,307 @@
|
|||||||
|
type AuditUser = { id: string; name: string | null; email: string | null } | null | undefined;
|
||||||
|
|
||||||
|
type AuditEntryShape = {
|
||||||
|
id: string;
|
||||||
|
entityType: string;
|
||||||
|
entityId: string;
|
||||||
|
entityName?: string | null;
|
||||||
|
action: string;
|
||||||
|
userId?: string | null;
|
||||||
|
source?: string | null;
|
||||||
|
summary?: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
user?: AuditUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AuditDetailEntryShape = AuditEntryShape & {
|
||||||
|
changes?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuditListInput = {
|
||||||
|
entityType?: string;
|
||||||
|
entityId?: string;
|
||||||
|
userId?: string;
|
||||||
|
action?: string;
|
||||||
|
source?: string;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
search?: string;
|
||||||
|
limit: number;
|
||||||
|
cursor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuditTimelineInput = {
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
limit: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatAuditListEntry(entry: AuditEntryShape) {
|
||||||
|
return {
|
||||||
|
id: entry.id,
|
||||||
|
entityType: entry.entityType,
|
||||||
|
entityId: entry.entityId,
|
||||||
|
entityName: entry.entityName ?? null,
|
||||||
|
action: entry.action,
|
||||||
|
userId: entry.userId ?? null,
|
||||||
|
source: entry.source ?? null,
|
||||||
|
summary: entry.summary ?? null,
|
||||||
|
createdAt: entry.createdAt.toISOString(),
|
||||||
|
user: entry.user
|
||||||
|
? {
|
||||||
|
id: entry.user.id,
|
||||||
|
name: entry.user.name,
|
||||||
|
email: entry.user.email,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAuditDetailEntry(entry: AuditDetailEntryShape) {
|
||||||
|
return {
|
||||||
|
...formatAuditListEntry(entry),
|
||||||
|
changes: entry.changes ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toAuditListInput(input: {
|
||||||
|
entityType?: string | undefined;
|
||||||
|
entityId?: string | undefined;
|
||||||
|
userId?: string | undefined;
|
||||||
|
action?: string | undefined;
|
||||||
|
source?: string | undefined;
|
||||||
|
startDate?: Date | undefined;
|
||||||
|
endDate?: Date | undefined;
|
||||||
|
search?: string | undefined;
|
||||||
|
limit: number;
|
||||||
|
cursor?: string | undefined;
|
||||||
|
}): AuditListInput {
|
||||||
|
return {
|
||||||
|
limit: input.limit,
|
||||||
|
...(input.entityType !== undefined ? { entityType: input.entityType } : {}),
|
||||||
|
...(input.entityId !== undefined ? { entityId: input.entityId } : {}),
|
||||||
|
...(input.userId !== undefined ? { userId: input.userId } : {}),
|
||||||
|
...(input.action !== undefined ? { action: input.action } : {}),
|
||||||
|
...(input.source !== undefined ? { source: input.source } : {}),
|
||||||
|
...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
|
||||||
|
...(input.endDate !== undefined ? { endDate: input.endDate } : {}),
|
||||||
|
...(input.search !== undefined ? { search: input.search } : {}),
|
||||||
|
...(input.cursor !== undefined ? { cursor: input.cursor } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toAuditTimelineInput(input: {
|
||||||
|
startDate?: Date | undefined;
|
||||||
|
endDate?: Date | undefined;
|
||||||
|
limit: number;
|
||||||
|
}): AuditTimelineInput {
|
||||||
|
return {
|
||||||
|
limit: input.limit,
|
||||||
|
...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
|
||||||
|
...(input.endDate !== undefined ? { endDate: input.endDate } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCreatedAtWhere(startDate?: Date, endDate?: Date) {
|
||||||
|
if (!startDate && !endDate) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdAt: Record<string, Date> = {};
|
||||||
|
if (startDate) createdAt.gte = startDate;
|
||||||
|
if (endDate) createdAt.lte = endDate;
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAuditListWhere(input: Omit<AuditListInput, "limit" | "cursor">) {
|
||||||
|
const { entityType, entityId, userId, action, source, startDate, endDate, search } = input;
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (entityType) where.entityType = entityType;
|
||||||
|
if (entityId) where.entityId = entityId;
|
||||||
|
if (userId) where.userId = userId;
|
||||||
|
if (action) where.action = action;
|
||||||
|
if (source) where.source = source;
|
||||||
|
|
||||||
|
const createdAt = buildCreatedAtWhere(startDate, endDate);
|
||||||
|
if (createdAt) {
|
||||||
|
where.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ entityName: { contains: search, mode: "insensitive" } },
|
||||||
|
{ summary: { contains: search, mode: "insensitive" } },
|
||||||
|
{ entityType: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startDate && !endDate && !entityId) {
|
||||||
|
const thirtyDaysAgo = new Date();
|
||||||
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
|
where.createdAt = { ...(where.createdAt as Record<string, Date> | undefined), gte: thirtyDaysAgo };
|
||||||
|
}
|
||||||
|
|
||||||
|
return where;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAuditEntries(
|
||||||
|
db: { auditLog: { findMany: Function } },
|
||||||
|
input: AuditListInput,
|
||||||
|
) {
|
||||||
|
const items = await db.auditLog.findMany({
|
||||||
|
where: buildAuditListWhere(input),
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
entityType: true,
|
||||||
|
entityId: true,
|
||||||
|
entityName: true,
|
||||||
|
action: true,
|
||||||
|
userId: true,
|
||||||
|
source: true,
|
||||||
|
summary: true,
|
||||||
|
createdAt: true,
|
||||||
|
user: { select: { id: true, name: true, email: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: input.limit + 1,
|
||||||
|
...(input.cursor ? { cursor: { id: input.cursor }, skip: 1 } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let nextCursor: string | undefined;
|
||||||
|
if (items.length > input.limit) {
|
||||||
|
const next = items.pop();
|
||||||
|
nextCursor = next?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { items, nextCursor };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAuditEntryById(
|
||||||
|
db: { auditLog: { findUniqueOrThrow: Function } },
|
||||||
|
id: string,
|
||||||
|
) {
|
||||||
|
return db.auditLog.findUniqueOrThrow({
|
||||||
|
where: { id },
|
||||||
|
include: { user: { select: { id: true, name: true, email: true } } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAuditEntriesByEntity(
|
||||||
|
db: { auditLog: { findMany: Function } },
|
||||||
|
input: { entityType: string; entityId: string; limit: number },
|
||||||
|
) {
|
||||||
|
return db.auditLog.findMany({
|
||||||
|
where: {
|
||||||
|
entityType: input.entityType,
|
||||||
|
entityId: input.entityId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, name: true, email: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: input.limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAuditTimeline(
|
||||||
|
db: { auditLog: { findMany: Function } },
|
||||||
|
input: AuditTimelineInput,
|
||||||
|
) {
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
const createdAt = buildCreatedAtWhere(input.startDate, input.endDate);
|
||||||
|
if (createdAt) {
|
||||||
|
where.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await db.auditLog.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, name: true, email: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: input.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const grouped: Record<string, typeof entries> = {};
|
||||||
|
for (const entry of entries) {
|
||||||
|
const dateKey = entry.createdAt.toISOString().slice(0, 10);
|
||||||
|
if (!grouped[dateKey]) grouped[dateKey] = [];
|
||||||
|
grouped[dateKey].push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAuditActivitySummary(
|
||||||
|
db: {
|
||||||
|
auditLog: { groupBy: Function; count: Function };
|
||||||
|
user: { findMany: Function };
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
startDate?: Date | undefined;
|
||||||
|
endDate?: Date | undefined;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
const createdAt = buildCreatedAtWhere(input.startDate, input.endDate);
|
||||||
|
if (createdAt) {
|
||||||
|
where.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [byEntityTypeRaw, byActionRaw, byUserRaw, total] = await Promise.all([
|
||||||
|
db.auditLog.groupBy({
|
||||||
|
by: ["entityType"],
|
||||||
|
where,
|
||||||
|
_count: { id: true },
|
||||||
|
}),
|
||||||
|
db.auditLog.groupBy({
|
||||||
|
by: ["action"],
|
||||||
|
where,
|
||||||
|
_count: { id: true },
|
||||||
|
}),
|
||||||
|
db.auditLog.groupBy({
|
||||||
|
by: ["userId"],
|
||||||
|
where,
|
||||||
|
_count: { id: true },
|
||||||
|
orderBy: { _count: { id: "desc" } },
|
||||||
|
take: 20,
|
||||||
|
}),
|
||||||
|
db.auditLog.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const byEntityType: Record<string, number> = {};
|
||||||
|
for (const row of byEntityTypeRaw) {
|
||||||
|
byEntityType[row.entityType] = row._count.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byAction: Record<string, number> = {};
|
||||||
|
for (const row of byActionRaw) {
|
||||||
|
byAction[row.action] = row._count.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIds = byUserRaw
|
||||||
|
.map((row: { userId: string | null }) => row.userId)
|
||||||
|
.filter((id: string | null): id is string => id !== null);
|
||||||
|
|
||||||
|
const users = userIds.length > 0
|
||||||
|
? await db.user.findMany({
|
||||||
|
where: { id: { in: userIds } },
|
||||||
|
select: { id: true, name: true, email: true },
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const userMap = new Map(users.map((user: { id: string; name: string | null; email: string | null }) => [
|
||||||
|
user.id,
|
||||||
|
user.name ?? user.email,
|
||||||
|
]));
|
||||||
|
|
||||||
|
const byUser = byUserRaw
|
||||||
|
.filter((row: { userId: string | null }) => row.userId !== null)
|
||||||
|
.map((row: { userId: string | null; _count: { id: number } }) => ({
|
||||||
|
name: userMap.get(row.userId!) ?? "Unknown",
|
||||||
|
count: row._count.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { byEntityType, byAction, byUser, total };
|
||||||
|
}
|
||||||
@@ -1,234 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, controllerProcedure } from "../trpc.js";
|
import { createTRPCRouter, controllerProcedure } from "../trpc.js";
|
||||||
|
import {
|
||||||
type AuditUser = { id: string; name: string | null; email: string | null } | null | undefined;
|
formatAuditDetailEntry,
|
||||||
|
formatAuditListEntry,
|
||||||
type AuditEntryShape = {
|
getAuditActivitySummary,
|
||||||
id: string;
|
getAuditEntriesByEntity,
|
||||||
entityType: string;
|
getAuditEntryById,
|
||||||
entityId: string;
|
getAuditTimeline,
|
||||||
entityName?: string | null;
|
listAuditEntries,
|
||||||
action: string;
|
toAuditListInput,
|
||||||
userId?: string | null;
|
toAuditTimelineInput,
|
||||||
source?: string | null;
|
} from "./audit-log-support.js";
|
||||||
summary?: string | null;
|
|
||||||
createdAt: Date;
|
|
||||||
user?: AuditUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AuditDetailEntryShape = AuditEntryShape & {
|
|
||||||
changes?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatAuditListEntry(entry: AuditEntryShape) {
|
|
||||||
return {
|
|
||||||
id: entry.id,
|
|
||||||
entityType: entry.entityType,
|
|
||||||
entityId: entry.entityId,
|
|
||||||
entityName: entry.entityName ?? null,
|
|
||||||
action: entry.action,
|
|
||||||
userId: entry.userId ?? null,
|
|
||||||
source: entry.source ?? null,
|
|
||||||
summary: entry.summary ?? null,
|
|
||||||
createdAt: entry.createdAt.toISOString(),
|
|
||||||
user: entry.user
|
|
||||||
? {
|
|
||||||
id: entry.user.id,
|
|
||||||
name: entry.user.name,
|
|
||||||
email: entry.user.email,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatAuditDetailEntry(entry: AuditDetailEntryShape) {
|
|
||||||
return {
|
|
||||||
...formatAuditListEntry(entry),
|
|
||||||
changes: entry.changes ?? null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuditListInput = {
|
|
||||||
entityType?: string;
|
|
||||||
entityId?: string;
|
|
||||||
userId?: string;
|
|
||||||
action?: string;
|
|
||||||
source?: string;
|
|
||||||
startDate?: Date;
|
|
||||||
endDate?: Date;
|
|
||||||
search?: string;
|
|
||||||
limit: number;
|
|
||||||
cursor?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AuditTimelineInput = {
|
|
||||||
startDate?: Date;
|
|
||||||
endDate?: Date;
|
|
||||||
limit: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
function toAuditListInput(input: {
|
|
||||||
entityType?: string | undefined;
|
|
||||||
entityId?: string | undefined;
|
|
||||||
userId?: string | undefined;
|
|
||||||
action?: string | undefined;
|
|
||||||
source?: string | undefined;
|
|
||||||
startDate?: Date | undefined;
|
|
||||||
endDate?: Date | undefined;
|
|
||||||
search?: string | undefined;
|
|
||||||
limit: number;
|
|
||||||
cursor?: string | undefined;
|
|
||||||
}): AuditListInput {
|
|
||||||
return {
|
|
||||||
limit: input.limit,
|
|
||||||
...(input.entityType !== undefined ? { entityType: input.entityType } : {}),
|
|
||||||
...(input.entityId !== undefined ? { entityId: input.entityId } : {}),
|
|
||||||
...(input.userId !== undefined ? { userId: input.userId } : {}),
|
|
||||||
...(input.action !== undefined ? { action: input.action } : {}),
|
|
||||||
...(input.source !== undefined ? { source: input.source } : {}),
|
|
||||||
...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
|
|
||||||
...(input.endDate !== undefined ? { endDate: input.endDate } : {}),
|
|
||||||
...(input.search !== undefined ? { search: input.search } : {}),
|
|
||||||
...(input.cursor !== undefined ? { cursor: input.cursor } : {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function toAuditTimelineInput(input: {
|
|
||||||
startDate?: Date | undefined;
|
|
||||||
endDate?: Date | undefined;
|
|
||||||
limit: number;
|
|
||||||
}): AuditTimelineInput {
|
|
||||||
return {
|
|
||||||
limit: input.limit,
|
|
||||||
...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
|
|
||||||
...(input.endDate !== undefined ? { endDate: input.endDate } : {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAuditListWhere(input: Omit<AuditListInput, "limit" | "cursor">) {
|
|
||||||
const { entityType, entityId, userId, action, source, startDate, endDate, search } = input;
|
|
||||||
const where: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
if (entityType) where.entityType = entityType;
|
|
||||||
if (entityId) where.entityId = entityId;
|
|
||||||
if (userId) where.userId = userId;
|
|
||||||
if (action) where.action = action;
|
|
||||||
if (source) where.source = source;
|
|
||||||
|
|
||||||
if (startDate || endDate) {
|
|
||||||
const createdAt: Record<string, Date> = {};
|
|
||||||
if (startDate) createdAt.gte = startDate;
|
|
||||||
if (endDate) createdAt.lte = endDate;
|
|
||||||
where.createdAt = createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
where.OR = [
|
|
||||||
{ entityName: { contains: search, mode: "insensitive" } },
|
|
||||||
{ summary: { contains: search, mode: "insensitive" } },
|
|
||||||
{ entityType: { contains: search, mode: "insensitive" } },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!startDate && !endDate && !entityId) {
|
|
||||||
const thirtyDaysAgo = new Date();
|
|
||||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
||||||
where.createdAt = { ...(where.createdAt as Record<string, Date> ?? {}), gte: thirtyDaysAgo };
|
|
||||||
}
|
|
||||||
|
|
||||||
return where;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listAuditEntries(
|
|
||||||
db: { auditLog: { findMany: Function } },
|
|
||||||
input: AuditListInput,
|
|
||||||
) {
|
|
||||||
const items = await db.auditLog.findMany({
|
|
||||||
where: buildAuditListWhere(input),
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
entityType: true,
|
|
||||||
entityId: true,
|
|
||||||
entityName: true,
|
|
||||||
action: true,
|
|
||||||
userId: true,
|
|
||||||
source: true,
|
|
||||||
summary: true,
|
|
||||||
createdAt: true,
|
|
||||||
user: { select: { id: true, name: true, email: true } },
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
take: input.limit + 1,
|
|
||||||
...(input.cursor ? { cursor: { id: input.cursor }, skip: 1 } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
let nextCursor: string | undefined;
|
|
||||||
if (items.length > input.limit) {
|
|
||||||
const next = items.pop();
|
|
||||||
nextCursor = next?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { items, nextCursor };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAuditEntryById(
|
|
||||||
db: { auditLog: { findUniqueOrThrow: Function } },
|
|
||||||
id: string,
|
|
||||||
) {
|
|
||||||
return db.auditLog.findUniqueOrThrow({
|
|
||||||
where: { id },
|
|
||||||
include: { user: { select: { id: true, name: true, email: true } } },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAuditEntriesByEntity(
|
|
||||||
db: { auditLog: { findMany: Function } },
|
|
||||||
input: { entityType: string; entityId: string; limit: number },
|
|
||||||
) {
|
|
||||||
return db.auditLog.findMany({
|
|
||||||
where: {
|
|
||||||
entityType: input.entityType,
|
|
||||||
entityId: input.entityId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
user: { select: { id: true, name: true, email: true } },
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
take: input.limit,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAuditTimeline(
|
|
||||||
db: { auditLog: { findMany: Function } },
|
|
||||||
input: AuditTimelineInput,
|
|
||||||
) {
|
|
||||||
const where: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
if (input.startDate || input.endDate) {
|
|
||||||
const createdAt: Record<string, Date> = {};
|
|
||||||
if (input.startDate) createdAt.gte = input.startDate;
|
|
||||||
if (input.endDate) createdAt.lte = input.endDate;
|
|
||||||
where.createdAt = createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await db.auditLog.findMany({
|
|
||||||
where,
|
|
||||||
include: {
|
|
||||||
user: { select: { id: true, name: true, email: true } },
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
take: input.limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
const grouped: Record<string, typeof entries> = {};
|
|
||||||
for (const entry of entries) {
|
|
||||||
const dateKey = entry.createdAt.toISOString().slice(0, 10);
|
|
||||||
if (!grouped[dateKey]) grouped[dateKey] = [];
|
|
||||||
grouped[dateKey].push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
return grouped;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Router ───────────────────────────────────────────────────────────────────
|
// ─── Router ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -403,69 +185,6 @@ export const auditLogRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const where: Record<string, unknown> = {};
|
return getAuditActivitySummary(ctx.db, input);
|
||||||
|
|
||||||
if (input.startDate || input.endDate) {
|
|
||||||
const createdAt: Record<string, Date> = {};
|
|
||||||
if (input.startDate) createdAt.gte = input.startDate;
|
|
||||||
if (input.endDate) createdAt.lte = input.endDate;
|
|
||||||
where.createdAt = createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run aggregation queries in parallel
|
|
||||||
const [byEntityTypeRaw, byActionRaw, byUserRaw, total] = await Promise.all([
|
|
||||||
ctx.db.auditLog.groupBy({
|
|
||||||
by: ["entityType"],
|
|
||||||
where,
|
|
||||||
_count: { id: true },
|
|
||||||
}),
|
|
||||||
ctx.db.auditLog.groupBy({
|
|
||||||
by: ["action"],
|
|
||||||
where,
|
|
||||||
_count: { id: true },
|
|
||||||
}),
|
|
||||||
ctx.db.auditLog.groupBy({
|
|
||||||
by: ["userId"],
|
|
||||||
where,
|
|
||||||
_count: { id: true },
|
|
||||||
orderBy: { _count: { id: "desc" } },
|
|
||||||
take: 20,
|
|
||||||
}),
|
|
||||||
ctx.db.auditLog.count({ where }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Convert to simple Record<string, number>
|
|
||||||
const byEntityType: Record<string, number> = {};
|
|
||||||
for (const row of byEntityTypeRaw) {
|
|
||||||
byEntityType[row.entityType] = row._count.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const byAction: Record<string, number> = {};
|
|
||||||
for (const row of byActionRaw) {
|
|
||||||
byAction[row.action] = row._count.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve user names for the top users
|
|
||||||
const userIds = byUserRaw
|
|
||||||
.map((row) => row.userId)
|
|
||||||
.filter((id): id is string => id !== null);
|
|
||||||
|
|
||||||
const users = userIds.length > 0
|
|
||||||
? await ctx.db.user.findMany({
|
|
||||||
where: { id: { in: userIds } },
|
|
||||||
select: { id: true, name: true, email: true },
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const userMap = new Map(users.map((u) => [u.id, u.name ?? u.email]));
|
|
||||||
|
|
||||||
const byUser = byUserRaw
|
|
||||||
.filter((row) => row.userId !== null)
|
|
||||||
.map((row) => ({
|
|
||||||
name: userMap.get(row.userId!) ?? "Unknown",
|
|
||||||
count: row._count.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { byEntityType, byAction, byUser, total };
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user