feat: add prefilling pdf form fields via api

This commit is contained in:
Mythie
2024-04-08 17:01:11 +07:00
parent 48a8f5fe07
commit 08b693ff95
8 changed files with 128 additions and 0 deletions

View File

@ -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 { deleteField } from '@documenso/lib/server-only/field/delete-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id'; import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { updateField } from '@documenso/lib/server-only/field/update-field'; 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 { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id'; import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; 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 { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; 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 { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client'; import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
@ -156,6 +159,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
title: body.title, title: body.title,
userId: user.id, userId: user.id,
teamId: team?.id, teamId: team?.id,
formValues: body.formValues,
documentDataId: documentData.id, documentDataId: documentData.id,
requestMetadata: extractNextApiRequestMetadata(args.req), requestMetadata: extractNextApiRequestMetadata(args.req),
}); });
@ -217,12 +221,37 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
recipients: body.recipients, 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({ await updateDocument({
documentId: document.id, documentId: document.id,
userId: user.id, userId: user.id,
teamId: team?.id, teamId: team?.id,
data: { data: {
title: fileName, title: fileName,
formValues: body.formValues,
documentData: {
connect: {
id: documentDataId,
},
},
}, },
}); });

View File

@ -73,6 +73,7 @@ export const ZCreateDocumentMutationSchema = z.object({
redirectUrl: z.string(), redirectUrl: z.string(),
}) })
.partial(), .partial(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
}); });
export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutationSchema>; export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutationSchema>;
@ -112,6 +113,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
}) })
.partial() .partial()
.optional(), .optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
}); });
export type TCreateDocumentFromTemplateMutationSchema = z.infer< export type TCreateDocumentFromTemplateMutationSchema = z.infer<

View File

@ -14,6 +14,7 @@ export type CreateDocumentOptions = {
userId: number; userId: number;
teamId?: number; teamId?: number;
documentDataId: string; documentDataId: string;
formValues?: Record<string, string | number | boolean>;
requestMetadata?: RequestMetadata; requestMetadata?: RequestMetadata;
}; };
@ -22,6 +23,7 @@ export const createDocument = async ({
title, title,
documentDataId, documentDataId,
teamId, teamId,
formValues,
requestMetadata, requestMetadata,
}: CreateDocumentOptions) => { }: CreateDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({ const user = await prisma.user.findFirstOrThrow({
@ -51,6 +53,7 @@ export const createDocument = async ({
documentDataId, documentDataId,
userId, userId,
teamId, teamId,
formValues,
}, },
}); });

View File

@ -17,6 +17,9 @@ import {
RECIPIENT_ROLES_DESCRIPTION, RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE, RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '../../constants/recipient-roles'; } 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'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type SendDocumentOptions = { export type SendDocumentOptions = {
@ -65,6 +68,7 @@ export const sendDocument = async ({
include: { include: {
Recipient: true, Recipient: true,
documentMeta: true, documentMeta: true,
documentData: true,
}, },
}); });
@ -82,6 +86,38 @@ export const sendDocument = async ({
throw new Error('Can not send completed document'); 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<string, string | number | boolean>,
});
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( await Promise.all(
document.Recipient.map(async (recipient) => { document.Recipient.map(async (recipient) => {
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) { if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {

View File

@ -0,0 +1,54 @@
import { PDFCheckBox, PDFDocument, PDFDropdown, PDFRadioGroup, PDFTextField } from 'pdf-lib';
export type InsertFormValuesInPdfOptions = {
pdf: Buffer;
formValues: Record<string, string | boolean | number>;
};
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));
};

View File

@ -79,6 +79,7 @@ export const createDocumentFromTemplate = async ({
id: 'asc', id: 'asc',
}, },
}, },
documentData: true,
}, },
}); });

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Document" ADD COLUMN "formValues" JSONB;

View File

@ -257,6 +257,7 @@ model Document {
userId Int userId Int
User User @relation(fields: [userId], references: [id], onDelete: Cascade) User User @relation(fields: [userId], references: [id], onDelete: Cascade)
authOptions Json? authOptions Json?
formValues Json?
title String title String
status DocumentStatus @default(DRAFT) status DocumentStatus @default(DRAFT)
Recipient Recipient[] Recipient Recipient[]