feat: create from template

This commit is contained in:
Mythie
2024-02-20 19:46:18 +11:00
parent 4c5b910a59
commit b9e5905469
8 changed files with 242 additions and 70 deletions

View File

@ -3,6 +3,8 @@ import { initContract } from '@ts-rest/core';
import {
ZSendDocumentForSigningMutationSchema as SendDocumentMutationSchema,
ZAuthorizationHeadersSchema,
ZCreateDocumentFromTemplateMutationResponseSchema,
ZCreateDocumentFromTemplateMutationSchema,
ZCreateDocumentMutationResponseSchema,
ZCreateDocumentMutationSchema,
ZCreateFieldMutationSchema,
@ -13,6 +15,7 @@ import {
ZGetDocumentsQuerySchema,
ZSuccessfulDocumentResponseSchema,
ZSuccessfulFieldResponseSchema,
ZSuccessfulGetDocumentResponseSchema,
ZSuccessfulRecipientResponseSchema,
ZSuccessfulResponseSchema,
ZSuccessfulSigningResponseSchema,
@ -41,7 +44,7 @@ export const ApiContractV1 = c.router(
method: 'GET',
path: '/api/v1/documents/:id',
responses: {
200: ZSuccessfulDocumentResponseSchema,
200: ZSuccessfulGetDocumentResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
},
@ -60,6 +63,18 @@ export const ApiContractV1 = c.router(
summary: 'Upload a new document and get a presigned URL',
},
createDocumentFromTemplate: {
method: 'POST',
path: '/api/v1/templates/:templateId/create-document',
body: ZCreateDocumentFromTemplateMutationSchema,
responses: {
200: ZCreateDocumentFromTemplateMutationResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
},
summary: 'Upload a new document and get a presigned URL',
},
sendDocument: {
method: 'POST',
path: '/api/v1/documents/:id/send',

View File

@ -1,11 +1,13 @@
import { createNextRoute } from '@ts-rest/next';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { createDocument } from '@documenso/lib/server-only/document/create-document';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { createField } from '@documenso/lib/server-only/field/create-field';
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
@ -15,6 +17,7 @@ import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recip
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
@ -42,10 +45,17 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
try {
const document = await getDocumentById({ id: Number(documentId), userId: user.id });
const recipients = await getRecipientsForDocument({
documentId: Number(documentId),
userId: user.id,
});
return {
status: 200,
body: document,
body: {
...document,
recipients,
},
};
} catch (err) {
return {
@ -124,6 +134,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
documentId: document.id,
recipients: recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
})),
@ -139,6 +151,53 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
createDocumentFromTemplate: authenticatedMiddleware(async (args, user) => {
const { body, params } = args;
const templateId = Number(params.templateId);
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
const document = await createDocumentFromTemplate({
templateId,
userId: user.id,
recipients: body.recipients,
});
await updateDocument({
documentId: document.id,
userId: user.id,
data: {
title: body.title,
},
});
if (body.meta) {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
subject: body.meta.subject,
message: body.meta.message,
dateFormat: body.meta.dateFormat,
timezone: body.meta.timezone,
});
}
return {
status: 200,
body: {
documentId: document.id,
recipients: document.Recipient.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
})),
},
};
}),
sendDocument: authenticatedMiddleware(async (args, user) => {
const { id } = args.params;
@ -461,6 +520,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
});
const remappedField = {
id: field.id,
documentId: field.documentId,
recipientId: field.recipientId ?? -1,
type: field.type,
@ -545,6 +605,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
});
const remappedField = {
id: updatedField.id,
documentId: updatedField.documentId,
recipientId: updatedField.recipientId ?? -1,
type: updatedField.type,
@ -635,6 +696,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
const remappedField = {
id: deletedField.id,
documentId: deletedField.documentId,
recipientId: deletedField.recipientId ?? -1,
type: deletedField.type,

View File

@ -16,7 +16,10 @@ export const authenticatedMiddleware = <
) => {
return async (args: T) => {
try {
const { authorization: token } = args.req.headers;
const { authorization } = args.req.headers;
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
const [token] = (authorization || '').split('Bearer ').filter((s) => s.length > 0);
if (!token) {
throw new Error('Token was not provided for authenticated middleware');
@ -26,6 +29,7 @@ export const authenticatedMiddleware = <
return await handler(args, user);
} catch (_err) {
console.log({ _err });
return {
status: 401,
body: {

View File

@ -12,8 +12,8 @@ import {
* Documents
*/
export const ZGetDocumentsQuerySchema = z.object({
page: z.number().min(1).optional().default(1),
perPage: z.number().min(1).optional().default(1),
page: z.coerce.number().min(1).optional().default(1),
perPage: z.coerce.number().min(1).optional().default(1),
});
export type TGetDocumentsQuerySchema = z.infer<typeof ZGetDocumentsQuerySchema>;
@ -33,6 +33,14 @@ export const ZSuccessfulDocumentResponseSchema = z.object({
completedAt: z.date().nullable(),
});
export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseSchema.extend({
recipients: z.lazy(() => z.array(ZSuccessfulRecipientResponseSchema)),
});
export type TSuccessfulGetDocumentResponseSchema = z.infer<
typeof ZSuccessfulGetDocumentResponseSchema
>;
export type TSuccessfulDocumentResponseSchema = z.infer<typeof ZSuccessfulDocumentResponseSchema>;
export const ZSendDocumentForSigningMutationSchema = null;
@ -84,6 +92,48 @@ export type TCreateDocumentMutationResponseSchema = z.infer<
typeof ZCreateDocumentMutationResponseSchema
>;
export const ZCreateDocumentFromTemplateMutationSchema = z.object({
title: z.string().min(1),
recipients: z.array(
z.object({
name: z.string().min(1),
email: z.string().email().min(1),
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
}),
),
meta: z
.object({
subject: z.string(),
message: z.string(),
timezone: z.string(),
dateFormat: z.string(),
redirectUrl: z.string(),
})
.partial()
.optional(),
});
export type TCreateDocumentFromTemplateMutationSchema = z.infer<
typeof ZCreateDocumentFromTemplateMutationSchema
>;
export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({
documentId: z.number(),
recipients: z.array(
z.object({
recipientId: z.number(),
name: z.string(),
email: z.string().email().min(1),
token: z.string(),
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
}),
),
});
export type TCreateDocumentFromTemplateMutationResponseSchema = z.infer<
typeof ZCreateDocumentFromTemplateMutationResponseSchema
>;
export const ZCreateRecipientMutationSchema = z.object({
name: z.string().min(1),
email: z.string().email().min(1),
@ -105,7 +155,9 @@ export type TDeleteRecipientMutationSchema = typeof ZDeleteRecipientMutationSche
export const ZSuccessfulRecipientResponseSchema = z.object({
id: z.number(),
documentId: z.number(),
// !: This handles the fact that we have null documentId's for templates
// !: while we won't need the default we must add it to satisfy typescript
documentId: z.number().nullish().default(-1),
email: z.string().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),