mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: add prefilling pdf form fields via api
This commit is contained in:
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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<
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
54
packages/lib/server-only/pdf/insert-form-values-in-pdf.ts
Normal file
54
packages/lib/server-only/pdf/insert-form-values-in-pdf.ts
Normal 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));
|
||||||
|
};
|
||||||
@ -79,6 +79,7 @@ export const createDocumentFromTemplate = async ({
|
|||||||
id: 'asc',
|
id: 'asc',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
documentData: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Document" ADD COLUMN "formValues" JSONB;
|
||||||
@ -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[]
|
||||||
|
|||||||
Reference in New Issue
Block a user