mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
chore: enhance recipient handling and streamline placeholder processing in PDF fields
This commit is contained in:
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user