mirror of
https://github.com/documenso/documenso.git
synced 2025-11-09 20:12:31 +10:00
feat: add formdata endpoints for documents,envelopes,templates
Adds the missing endpoints for documents, envelopes and templates supporting file uploads in a singular request. Also updates frontend components that would use the prior hidden endpoints.
This commit is contained in:
@ -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`),
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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`),
|
||||
|
||||
@ -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<{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
@ -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<typeof ZCreateDocumentPayloadSchema>;
|
||||
export type TCreateDocumentRequest = z.infer<typeof ZCreateDocumentRequestSchema>;
|
||||
export type TCreateDocumentResponse = z.infer<typeof ZCreateDocumentResponseSchema>;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<typeof ZCreateEnvelopePayloadSchema>;
|
||||
export type TCreateEnvelopeRequest = z.infer<typeof ZCreateEnvelopeRequestSchema>;
|
||||
export type TCreateEnvelopeResponse = z.infer<typeof ZCreateEnvelopeResponseSchema>;
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}),
|
||||
|
||||
@ -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<typeof ZCreateTemplatePayloadSchema>;
|
||||
export type TCreateTemplateMutationSchema = z.infer<typeof ZCreateTemplateMutationSchema>;
|
||||
export type TDuplicateTemplateMutationSchema = z.infer<typeof ZDuplicateTemplateMutationSchema>;
|
||||
export type TDeleteTemplateMutationSchema = z.infer<typeof ZDeleteTemplateMutationSchema>;
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user