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

@ -5,6 +5,7 @@ import type { Field, Signature } from '@prisma/client';
import {
DocumentSource,
DocumentStatus,
EnvelopeType,
FieldType,
Prisma,
RecipientRole,
@ -31,7 +32,7 @@ import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/doc
import { ZFieldMetaSchema } from '../../types/field-meta';
import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { isRequiredField } from '../../utils/advanced-fields-helpers';
@ -43,11 +44,13 @@ import {
createRecipientAuthOptions,
extractDocumentAuthMethods,
} from '../../utils/document-auth';
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { formatDocumentsPath } from '../../utils/teams';
import { sendDocument } from '../document/send-document';
import { validateFieldAuth } from '../document/validate-field-auth';
import { getEmailContext } from '../email/get-email-context';
import { incrementDocumentId } from '../envelope/increment-id';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type CreateDocumentFromDirectTemplateOptions = {
@ -90,7 +93,7 @@ export const createDocumentFromDirectTemplate = async ({
requestMetadata,
user,
}: CreateDocumentFromDirectTemplateOptions): Promise<TCreateDocumentFromDirectTemplateResponse> => {
const template = await prisma.template.findFirst({
const directTemplateEnvelope = await prisma.envelope.findFirst({
where: {
directLink: {
token: directTemplateToken,
@ -103,8 +106,12 @@ export const createDocumentFromDirectTemplate = async ({
},
},
directLink: true,
templateDocumentData: true,
templateMeta: true,
envelopeItems: {
select: {
documentData: true,
},
},
documentMeta: true,
user: {
select: {
id: true,
@ -115,19 +122,31 @@ export const createDocumentFromDirectTemplate = async ({
},
});
if (!template?.directLink?.enabled) {
if (!directTemplateEnvelope?.directLink?.enabled) {
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
}
const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId(
directTemplateEnvelope.secondaryId,
);
const firstEnvelopeItem = directTemplateEnvelope.envelopeItems[0];
// Todo: Envelopes
if (directTemplateEnvelope.envelopeItems.length !== 1 || !firstEnvelopeItem) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Invalid number of envelope items',
});
}
const { branding, settings, senderEmail, emailLanguage } = await getEmailContext({
emailType: 'INTERNAL',
source: {
type: 'team',
teamId: template.teamId,
teamId: directTemplateEnvelope.teamId,
},
});
const { recipients, directLink, user: templateOwner } = template;
const { recipients, directLink, user: templateOwner } = directTemplateEnvelope;
const directTemplateRecipient = recipients.find(
(recipient) => recipient.id === directLink.directTemplateRecipientId,
@ -139,7 +158,7 @@ export const createDocumentFromDirectTemplate = async ({
});
}
if (template.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
if (directTemplateEnvelope.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
}
@ -151,7 +170,7 @@ export const createDocumentFromDirectTemplate = async ({
const { derivedRecipientAccessAuth, documentAuthOption: templateAuthOptions } =
extractDocumentAuthMethods({
documentAuth: template.authOptions,
documentAuth: directTemplateEnvelope.authOptions,
});
const directRecipientName = user?.name || initialDirectRecipientName;
@ -171,10 +190,13 @@ export const createDocumentFromDirectTemplate = async ({
directTemplateRecipient.authOptions,
);
const nonDirectTemplateRecipients = template.recipients.filter(
const nonDirectTemplateRecipients = directTemplateEnvelope.recipients.filter(
(recipient) => recipient.id !== directTemplateRecipient.id,
);
const derivedDocumentMeta = extractDerivedDocumentMeta(settings, template.templateMeta);
const derivedDocumentMeta = extractDerivedDocumentMeta(
settings,
directTemplateEnvelope.documentMeta,
);
// Associate, validate and map to a query every direct template recipient field with the provided fields.
// Only process fields that are either required or have been signed by the user
@ -202,7 +224,7 @@ export const createDocumentFromDirectTemplate = async ({
}
const derivedRecipientActionAuth = await validateFieldAuth({
documentAuthOptions: template.authOptions,
documentAuthOptions: directTemplateEnvelope.authOptions,
recipient: {
authOptions: directTemplateRecipient.authOptions,
email: directRecipientEmail,
@ -267,29 +289,45 @@ export const createDocumentFromDirectTemplate = async ({
const initialRequestTime = new Date();
const { documentId, recipientId, token } = await prisma.$transaction(async (tx) => {
const documentData = await tx.documentData.create({
data: {
type: template.templateDocumentData.type,
data: template.templateDocumentData.data,
initialData: template.templateDocumentData.initialData,
},
});
// Todo: Envelopes
const documentData = await prisma.documentData.create({
data: {
type: firstEnvelopeItem.documentData.type,
data: firstEnvelopeItem.documentData.data,
initialData: firstEnvelopeItem.documentData.initialData,
},
});
// Create the document and non direct template recipients.
const document = await tx.document.create({
const documentMeta = await prisma.documentMeta.create({
data: derivedDocumentMeta,
});
const incrementedDocumentId = await incrementDocumentId();
const { createdEnvelope, recipientId, token } = await prisma.$transaction(async (tx) => {
// Create the envelope and non direct template recipients.
const createdEnvelope = await tx.envelope.create({
data: {
id: prefixedId('envelope'),
secondaryId: incrementedDocumentId.formattedDocumentId,
type: EnvelopeType.DOCUMENT,
qrToken: prefixedId('qr'),
source: DocumentSource.TEMPLATE_DIRECT_LINK,
templateId: template.id,
userId: template.userId,
teamId: template.teamId,
title: template.title,
templateId: directTemplateEnvelopeLegacyId,
userId: directTemplateEnvelope.userId,
teamId: directTemplateEnvelope.teamId,
title: directTemplateEnvelope.title,
createdAt: initialRequestTime,
status: DocumentStatus.PENDING,
externalId: directTemplateExternalId,
visibility: settings.documentVisibility,
documentDataId: documentData.id,
envelopeItems: {
create: {
id: prefixedId('envelope_item'),
title: directTemplateEnvelope.title, // Todo: Envelopes use item title instead
documentDataId: documentData.id,
},
},
authOptions: createDocumentAuthOptions({
globalAccessAuth: templateAuthOptions.globalAccessAuth,
globalActionAuth: templateAuthOptions.globalActionAuth,
@ -319,9 +357,7 @@ export const createDocumentFromDirectTemplate = async ({
}),
},
},
documentMeta: {
create: derivedDocumentMeta,
},
documentMetaId: documentMeta.id,
},
include: {
recipients: true,
@ -330,13 +366,20 @@ export const createDocumentFromDirectTemplate = async ({
url: true,
},
},
envelopeItems: {
select: {
id: true,
},
},
},
});
const envelopeItemId = createdEnvelope.envelopeItems[0].id;
let nonDirectRecipientFieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
Object.values(nonDirectTemplateRecipients).forEach((templateRecipient) => {
const recipient = document.recipients.find(
const recipient = createdEnvelope.recipients.find(
(recipient) => recipient.email === templateRecipient.email,
);
@ -346,7 +389,8 @@ export const createDocumentFromDirectTemplate = async ({
nonDirectRecipientFieldsToCreate = nonDirectRecipientFieldsToCreate.concat(
templateRecipient.fields.map((field) => ({
documentId: document.id,
envelopeId: createdEnvelope.id,
envelopeItemId: envelopeItemId, // Todo: Envelopes
recipientId: recipient.id,
type: field.type,
page: field.page,
@ -371,7 +415,7 @@ export const createDocumentFromDirectTemplate = async ({
// Create the direct recipient and their non signature fields.
const createdDirectRecipient = await tx.recipient.create({
data: {
documentId: document.id,
envelopeId: createdEnvelope.id,
email: directRecipientEmail,
name: directRecipientName,
authOptions: createRecipientAuthOptions({
@ -387,7 +431,8 @@ export const createDocumentFromDirectTemplate = async ({
fields: {
createMany: {
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => ({
documentId: document.id,
envelopeId: createdEnvelope.id,
envelopeItemId: envelopeItemId, // Todo: Envelopes
type: templateField.type,
page: templateField.page,
positionX: templateField.positionX,
@ -417,7 +462,8 @@ export const createDocumentFromDirectTemplate = async ({
const field = await tx.field.create({
data: {
documentId: document.id,
envelopeId: createdEnvelope.id,
envelopeItemId: envelopeItemId, // Todo: Envelopes
recipientId: createdDirectRecipient.id,
type: templateField.type,
page: templateField.page,
@ -466,7 +512,7 @@ export const createDocumentFromDirectTemplate = async ({
const auditLogsToCreate: CreateDocumentAuditLogDataResponse[] = [
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
documentId: document.id,
envelopeId: createdEnvelope.id,
user: {
id: user?.id,
name: user?.name,
@ -474,17 +520,17 @@ export const createDocumentFromDirectTemplate = async ({
},
metadata: requestMetadata,
data: {
title: document.title,
title: createdEnvelope.title,
source: {
type: DocumentSource.TEMPLATE_DIRECT_LINK,
templateId: template.id,
templateId: directTemplateEnvelopeLegacyId,
directRecipientEmail,
},
},
}),
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
documentId: document.id,
envelopeId: createdEnvelope.id,
user: {
id: user?.id,
name: user?.name,
@ -502,7 +548,7 @@ export const createDocumentFromDirectTemplate = async ({
...createdDirectRecipientFields.map(({ field, derivedRecipientActionAuth }) =>
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
documentId: document.id,
envelopeId: createdEnvelope.id,
user: {
id: user?.id,
name: user?.name,
@ -547,7 +593,7 @@ export const createDocumentFromDirectTemplate = async ({
),
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
documentId: document.id,
envelopeId: createdEnvelope.id,
user: {
id: user?.id,
name: user?.name,
@ -572,10 +618,10 @@ export const createDocumentFromDirectTemplate = async ({
const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, {
recipientName: directRecipientEmail,
recipientRole: directTemplateRecipient.role,
documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${
document.id
documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(createdEnvelope.team?.url)}/${
createdEnvelope.id
}`,
documentName: document.title,
documentName: createdEnvelope.title,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
});
@ -600,8 +646,8 @@ export const createDocumentFromDirectTemplate = async ({
});
return {
createdEnvelope,
token: createdDirectRecipient.token,
documentId: document.id,
recipientId: createdDirectRecipient.id,
};
});
@ -609,18 +655,21 @@ export const createDocumentFromDirectTemplate = async ({
try {
// This handles sending emails and sealing the document if required.
await sendDocument({
documentId,
userId: template.userId,
teamId: template.teamId,
id: {
type: 'envelopeId',
id: createdEnvelope.id,
},
userId: createdEnvelope.userId,
teamId: createdEnvelope.teamId,
requestMetadata,
});
const createdDocument = await prisma.document.findFirstOrThrow({
// Refetch envelope so we get the final data.
const refetchedEnvelope = await prisma.envelope.findFirstOrThrow({
where: {
id: documentId,
id: createdEnvelope.id,
},
include: {
documentData: true,
documentMeta: true,
recipients: true,
},
@ -628,9 +677,9 @@ export const createDocumentFromDirectTemplate = async ({
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_SIGNED,
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
userId: template.userId,
teamId: template.teamId ?? undefined,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(refetchedEnvelope)),
userId: refetchedEnvelope.userId,
teamId: refetchedEnvelope.teamId ?? undefined,
});
} catch (err) {
console.error('[CREATE_DOCUMENT_FROM_DIRECT_TEMPLATE]:', err);
@ -641,7 +690,7 @@ export const createDocumentFromDirectTemplate = async ({
return {
token,
documentId,
documentId: incrementedDocumentId.documentId,
recipientId,
};
};

View File

@ -1,183 +0,0 @@
import { DocumentSource, type RecipientRole } from '@prisma/client';
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { extractDerivedDocumentMeta } from '../../utils/document';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getTeamSettings } from '../team/get-team-settings';
export type CreateDocumentFromTemplateLegacyOptions = {
templateId: number;
userId: number;
teamId: number;
recipients?: {
name?: string;
email: string;
role?: RecipientRole;
signingOrder?: number | null;
}[];
};
// !TODO: Make this work
/**
* Legacy server function for /api/v1
*/
export const createDocumentFromTemplateLegacy = async ({
templateId,
userId,
teamId,
recipients,
}: CreateDocumentFromTemplateLegacyOptions) => {
const template = await prisma.template.findUnique({
where: {
id: templateId,
team: buildTeamWhereQuery({ teamId, userId }),
},
include: {
recipients: true,
fields: true,
templateDocumentData: true,
templateMeta: true,
},
});
if (!template) {
throw new Error('Template not found.');
}
const settings = await getTeamSettings({
userId,
teamId,
});
const documentData = await prisma.documentData.create({
data: {
type: template.templateDocumentData.type,
data: template.templateDocumentData.data,
initialData: template.templateDocumentData.initialData,
},
});
const recipientsToCreate = template.recipients.map((recipient) => ({
id: recipient.id,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
}));
const document = await prisma.document.create({
data: {
qrToken: prefixedId('qr'),
source: DocumentSource.TEMPLATE,
templateId: template.id,
userId,
teamId: template.teamId,
title: template.title,
visibility: settings.documentVisibility,
documentDataId: documentData.id,
useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false,
recipients: {
create: recipientsToCreate.map((recipient) => ({
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: recipient.token,
})),
},
documentMeta: {
create: extractDerivedDocumentMeta(settings, template.templateMeta),
},
},
include: {
recipients: {
orderBy: {
id: 'asc',
},
},
documentData: true,
},
});
await prisma.field.createMany({
data: template.fields.map((field) => {
const recipient = recipientsToCreate.find((recipient) => recipient.id === field.recipientId);
const documentRecipient = document.recipients.find(
(documentRecipient) => documentRecipient.token === recipient?.token,
);
if (!documentRecipient) {
throw new Error('Recipient not found.');
}
return {
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: field.customText,
inserted: field.inserted,
documentId: document.id,
recipientId: documentRecipient.id,
};
}),
});
// Replicate the old logic, get by index and create if we exceed the number of existing recipients.
if (recipients && recipients.length > 0) {
await Promise.all(
recipients.map(async (recipient, index) => {
const existingRecipient = document.recipients.at(index);
if (existingRecipient) {
return await prisma.recipient.update({
where: {
id: existingRecipient.id,
documentId: document.id,
},
data: {
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
},
});
}
return await prisma.recipient.create({
data: {
documentId: document.id,
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
},
});
}),
);
}
// Gross but we need to do the additional fetch since we mutate above.
const updatedRecipients = await prisma.recipient.findMany({
where: {
documentId: document.id,
},
orderBy: {
id: 'asc',
},
});
return {
...document,
recipients: updatedRecipients,
};
};

View File

@ -1,6 +1,7 @@
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
import {
DocumentSource,
EnvelopeType,
type Field,
FolderType,
type Recipient,
@ -37,7 +38,7 @@ import {
} from '../../types/field-meta';
import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { extractDerivedDocumentMeta } from '../../utils/document';
@ -47,7 +48,11 @@ import {
createRecipientAuthOptions,
extractDocumentAuthMethods,
} from '../../utils/document-auth';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { incrementDocumentId } from '../envelope/increment-id';
import { getTeamSettings } from '../team/get-team-settings';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@ -60,7 +65,7 @@ type FinalRecipient = Pick<
};
export type CreateDocumentFromTemplateOptions = {
templateId: number;
id: EnvelopeIdOptions;
externalId?: string | null;
userId: number;
teamId: number;
@ -268,7 +273,7 @@ const getUpdatedFieldMeta = (field: Field, prefillField?: TFieldMetaPrefillField
};
export const createDocumentFromTemplate = async ({
templateId,
id,
externalId,
userId,
teamId,
@ -279,19 +284,27 @@ export const createDocumentFromTemplate = async ({
folderId,
prefillFields,
}: CreateDocumentFromTemplateOptions) => {
const template = await prisma.template.findUnique({
where: {
id: templateId,
team: buildTeamWhereQuery({ teamId, userId }),
},
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id,
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const template = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
recipients: {
include: {
fields: true,
},
},
templateDocumentData: true,
templateMeta: true,
envelopeItems: {
include: {
documentData: true,
},
},
documentMeta: true,
},
});
@ -317,6 +330,15 @@ export const createDocumentFromTemplate = async ({
}
}
const legacyTemplateId = mapSecondaryIdToTemplateId(template.secondaryId);
// Todo: Envelopes
if (template.envelopeItems.length !== 1) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Template must have exactly 1 envelope item',
});
}
const settings = await getTeamSettings({
userId,
teamId,
@ -354,7 +376,8 @@ export const createDocumentFromTemplate = async ({
};
});
let parentDocumentData = template.templateDocumentData;
// Todo: Envelopes
let parentDocumentData = template.envelopeItems[0].documentData;
if (customDocumentDataId) {
const customDocumentData = await prisma.documentData.findFirst({
@ -380,48 +403,58 @@ export const createDocumentFromTemplate = async ({
},
});
const incrementedDocumentId = await incrementDocumentId();
const documentMeta = await prisma.documentMeta.create({
data: extractDerivedDocumentMeta(settings, {
subject: override?.subject || template.documentMeta?.subject,
message: override?.message || template.documentMeta?.message,
timezone: override?.timezone || template.documentMeta?.timezone,
dateFormat: override?.dateFormat || template.documentMeta?.dateFormat,
redirectUrl: override?.redirectUrl || template.documentMeta?.redirectUrl,
distributionMethod: override?.distributionMethod || template.documentMeta?.distributionMethod,
emailSettings: override?.emailSettings || template.documentMeta?.emailSettings,
signingOrder: override?.signingOrder || template.documentMeta?.signingOrder,
language: override?.language || template.documentMeta?.language || settings.documentLanguage,
typedSignatureEnabled:
override?.typedSignatureEnabled ?? template.documentMeta?.typedSignatureEnabled,
uploadSignatureEnabled:
override?.uploadSignatureEnabled ?? template.documentMeta?.uploadSignatureEnabled,
drawSignatureEnabled:
override?.drawSignatureEnabled ?? template.documentMeta?.drawSignatureEnabled,
allowDictateNextSigner:
override?.allowDictateNextSigner ?? template.documentMeta?.allowDictateNextSigner,
}),
});
return await prisma.$transaction(async (tx) => {
const document = await tx.document.create({
const envelope = await tx.envelope.create({
data: {
id: prefixedId('envelope'),
secondaryId: incrementedDocumentId.formattedDocumentId,
type: EnvelopeType.DOCUMENT,
qrToken: prefixedId('qr'),
source: DocumentSource.TEMPLATE,
externalId: externalId || template.externalId,
templateId: template.id,
templateId: legacyTemplateId, // The template this envelope was created from.
userId,
folderId,
teamId: template.teamId,
title: override?.title || template.title,
documentDataId: documentData.id,
envelopeItems: {
create: {
id: prefixedId('envelope_item'),
title: override?.title || template.title,
documentDataId: documentData.id,
},
},
authOptions: createDocumentAuthOptions({
globalAccessAuth: templateAuthOptions.globalAccessAuth,
globalActionAuth: templateAuthOptions.globalActionAuth,
}),
visibility: template.visibility || settings.documentVisibility,
useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false,
documentMeta: {
create: extractDerivedDocumentMeta(settings, {
subject: override?.subject || template.templateMeta?.subject,
message: override?.message || template.templateMeta?.message,
timezone: override?.timezone || template.templateMeta?.timezone,
password: override?.password || template.templateMeta?.password,
dateFormat: override?.dateFormat || template.templateMeta?.dateFormat,
redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl,
distributionMethod:
override?.distributionMethod || template.templateMeta?.distributionMethod,
emailSettings: override?.emailSettings || template.templateMeta?.emailSettings,
signingOrder: override?.signingOrder || template.templateMeta?.signingOrder,
language:
override?.language || template.templateMeta?.language || settings.documentLanguage,
typedSignatureEnabled:
override?.typedSignatureEnabled ?? template.templateMeta?.typedSignatureEnabled,
uploadSignatureEnabled:
override?.uploadSignatureEnabled ?? template.templateMeta?.uploadSignatureEnabled,
drawSignatureEnabled:
override?.drawSignatureEnabled ?? template.templateMeta?.drawSignatureEnabled,
allowDictateNextSigner:
override?.allowDictateNextSigner ?? template.templateMeta?.allowDictateNextSigner,
}),
},
documentMetaId: documentMeta.id,
recipients: {
createMany: {
data: finalRecipients.map((recipient) => {
@ -454,11 +487,17 @@ export const createDocumentFromTemplate = async ({
id: 'asc',
},
},
documentData: true,
envelopeItems: {
select: {
id: true,
},
},
},
});
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
const envelopeItemId = envelope.envelopeItems[0].id;
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId'>[] = [];
// Get all template field IDs first so we can validate later
const allTemplateFieldIds = finalRecipients.flatMap((recipient) =>
@ -502,7 +541,7 @@ export const createDocumentFromTemplate = async ({
}
Object.values(finalRecipients).forEach(({ token, fields }) => {
const recipient = document.recipients.find((recipient) => recipient.token === token);
const recipient = envelope.recipients.find((recipient) => recipient.token === token);
if (!recipient) {
throw new Error('Recipient not found.');
@ -513,7 +552,8 @@ export const createDocumentFromTemplate = async ({
const prefillField = prefillFields?.find((value) => value.id === field.id);
const payload = {
documentId: document.id,
envelopeItemId,
envelopeId: envelope.id, // Todo: Envelopes
recipientId: recipient.id,
type: field.type,
page: field.page,
@ -544,7 +584,7 @@ export const createDocumentFromTemplate = async ({
}
payload.customText = DateTime.fromJSDate(date).toFormat(
template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
template.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
);
payload.inserted = true;
@ -569,21 +609,21 @@ export const createDocumentFromTemplate = async ({
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
documentId: document.id,
envelopeId: envelope.id,
metadata: requestMetadata,
data: {
title: document.title,
title: envelope.title,
source: {
type: DocumentSource.TEMPLATE,
templateId: template.id,
templateId: legacyTemplateId,
},
},
}),
});
const createdDocument = await tx.document.findFirst({
const createdEnvelope = await tx.envelope.findFirst({
where: {
id: document.id,
id: envelope.id,
},
include: {
documentMeta: true,
@ -591,17 +631,17 @@ export const createDocumentFromTemplate = async ({
},
});
if (!createdDocument) {
if (!createdEnvelope) {
throw new Error('Document not found');
}
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_CREATED,
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
userId,
teamId,
});
return document;
return envelope;
});
};

View File

@ -1,4 +1,4 @@
import type { Recipient } from '@prisma/client';
import { EnvelopeType, type Recipient } from '@prisma/client';
import { nanoid } from 'nanoid';
import {
@ -8,50 +8,55 @@ import {
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildTeamWhereQuery } from '../../utils/teams';
import { type EnvelopeIdOptions, mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export type CreateTemplateDirectLinkOptions = {
templateId: number;
id: EnvelopeIdOptions;
userId: number;
teamId: number;
directRecipientId?: number;
};
export const createTemplateDirectLink = async ({
templateId,
id,
userId,
teamId,
directRecipientId,
}: CreateTemplateDirectLinkOptions) => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
team: buildTeamWhereQuery({ teamId, userId }),
},
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id,
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
directLink: true,
},
});
if (!template) {
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Template not found' });
}
if (template.directLink) {
if (envelope.directLink) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, { message: 'Direct template already exists' });
}
if (
directRecipientId &&
!template.recipients.find((recipient) => recipient.id === directRecipientId)
!envelope.recipients.find((recipient) => recipient.id === directRecipientId)
) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Recipient not found' });
}
if (
!directRecipientId &&
template.recipients.find(
envelope.recipients.find(
(recipient) => recipient.email.toLowerCase() === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
)
) {
@ -60,13 +65,13 @@ export const createTemplateDirectLink = async ({
});
}
return await prisma.$transaction(async (tx) => {
const createdDirectLink = await prisma.$transaction(async (tx) => {
let recipient: Recipient | undefined;
if (directRecipientId) {
recipient = await tx.recipient.update({
where: {
templateId,
envelopeId: envelope.id,
id: directRecipientId,
},
data: {
@ -77,7 +82,7 @@ export const createTemplateDirectLink = async ({
} else {
recipient = await tx.recipient.create({
data: {
templateId,
envelopeId: envelope.id,
name: DIRECT_TEMPLATE_RECIPIENT_NAME,
email: DIRECT_TEMPLATE_RECIPIENT_EMAIL,
token: nanoid(),
@ -87,11 +92,20 @@ export const createTemplateDirectLink = async ({
return await tx.templateDirectLink.create({
data: {
templateId,
envelopeId: envelope.id,
enabled: true,
token: nanoid(),
directTemplateRecipientId: recipient.id,
},
});
});
return {
id: createdDirectLink.id,
token: createdDirectLink.token,
createdAt: createdDirectLink.createdAt,
enabled: createdDirectLink.enabled,
directTemplateRecipientId: createdDirectLink.directTemplateRecipientId,
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
};
};

View File

@ -1,112 +0,0 @@
import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client';
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema//TemplateSchema';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
import { extractDerivedDocumentMeta } from '../../utils/document';
import { createDocumentAuthOptions } from '../../utils/document-auth';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getTeamSettings } from '../team/get-team-settings';
export type CreateTemplateOptions = {
userId: number;
teamId: number;
templateDocumentDataId: string;
data: {
title: string;
folderId?: string;
externalId?: string | null;
visibility?: DocumentVisibility;
globalAccessAuth?: TDocumentAccessAuthTypes[];
globalActionAuth?: TDocumentActionAuthTypes[];
publicTitle?: string;
publicDescription?: string;
type?: Template['type'];
};
meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
};
export const ZCreateTemplateResponseSchema = TemplateSchema;
export type TCreateTemplateResponse = z.infer<typeof ZCreateTemplateResponseSchema>;
export const createTemplate = async ({
userId,
teamId,
templateDocumentDataId,
data,
meta = {},
}: CreateTemplateOptions) => {
const { title, folderId } = data;
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery({ teamId, userId }),
});
if (!team) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
if (folderId) {
const folder = await prisma.folder.findFirst({
where: {
id: folderId,
teamId: team.id,
},
});
if (!folder) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Folder not found',
});
}
}
const settings = await getTeamSettings({
userId,
teamId,
});
const emailId = meta.emailId;
// Validate that the email ID belongs to the organisation.
if (emailId) {
const email = await prisma.organisationEmail.findFirst({
where: {
id: emailId,
organisationId: team.organisationId,
},
});
if (!email) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Email not found',
});
}
}
return await prisma.template.create({
data: {
title,
teamId,
userId,
templateDocumentDataId,
folderId,
externalId: data.externalId,
visibility: data.visibility ?? settings.documentVisibility,
authOptions: createDocumentAuthOptions({
globalAccessAuth: data.globalAccessAuth || [],
globalActionAuth: data.globalActionAuth || [],
}),
publicTitle: data.publicTitle,
publicDescription: data.publicDescription,
type: data.type,
templateMeta: {
create: extractDerivedDocumentMeta(settings, meta),
},
},
});
};

View File

@ -1,8 +1,10 @@
import { EnvelopeType } from '@prisma/client';
import { generateAvaliableRecipientPlaceholder } from '@documenso/lib/utils/templates';
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 DeleteTemplateDirectLinkOptions = {
templateId: number;
@ -15,24 +17,31 @@ export const deleteTemplateDirectLink = async ({
userId,
teamId,
}: DeleteTemplateDirectLinkOptions): Promise<void> => {
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.findUnique({
where: envelopeWhereInput,
include: {
directLink: true,
recipients: true,
},
});
if (!template) {
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const { directLink } = template;
const { directLink } = envelope;
if (!directLink) {
return;
@ -41,17 +50,17 @@ export const deleteTemplateDirectLink = async ({
await prisma.$transaction(async (tx) => {
await tx.recipient.update({
where: {
templateId: template.id,
envelopeId: envelope.id,
id: directLink.directTemplateRecipientId,
},
data: {
...generateAvaliableRecipientPlaceholder(template.recipients),
...generateAvaliableRecipientPlaceholder(envelope.recipients),
},
});
await tx.templateDirectLink.delete({
where: {
templateId,
envelopeId: envelope.id,
},
});
});

View File

@ -1,6 +1,8 @@
import { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export type DeleteTemplateOptions = {
id: number;
@ -9,10 +11,17 @@ export type DeleteTemplateOptions = {
};
export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptions) => {
return await prisma.template.delete({
where: {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id,
team: buildTeamWhereQuery({ teamId, userId }),
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
return await prisma.envelope.delete({
where: envelopeWhereInput,
});
};

View File

@ -1,27 +1,30 @@
import type { Prisma } from '@prisma/client';
import { DocumentSource, EnvelopeType } from '@prisma/client';
import { omit } from 'remeda';
import { nanoid } from '@documenso/lib/universal/id';
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import type { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
import { buildTeamWhereQuery } from '../../utils/teams';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { type EnvelopeIdOptions } from '../../utils/envelope';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { incrementTemplateId } from '../envelope/increment-id';
export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & {
export type DuplicateTemplateOptions = {
userId: number;
teamId: number;
id: EnvelopeIdOptions;
};
export const duplicateTemplate = async ({
templateId,
userId,
teamId,
}: DuplicateTemplateOptions) => {
const template = await prisma.template.findUnique({
where: {
id: templateId,
team: buildTeamWhereQuery({ teamId, userId }),
},
export const duplicateTemplate = async ({ id, userId, teamId }: DuplicateTemplateOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id,
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
recipients: {
select: {
@ -32,79 +35,105 @@ export const duplicateTemplate = async ({
fields: true,
},
},
templateDocumentData: true,
templateMeta: true,
},
});
if (!template) {
throw new Error('Template not found.');
}
const documentData = await prisma.documentData.create({
data: {
type: template.templateDocumentData.type,
data: template.templateDocumentData.data,
initialData: template.templateDocumentData.initialData,
},
});
let templateMeta: Prisma.TemplateCreateArgs['data']['templateMeta'] | undefined = undefined;
if (template.templateMeta) {
templateMeta = {
create: {
...omit(template.templateMeta, ['id', 'templateId']),
emailSettings: template.templateMeta.emailSettings || undefined,
envelopeItems: {
include: {
documentData: true,
},
},
};
documentMeta: true,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const duplicatedTemplate = await prisma.template.create({
const { formattedTemplateId } = await incrementTemplateId();
const createdDocumentMeta = await prisma.documentMeta.create({
data: {
...omit(envelope.documentMeta, ['id']),
emailSettings: envelope.documentMeta.emailSettings || undefined,
},
});
const duplicatedEnvelope = await prisma.envelope.create({
data: {
id: prefixedId('envelope'),
secondaryId: formattedTemplateId,
type: EnvelopeType.TEMPLATE,
userId,
teamId,
title: template.title + ' (copy)',
templateDocumentDataId: documentData.id,
authOptions: template.authOptions || undefined,
visibility: template.visibility,
templateMeta,
title: envelope.title + ' (copy)',
documentMetaId: createdDocumentMeta.id,
authOptions: envelope.authOptions || undefined,
visibility: envelope.visibility,
source: DocumentSource.DOCUMENT, // Todo: Migration what to use here.
},
include: {
recipients: true,
},
});
const recipientsToCreate = template.recipients.map((recipient) => ({
templateId: duplicatedTemplate.id,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
fields: {
createMany: {
data: recipient.fields.map((field) => ({
templateId: duplicatedTemplate.id,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
})),
},
},
}));
// Key = original envelope item ID
// Value = duplicated envelope item ID.
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
for (const recipientData of recipientsToCreate) {
// Duplicate the envelope items.
await Promise.all(
envelope.envelopeItems.map(async (envelopeItem) => {
const duplicatedDocumentData = await prisma.documentData.create({
data: {
type: envelopeItem.documentData.type,
data: envelopeItem.documentData.initialData,
initialData: envelopeItem.documentData.initialData,
},
});
const duplicatedEnvelopeItem = await prisma.envelopeItem.create({
data: {
id: prefixedId('envelope_item'),
title: envelopeItem.title,
envelopeId: duplicatedEnvelope.id,
documentDataId: duplicatedDocumentData.id,
},
});
oldEnvelopeItemToNewEnvelopeItemIdMap[envelopeItem.id] = duplicatedEnvelopeItem.id;
}),
);
for (const recipient of envelope.recipients) {
await prisma.recipient.create({
data: recipientData,
data: {
envelopeId: duplicatedEnvelope.id,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
fields: {
createMany: {
data: recipient.fields.map((field) => ({
envelopeId: duplicatedEnvelope.id,
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
})),
},
},
},
});
}
return duplicatedTemplate;
return duplicatedEnvelope;
};

View File

@ -1,15 +1,16 @@
import { DocumentVisibility, type Prisma, TeamMemberRole, type Template } from '@prisma/client';
import { match } from 'ts-pattern';
import type { TemplateType } from '@prisma/client';
import { EnvelopeType, type Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
import { type FindResultResponse } from '../../types/search-params';
import { getMemberRoles } from '../team/get-member-roles';
export type FindTemplatesOptions = {
userId: number;
teamId: number;
type?: Template['type'];
type?: TemplateType;
page?: number;
perPage?: number;
folderId?: string;
@ -23,46 +24,29 @@ export const findTemplates = async ({
perPage = 10,
folderId,
}: FindTemplatesOptions) => {
const whereFilter: Prisma.TemplateWhereInput[] = [];
const whereFilter: Prisma.EnvelopeWhereInput[] = [];
if (teamId === undefined) {
whereFilter.push({ userId });
}
const { teamRole } = await getMemberRoles({
teamId,
reference: {
type: 'User',
id: userId,
},
});
if (teamId !== undefined) {
const { teamRole } = await getMemberRoles({
teamId,
reference: {
type: 'User',
id: userId,
},
});
whereFilter.push(
{ teamId },
{
OR: [
match(teamRole)
.with(TeamMemberRole.ADMIN, () => ({
visibility: {
in: [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.ADMIN,
],
},
}))
.with(TeamMemberRole.MANAGER, () => ({
visibility: {
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
},
}))
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })),
{ userId, teamId },
],
},
);
}
whereFilter.push(
{ teamId },
{
OR: [
{
visibility: {
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
},
},
{ userId, teamId },
],
},
);
if (folderId) {
whereFilter.push({ folderId });
@ -71,9 +55,10 @@ export const findTemplates = async ({
}
const [data, count] = await Promise.all([
prisma.template.findMany({
prisma.envelope.findMany({
where: {
type,
type: EnvelopeType.TEMPLATE,
templateType: type,
AND: whereFilter,
},
include: {
@ -85,7 +70,7 @@ export const findTemplates = async ({
},
fields: true,
recipients: true,
templateMeta: true,
documentMeta: true,
directLink: {
select: {
token: true,
@ -98,8 +83,10 @@ export const findTemplates = async ({
createdAt: 'desc',
},
}),
prisma.template.count({
prisma.envelope.count({
where: {
type: EnvelopeType.TEMPLATE,
templateType: type,
AND: whereFilter,
},
}),

View File

@ -1,6 +1,9 @@
import { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
export interface GetTemplateByDirectLinkTokenOptions {
token: string;
@ -9,8 +12,9 @@ export interface GetTemplateByDirectLinkTokenOptions {
export const getTemplateByDirectLinkToken = async ({
token,
}: GetTemplateByDirectLinkTokenOptions) => {
const template = await prisma.template.findFirst({
const envelope = await prisma.envelope.findFirst({
where: {
type: EnvelopeType.TEMPLATE,
directLink: {
token,
enabled: true,
@ -23,21 +27,53 @@ export const getTemplateByDirectLinkToken = async ({
fields: true,
},
},
templateDocumentData: true,
templateMeta: true,
envelopeItems: {
include: {
documentData: true,
},
},
documentMeta: true,
},
});
const directLink = template?.directLink;
const directLink = envelope?.directLink;
// Todo: Envelopes
const firstDocumentData = envelope?.envelopeItems[0]?.documentData;
// Doing this to enforce type safety for directLink.
if (!directLink) {
if (!directLink || !firstDocumentData) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const recipientsWithMappedFields = envelope.recipients.map((recipient) => ({
...recipient,
fields: recipient.fields.map((field) => ({
...field,
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
documentId: undefined,
})),
}));
// Backwards compatibility mapping.
return {
...template,
id: mapSecondaryIdToTemplateId(envelope.secondaryId),
type: envelope.templateType,
visibility: envelope.visibility,
externalId: envelope.externalId,
title: envelope.title,
userId: envelope.userId,
teamId: envelope.teamId,
authOptions: envelope.authOptions,
createdAt: envelope.createdAt,
updatedAt: envelope.updatedAt,
publicTitle: envelope.publicTitle,
publicDescription: envelope.publicDescription,
folderId: envelope.folderId,
templateDocumentData: firstDocumentData,
directLink,
fields: template.recipients.map((recipient) => recipient.fields).flat(),
templateMeta: envelope.documentMeta,
recipients: recipientsWithMappedFields,
fields: recipientsWithMappedFields.flatMap((recipient) => recipient.fields),
};
};

View File

@ -1,31 +1,38 @@
import { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildTeamWhereQuery } from '../../utils/teams';
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export type GetTemplateByIdOptions = {
id: number;
userId: number;
teamId: number;
folderId?: string | null;
};
export const getTemplateById = async ({
id,
userId,
teamId,
folderId = null,
}: GetTemplateByIdOptions) => {
const template = await prisma.template.findFirst({
where: {
export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id,
team: buildTeamWhereQuery({ teamId, userId }),
...(folderId ? { folderId } : {}),
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
directLink: true,
templateDocumentData: true,
templateMeta: true,
documentMeta: true,
envelopeItems: {
select: {
documentData: true,
},
},
recipients: true,
fields: true,
user: {
@ -39,11 +46,35 @@ export const getTemplateById = async ({
},
});
if (!template) {
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
return template;
// Todo: Envelopes
const firstTemplateDocumentData = envelope.envelopeItems[0].documentData;
if (!firstTemplateDocumentData) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template document data not found',
});
}
// eslint-disable-next-line unused-imports/no-unused-vars
const { envelopeItems, documentMeta, ...rest } = envelope;
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
return {
...rest,
type: envelope.templateType,
templateDocumentData: firstTemplateDocumentData,
templateMeta: envelope.documentMeta,
fields: envelope.fields.map((field) => ({
...field,
templateId: legacyTemplateId,
})),
id: mapSecondaryIdToTemplateId(envelope.secondaryId),
};
};

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 { mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export type ToggleTemplateDirectLinkOptions = {
templateId: number;
@ -16,24 +19,31 @@ export const toggleTemplateDirectLink = async ({
teamId,
enabled,
}: ToggleTemplateDirectLinkOptions) => {
const template = await prisma.template.findFirst({
where: {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
type: EnvelopeType.TEMPLATE,
id: {
type: 'templateId',
id: templateId,
team: buildTeamWhereQuery({ teamId, userId }),
},
userId,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
directLink: true,
},
});
if (!template) {
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const { directLink } = template;
const { directLink } = envelope;
if (!directLink) {
throw new AppError(AppErrorCode.NOT_FOUND, {
@ -41,13 +51,22 @@ export const toggleTemplateDirectLink = async ({
});
}
return await prisma.templateDirectLink.update({
const updatedDirectLink = await prisma.templateDirectLink.update({
where: {
id: directLink.id,
},
data: {
templateId: template.id,
envelopeId: envelope.id,
enabled,
},
});
return {
id: updatedDirectLink.id,
token: updatedDirectLink.token,
createdAt: updatedDirectLink.createdAt,
enabled: updatedDirectLink.enabled,
directTemplateRecipientId: updatedDirectLink.directTemplateRecipientId,
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
};
};

View File

@ -1,11 +1,19 @@
import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client';
import type { Prisma, TemplateType } from '@prisma/client';
import {
type DocumentMeta,
type DocumentVisibility,
EnvelopeType,
FolderType,
} from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export type UpdateTemplateOptions = {
userId: number;
@ -13,13 +21,14 @@ export type UpdateTemplateOptions = {
templateId: number;
data?: {
title?: string;
folderId?: string | null;
externalId?: string | null;
visibility?: DocumentVisibility;
globalAccessAuth?: TDocumentAccessAuthTypes[];
globalActionAuth?: TDocumentActionAuthTypes[];
publicTitle?: string;
publicDescription?: string;
type?: Template['type'];
type?: TemplateType;
useLegacyFieldInsertion?: boolean;
};
meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
@ -32,13 +41,20 @@ export const updateTemplate = async ({
meta = {},
data = {},
}: UpdateTemplateOptions) => {
const template = await prisma.template.findFirst({
where: {
const { envelopeWhereInput, team } = 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: {
templateMeta: true,
documentMeta: true,
team: {
select: {
organisationId: true,
@ -52,18 +68,18 @@ export const updateTemplate = async ({
},
});
if (!template) {
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
if (Object.values(data).length === 0 && Object.keys(meta).length === 0) {
return template;
return envelope;
}
const { documentAuthOption } = extractDocumentAuthMethods({
documentAuth: template.authOptions,
documentAuth: envelope.authOptions,
});
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
@ -76,7 +92,7 @@ export const updateTemplate = async ({
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
// Check if user has permission to set the global action auth.
if (newGlobalActionAuth.length > 0 && !template.team.organisation.organisationClaim.flags.cfr21) {
if (newGlobalActionAuth.length > 0 && !envelope.team.organisation.organisationClaim.flags.cfr21) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
@ -94,7 +110,7 @@ export const updateTemplate = async ({
const email = await prisma.organisationEmail.findFirst({
where: {
id: emailId,
organisationId: template.team.organisationId,
organisationId: envelope.team.organisationId,
},
});
@ -105,32 +121,63 @@ export const updateTemplate = async ({
}
}
return await prisma.template.update({
let folderUpdateQuery: Prisma.FolderUpdateOneWithoutEnvelopesNestedInput | undefined = undefined;
// Validate folder ID.
if (data.folderId) {
const folder = await prisma.folder.findFirst({
where: {
id: data.folderId,
team: buildTeamWhereQuery({
teamId,
userId,
}),
type: FolderType.TEMPLATE,
visibility: {
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
},
},
});
if (!folder) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Folder not found',
});
}
folderUpdateQuery = {
connect: {
id: data.folderId,
},
};
}
// Move template to root folder if folderId is null.
if (data.folderId === null) {
folderUpdateQuery = {
disconnect: true,
};
}
return await prisma.envelope.update({
where: {
id: templateId,
id: envelope.id,
type: EnvelopeType.TEMPLATE,
},
data: {
templateType: data?.type,
title: data?.title,
externalId: data?.externalId,
type: data?.type,
visibility: data?.visibility,
publicDescription: data?.publicDescription,
publicTitle: data?.publicTitle,
useLegacyFieldInsertion: data?.useLegacyFieldInsertion,
folder: folderUpdateQuery,
authOptions,
templateMeta: {
upsert: {
where: {
templateId,
},
create: {
...meta,
emailSettings: meta?.emailSettings || undefined,
},
update: {
...meta,
emailSettings: meta?.emailSettings || undefined,
},
documentMeta: {
update: {
...meta,
emailSettings: meta?.emailSettings || undefined,
},
},
},