diff --git a/apps/remix/app/components/dialogs/template-create-dialog.tsx b/apps/remix/app/components/dialogs/template-create-dialog.tsx index 76d4e0c8a..160c332db 100644 --- a/apps/remix/app/components/dialogs/template-create-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-create-dialog.tsx @@ -7,9 +7,9 @@ import { FilePlus, Loader } from 'lucide-react'; import { useNavigate } from 'react-router'; import { useSession } from '@documenso/lib/client-only/providers/session'; -import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { formatTemplatesPath } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; +import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, @@ -54,13 +54,17 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) => setIsUploadingFile(true); try { - const response = await putPdfFile(file); - - const { legacyTemplateId: id } = await createTemplate({ + const payload = { title: file.name, - templateDocumentDataId: response.id, folderId: folderId, - }); + } satisfies TCreateTemplatePayloadSchema; + + const formData = new FormData(); + + formData.append('payload', JSON.stringify(payload)); + formData.append('file', file); + + const { envelopeId: id } = await createTemplate(formData); toast({ title: _(msg`Template document uploaded`), diff --git a/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx b/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx index 01cee5ad3..2dbe7e59c 100644 --- a/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +++ b/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx @@ -16,9 +16,9 @@ import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/l import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; -import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; +import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types'; import { cn } from '@documenso/ui/lib/utils'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -62,14 +62,18 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon try { setIsLoading(true); - const response = await putPdfFile(file); - - const { legacyDocumentId: id } = await createDocument({ + const payload = { title: file.name, - documentDataId: response.id, - timezone: userTimezone, // Note: When migrating to v2 document upload remember to pass this through as a 'userTimezone' field. + timezone: userTimezone, folderId: folderId ?? undefined, - }); + } satisfies TCreateDocumentPayloadSchema; + + const formData = new FormData(); + + formData.append('payload', JSON.stringify(payload)); + formData.append('file', file); + + const { envelopeId: id } = await createDocument(formData); void refreshLimits(); diff --git a/apps/remix/app/components/general/document/document-upload-button.tsx b/apps/remix/app/components/general/document/document-upload-button.tsx index 7e092363e..ea95bae23 100644 --- a/apps/remix/app/components/general/document/document-upload-button.tsx +++ b/apps/remix/app/components/general/document/document-upload-button.tsx @@ -13,9 +13,9 @@ import { useSession } from '@documenso/lib/client-only/providers/session'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; +import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types'; import { cn } from '@documenso/ui/lib/utils'; import { DocumentDropzone } from '@documenso/ui/primitives/document-upload'; import { @@ -73,14 +73,18 @@ export const DocumentUploadButton = ({ className }: DocumentUploadButtonProps) = try { setIsLoading(true); - const response = await putPdfFile(file); - - const { legacyDocumentId: id } = await createDocument({ + const payload = { title: file.name, - documentDataId: response.id, timezone: userTimezone, folderId: folderId ?? undefined, - }); + } satisfies TCreateDocumentPayloadSchema; + + const formData = new FormData(); + + formData.append('payload', JSON.stringify(payload)); + formData.append('file', file); + + const { envelopeId: id } = await createDocument(formData); void refreshLimits(); diff --git a/apps/remix/app/components/general/document/envelope-upload-button.tsx b/apps/remix/app/components/general/document/envelope-upload-button.tsx index 645c3845f..218235706 100644 --- a/apps/remix/app/components/general/document/envelope-upload-button.tsx +++ b/apps/remix/app/components/general/document/envelope-upload-button.tsx @@ -14,9 +14,9 @@ import { useSession } from '@documenso/lib/client-only/providers/session'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; +import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types'; import { cn } from '@documenso/ui/lib/utils'; import { DocumentDropzone } from '@documenso/ui/primitives/document-upload'; import { @@ -78,35 +78,24 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo try { setIsLoading(true); - const result = await Promise.all( - files.map(async (file) => { - try { - const response = await putPdfFile(file); - - return { - title: file.name, - documentDataId: response.id, - }; - } catch (err) { - console.error(err); - throw new Error('Failed to upload document'); - } - }), - ); - - const envelopeItemsToCreate = result.filter( - (item): item is { title: string; documentDataId: string } => item !== undefined, - ); - - const { id } = await createEnvelope({ + const payload = { folderId, type, title: files[0].name, - items: envelopeItemsToCreate, meta: { timezone: userTimezone, }, - }).catch((error) => { + } satisfies TCreateEnvelopePayload; + + const formData = new FormData(); + + formData.append('payload', JSON.stringify(payload)); + + for (const file of files) { + formData.append('files', file); + } + + const { id } = await createEnvelope(formData).catch((error) => { console.error(error); throw error; diff --git a/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx b/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx index c0942f693..4daea014b 100644 --- a/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx +++ b/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx @@ -10,9 +10,9 @@ import { match } from 'ts-pattern'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; -import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { formatTemplatesPath } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; +import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema'; import { cn } from '@documenso/ui/lib/utils'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -40,13 +40,17 @@ export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZon try { setIsLoading(true); - const documentData = await putPdfFile(file); - - const { legacyTemplateId: id } = await createTemplate({ + const payload = { title: file.name, - templateDocumentDataId: documentData.id, folderId: folderId ?? undefined, - }); + } satisfies TCreateTemplatePayloadSchema; + + const formData = new FormData(); + + formData.append('payload', JSON.stringify(payload)); + formData.append('file', file); + + const { envelopeId: id } = await createTemplate(formData); toast({ title: _(msg`Template uploaded`), diff --git a/packages/lib/server-only/envelope/create-envelope.ts b/packages/lib/server-only/envelope/create-envelope.ts index 5f4198377..6e24ee35f 100644 --- a/packages/lib/server-only/envelope/create-envelope.ts +++ b/packages/lib/server-only/envelope/create-envelope.ts @@ -16,11 +16,16 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques import { nanoid, prefixedId } from '@documenso/lib/universal/id'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { prisma } from '@documenso/prisma'; -import type { TCreateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/create-envelope.types'; -import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth'; +import type { + TDocumentAccessAuthTypes, + TDocumentActionAuthTypes, + TRecipientAccessAuthTypes, + TRecipientActionAuthTypes, +} from '../../types/document-auth'; import type { TDocumentFormValues } from '../../types/document-form-values'; import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment'; +import type { TFieldAndMeta } from '../../types/field-meta'; import { ZWebhookDocumentSchema, mapEnvelopeToWebhookDocumentPayload, @@ -34,6 +39,25 @@ import { incrementDocumentId, incrementTemplateId } from '../envelope/increment- import { getTeamSettings } from '../team/get-team-settings'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; +type CreateEnvelopeRecipientFieldOptions = TFieldAndMeta & { + documentDataId: string; + page: number; + positionX: number; + positionY: number; + width: number; + height: number; +}; + +type CreateEnvelopeRecipientOptions = { + email: string; + name: string; + role: RecipientRole; + signingOrder?: number; + accessAuth?: TRecipientAccessAuthTypes[]; + actionAuth?: TRecipientActionAuthTypes[]; + fields?: CreateEnvelopeRecipientFieldOptions[]; +}; + export type CreateEnvelopeOptions = { userId: number; teamId: number; @@ -56,7 +80,7 @@ export type CreateEnvelopeOptions = { visibility?: DocumentVisibility; globalAccessAuth?: TDocumentAccessAuthTypes[]; globalActionAuth?: TDocumentActionAuthTypes[]; - recipients?: TCreateEnvelopeRequest['recipients']; + recipients?: CreateEnvelopeRecipientOptions[]; folderId?: string; }; attachments?: Array<{ diff --git a/packages/lib/server-only/pdf/normalize-pdf.ts b/packages/lib/server-only/pdf/normalize-pdf.ts index ffbe5be06..071134411 100644 --- a/packages/lib/server-only/pdf/normalize-pdf.ts +++ b/packages/lib/server-only/pdf/normalize-pdf.ts @@ -1,13 +1,22 @@ import { PDFDocument } from '@cantoo/pdf-lib'; +import { AppError } from '../../errors/app-error'; import { flattenAnnotations } from './flatten-annotations'; import { flattenForm, removeOptionalContentGroups } from './flatten-form'; export const normalizePdf = async (pdf: Buffer) => { - const pdfDoc = await PDFDocument.load(pdf).catch(() => null); + const pdfDoc = await PDFDocument.load(pdf).catch((e) => { + console.error(`PDF normalization error: ${e.message}`); - if (!pdfDoc) { - return pdf; + throw new AppError('INVALID_DOCUMENT_FILE', { + message: 'The document is not a valid PDF', + }); + }); + + if (pdfDoc.isEncrypted) { + throw new AppError('INVALID_DOCUMENT_FILE', { + message: 'The document is encrypted', + }); } removeOptionalContentGroups(pdfDoc); diff --git a/packages/lib/universal/upload/put-file.server.ts b/packages/lib/universal/upload/put-file.server.ts index f414be62f..47c2be747 100644 --- a/packages/lib/universal/upload/put-file.server.ts +++ b/packages/lib/universal/upload/put-file.server.ts @@ -7,6 +7,7 @@ import { env } from '@documenso/lib/utils/env'; import { AppError } from '../../errors/app-error'; import { createDocumentData } from '../../server-only/document-data/create-document-data'; +import { normalizePdf } from '../../server-only/pdf/normalize-pdf'; import { uploadS3File } from './server-actions'; type File = { @@ -43,6 +44,28 @@ export const putPdfFileServerSide = async (file: File) => { return await createDocumentData({ type, data }); }; +/** + * Uploads a pdf file and normalizes it. + */ +export const putNormalizedPdfFileServerSide = async (file: File) => { + const buffer = Buffer.from(await file.arrayBuffer()); + + const normalized = await normalizePdf(buffer); + + const fileName = file.name.endsWith('.pdf') ? file.name : `${file.name}.pdf`; + + const documentData = await putFileServerSide({ + name: fileName, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(normalized), + }); + + return await createDocumentData({ + type: documentData.type, + data: documentData.data, + }); +}; + /** * Uploads a file to the appropriate storage location. */ diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 40047c01e..6cf6ecfe8 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -134,8 +134,8 @@ model Passkey { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) lastUsedAt DateTime? - credentialId Bytes - credentialPublicKey Bytes + credentialId Bytes /// @zod.custom.use(z.instanceof(Uint8Array)) + credentialPublicKey Bytes /// @zod.custom.use(z.instanceof(Uint8Array)) counter BigInt credentialDeviceType String credentialBackedUp Boolean diff --git a/packages/trpc/server/document-router/create-document.ts b/packages/trpc/server/document-router/create-document.ts index 9a845090d..10d771c11 100644 --- a/packages/trpc/server/document-router/create-document.ts +++ b/packages/trpc/server/document-router/create-document.ts @@ -3,6 +3,7 @@ import { EnvelopeType } from '@prisma/client'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; +import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { authenticatedProcedure } from '../trpc'; @@ -16,7 +17,12 @@ export const createDocumentRoute = authenticatedProcedure .output(ZCreateDocumentResponseSchema) .mutation(async ({ input, ctx }) => { const { user, teamId } = ctx; - const { title, documentDataId, timezone, folderId, attachments } = input; + + const { payload, file } = input; + + const { title, timezone, folderId, attachments } = payload; + + const { id: documentDataId } = await putNormalizedPdfFileServerSide(file); ctx.logger.info({ input: { @@ -55,6 +61,7 @@ export const createDocumentRoute = authenticatedProcedure }); return { + envelopeId: document.id, legacyDocumentId: mapSecondaryIdToDocumentId(document.secondaryId), }; }); diff --git a/packages/trpc/server/document-router/create-document.types.ts b/packages/trpc/server/document-router/create-document.types.ts index 43fa32291..51587498c 100644 --- a/packages/trpc/server/document-router/create-document.types.ts +++ b/packages/trpc/server/document-router/create-document.types.ts @@ -1,23 +1,27 @@ import { z } from 'zod'; +import { zfd } from 'zod-form-data'; import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta'; import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; +import { zodFormData } from '../../utils/zod-form-data'; +import type { TrpcRouteMeta } from '../trpc'; import { ZDocumentTitleSchema } from './schema'; // Currently not in use until we allow passthrough documents on create. -// export const createDocumentMeta: TrpcRouteMeta = { -// openapi: { -// method: 'POST', -// path: '/document/create', -// summary: 'Create document', -// tags: ['Document'], -// }, -// }; +export const createDocumentMeta: TrpcRouteMeta = { + openapi: { + method: 'POST', + path: '/document/create', + contentTypes: ['multipart/form-data'], + summary: 'Create document', + description: 'Create a document using form data.', + tags: ['Document'], + }, +}; -export const ZCreateDocumentRequestSchema = z.object({ +export const ZCreateDocumentPayloadSchema = z.object({ title: ZDocumentTitleSchema, - documentDataId: z.string().min(1), timezone: ZDocumentMetaTimezoneSchema.optional(), folderId: z.string().describe('The ID of the folder to create the document in').optional(), attachments: z @@ -31,9 +35,16 @@ export const ZCreateDocumentRequestSchema = z.object({ .optional(), }); +export const ZCreateDocumentRequestSchema = zodFormData({ + payload: zfd.json(ZCreateDocumentPayloadSchema), + file: zfd.file(), +}); + export const ZCreateDocumentResponseSchema = z.object({ + envelopeId: z.string(), legacyDocumentId: z.number(), }); +export type TCreateDocumentPayloadSchema = z.infer; export type TCreateDocumentRequest = z.infer; export type TCreateDocumentResponse = z.infer; diff --git a/packages/trpc/server/envelope-router/create-envelope.ts b/packages/trpc/server/envelope-router/create-envelope.ts index 7f529362a..0ed92187b 100644 --- a/packages/trpc/server/envelope-router/create-envelope.ts +++ b/packages/trpc/server/envelope-router/create-envelope.ts @@ -1,6 +1,7 @@ import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; +import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; import { authenticatedProcedure } from '../trpc'; import { @@ -13,6 +14,9 @@ export const createEnvelopeRoute = authenticatedProcedure .output(ZCreateEnvelopeResponseSchema) .mutation(async ({ input, ctx }) => { const { user, teamId } = ctx; + + const { payload, files } = input; + const { title, type, @@ -22,10 +26,9 @@ export const createEnvelopeRoute = authenticatedProcedure globalActionAuth, recipients, folderId, - items, meta, attachments, - } = input; + } = payload; ctx.logger.info({ input: { @@ -45,13 +48,62 @@ export const createEnvelopeRoute = authenticatedProcedure }); } - if (items.length > maximumEnvelopeItemCount) { + if (files.length > maximumEnvelopeItemCount) { throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', { message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`, statusCode: 400, }); } + // For each file, stream to s3 and create the document data. + const envelopeItems = await Promise.all( + files.map(async (file) => { + const { id: documentDataId } = await putNormalizedPdfFileServerSide(file); + + return { + title: file.name, + documentDataId, + }; + }), + ); + + const recipientsToCreate = recipients?.map((recipient) => ({ + email: recipient.email, + name: recipient.name, + role: recipient.role, + signingOrder: recipient.signingOrder, + accessAuth: recipient.accessAuth, + actionAuth: recipient.actionAuth, + fields: recipient.fields?.map((field) => { + let documentDataId: string | undefined = undefined; + + if (typeof field.identifier === 'string') { + documentDataId = envelopeItems.find( + (item) => item.title === field.identifier, + )?.documentDataId; + } + + if (typeof field.identifier === 'number') { + documentDataId = envelopeItems.at(field.identifier)?.documentDataId; + } + + if (field.identifier === undefined) { + documentDataId = envelopeItems[0]?.documentDataId; + } + + if (!documentDataId) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Document data not found', + }); + } + + return { + ...field, + documentDataId, + }; + }), + })); + const envelope = await createEnvelope({ userId: user.id, teamId, @@ -63,9 +115,9 @@ export const createEnvelopeRoute = authenticatedProcedure visibility, globalAccessAuth, globalActionAuth, - recipients, + recipients: recipientsToCreate, folderId, - envelopeItems: items, + envelopeItems, }, attachments, meta, diff --git a/packages/trpc/server/envelope-router/create-envelope.types.ts b/packages/trpc/server/envelope-router/create-envelope.types.ts index ea0fee260..18207391f 100644 --- a/packages/trpc/server/envelope-router/create-envelope.types.ts +++ b/packages/trpc/server/envelope-router/create-envelope.types.ts @@ -1,5 +1,6 @@ import { EnvelopeType } from '@prisma/client'; import { z } from 'zod'; +import { zfd } from 'zod-form-data'; import { ZDocumentAccessAuthTypesSchema, @@ -17,24 +18,28 @@ import { } from '@documenso/lib/types/field'; import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta'; +import { zodFormData } from '../../utils/zod-form-data'; import { ZDocumentExternalIdSchema, ZDocumentTitleSchema, ZDocumentVisibilitySchema, } from '../document-router/schema'; import { ZCreateRecipientSchema } from '../recipient-router/schema'; +import type { TrpcRouteMeta } from '../trpc'; // Currently not in use until we allow passthrough documents on create. -// export const createEnvelopeMeta: TrpcRouteMeta = { -// openapi: { -// method: 'POST', -// path: '/envelope/create', -// summary: 'Create envelope', -// tags: ['Envelope'], -// }, -// }; +export const createEnvelopeMeta: TrpcRouteMeta = { + openapi: { + method: 'POST', + path: '/envelope/create', + contentTypes: ['multipart/form-data'], + summary: 'Create envelope', + description: 'Create a envelope using form data.', + tags: ['Envelope'], + }, +}; -export const ZCreateEnvelopeRequestSchema = z.object({ +export const ZCreateEnvelopePayloadSchema = z.object({ title: ZDocumentTitleSchema, type: z.nativeEnum(EnvelopeType), externalId: ZDocumentExternalIdSchema.optional(), @@ -42,12 +47,6 @@ export const ZCreateEnvelopeRequestSchema = z.object({ globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(), globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(), formValues: ZDocumentFormValuesSchema.optional(), - items: z - .object({ - title: ZDocumentTitleSchema.optional(), - documentDataId: z.string(), - }) - .array(), folderId: z .string() .describe( @@ -59,11 +58,12 @@ export const ZCreateEnvelopeRequestSchema = z.object({ ZCreateRecipientSchema.extend({ fields: ZFieldAndMetaSchema.and( z.object({ - documentDataId: z - .string() + identifier: z + .union([z.string(), z.number()]) .describe( - 'The ID of the document data to create the field on. If empty, the first document data will be used.', - ), + 'Either the filename or the index of the file that was uploaded to attach the field to.', + ) + .optional(), page: ZFieldPageNumberSchema, positionX: ZFieldPageXSchema, positionY: ZFieldPageYSchema, @@ -88,9 +88,15 @@ export const ZCreateEnvelopeRequestSchema = z.object({ .optional(), }); +export const ZCreateEnvelopeRequestSchema = zodFormData({ + payload: zfd.json(ZCreateEnvelopePayloadSchema), + files: zfd.repeatableOfType(zfd.file()), +}); + export const ZCreateEnvelopeResponseSchema = z.object({ id: z.string(), }); +export type TCreateEnvelopePayload = z.infer; export type TCreateEnvelopeRequest = z.infer; export type TCreateEnvelopeResponse = z.infer; diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts index 3cefb136f..9b5b26e1b 100644 --- a/packages/trpc/server/template-router/router.ts +++ b/packages/trpc/server/template-router/router.ts @@ -21,6 +21,7 @@ import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/de import { findTemplates } from '@documenso/lib/server-only/template/find-templates'; 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 { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope'; import { mapFieldToLegacyField } from '@documenso/lib/utils/fields'; @@ -159,20 +160,27 @@ export const templateRouter = router({ * @private */ createTemplate: authenticatedProcedure - // .meta({ // Note before releasing this to public, update the response schema to be correct. - // openapi: { - // method: 'POST', - // path: '/template/create', - // summary: 'Create template', - // description: 'Create a new template', - // tags: ['Template'], - // }, - // }) + .meta({ + // Note before releasing this to public, update the response schema to be correct. + openapi: { + method: 'POST', + path: '/template/create', + contentTypes: ['multipart/form-data'], + summary: 'Create template', + description: 'Create a new template', + tags: ['Template'], + }, + }) .input(ZCreateTemplateMutationSchema) .output(ZCreateTemplateResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; - const { title, templateDocumentDataId, folderId } = input; + + const { payload, file } = input; + + const { title, folderId } = payload; + + const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(file); ctx.logger.info({ input: { @@ -198,6 +206,7 @@ export const templateRouter = router({ }); return { + envelopeId: envelope.id, legacyTemplateId: mapSecondaryIdToTemplateId(envelope.secondaryId), }; }), diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index 0783ef232..8149629f9 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -1,5 +1,6 @@ import { DocumentSigningOrder, DocumentVisibility, TemplateType } from '@prisma/client'; import { z } from 'zod'; +import { zfd } from 'zod-form-data'; import { ZDocumentSchema } from '@documenso/lib/types/document'; import { @@ -29,6 +30,7 @@ import { } from '@documenso/lib/types/template'; import { LegacyTemplateDirectLinkSchema } from '@documenso/prisma/types/template-legacy-schema'; +import { zodFormData } from '../../utils/zod-form-data'; import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema'; export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50; @@ -77,12 +79,16 @@ export const ZTemplateMetaUpsertSchema = z.object({ allowDictateNextSigner: z.boolean().optional(), }); -export const ZCreateTemplateMutationSchema = z.object({ +export const ZCreateTemplatePayloadSchema = z.object({ title: z.string().min(1).trim(), - templateDocumentDataId: z.string().min(1), folderId: z.string().optional(), }); +export const ZCreateTemplateMutationSchema = zodFormData({ + payload: zfd.json(ZCreateTemplatePayloadSchema), + file: zfd.file(), +}); + export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({ directRecipientName: z.string().max(255).optional(), directRecipientEmail: z.string().email().max(254), @@ -218,6 +224,7 @@ export const ZCreateTemplateV2ResponseSchema = z.object({ }); export const ZCreateTemplateResponseSchema = z.object({ + envelopeId: z.string(), legacyTemplateId: z.number(), }); @@ -267,6 +274,7 @@ export const ZBulkSendTemplateMutationSchema = z.object({ sendImmediately: z.boolean(), }); +export type TCreateTemplatePayloadSchema = z.infer; export type TCreateTemplateMutationSchema = z.infer; export type TDuplicateTemplateMutationSchema = z.infer; export type TDeleteTemplateMutationSchema = z.infer; diff --git a/packages/trpc/utils/data-transformer.ts b/packages/trpc/utils/data-transformer.ts index e163003c9..2c632c1cc 100644 --- a/packages/trpc/utils/data-transformer.ts +++ b/packages/trpc/utils/data-transformer.ts @@ -2,14 +2,16 @@ import type { DataTransformer } from '@trpc/server'; import SuperJSON from 'superjson'; export const dataTransformer: DataTransformer = { - serialize: (data: unknown) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + serialize: (data: any) => { if (data instanceof FormData) { return data; } return SuperJSON.serialize(data); }, - deserialize: (data: unknown) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + deserialize: (data: any) => { return SuperJSON.deserialize(data); }, };