import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { prisma } from '@documenso/prisma'; import type { Envelope, Field, Recipient } from '@prisma/client'; import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable'; import { convertPlaceholdersToFieldInputs, extractPdfPlaceholders } from '../pdf/auto-place-fields'; import { findRecipientByPlaceholder } from '../pdf/helpers'; import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf'; type UnsafeReplaceEnvelopeItemPdfOptions = { envelope: Pick; /** * Recipients used to resolve placeholder field assignments. * When provided and placeholders are found in the replacement PDF, * fields will be auto-created for matching recipients. */ recipients: Recipient[]; /** * The ID of the envelope item which we will be replacing the PDF for. */ envelopeItemId: string; /** * The ID of the old document data we will be deleting. */ oldDocumentDataId: string; /** * The data we will be replacing. */ data: { title?: string; order?: number; file: File; }; user: { id: number; name: string | null; email: string; }; apiRequestMetadata: ApiRequestMetadata; }; type UnsafeReplaceEnvelopeItemPdfResult = { updatedItem: { id: string; title: string; envelopeId: string; order: number; documentDataId: string; }; /** * The full list of fields for the envelope after the replacement. * * Only returned when fields were created or deleted during the replacement, * otherwise `undefined`. */ fields: Field[] | undefined; }; export const UNSAFE_replaceEnvelopeItemPdf = async ({ envelope, recipients, envelopeItemId, oldDocumentDataId, data, user, apiRequestMetadata, }: UnsafeReplaceEnvelopeItemPdfOptions): Promise => { let buffer = Buffer.from(await data.file.arrayBuffer()); if (envelope.formValues) { buffer = await insertFormValuesInPdf({ pdf: buffer, formValues: envelope.formValues }); } const normalized = await normalizePdf(buffer, { flattenForm: envelope.type !== 'TEMPLATE', }); const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized); // Upload the new PDF and get a new DocumentData record. const { documentData: newDocumentData, filePageCount } = await putPdfFileServerSide({ name: data.file.name, type: 'application/pdf', arrayBuffer: async () => Promise.resolve(cleanedPdf), }); let didFieldsChange = false; const updatedEnvelopeItem = await prisma.$transaction(async (tx) => { await assertEnvelopeMutable(envelope, tx); const updatedItem = await tx.envelopeItem.update({ where: { id: envelopeItemId, envelopeId: envelope.id, }, data: { documentDataId: newDocumentData.id, title: data.title, order: data.order, }, }); // Todo: Audit log if we're updating the title or order. // Delete fields that reference pages beyond the new PDF's page count. const outOfBoundsFields = await tx.field.findMany({ where: { envelopeId: envelope.id, envelopeItemId, page: { gt: filePageCount, }, }, select: { id: true, }, }); const deletedFieldIds = outOfBoundsFields.map((f) => f.id); if (deletedFieldIds.length > 0) { await tx.field.deleteMany({ where: { id: { in: deletedFieldIds, }, }, }); didFieldsChange = true; } if (recipients.length > 0 && placeholders.length > 0) { const orderedRecipients = [...recipients].sort((a, b) => { const aOrder = a.signingOrder ?? Number.MAX_SAFE_INTEGER; const bOrder = b.signingOrder ?? Number.MAX_SAFE_INTEGER; if (aOrder !== bOrder) { return aOrder - bOrder; } return a.id - b.id; }); const fieldsToCreate = convertPlaceholdersToFieldInputs( placeholders, (recipientPlaceholder, placeholder) => findRecipientByPlaceholder(recipientPlaceholder, placeholder, orderedRecipients, orderedRecipients), updatedItem.id, ); if (fieldsToCreate.length > 0) { await tx.field.createMany({ data: fieldsToCreate.map((field) => ({ envelopeId: envelope.id, envelopeItemId: updatedItem.id, recipientId: field.recipientId, type: field.type, page: field.page, positionX: field.positionX, positionY: field.positionY, width: field.width, height: field.height, customText: '', inserted: false, fieldMeta: field.fieldMeta || undefined, })), }); didFieldsChange = true; } } await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_PDF_REPLACED, envelopeId: envelope.id, data: { envelopeItemId: updatedItem.id, envelopeItemTitle: updatedItem.title, }, user: { name: user.name, email: user.email, }, requestMetadata: apiRequestMetadata.requestMetadata, }), }); return updatedItem; }); // Delete the old DocumentData (now orphaned). await prisma.documentData.delete({ where: { id: oldDocumentDataId, }, }); let fields: Field[] | undefined; if (didFieldsChange) { try { fields = await prisma.field.findMany({ where: { envelopeId: envelope.id, }, }); } catch (err) { // Do nothing. console.error(err); } } return { updatedItem: updatedEnvelopeItem, fields, }; };