feat: add attachments (#2091)

This commit is contained in:
Lucas Smith
2025-10-23 23:07:10 +11:00
committed by GitHub
parent 4a3859ec60
commit 2eebc0e439
51 changed files with 1284 additions and 15 deletions
+4
View File
@@ -427,6 +427,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
globalAccessAuth: body.authOptions?.globalAccessAuth,
globalActionAuth: body.authOptions?.globalActionAuth,
},
attachments: body.attachments,
meta: {
subject: body.meta.subject,
message: body.meta.message,
@@ -497,6 +498,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
publicDescription,
type,
meta,
attachments,
} = body;
try {
@@ -568,6 +570,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
publicDescription,
},
meta,
attachments,
requestMetadata: metadata,
});
@@ -792,6 +795,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
...body.meta,
title: body.title,
},
attachments: body.attachments,
requestMetadata: metadata,
});
+19
View File
@@ -22,6 +22,7 @@ import {
ZRecipientActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import { ZFieldMetaPrefillFieldsSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
extendZodWithOpenApi(z);
@@ -197,6 +198,15 @@ export const ZCreateDocumentMutationSchema = z.object({
description: 'The globalActionAuth property is only available for Enterprise accounts.',
}),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
attachments: z
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
});
export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutationSchema>;
@@ -262,6 +272,15 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
})
.optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
attachments: z
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
});
export type TCreateDocumentFromTemplateMutationSchema = z.infer<
@@ -0,0 +1,50 @@
import { DocumentStatus } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
export type CreateAttachmentOptions = {
envelopeId: string;
teamId: number;
userId: number;
data: {
label: string;
data: string;
};
};
export const createAttachment = async ({
envelopeId,
teamId,
userId,
data,
}: CreateAttachmentOptions) => {
const envelope = await prisma.envelope.findFirst({
where: {
id: envelopeId,
team: buildTeamWhereQuery({ teamId, userId }),
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
if (envelope.status === DocumentStatus.COMPLETED || envelope.status === DocumentStatus.REJECTED) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Attachments can not be modified after the document has been completed or rejected',
});
}
return await prisma.envelopeAttachment.create({
data: {
envelopeId,
type: 'link',
...data,
},
});
};
@@ -0,0 +1,47 @@
import { DocumentStatus } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
export type DeleteAttachmentOptions = {
id: string;
userId: number;
teamId: number;
};
export const deleteAttachment = async ({ id, userId, teamId }: DeleteAttachmentOptions) => {
const attachment = await prisma.envelopeAttachment.findFirst({
where: {
id,
envelope: {
team: buildTeamWhereQuery({ teamId, userId }),
},
},
include: {
envelope: true,
},
});
if (!attachment) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Attachment not found',
});
}
if (
attachment.envelope.status === DocumentStatus.COMPLETED ||
attachment.envelope.status === DocumentStatus.REJECTED
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Attachments can not be modified after the document has been completed or rejected',
});
}
await prisma.envelopeAttachment.delete({
where: {
id,
},
});
};
@@ -0,0 +1,38 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
export type FindAttachmentsByEnvelopeIdOptions = {
envelopeId: string;
userId: number;
teamId: number;
};
export const findAttachmentsByEnvelopeId = async ({
envelopeId,
userId,
teamId,
}: FindAttachmentsByEnvelopeIdOptions) => {
const envelope = await prisma.envelope.findFirst({
where: {
id: envelopeId,
team: buildTeamWhereQuery({ teamId, userId }),
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
return await prisma.envelopeAttachment.findMany({
where: {
envelopeId,
},
orderBy: {
createdAt: 'asc',
},
});
};
@@ -0,0 +1,70 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
export type FindAttachmentsByTokenOptions = {
envelopeId: string;
token: string;
};
export const findAttachmentsByToken = async ({
envelopeId,
token,
}: FindAttachmentsByTokenOptions) => {
const envelope = await prisma.envelope.findFirst({
where: {
id: envelopeId,
recipients: {
some: {
token,
},
},
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
return await prisma.envelopeAttachment.findMany({
where: {
envelopeId,
},
orderBy: {
createdAt: 'asc',
},
});
};
export type FindAttachmentsByTeamOptions = {
envelopeId: string;
teamId: number;
};
export const findAttachmentsByTeam = async ({
envelopeId,
teamId,
}: FindAttachmentsByTeamOptions) => {
const envelope = await prisma.envelope.findFirst({
where: {
id: envelopeId,
teamId,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
return await prisma.envelopeAttachment.findMany({
where: {
envelopeId,
},
orderBy: {
createdAt: 'asc',
},
});
};
@@ -0,0 +1,49 @@
import { DocumentStatus } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
export type UpdateAttachmentOptions = {
id: string;
userId: number;
teamId: number;
data: { label?: string; data?: string };
};
export const updateAttachment = async ({ id, teamId, userId, data }: UpdateAttachmentOptions) => {
const attachment = await prisma.envelopeAttachment.findFirst({
where: {
id,
envelope: {
team: buildTeamWhereQuery({ teamId, userId }),
},
},
include: {
envelope: true,
},
});
if (!attachment) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Attachment not found',
});
}
if (
attachment.envelope.status === DocumentStatus.COMPLETED ||
attachment.envelope.status === DocumentStatus.REJECTED
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Attachments can not be modified after the document has been completed or rejected',
});
}
return await prisma.envelopeAttachment.update({
where: {
id,
},
data,
});
};
@@ -20,6 +20,7 @@ import type { TCreateEnvelopeRequest } from '@documenso/trpc/server/envelope-rou
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
import type { TDocumentFormValues } from '../../types/document-form-values';
import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
@@ -58,6 +59,11 @@ export type CreateEnvelopeOptions = {
recipients?: TCreateEnvelopeRequest['recipients'];
folderId?: string;
};
attachments?: Array<{
label: string;
data: string;
type?: TEnvelopeAttachmentType;
}>;
meta?: Partial<Omit<DocumentMeta, 'id'>>;
requestMetadata: ApiRequestMetadata;
};
@@ -67,6 +73,7 @@ export const createEnvelope = async ({
teamId,
normalizePdf,
data,
attachments,
meta,
requestMetadata,
internalVersion,
@@ -246,6 +253,15 @@ export const createEnvelope = async ({
})),
},
},
envelopeAttachments: {
createMany: {
data: (attachments || []).map((attachment) => ({
label: attachment.label,
data: attachment.data,
type: attachment.type ?? 'link',
})),
},
},
userId,
teamId,
authOptions,
@@ -338,6 +354,7 @@ export const createEnvelope = async ({
fields: true,
folder: true,
envelopeItems: true,
envelopeAttachments: true,
},
});
@@ -640,6 +640,23 @@ export const createDocumentFromDirectTemplate = async ({
data: auditLogsToCreate,
});
const templateAttachments = await tx.envelopeAttachment.findMany({
where: {
envelopeId: directTemplateEnvelope.id,
},
});
if (templateAttachments.length > 0) {
await tx.envelopeAttachment.createMany({
data: templateAttachments.map((attachment) => ({
envelopeId: createdEnvelope.id,
type: attachment.type,
label: attachment.label,
data: attachment.data,
})),
});
}
// Send email to template owner.
const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, {
recipientName: directRecipientEmail,
@@ -91,6 +91,12 @@ export type CreateDocumentFromTemplateOptions = {
envelopeItemId?: string;
}[];
attachments?: Array<{
label: string;
data: string;
type?: 'link';
}>;
/**
* Values that will override the predefined values in the template.
*/
@@ -295,6 +301,7 @@ export const createDocumentFromTemplate = async ({
requestMetadata,
folderId,
prefillFields,
attachments,
}: CreateDocumentFromTemplateOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id,
@@ -667,6 +674,33 @@ export const createDocumentFromTemplate = async ({
}),
});
const templateAttachments = await tx.envelopeAttachment.findMany({
where: {
envelopeId: template.id,
},
});
const attachmentsToCreate = [
...templateAttachments.map((attachment) => ({
envelopeId: envelope.id,
type: attachment.type,
label: attachment.label,
data: attachment.data,
})),
...(attachments || []).map((attachment) => ({
envelopeId: envelope.id,
type: attachment.type || 'link',
label: attachment.label,
data: attachment.data,
})),
];
if (attachmentsToCreate.length > 0) {
await tx.envelopeAttachment.createMany({
data: attachmentsToCreate,
});
}
const createdEnvelope = await tx.envelope.findFirst({
where: {
id: envelope.id,
@@ -0,0 +1,5 @@
import { z } from 'zod';
export const ZEnvelopeAttachmentTypeSchema = z.enum(['link']);
export type TEnvelopeAttachmentType = z.infer<typeof ZEnvelopeAttachmentTypeSchema>;
@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "EnvelopeAttachment" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"label" TEXT NOT NULL,
"data" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"envelopeId" TEXT NOT NULL,
CONSTRAINT "EnvelopeAttachment_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "EnvelopeAttachment" ADD CONSTRAINT "EnvelopeAttachment_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+18
View File
@@ -422,6 +422,8 @@ model Envelope {
documentMetaId String @unique
documentMeta DocumentMeta @relation(fields: [documentMetaId], references: [id])
envelopeAttachments EnvelopeAttachment[]
}
model EnvelopeItem {
@@ -508,6 +510,22 @@ model DocumentMeta {
envelope Envelope?
}
/// @zod.import(["import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';"])
model EnvelopeAttachment {
id String @id @default(cuid())
type String /// [EnvelopeAttachmentType] @zod.custom.use(ZEnvelopeAttachmentTypeSchema)
label String
data String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
envelopeId String
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
}
enum ReadStatus {
NOT_OPENED
OPENED
+3
View File
@@ -5,6 +5,7 @@ import type {
} from '@documenso/lib/types/document-auth';
import type { TDocumentEmailSettings } from '@documenso/lib/types/document-email';
import type { TDocumentFormValues } from '@documenso/lib/types/document-form-values';
import type { TEnvelopeAttachmentType } from '@documenso/lib/types/envelope-attachment';
import type { TFieldMetaNotOptionalSchema } from '@documenso/lib/types/field-meta';
import type { TClaimFlags } from '@documenso/lib/types/subscription';
@@ -23,6 +24,8 @@ declare global {
type RecipientAuthOptions = TRecipientAuthOptions;
type FieldMeta = TFieldMetaNotOptionalSchema;
type EnvelopeAttachmentType = TEnvelopeAttachmentType;
}
}
@@ -0,0 +1,50 @@
import { EnvelopeType } from '@prisma/client';
import { createAttachment } from '@documenso/lib/server-only/envelope-attachment/create-attachment';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { authenticatedProcedure } from '../../trpc';
import {
ZCreateAttachmentRequestSchema,
ZCreateAttachmentResponseSchema,
} from './create-attachment.types';
export const createAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/attachment/create',
summary: 'Create attachment',
description: 'Create a new attachment for a document',
tags: ['Document'],
},
})
.input(ZCreateAttachmentRequestSchema)
.output(ZCreateAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { documentId, data } = input;
ctx.logger.info({
input: { documentId, label: data.label },
});
const envelope = await getEnvelopeById({
id: {
type: 'documentId',
id: documentId,
},
userId,
teamId,
type: EnvelopeType.DOCUMENT,
});
await createAttachment({
envelopeId: envelope.id,
teamId,
userId,
data,
});
});
@@ -0,0 +1,14 @@
import { z } from 'zod';
export const ZCreateAttachmentRequestSchema = z.object({
documentId: z.number(),
data: z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
}),
});
export const ZCreateAttachmentResponseSchema = z.void();
export type TCreateAttachmentRequest = z.infer<typeof ZCreateAttachmentRequestSchema>;
export type TCreateAttachmentResponse = z.infer<typeof ZCreateAttachmentResponseSchema>;
@@ -0,0 +1,36 @@
import { deleteAttachment } from '@documenso/lib/server-only/envelope-attachment/delete-attachment';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteAttachmentRequestSchema,
ZDeleteAttachmentResponseSchema,
} from './delete-attachment.types';
export const deleteAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/attachment/delete',
summary: 'Delete attachment',
description: 'Delete an attachment from a document',
tags: ['Document'],
},
})
.input(ZDeleteAttachmentRequestSchema)
.output(ZDeleteAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { id } = input;
ctx.logger.info({
input: { id },
});
await deleteAttachment({
id,
userId,
teamId,
});
});
@@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZDeleteAttachmentRequestSchema = z.object({
id: z.string(),
});
export const ZDeleteAttachmentResponseSchema = z.void();
export type TDeleteAttachmentRequest = z.infer<typeof ZDeleteAttachmentRequestSchema>;
export type TDeleteAttachmentResponse = z.infer<typeof ZDeleteAttachmentResponseSchema>;
@@ -0,0 +1,52 @@
import { EnvelopeType } from '@prisma/client';
import { findAttachmentsByEnvelopeId } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-envelope-id';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { authenticatedProcedure } from '../../trpc';
import {
ZFindAttachmentsRequestSchema,
ZFindAttachmentsResponseSchema,
} from './find-attachments.types';
export const findAttachmentsRoute = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/document/attachment',
summary: 'Find attachments',
description: 'Find all attachments for a document',
tags: ['Document'],
},
})
.input(ZFindAttachmentsRequestSchema)
.output(ZFindAttachmentsResponseSchema)
.query(async ({ input, ctx }) => {
const { documentId } = input;
const { teamId } = ctx;
const userId = ctx.user.id;
ctx.logger.info({
input: { documentId },
});
const envelope = await getEnvelopeById({
id: {
type: 'documentId',
id: documentId,
},
userId,
teamId,
type: EnvelopeType.DOCUMENT,
});
const data = await findAttachmentsByEnvelopeId({
envelopeId: envelope.id,
teamId,
userId,
});
return {
data,
};
});
@@ -0,0 +1,21 @@
import { z } from 'zod';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
export const ZFindAttachmentsRequestSchema = z.object({
documentId: z.number(),
});
export const ZFindAttachmentsResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
type: ZEnvelopeAttachmentTypeSchema,
label: z.string(),
data: z.string(),
}),
),
});
export type TFindAttachmentsRequest = z.infer<typeof ZFindAttachmentsRequestSchema>;
export type TFindAttachmentsResponse = z.infer<typeof ZFindAttachmentsResponseSchema>;
@@ -0,0 +1,37 @@
import { updateAttachment } from '@documenso/lib/server-only/envelope-attachment/update-attachment';
import { authenticatedProcedure } from '../../trpc';
import {
ZUpdateAttachmentRequestSchema,
ZUpdateAttachmentResponseSchema,
} from './update-attachment.types';
export const updateAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/attachment/update',
summary: 'Update attachment',
description: 'Update an existing attachment',
tags: ['Document'],
},
})
.input(ZUpdateAttachmentRequestSchema)
.output(ZUpdateAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { id, data } = input;
ctx.logger.info({
input: { id },
});
await updateAttachment({
id,
userId,
teamId,
data,
});
});
@@ -0,0 +1,14 @@
import { z } from 'zod';
export const ZUpdateAttachmentRequestSchema = z.object({
id: z.string(),
data: z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
}),
});
export const ZUpdateAttachmentResponseSchema = z.void();
export type TUpdateAttachmentRequest = z.infer<typeof ZUpdateAttachmentRequestSchema>;
export type TUpdateAttachmentResponse = z.infer<typeof ZUpdateAttachmentResponseSchema>;
@@ -37,6 +37,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure
recipients,
meta,
folderId,
attachments,
} = input;
const { remaining } = await getServerLimits({ userId: user.id, teamId });
@@ -86,6 +87,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure
},
],
},
attachments,
meta: {
...meta,
emailSettings: meta?.emailSettings ?? undefined,
@@ -7,6 +7,7 @@ import {
} from '@documenso/lib/types/document-auth';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
@@ -68,6 +69,15 @@ export const ZCreateDocumentTemporaryRequestSchema = z.object({
}),
)
.optional(),
attachments: z
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
meta: ZDocumentMetaCreateSchema.optional(),
});
@@ -16,7 +16,7 @@ export const createDocumentRoute = authenticatedProcedure
.output(ZCreateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { title, documentDataId, timezone, folderId } = input;
const { title, documentDataId, timezone, folderId, attachments } = input;
ctx.logger.info({
input: {
@@ -48,6 +48,7 @@ export const createDocumentRoute = authenticatedProcedure
},
],
},
attachments,
normalizePdf: true,
requestMetadata: ctx.metadata,
});
@@ -1,6 +1,7 @@
import { z } from 'zod';
import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import { ZDocumentTitleSchema } from './schema';
@@ -19,6 +20,15 @@ export const ZCreateDocumentRequestSchema = z.object({
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
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
});
export const ZCreateDocumentResponseSchema = z.object({
@@ -1,5 +1,9 @@
import { router } from '../trpc';
import { accessAuthRequest2FAEmailRoute } from './access-auth-request-2fa-email';
import { createAttachmentRoute } from './attachment/create-attachment';
import { deleteAttachmentRoute } from './attachment/delete-attachment';
import { findAttachmentsRoute } from './attachment/find-attachments';
import { updateAttachmentRoute } from './attachment/update-attachment';
import { createDocumentRoute } from './create-document';
import { createDocumentTemporaryRoute } from './create-document-temporary';
import { deleteDocumentRoute } from './delete-document';
@@ -53,4 +57,10 @@ export const documentRouter = router({
find: findInboxRoute,
getCount: getInboxCountRoute,
}),
attachment: {
create: createAttachmentRoute,
update: updateAttachmentRoute,
delete: deleteAttachmentRoute,
find: findAttachmentsRoute,
},
});
@@ -0,0 +1,37 @@
import { createAttachment } from '@documenso/lib/server-only/envelope-attachment/create-attachment';
import { authenticatedProcedure } from '../../trpc';
import {
ZCreateAttachmentRequestSchema,
ZCreateAttachmentResponseSchema,
} from './create-attachment.types';
export const createAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/attachment/create',
summary: 'Create attachment',
description: 'Create a new attachment for an envelope',
tags: ['Envelope'],
},
})
.input(ZCreateAttachmentRequestSchema)
.output(ZCreateAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { envelopeId, data } = input;
ctx.logger.info({
input: { envelopeId, label: data.label },
});
await createAttachment({
envelopeId,
teamId,
userId,
data,
});
});
@@ -0,0 +1,14 @@
import { z } from 'zod';
export const ZCreateAttachmentRequestSchema = z.object({
envelopeId: z.string(),
data: z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
}),
});
export const ZCreateAttachmentResponseSchema = z.void();
export type TCreateAttachmentRequest = z.infer<typeof ZCreateAttachmentRequestSchema>;
export type TCreateAttachmentResponse = z.infer<typeof ZCreateAttachmentResponseSchema>;
@@ -0,0 +1,36 @@
import { deleteAttachment } from '@documenso/lib/server-only/envelope-attachment/delete-attachment';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteAttachmentRequestSchema,
ZDeleteAttachmentResponseSchema,
} from './delete-attachment.types';
export const deleteAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/attachment/delete',
summary: 'Delete attachment',
description: 'Delete an attachment from an envelope',
tags: ['Envelope'],
},
})
.input(ZDeleteAttachmentRequestSchema)
.output(ZDeleteAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { id } = input;
ctx.logger.info({
input: { id },
});
await deleteAttachment({
id,
userId,
teamId,
});
});
@@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZDeleteAttachmentRequestSchema = z.object({
id: z.string(),
});
export const ZDeleteAttachmentResponseSchema = z.void();
export type TDeleteAttachmentRequest = z.infer<typeof ZDeleteAttachmentRequestSchema>;
export type TDeleteAttachmentResponse = z.infer<typeof ZDeleteAttachmentResponseSchema>;
@@ -0,0 +1,52 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { findAttachmentsByEnvelopeId } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-envelope-id';
import { findAttachmentsByToken } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-token';
import { procedure } from '../../trpc';
import {
ZFindAttachmentsRequestSchema,
ZFindAttachmentsResponseSchema,
} from './find-attachments.types';
export const findAttachmentsRoute = procedure
.meta({
openapi: {
method: 'GET',
path: '/envelope/attachment',
summary: 'Find attachments',
description: 'Find all attachments for an envelope',
tags: ['Envelope'],
},
})
.input(ZFindAttachmentsRequestSchema)
.output(ZFindAttachmentsResponseSchema)
.query(async ({ input, ctx }) => {
const { envelopeId, token } = input;
ctx.logger.info({
input: { envelopeId },
});
if (token) {
const data = await findAttachmentsByToken({ envelopeId, token });
return {
data,
};
}
const { teamId } = ctx;
const userId = ctx.user?.id;
if (!userId || !teamId) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You must be authenticated to access this resource',
});
}
const data = await findAttachmentsByEnvelopeId({ envelopeId, teamId, userId });
return {
data,
};
});
@@ -0,0 +1,22 @@
import { z } from 'zod';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
export const ZFindAttachmentsRequestSchema = z.object({
envelopeId: z.string(),
token: z.string().optional(),
});
export const ZFindAttachmentsResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
type: ZEnvelopeAttachmentTypeSchema,
label: z.string(),
data: z.string(),
}),
),
});
export type TFindAttachmentsRequest = z.infer<typeof ZFindAttachmentsRequestSchema>;
export type TFindAttachmentsResponse = z.infer<typeof ZFindAttachmentsResponseSchema>;
@@ -0,0 +1,37 @@
import { updateAttachment } from '@documenso/lib/server-only/envelope-attachment/update-attachment';
import { authenticatedProcedure } from '../../trpc';
import {
ZUpdateAttachmentRequestSchema,
ZUpdateAttachmentResponseSchema,
} from './update-attachment.types';
export const updateAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/attachment/update',
summary: 'Update attachment',
description: 'Update an existing attachment',
tags: ['Envelope'],
},
})
.input(ZUpdateAttachmentRequestSchema)
.output(ZUpdateAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { id, data } = input;
ctx.logger.info({
input: { id },
});
await updateAttachment({
id,
userId,
teamId,
data,
});
});
@@ -0,0 +1,14 @@
import { z } from 'zod';
export const ZUpdateAttachmentRequestSchema = z.object({
id: z.string(),
data: z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
}),
});
export const ZUpdateAttachmentResponseSchema = z.void();
export type TUpdateAttachmentRequest = z.infer<typeof ZUpdateAttachmentRequestSchema>;
export type TUpdateAttachmentResponse = z.infer<typeof ZUpdateAttachmentResponseSchema>;
@@ -9,7 +9,7 @@ import {
} from './create-envelope.types';
export const createEnvelopeRoute = authenticatedProcedure
.input(ZCreateEnvelopeRequestSchema) // Note: Before releasing this to public, update the response schema to be correct.
.input(ZCreateEnvelopeRequestSchema)
.output(ZCreateEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
@@ -24,6 +24,7 @@ export const createEnvelopeRoute = authenticatedProcedure
folderId,
items,
meta,
attachments,
} = input;
ctx.logger.info({
@@ -57,6 +58,7 @@ export const createEnvelopeRoute = authenticatedProcedure
folderId,
envelopeItems: items,
},
attachments,
meta,
normalizePdf: true,
requestMetadata: ctx.metadata,
@@ -7,6 +7,7 @@ import {
} from '@documenso/lib/types/document-auth';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
@@ -76,6 +77,15 @@ export const ZCreateEnvelopeRequestSchema = z.object({
)
.optional(),
meta: ZDocumentMetaCreateSchema.optional(),
attachments: z
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
});
export const ZCreateEnvelopeResponseSchema = z.object({
@@ -1,4 +1,8 @@
import { router } from '../trpc';
import { createAttachmentRoute } from './attachment/create-attachment';
import { deleteAttachmentRoute } from './attachment/delete-attachment';
import { findAttachmentsRoute } from './attachment/find-attachments';
import { updateAttachmentRoute } from './attachment/update-attachment';
import { createEnvelopeRoute } from './create-envelope';
import { createEnvelopeItemsRoute } from './create-envelope-items';
import { deleteEnvelopeRoute } from './delete-envelope';
@@ -35,4 +39,10 @@ export const envelopeRouter = router({
set: setEnvelopeFieldsRoute,
sign: signEnvelopeFieldRoute,
},
attachment: {
find: findAttachmentsRoute,
create: createAttachmentRoute,
update: updateAttachmentRoute,
delete: deleteAttachmentRoute,
},
});
@@ -235,6 +235,7 @@ export const templateRouter = router({
publicDescription,
type,
meta,
attachments,
} = input;
const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`;
@@ -268,6 +269,7 @@ export const templateRouter = router({
publicDescription,
},
meta,
attachments,
requestMetadata: ctx.metadata,
});
@@ -19,6 +19,7 @@ import {
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
} from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import {
@@ -197,6 +198,15 @@ export const ZCreateTemplateV2RequestSchema = z.object({
publicDescription: ZTemplatePublicDescriptionSchema.optional(),
type: z.nativeEnum(TemplateType).optional(),
meta: ZTemplateMetaUpsertSchema.optional(),
attachments: z
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
});
/**