feat: upload template via API (#1842)

Allow users to upload templates via both v1 and v2 APIs. Similar to
uploading documents.
This commit is contained in:
Catalin Pit
2025-07-23 07:41:12 +03:00
committed by GitHub
parent 5570690b3b
commit 7a5a9eefe8
9 changed files with 322 additions and 55 deletions
@@ -322,7 +322,7 @@ export const documentRouter = router({
return {
document: createdDocument,
folder: createdDocument.folder,
folder: createdDocument.folder, // Todo: Remove this prior to api-v2 release.
uploadUrl: url,
};
}),
@@ -33,7 +33,9 @@ export const createEmbeddingTemplateRoute = procedure
// First create the template
const template = await createTemplate({
userId: apiToken.userId,
title,
data: {
title,
},
templateDocumentDataId: documentDataId,
teamId: apiToken.teamId ?? undefined,
});
+83 -2
View File
@@ -1,9 +1,11 @@
import type { Document } from '@prisma/client';
import { DocumentDataType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
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 {
@@ -23,6 +25,7 @@ import { findTemplates } from '@documenso/lib/server-only/template/find-template
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 { updateTemplate } from '@documenso/lib/server-only/template/update-template';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
@@ -34,6 +37,8 @@ import {
ZCreateTemplateDirectLinkRequestSchema,
ZCreateTemplateDirectLinkResponseSchema,
ZCreateTemplateMutationSchema,
ZCreateTemplateV2RequestSchema,
ZCreateTemplateV2ResponseSchema,
ZDeleteTemplateDirectLinkRequestSchema,
ZDeleteTemplateMutationSchema,
ZDuplicateTemplateMutationSchema,
@@ -141,12 +146,88 @@ export const templateRouter = router({
return await createTemplate({
userId: ctx.user.id,
teamId,
title,
templateDocumentDataId,
folderId,
data: {
title,
folderId,
},
});
}),
/**
* 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,
} = 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 createTemplate({
userId: user.id,
teamId,
templateDocumentDataId: templateDocumentData.id,
data: {
title,
folderId,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
publicTitle,
publicDescription,
type,
},
meta,
});
const fullTemplate = await getTemplateById({
id: createdTemplate.id,
userId: user.id,
teamId,
});
return {
template: fullTemplate,
uploadUrl: url,
};
}),
/**
* @public
*/
+71 -38
View File
@@ -30,6 +30,50 @@ import {
} from '../document-router/schema';
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
export const MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH = 256;
export const ZTemplateTitleSchema = z
.string()
.trim()
.min(1)
.max(255)
.describe('The title of the document.');
export const ZTemplatePublicTitleSchema = z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH)
.describe(
'The title of the template that will be displayed to the public. Only applicable for public templates.',
);
export const ZTemplatePublicDescriptionSchema = z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.describe(
'The description of the template that will be displayed to the public. Only applicable for public templates.',
);
export const ZTemplateMetaUpsertSchema = z.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
});
export const ZCreateTemplateMutationSchema = z.object({
title: z.string().min(1).trim(),
templateDocumentDataId: z.string().min(1),
@@ -123,57 +167,46 @@ export const ZDeleteTemplateMutationSchema = z.object({
templateId: z.number(),
});
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
export const MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH = 256;
/**
* Note: This is the same between V1 and V2. Be careful when updating this schema and think of the consequences.
*/
export const ZCreateTemplateV2RequestSchema = z.object({
title: ZTemplateTitleSchema,
folderId: z.string().optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
publicTitle: ZTemplatePublicTitleSchema.optional(),
publicDescription: ZTemplatePublicDescriptionSchema.optional(),
type: z.nativeEnum(TemplateType).optional(),
meta: ZTemplateMetaUpsertSchema.optional(),
});
/**
* Note: This is the same between V1 and V2. Be careful when updating this schema and think of the consequences.
*/
export const ZCreateTemplateV2ResponseSchema = z.object({
template: ZTemplateSchema,
uploadUrl: z.string().min(1),
});
export const ZUpdateTemplateRequestSchema = z.object({
templateId: z.number(),
data: z
.object({
title: z.string().min(1).optional(),
title: ZTemplateTitleSchema.optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
publicTitle: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH)
.describe(
'The title of the template that will be displayed to the public. Only applicable for public templates.',
)
.optional(),
publicDescription: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.describe(
'The description of the template that will be displayed to the public. Only applicable for public templates.',
)
.optional(),
publicTitle: ZTemplatePublicTitleSchema.optional(),
publicDescription: ZTemplatePublicDescriptionSchema.optional(),
type: z.nativeEnum(TemplateType).optional(),
useLegacyFieldInsertion: z.boolean().optional(),
})
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
})
.optional(),
meta: ZTemplateMetaUpsertSchema.optional(),
});
export const ZUpdateTemplateResponseSchema = ZTemplateLiteSchema;