diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index 675c3b532..d9bc1a6d7 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -13,6 +13,7 @@ import { createField } from '@documenso/lib/server-only/field/create-field'; import { deleteField } from '@documenso/lib/server-only/field/delete-field'; import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id'; import { updateField } from '@documenso/lib/server-only/field/update-field'; +import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf'; import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient'; import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; @@ -20,6 +21,8 @@ import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/s import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; +import { getFile } from '@documenso/lib/universal/upload/get-file'; +import { putFile } from '@documenso/lib/universal/upload/put-file'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client'; @@ -156,6 +159,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { title: body.title, userId: user.id, teamId: team?.id, + formValues: body.formValues, documentDataId: documentData.id, requestMetadata: extractNextApiRequestMetadata(args.req), }); @@ -217,12 +221,37 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { recipients: body.recipients, }); + let documentDataId = document.documentDataId; + + if (body.formValues) { + const pdf = await getFile(document.documentData); + + const prefilled = await insertFormValuesInPdf({ + pdf: Buffer.from(pdf), + formValues: body.formValues, + }); + + const newDocumentData = await putFile({ + name: fileName, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(prefilled), + }); + + documentDataId = newDocumentData.id; + } + await updateDocument({ documentId: document.id, userId: user.id, teamId: team?.id, data: { title: fileName, + formValues: body.formValues, + documentData: { + connect: { + id: documentDataId, + }, + }, }, }); diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index fbe3ba5c1..01f6e2d58 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -73,6 +73,7 @@ export const ZCreateDocumentMutationSchema = z.object({ redirectUrl: z.string(), }) .partial(), + formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(), }); export type TCreateDocumentMutationSchema = z.infer; @@ -112,6 +113,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({ }) .partial() .optional(), + formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(), }); export type TCreateDocumentFromTemplateMutationSchema = z.infer< diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts index ce1f16670..1d145a60d 100644 --- a/packages/lib/server-only/document/create-document.ts +++ b/packages/lib/server-only/document/create-document.ts @@ -14,6 +14,7 @@ export type CreateDocumentOptions = { userId: number; teamId?: number; documentDataId: string; + formValues?: Record; requestMetadata?: RequestMetadata; }; @@ -22,6 +23,7 @@ export const createDocument = async ({ title, documentDataId, teamId, + formValues, requestMetadata, }: CreateDocumentOptions) => { const user = await prisma.user.findFirstOrThrow({ @@ -51,6 +53,7 @@ export const createDocument = async ({ documentDataId, userId, teamId, + formValues, }, }); diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 7c928f9a9..acbcc499f 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -17,6 +17,9 @@ import { RECIPIENT_ROLES_DESCRIPTION, RECIPIENT_ROLE_TO_EMAIL_TYPE, } from '../../constants/recipient-roles'; +import { getFile } from '../../universal/upload/get-file'; +import { putFile } from '../../universal/upload/put-file'; +import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; export type SendDocumentOptions = { @@ -65,6 +68,7 @@ export const sendDocument = async ({ include: { Recipient: true, documentMeta: true, + documentData: true, }, }); @@ -82,6 +86,38 @@ export const sendDocument = async ({ throw new Error('Can not send completed document'); } + const { documentData } = document; + + if (!documentData.data) { + throw new Error('Document data not found'); + } + + if (document.formValues) { + const file = await getFile(documentData); + + const prefilled = await insertFormValuesInPdf({ + pdf: Buffer.from(file), + formValues: document.formValues as Record, + }); + + const newDocumentData = await putFile({ + name: document.title, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(prefilled), + }); + + const result = await prisma.document.update({ + where: { + id: document.id, + }, + data: { + documentDataId: newDocumentData.id, + }, + }); + + Object.assign(document, result); + } + await Promise.all( document.Recipient.map(async (recipient) => { if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) { diff --git a/packages/lib/server-only/pdf/insert-form-values-in-pdf.ts b/packages/lib/server-only/pdf/insert-form-values-in-pdf.ts new file mode 100644 index 000000000..a3c311895 --- /dev/null +++ b/packages/lib/server-only/pdf/insert-form-values-in-pdf.ts @@ -0,0 +1,54 @@ +import { PDFCheckBox, PDFDocument, PDFDropdown, PDFRadioGroup, PDFTextField } from 'pdf-lib'; + +export type InsertFormValuesInPdfOptions = { + pdf: Buffer; + formValues: Record; +}; + +export const insertFormValuesInPdf = async ({ pdf, formValues }: InsertFormValuesInPdfOptions) => { + const doc = await PDFDocument.load(pdf); + + const form = doc.getForm(); + + if (!form) { + return pdf; + } + + for (const [key, value] of Object.entries(formValues)) { + try { + const field = form.getField(key); + + if (!field) { + continue; + } + + if (typeof value === 'boolean' && field instanceof PDFCheckBox) { + if (value) { + field.check(); + } else { + field.uncheck(); + } + } + + if (field instanceof PDFTextField) { + field.setText(value.toString()); + } + + if (field instanceof PDFDropdown) { + field.select(value.toString()); + } + + if (field instanceof PDFRadioGroup) { + field.select(value.toString()); + } + } catch (err) { + if (err instanceof Error) { + console.error(`Error setting value for field ${key}: ${err.message}`); + } else { + console.error(`Error setting value for field ${key}`); + } + } + } + + return await doc.save().then((buf) => Buffer.from(buf)); +}; diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index 55519a30e..8ae5fecaf 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -79,6 +79,7 @@ export const createDocumentFromTemplate = async ({ id: 'asc', }, }, + documentData: true, }, }); diff --git a/packages/prisma/migrations/20240408083413_add_form_values_column/migration.sql b/packages/prisma/migrations/20240408083413_add_form_values_column/migration.sql new file mode 100644 index 000000000..fbf67b637 --- /dev/null +++ b/packages/prisma/migrations/20240408083413_add_form_values_column/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Document" ADD COLUMN "formValues" JSONB; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 868b8d8e1..35d429779 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -257,6 +257,7 @@ model Document { userId Int User User @relation(fields: [userId], references: [id], onDelete: Cascade) authOptions Json? + formValues Json? title String status DocumentStatus @default(DRAFT) Recipient Recipient[]