chore: extract shared audit log query logic

This commit is contained in:
Ephraim Atta-Duncan
2025-11-22 23:24:40 +00:00
parent e7affea053
commit fc513800ae
6 changed files with 154 additions and 223 deletions

View File

@ -0,0 +1,110 @@
import type { DocumentAuditLog, Envelope, Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { FindResultResponse } from '../../types/search-params';
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
const RECENT_ACTIVITY_EVENT_TYPES = [
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
];
export interface AuditLogQueryOptions {
envelope: Envelope;
page?: number;
perPage?: number;
orderBy?: {
column: keyof DocumentAuditLog;
direction: 'asc' | 'desc';
};
cursor?: string;
filterForRecentActivity?: boolean;
}
function buildAuditLogWhereClause(
envelope: Envelope,
filterForRecentActivity?: boolean,
): Prisma.DocumentAuditLogWhereInput {
const baseWhereClause: Prisma.DocumentAuditLogWhereInput = {
envelopeId: envelope.id,
};
if (!filterForRecentActivity) {
return baseWhereClause;
}
const recentActivityConditions: Prisma.DocumentAuditLogWhereInput['OR'] = [
{
type: {
in: RECENT_ACTIVITY_EVENT_TYPES,
},
},
{
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
data: {
path: ['isResending'],
equals: true,
},
},
];
return {
...baseWhereClause,
OR: recentActivityConditions,
};
}
export async function queryAuditLogs({
envelope,
page = 1,
perPage = 30,
orderBy,
cursor,
filterForRecentActivity,
}: AuditLogQueryOptions) {
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const whereClause = buildAuditLogWhereClause(envelope, filterForRecentActivity);
const normalizedPage = Math.max(page, 1);
const skip = (normalizedPage - 1) * perPage;
const [data, count] = await Promise.all([
prisma.documentAuditLog.findMany({
where: whereClause,
skip,
take: perPage + 1,
orderBy: {
[orderByColumn]: orderByDirection,
},
cursor: cursor ? { id: cursor } : undefined,
}),
prisma.documentAuditLog.count({
where: whereClause,
}),
]);
const allParsedData = data.map((auditLog) => parseDocumentAuditLogData(auditLog));
const hasNextPage = allParsedData.length > perPage;
const parsedData = hasNextPage ? allParsedData.slice(0, perPage) : allParsedData;
const nextCursor = hasNextPage ? allParsedData[perPage].id : undefined;
return {
data: parsedData,
count,
currentPage: normalizedPage,
perPage,
totalPages: Math.ceil(count / perPage),
nextCursor,
} satisfies FindResultResponse<typeof parsedData> & { nextCursor?: string };
}

View File

@ -1,17 +1,15 @@
import { type DocumentAuditLog, EnvelopeType, type Prisma } from '@prisma/client';
import type { DocumentAuditLog } from '@prisma/client';
import { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { FindResultResponse } from '../../types/search-params';
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { queryAuditLogs } from './audit-log-query';
export interface FindDocumentAuditLogsOptions {
interface BaseAuditLogOptions {
userId: number;
teamId: number;
documentId: number;
page?: number;
perPage?: number;
orderBy?: {
@ -20,23 +18,26 @@ export interface FindDocumentAuditLogsOptions {
};
cursor?: string;
filterForRecentActivity?: boolean;
eventTypes?: string[];
}
export interface FindDocumentAuditLogsOptions extends BaseAuditLogOptions {
documentId: number;
}
export interface FindEnvelopeAuditLogsOptions extends BaseAuditLogOptions {
envelopeId: string;
}
export const findDocumentAuditLogs = async ({
userId,
teamId,
documentId,
page = 1,
perPage = 30,
page,
perPage,
orderBy,
cursor,
filterForRecentActivity,
eventTypes,
}: FindDocumentAuditLogsOptions) => {
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'documentId',
@ -55,118 +56,35 @@ export const findDocumentAuditLogs = async ({
throw new AppError(AppErrorCode.NOT_FOUND);
}
const whereClause: Prisma.DocumentAuditLogWhereInput = {
envelopeId: envelope.id,
};
if (eventTypes && eventTypes.length > 0) {
whereClause.type = {
in: eventTypes,
};
}
if (filterForRecentActivity) {
whereClause.OR = [
{
type: {
in: [
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
],
},
},
{
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
data: {
path: ['isResending'],
equals: true,
},
},
];
}
const [data, count] = await Promise.all([
prisma.documentAuditLog.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage + 1,
orderBy: {
[orderByColumn]: orderByDirection,
},
cursor: cursor ? { id: cursor } : undefined,
}),
prisma.documentAuditLog.count({
where: whereClause,
}),
]);
let nextCursor: string | undefined = undefined;
const parsedData = data.map((auditLog) => parseDocumentAuditLogData(auditLog));
if (parsedData.length > perPage) {
const nextItem = parsedData.pop();
nextCursor = nextItem!.id;
}
return {
data: parsedData,
count,
currentPage: Math.max(page, 1),
return queryAuditLogs({
envelope,
page,
perPage,
totalPages: Math.ceil(count / perPage),
nextCursor,
} satisfies FindResultResponse<typeof parsedData> & { nextCursor?: string };
orderBy,
cursor,
filterForRecentActivity,
});
};
export interface FindEnvelopeAuditLogsOptions {
userId: number;
teamId: number;
envelopeId: string;
page?: number;
perPage?: number;
orderBy?: {
column: keyof DocumentAuditLog;
direction: 'asc' | 'desc';
};
cursor?: string;
filterForRecentActivity?: boolean;
eventTypes?: string[];
}
export const findEnvelopeAuditLogs = async ({
userId,
teamId,
envelopeId,
page = 1,
perPage = 30,
page,
perPage,
orderBy,
cursor,
filterForRecentActivity,
eventTypes,
}: FindEnvelopeAuditLogsOptions) => {
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const isLegacyDocumentId = /^\d+$/.test(envelopeId);
const isNumericId = /^\d+$/.test(envelopeId);
const idConfig = isLegacyDocumentId
? { type: 'documentId' as const, id: Number(envelopeId) }
: { type: 'envelopeId' as const, id: envelopeId };
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: isNumericId
? {
type: 'documentId',
id: Number(envelopeId),
}
: {
type: 'envelopeId',
id: envelopeId,
},
type: isNumericId ? EnvelopeType.DOCUMENT : null,
id: idConfig,
type: isLegacyDocumentId ? EnvelopeType.DOCUMENT : null,
userId,
teamId,
});
@ -179,72 +97,12 @@ export const findEnvelopeAuditLogs = async ({
throw new AppError(AppErrorCode.NOT_FOUND);
}
const whereClause: Prisma.DocumentAuditLogWhereInput = {
envelopeId: envelope.id,
};
if (eventTypes && eventTypes.length > 0) {
whereClause.type = {
in: eventTypes,
};
}
if (filterForRecentActivity) {
whereClause.OR = [
{
type: {
in: [
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
],
},
},
{
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
data: {
path: ['isResending'],
equals: true,
},
},
];
}
const [data, count] = await Promise.all([
prisma.documentAuditLog.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage + 1,
orderBy: {
[orderByColumn]: orderByDirection,
},
cursor: cursor ? { id: cursor } : undefined,
}),
prisma.documentAuditLog.count({
where: whereClause,
}),
]);
let nextCursor: string | undefined = undefined;
const parsedData = data.map((auditLog) => parseDocumentAuditLogData(auditLog));
if (parsedData.length > perPage) {
const nextItem = parsedData.pop();
nextCursor = nextItem!.id;
}
return {
data: parsedData,
count,
currentPage: Math.max(page, 1),
return queryAuditLogs({
envelope,
page,
perPage,
totalPages: Math.ceil(count / perPage),
nextCursor,
} satisfies FindResultResponse<typeof parsedData> & { nextCursor?: string };
orderBy,
cursor,
filterForRecentActivity,
});
};

View File

@ -10,34 +10,18 @@ export const findDocumentAuditLogsRoute = authenticatedProcedure
.input(ZFindDocumentAuditLogsRequestSchema)
.output(ZFindDocumentAuditLogsResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const {
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
eventTypes,
orderByColumn,
orderByDirection,
} = input;
const { orderByColumn, orderByDirection, ...auditLogParams } = input;
ctx.logger.info({
input: {
documentId,
documentId: input.documentId,
},
});
return await findDocumentAuditLogs({
...auditLogParams,
userId: ctx.user.id,
teamId,
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
eventTypes,
teamId: ctx.teamId,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
});
});

View File

@ -7,7 +7,6 @@ export const ZFindDocumentAuditLogsRequestSchema = ZFindSearchParamsSchema.exten
documentId: z.number().min(1),
cursor: z.string().optional(),
filterForRecentActivity: z.boolean().optional(),
eventTypes: z.array(z.string()).optional(),
orderByColumn: z.enum(['createdAt', 'type']).optional(),
orderByDirection: z.enum(['asc', 'desc']).default('desc'),
});

View File

@ -12,34 +12,18 @@ export const findEnvelopeAuditLogsRoute = authenticatedProcedure
.input(ZFindEnvelopeAuditLogsRequestSchema)
.output(ZFindEnvelopeAuditLogsResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const {
page,
perPage,
envelopeId,
cursor,
filterForRecentActivity,
eventTypes,
orderByColumn,
orderByDirection,
} = input;
const { orderByColumn, orderByDirection, ...auditLogParams } = input;
ctx.logger.info({
input: {
envelopeId,
envelopeId: input.envelopeId,
},
});
return await findEnvelopeAuditLogs({
...auditLogParams,
userId: ctx.user.id,
teamId,
page,
perPage,
envelopeId,
cursor,
filterForRecentActivity,
eventTypes,
teamId: ctx.teamId,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
});
});

View File

@ -22,10 +22,6 @@ export const ZFindEnvelopeAuditLogsRequestSchema = ZFindSearchParamsSchema.exten
.describe('Envelope ID (e.g., envelope_xxx) or legacy document ID (e.g., 12345)'),
cursor: z.string().optional(),
filterForRecentActivity: z.boolean().optional(),
eventTypes: z
.array(z.string())
.optional()
.describe('Filter by specific event types (e.g., ["DOCUMENT_CREATED", "DOCUMENT_SENT"])'),
orderByColumn: z.enum(['createdAt', 'type']).optional(),
orderByDirection: z.enum(['asc', 'desc']).default('desc'),
});