Compare commits

..

4 Commits

Author SHA1 Message Date
Ephraim Duncan
f9a855c99f Merge branch 'main' into feat/audit-logs-api 2025-11-26 00:22:13 +00:00
Ephraim Atta-Duncan
fc513800ae chore: extract shared audit log query logic 2025-11-22 23:24:40 +00:00
Ephraim Atta-Duncan
e7affea053 feat: filter by audit log event type 2025-11-22 01:24:13 +00:00
Ephraim Atta-Duncan
1a577e55a9 feat: add envelope audit logs endpoint 2025-11-22 01:12:56 +00:00
12 changed files with 248 additions and 638 deletions

View File

@@ -17,6 +17,5 @@
}, },
"[typescriptreact]": { "[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, }
"prisma.pinToPrisma6": true
} }

View File

@@ -1,4 +1,4 @@
import { type APIRequestContext, expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client'; import type { Team, User } from '@prisma/client';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -24,7 +24,6 @@ import type {
TCreateEnvelopeResponse, TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types'; } from '@documenso/trpc/server/envelope-router/create-envelope.types';
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types'; import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
import type { TFindEnvelopesResponse } from '@documenso/trpc/server/envelope-router/find-envelopes.types';
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types'; import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types'; import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
@@ -165,9 +164,6 @@ test.describe('API V2 Envelopes', () => {
positionY: 0, positionY: 0,
width: 0, width: 0,
height: 0, height: 0,
fieldMeta: {
type: 'signature',
},
}, },
{ {
type: FieldType.SIGNATURE, type: FieldType.SIGNATURE,
@@ -177,9 +173,6 @@ test.describe('API V2 Envelopes', () => {
positionY: 0, positionY: 0,
width: 0, width: 0,
height: 0, height: 0,
fieldMeta: {
type: 'signature',
},
}, },
], ],
}, },
@@ -564,198 +557,4 @@ test.describe('API V2 Envelopes', () => {
userEmail: userA.email, userEmail: userA.email,
}); });
}); });
test.describe('Envelope find endpoint', () => {
const createEnvelope = async (
request: APIRequestContext,
token: string,
payload: TCreateEnvelopePayload,
) => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const pdfData = fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
);
formData.append('files', new File([pdfData], 'test.pdf', { type: 'application/pdf' }));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
return (await res.json()) as TCreateEnvelopeResponse;
};
test('should find envelopes with pagination', async ({ request }) => {
// Create 3 envelopes
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Document 1',
});
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Document 2',
});
await createEnvelope(request, tokenA, {
type: EnvelopeType.TEMPLATE,
title: 'Template 1',
});
// Find all envelopes
const res = await request.get(`${baseUrl}/envelope`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TFindEnvelopesResponse;
expect(response.data.length).toBe(3);
expect(response.count).toBe(3);
expect(response.currentPage).toBe(1);
expect(response.totalPages).toBe(1);
// Test pagination
const paginatedRes = await request.get(`${baseUrl}/envelope?perPage=2&page=1`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(paginatedRes.ok()).toBeTruthy();
const paginatedResponse = (await paginatedRes.json()) as TFindEnvelopesResponse;
expect(paginatedResponse.data.length).toBe(2);
expect(paginatedResponse.count).toBe(3);
expect(paginatedResponse.totalPages).toBe(2);
});
test('should filter envelopes by type', async ({ request }) => {
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Document Only',
});
await createEnvelope(request, tokenA, {
type: EnvelopeType.TEMPLATE,
title: 'Template Only',
});
// Filter by DOCUMENT type
const documentRes = await request.get(`${baseUrl}/envelope?type=DOCUMENT`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(documentRes.ok()).toBeTruthy();
const documentResponse = (await documentRes.json()) as TFindEnvelopesResponse;
expect(documentResponse.data.every((e) => e.type === EnvelopeType.DOCUMENT)).toBe(true);
// Filter by TEMPLATE type
const templateRes = await request.get(`${baseUrl}/envelope?type=TEMPLATE`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(templateRes.ok()).toBeTruthy();
const templateResponse = (await templateRes.json()) as TFindEnvelopesResponse;
expect(templateResponse.data.every((e) => e.type === EnvelopeType.TEMPLATE)).toBe(true);
});
test('should filter envelopes by status', async ({ request }) => {
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Draft Document',
});
// Filter by DRAFT status (default for new envelopes)
const res = await request.get(`${baseUrl}/envelope?status=DRAFT`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
const response = (await res.json()) as TFindEnvelopesResponse;
expect(response.data.every((e) => e.status === DocumentStatus.DRAFT)).toBe(true);
});
test('should search envelopes by query', async ({ request }) => {
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Unique Searchable Title',
});
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Another Document',
});
const res = await request.get(`${baseUrl}/envelope?query=Unique%20Searchable`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
const response = (await res.json()) as TFindEnvelopesResponse;
expect(response.data.length).toBe(1);
expect(response.data[0].title).toBe('Unique Searchable Title');
});
test('should not return envelopes from other users', async ({ request }) => {
// Create envelope for userA
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'UserA Document',
});
// Create envelope for userB
await createEnvelope(request, tokenB, {
type: EnvelopeType.DOCUMENT,
title: 'UserB Document',
});
// userA should only see their own envelopes
const resA = await request.get(`${baseUrl}/envelope`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(resA.ok()).toBeTruthy();
const responseA = (await resA.json()) as TFindEnvelopesResponse;
expect(responseA.data.every((e) => e.title !== 'UserB Document')).toBe(true);
// userB should only see their own envelopes
const resB = await request.get(`${baseUrl}/envelope`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
expect(resB.ok()).toBeTruthy();
const responseB = (await resB.json()) as TFindEnvelopesResponse;
expect(responseB.data.every((e) => e.title !== 'UserA Document')).toBe(true);
});
test('should return envelope with expected schema fields', async ({ request }) => {
await createEnvelope(request, tokenA, {
type: EnvelopeType.DOCUMENT,
title: 'Schema Test Document',
});
const res = await request.get(`${baseUrl}/envelope`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.ok()).toBeTruthy();
const response = (await res.json()) as TFindEnvelopesResponse;
const envelope = response.data.find((e) => e.title === 'Schema Test Document');
expect(envelope).toBeDefined();
expect(envelope?.id).toBeDefined();
expect(envelope?.type).toBe(EnvelopeType.DOCUMENT);
expect(envelope?.status).toBe(DocumentStatus.DRAFT);
expect(envelope?.recipients).toBeDefined();
expect(envelope?.user).toBeDefined();
expect(envelope?.team).toBeDefined();
});
});
}); });

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 { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error'; 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 { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { queryAuditLogs } from './audit-log-query';
export interface FindDocumentAuditLogsOptions { interface BaseAuditLogOptions {
userId: number; userId: number;
teamId: number; teamId: number;
documentId: number;
page?: number; page?: number;
perPage?: number; perPage?: number;
orderBy?: { orderBy?: {
@@ -22,19 +20,24 @@ export interface FindDocumentAuditLogsOptions {
filterForRecentActivity?: boolean; filterForRecentActivity?: boolean;
} }
export interface FindDocumentAuditLogsOptions extends BaseAuditLogOptions {
documentId: number;
}
export interface FindEnvelopeAuditLogsOptions extends BaseAuditLogOptions {
envelopeId: string;
}
export const findDocumentAuditLogs = async ({ export const findDocumentAuditLogs = async ({
userId, userId,
teamId, teamId,
documentId, documentId,
page = 1, page,
perPage = 30, perPage,
orderBy, orderBy,
cursor, cursor,
filterForRecentActivity, filterForRecentActivity,
}: FindDocumentAuditLogsOptions) => { }: FindDocumentAuditLogsOptions) => {
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const { envelopeWhereInput } = await getEnvelopeWhereInput({ const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: { id: {
type: 'documentId', type: 'documentId',
@@ -53,67 +56,53 @@ export const findDocumentAuditLogs = async ({
throw new AppError(AppErrorCode.NOT_FOUND); throw new AppError(AppErrorCode.NOT_FOUND);
} }
const whereClause: Prisma.DocumentAuditLogWhereInput = { return queryAuditLogs({
envelopeId: envelope.id, envelope,
}; page,
// Filter events down to what we consider recent activity.
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),
perPage, perPage,
totalPages: Math.ceil(count / perPage), orderBy,
nextCursor, cursor,
} satisfies FindResultResponse<typeof parsedData> & { nextCursor?: string }; filterForRecentActivity,
});
};
export const findEnvelopeAuditLogs = async ({
userId,
teamId,
envelopeId,
page,
perPage,
orderBy,
cursor,
filterForRecentActivity,
}: FindEnvelopeAuditLogsOptions) => {
const isLegacyDocumentId = /^\d+$/.test(envelopeId);
const idConfig = isLegacyDocumentId
? { type: 'documentId' as const, id: Number(envelopeId) }
: { type: 'envelopeId' as const, id: envelopeId };
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: idConfig,
type: isLegacyDocumentId ? EnvelopeType.DOCUMENT : null,
userId,
teamId,
});
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
return queryAuditLogs({
envelope,
page,
perPage,
orderBy,
cursor,
filterForRecentActivity,
});
}; };

View File

@@ -1,202 +0,0 @@
import type {
DocumentSource,
DocumentStatus,
Envelope,
EnvelopeType,
Prisma,
} from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
import type { FindResultResponse } from '../../types/search-params';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
import { getTeamById } from '../team/get-team';
export type FindEnvelopesOptions = {
userId: number;
teamId: number;
type?: EnvelopeType;
templateId?: number;
source?: DocumentSource;
status?: DocumentStatus;
page?: number;
perPage?: number;
orderBy?: {
column: keyof Pick<Envelope, 'createdAt'>;
direction: 'asc' | 'desc';
};
query?: string;
folderId?: string;
};
export const findEnvelopes = async ({
userId,
teamId,
type,
templateId,
source,
status,
page = 1,
perPage = 10,
orderBy,
query = '',
folderId,
}: FindEnvelopesOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
select: {
id: true,
email: true,
name: true,
},
});
const team = await getTeamById({
userId,
teamId,
});
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const searchFilter: Prisma.EnvelopeWhereInput = query
? {
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ externalId: { contains: query, mode: 'insensitive' } },
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
],
}
: {};
const visibilityFilter: Prisma.EnvelopeWhereInput = {
visibility: {
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
},
};
const teamEmailFilters: Prisma.EnvelopeWhereInput[] = [];
if (team.teamEmail) {
teamEmailFilters.push(
{
user: {
email: team.teamEmail.email,
},
},
{
recipients: {
some: {
email: team.teamEmail.email,
},
},
},
);
}
const whereClause: Prisma.EnvelopeWhereInput = {
AND: [
searchFilter,
{
OR: [
{
teamId: team.id,
...visibilityFilter,
},
{
userId,
},
...teamEmailFilters,
],
},
{
deletedAt: null,
},
],
};
if (type) {
whereClause.type = type;
}
if (templateId) {
whereClause.templateId = templateId;
}
if (source) {
whereClause.source = source;
}
if (status) {
whereClause.status = status;
}
if (folderId !== undefined) {
whereClause.folderId = folderId;
} else {
whereClause.folderId = null;
}
const [data, count] = await Promise.all([
prisma.envelope.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
[orderByColumn]: orderByDirection,
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
recipients: {
orderBy: {
id: 'asc',
},
},
team: {
select: {
id: true,
url: true,
},
},
},
}),
prisma.envelope.count({
where: whereClause,
}),
]);
const maskedData = data.map((envelope) =>
maskRecipientTokensForDocument({
document: envelope,
user,
}),
);
const mappedData = maskedData.map((envelope) => ({
...envelope,
recipients: envelope.Recipient,
user: {
id: envelope.user.id,
name: envelope.user.name || '',
email: envelope.user.email,
},
}));
return {
data: mappedData,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof mappedData>;
};

View File

@@ -115,40 +115,5 @@ export type TEnvelopeLite = z.infer<typeof ZEnvelopeLiteSchema>;
/** /**
* A version of the envelope response schema when returning multiple envelopes at once from a single API endpoint. * A version of the envelope response schema when returning multiple envelopes at once from a single API endpoint.
*/ */
export const ZEnvelopeManySchema = EnvelopeSchema.pick({ // export const ZEnvelopeManySchema = X
internalVersion: true, // export type TEnvelopeMany = z.infer<typeof ZEnvelopeManySchema>;
type: true,
status: true,
source: true,
visibility: true,
templateType: true,
id: true,
secondaryId: true,
externalId: true,
createdAt: true,
updatedAt: true,
completedAt: true,
deletedAt: true,
title: true,
authOptions: true,
formValues: true,
publicTitle: true,
publicDescription: true,
userId: true,
teamId: true,
folderId: true,
templateId: true,
}).extend({
user: z.object({
id: z.number(),
name: z.string(),
email: z.string(),
}),
recipients: ZEnvelopeRecipientLiteSchema.array(),
team: TeamSchema.pick({
id: true,
url: true,
}).nullable(),
});
export type TEnvelopeMany = z.infer<typeof ZEnvelopeManySchema>;

View File

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

View File

@@ -0,0 +1,29 @@
import { findEnvelopeAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
import { authenticatedProcedure } from '../trpc';
import {
ZFindEnvelopeAuditLogsRequestSchema,
ZFindEnvelopeAuditLogsResponseSchema,
findEnvelopeAuditLogsMeta,
} from './find-envelope-audit-logs.types';
export const findEnvelopeAuditLogsRoute = authenticatedProcedure
.meta(findEnvelopeAuditLogsMeta)
.input(ZFindEnvelopeAuditLogsRequestSchema)
.output(ZFindEnvelopeAuditLogsResponseSchema)
.query(async ({ input, ctx }) => {
const { orderByColumn, orderByDirection, ...auditLogParams } = input;
ctx.logger.info({
input: {
envelopeId: input.envelopeId,
},
});
return await findEnvelopeAuditLogs({
...auditLogParams,
userId: ctx.user.id,
teamId: ctx.teamId,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
});
});

View File

@@ -0,0 +1,35 @@
import { z } from 'zod';
import { ZDocumentAuditLogSchema } from '@documenso/lib/types/document-audit-logs';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import type { TrpcRouteMeta } from '../trpc';
export const findEnvelopeAuditLogsMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/envelope/{envelopeId}/audit-log',
summary: 'Get envelope audit logs',
description:
'Returns paginated audit logs for an envelope given an ID. Accepts both envelope IDs (string) and legacy document IDs (number).',
tags: ['Envelope'],
},
};
export const ZFindEnvelopeAuditLogsRequestSchema = ZFindSearchParamsSchema.extend({
envelopeId: z
.string()
.describe('Envelope ID (e.g., envelope_xxx) or legacy document ID (e.g., 12345)'),
cursor: z.string().optional(),
filterForRecentActivity: z.boolean().optional(),
orderByColumn: z.enum(['createdAt', 'type']).optional(),
orderByDirection: z.enum(['asc', 'desc']).default('desc'),
});
export const ZFindEnvelopeAuditLogsResponseSchema = ZFindResultResponse.extend({
data: ZDocumentAuditLogSchema.array(),
nextCursor: z.string().optional(),
});
export type TFindEnvelopeAuditLogsRequest = z.infer<typeof ZFindEnvelopeAuditLogsRequestSchema>;
export type TFindEnvelopeAuditLogsResponse = z.infer<typeof ZFindEnvelopeAuditLogsResponseSchema>;

View File

@@ -1,56 +0,0 @@
import { findEnvelopes } from '@documenso/lib/server-only/envelope/find-envelopes';
import { authenticatedProcedure } from '../trpc';
import {
ZFindEnvelopesRequestSchema,
ZFindEnvelopesResponseSchema,
findEnvelopesMeta,
} from './find-envelopes.types';
export const findEnvelopesRoute = authenticatedProcedure
.meta(findEnvelopesMeta)
.input(ZFindEnvelopesRequestSchema)
.output(ZFindEnvelopesResponseSchema)
.query(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const {
query,
type,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
folderId,
} = input;
ctx.logger.info({
input: {
query,
type,
templateId,
source,
status,
folderId,
page,
perPage,
},
});
return await findEnvelopes({
userId: user.id,
teamId,
type,
templateId,
query,
source,
status,
page,
perPage,
folderId,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
});
});

View File

@@ -1,46 +0,0 @@
import { DocumentSource, DocumentStatus, EnvelopeType } from '@prisma/client';
import { z } from 'zod';
import { ZEnvelopeManySchema } from '@documenso/lib/types/envelope';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import type { TrpcRouteMeta } from '../trpc';
export const findEnvelopesMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/envelope',
summary: 'Find envelopes',
description: 'Find envelopes based on search criteria',
tags: ['Envelope'],
},
};
export const ZFindEnvelopesRequestSchema = ZFindSearchParamsSchema.extend({
type: z
.nativeEnum(EnvelopeType)
.describe('Filter envelopes by type (DOCUMENT or TEMPLATE).')
.optional(),
templateId: z
.number()
.describe('Filter envelopes by the template ID used to create it.')
.optional(),
source: z
.nativeEnum(DocumentSource)
.describe('Filter envelopes by how it was created.')
.optional(),
status: z
.nativeEnum(DocumentStatus)
.describe('Filter envelopes by the current status.')
.optional(),
folderId: z.string().describe('Filter envelopes by folder ID.').optional(),
orderByColumn: z.enum(['createdAt']).optional(),
orderByDirection: z.enum(['asc', 'desc']).describe('Sort direction.').default('desc'),
});
export const ZFindEnvelopesResponseSchema = ZFindResultResponse.extend({
data: ZEnvelopeManySchema.array(),
});
export type TFindEnvelopesRequest = z.infer<typeof ZFindEnvelopesRequestSchema>;
export type TFindEnvelopesResponse = z.infer<typeof ZFindEnvelopesResponseSchema>;

View File

@@ -18,7 +18,7 @@ import { createEnvelopeRecipientsRoute } from './envelope-recipients/create-enve
import { deleteEnvelopeRecipientRoute } from './envelope-recipients/delete-envelope-recipient'; import { deleteEnvelopeRecipientRoute } from './envelope-recipients/delete-envelope-recipient';
import { getEnvelopeRecipientRoute } from './envelope-recipients/get-envelope-recipient'; import { getEnvelopeRecipientRoute } from './envelope-recipients/get-envelope-recipient';
import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-envelope-recipients'; import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-envelope-recipients';
import { findEnvelopesRoute } from './find-envelopes'; import { findEnvelopeAuditLogsRoute } from './find-envelope-audit-logs';
import { getEnvelopeRoute } from './get-envelope'; import { getEnvelopeRoute } from './get-envelope';
import { getEnvelopeItemsRoute } from './get-envelope-items'; import { getEnvelopeItemsRoute } from './get-envelope-items';
import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token'; import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token';
@@ -66,7 +66,9 @@ export const envelopeRouter = router({
set: setEnvelopeFieldsRoute, set: setEnvelopeFieldsRoute,
sign: signEnvelopeFieldRoute, sign: signEnvelopeFieldRoute,
}, },
find: findEnvelopesRoute, auditLog: {
find: findEnvelopeAuditLogsRoute,
},
get: getEnvelopeRoute, get: getEnvelopeRoute,
create: createEnvelopeRoute, create: createEnvelopeRoute,
use: useEnvelopeRoute, use: useEnvelopeRoute,