chore: enhance recipient handling and streamline placeholder processing in PDF fields

This commit is contained in:
Catalin Pit
2025-11-07 10:35:59 +02:00
parent 498a2be1c7
commit 0b91d33bfb
2 changed files with 102 additions and 72 deletions

View File

@ -12,12 +12,18 @@ import { prisma } from '@documenso/prisma';
import { getPageSize } from './get-page-size'; import { getPageSize } from './get-page-size';
import { import {
createRecipientsFromPlaceholders, determineRecipientsForPlaceholders,
extractRecipientPlaceholder, extractRecipientPlaceholder,
findRecipientByPlaceholder,
parseFieldMetaFromPlaceholder, parseFieldMetaFromPlaceholder,
parseFieldTypeFromPlaceholder, parseFieldTypeFromPlaceholder,
} from './helpers'; } from './helpers';
const PLACEHOLDER_REGEX = /{{([^}]+)}}/g;
const DEFAULT_FIELD_HEIGHT_PERCENT = 2;
const WIDTH_ADJUSTMENT_FACTOR = 0.1;
const MIN_HEIGHT_THRESHOLD = 0.01;
type TextPosition = { type TextPosition = {
text: string; text: string;
x: number; x: number;
@ -52,16 +58,6 @@ type FieldToCreate = TFieldAndMeta & {
height: number; height: number;
}; };
/*
Questions for later:
- Does it handle multi-page PDFs? ✅ YES! ✅
- Does it handle multiple recipients on the same page? ✅ YES! ✅
- Does it handle multiple recipients on multiple pages? ✅ YES! ✅
- What happens with incorrect placeholders? E.g. those containing non-accepted properties.
- The placeholder data is dynamic. How to handle this parsing? Perhaps we need to do it similar to the fieldMeta parsing. ✅
- Need to handle envelopes with multiple items. ✅
*/
export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<PlaceholderInfo[]> => { export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<PlaceholderInfo[]> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const parser = new PDFParser(null, true); const parser = new PDFParser(null, true);
@ -114,7 +110,7 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<Placehold
}); });
}); });
const placeholderMatches = pageText.matchAll(/{{([^}]+)}}/g); const placeholderMatches = pageText.matchAll(PLACEHOLDER_REGEX);
/* /*
A placeholder match has the following format: A placeholder match has the following format:
@ -150,12 +146,6 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<Placehold
Then find the position of the characters in the textPositions array. Then find the position of the characters in the textPositions array.
This allows us to quickly find the position of a character in the textPositions array by its index. This allows us to quickly find the position of a character in the textPositions array by its index.
*/ */
if (placeholderMatch.index === undefined) {
console.error('Placeholder match index is undefined for placeholder', placeholder);
continue;
}
const placeholderEndCharIndex = placeholderMatch.index + placeholder.length; const placeholderEndCharIndex = placeholderMatch.index + placeholder.length;
/* /*
@ -179,7 +169,8 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<Placehold
const placeholderStart = textPositions[startTextPosIndex]; const placeholderStart = textPositions[startTextPosIndex];
const placeholderEnd = textPositions[endTextPosIndex]; const placeholderEnd = textPositions[endTextPosIndex];
const width = placeholderEnd.x + placeholderEnd.w * 0.1 - placeholderStart.x; const width =
placeholderEnd.x + placeholderEnd.w * WIDTH_ADJUSTMENT_FACTOR - placeholderStart.x;
placeholders.push({ placeholders.push({
placeholder, placeholder,
@ -298,20 +289,14 @@ export const insertFieldsFromPlaceholdersInPDF = async (
}); });
} }
let createdRecipients: Pick<Recipient, 'id' | 'email'>[]; const createdRecipients = await determineRecipientsForPlaceholders(
recipients,
if (recipients && recipients.length > 0) {
createdRecipients = recipients;
} else {
createdRecipients = await createRecipientsFromPlaceholders(
recipientPlaceholders, recipientPlaceholders,
envelope, envelope,
envelopeId,
userId, userId,
teamId, teamId,
requestMetadata, requestMetadata,
); );
}
const fieldsToCreate: FieldToCreate[] = []; const fieldsToCreate: FieldToCreate[] = [];
@ -327,46 +312,21 @@ export const insertFieldsFromPlaceholdersInPDF = async (
const widthPercent = (placeholder.width / placeholder.pageWidth) * 100; const widthPercent = (placeholder.width / placeholder.pageWidth) * 100;
const heightPercent = (placeholder.height / placeholder.pageHeight) * 100; const heightPercent = (placeholder.height / placeholder.pageHeight) * 100;
let recipient: Pick<Recipient, 'id' | 'email'> | undefined; const recipient = findRecipientByPlaceholder(
placeholder.recipient,
if (recipients && recipients.length > 0) { placeholder.placeholder,
/* recipients,
Map placeholder by index: r1 -> recipients[0], r2 -> recipients[1], etc. createdRecipients,
recipientIndex is 1-based, so we subtract 1 to get the array index. );
*/
const { recipientIndex } = extractRecipientPlaceholder(placeholder.recipient);
const recipientArrayIndex = recipientIndex - 1;
if (recipientArrayIndex < 0 || recipientArrayIndex >= recipients.length) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Recipient placeholder ${placeholder.recipient} (index ${recipientIndex}) is out of range. Provided ${recipients.length} recipient(s).`,
});
}
recipient = recipients[recipientArrayIndex];
} else {
/*
Use email-based matching for placeholder recipients.
*/
const { email } = extractRecipientPlaceholder(placeholder.recipient);
recipient = createdRecipients.find((r) => r.email === email);
}
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Could not find recipient ID for placeholder: ${placeholder.placeholder}`,
});
}
const recipientId = recipient.id;
// Default height percentage if too small (use 2% as a reasonable default) // Default height percentage if too small (use 2% as a reasonable default)
const finalHeightPercent = heightPercent > 0.01 ? heightPercent : 2; const finalHeightPercent =
heightPercent > MIN_HEIGHT_THRESHOLD ? heightPercent : DEFAULT_FIELD_HEIGHT_PERCENT;
fieldsToCreate.push({ fieldsToCreate.push({
...placeholder.fieldAndMeta, ...placeholder.fieldAndMeta,
envelopeItemId, envelopeItemId,
recipientId, recipientId: recipient.id,
pageNumber: placeholder.page, pageNumber: placeholder.page,
pageX: xPercent, pageX: xPercent,
pageY: yPercent, pageY: yPercent,

View File

@ -115,10 +115,78 @@ export const extractRecipientPlaceholder = (placeholder: string): RecipientPlace
}; };
}; };
/*
Finds a recipient based on a placeholder reference.
If recipients array is provided, uses index-based matching (r1 -> recipients[0], etc.).
Otherwise, uses email-based matching from createdRecipients.
*/
export const findRecipientByPlaceholder = (
recipientPlaceholder: string,
placeholder: string,
recipients: Pick<Recipient, 'id' | 'email'>[] | undefined,
createdRecipients: Pick<Recipient, 'id' | 'email'>[],
): Pick<Recipient, 'id' | 'email'> => {
if (recipients && recipients.length > 0) {
/*
Map placeholder by index: r1 -> recipients[0], r2 -> recipients[1], etc.
recipientIndex is 1-based, so we subtract 1 to get the array index.
*/
const { recipientIndex } = extractRecipientPlaceholder(recipientPlaceholder);
const recipientArrayIndex = recipientIndex - 1;
if (recipientArrayIndex < 0 || recipientArrayIndex >= recipients.length) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Recipient placeholder ${recipientPlaceholder} (index ${recipientIndex}) is out of range. Provided ${recipients.length} recipient(s).`,
});
}
return recipients[recipientArrayIndex];
}
/*
Use email-based matching for placeholder recipients.
*/
const { email } = extractRecipientPlaceholder(recipientPlaceholder);
const recipient = createdRecipients.find((r) => r.email === email);
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Could not find recipient ID for placeholder: ${placeholder}`,
});
}
return recipient;
};
/*
Determines the recipients to use for field creation.
If recipients are provided, uses them directly.
Otherwise, creates recipients from placeholders.
*/
export const determineRecipientsForPlaceholders = async (
recipients: Pick<Recipient, 'id' | 'email'>[] | undefined,
recipientPlaceholders: Map<number, string>,
envelope: Pick<Envelope, 'id' | 'type' | 'secondaryId'>,
userId: number,
teamId: number,
requestMetadata: ApiRequestMetadata,
): Promise<Pick<Recipient, 'id' | 'email'>[]> => {
if (recipients && recipients.length > 0) {
return recipients;
}
return createRecipientsFromPlaceholders(
recipientPlaceholders,
envelope,
userId,
teamId,
requestMetadata,
);
};
export const createRecipientsFromPlaceholders = async ( export const createRecipientsFromPlaceholders = async (
recipientPlaceholders: Map<number, string>, recipientPlaceholders: Map<number, string>,
envelope: Pick<Envelope, 'id' | 'type' | 'secondaryId'>, envelope: Pick<Envelope, 'id' | 'type' | 'secondaryId'>,
envelopeId: EnvelopeIdOptions,
userId: number, userId: number,
teamId: number, teamId: number,
requestMetadata: ApiRequestMetadata, requestMetadata: ApiRequestMetadata,
@ -156,6 +224,11 @@ export const createRecipientsFromPlaceholders = async (
const newRecipients = await match(envelope.type) const newRecipients = await match(envelope.type)
.with(EnvelopeType.DOCUMENT, async () => { .with(EnvelopeType.DOCUMENT, async () => {
const envelopeId: EnvelopeIdOptions = {
type: 'envelopeId',
id: envelope.id,
};
const { recipients } = await createDocumentRecipients({ const { recipients } = await createDocumentRecipients({
userId, userId,
teamId, teamId,
@ -167,10 +240,7 @@ export const createRecipientsFromPlaceholders = async (
return recipients; return recipients;
}) })
.with(EnvelopeType.TEMPLATE, async () => { .with(EnvelopeType.TEMPLATE, async () => {
const templateId = const templateId = mapSecondaryIdToTemplateId(envelope.secondaryId ?? '');
envelopeId.type === 'templateId'
? envelopeId.id
: mapSecondaryIdToTemplateId(envelope.secondaryId ?? '');
const { recipients } = await createTemplateRecipients({ const { recipients } = await createTemplateRecipients({
userId, userId,