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

@@ -39,12 +39,10 @@ import {
sendDocument,
} from '@documenso/lib/server-only/document/send-document';
import {
ZUpdateDocumentSettingsResponseSchema,
updateDocumentSettings,
} from '@documenso/lib/server-only/document/update-document-settings';
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
ZUpdateDocumentResponseSchema,
updateDocument,
} from '@documenso/lib/server-only/document/update-document';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { DocumentStatus } from '@documenso/prisma/client';
import { authenticatedProcedure, procedure, router } from '../trpc';
@@ -64,9 +62,8 @@ import {
ZSearchDocumentsMutationSchema,
ZSendDocumentMutationSchema,
ZSetPasswordForDocumentMutationSchema,
ZSetSettingsForDocumentMutationSchema,
ZSetSigningOrderForDocumentMutationSchema,
ZSetTitleForDocumentMutationSchema,
ZUpdateDocumentRequestSchema,
ZUpdateTypedSignatureSettingsMutationSchema,
} from './schema';
@@ -77,9 +74,13 @@ export const documentRouter = router({
getDocumentById: authenticatedProcedure
.input(ZGetDocumentByIdQuerySchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId } = input;
return await getDocumentById({
...input,
userId: ctx.user.id,
teamId,
documentId,
});
}),
@@ -104,28 +105,19 @@ export const documentRouter = router({
.meta({
openapi: {
method: 'GET',
path: '/document/find',
path: '/document',
summary: 'Find documents',
description: 'Find documents based on a search criteria',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZFindDocumentsQuerySchema)
.output(ZFindDocumentsResponseSchema)
.query(async ({ input, ctx }) => {
const { user } = ctx;
const { user, teamId } = ctx;
const {
query,
teamId,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
} = input;
const { query, templateId, page, perPage, orderByDirection, orderByColumn, source, status } =
input;
const documents = await findDocuments({
userId: user.id,
@@ -154,34 +146,41 @@ export const documentRouter = router({
path: '/document/{documentId}',
summary: 'Get document',
description: 'Returns a document given an ID',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZGetDocumentWithDetailsByIdQuerySchema)
.output(ZGetDocumentWithDetailsByIdResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId } = input;
return await getDocumentWithDetailsById({
...input,
userId: ctx.user.id,
userId: user.id,
teamId,
documentId,
});
}),
/**
* @public
* Wait until RR7 so we can passthrough documents.
*
* @private
*/
createDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/create',
summary: 'Create document',
tags: ['Documents'],
},
})
// .meta({
// openapi: {
// method: 'POST',
// path: '/document/create',
// summary: 'Create document',
// tags: ['Document'],
// },
// })
.input(ZCreateDocumentMutationSchema)
.output(ZCreateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { title, documentDataId, teamId, timezone } = input;
const { teamId } = ctx;
const { title, documentDataId, timezone } = input;
const { remaining } = await getServerLimits({ email: ctx.user.email, teamId });
@@ -199,7 +198,7 @@ export const documentRouter = router({
documentDataId,
normalizePdf: true,
timezone,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@@ -212,38 +211,43 @@ export const documentRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}',
path: '/document/update',
summary: 'Update document',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZSetSettingsForDocumentMutationSchema)
.output(ZUpdateDocumentSettingsResponseSchema)
.input(ZUpdateDocumentRequestSchema)
.output(ZUpdateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, data, meta } = input;
const { teamId } = ctx;
const { documentId, data, meta = {} } = input;
const userId = ctx.user.id;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
if (meta.timezone || meta.dateFormat || meta.redirectUrl) {
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
documentId,
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
language: meta.language,
userId: ctx.user.id,
requestMetadata,
teamId,
documentId,
subject: meta.subject,
message: meta.message,
timezone: meta.timezone,
dateFormat: meta.dateFormat,
language: meta.language,
typedSignatureEnabled: meta.typedSignatureEnabled,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
emailSettings: meta.emailSettings,
requestMetadata: ctx.metadata,
});
}
return await updateDocumentSettings({
return await updateDocument({
userId,
teamId,
documentId,
data,
requestMetadata,
requestMetadata: ctx.metadata,
});
}),
@@ -253,16 +257,17 @@ export const documentRouter = router({
deleteDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/delete',
method: 'DELETE',
path: '/document/{documentId}',
summary: 'Delete document',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZDeleteDocumentMutationSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { documentId, teamId } = input;
const { teamId } = ctx;
const { documentId } = input;
const userId = ctx.user.id;
@@ -270,7 +275,7 @@ export const documentRouter = router({
id: documentId,
userId,
teamId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@@ -281,10 +286,10 @@ export const documentRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/move',
path: '/document/move',
summary: 'Move document',
description: 'Move a document to a team',
tags: ['Documents'],
description: 'Move a document from your personal account to a team',
tags: ['Document'],
},
})
.input(ZMoveDocumentToTeamSchema)
@@ -297,27 +302,7 @@ export const documentRouter = router({
documentId,
teamId,
userId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
/**
* @private
*/
// Should probably use `updateDocument`
setTitleForDocument: authenticatedProcedure
.input(ZSetTitleForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, title } = input;
const userId = ctx.user.id;
return await updateTitle({
title,
userId,
teamId,
documentId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@@ -327,6 +312,7 @@ export const documentRouter = router({
setPasswordForDocument: authenticatedProcedure
.input(ZSetPasswordForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, password } = input;
const key = DOCUMENSO_ENCRYPTION_KEY;
@@ -341,10 +327,11 @@ export const documentRouter = router({
});
await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
password: securePassword,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@@ -354,23 +341,28 @@ export const documentRouter = router({
setSigningOrderForDocument: authenticatedProcedure
.input(ZSetSigningOrderForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, signingOrder } = input;
return await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
signingOrder,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
/**
* @deprecated Remove after deployment.
*
* @private
*/
updateTypedSignatureSettings: authenticatedProcedure
.input(ZUpdateTypedSignatureSettingsMutationSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, typedSignatureEnabled } = input;
const { teamId } = ctx;
const { documentId, typedSignatureEnabled } = input;
const document = await getDocumentById({
documentId,
@@ -386,10 +378,11 @@ export const documentRouter = router({
}
return await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
typedSignatureEnabled,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@@ -403,27 +396,22 @@ export const documentRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/distribute',
path: '/document/distribute',
summary: 'Distribute document',
description: 'Send the document out to recipients based on your distribution method',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZSendDocumentMutationSchema)
.output(ZSendDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, meta } = input;
const { teamId } = ctx;
const { documentId, meta = {} } = input;
if (
meta.message ||
meta.subject ||
meta.timezone ||
meta.dateFormat ||
meta.redirectUrl ||
meta.distributionMethod ||
meta.emailSettings
) {
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
subject: meta.subject,
message: meta.message,
@@ -431,9 +419,9 @@ export const documentRouter = router({
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
userId: ctx.user.id,
emailSettings: meta.emailSettings,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
language: meta.language,
requestMetadata: ctx.metadata,
});
}
@@ -441,31 +429,38 @@ export const documentRouter = router({
userId: ctx.user.id,
documentId,
teamId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*
* Todo: Refactor to redistributeDocument.
*/
resendDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/resend',
summary: 'Resend document',
path: '/document/redistribute',
summary: 'Redistribute document',
description:
'Resend the document to recipients who have not signed. Will use the distribution method set in the document.',
tags: ['Documents'],
'Redistribute the document to the provided recipients who have not actioned the document. Will use the distribution method set in the document',
tags: ['Document'],
},
})
.input(ZResendDocumentMutationSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipients } = input;
return await resendDocument({
userId: ctx.user.id,
...input,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
teamId,
documentId,
recipients,
requestMetadata: ctx.metadata,
});
}),
@@ -476,17 +471,21 @@ export const documentRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/duplicate',
path: '/document/duplicate',
summary: 'Duplicate document',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZDuplicateDocumentMutationSchema)
.output(ZDuplicateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId } = input;
return await duplicateDocument({
userId: ctx.user.id,
...input,
userId: user.id,
teamId,
documentId,
});
}),
@@ -512,6 +511,8 @@ export const documentRouter = router({
findDocumentAuditLogs: authenticatedProcedure
.input(ZFindDocumentAuditLogsQuerySchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const {
page,
perPage,
@@ -523,13 +524,14 @@ export const documentRouter = router({
} = input;
return await findDocumentAuditLogs({
userId: ctx.user.id,
teamId,
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
userId: ctx.user.id,
});
}),
@@ -539,7 +541,8 @@ export const documentRouter = router({
downloadAuditLogs: authenticatedProcedure
.input(ZDownloadAuditLogsMutationSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId } = input;
const { teamId } = ctx;
const { documentId } = input;
const document = await getDocumentById({
documentId,
@@ -570,7 +573,8 @@ export const documentRouter = router({
downloadCertificate: authenticatedProcedure
.input(ZDownloadCertificateMutationSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId } = input;
const { teamId } = ctx;
const { documentId } = input;
const document = await getDocumentById({
documentId,

View File

@@ -15,16 +15,59 @@ import {
DocumentStatus,
DocumentVisibility,
FieldType,
RecipientRole,
} from '@documenso/prisma/client';
// Todo: Refactor all to ZDocumentMeta---
export const ZDocumentMetaTimezoneSchema = z
.string()
.describe('The timezone to use for date fields and signing the document.');
export const ZDocumentMetaDateFormatSchema = z
.string()
.describe('The date format to use for date fields and signing the document.');
export const ZDocumentMetaRedirectUrlSchema = z
.string()
.describe('The URL to which the recipient should be redirected after signing the document.')
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message: 'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
});
export const ZDocumentMetaLanguageSchema = z
.enum(SUPPORTED_LANGUAGE_CODES)
.describe('The language to use for email communications with recipients.');
export const ZDocumentMetaSubjectSchema = z
.string()
.describe('The subject of the email that will be sent to the recipients.');
export const ZDocumentMetaMessageSchema = z
.string()
.describe('The message of the email that will be sent to the recipients.');
export const ZDocumentMetaDistributionMethodSchema = z
.nativeEnum(DocumentDistributionMethod)
.describe('The distribution method to use when sending the document to the recipients.');
export const ZDocumentMetaTypedSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow typed signatures.');
export const ZFindDocumentsQuerySchema = ZFindSearchParamsSchema.extend({
teamId: z.number().min(1).optional(),
templateId: z.number().min(1).optional(),
source: z.nativeEnum(DocumentSource).optional(),
status: z.nativeEnum(DocumentStatus).optional(),
templateId: z
.number()
.describe('Filter documents by the template ID used to create it.')
.optional(),
source: z
.nativeEnum(DocumentSource)
.describe('Filter documents by how it was created.')
.optional(),
status: z
.nativeEnum(DocumentStatus)
.describe('Filter documents by the current status')
.optional(),
orderByColumn: z.enum(['createdAt']).optional(),
orderByDirection: z.enum(['asc', 'desc']).default('desc'),
orderByDirection: z.enum(['asc', 'desc']).describe('').default('desc'),
});
export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend({
@@ -36,13 +79,11 @@ export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend(
});
export const ZGetDocumentByIdQuerySchema = z.object({
documentId: z.number().min(1),
teamId: z.number().min(1).optional(),
documentId: z.number(),
});
export const ZDuplicateDocumentMutationSchema = z.object({
documentId: z.number().min(1),
teamId: z.number().min(1).optional(),
documentId: z.number(),
});
export type TGetDocumentByIdQuerySchema = z.infer<typeof ZGetDocumentByIdQuerySchema>;
@@ -54,8 +95,7 @@ export const ZGetDocumentByTokenQuerySchema = z.object({
export type TGetDocumentByTokenQuerySchema = z.infer<typeof ZGetDocumentByTokenQuerySchema>;
export const ZGetDocumentWithDetailsByIdQuerySchema = z.object({
documentId: z.number().min(1),
teamId: z.number().min(1).optional(),
documentId: z.number(),
});
export type TGetDocumentWithDetailsByIdQuerySchema = z.infer<
@@ -65,64 +105,41 @@ export type TGetDocumentWithDetailsByIdQuerySchema = z.infer<
export const ZCreateDocumentMutationSchema = z.object({
title: z.string().min(1),
documentDataId: z.string().min(1),
teamId: z.number().optional(),
timezone: z.string().optional(),
});
export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutationSchema>;
export const ZSetSettingsForDocumentMutationSchema = z.object({
export const ZUpdateDocumentRequestSchema = z.object({
documentId: 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(),
}),
meta: z.object({
timezone: z.string(),
dateFormat: z.string(),
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(),
}),
data: z
.object({
title: z.string().describe('The title of the document.').min(1).optional(),
externalId: z.string().nullish().describe('The external ID of the document.'),
visibility: z
.nativeEnum(DocumentVisibility)
.describe('The visibility of the document.')
.optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
})
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export type TSetGeneralSettingsForDocumentMutationSchema = z.infer<
typeof ZSetSettingsForDocumentMutationSchema
>;
export const ZSetTitleForDocumentMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().min(1).optional(),
title: z.string().min(1),
});
export type TSetTitleForDocumentMutationSchema = z.infer<typeof ZSetTitleForDocumentMutationSchema>;
export const ZSetRecipientsForDocumentMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().min(1).optional(),
recipients: z.array(
z.object({
id: z.number().nullish(),
email: z.string().min(1).email(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
}),
),
});
export type TSetRecipientsForDocumentMutationSchema = z.infer<
typeof ZSetRecipientsForDocumentMutationSchema
>;
export type TUpdateDocumentRequestSchema = z.infer<typeof ZUpdateDocumentRequestSchema>;
export const ZSetFieldsForDocumentMutationSchema = z.object({
documentId: z.number(),
@@ -145,23 +162,19 @@ export type TSetFieldsForDocumentMutationSchema = z.infer<
>;
export const ZSendDocumentMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
meta: z.object({
subject: z.string(),
message: z.string(),
timezone: z.string().optional(),
dateFormat: z.string().optional(),
distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional(),
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.',
}),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
}),
documentId: z.number().describe('The ID of the document to send.'),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export const ZSetPasswordForDocumentMutationSchema = z.object({
@@ -184,7 +197,6 @@ export type TSetSigningOrderForDocumentMutationSchema = z.infer<
export const ZUpdateTypedSignatureSettingsMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
typedSignatureEnabled: z.boolean(),
});
@@ -194,15 +206,16 @@ export type TUpdateTypedSignatureSettingsMutationSchema = z.infer<
export const ZResendDocumentMutationSchema = z.object({
documentId: z.number(),
recipients: z.array(z.number()).min(1),
teamId: z.number().min(1).optional(),
recipients: z
.array(z.number())
.min(1)
.describe('The IDs of the recipients to redistribute the document to.'),
});
export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSchema>;
export const ZDeleteDocumentMutationSchema = z.object({
documentId: z.number().min(1),
teamId: z.number().min(1).optional(),
documentId: z.number(),
});
export type TDeleteDocumentMutationSchema = z.infer<typeof ZDeleteDocumentMutationSchema>;
@@ -213,15 +226,13 @@ export const ZSearchDocumentsMutationSchema = z.object({
export const ZDownloadAuditLogsMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
});
export const ZDownloadCertificateMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
});
export const ZMoveDocumentToTeamSchema = z.object({
documentId: z.number(),
teamId: z.number(),
documentId: z.number().describe('The ID of the document to move to a team.'),
teamId: z.number().describe('The ID of the team to move the document to.'),
});