mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
feat: add envelopes (#2025)
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.
This commit is contained in:
@ -5,6 +5,7 @@ import type { Field, Signature } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
FieldType,
|
||||
Prisma,
|
||||
RecipientRole,
|
||||
@ -31,9 +32,11 @@ 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 { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { isRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
|
||||
@ -43,11 +46,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 +95,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 +108,12 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
},
|
||||
},
|
||||
directLink: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
@ -115,19 +124,29 @@ 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,
|
||||
);
|
||||
|
||||
if (directTemplateEnvelope.envelopeItems.length < 1) {
|
||||
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,11 +224,11 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
}
|
||||
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
documentAuthOptions: template.authOptions,
|
||||
documentAuthOptions: directTemplateEnvelope.authOptions,
|
||||
recipient: {
|
||||
authOptions: directTemplateRecipient.authOptions,
|
||||
email: directRecipientEmail,
|
||||
documentId: template.id,
|
||||
envelopeId: directTemplateEnvelope.id,
|
||||
},
|
||||
field: templateField,
|
||||
userId: user?.id,
|
||||
@ -267,29 +289,73 @@ 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,
|
||||
},
|
||||
});
|
||||
// Key = original envelope item ID
|
||||
// Value = duplicated envelope item ID.
|
||||
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
|
||||
|
||||
// Create the document and non direct template recipients.
|
||||
const document = await tx.document.create({
|
||||
// Duplicate the envelope item data.
|
||||
const envelopeItemsToCreate = await Promise.all(
|
||||
directTemplateEnvelope.envelopeItems.map(async (item, i) => {
|
||||
const buffer = await getFileServerSide(item.documentData);
|
||||
|
||||
const titleToUse = item.title || directTemplateEnvelope.title;
|
||||
|
||||
const duplicatedFile = await putPdfFileServerSide({
|
||||
name: titleToUse,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(buffer),
|
||||
});
|
||||
|
||||
const newDocumentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: duplicatedFile.type,
|
||||
data: duplicatedFile.data,
|
||||
initialData: duplicatedFile.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
const newEnvelopeItemId = prefixedId('envelope_item');
|
||||
|
||||
oldEnvelopeItemToNewEnvelopeItemIdMap[item.id] = newEnvelopeItemId;
|
||||
|
||||
return {
|
||||
id: newEnvelopeItemId,
|
||||
title: titleToUse.endsWith('.pdf') ? titleToUse.slice(0, -4) : titleToUse,
|
||||
documentDataId: newDocumentData.id,
|
||||
order: item.order !== undefined ? item.order : i + 1,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
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,
|
||||
internalVersion: 1,
|
||||
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: {
|
||||
createMany: {
|
||||
data: envelopeItemsToCreate,
|
||||
},
|
||||
},
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
||||
globalActionAuth: templateAuthOptions.globalActionAuth,
|
||||
@ -319,9 +385,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
}),
|
||||
},
|
||||
},
|
||||
documentMeta: {
|
||||
create: derivedDocumentMeta,
|
||||
},
|
||||
documentMetaId: documentMeta.id,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
@ -330,13 +394,18 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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 +415,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
nonDirectRecipientFieldsToCreate = nonDirectRecipientFieldsToCreate.concat(
|
||||
templateRecipient.fields.map((field) => ({
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
|
||||
recipientId: recipient.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
@ -371,7 +441,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 +457,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
fields: {
|
||||
createMany: {
|
||||
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => ({
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
||||
type: templateField.type,
|
||||
page: templateField.page,
|
||||
positionX: templateField.positionX,
|
||||
@ -417,7 +488,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
const field = await tx.field.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
||||
recipientId: createdDirectRecipient.id,
|
||||
type: templateField.type,
|
||||
page: templateField.page,
|
||||
@ -466,7 +538,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 +546,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 +574,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 +619,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 +644,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 +672,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
createdEnvelope,
|
||||
token: createdDirectRecipient.token,
|
||||
documentId: document.id,
|
||||
recipientId: createdDirectRecipient.id,
|
||||
};
|
||||
});
|
||||
@ -609,18 +681,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 +703,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 +716,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
return {
|
||||
token,
|
||||
documentId,
|
||||
documentId: incrementedDocumentId.documentId,
|
||||
recipientId,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
EnvelopeType,
|
||||
type Field,
|
||||
FolderType,
|
||||
type Recipient,
|
||||
@ -37,9 +38,11 @@ import {
|
||||
} from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import {
|
||||
@ -47,7 +50,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 +67,7 @@ type FinalRecipient = Pick<
|
||||
};
|
||||
|
||||
export type CreateDocumentFromTemplateOptions = {
|
||||
templateId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
externalId?: string | null;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
@ -72,7 +79,17 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
}[];
|
||||
folderId?: string;
|
||||
prefillFields?: TFieldMetaPrefillFieldsSchema[];
|
||||
customDocumentDataId?: string;
|
||||
|
||||
customDocumentData?: {
|
||||
documentDataId: string;
|
||||
|
||||
/**
|
||||
* The envelope item ID which will be updated to use the custom document data.
|
||||
*
|
||||
* If undefined, will use the first envelope item. This is done for backwards compatibility reasons.
|
||||
*/
|
||||
envelopeItemId?: string;
|
||||
}[];
|
||||
|
||||
/**
|
||||
* Values that will override the predefined values in the template.
|
||||
@ -268,30 +285,38 @@ const getUpdatedFieldMeta = (field: Field, prefillField?: TFieldMetaPrefillField
|
||||
};
|
||||
|
||||
export const createDocumentFromTemplate = async ({
|
||||
templateId,
|
||||
id,
|
||||
externalId,
|
||||
userId,
|
||||
teamId,
|
||||
recipients,
|
||||
customDocumentDataId,
|
||||
customDocumentData = [],
|
||||
override,
|
||||
requestMetadata,
|
||||
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 +342,15 @@ export const createDocumentFromTemplate = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const legacyTemplateId = mapSecondaryIdToTemplateId(template.secondaryId);
|
||||
const finalEnvelopeTitle = override?.title || template.title;
|
||||
|
||||
if (template.envelopeItems.length < 1) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Template must have at least 1 envelope item',
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
@ -354,74 +388,121 @@ export const createDocumentFromTemplate = async ({
|
||||
};
|
||||
});
|
||||
|
||||
let parentDocumentData = template.templateDocumentData;
|
||||
const firstEnvelopeItemId = template.envelopeItems[0].id;
|
||||
|
||||
if (customDocumentDataId) {
|
||||
const customDocumentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: customDocumentDataId,
|
||||
},
|
||||
});
|
||||
// Key = original envelope item ID
|
||||
// Value = duplicated envelope item ID.
|
||||
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
|
||||
|
||||
if (!customDocumentData) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Custom document data not found',
|
||||
// Duplicate the envelope item data.
|
||||
// Note: This is duplicated in createDocumentFromDirectTemplate
|
||||
const envelopeItemsToCreate = await Promise.all(
|
||||
template.envelopeItems.map(async (item, i) => {
|
||||
let documentDataIdToDuplicate = item.documentDataId;
|
||||
|
||||
const foundCustomDocumentData = customDocumentData.find(
|
||||
(customDocumentDataItem) =>
|
||||
customDocumentDataItem.envelopeItemId || firstEnvelopeItemId === item.id,
|
||||
);
|
||||
|
||||
if (foundCustomDocumentData) {
|
||||
documentDataIdToDuplicate = foundCustomDocumentData.documentDataId;
|
||||
}
|
||||
|
||||
const documentDataToDuplicate = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: documentDataIdToDuplicate,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
parentDocumentData = customDocumentData;
|
||||
}
|
||||
if (!documentDataToDuplicate) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document data not found',
|
||||
});
|
||||
}
|
||||
|
||||
const documentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: parentDocumentData.type,
|
||||
data: parentDocumentData.data,
|
||||
initialData: parentDocumentData.initialData,
|
||||
},
|
||||
const buffer = await getFileServerSide(documentDataToDuplicate);
|
||||
|
||||
const titleToUse = item.title || finalEnvelopeTitle;
|
||||
|
||||
const duplicatedFile = await putPdfFileServerSide({
|
||||
name: titleToUse,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(buffer),
|
||||
});
|
||||
|
||||
const newDocumentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: duplicatedFile.type,
|
||||
data: duplicatedFile.data,
|
||||
initialData: duplicatedFile.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
const newEnvelopeItemId = prefixedId('envelope_item');
|
||||
|
||||
oldEnvelopeItemToNewEnvelopeItemIdMap[item.id] = newEnvelopeItemId;
|
||||
|
||||
return {
|
||||
id: newEnvelopeItemId,
|
||||
title: titleToUse.endsWith('.pdf') ? titleToUse.slice(0, -4) : titleToUse,
|
||||
documentDataId: newDocumentData.id,
|
||||
order: item.order !== undefined ? item.order : i + 1,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
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,
|
||||
internalVersion: template.internalVersion,
|
||||
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,
|
||||
title: finalEnvelopeTitle,
|
||||
envelopeItems: {
|
||||
createMany: {
|
||||
data: envelopeItemsToCreate,
|
||||
},
|
||||
},
|
||||
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 +535,15 @@ export const createDocumentFromTemplate = async ({
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
|
||||
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId'>[] = [];
|
||||
|
||||
// Get all template field IDs first so we can validate later
|
||||
const allTemplateFieldIds = finalRecipients.flatMap((recipient) =>
|
||||
@ -502,7 +587,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 +598,8 @@ export const createDocumentFromTemplate = async ({
|
||||
const prefillField = prefillFields?.find((value) => value.id === field.id);
|
||||
|
||||
const payload = {
|
||||
documentId: document.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
|
||||
envelopeId: envelope.id,
|
||||
recipientId: recipient.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
@ -544,7 +630,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 +655,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 +677,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;
|
||||
});
|
||||
};
|
||||
|
||||
@ -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,21 @@ 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),
|
||||
envelopeId: envelope.id,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,112 +0,0 @@
|
||||
import type { DocumentVisibility, Template, TemplateMeta } 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<TemplateMeta, '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),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,18 +1,25 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { type EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type DeleteTemplateOptions = {
|
||||
id: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptions) => {
|
||||
return await prisma.template.delete({
|
||||
where: {
|
||||
id,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return await prisma.envelope.delete({
|
||||
where: envelopeWhereInput,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { omit } from 'remeda';
|
||||
|
||||
import { nanoid } 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';
|
||||
|
||||
export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const duplicateTemplate = async ({
|
||||
templateId,
|
||||
userId,
|
||||
teamId,
|
||||
}: DuplicateTemplateOptions) => {
|
||||
const template = await prisma.template.findUnique({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
signingOrder: true,
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const duplicatedTemplate = await prisma.template.create({
|
||||
data: {
|
||||
userId,
|
||||
teamId,
|
||||
title: template.title + ' (copy)',
|
||||
templateDocumentDataId: documentData.id,
|
||||
authOptions: template.authOptions || undefined,
|
||||
visibility: template.visibility,
|
||||
templateMeta,
|
||||
},
|
||||
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,
|
||||
})),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
for (const recipientData of recipientsToCreate) {
|
||||
await prisma.recipient.create({
|
||||
data: recipientData,
|
||||
});
|
||||
}
|
||||
|
||||
return duplicatedTemplate;
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
}),
|
||||
|
||||
@ -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,66 @@ 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,
|
||||
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
documentId: null,
|
||||
fields: recipient.fields.map((field) => ({
|
||||
...field,
|
||||
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
documentId: null,
|
||||
})),
|
||||
}));
|
||||
|
||||
// Backwards compatibility mapping.
|
||||
return {
|
||||
...template,
|
||||
directLink,
|
||||
fields: template.recipients.map((recipient) => recipient.fields).flat(),
|
||||
id: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
envelopeId: envelope.id,
|
||||
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,
|
||||
templateDocumentDataId: firstDocumentData.id,
|
||||
templateDocumentData: {
|
||||
...firstDocumentData,
|
||||
envelopeItemId: envelope.envelopeItems[0].id,
|
||||
},
|
||||
directLink: {
|
||||
...directLink,
|
||||
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
},
|
||||
templateMeta: {
|
||||
...envelope.documentMeta,
|
||||
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
},
|
||||
recipients: recipientsWithMappedFields,
|
||||
fields: recipientsWithMappedFields.flatMap((recipient) => recipient.fields),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,31 +1,37 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type GetTemplateByIdOptions = {
|
||||
id: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
folderId?: string | null;
|
||||
};
|
||||
|
||||
export const getTemplateById = async ({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
folderId = null,
|
||||
}: GetTemplateByIdOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
...(folderId ? { folderId } : {}),
|
||||
},
|
||||
export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
directLink: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
documentMeta: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
fields: true,
|
||||
user: {
|
||||
@ -39,11 +45,54 @@ export const getTemplateById = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
return template;
|
||||
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,
|
||||
envelopeId: envelope.id,
|
||||
type: envelope.templateType,
|
||||
templateDocumentDataId: firstTemplateDocumentData.id,
|
||||
templateDocumentData: {
|
||||
...firstTemplateDocumentData,
|
||||
envelopeItemId: envelope.envelopeItems[0].id,
|
||||
},
|
||||
templateMeta: {
|
||||
...envelope.documentMeta,
|
||||
templateId: legacyTemplateId,
|
||||
},
|
||||
fields: envelope.fields.map((field) => ({
|
||||
...field,
|
||||
documentId: null,
|
||||
templateId: legacyTemplateId,
|
||||
})),
|
||||
recipients: envelope.recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
documentId: null,
|
||||
templateId: legacyTemplateId,
|
||||
})),
|
||||
directLink: envelope.directLink
|
||||
? {
|
||||
...envelope.directLink,
|
||||
templateId: legacyTemplateId,
|
||||
}
|
||||
: null,
|
||||
id: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
};
|
||||
};
|
||||
|
||||
@ -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,23 @@ 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),
|
||||
envelopeId: envelope.id,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,138 +0,0 @@
|
||||
import type { DocumentVisibility, Template, TemplateMeta } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
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';
|
||||
|
||||
export type UpdateTemplateOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
data?: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
publicTitle?: string;
|
||||
publicDescription?: string;
|
||||
type?: Template['type'];
|
||||
useLegacyFieldInsertion?: boolean;
|
||||
};
|
||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||
};
|
||||
|
||||
export const updateTemplate = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
meta = {},
|
||||
data = {},
|
||||
}: UpdateTemplateOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
include: {
|
||||
templateMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
organisationId: true,
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.values(data).length === 0 && Object.keys(meta).length === 0) {
|
||||
return template;
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: template.authOptions,
|
||||
});
|
||||
|
||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
||||
|
||||
// If the new global auth values aren't passed in, fallback to the current document values.
|
||||
const newGlobalAccessAuth =
|
||||
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||
const newGlobalActionAuth =
|
||||
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) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: newGlobalAccessAuth,
|
||||
globalActionAuth: newGlobalActionAuth,
|
||||
});
|
||||
|
||||
const emailId = meta.emailId;
|
||||
|
||||
// Validate the emailId belongs to the organisation.
|
||||
if (emailId) {
|
||||
const email = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisationId: template.team.organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.template.update({
|
||||
where: {
|
||||
id: templateId,
|
||||
},
|
||||
data: {
|
||||
title: data?.title,
|
||||
externalId: data?.externalId,
|
||||
type: data?.type,
|
||||
visibility: data?.visibility,
|
||||
publicDescription: data?.publicDescription,
|
||||
publicTitle: data?.publicTitle,
|
||||
useLegacyFieldInsertion: data?.useLegacyFieldInsertion,
|
||||
authOptions,
|
||||
templateMeta: {
|
||||
upsert: {
|
||||
where: {
|
||||
templateId,
|
||||
},
|
||||
create: {
|
||||
...meta,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
},
|
||||
update: {
|
||||
...meta,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user