Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/db/logs.db.server.ts
T

337 lines
8.5 KiB
TypeScript

import { Tenant, Log, ApiKey, Prisma } from "@toolctrl/db/prisma-browser";
import { FiltersDto } from "~/application/dtos/data/FiltersDto";
import { PaginationDto } from "~/application/dtos/data/PaginationDto";
import { DefaultLogActions } from "~/application/dtos/shared/DefaultLogActions";
import { db } from "../db.server";
import ApiHelper from "../helpers/ApiHelper";
import RowFiltersHelper from "../helpers/RowFiltersHelper";
import { getUserInfo } from "../session.server";
import { EntityWithDetails } from "./entities/entities.db.server";
import { callEntityWebhooks } from "./entities/entityWebhooks.db.server";
import { RowCommentWithDetails } from "./entities/rowComments.db.server";
import { RowWithDetails } from "./entities/rows.db.server";
import { UserWithoutPassword } from "./users.db.server";
import UserModelHelper from "../helpers/models/UserModelHelper";
export type LogWithDetails = Log & {
user: UserWithoutPassword | null;
apiKey: ApiKey | null;
tenant?: Tenant | null;
comment?: RowCommentWithDetails | null;
};
const include = {
user: true,
tenant: true,
apiKey: true,
comment: {
include: {
...UserModelHelper.includeSimpleCreatedByUser,
reactions: {
include: {
...UserModelHelper.includeSimpleCreatedByUser,
},
},
},
},
};
export async function getAllLogs(
pagination: { page: number; pageSize: number },
filters: FiltersDto
): Promise<{ items: LogWithDetails[]; pagination: PaginationDto }> {
const where = RowFiltersHelper.getFiltersCondition(filters);
const items = await db.log.findMany({
take: pagination.pageSize,
skip: pagination.pageSize * (pagination.page - 1),
include,
where,
});
const totalItems = await db.log.count({
where,
});
return {
items,
pagination: {
page: pagination.page,
pageSize: pagination.pageSize,
totalItems,
totalPages: Math.ceil(totalItems / pagination.pageSize),
},
};
}
export async function getLogs(
tenantId: string,
pagination: { page: number; pageSize: number },
filters: FiltersDto
): Promise<{ items: LogWithDetails[]; pagination: PaginationDto }> {
const where = RowFiltersHelper.getFiltersCondition(filters);
const items = await db.log.findMany({
take: pagination.pageSize,
skip: pagination.pageSize * (pagination.page - 1),
where: { tenantId, ...where },
include,
});
const totalItems = await db.log.count({
where: { tenantId, ...where },
});
return {
items,
pagination: {
page: pagination.page,
pageSize: pagination.pageSize,
totalItems,
totalPages: Math.ceil(totalItems / pagination.pageSize),
},
};
}
export async function getAllRowLogs({
entityId,
rowId,
tenantId,
pagination,
}: {
entityId?: string;
rowId?: string;
tenantId?: string | null;
pagination: { page: number; pageSize: number };
}): Promise<{ items: LogWithDetails[]; pagination: PaginationDto }> {
let where: Prisma.LogWhereInput = { rowId: { not: null } };
if (entityId) {
where = { ...where, row: { entityId } };
}
if (rowId) {
where = { ...where, rowId };
}
if (tenantId !== undefined) {
where = { ...where, tenantId };
}
const items = await db.log.findMany({
take: pagination.pageSize,
skip: pagination.pageSize * (pagination.page - 1),
where,
include,
});
const totalItems = await db.log.count({
where,
});
return {
items,
pagination: {
page: pagination.page,
pageSize: pagination.pageSize,
totalItems,
totalPages: Math.ceil(totalItems / pagination.pageSize),
},
};
}
export async function getRowLogs(tenantId: string | null, rowId: string): Promise<LogWithDetails[]> {
return await db.log.findMany({
where: {
tenantId,
rowId,
},
include,
orderBy: {
createdAt: "desc",
},
});
}
export async function getRowLogsById(rowId: string): Promise<LogWithDetails[]> {
return await db.log.findMany({
where: {
rowId,
},
include,
orderBy: {
createdAt: "desc",
},
});
}
export async function createLog(request: Request, tenantId: string | null, action: string, details: string) {
const userInfo = await getUserInfo(request);
await db.log.create({
data: {
tenantId,
userId: userInfo.userId,
url: new URL(request.url).pathname,
action,
details,
},
});
}
export async function createManualLog(userId: string, tenantId: string | null, pathname: string, action: string, details: string) {
await db.log.create({
data: {
tenantId,
userId,
url: pathname,
action,
details,
},
});
}
export async function createRowLog(
request: Request,
data: {
tenantId: string | null;
createdByUserId?: string | null;
createdByApiKey?: string | null;
action: string;
entity: EntityWithDetails;
item: RowWithDetails | null;
commentId?: string | null;
details?: string;
}
) {
const log = await db.log.create({
data: {
tenantId: data.tenantId,
userId: data.createdByUserId ?? null,
apiKeyId: data.createdByApiKey ?? null,
url: new URL(request.url).pathname,
rowId: data.item?.id ?? null,
action: data.action,
details: data.details ?? null,
commentId: data.commentId ?? null,
},
});
const apiFormat = ApiHelper.getApiFormat(data.entity, data.item);
await callEntityWebhooks({
logId: log.id,
entityId: data.entity.id,
action: data.action,
body: apiFormat,
request,
});
return log;
}
export async function createManualRowLog(
data: {
tenantId: string | null;
createdByUserId?: string | null;
createdByApiKey?: string | null;
action: string;
entity: EntityWithDetails;
item: RowWithDetails | null;
commentId?: string | null;
},
request?: Request
) {
let details = "";
const log = await db.log.create({
data: {
tenantId: data.tenantId,
userId: data.createdByUserId ?? null,
apiKeyId: data.createdByApiKey ?? null,
url: request ? new URL(request.url).pathname : "",
rowId: data.item?.id ?? null,
action: data.action,
details,
commentId: data.commentId ?? null,
},
});
const apiFormat = ApiHelper.getApiFormat(data.entity, data.item);
await callEntityWebhooks({
logId: log.id,
entityId: data.entity.id,
action: data.action,
body: apiFormat,
// request,
});
return log;
}
export async function createLogLogin(request: Request, user: UserWithoutPassword) {
// eslint-disable-next-line no-console
// console.log({ clientIpAddress: getClientIPAddress(request.headers) });
await db.log.create({
data: {
userId: user.id,
url: "",
action: DefaultLogActions.Login,
details: "",
},
});
}
export async function createLogLogout(request: Request, userId: string) {
await db.log.create({
data: {
userId,
url: "",
action: DefaultLogActions.Logout,
details: "",
},
});
}
export async function createAdminLog(request: Request, action: string, details: string) {
const userInfo = await getUserInfo(request);
await db.log.create({
data: {
userId: userInfo.userId,
url: new URL(request.url).pathname,
action,
details,
},
});
}
export async function getLastUserLog(userId: string): Promise<Log | null> {
return await db.log.findFirst({
where: {
userId,
},
orderBy: {
createdAt: "desc",
},
});
}
/**
* Returns the most recent log entry per user for the given userIds in a
* constant number of queries (1 groupBy + 1 findMany), avoiding the N+1
* pattern of calling `getLastUserLog` per user.
*/
export async function getLastUserLogsByUserIds(userIds: string[]): Promise<Map<string, Log>> {
if (userIds.length === 0) {
return new Map();
}
const groups = await db.log.groupBy({
by: ["userId"],
where: { userId: { in: userIds } },
_max: { createdAt: true },
});
const filters = groups
.map((group) => (group._max.createdAt ? { userId: group.userId, createdAt: group._max.createdAt } : null))
.filter((entry): entry is { userId: string; createdAt: Date } => entry !== null);
if (filters.length === 0) {
return new Map();
}
const logs = await db.log.findMany({
where: { OR: filters },
});
const byUserId = new Map<string, Log>();
for (const log of logs) {
const existing = byUserId.get(log.userId ?? "");
if (!log.userId) continue;
if (!existing || existing.createdAt < log.createdAt) {
byUserId.set(log.userId, log);
}
}
return byUserId;
}