From fc513800ae53ea75b039cf430cf7bab74d33a904 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Sat, 22 Nov 2025 23:24:40 +0000 Subject: [PATCH] chore: extract shared audit log query logic --- .../server-only/document/audit-log-query.ts | 110 +++++++++ .../document/find-document-audit-logs.ts | 214 +++--------------- .../find-document-audit-logs.ts | 24 +- .../find-document-audit-logs.types.ts | 1 - .../find-envelope-audit-logs.ts | 24 +- .../find-envelope-audit-logs.types.ts | 4 - 6 files changed, 154 insertions(+), 223 deletions(-) create mode 100644 packages/lib/server-only/document/audit-log-query.ts diff --git a/packages/lib/server-only/document/audit-log-query.ts b/packages/lib/server-only/document/audit-log-query.ts new file mode 100644 index 000000000..863257bbd --- /dev/null +++ b/packages/lib/server-only/document/audit-log-query.ts @@ -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 & { nextCursor?: string }; +} diff --git a/packages/lib/server-only/document/find-document-audit-logs.ts b/packages/lib/server-only/document/find-document-audit-logs.ts index d23e99761..85deebc1d 100644 --- a/packages/lib/server-only/document/find-document-audit-logs.ts +++ b/packages/lib/server-only/document/find-document-audit-logs.ts @@ -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 & { 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 & { nextCursor?: string }; + orderBy, + cursor, + filterForRecentActivity, + }); }; diff --git a/packages/trpc/server/document-router/find-document-audit-logs.ts b/packages/trpc/server/document-router/find-document-audit-logs.ts index 17a2d9e7c..fcc963b8b 100644 --- a/packages/trpc/server/document-router/find-document-audit-logs.ts +++ b/packages/trpc/server/document-router/find-document-audit-logs.ts @@ -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, }); }); diff --git a/packages/trpc/server/document-router/find-document-audit-logs.types.ts b/packages/trpc/server/document-router/find-document-audit-logs.types.ts index 05ea21cf2..6e8991667 100644 --- a/packages/trpc/server/document-router/find-document-audit-logs.types.ts +++ b/packages/trpc/server/document-router/find-document-audit-logs.types.ts @@ -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'), }); diff --git a/packages/trpc/server/envelope-router/find-envelope-audit-logs.ts b/packages/trpc/server/envelope-router/find-envelope-audit-logs.ts index ed6d6b0ad..14b3013c5 100644 --- a/packages/trpc/server/envelope-router/find-envelope-audit-logs.ts +++ b/packages/trpc/server/envelope-router/find-envelope-audit-logs.ts @@ -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, }); }); diff --git a/packages/trpc/server/envelope-router/find-envelope-audit-logs.types.ts b/packages/trpc/server/envelope-router/find-envelope-audit-logs.types.ts index 5ed42e8fe..732cf74d3 100644 --- a/packages/trpc/server/envelope-router/find-envelope-audit-logs.types.ts +++ b/packages/trpc/server/envelope-router/find-envelope-audit-logs.types.ts @@ -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'), });