+
+
;
@@ -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<
diff --git a/packages/lib/server-only/envelope-attachment/create-attachment.ts b/packages/lib/server-only/envelope-attachment/create-attachment.ts
new file mode 100644
index 000000000..f55b7f56d
--- /dev/null
+++ b/packages/lib/server-only/envelope-attachment/create-attachment.ts
@@ -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,
+ },
+ });
+};
diff --git a/packages/lib/server-only/envelope-attachment/delete-attachment.ts b/packages/lib/server-only/envelope-attachment/delete-attachment.ts
new file mode 100644
index 000000000..3268bf5ba
--- /dev/null
+++ b/packages/lib/server-only/envelope-attachment/delete-attachment.ts
@@ -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,
+ },
+ });
+};
diff --git a/packages/lib/server-only/envelope-attachment/find-attachments-by-envelope-id.ts b/packages/lib/server-only/envelope-attachment/find-attachments-by-envelope-id.ts
new file mode 100644
index 000000000..ff6402fea
--- /dev/null
+++ b/packages/lib/server-only/envelope-attachment/find-attachments-by-envelope-id.ts
@@ -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',
+ },
+ });
+};
diff --git a/packages/lib/server-only/envelope-attachment/find-attachments-by-token.ts b/packages/lib/server-only/envelope-attachment/find-attachments-by-token.ts
new file mode 100644
index 000000000..699d0c00a
--- /dev/null
+++ b/packages/lib/server-only/envelope-attachment/find-attachments-by-token.ts
@@ -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',
+ },
+ });
+};
diff --git a/packages/lib/server-only/envelope-attachment/update-attachment.ts b/packages/lib/server-only/envelope-attachment/update-attachment.ts
new file mode 100644
index 000000000..4d1dae350
--- /dev/null
+++ b/packages/lib/server-only/envelope-attachment/update-attachment.ts
@@ -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,
+ });
+};
diff --git a/packages/lib/server-only/envelope/create-envelope.ts b/packages/lib/server-only/envelope/create-envelope.ts
index 2a0bc9fee..5f4198377 100644
--- a/packages/lib/server-only/envelope/create-envelope.ts
+++ b/packages/lib/server-only/envelope/create-envelope.ts
@@ -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>;
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,
},
});
diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts
index fb354d8db..f8005ca51 100644
--- a/packages/lib/server-only/template/create-document-from-direct-template.ts
+++ b/packages/lib/server-only/template/create-document-from-direct-template.ts
@@ -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,
diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts
index 3aa48e29a..7c3e63d48 100644
--- a/packages/lib/server-only/template/create-document-from-template.ts
+++ b/packages/lib/server-only/template/create-document-from-template.ts
@@ -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,
diff --git a/packages/lib/types/envelope-attachment.ts b/packages/lib/types/envelope-attachment.ts
new file mode 100644
index 000000000..3634a8ab7
--- /dev/null
+++ b/packages/lib/types/envelope-attachment.ts
@@ -0,0 +1,5 @@
+import { z } from 'zod';
+
+export const ZEnvelopeAttachmentTypeSchema = z.enum(['link']);
+
+export type TEnvelopeAttachmentType = z.infer;
diff --git a/packages/prisma/migrations/20251023021213_add_envelope_attachments/migration.sql b/packages/prisma/migrations/20251023021213_add_envelope_attachments/migration.sql
new file mode 100644
index 000000000..7540b41c9
--- /dev/null
+++ b/packages/prisma/migrations/20251023021213_add_envelope_attachments/migration.sql
@@ -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;
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 10e68cade..0db892527 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -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
diff --git a/packages/prisma/types/types.d.ts b/packages/prisma/types/types.d.ts
index 2621bd54a..d8c0f8103 100644
--- a/packages/prisma/types/types.d.ts
+++ b/packages/prisma/types/types.d.ts
@@ -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;
}
}
diff --git a/packages/trpc/server/document-router/attachment/create-attachment.ts b/packages/trpc/server/document-router/attachment/create-attachment.ts
new file mode 100644
index 000000000..16eea3121
--- /dev/null
+++ b/packages/trpc/server/document-router/attachment/create-attachment.ts
@@ -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,
+ });
+ });
diff --git a/packages/trpc/server/document-router/attachment/create-attachment.types.ts b/packages/trpc/server/document-router/attachment/create-attachment.types.ts
new file mode 100644
index 000000000..1baf69b24
--- /dev/null
+++ b/packages/trpc/server/document-router/attachment/create-attachment.types.ts
@@ -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;
+export type TCreateAttachmentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/attachment/delete-attachment.ts b/packages/trpc/server/document-router/attachment/delete-attachment.ts
new file mode 100644
index 000000000..f7c058c5b
--- /dev/null
+++ b/packages/trpc/server/document-router/attachment/delete-attachment.ts
@@ -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,
+ });
+ });
diff --git a/packages/trpc/server/document-router/attachment/delete-attachment.types.ts b/packages/trpc/server/document-router/attachment/delete-attachment.types.ts
new file mode 100644
index 000000000..81d7c0614
--- /dev/null
+++ b/packages/trpc/server/document-router/attachment/delete-attachment.types.ts
@@ -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;
+export type TDeleteAttachmentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/attachment/find-attachments.ts b/packages/trpc/server/document-router/attachment/find-attachments.ts
new file mode 100644
index 000000000..a90718363
--- /dev/null
+++ b/packages/trpc/server/document-router/attachment/find-attachments.ts
@@ -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,
+ };
+ });
diff --git a/packages/trpc/server/document-router/attachment/find-attachments.types.ts b/packages/trpc/server/document-router/attachment/find-attachments.types.ts
new file mode 100644
index 000000000..7f73f6210
--- /dev/null
+++ b/packages/trpc/server/document-router/attachment/find-attachments.types.ts
@@ -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;
+export type TFindAttachmentsResponse = z.infer;
diff --git a/packages/trpc/server/document-router/attachment/update-attachment.ts b/packages/trpc/server/document-router/attachment/update-attachment.ts
new file mode 100644
index 000000000..d03715d03
--- /dev/null
+++ b/packages/trpc/server/document-router/attachment/update-attachment.ts
@@ -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,
+ });
+ });
diff --git a/packages/trpc/server/document-router/attachment/update-attachment.types.ts b/packages/trpc/server/document-router/attachment/update-attachment.types.ts
new file mode 100644
index 000000000..eaf12b559
--- /dev/null
+++ b/packages/trpc/server/document-router/attachment/update-attachment.types.ts
@@ -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;
+export type TUpdateAttachmentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/create-document-temporary.ts b/packages/trpc/server/document-router/create-document-temporary.ts
index 878da25c7..2159b0a65 100644
--- a/packages/trpc/server/document-router/create-document-temporary.ts
+++ b/packages/trpc/server/document-router/create-document-temporary.ts
@@ -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,
diff --git a/packages/trpc/server/document-router/create-document-temporary.types.ts b/packages/trpc/server/document-router/create-document-temporary.types.ts
index 5223adbfb..8895e1a99 100644
--- a/packages/trpc/server/document-router/create-document-temporary.types.ts
+++ b/packages/trpc/server/document-router/create-document-temporary.types.ts
@@ -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(),
});
diff --git a/packages/trpc/server/document-router/create-document.ts b/packages/trpc/server/document-router/create-document.ts
index 71e1b2594..275978df9 100644
--- a/packages/trpc/server/document-router/create-document.ts
+++ b/packages/trpc/server/document-router/create-document.ts
@@ -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,
});
diff --git a/packages/trpc/server/document-router/create-document.types.ts b/packages/trpc/server/document-router/create-document.types.ts
index 7c42e9286..43fa32291 100644
--- a/packages/trpc/server/document-router/create-document.types.ts
+++ b/packages/trpc/server/document-router/create-document.types.ts
@@ -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({
diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts
index d49037f8c..f5d34a141 100644
--- a/packages/trpc/server/document-router/router.ts
+++ b/packages/trpc/server/document-router/router.ts
@@ -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,
+ },
});
diff --git a/packages/trpc/server/envelope-router/attachment/create-attachment.ts b/packages/trpc/server/envelope-router/attachment/create-attachment.ts
new file mode 100644
index 000000000..239052005
--- /dev/null
+++ b/packages/trpc/server/envelope-router/attachment/create-attachment.ts
@@ -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,
+ });
+ });
diff --git a/packages/trpc/server/envelope-router/attachment/create-attachment.types.ts b/packages/trpc/server/envelope-router/attachment/create-attachment.types.ts
new file mode 100644
index 000000000..106a6e751
--- /dev/null
+++ b/packages/trpc/server/envelope-router/attachment/create-attachment.types.ts
@@ -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;
+export type TCreateAttachmentResponse = z.infer;
diff --git a/packages/trpc/server/envelope-router/attachment/delete-attachment.ts b/packages/trpc/server/envelope-router/attachment/delete-attachment.ts
new file mode 100644
index 000000000..3fd82805e
--- /dev/null
+++ b/packages/trpc/server/envelope-router/attachment/delete-attachment.ts
@@ -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,
+ });
+ });
diff --git a/packages/trpc/server/envelope-router/attachment/delete-attachment.types.ts b/packages/trpc/server/envelope-router/attachment/delete-attachment.types.ts
new file mode 100644
index 000000000..81d7c0614
--- /dev/null
+++ b/packages/trpc/server/envelope-router/attachment/delete-attachment.types.ts
@@ -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;
+export type TDeleteAttachmentResponse = z.infer;
diff --git a/packages/trpc/server/envelope-router/attachment/find-attachments.ts b/packages/trpc/server/envelope-router/attachment/find-attachments.ts
new file mode 100644
index 000000000..74e81c5e1
--- /dev/null
+++ b/packages/trpc/server/envelope-router/attachment/find-attachments.ts
@@ -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,
+ };
+ });
diff --git a/packages/trpc/server/envelope-router/attachment/find-attachments.types.ts b/packages/trpc/server/envelope-router/attachment/find-attachments.types.ts
new file mode 100644
index 000000000..e61b2d2cb
--- /dev/null
+++ b/packages/trpc/server/envelope-router/attachment/find-attachments.types.ts
@@ -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;
+export type TFindAttachmentsResponse = z.infer;
diff --git a/packages/trpc/server/envelope-router/attachment/update-attachment.ts b/packages/trpc/server/envelope-router/attachment/update-attachment.ts
new file mode 100644
index 000000000..11a7dfa5b
--- /dev/null
+++ b/packages/trpc/server/envelope-router/attachment/update-attachment.ts
@@ -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,
+ });
+ });
diff --git a/packages/trpc/server/envelope-router/attachment/update-attachment.types.ts b/packages/trpc/server/envelope-router/attachment/update-attachment.types.ts
new file mode 100644
index 000000000..eaf12b559
--- /dev/null
+++ b/packages/trpc/server/envelope-router/attachment/update-attachment.types.ts
@@ -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;
+export type TUpdateAttachmentResponse = z.infer;
diff --git a/packages/trpc/server/envelope-router/create-envelope.ts b/packages/trpc/server/envelope-router/create-envelope.ts
index ce3bb0a47..517fd699d 100644
--- a/packages/trpc/server/envelope-router/create-envelope.ts
+++ b/packages/trpc/server/envelope-router/create-envelope.ts
@@ -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,
diff --git a/packages/trpc/server/envelope-router/create-envelope.types.ts b/packages/trpc/server/envelope-router/create-envelope.types.ts
index 5f1e04cbf..ea0fee260 100644
--- a/packages/trpc/server/envelope-router/create-envelope.types.ts
+++ b/packages/trpc/server/envelope-router/create-envelope.types.ts
@@ -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({
diff --git a/packages/trpc/server/envelope-router/router.ts b/packages/trpc/server/envelope-router/router.ts
index 0f57741f0..7f4f4c785 100644
--- a/packages/trpc/server/envelope-router/router.ts
+++ b/packages/trpc/server/envelope-router/router.ts
@@ -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,
+ },
});
diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts
index 68bbd2127..3cefb136f 100644
--- a/packages/trpc/server/template-router/router.ts
+++ b/packages/trpc/server/template-router/router.ts
@@ -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,
});
diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts
index a53e8ba8c..0783ef232 100644
--- a/packages/trpc/server/template-router/schema.ts
+++ b/packages/trpc/server/template-router/schema.ts
@@ -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(),
});
/**