mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
This PR is handles the changes required to support envelopes. The new envelope editor/signing page will be hidden during release. The core changes here is to migrate the documents and templates model to a centralized envelopes model. Even though Documents and Templates are removed, from the user perspective they will still exist as we remap envelopes to documents and templates.
249 lines
7.5 KiB
TypeScript
249 lines
7.5 KiB
TypeScript
import { EnvelopeType, FieldType } from '@prisma/client';
|
|
|
|
import { validateCheckboxField } 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 { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
|
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
|
|
import {
|
|
type TFieldMetaSchema as FieldMeta,
|
|
ZCheckboxFieldMeta,
|
|
ZDropdownFieldMeta,
|
|
ZFieldMetaSchema,
|
|
ZNumberFieldMeta,
|
|
ZRadioFieldMeta,
|
|
ZTextFieldMeta,
|
|
} from '@documenso/lib/types/field-meta';
|
|
import { prisma } from '@documenso/prisma';
|
|
|
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
|
import { mapFieldToLegacyField } from '../../utils/fields';
|
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
|
|
|
export type SetFieldsForTemplateOptions = {
|
|
userId: number;
|
|
teamId: number;
|
|
id: EnvelopeIdOptions;
|
|
fields: {
|
|
id?: number | null;
|
|
envelopeItemId: string;
|
|
type: FieldType;
|
|
recipientId: number;
|
|
pageNumber: number;
|
|
pageX: number;
|
|
pageY: number;
|
|
pageWidth: number;
|
|
pageHeight: number;
|
|
fieldMeta?: FieldMeta;
|
|
}[];
|
|
};
|
|
|
|
export const setFieldsForTemplate = async ({
|
|
userId,
|
|
teamId,
|
|
id,
|
|
fields,
|
|
}: SetFieldsForTemplateOptions) => {
|
|
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
|
id,
|
|
type: EnvelopeType.TEMPLATE,
|
|
userId,
|
|
teamId,
|
|
});
|
|
|
|
const envelope = await prisma.envelope.findFirst({
|
|
where: envelopeWhereInput,
|
|
include: {
|
|
recipients: true,
|
|
envelopeItems: {
|
|
select: {
|
|
id: true,
|
|
},
|
|
},
|
|
fields: {
|
|
include: {
|
|
recipient: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!envelope) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Document not found',
|
|
});
|
|
}
|
|
|
|
const existingFields = envelope.fields;
|
|
|
|
const removedFields = existingFields.filter(
|
|
(existingField) => !fields.find((field) => field.id === existingField.id),
|
|
);
|
|
|
|
const linkedFields = fields.map((field) => {
|
|
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
|
|
|
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
|
|
|
|
// Check whether the field is being attached to an allowed envelope item.
|
|
const foundEnvelopeItem = envelope.envelopeItems.find(
|
|
(envelopeItem) => envelopeItem.id === field.envelopeItemId,
|
|
);
|
|
|
|
if (!foundEnvelopeItem) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: `Envelope item ${field.envelopeItemId} not found`,
|
|
});
|
|
}
|
|
|
|
// Each field MUST have a recipient associated with it.
|
|
if (!recipient) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: `Recipient not found for field ${field.id}`,
|
|
});
|
|
}
|
|
|
|
return {
|
|
...field,
|
|
_persisted: existing,
|
|
_recipient: recipient,
|
|
};
|
|
});
|
|
|
|
const persistedFields = await prisma.$transaction(
|
|
// Disabling as wrapping promises here causes type issues
|
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
linkedFields.map((field) => {
|
|
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
|
|
|
|
if (field.type === FieldType.TEXT && field.fieldMeta) {
|
|
const textFieldParsedMeta = ZTextFieldMeta.parse(field.fieldMeta);
|
|
const errors = validateTextField(textFieldParsedMeta.text || '', textFieldParsedMeta);
|
|
if (errors.length > 0) {
|
|
throw new Error(errors.join(', '));
|
|
}
|
|
}
|
|
|
|
if (field.type === FieldType.NUMBER && field.fieldMeta) {
|
|
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
|
const errors = validateNumberField(
|
|
String(numberFieldParsedMeta.value),
|
|
numberFieldParsedMeta,
|
|
);
|
|
if (errors.length > 0) {
|
|
throw new Error(errors.join(', '));
|
|
}
|
|
}
|
|
|
|
if (field.type === FieldType.CHECKBOX) {
|
|
if (!field.fieldMeta) {
|
|
throw new Error('Checkbox field is missing required metadata');
|
|
}
|
|
const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
|
const errors = validateCheckboxField(
|
|
checkboxFieldParsedMeta?.values?.map((item) => item.value) ?? [],
|
|
checkboxFieldParsedMeta,
|
|
);
|
|
if (errors.length > 0) {
|
|
throw new Error(errors.join(', '));
|
|
}
|
|
}
|
|
|
|
if (field.type === FieldType.RADIO) {
|
|
if (!field.fieldMeta) {
|
|
throw new Error('Radio field is missing required metadata');
|
|
}
|
|
const radioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
|
const checkedRadioFieldValue = radioFieldParsedMeta.values?.find(
|
|
(option) => option.checked,
|
|
)?.value;
|
|
const errors = validateRadioField(checkedRadioFieldValue, radioFieldParsedMeta);
|
|
if (errors.length > 0) {
|
|
throw new Error(errors.join('. '));
|
|
}
|
|
}
|
|
|
|
if (field.type === FieldType.DROPDOWN) {
|
|
if (!field.fieldMeta) {
|
|
throw new Error('Dropdown field is missing required metadata');
|
|
}
|
|
const dropdownFieldParsedMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
|
const errors = validateDropdownField(undefined, dropdownFieldParsedMeta);
|
|
if (errors.length > 0) {
|
|
throw new Error(errors.join('. '));
|
|
}
|
|
}
|
|
|
|
// Proceed with upsert operation
|
|
return prisma.field.upsert({
|
|
where: {
|
|
id: field._persisted?.id ?? -1,
|
|
envelopeId: envelope.id,
|
|
envelopeItemId: field.envelopeItemId,
|
|
},
|
|
update: {
|
|
page: field.pageNumber,
|
|
positionX: field.pageX,
|
|
positionY: field.pageY,
|
|
width: field.pageWidth,
|
|
height: field.pageHeight,
|
|
fieldMeta: parsedFieldMeta,
|
|
},
|
|
create: {
|
|
type: field.type,
|
|
page: field.pageNumber,
|
|
positionX: field.pageX,
|
|
positionY: field.pageY,
|
|
width: field.pageWidth,
|
|
height: field.pageHeight,
|
|
customText: '',
|
|
inserted: false,
|
|
fieldMeta: parsedFieldMeta,
|
|
envelope: {
|
|
connect: {
|
|
id: envelope.id,
|
|
},
|
|
},
|
|
envelopeItem: {
|
|
connect: {
|
|
id: field.envelopeItemId,
|
|
envelopeId: envelope.id,
|
|
},
|
|
},
|
|
recipient: {
|
|
connect: {
|
|
id: field._recipient.id,
|
|
envelopeId: envelope.id,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}),
|
|
);
|
|
|
|
if (removedFields.length > 0) {
|
|
await prisma.field.deleteMany({
|
|
where: {
|
|
id: {
|
|
in: removedFields.map((field) => field.id),
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
// Filter out fields that have been removed or have been updated.
|
|
const filteredFields = existingFields.filter((field) => {
|
|
const isRemoved = removedFields.find((removedField) => removedField.id === field.id);
|
|
const isUpdated = persistedFields.find((persistedField) => persistedField.id === field.id);
|
|
|
|
return !isRemoved && !isUpdated;
|
|
});
|
|
|
|
return {
|
|
fields: [...filteredFields, ...persistedFields].map((field) =>
|
|
mapFieldToLegacyField(field, envelope),
|
|
),
|
|
};
|
|
};
|