feat: add template and field endpoints (#1572)

This commit is contained in:
David Nguyen
2025-01-11 15:33:20 +11:00
committed by GitHub
parent 6520bbd5e3
commit ebbe922982
92 changed files with 3920 additions and 1396 deletions

View File

@@ -1,8 +1,6 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import {
ZGetDocumentWithDetailsByIdResponseSchema,
@@ -45,10 +43,9 @@ import {
toggleTemplateDirectLink,
} from '@documenso/lib/server-only/template/toggle-template-direct-link';
import {
ZUpdateTemplateSettingsResponseSchema,
updateTemplateSettings,
} from '@documenso/lib/server-only/template/update-template-settings';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
ZUpdateTemplateResponseSchema,
updateTemplate,
} from '@documenso/lib/server-only/template/update-template';
import type { Document } from '@documenso/prisma/client';
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
@@ -63,10 +60,8 @@ import {
ZFindTemplatesQuerySchema,
ZGetTemplateByIdQuerySchema,
ZMoveTemplatesToTeamSchema,
ZSetSigningOrderForTemplateMutationSchema,
ZToggleTemplateDirectLinkMutationSchema,
ZUpdateTemplateSettingsMutationSchema,
ZUpdateTemplateTypedSignatureSettingsMutationSchema,
ZUpdateTemplateRequestSchema,
} from './schema';
export const templateRouter = router({
@@ -77,7 +72,7 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'GET',
path: '/template/find',
path: '/template',
summary: 'Find templates',
description: 'Find templates based on a search criteria',
tags: ['Template'],
@@ -86,8 +81,11 @@ export const templateRouter = router({
.input(ZFindTemplatesQuerySchema)
.output(ZFindTemplatesResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
return await findTemplates({
userId: ctx.user.id,
teamId,
...input,
});
}),
@@ -107,7 +105,8 @@ export const templateRouter = router({
.input(ZGetTemplateByIdQuerySchema)
.output(ZGetTemplateByIdResponseSchema)
.query(async ({ input, ctx }) => {
const { templateId, teamId } = input;
const { teamId } = ctx;
const { templateId } = input;
return await getTemplateById({
id: templateId,
@@ -117,22 +116,25 @@ export const templateRouter = router({
}),
/**
* @public
* Wait until RR7 so we can passthrough documents.
*
* @private
*/
createTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/create',
summary: 'Create template',
description: 'Create a new template',
tags: ['Template'],
},
})
// .meta({
// 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, title, templateDocumentDataId } = input;
const { teamId } = ctx;
const { title, templateDocumentDataId } = input;
return await createTemplate({
userId: ctx.user.id,
@@ -149,30 +151,25 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}',
path: '/template/update',
summary: 'Update template',
tags: ['Template'],
},
})
.input(ZUpdateTemplateSettingsMutationSchema)
.output(ZUpdateTemplateSettingsResponseSchema)
.input(ZUpdateTemplateRequestSchema)
.output(ZUpdateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, data, meta } = input;
const { teamId } = ctx;
const { templateId, data, meta } = input;
const userId = ctx.user.id;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
return await updateTemplateSettings({
return await updateTemplate({
userId,
teamId,
templateId,
data,
meta: {
...meta,
language: isValidLanguageCode(meta?.language) ? meta?.language : undefined,
},
requestMetadata,
meta,
});
}),
@@ -183,7 +180,7 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/duplicate',
path: '/template/duplicate',
summary: 'Duplicate template',
tags: ['Template'],
},
@@ -191,7 +188,8 @@ export const templateRouter = router({
.input(ZDuplicateTemplateMutationSchema)
.output(ZDuplicateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, templateId } = input;
const { teamId } = ctx;
const { templateId } = input;
return await duplicateTemplate({
userId: ctx.user.id,
@@ -206,8 +204,8 @@ export const templateRouter = router({
deleteTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/delete',
method: 'DELETE',
path: '/template/{templateId}',
summary: 'Delete template',
tags: ['Template'],
},
@@ -215,7 +213,8 @@ export const templateRouter = router({
.input(ZDeleteTemplateMutationSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { templateId, teamId } = input;
const { teamId } = ctx;
const { templateId } = input;
const userId = ctx.user.id;
@@ -229,7 +228,7 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/use',
path: '/template/use',
summary: 'Use template',
description: 'Use the template to create a document',
tags: ['Template'],
@@ -238,7 +237,8 @@ export const templateRouter = router({
.input(ZCreateDocumentFromTemplateMutationSchema)
.output(ZGetDocumentWithDetailsByIdResponseSchema)
.mutation(async ({ ctx, input }) => {
const { templateId, teamId, recipients, distributeDocument, customDocumentDataId } = input;
const { teamId } = ctx;
const { templateId, recipients, distributeDocument, customDocumentDataId } = input;
const limits = await getServerLimits({ email: ctx.user.email, teamId });
@@ -246,15 +246,13 @@ export const templateRouter = router({
throw new Error('You have reached your document limit.');
}
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
const document: Document = await createDocumentFromTemplate({
templateId,
teamId,
userId: ctx.user.id,
recipients,
customDocumentDataId,
requestMetadata,
requestMetadata: ctx.metadata,
});
if (distributeDocument) {
@@ -262,7 +260,7 @@ export const templateRouter = router({
documentId: document.id,
userId: ctx.user.id,
teamId,
requestMetadata,
requestMetadata: ctx.metadata,
}).catch((err) => {
console.error(err);
@@ -278,18 +276,20 @@ export const templateRouter = router({
}),
/**
* @public
* Leaving this endpoint as private for now until there is a use case for it.
*
* @private
*/
createDocumentFromDirectTemplate: maybeAuthenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/use',
summary: 'Use direct template',
description: 'Use a direct template to create a document',
tags: ['Template'],
},
})
// .meta({
// openapi: {
// method: 'POST',
// path: '/template/direct/use',
// summary: 'Use direct template',
// description: 'Use a direct template to create a document',
// tags: ['Template'],
// },
// })
.input(ZCreateDocumentFromDirectTemplateMutationSchema)
.output(ZCreateDocumentFromDirectTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
@@ -302,8 +302,6 @@ export const templateRouter = router({
templateUpdatedAt,
} = input;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
return await createDocumentFromDirectTemplate({
directRecipientName,
directRecipientEmail,
@@ -318,25 +316,7 @@ export const templateRouter = router({
email: ctx.user.email,
}
: undefined,
requestMetadata,
});
}),
/**
* @private
*/
setSigningOrderForTemplate: authenticatedProcedure
.input(ZSetSigningOrderForTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, signingOrder } = input;
return await updateTemplateSettings({
templateId,
teamId,
data: {},
meta: { signingOrder },
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@@ -347,7 +327,7 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/direct/create',
path: '/template/direct/create',
summary: 'Create direct link',
description: 'Create a direct link for a template',
tags: ['Template'],
@@ -356,7 +336,8 @@ export const templateRouter = router({
.input(ZCreateTemplateDirectLinkMutationSchema)
.output(ZCreateTemplateDirectLinkResponseSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, directRecipientId } = input;
const { teamId } = ctx;
const { templateId, directRecipientId } = input;
const userId = ctx.user.id;
@@ -370,7 +351,7 @@ export const templateRouter = router({
});
}
return await createTemplateDirectLink({ userId, templateId, directRecipientId });
return await createTemplateDirectLink({ userId, teamId, templateId, directRecipientId });
}),
/**
@@ -379,8 +360,8 @@ export const templateRouter = router({
deleteTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/direct/delete',
method: 'DELETE',
path: '/template/direct/{templateId}',
summary: 'Delete direct link',
description: 'Delete a direct link for a template',
tags: ['Template'],
@@ -389,11 +370,12 @@ export const templateRouter = router({
.input(ZDeleteTemplateDirectLinkMutationSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
const userId = ctx.user.id;
await deleteTemplateDirectLink({ userId, templateId });
await deleteTemplateDirectLink({ userId, teamId, templateId });
}),
/**
@@ -412,11 +394,12 @@ export const templateRouter = router({
.input(ZToggleTemplateDirectLinkMutationSchema)
.output(ZToggleTemplateDirectLinkResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, enabled } = input;
const userId = ctx.user.id;
return await toggleTemplateDirectLink({ userId, templateId, enabled });
return await toggleTemplateDirectLink({ userId, teamId, templateId, enabled });
}),
/**
@@ -426,7 +409,7 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/move',
path: '/template/move',
summary: 'Move template',
description: 'Move a template to a team',
tags: ['Template'],
@@ -444,37 +427,4 @@ export const templateRouter = router({
userId,
});
}),
/**
* @private
*/
updateTemplateTypedSignatureSettings: authenticatedProcedure
.input(ZUpdateTemplateTypedSignatureSettingsMutationSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, typedSignatureEnabled } = input;
const template = await getTemplateById({
id: templateId,
userId: ctx.user.id,
teamId,
}).catch(() => null);
if (!template) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Template not found',
});
}
return await updateTemplateSettings({
templateId,
teamId,
userId: ctx.user.id,
data: {},
meta: {
typedSignatureEnabled,
},
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
});

View File

@@ -1,25 +1,27 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import {
DocumentDistributionMethod,
DocumentSigningOrder,
DocumentVisibility,
TemplateType,
} from '@documenso/prisma/client';
import { DocumentSigningOrder, DocumentVisibility, TemplateType } from '@documenso/prisma/client';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
} from '../document-router/schema';
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
export const ZCreateTemplateMutationSchema = z.object({
title: z.string().min(1).trim(),
teamId: z.number().optional(),
templateDocumentDataId: z.string().min(1),
});
@@ -34,119 +36,119 @@ export const ZCreateDocumentFromDirectTemplateMutationSchema = z.object({
export const ZCreateDocumentFromTemplateMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
recipients: z
.array(
z.object({
id: z.number(),
id: z.number().describe('The ID of the recipient in the template.'),
email: z.string().email(),
name: z.string().optional(),
}),
)
.describe('The information of the recipients to create the document with.')
.refine((recipients) => {
const emails = recipients.map((signer) => signer.email);
return new Set(emails).size === emails.length;
}, 'Recipients must have unique emails'),
distributeDocument: z.boolean().optional(),
customDocumentDataId: z.string().optional(),
distributeDocument: z
.boolean()
.describe('Whether to create the document as pending and distribute it to recipients.')
.optional(),
customDocumentDataId: z
.string()
.describe(
'The data ID of an alternative PDF to use when creating the document. If not provided, the PDF attached to the template will be used.',
)
.optional(),
});
export const ZDuplicateTemplateMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
});
export const ZCreateTemplateDirectLinkMutationSchema = z.object({
templateId: z.number().min(1),
teamId: z.number().optional(),
directRecipientId: z.number().min(1).optional(),
templateId: z.number(),
directRecipientId: z
.number()
.describe(
'The of the recipient in the current template to transform into the primary recipient when the template is used.',
)
.optional(),
});
export const ZDeleteTemplateDirectLinkMutationSchema = z.object({
templateId: z.number().min(1),
templateId: z.number(),
});
export const ZToggleTemplateDirectLinkMutationSchema = z.object({
templateId: z.number().min(1),
templateId: z.number(),
enabled: z.boolean(),
});
export const ZDeleteTemplateMutationSchema = z.object({
templateId: z.number().min(1),
teamId: z.number().optional(),
templateId: z.number(),
});
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
export const MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH = 256;
export const ZUpdateTemplateSettingsMutationSchema = z.object({
export const ZUpdateTemplateRequestSchema = z.object({
templateId: z.number(),
teamId: z.number().min(1).optional(),
data: z.object({
title: z.string().min(1).optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
publicTitle: z.string().trim().min(1).max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH).optional(),
publicDescription: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.optional(),
type: z.nativeEnum(TemplateType).optional(),
language: z
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
.optional()
.default('en'),
}),
data: z
.object({
title: z.string().min(1).optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
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(),
type: z.nativeEnum(TemplateType).optional(),
})
.optional(),
meta: z
.object({
subject: z.string(),
message: z.string(),
timezone: z.string(),
dateFormat: z.string(),
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
emailSettings: ZDocumentEmailSettingsSchema,
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
typedSignatureEnabled: z.boolean().optional(),
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(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
})
.optional(),
});
export const ZSetSigningOrderForTemplateMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder),
});
export const ZFindTemplatesQuerySchema = ZFindSearchParamsSchema.extend({
teamId: z.number().optional(),
type: z.nativeEnum(TemplateType).optional(),
type: z.nativeEnum(TemplateType).describe('Filter templates by type.').optional(),
});
export const ZGetTemplateByIdQuerySchema = z.object({
templateId: z.number().min(1),
teamId: z.number().optional(),
});
export const ZMoveTemplatesToTeamSchema = z.object({
templateId: z.number(),
teamId: z.number(),
});
export const ZUpdateTemplateTypedSignatureSettingsMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
typedSignatureEnabled: z.boolean(),
templateId: z.number().describe('The ID of the template to move to.'),
teamId: z.number().describe('The ID of the team to move the template to.'),
});
export type TCreateTemplateMutationSchema = z.infer<typeof ZCreateTemplateMutationSchema>;