mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
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:
@ -1,5 +1,10 @@
|
||||
import { initContract } from '@ts-rest/core';
|
||||
|
||||
import {
|
||||
ZCreateTemplateV2RequestSchema,
|
||||
ZCreateTemplateV2ResponseSchema,
|
||||
} from '@documenso/trpc/server/template-router/schema';
|
||||
|
||||
import {
|
||||
ZAuthorizationHeadersSchema,
|
||||
ZCreateDocumentFromTemplateMutationResponseSchema,
|
||||
@ -87,6 +92,18 @@ export const ApiContractV1 = c.router(
|
||||
summary: 'Upload a new document and get a presigned URL',
|
||||
},
|
||||
|
||||
createTemplate: {
|
||||
method: 'POST',
|
||||
path: '/api/v1/templates',
|
||||
body: ZCreateTemplateV2RequestSchema,
|
||||
responses: {
|
||||
200: ZCreateTemplateV2ResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Create a new template and get a presigned URL',
|
||||
},
|
||||
|
||||
deleteTemplate: {
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/templates/:id',
|
||||
|
||||
@ -30,6 +30,7 @@ import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-
|
||||
import { updateDocumentRecipients } from '@documenso/lib/server-only/recipient/update-document-recipients';
|
||||
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
||||
import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy';
|
||||
import { createTemplate } from '@documenso/lib/server-only/template/create-template';
|
||||
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
||||
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
@ -400,6 +401,109 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
createTemplate: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { body } = args;
|
||||
const {
|
||||
title,
|
||||
folderId,
|
||||
externalId,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
publicTitle,
|
||||
publicDescription,
|
||||
type,
|
||||
meta,
|
||||
} = body;
|
||||
|
||||
try {
|
||||
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
message: 'Create template is not available without S3 transport.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dateFormat = meta?.dateFormat
|
||||
? DATE_FORMATS.find((format) => format.value === meta?.dateFormat)
|
||||
: DATE_FORMATS.find((format) => format.value === DEFAULT_DOCUMENT_DATE_FORMAT);
|
||||
|
||||
if (meta?.dateFormat && !dateFormat) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Invalid date format. Please provide a valid date format',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const timezone = meta?.timezone
|
||||
? TIME_ZONES.find((tz) => tz === meta?.timezone)
|
||||
: DEFAULT_DOCUMENT_TIME_ZONE;
|
||||
|
||||
const isTimeZoneValid = meta?.timezone ? TIME_ZONES.includes(String(timezone)) : true;
|
||||
|
||||
if (!isTimeZoneValid) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Invalid timezone. Please provide a valid timezone',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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: team.id,
|
||||
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: team.id,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
uploadUrl: url,
|
||||
template: fullTemplate,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'An error has occured while creating the template',
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
deleteTemplate: authenticatedMiddleware(async (args, user, team, { logger }) => {
|
||||
const { id: templateId } = args.params;
|
||||
|
||||
|
||||
@ -1,16 +1,31 @@
|
||||
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 type { TCreateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions } from '../../utils/document-auth';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
|
||||
export type CreateTemplateOptions = TCreateTemplateMutationSchema & {
|
||||
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;
|
||||
@ -18,12 +33,14 @@ export const ZCreateTemplateResponseSchema = TemplateSchema;
|
||||
export type TCreateTemplateResponse = z.infer<typeof ZCreateTemplateResponseSchema>;
|
||||
|
||||
export const createTemplate = async ({
|
||||
title,
|
||||
userId,
|
||||
teamId,
|
||||
templateDocumentDataId,
|
||||
folderId,
|
||||
data,
|
||||
meta = {},
|
||||
}: CreateTemplateOptions) => {
|
||||
const { title, folderId } = data;
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
});
|
||||
@ -55,16 +72,27 @@ export const createTemplate = async ({
|
||||
return await prisma.template.create({
|
||||
data: {
|
||||
title,
|
||||
teamId,
|
||||
userId,
|
||||
templateDocumentDataId,
|
||||
teamId,
|
||||
folderId: folderId,
|
||||
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: {
|
||||
language: settings.documentLanguage,
|
||||
typedSignatureEnabled: settings.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: settings.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: settings.drawSignatureEnabled,
|
||||
...meta,
|
||||
language: meta?.language ?? settings.documentLanguage,
|
||||
typedSignatureEnabled: meta?.typedSignatureEnabled ?? settings.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: meta?.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: meta?.drawSignatureEnabled ?? settings.drawSignatureEnabled,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -207,7 +207,9 @@ export const seedTeamTemplateWithMeta = async (team: Team) => {
|
||||
const ownerUser = organisation.owner;
|
||||
|
||||
const template = await createTemplate({
|
||||
title: `[TEST] Template ${nanoid(8)} - Draft`,
|
||||
data: {
|
||||
title: `[TEST] Template ${nanoid(8)} - Draft`,
|
||||
},
|
||||
userId: ownerUser.id,
|
||||
teamId: team.id,
|
||||
templateDocumentDataId: documentData.id,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -206,8 +206,8 @@ export const AddSubjectFormPartial = ({
|
||||
|
||||
<p className="mt-2">
|
||||
<Trans>
|
||||
We will generate signing links for you, which you can send to the
|
||||
recipients through your method of choice.
|
||||
We will generate signing links for you, which you can send to the recipients
|
||||
through your method of choice.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user