From b3cb75047021e5318a263fecd91d9c419374dc20 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 31 Oct 2025 11:41:34 +0200 Subject: [PATCH] feat: refactor field metadata handling and enhance field type parsing in PDF processing --- .../lib/server-only/pdf/auto-place-fields.ts | 183 +++++++++--------- 1 file changed, 95 insertions(+), 88 deletions(-) diff --git a/packages/lib/server-only/pdf/auto-place-fields.ts b/packages/lib/server-only/pdf/auto-place-fields.ts index a3ac7d4e0..8998057a0 100644 --- a/packages/lib/server-only/pdf/auto-place-fields.ts +++ b/packages/lib/server-only/pdf/auto-place-fields.ts @@ -31,9 +31,8 @@ type CharIndexMapping = { type PlaceholderInfo = { placeholder: string; - fieldType: string; recipient: string; - fieldMeta: Record; + fieldAndMeta: TFieldAndMeta; page: number; x: number; y: number; @@ -62,6 +61,88 @@ type FieldToCreate = TFieldAndMeta & { - Need to handle envelopes with multiple items. */ +/* + Parse field type string to FieldType enum. + Normalizes the input (uppercase, trim) and validates it's a valid field type. + This ensures we handle case variations and whitespace, and provides clear error messages. +*/ +const parseFieldType = (fieldTypeString: string): FieldType => { + const normalizedType = fieldTypeString.toUpperCase().trim(); + + return match(normalizedType) + .with('SIGNATURE', () => FieldType.SIGNATURE) + .with('FREE_SIGNATURE', () => FieldType.FREE_SIGNATURE) + .with('INITIALS', () => FieldType.INITIALS) + .with('NAME', () => FieldType.NAME) + .with('EMAIL', () => FieldType.EMAIL) + .with('DATE', () => FieldType.DATE) + .with('TEXT', () => FieldType.TEXT) + .with('NUMBER', () => FieldType.NUMBER) + .with('RADIO', () => FieldType.RADIO) + .with('CHECKBOX', () => FieldType.CHECKBOX) + .with('DROPDOWN', () => FieldType.DROPDOWN) + .otherwise(() => { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: `Invalid field type: ${fieldTypeString}`, + }); + }); +}; + +/* + Transform raw field metadata from placeholder format to schema format. + Users should provide properly capitalized property names (e.g., readOnly, fontSize, textAlign). + Converts string values to proper types (booleans, numbers). +*/ +const parseFieldMeta = ( + rawFieldMeta: Record, + fieldType: FieldType, +): Record | undefined => { + if (fieldType === FieldType.SIGNATURE || fieldType === FieldType.FREE_SIGNATURE) { + return; + } + + if (Object.keys(rawFieldMeta).length === 0) { + return; + } + + const fieldTypeString = String(fieldType).toLowerCase(); + + const parsedFieldMeta: Record = { + type: fieldTypeString, + }; + + /* + rawFieldMeta is an object with string keys and string values. + It contains string values because the PDF parser returns the values as strings. + + E.g. { required: 'true', fontSize: '12', maxValue: '100', minValue: '0', characterLimit: '100' } + */ + const rawFieldMetaEntries = Object.entries(rawFieldMeta); + + for (const entry of rawFieldMetaEntries) { + const [key, value] = entry; + + if (key === 'readOnly' || key === 'required') { + parsedFieldMeta[key] = value === 'true'; + } else if ( + key === 'fontSize' || + key === 'maxValue' || + key === 'minValue' || + key === 'characterLimit' + ) { + const numValue = Number(value); + + if (!Number.isNaN(numValue)) { + parsedFieldMeta[key] = numValue; + } + } else { + parsedFieldMeta[key] = value; + } + } + + return parsedFieldMeta; +}; + export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise => { return new Promise((resolve, reject) => { const parser = new PDFParser(null, true); @@ -119,9 +200,17 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise part.trim()); - const [fieldType, recipient, ...fieldMetaData] = placeholderData; + const [fieldTypeString, recipient, ...fieldMetaData] = placeholderData; - const fieldMeta = Object.fromEntries(fieldMetaData.map((meta) => meta.split('='))); + const rawFieldMeta = Object.fromEntries(fieldMetaData.map((meta) => meta.split('='))); + + const fieldType = parseFieldType(fieldTypeString); + const parsedFieldMeta = parseFieldMeta(rawFieldMeta, fieldType); + + const fieldAndMeta: TFieldAndMeta = ZFieldAndMetaSchema.parse({ + type: fieldType, + fieldMeta: parsedFieldMeta, + }); /* Find the position of where the placeholder starts in the text @@ -155,9 +244,8 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise { - const normalizedType = fieldTypeString.toUpperCase().trim(); - - return match(normalizedType) - .with('SIGNATURE', () => FieldType.SIGNATURE) - .with('FREE_SIGNATURE', () => FieldType.FREE_SIGNATURE) - .with('INITIALS', () => FieldType.INITIALS) - .with('NAME', () => FieldType.NAME) - .with('EMAIL', () => FieldType.EMAIL) - .with('DATE', () => FieldType.DATE) - .with('TEXT', () => FieldType.TEXT) - .with('NUMBER', () => FieldType.NUMBER) - .with('RADIO', () => FieldType.RADIO) - .with('CHECKBOX', () => FieldType.CHECKBOX) - .with('DROPDOWN', () => FieldType.DROPDOWN) - .otherwise(() => { - throw new AppError(AppErrorCode.INVALID_BODY, { - message: `Invalid field type: ${fieldTypeString}`, - }); - }); -}; - -/* - Transform raw field metadata from placeholder format to schema format. - Users should provide properly capitalized property names (e.g., readOnly, fontSize, textAlign). - Converts string values to proper types (booleans, numbers). -*/ -const transformFieldMeta = ( - rawMeta: Record, - fieldType: FieldType, -): Record | undefined => { - if (fieldType === FieldType.SIGNATURE || fieldType === FieldType.FREE_SIGNATURE) { - return undefined; - } - - if (Object.keys(rawMeta).length === 0) { - return undefined; - } - - const fieldTypeString = String(fieldType).toLowerCase(); - - const transformed: Record = { - type: fieldTypeString, - }; - - for (const [key, value] of Object.entries(rawMeta)) { - if (key === 'readOnly' || key === 'required') { - transformed[key] = value === 'true'; - } else if ( - key === 'fontSize' || - key === 'maxValue' || - key === 'minValue' || - key === 'characterLimit' - ) { - const numValue = Number(value); - - if (!Number.isNaN(numValue)) { - transformed[key] = numValue; - } - } else { - transformed[key] = value; - } - } - - return transformed; -}; - export const insertFieldsFromPlaceholdersInPDF = async ( pdf: Buffer, userId: number, @@ -428,8 +444,6 @@ export const insertFieldsFromPlaceholdersInPDF = async ( const widthPercent = (placeholder.width / placeholder.pageWidth) * 100; const heightPercent = (placeholder.height / placeholder.pageHeight) * 100; - const fieldType = parseFieldType(placeholder.fieldType); - const { email } = extractRecipientPlaceholder(placeholder.recipient); const normalizedEmail = email.toLowerCase(); const recipient = createdRecipients.find((r) => r.email.toLowerCase() === normalizedEmail); @@ -445,15 +459,8 @@ export const insertFieldsFromPlaceholdersInPDF = async ( // Default height percentage if too small (use 2% as a reasonable default) const finalHeightPercent = heightPercent > 0.01 ? heightPercent : 2; - const transformedFieldMeta = transformFieldMeta(placeholder.fieldMeta, fieldType); - - const baseFieldAndMeta: TFieldAndMeta = ZFieldAndMetaSchema.parse({ - type: fieldType, - fieldMeta: transformedFieldMeta, - }); - fieldsToCreate.push({ - ...baseFieldAndMeta, + ...placeholder.fieldAndMeta, recipientId, pageNumber: placeholder.page, pageX: xPercent,