Files
documenso/packages/trpc/server/template-router/router.ts
2025-11-01 12:44:34 +11:00

721 lines
19 KiB
TypeScript

import type { Envelope } from '@prisma/client';
import { DocumentDataType, EnvelopeType } from '@prisma/client';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { duplicateEnvelope } from '@documenso/lib/server-only/envelope/duplicate-envelope';
import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope';
import {
ZCreateDocumentFromDirectTemplateResponseSchema,
createDocumentFromDirectTemplate,
} from '@documenso/lib/server-only/template/create-document-from-direct-template';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { createTemplateDirectLink } from '@documenso/lib/server-only/template/create-template-direct-link';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/delete-template-direct-link';
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { mapFieldToLegacyField } from '@documenso/lib/utils/fields';
import { mapRecipientToLegacyRecipient } from '@documenso/lib/utils/recipients';
import { mapEnvelopeToTemplateLite } from '@documenso/lib/utils/templates';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
import {
ZBulkSendTemplateMutationSchema,
ZCreateDocumentFromDirectTemplateRequestSchema,
ZCreateDocumentFromTemplateRequestSchema,
ZCreateDocumentFromTemplateResponseSchema,
ZCreateTemplateDirectLinkRequestSchema,
ZCreateTemplateDirectLinkResponseSchema,
ZCreateTemplateMutationSchema,
ZCreateTemplateResponseSchema,
ZCreateTemplateV2RequestSchema,
ZCreateTemplateV2ResponseSchema,
ZDeleteTemplateDirectLinkRequestSchema,
ZDeleteTemplateMutationSchema,
ZDuplicateTemplateMutationSchema,
ZDuplicateTemplateResponseSchema,
ZFindTemplatesRequestSchema,
ZFindTemplatesResponseSchema,
ZGetTemplateByIdRequestSchema,
ZGetTemplateByIdResponseSchema,
ZToggleTemplateDirectLinkRequestSchema,
ZToggleTemplateDirectLinkResponseSchema,
ZUpdateTemplateRequestSchema,
ZUpdateTemplateResponseSchema,
} from './schema';
export const templateRouter = router({
/**
* @public
*/
findTemplates: authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/template',
summary: 'Find templates',
description: 'Find templates based on a search criteria',
tags: ['Template'],
},
})
.input(ZFindTemplatesRequestSchema)
.output(ZFindTemplatesResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
ctx.logger.info({
input: {
folderId: input.folderId,
},
});
const result = await findTemplates({
userId: ctx.user.id,
teamId,
...input,
});
// Remapping for backwards compatibility.
return {
...result,
data: result.data.map((envelope) => {
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
return {
id: legacyTemplateId,
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,
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
team: envelope.team,
fields: envelope.fields.map((field) => mapFieldToLegacyField(field, envelope)),
recipients: envelope.recipients.map((recipient) =>
mapRecipientToLegacyRecipient(recipient, envelope),
),
templateMeta: envelope.documentMeta,
directLink: envelope.directLink,
};
}),
};
}),
/**
* @public
*/
getTemplateById: authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/template/{templateId}',
summary: 'Get template',
tags: ['Template'],
},
})
.input(ZGetTemplateByIdRequestSchema)
.output(ZGetTemplateByIdResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
ctx.logger.info({
input: {
templateId,
},
});
return await getTemplateById({
id: {
type: 'templateId',
id: templateId,
},
userId: ctx.user.id,
teamId,
});
}),
/**
* Wait until RR7 so we can passthrough documents.
*
* @private
*/
createTemplate: authenticatedProcedure
// .meta({ // Note before releasing this to public, update the response schema to be correct.
// openapi: {
// method: 'POST',
// path: '/template/create',
// summary: 'Create template',
// description: 'Create a new template',
// tags: ['Template'],
// },
// })
.input(ZCreateTemplateMutationSchema)
.output(ZCreateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { title, templateDocumentDataId, folderId } = input;
ctx.logger.info({
input: {
folderId,
},
});
const envelope = await createEnvelope({
userId: ctx.user.id,
teamId,
internalVersion: 1,
data: {
type: EnvelopeType.TEMPLATE,
title,
folderId,
envelopeItems: [
{
documentDataId: templateDocumentDataId,
},
],
},
requestMetadata: ctx.metadata,
});
return {
legacyTemplateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
};
}),
/**
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
*
* @public
* @deprecated
*/
createTemplateTemporary: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/create/beta',
summary: 'Create template',
description:
'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
tags: ['Template'],
},
})
.input(ZCreateTemplateV2RequestSchema)
.output(ZCreateTemplateV2ResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const {
title,
folderId,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
publicTitle,
publicDescription,
type,
meta,
attachments,
} = input;
const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`;
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
const templateDocumentData = await createDocumentData({
data: key,
type: DocumentDataType.S3_PATH,
});
const createdTemplate = await createEnvelope({
userId: user.id,
teamId,
internalVersion: 1,
data: {
type: EnvelopeType.TEMPLATE,
title,
envelopeItems: [
{
documentDataId: templateDocumentData.id,
},
],
folderId,
externalId: externalId ?? undefined,
visibility,
globalAccessAuth,
globalActionAuth,
templateType: type,
publicTitle,
publicDescription,
},
meta,
attachments,
requestMetadata: ctx.metadata,
});
const legacyTemplateId = mapSecondaryIdToTemplateId(createdTemplate.secondaryId);
const fullTemplate = await getTemplateById({
id: {
type: 'templateId',
id: legacyTemplateId,
},
userId: user.id,
teamId,
});
return {
template: fullTemplate,
uploadUrl: url,
};
}),
/**
* @public
*/
updateTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/update',
summary: 'Update template',
tags: ['Template'],
},
})
.input(ZUpdateTemplateRequestSchema)
.output(ZUpdateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, data, meta } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
templateId,
},
});
const envelope = await updateEnvelope({
userId,
teamId,
id: {
type: 'templateId',
id: templateId,
},
data: {
...data,
templateType: data?.type, // Backwards compatibility.
},
meta,
requestMetadata: ctx.metadata,
});
return mapEnvelopeToTemplateLite(envelope);
}),
/**
* @public
*/
duplicateTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/duplicate',
summary: 'Duplicate template',
tags: ['Template'],
},
})
.input(ZDuplicateTemplateMutationSchema)
.output(ZDuplicateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
ctx.logger.info({
input: {
templateId,
},
});
const duplicatedEnvelope = await duplicateEnvelope({
userId: ctx.user.id,
teamId,
id: {
type: 'templateId',
id: templateId,
},
});
return mapEnvelopeToTemplateLite(duplicatedEnvelope.envelope);
}),
/**
* @public
*/
deleteTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/delete',
summary: 'Delete template',
tags: ['Template'],
},
})
.input(ZDeleteTemplateMutationSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
templateId,
},
});
await deleteTemplate({
userId,
id: {
type: 'templateId',
id: templateId,
},
teamId,
});
return ZGenericSuccessResponse;
}),
/**
* @public
*/
createDocumentFromTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/use',
summary: 'Use template',
description: 'Use the template to create a document',
tags: ['Template'],
},
})
.input(ZCreateDocumentFromTemplateRequestSchema)
.output(ZCreateDocumentFromTemplateResponseSchema)
.mutation(async ({ ctx, input }) => {
const { teamId } = ctx;
const {
templateId,
recipients,
distributeDocument,
customDocumentDataId,
prefillFields,
folderId,
} = input;
ctx.logger.info({
input: {
templateId,
},
});
const limits = await getServerLimits({ userId: ctx.user.id, teamId });
if (limits.remaining.documents === 0) {
throw new Error('You have reached your document limit.');
}
// Backwards compatibility mapping since we need the envelopeItemId for the custom document data.
const customDocumentData = customDocumentDataId
? [
{
documentDataId: customDocumentDataId,
envelopeItemId: undefined,
},
]
: input.customDocumentData || [];
const envelope: Envelope = await createDocumentFromTemplate({
id: {
type: 'templateId',
id: templateId,
},
teamId,
userId: ctx.user.id,
recipients,
customDocumentData,
requestMetadata: ctx.metadata,
folderId,
prefillFields,
});
if (distributeDocument) {
await sendDocument({
id: {
type: 'envelopeId',
id: envelope.id,
},
userId: ctx.user.id,
teamId,
requestMetadata: ctx.metadata,
}).catch((err) => {
console.error(err);
throw new AppError('DOCUMENT_SEND_FAILED');
});
}
return getDocumentWithDetailsById({
id: {
type: 'envelopeId',
id: envelope.id,
},
userId: ctx.user.id,
teamId,
});
}),
/**
* Leaving this endpoint as private for now until there is a use case for it.
*
* @private
*/
createDocumentFromDirectTemplate: maybeAuthenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/template/direct/use',
// summary: 'Use direct template',
// description: 'Use a direct template to create a document',
// tags: ['Template'],
// },
// })
.input(ZCreateDocumentFromDirectTemplateRequestSchema)
.output(ZCreateDocumentFromDirectTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const {
directRecipientName,
directRecipientEmail,
directTemplateToken,
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
nextSigner,
} = input;
ctx.logger.info({
input: {
directTemplateToken,
},
});
return await createDocumentFromDirectTemplate({
directRecipientName,
directRecipientEmail,
directTemplateToken,
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
user: ctx.user
? {
id: ctx.user.id,
name: ctx.user.name || undefined,
email: ctx.user.email,
}
: undefined,
nextSigner,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
createTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/direct/create',
summary: 'Create direct link',
description: 'Create a direct link for a template',
tags: ['Template'],
},
})
.input(ZCreateTemplateDirectLinkRequestSchema)
.output(ZCreateTemplateDirectLinkResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, directRecipientId } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
templateId,
directRecipientId,
},
});
const template = await getTemplateById({
id: {
type: 'templateId',
id: templateId,
},
teamId,
userId: ctx.user.id,
});
const limits = await getServerLimits({ userId: ctx.user.id, teamId: template.teamId });
if (limits.remaining.directTemplates === 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your direct templates limit.',
});
}
return await createTemplateDirectLink({
userId,
teamId,
id: {
type: 'templateId',
id: templateId,
},
directRecipientId,
});
}),
/**
* @public
*/
deleteTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/direct/delete',
summary: 'Delete direct link',
description: 'Delete a direct link for a template',
tags: ['Template'],
},
})
.input(ZDeleteTemplateDirectLinkRequestSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
templateId,
},
});
await deleteTemplateDirectLink({ userId, teamId, templateId });
return ZGenericSuccessResponse;
}),
/**
* @public
*/
toggleTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/direct/toggle',
summary: 'Toggle direct link',
description: 'Enable or disable a direct link for a template',
tags: ['Template'],
},
})
.input(ZToggleTemplateDirectLinkRequestSchema)
.output(ZToggleTemplateDirectLinkResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, enabled } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
templateId,
},
});
return await toggleTemplateDirectLink({ userId, teamId, templateId, enabled });
}),
/**
* @private
*/
uploadBulkSend: authenticatedProcedure
.input(ZBulkSendTemplateMutationSchema)
.mutation(async ({ ctx, input }) => {
const { templateId, teamId, csv, sendImmediately } = input;
const { user } = ctx;
ctx.logger.info({
input: {
templateId,
teamId,
},
});
if (csv.length > 4 * 1024 * 1024) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'File size exceeds 4MB limit',
statusCode: 400,
});
}
const template = await getTemplateById({
id: {
type: 'templateId',
id: templateId,
},
teamId,
userId: user.id,
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
await jobs.triggerJob({
name: 'internal.bulk-send-template',
payload: {
userId: user.id,
teamId,
templateId,
csvContent: csv,
sendImmediately,
requestMetadata: ctx.metadata.requestMetadata,
},
});
return { success: true };
}),
});