Files
documenso/packages/lib/utils/envelope-signing.ts
David Nguyen 03eb6af69a feat: polish envelopes (#2090)
## Description

The rest of the owl
2025-10-24 16:22:06 +11:00

264 lines
7.9 KiB
TypeScript

import type { Field } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { DateTime } from 'luxon';
import { P, match } from 'ts-pattern';
import { z } from 'zod';
import { validateCheckboxLength } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { isBase64Image } from '@documenso/lib/constants/signatures';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TDocumentMeta } from '@documenso/lib/types/document-meta';
import {
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZNumberFieldMeta,
ZRadioFieldMeta,
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import { toCheckboxCustomText, toRadioCustomText } from '@documenso/lib/utils/fields';
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
export type ExtractFieldInsertionValuesOptions = {
fieldValue: TSignEnvelopeFieldValue;
field: Field;
documentMeta: Pick<TDocumentMeta, 'timezone' | 'dateFormat' | 'typedSignatureEnabled'>;
};
export const extractFieldInsertionValues = ({
fieldValue,
field,
documentMeta,
}: ExtractFieldInsertionValuesOptions): { customText: string; inserted: boolean } => {
return match(fieldValue)
.with({ type: FieldType.EMAIL }, (fieldValue) => {
const parsedEmailValue = z.string().email().nullable().safeParse(fieldValue.value);
if (!parsedEmailValue.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid email',
});
}
if (parsedEmailValue.data === null) {
return {
customText: '',
inserted: false,
};
}
return {
customText: parsedEmailValue.data,
inserted: true,
};
})
.with({ type: P.union(FieldType.NAME, FieldType.INITIALS) }, (fieldValue) => {
const parsedGenericStringValue = z.string().min(1).nullable().safeParse(fieldValue.value);
if (!parsedGenericStringValue.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Value is required',
});
}
if (parsedGenericStringValue.data === null) {
return {
customText: '',
inserted: false,
};
}
return {
customText: parsedGenericStringValue.data,
inserted: true,
};
})
.with({ type: FieldType.DATE }, (fieldValue) => {
if (!fieldValue.value) {
return {
customText: '',
inserted: false,
};
}
return {
customText: DateTime.now()
.setZone(documentMeta.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE)
.toFormat(documentMeta.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT),
inserted: true,
};
})
.with({ type: FieldType.NUMBER }, (fieldValue) => {
if (!fieldValue.value) {
return {
customText: '',
inserted: false,
};
}
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
const errors = validateNumberField(fieldValue.value.toString(), numberFieldParsedMeta, true);
// Todo
if (errors.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid number',
});
}
return {
customText: fieldValue.value.toString(),
inserted: true,
};
})
.with({ type: FieldType.TEXT }, (fieldValue) => {
if (fieldValue.value === null) {
return {
customText: '',
inserted: false,
};
}
const parsedTextFieldMeta = ZTextFieldMeta.parse(field.fieldMeta);
const errors = validateTextField(fieldValue.value, parsedTextFieldMeta, true);
// Todo
if (errors.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid email',
});
}
return {
customText: fieldValue.value,
inserted: true,
};
})
.with({ type: FieldType.RADIO }, (fieldValue) => {
if (fieldValue.value === null) {
return {
customText: '',
inserted: false,
};
}
const parsedRadioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
const radioFieldValues = parsedRadioFieldParsedMeta.values || [];
if (!radioFieldValues[fieldValue.value]) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid radio value',
});
}
return {
customText: toRadioCustomText(fieldValue.value),
inserted: true,
};
})
.with({ type: FieldType.CHECKBOX }, (fieldValue) => {
if (fieldValue.value === null || fieldValue.value.length === 0) {
return {
customText: '',
inserted: false,
};
}
const parsedCheckboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
const checkboxFieldValues = parsedCheckboxFieldParsedMeta.values || [];
const { value } = fieldValue;
const selectedValues = value.map((valueIndex) => checkboxFieldValues[valueIndex]);
if (selectedValues.some((value) => !value)) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid checkbox values',
});
}
const { validationRule, validationLength } = parsedCheckboxFieldParsedMeta;
if (validationRule && validationLength) {
const checkboxValidationRule = checkboxValidationSigns.find(
(sign) => sign.label === validationRule,
);
// Todo: Envelopes - Test this.
if (checkboxValidationRule) {
const isValid = validateCheckboxLength(
selectedValues.length,
checkboxValidationRule.value,
validationLength,
);
if (!isValid) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Checkbox values failed length validation',
});
}
} else {
// Should throw an error, but we don't want to throw configuration errors during signing.
// Todo: Logging.
}
}
return {
customText: toCheckboxCustomText(fieldValue.value),
inserted: true,
};
})
.with({ type: FieldType.DROPDOWN }, (fieldValue) => {
if (fieldValue.value === null) {
return {
customText: '',
inserted: false,
};
}
const parsedDropdownFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
const errors = validateDropdownField(fieldValue.value, parsedDropdownFieldMeta, true);
// Todo: Envelopes
if (errors.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid dropdown value',
});
}
return {
customText: fieldValue.value,
inserted: true,
};
})
.with({ type: FieldType.SIGNATURE }, (fieldValue) => {
const { value } = fieldValue;
if (!value) {
return {
customText: '',
inserted: false,
};
}
const isBase64 = isBase64Image(value);
if (documentMeta.typedSignatureEnabled === false && !isBase64) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Typed signatures are not allowed. Please draw your signature',
});
}
return {
customText: '',
inserted: true,
};
})
.exhaustive();
};