feat: migrate templates and documents to envelope model

This commit is contained in:
David Nguyen
2025-09-11 18:23:38 +10:00
parent eec2307634
commit bf89bc781b
234 changed files with 8677 additions and 6054 deletions

View File

@ -1,126 +0,0 @@
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { getDocumentWhereInput } from '../document/get-document-by-id';
export interface CreateDocumentFieldsOptions {
userId: number;
teamId: number;
documentId: number;
fields: (TFieldAndMeta & {
recipientId: number;
pageNumber: number;
pageX: number;
pageY: number;
width: number;
height: number;
})[];
requestMetadata: ApiRequestMetadata;
}
export const createDocumentFields = async ({
userId,
teamId,
documentId,
fields,
requestMetadata,
}: CreateDocumentFieldsOptions) => {
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
userId,
teamId,
});
const document = await prisma.document.findFirst({
where: documentWhereInput,
include: {
recipients: true,
fields: true,
},
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
if (document.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document already complete',
});
}
// Field validation.
const validatedFields = fields.map((field) => {
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
// Each field MUST have a recipient associated with it.
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Recipient ${field.recipientId} not found`,
});
}
// Check whether the recipient associated with the field can have new fields created.
if (!canRecipientFieldsBeModified(recipient, document.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'Recipient type cannot have fields, or they have already interacted with the document.',
});
}
return {
...field,
recipientEmail: recipient.email,
};
});
const createdFields = await prisma.$transaction(async (tx) => {
return await Promise.all(
validatedFields.map(async (field) => {
const createdField = await tx.field.create({
data: {
type: field.type,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta,
documentId,
recipientId: field.recipientId,
},
});
// Handle field created audit log.
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
documentId,
metadata: requestMetadata,
data: {
fieldId: createdField.secondaryId,
fieldRecipientEmail: field.recipientEmail,
fieldRecipientId: createdField.recipientId,
fieldType: createdField.type,
},
}),
});
return createdField;
}),
);
});
return {
fields: createdFields,
};
};

View File

@ -0,0 +1,167 @@
import { EnvelopeType } from '@prisma/client';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface CreateEnvelopeFieldsOptions {
userId: number;
teamId: number;
id: EnvelopeIdOptions;
fields: (TFieldAndMeta & {
/**
* The ID of the item to insert the fields into.
*
* If blank, the first item will be used.
*/
envelopeItemId?: string;
recipientId: number;
pageNumber: number;
pageX: number;
pageY: number;
width: number;
height: number;
})[];
requestMetadata: ApiRequestMetadata;
}
export const createEnvelopeFields = async ({
userId,
teamId,
id,
fields,
requestMetadata,
}: CreateEnvelopeFieldsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id,
type: null, // Null to allow any type of envelope.
userId,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
fields: true,
envelopeItems: {
select: {
id: true,
},
},
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
if (envelope.type === EnvelopeType.DOCUMENT && envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope already complete',
});
}
const firstEnvelopeItem = envelope.envelopeItems[0];
if (!firstEnvelopeItem) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope item not found',
});
}
// Field validation.
const validatedFields = fields.map((field) => {
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
// The item to attach the fields to MUST belong to the document.
if (
field.envelopeItemId &&
!envelope.envelopeItems.find((envelopeItem) => envelopeItem.id === field.envelopeItemId) // Todo: Migration test this
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Item to attach fields to must belong to the document',
});
}
// Each field MUST have a recipient associated with it.
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Recipient ${field.recipientId} not found`,
});
}
// Check whether the recipient associated with the field can have new fields created.
if (!canRecipientFieldsBeModified(recipient, envelope.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'Recipient type cannot have fields, or they have already interacted with the document.',
});
}
return {
...field,
envelopeItemId: field.envelopeItemId || firstEnvelopeItem.id, // Fallback to first envelope item if no envelope item ID is provided.
recipientEmail: recipient.email,
};
});
const createdFields = await prisma.$transaction(async (tx) => {
const newlyCreatedFields = await tx.field.createManyAndReturn({
data: validatedFields.map((field) => ({
type: field.type,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta,
envelopeId: envelope.id,
envelopeItemId: field.envelopeItemId,
recipientId: field.recipientId,
})),
});
// Handle field created audit log.
if (envelope.type === EnvelopeType.DOCUMENT) {
await tx.documentAuditLog.createMany({
data: newlyCreatedFields.map((createdField) => {
const recipient = validatedFields.find(
(field) => field.recipientId === createdField.recipientId,
);
return createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
envelopeId: envelope.id,
metadata: requestMetadata,
data: {
fieldId: createdField.secondaryId,
fieldRecipientEmail: recipient?.recipientEmail || '',
fieldRecipientId: createdField.recipientId,
fieldType: createdField.type,
},
});
}),
});
}
return newlyCreatedFields;
});
return {
fields: createdFields,
};
};

View File

@ -1,136 +0,0 @@
import type { FieldType } from '@prisma/client';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import {
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZNumberFieldMeta,
ZRadioFieldMeta,
ZTextFieldMeta,
} from '../../types/field-meta';
import type { TFieldMetaSchema as FieldMeta } from '../../types/field-meta';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { getDocumentWhereInput } from '../document/get-document-by-id';
export type CreateFieldOptions = {
documentId: number;
userId: number;
teamId: number;
recipientId: number;
type: FieldType;
pageNumber: number;
pageX: number;
pageY: number;
pageWidth: number;
pageHeight: number;
fieldMeta?: FieldMeta;
requestMetadata?: RequestMetadata;
};
export const createField = async ({
documentId,
userId,
teamId,
recipientId,
type,
pageNumber,
pageX,
pageY,
pageWidth,
pageHeight,
fieldMeta,
requestMetadata,
}: CreateFieldOptions) => {
const { documentWhereInput, team } = await getDocumentWhereInput({
documentId,
userId,
teamId,
});
const document = await prisma.document.findFirst({
where: documentWhereInput,
select: {
id: true,
},
});
if (!document) {
throw new Error('Document not found');
}
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(type);
if (advancedField && !fieldMeta) {
throw new Error(
'Field meta is required for this type of field. Please provide the appropriate field meta object.',
);
}
if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) {
throw new Error('Field meta type does not match the field type');
}
const result = match(type)
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
success: true,
data: {},
}))
.with('FREE_SIGNATURE', () => ({
success: false,
error: 'FREE_SIGNATURE is not supported',
data: {},
}))
.exhaustive();
if (!result.success) {
throw new Error('Field meta parsing failed');
}
const field = await prisma.field.create({
data: {
documentId,
recipientId,
type,
page: pageNumber,
positionX: pageX,
positionY: pageY,
width: pageWidth,
height: pageHeight,
customText: '',
inserted: false,
fieldMeta: result.data,
},
include: {
recipient: true,
},
});
await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({
type: 'FIELD_CREATED',
documentId,
user: {
id: team.id,
email: team.name,
name: '',
},
data: {
fieldId: field.secondaryId,
fieldRecipientEmail: field.recipient?.email ?? '',
fieldRecipientId: recipientId,
fieldType: field.type,
},
requestMetadata,
}),
});
return field;
};

View File

@ -1,101 +0,0 @@
import type { FieldType } from '@prisma/client';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { buildTeamWhereQuery } from '../../utils/teams';
export interface CreateTemplateFieldsOptions {
userId: number;
teamId: number;
templateId: number;
fields: {
recipientId: number;
type: FieldType;
pageNumber: number;
pageX: number;
pageY: number;
width: number;
height: number;
fieldMeta?: TFieldMetaSchema;
}[];
}
export const createTemplateFields = async ({
userId,
teamId,
templateId,
fields,
}: CreateTemplateFieldsOptions) => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
team: buildTeamWhereQuery({ teamId, userId }),
},
include: {
recipients: true,
fields: true,
},
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'template not found',
});
}
// Field validation.
const validatedFields = fields.map((field) => {
const recipient = template.recipients.find((recipient) => recipient.id === field.recipientId);
// Each field MUST have a recipient associated with it.
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Recipient ${field.recipientId} not found`,
});
}
// Check whether the recipient associated with the field can have new fields created.
if (!canRecipientFieldsBeModified(recipient, template.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'Recipient type cannot have fields, or they have already interacted with the template.',
});
}
return {
...field,
recipientEmail: recipient.email,
};
});
const createdFields = await prisma.$transaction(async (tx) => {
return await Promise.all(
validatedFields.map(async (field) => {
const createdField = await tx.field.create({
data: {
type: field.type,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta,
templateId,
recipientId: field.recipientId,
},
});
return createdField;
}),
);
});
return {
fields: createdFields,
};
};

View File

@ -1,3 +1,5 @@
import { EnvelopeType } from '@prisma/client';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
@ -5,7 +7,7 @@ import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { getDocumentWhereInput } from '../document/get-document-by-id';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface DeleteDocumentFieldOptions {
userId: number;
@ -19,7 +21,8 @@ export const deleteDocumentField = async ({
teamId,
fieldId,
requestMetadata,
}: DeleteDocumentFieldOptions): Promise<void> => {
}: DeleteDocumentFieldOptions) => {
// Unauthenticated check, we do the real check later.
const field = await prisma.field.findFirst({
where: {
id: fieldId,
@ -32,22 +35,18 @@ export const deleteDocumentField = async ({
});
}
const documentId = field.documentId;
if (!documentId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field does not belong to a document. Use delete template field instead.',
});
}
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: field.envelopeId,
},
type: EnvelopeType.DOCUMENT,
userId,
teamId,
});
const document = await prisma.document.findFirst({
where: documentWhereInput,
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
recipients: {
where: {
@ -60,19 +59,19 @@ export const deleteDocumentField = async ({
},
});
if (!document) {
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
if (document.completedAt) {
if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document already complete',
});
}
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
@ -87,10 +86,11 @@ export const deleteDocumentField = async ({
});
}
await prisma.$transaction(async (tx) => {
return await prisma.$transaction(async (tx) => {
const deletedField = await tx.field.delete({
where: {
id: fieldId,
envelopeId: envelope.id,
},
});
@ -98,7 +98,7 @@ export const deleteDocumentField = async ({
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
documentId,
envelopeId: envelope.id,
metadata: requestMetadata,
data: {
fieldId: deletedField.secondaryId,
@ -108,5 +108,7 @@ export const deleteDocumentField = async ({
},
}),
});
return deletedField;
});
};

View File

@ -1,78 +0,0 @@
import type { Team } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { buildTeamWhereQuery } from '../../utils/teams';
export type DeleteFieldOptions = {
fieldId: number;
documentId: number;
userId: number;
teamId: number;
requestMetadata?: RequestMetadata;
};
export const deleteField = async ({
fieldId,
userId,
teamId,
documentId,
requestMetadata,
}: DeleteFieldOptions) => {
const field = await prisma.field.delete({
where: {
id: fieldId,
document: {
id: documentId,
team: buildTeamWhereQuery({ teamId, userId }),
},
},
include: {
recipient: true,
},
});
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
select: {
id: true,
name: true,
email: true,
},
});
let team: Team | null = null;
if (teamId) {
team = await prisma.team.findFirstOrThrow({
where: {
id: teamId,
},
});
}
await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({
type: 'FIELD_DELETED',
documentId,
user: {
id: team?.id ?? user.id,
email: team?.name ?? user.email,
name: team ? '' : user.name,
},
data: {
fieldId: field.secondaryId,
fieldRecipientEmail: field.recipient?.email ?? '',
fieldRecipientId: field.recipientId ?? -1,
fieldType: field.type,
},
requestMetadata,
}),
});
return field;
};

View File

@ -1,7 +1,10 @@
import { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface DeleteTemplateFieldOptions {
userId: number;
@ -17,21 +20,34 @@ export const deleteTemplateField = async ({
const field = await prisma.field.findFirst({
where: {
id: fieldId,
template: {
envelope: {
type: EnvelopeType.TEMPLATE,
team: buildTeamWhereQuery({ teamId, userId }),
},
},
});
if (!field || !field.templateId) {
if (!field) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field not found',
});
}
// Additional validation to check visibility.
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: field.envelopeId,
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
await prisma.field.delete({
where: {
id: fieldId,
id: field.id,
envelope: envelopeWhereInput,
},
});
};

View File

@ -1,4 +1,4 @@
import { SigningStatus } from '@prisma/client';
import { EnvelopeType, SigningStatus } from '@prisma/client';
import { prisma } from '@documenso/prisma';
@ -6,10 +6,12 @@ export type GetCompletedFieldsForTokenOptions = {
token: string;
};
// Todo: Envelopes - This needs to be redone since we need to determine which document to show the fields on.
export const getCompletedFieldsForToken = async ({ token }: GetCompletedFieldsForTokenOptions) => {
return await prisma.field.findMany({
where: {
document: {
envelope: {
type: EnvelopeType.DOCUMENT,
recipients: {
some: {
token,

View File

@ -1,49 +1,56 @@
import type { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export type GetFieldByIdOptions = {
userId: number;
teamId: number;
fieldId: number;
envelopeType: EnvelopeType;
};
export const getFieldById = async ({ userId, teamId, fieldId }: GetFieldByIdOptions) => {
export const getFieldById = async ({
userId,
teamId,
fieldId,
envelopeType,
}: GetFieldByIdOptions) => {
const field = await prisma.field.findFirst({
where: {
id: fieldId,
},
include: {
document: {
select: {
teamId: true,
},
},
template: {
select: {
teamId: true,
},
envelope: {
type: envelopeType,
team: buildTeamWhereQuery({ teamId, userId }),
},
},
});
const foundTeamId = field?.document?.teamId || field?.template?.teamId;
if (!field || !foundTeamId || foundTeamId !== teamId) {
if (!field) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field not found',
});
}
const team = await prisma.team.findUnique({
where: buildTeamWhereQuery({
teamId: foundTeamId,
userId,
}),
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: field.envelopeId,
},
type: envelopeType,
userId,
teamId,
});
if (!team) {
// Additional validation to check visibility.
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field not found',
});

View File

@ -1,41 +0,0 @@
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
export interface GetFieldsForDocumentOptions {
documentId: number;
userId: number;
teamId: number;
}
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
export const getFieldsForDocument = async ({
documentId,
userId,
teamId,
}: GetFieldsForDocumentOptions) => {
const fields = await prisma.field.findMany({
where: {
document: {
id: documentId,
team: buildTeamWhereQuery({ teamId, userId }),
},
},
include: {
signature: true,
recipient: {
select: {
name: true,
email: true,
signingStatus: true,
},
},
},
orderBy: {
id: 'asc',
},
});
return fields;
};

View File

@ -1,4 +1,4 @@
import { FieldType, RecipientRole, SigningStatus } from '@prisma/client';
import { EnvelopeType, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
import { prisma } from '@documenso/prisma';
@ -6,6 +6,7 @@ export type GetFieldsForTokenOptions = {
token: string;
};
// Todo: Envelopes, this will return all fields, might need to filter based on actual documentId.
export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) => {
if (!token) {
throw new Error('Missing token');
@ -35,7 +36,10 @@ export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) =>
gte: recipient.signingOrder ?? 0,
},
},
documentId: recipient.documentId,
envelope: {
id: recipient.envelopeId,
type: EnvelopeType.DOCUMENT,
},
},
{
recipientId: recipient.id,

View File

@ -41,19 +41,19 @@ export const removeSignedFieldWithToken = async ({
},
},
include: {
document: true,
envelope: true,
recipient: true,
},
});
const { document } = field;
const { envelope } = field;
if (!document) {
if (!envelope) {
throw new Error(`Document not found for field ${field.id}`);
}
if (document.status !== DocumentStatus.PENDING) {
throw new Error(`Document ${document.id} must be pending`);
if (envelope.status !== DocumentStatus.PENDING) {
throw new Error(`Document ${envelope.id} must be pending`);
}
if (
@ -89,7 +89,7 @@ export const removeSignedFieldWithToken = async ({
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
documentId: document.id,
envelopeId: envelope.id,
user: {
name: recipient.name,
email: recipient.email,

View File

@ -1,5 +1,4 @@
import type { Field } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { EnvelopeType, type Field, FieldType } from '@prisma/client';
import { isDeepEqual } from 'remeda';
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
@ -26,7 +25,7 @@ import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { getDocumentWhereInput } from '../document/get-document-by-id';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface SetFieldsForDocumentOptions {
userId: number;
@ -43,39 +42,49 @@ export const setFieldsForDocument = async ({
fields,
requestMetadata,
}: SetFieldsForDocumentOptions) => {
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'documentId',
id: documentId,
},
type: EnvelopeType.DOCUMENT,
userId,
teamId,
});
const document = await prisma.document.findFirst({
where: documentWhereInput,
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
envelopeItems: {
select: {
id: true,
},
},
fields: {
include: {
recipient: true,
},
},
},
});
if (!document) {
// Todo: Envelopes
const firstEnvelopeItemId = envelope?.envelopeItems[0]?.id;
if (!envelope || !firstEnvelopeItemId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
if (document.completedAt) {
if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document already complete',
});
}
const existingFields = await prisma.field.findMany({
where: {
documentId,
},
include: {
recipient: true,
},
});
const existingFields = envelope.fields;
const removedFields = existingFields.filter(
(existingField) => !fields.find((field) => field.id === existingField.id),
@ -84,7 +93,7 @@ export const setFieldsForDocument = async ({
const linkedFields = fields.map((field) => {
const existing = existingFields.find((existingField) => existingField.id === field.id);
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
// Each field MUST have a recipient associated with it.
if (!recipient) {
@ -197,7 +206,7 @@ export const setFieldsForDocument = async ({
const upsertedField = await tx.field.upsert({
where: {
id: field._persisted?.id ?? -1,
documentId,
envelopeId: envelope.id,
},
update: {
page: field.pageNumber,
@ -208,6 +217,8 @@ export const setFieldsForDocument = async ({
fieldMeta: parsedFieldMeta,
},
create: {
envelopeId: envelope.id,
envelopeItemId: firstEnvelopeItemId, // Todo: Envelopes
type: field.type,
page: field.pageNumber,
positionX: field.pageX,
@ -217,17 +228,7 @@ export const setFieldsForDocument = async ({
customText: '',
inserted: false,
fieldMeta: parsedFieldMeta,
document: {
connect: {
id: documentId,
},
},
recipient: {
connect: {
id: field.recipientId,
documentId,
},
},
recipientId: field._recipient.id,
},
});
@ -249,7 +250,7 @@ export const setFieldsForDocument = async ({
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
documentId: documentId,
envelopeId: envelope.id,
metadata: requestMetadata,
data: {
changes,
@ -264,7 +265,7 @@ export const setFieldsForDocument = async ({
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
documentId: documentId,
envelopeId: envelope.id,
metadata: requestMetadata,
data: {
...baseAuditLog,
@ -292,7 +293,7 @@ export const setFieldsForDocument = async ({
data: removedFields.map((field) =>
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
documentId: documentId,
envelopeId: envelope.id,
metadata: requestMetadata,
data: {
fieldId: field.secondaryId,

View File

@ -1,4 +1,4 @@
import { FieldType } from '@prisma/client';
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';
@ -16,7 +16,7 @@ import {
} from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export type SetFieldsForTemplateOptions = {
userId: number;
@ -42,25 +42,40 @@ export const setFieldsForTemplate = async ({
templateId,
fields,
}: SetFieldsForTemplateOptions) => {
const template = await prisma.template.findFirst({
where: {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id: templateId,
team: buildTeamWhereQuery({ teamId, userId }),
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
envelopeItems: {
select: {
id: true,
},
},
fields: {
include: {
recipient: true,
},
},
},
});
if (!template) {
// Todo: Envelopes
const firstEnvelopeItemId = envelope?.envelopeItems[0]?.id;
if (!envelope || !firstEnvelopeItemId) {
throw new Error('Template not found');
}
const existingFields = await prisma.field.findMany({
where: {
templateId,
},
include: {
recipient: true,
},
});
const existingFields = envelope.fields;
const removedFields = existingFields.filter(
(existingField) => !fields.find((field) => field.id === existingField.id),
@ -143,7 +158,8 @@ export const setFieldsForTemplate = async ({
return prisma.field.upsert({
where: {
id: field._persisted?.id ?? -1,
templateId,
envelopeId: envelope.id,
envelopeItemId: firstEnvelopeItemId, // Todo: Envelopes
},
update: {
page: field.pageNumber,
@ -163,15 +179,20 @@ export const setFieldsForTemplate = async ({
customText: '',
inserted: false,
fieldMeta: parsedFieldMeta,
template: {
envelope: {
connect: {
id: templateId,
id: envelope.id,
},
},
envelopeItem: {
connect: {
id: firstEnvelopeItemId, // Todo: Envelopes
},
},
recipient: {
connect: {
id: field.recipientId,
templateId,
envelopeId: envelope.id,
},
},
},

View File

@ -81,7 +81,7 @@ export const signFieldWithToken = async ({
},
},
include: {
document: {
envelope: {
include: {
recipients: true,
},
@ -90,9 +90,9 @@ export const signFieldWithToken = async ({
},
});
const { document } = field;
const { envelope } = field;
if (!document) {
if (!envelope) {
throw new Error(`Document not found for field ${field.id}`);
}
@ -100,12 +100,12 @@ export const signFieldWithToken = async ({
throw new Error(`Recipient not found for field ${field.id}`);
}
if (document.deletedAt) {
throw new Error(`Document ${document.id} has been deleted`);
if (envelope.deletedAt) {
throw new Error(`Document ${envelope.id} has been deleted`);
}
if (document.status !== DocumentStatus.PENDING) {
throw new Error(`Document ${document.id} must be pending for signing`);
if (envelope.status !== DocumentStatus.PENDING) {
throw new Error(`Document ${envelope.id} must be pending for signing`);
}
if (
@ -172,7 +172,7 @@ export const signFieldWithToken = async ({
}
const derivedRecipientActionAuth = await validateFieldAuth({
documentAuthOptions: document.authOptions,
documentAuthOptions: envelope.authOptions,
recipient,
field,
userId,
@ -181,7 +181,9 @@ export const signFieldWithToken = async ({
const documentMeta = await prisma.documentMeta.findFirst({
where: {
documentId: document.id,
envelope: {
id: envelope.id,
},
},
});
@ -272,7 +274,7 @@ export const signFieldWithToken = async ({
assistant && field.recipientId !== assistant.id
? DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED
: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
documentId: document.id,
envelopeId: envelope.id,
user: {
email: assistant?.email ?? recipient.email,
name: assistant?.name ?? recipient.name,

View File

@ -1,4 +1,4 @@
import type { FieldType } from '@prisma/client';
import { EnvelopeType, type FieldType } from '@prisma/client';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
@ -11,7 +11,7 @@ import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { getDocumentWhereInput } from '../document/get-document-by-id';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface UpdateDocumentFieldsOptions {
userId: number;
@ -37,34 +37,38 @@ export const updateDocumentFields = async ({
fields,
requestMetadata,
}: UpdateDocumentFieldsOptions) => {
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'documentId',
id: documentId,
},
type: EnvelopeType.DOCUMENT,
userId,
teamId,
});
const document = await prisma.document.findFirst({
where: documentWhereInput,
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
fields: true,
},
});
if (!document) {
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
if (document.completedAt) {
if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document already complete',
});
}
const fieldsToUpdate = fields.map((field) => {
const originalField = document.fields.find((existingField) => existingField.id === field.id);
const originalField = envelope.fields.find((existingField) => existingField.id === field.id);
if (!originalField) {
throw new AppError(AppErrorCode.NOT_FOUND, {
@ -72,7 +76,7 @@ export const updateDocumentFields = async ({
});
}
const recipient = document.recipients.find(
const recipient = envelope.recipients.find(
(recipient) => recipient.id === originalField.recipientId,
);
@ -84,7 +88,7 @@ export const updateDocumentFields = async ({
}
// Check whether the recipient associated with the field can be modified.
if (!canRecipientFieldsBeModified(recipient, document.fields)) {
if (!canRecipientFieldsBeModified(recipient, envelope.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'Cannot modify a field where the recipient has already interacted with the document',
@ -123,7 +127,7 @@ export const updateDocumentFields = async ({
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
documentId: documentId,
envelopeId: envelope.id,
metadata: requestMetadata,
data: {
fieldId: updatedField.secondaryId,

View File

@ -1,119 +0,0 @@
import type { FieldType, Team } from '@prisma/client';
import { type TFieldMetaSchema as FieldMeta } from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData, diffFieldChanges } from '../../utils/document-audit-logs';
import { buildTeamWhereQuery } from '../../utils/teams';
export type UpdateFieldOptions = {
fieldId: number;
documentId: number;
userId: number;
teamId: number;
recipientId?: number;
type?: FieldType;
pageNumber?: number;
pageX?: number;
pageY?: number;
pageWidth?: number;
pageHeight?: number;
requestMetadata?: RequestMetadata;
fieldMeta?: FieldMeta;
};
export const updateField = async ({
fieldId,
documentId,
userId,
teamId,
recipientId,
type,
pageNumber,
pageX,
pageY,
pageWidth,
pageHeight,
requestMetadata,
fieldMeta,
}: UpdateFieldOptions) => {
if (type === 'FREE_SIGNATURE') {
throw new Error('Cannot update a FREE_SIGNATURE field');
}
const oldField = await prisma.field.findFirstOrThrow({
where: {
id: fieldId,
document: {
id: documentId,
team: buildTeamWhereQuery({ teamId, userId }),
},
},
});
const field = prisma.$transaction(async (tx) => {
const updatedField = await tx.field.update({
where: {
id: fieldId,
},
data: {
recipientId,
type,
page: pageNumber,
positionX: pageX,
positionY: pageY,
width: pageWidth,
height: pageHeight,
fieldMeta,
},
include: {
recipient: true,
},
});
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
select: {
id: true,
name: true,
email: true,
},
});
let team: Team | null = null;
if (teamId) {
team = await prisma.team.findFirst({
where: buildTeamWhereQuery({ teamId, userId }),
});
}
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
documentId,
user: {
id: team?.id ?? user.id,
email: team?.name ?? user.email,
name: team ? '' : user.name,
},
data: {
fieldId: updatedField.secondaryId,
fieldRecipientEmail: updatedField.recipient?.email ?? '',
fieldRecipientId: recipientId ?? -1,
fieldType: updatedField.type,
changes: diffFieldChanges(oldField, updatedField),
},
requestMetadata,
}),
});
return updatedField;
});
return field;
};

View File

@ -1,11 +1,11 @@
import type { FieldType } from '@prisma/client';
import { EnvelopeType, type FieldType } from '@prisma/client';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface UpdateTemplateFieldsOptions {
userId: number;
@ -29,25 +29,32 @@ export const updateTemplateFields = async ({
templateId,
fields,
}: UpdateTemplateFieldsOptions) => {
const template = await prisma.template.findFirst({
where: {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id: templateId,
team: buildTeamWhereQuery({ teamId, userId }),
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
fields: true,
},
});
if (!template) {
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
const fieldsToUpdate = fields.map((field) => {
const originalField = template.fields.find((existingField) => existingField.id === field.id);
const originalField = envelope.fields.find((existingField) => existingField.id === field.id);
if (!originalField) {
throw new AppError(AppErrorCode.NOT_FOUND, {
@ -55,7 +62,7 @@ export const updateTemplateFields = async ({
});
}
const recipient = template.recipients.find(
const recipient = envelope.recipients.find(
(recipient) => recipient.id === originalField.recipientId,
);
@ -67,7 +74,7 @@ export const updateTemplateFields = async ({
}
// Check whether the recipient associated with the field can be modified.
if (!canRecipientFieldsBeModified(recipient, template.fields)) {
if (!canRecipientFieldsBeModified(recipient, envelope.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'Cannot modify a field where the recipient has already interacted with the document',