feat: add envelopes api (#2105)

This commit is contained in:
David Nguyen
2025-11-07 14:17:52 +11:00
committed by GitHub
parent d2a009d52e
commit d05bfa9fed
230 changed files with 10066 additions and 2812 deletions
@@ -4,18 +4,11 @@ import { authenticatedProcedure } from '../../trpc';
import {
ZCreateAttachmentRequestSchema,
ZCreateAttachmentResponseSchema,
createAttachmentMeta,
} 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'],
},
})
.meta(createAttachmentMeta)
.input(ZCreateAttachmentRequestSchema)
.output(ZCreateAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
@@ -1,5 +1,17 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../../trpc';
export const createAttachmentMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/attachment/create',
summary: 'Create attachment',
description: 'Create a new attachment for an envelope',
tags: ['Envelope Attachments'],
},
};
export const ZCreateAttachmentRequestSchema = z.object({
envelopeId: z.string(),
data: z.object({
@@ -4,18 +4,11 @@ import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteAttachmentRequestSchema,
ZDeleteAttachmentResponseSchema,
deleteAttachmentMeta,
} 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'],
},
})
.meta(deleteAttachmentMeta)
.input(ZDeleteAttachmentRequestSchema)
.output(ZDeleteAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
@@ -1,5 +1,17 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../../trpc';
export const deleteAttachmentMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/attachment/delete',
summary: 'Delete attachment',
description: 'Delete an attachment from an envelope',
tags: ['Envelope Attachments'],
},
};
export const ZDeleteAttachmentRequestSchema = z.object({
id: z.string(),
});
@@ -2,22 +2,15 @@ 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 { maybeAuthenticatedProcedure } from '../../trpc';
import {
ZFindAttachmentsRequestSchema,
ZFindAttachmentsResponseSchema,
findAttachmentsMeta,
} 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'],
},
})
export const findAttachmentsRoute = maybeAuthenticatedProcedure
.meta(findAttachmentsMeta)
.input(ZFindAttachmentsRequestSchema)
.output(ZFindAttachmentsResponseSchema)
.query(async ({ input, ctx }) => {
@@ -2,6 +2,18 @@ import { z } from 'zod';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import type { TrpcRouteMeta } from '../../trpc';
export const findAttachmentsMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/envelope/attachment',
summary: 'Find attachments',
description: 'Find all attachments for an envelope',
tags: ['Envelope Attachments'],
},
};
export const ZFindAttachmentsRequestSchema = z.object({
envelopeId: z.string(),
token: z.string().optional(),
@@ -4,18 +4,11 @@ import { authenticatedProcedure } from '../../trpc';
import {
ZUpdateAttachmentRequestSchema,
ZUpdateAttachmentResponseSchema,
updateAttachmentMeta,
} 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'],
},
})
.meta(updateAttachmentMeta)
.input(ZUpdateAttachmentRequestSchema)
.output(ZUpdateAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
@@ -1,5 +1,17 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../../trpc';
export const updateAttachmentMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/attachment/update',
summary: 'Update attachment',
description: 'Update an existing attachment',
tags: ['Envelope Attachments'],
},
};
export const ZUpdateAttachmentRequestSchema = z.object({
id: z.string(),
data: z.object({
@@ -2,6 +2,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { prefixedId } from '@documenso/lib/universal/id';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
@@ -10,14 +11,17 @@ import { authenticatedProcedure } from '../trpc';
import {
ZCreateEnvelopeItemsRequestSchema,
ZCreateEnvelopeItemsResponseSchema,
createEnvelopeItemsMeta,
} from './create-envelope-items.types';
export const createEnvelopeItemsRoute = authenticatedProcedure
.meta(createEnvelopeItemsMeta)
.input(ZCreateEnvelopeItemsRequestSchema)
.output(ZCreateEnvelopeItemsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx;
const { envelopeId, items } = input;
const { payload, files } = input;
const { envelopeId } = payload;
ctx.logger.info({
input: {
@@ -71,7 +75,7 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
const organisationClaim = envelope.team.organisation.organisationClaim;
const remainingEnvelopeItems =
organisationClaim.envelopeItemCount - envelope.envelopeItems.length - items.length;
organisationClaim.envelopeItemCount - envelope.envelopeItems.length - files.length;
if (remainingEnvelopeItems < 0) {
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
@@ -80,41 +84,24 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
});
}
const foundDocumentData = await prisma.documentData.findMany({
where: {
id: {
in: items.map((item) => item.documentDataId),
},
},
select: {
envelopeItem: {
select: {
id: true,
},
},
},
});
// 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);
// Check that all the document data was found.
if (foundDocumentData.length !== items.length) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document data not found',
});
}
// Check that it doesn't already have an envelope item.
if (foundDocumentData.some((documentData) => documentData.envelopeItem?.id)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document data not found',
});
}
return {
title: file.name,
documentDataId,
};
}),
);
const currentHighestOrderValue =
envelope.envelopeItems[envelope.envelopeItems.length - 1]?.order ?? 1;
const result = await prisma.$transaction(async (tx) => {
const createdItems = await tx.envelopeItem.createManyAndReturn({
data: items.map((item) => ({
data: envelopeItems.map((item) => ({
id: prefixedId('envelope_item'),
envelopeId,
title: item.title,
@@ -148,6 +135,6 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
});
return {
createdEnvelopeItems: result,
data: result,
};
});
@@ -1,38 +1,40 @@
import { z } from 'zod';
import { zfd } from 'zod-form-data';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import { ZDocumentTitleSchema } from '../document-router/schema';
import { zodFormData } from '../../utils/zod-form-data';
import type { TrpcRouteMeta } from '../trpc';
export const ZCreateEnvelopeItemsRequestSchema = z.object({
export const createEnvelopeItemsMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/item/create-many',
summary: 'Create envelope items',
description: 'Create multiple envelope items for an envelope',
tags: ['Envelope Items'],
},
};
export const ZCreateEnvelopeItemsPayloadSchema = z.object({
envelopeId: z.string(),
items: z
.object({
title: ZDocumentTitleSchema,
documentDataId: z.string(),
})
.array(),
// data: z.object() // Currently not used.
});
export const ZCreateEnvelopeItemsRequestSchema = zodFormData({
payload: zfd.json(ZCreateEnvelopeItemsPayloadSchema),
files: zfd.repeatableOfType(zfd.file()),
});
export const ZCreateEnvelopeItemsResponseSchema = z.object({
createdEnvelopeItems: EnvelopeItemSchema.pick({
data: EnvelopeItemSchema.pick({
id: true,
title: true,
documentDataId: true,
envelopeId: true,
order: true,
})
.extend({
documentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true,
}),
})
.array(),
}).array(),
});
export type TCreateEnvelopeItemsPayload = z.infer<typeof ZCreateEnvelopeItemsPayloadSchema>;
export type TCreateEnvelopeItemsRequest = z.infer<typeof ZCreateEnvelopeItemsRequestSchema>;
export type TCreateEnvelopeItemsResponse = z.infer<typeof ZCreateEnvelopeItemsResponseSchema>;
@@ -1,18 +1,24 @@
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 {
ZCreateEnvelopeRequestSchema,
ZCreateEnvelopeResponseSchema,
createEnvelopeMeta,
} from './create-envelope.types';
export const createEnvelopeRoute = authenticatedProcedure
.meta(createEnvelopeMeta)
.input(ZCreateEnvelopeRequestSchema)
.output(ZCreateEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { payload, files } = input;
const {
title,
type,
@@ -20,12 +26,12 @@ export const createEnvelopeRoute = authenticatedProcedure
visibility,
globalAccessAuth,
globalActionAuth,
formValues,
recipients,
folderId,
items,
meta,
attachments,
} = input;
} = payload;
ctx.logger.info({
input: {
@@ -45,13 +51,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.at(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,
@@ -60,16 +115,16 @@ export const createEnvelopeRoute = authenticatedProcedure
type,
title,
externalId,
formValues,
visibility,
globalAccessAuth,
globalActionAuth,
recipients,
recipients: recipientsToCreate,
folderId,
envelopeItems: items,
envelopeItems,
},
attachments,
meta,
normalizePdf: true,
requestMetadata: ctx.metadata,
});
@@ -1,5 +1,6 @@
import { EnvelopeType } from '@prisma/client';
import { z } from 'zod';
import { zfd } from 'zod-form-data';
import {
ZDocumentAccessAuthTypesSchema,
@@ -9,32 +10,35 @@ import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-va
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import {
ZFieldHeightSchema,
ZClampedFieldHeightSchema,
ZClampedFieldPositionXSchema,
ZClampedFieldPositionYSchema,
ZClampedFieldWidthSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} 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 an 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 +46,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,16 +57,17 @@ 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,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
positionX: ZClampedFieldPositionXSchema,
positionY: ZClampedFieldPositionYSchema,
width: ZClampedFieldWidthSchema,
height: ZClampedFieldHeightSchema,
}),
)
.array()
@@ -88,9 +87,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>;
@@ -9,9 +9,11 @@ import { authenticatedProcedure } from '../trpc';
import {
ZDeleteEnvelopeItemRequestSchema,
ZDeleteEnvelopeItemResponseSchema,
deleteEnvelopeItemMeta,
} from './delete-envelope-item.types';
export const deleteEnvelopeItemRoute = authenticatedProcedure
.meta(deleteEnvelopeItemMeta)
.input(ZDeleteEnvelopeItemRequestSchema)
.output(ZDeleteEnvelopeItemResponseSchema)
.mutation(async ({ input, ctx }) => {
@@ -1,5 +1,17 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../trpc';
export const deleteEnvelopeItemMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/item/delete',
summary: 'Delete envelope item',
description: 'Delete an envelope item from an envelope',
tags: ['Envelope Items'],
},
};
export const ZDeleteEnvelopeItemRequestSchema = z.object({
envelopeId: z.string(),
envelopeItemId: z.string(),
@@ -1,22 +1,25 @@
import { EnvelopeType } from '@prisma/client';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteEnvelopeRequestSchema,
ZDeleteEnvelopeResponseSchema,
deleteEnvelopeMeta,
} from './delete-envelope.types';
export const deleteEnvelopeRoute = authenticatedProcedure
// .meta(deleteEnvelopeMeta)
.meta(deleteEnvelopeMeta)
.input(ZDeleteEnvelopeRequestSchema)
.output(ZDeleteEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { envelopeId, envelopeType } = input;
const { envelopeId } = input;
ctx.logger.info({
input: {
@@ -24,7 +27,22 @@ export const deleteEnvelopeRoute = authenticatedProcedure
},
});
await match(envelopeType)
const unsafeEnvelope = await prisma.envelope.findUnique({
where: {
id: envelopeId,
},
select: {
type: true,
},
});
if (!unsafeEnvelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
await match(unsafeEnvelope.type)
.with(EnvelopeType.DOCUMENT, async () =>
deleteDocument({
userId: ctx.user.id,
@@ -1,18 +1,18 @@
import { EnvelopeType } from '@prisma/client';
import { z } from 'zod';
// export const deleteEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/delete',
// summary: 'Delete envelope',
// tags: ['Envelope'],
// },
// };
import type { TrpcRouteMeta } from '../trpc';
export const deleteEnvelopeMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/delete',
summary: 'Delete envelope',
tags: ['Envelope'],
},
};
export const ZDeleteEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
envelopeType: z.nativeEnum(EnvelopeType),
});
export const ZDeleteEnvelopeResponseSchema = z.void();
@@ -5,10 +5,11 @@ import { authenticatedProcedure } from '../trpc';
import {
ZDistributeEnvelopeRequestSchema,
ZDistributeEnvelopeResponseSchema,
distributeEnvelopeMeta,
} from './distribute-envelope.types';
export const distributeEnvelopeRoute = authenticatedProcedure
// .meta(distributeEnvelopeMeta)
.meta(distributeEnvelopeMeta)
.input(ZDistributeEnvelopeRequestSchema)
.output(ZDistributeEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
@@ -2,15 +2,17 @@ import { z } from 'zod';
import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta';
// export const distributeEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/distribute',
// summary: 'Distribute envelope',
// description: 'Send the document out to recipients based on your distribution method',
// tags: ['Envelope'],
// },
// };
import type { TrpcRouteMeta } from '../trpc';
export const distributeEnvelopeMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/distribute',
summary: 'Distribute envelope',
description: 'Send the envelope to recipients based on your distribution method',
tags: ['Envelope'],
},
};
export const ZDistributeEnvelopeRequestSchema = z.object({
envelopeId: z.string().describe('The ID of the envelope to send.'),
@@ -0,0 +1,23 @@
import { authenticatedProcedure } from '../trpc';
import {
ZDownloadEnvelopeItemRequestSchema,
ZDownloadEnvelopeItemResponseSchema,
downloadEnvelopeItemMeta,
} from './download-envelope-item.types';
export const downloadEnvelopeItemRoute = authenticatedProcedure
.meta(downloadEnvelopeItemMeta)
.input(ZDownloadEnvelopeItemRequestSchema)
.output(ZDownloadEnvelopeItemResponseSchema)
.query(({ input, ctx }) => {
const { envelopeItemId, version } = input;
ctx.logger.info({
input: {
envelopeItemId,
version,
},
});
throw new Error('NOT_IMPLEMENTED');
});
@@ -0,0 +1,31 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../trpc';
export const downloadEnvelopeItemMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/envelopeItem/{envelopeItemId}/download',
summary: 'Download an envelope item',
description: 'Download an envelope item by its ID',
tags: ['Envelope Items'],
responseHeaders: z.object({
'Content-Type': z.literal('application/pdf'),
}),
},
};
export const ZDownloadEnvelopeItemRequestSchema = z.object({
envelopeItemId: z.string().describe('The ID of the envelope item to download.'),
version: z
.enum(['original', 'signed'])
.describe(
'The version of the envelope item to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
)
.default('signed'),
});
export const ZDownloadEnvelopeItemResponseSchema = z.instanceof(Uint8Array);
export type TDownloadEnvelopeItemRequest = z.infer<typeof ZDownloadEnvelopeItemRequestSchema>;
export type TDownloadEnvelopeItemResponse = z.infer<typeof ZDownloadEnvelopeItemResponseSchema>;
@@ -4,9 +4,11 @@ import { authenticatedProcedure } from '../trpc';
import {
ZDuplicateEnvelopeRequestSchema,
ZDuplicateEnvelopeResponseSchema,
duplicateEnvelopeMeta,
} from './duplicate-envelope.types';
export const duplicateEnvelopeRoute = authenticatedProcedure
.meta(duplicateEnvelopeMeta)
.input(ZDuplicateEnvelopeRequestSchema)
.output(ZDuplicateEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
@@ -29,6 +31,6 @@ export const duplicateEnvelopeRoute = authenticatedProcedure
});
return {
duplicatedEnvelopeId: duplicatedEnvelope.id,
id: duplicatedEnvelope.id,
};
});
@@ -1,11 +1,23 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../trpc';
export const duplicateEnvelopeMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/duplicate',
summary: 'Duplicate envelope',
description: 'Duplicate an envelope with all its settings',
tags: ['Envelope'],
},
};
export const ZDuplicateEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
});
export const ZDuplicateEnvelopeResponseSchema = z.object({
duplicatedEnvelopeId: z.string(),
id: z.string().describe('The ID of the newly created envelope.'),
});
export type TDuplicateEnvelopeRequest = z.infer<typeof ZDuplicateEnvelopeRequestSchema>;
@@ -0,0 +1,38 @@
import { createEnvelopeFields } from '@documenso/lib/server-only/field/create-envelope-fields';
import { authenticatedProcedure } from '../../trpc';
import {
ZCreateEnvelopeFieldsRequestSchema,
ZCreateEnvelopeFieldsResponseSchema,
createEnvelopeFieldsMeta,
} from './create-envelope-fields.types';
export const createEnvelopeFieldsRoute = authenticatedProcedure
.meta(createEnvelopeFieldsMeta)
.input(ZCreateEnvelopeFieldsRequestSchema)
.output(ZCreateEnvelopeFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx;
const { envelopeId, data: fields } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const { fields: data } = await createEnvelopeFields({
userId: user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
fields,
requestMetadata: metadata,
});
return {
data,
};
});
@@ -0,0 +1,53 @@
import { z } from 'zod';
import {
ZClampedFieldHeightSchema,
ZClampedFieldPositionXSchema,
ZClampedFieldPositionYSchema,
ZClampedFieldWidthSchema,
ZFieldPageNumberSchema,
ZFieldSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import type { TrpcRouteMeta } from '../../trpc';
export const createEnvelopeFieldsMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/field/create-many',
contentTypes: ['multipart/form-data'],
summary: 'Create envelope fields',
description: 'Create multiple fields for an envelope',
tags: ['Envelope Fields'],
},
};
const ZCreateFieldSchema = ZFieldAndMetaSchema.and(
z.object({
recipientId: z.number().describe('The ID of the recipient to create the field for'),
envelopeItemId: z
.string()
.optional()
.describe(
'The ID of the envelope item to put the field on. If not provided, field will be placed on the first item.',
),
page: ZFieldPageNumberSchema,
positionX: ZClampedFieldPositionXSchema,
positionY: ZClampedFieldPositionYSchema,
width: ZClampedFieldWidthSchema,
height: ZClampedFieldHeightSchema,
}),
);
export const ZCreateEnvelopeFieldsRequestSchema = z.object({
envelopeId: z.string(),
data: ZCreateFieldSchema.array(),
});
export const ZCreateEnvelopeFieldsResponseSchema = z.object({
data: z.array(ZFieldSchema),
});
export type TCreateEnvelopeFieldsRequest = z.infer<typeof ZCreateEnvelopeFieldsRequestSchema>;
export type TCreateEnvelopeFieldsResponse = z.infer<typeof ZCreateEnvelopeFieldsResponseSchema>;
@@ -0,0 +1,118 @@
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteEnvelopeFieldRequestSchema,
ZDeleteEnvelopeFieldResponseSchema,
deleteEnvelopeFieldMeta,
} from './delete-envelope-field.types';
export const deleteEnvelopeFieldRoute = authenticatedProcedure
.meta(deleteEnvelopeFieldMeta)
.input(ZDeleteEnvelopeFieldRequestSchema)
.output(ZDeleteEnvelopeFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx;
const { fieldId } = input;
ctx.logger.info({
input: {
fieldId,
},
});
const unsafeField = await prisma.field.findUnique({
where: {
id: fieldId,
},
select: {
envelopeId: true,
},
});
if (!unsafeField) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field not found',
});
}
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: unsafeField.envelopeId,
},
type: null,
userId: user.id,
teamId,
});
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
recipients: {
include: {
fields: true,
},
},
},
});
const recipientWithFields = envelope?.recipients.find((recipient) =>
recipient.fields.some((field) => field.id === fieldId),
);
const fieldToDelete = recipientWithFields?.fields.find((field) => field.id === fieldId);
if (!envelope || !recipientWithFields || !fieldToDelete) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field not found',
});
}
if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope already complete',
});
}
// Check whether the recipient associated with the field can have new fields created.
if (!canRecipientFieldsBeModified(recipientWithFields, recipientWithFields.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Recipient has already interacted with the document.',
});
}
await prisma.$transaction(async (tx) => {
const deletedField = await tx.field.delete({
where: {
id: fieldToDelete.id,
envelopeId: envelope.id,
},
});
// Handle field deleted audit log.
if (envelope.type === EnvelopeType.DOCUMENT) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
envelopeId: envelope.id,
metadata,
data: {
fieldId: deletedField.secondaryId,
fieldRecipientEmail: recipientWithFields.email,
fieldRecipientId: deletedField.recipientId,
fieldType: deletedField.type,
},
}),
});
}
return deletedField;
});
});
@@ -0,0 +1,22 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../../trpc';
export const deleteEnvelopeFieldMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/field/delete',
summary: 'Delete envelope field',
description: 'Delete an envelope field',
tags: ['Envelope Fields'],
},
};
export const ZDeleteEnvelopeFieldRequestSchema = z.object({
fieldId: z.number(),
});
export const ZDeleteEnvelopeFieldResponseSchema = z.void();
export type TDeleteEnvelopeFieldRequest = z.infer<typeof ZDeleteEnvelopeFieldRequestSchema>;
export type TDeleteEnvelopeFieldResponse = z.infer<typeof ZDeleteEnvelopeFieldResponseSchema>;
@@ -0,0 +1,29 @@
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { authenticatedProcedure } from '../../trpc';
import {
ZGetEnvelopeFieldRequestSchema,
ZGetEnvelopeFieldResponseSchema,
getEnvelopeFieldMeta,
} from './get-envelope-field.types';
export const getEnvelopeFieldRoute = authenticatedProcedure
.meta(getEnvelopeFieldMeta)
.input(ZGetEnvelopeFieldRequestSchema)
.output(ZGetEnvelopeFieldResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { fieldId } = input;
ctx.logger.info({
input: {
fieldId,
},
});
return await getFieldById({
userId: user.id,
teamId,
fieldId,
});
});
@@ -0,0 +1,24 @@
import { z } from 'zod';
import { ZEnvelopeFieldSchema } from '@documenso/lib/types/field';
import type { TrpcRouteMeta } from '../../trpc';
export const getEnvelopeFieldMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/envelope/field/{fieldId}',
summary: 'Get envelope field',
description: 'Returns an envelope field given an ID',
tags: ['Envelope Fields'],
},
};
export const ZGetEnvelopeFieldRequestSchema = z.object({
fieldId: z.number(),
});
export const ZGetEnvelopeFieldResponseSchema = ZEnvelopeFieldSchema;
export type TGetEnvelopeFieldRequest = z.infer<typeof ZGetEnvelopeFieldRequestSchema>;
export type TGetEnvelopeFieldResponse = z.infer<typeof ZGetEnvelopeFieldResponseSchema>;
@@ -0,0 +1,39 @@
import { updateEnvelopeFields } from '@documenso/lib/server-only/field/update-envelope-fields';
import { authenticatedProcedure } from '../../trpc';
import {
ZUpdateEnvelopeFieldsRequestSchema,
ZUpdateEnvelopeFieldsResponseSchema,
updateEnvelopeFieldsMeta,
} from './update-envelope-fields.types';
export const updateEnvelopeFieldsRoute = authenticatedProcedure
.meta(updateEnvelopeFieldsMeta)
.input(ZUpdateEnvelopeFieldsRequestSchema)
.output(ZUpdateEnvelopeFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { envelopeId, data: fields } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const { fields: data } = await updateEnvelopeFields({
userId: user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
type: null,
fields,
requestMetadata: ctx.metadata,
});
return {
data,
};
});
@@ -0,0 +1,52 @@
import { z } from 'zod';
import {
ZClampedFieldHeightSchema,
ZClampedFieldPositionXSchema,
ZClampedFieldPositionYSchema,
ZClampedFieldWidthSchema,
ZFieldPageNumberSchema,
ZFieldSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import type { TrpcRouteMeta } from '../../trpc';
export const updateEnvelopeFieldsMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/field/update-many',
summary: 'Update envelope fields',
description: 'Update multiple envelope fields for an envelope',
tags: ['Envelope Fields'],
},
};
const ZUpdateFieldSchema = ZFieldAndMetaSchema.and(
z.object({
id: z.number().describe('The ID of the field to update.'),
envelopeItemId: z
.string()
.optional()
.describe(
'The ID of the envelope item to put the field on. If not provided, field will be placed on the first item.',
),
page: ZFieldPageNumberSchema.optional(),
positionX: ZClampedFieldPositionXSchema.optional(),
positionY: ZClampedFieldPositionYSchema.optional(),
width: ZClampedFieldWidthSchema.optional(),
height: ZClampedFieldHeightSchema.optional(),
}),
);
export const ZUpdateEnvelopeFieldsRequestSchema = z.object({
envelopeId: z.string(),
data: ZUpdateFieldSchema.array(),
});
export const ZUpdateEnvelopeFieldsResponseSchema = z.object({
data: z.array(ZFieldSchema),
});
export type TUpdateEnvelopeFieldsRequest = z.infer<typeof ZUpdateEnvelopeFieldsRequestSchema>;
export type TUpdateEnvelopeFieldsResponse = z.infer<typeof ZUpdateEnvelopeFieldsResponseSchema>;
@@ -0,0 +1,38 @@
import { createEnvelopeRecipients } from '@documenso/lib/server-only/recipient/create-envelope-recipients';
import { authenticatedProcedure } from '../../trpc';
import {
ZCreateEnvelopeRecipientsRequestSchema,
ZCreateEnvelopeRecipientsResponseSchema,
createEnvelopeRecipientsMeta,
} from './create-envelope-recipients.types';
export const createEnvelopeRecipientsRoute = authenticatedProcedure
.meta(createEnvelopeRecipientsMeta)
.input(ZCreateEnvelopeRecipientsRequestSchema)
.output(ZCreateEnvelopeRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx;
const { envelopeId, data: recipients } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const { recipients: data } = await createEnvelopeRecipients({
userId: user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
recipients,
requestMetadata: metadata,
});
return {
data,
};
});
@@ -0,0 +1,32 @@
import { z } from 'zod';
import { ZEnvelopeRecipientLiteSchema } from '@documenso/lib/types/recipient';
import { ZCreateRecipientSchema } from '../../recipient-router/schema';
import type { TrpcRouteMeta } from '../../trpc';
export const createEnvelopeRecipientsMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/recipient/create-many',
summary: 'Create envelope recipients',
description: 'Create multiple recipients for an envelope',
tags: ['Envelope Recipients'],
},
};
export const ZCreateEnvelopeRecipientsRequestSchema = z.object({
envelopeId: z.string(),
data: ZCreateRecipientSchema.array(),
});
export const ZCreateEnvelopeRecipientsResponseSchema = z.object({
data: ZEnvelopeRecipientLiteSchema.array(),
});
export type TCreateEnvelopeRecipientsRequest = z.infer<
typeof ZCreateEnvelopeRecipientsRequestSchema
>;
export type TCreateEnvelopeRecipientsResponse = z.infer<
typeof ZCreateEnvelopeRecipientsResponseSchema
>;
@@ -0,0 +1,30 @@
import { deleteEnvelopeRecipient } from '@documenso/lib/server-only/recipient/delete-envelope-recipient';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteEnvelopeRecipientRequestSchema,
ZDeleteEnvelopeRecipientResponseSchema,
deleteEnvelopeRecipientMeta,
} from './delete-envelope-recipient.types';
export const deleteEnvelopeRecipientRoute = authenticatedProcedure
.meta(deleteEnvelopeRecipientMeta)
.input(ZDeleteEnvelopeRecipientRequestSchema)
.output(ZDeleteEnvelopeRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx;
const { recipientId } = input;
ctx.logger.info({
input: {
recipientId,
},
});
await deleteEnvelopeRecipient({
userId: user.id,
teamId,
recipientId,
requestMetadata: metadata,
});
});
@@ -0,0 +1,24 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../../trpc';
export const deleteEnvelopeRecipientMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/recipient/delete',
summary: 'Delete envelope recipient',
description: 'Delete an envelope recipient',
tags: ['Envelope Recipients'],
},
};
export const ZDeleteEnvelopeRecipientRequestSchema = z.object({
recipientId: z.number(),
});
export const ZDeleteEnvelopeRecipientResponseSchema = z.void();
export type TDeleteEnvelopeRecipientRequest = z.infer<typeof ZDeleteEnvelopeRecipientRequestSchema>;
export type TDeleteEnvelopeRecipientResponse = z.infer<
typeof ZDeleteEnvelopeRecipientResponseSchema
>;
@@ -0,0 +1,45 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../../trpc';
import {
ZGetEnvelopeRecipientRequestSchema,
ZGetEnvelopeRecipientResponseSchema,
getEnvelopeRecipientMeta,
} from './get-envelope-recipient.types';
export const getEnvelopeRecipientRoute = authenticatedProcedure
.meta(getEnvelopeRecipientMeta)
.input(ZGetEnvelopeRecipientRequestSchema)
.output(ZGetEnvelopeRecipientResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { recipientId } = input;
ctx.logger.info({
input: {
recipientId,
},
});
const recipient = await prisma.recipient.findFirst({
where: {
id: recipientId,
envelope: {
team: buildTeamWhereQuery({ teamId, userId: user.id }),
},
},
include: {
fields: true,
},
});
if (!recipient) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
return recipient;
});
@@ -0,0 +1,24 @@
import { z } from 'zod';
import { ZEnvelopeRecipientSchema } from '@documenso/lib/types/recipient';
import type { TrpcRouteMeta } from '../../trpc';
export const getEnvelopeRecipientMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/envelope/recipient/{recipientId}',
summary: 'Get envelope recipient',
description: 'Returns an envelope recipient given an ID',
tags: ['Envelope Recipients'],
},
};
export const ZGetEnvelopeRecipientRequestSchema = z.object({
recipientId: z.number(),
});
export const ZGetEnvelopeRecipientResponseSchema = ZEnvelopeRecipientSchema;
export type TGetEnvelopeRecipientRequest = z.infer<typeof ZGetEnvelopeRecipientRequestSchema>;
export type TGetEnvelopeRecipientResponse = z.infer<typeof ZGetEnvelopeRecipientResponseSchema>;
@@ -0,0 +1,38 @@
import { updateEnvelopeRecipients } from '@documenso/lib/server-only/recipient/update-envelope-recipients';
import { authenticatedProcedure } from '../../trpc';
import {
ZUpdateEnvelopeRecipientsRequestSchema,
ZUpdateEnvelopeRecipientsResponseSchema,
updateEnvelopeRecipientsMeta,
} from './update-envelope-recipients.types';
export const updateEnvelopeRecipientsRoute = authenticatedProcedure
.meta(updateEnvelopeRecipientsMeta)
.input(ZUpdateEnvelopeRecipientsRequestSchema)
.output(ZUpdateEnvelopeRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { envelopeId, data: recipients } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const { recipients: data } = await updateEnvelopeRecipients({
userId: user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
recipients,
requestMetadata: ctx.metadata,
});
return {
data,
};
});
@@ -0,0 +1,32 @@
import { z } from 'zod';
import { ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
import { ZUpdateRecipientSchema } from '../../recipient-router/schema';
import type { TrpcRouteMeta } from '../../trpc';
export const updateEnvelopeRecipientsMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/recipient/update-many',
summary: 'Update envelope recipients',
description: 'Update multiple recipients for an envelope',
tags: ['Envelope Recipients'],
},
};
export const ZUpdateEnvelopeRecipientsRequestSchema = z.object({
envelopeId: z.string(),
data: ZUpdateRecipientSchema.array(),
});
export const ZUpdateEnvelopeRecipientsResponseSchema = z.object({
data: ZRecipientLiteSchema.array(),
});
export type TUpdateEnvelopeRecipientsRequest = z.infer<
typeof ZUpdateEnvelopeRecipientsRequestSchema
>;
export type TUpdateEnvelopeRecipientsResponse = z.infer<
typeof ZUpdateEnvelopeRecipientsResponseSchema
>;
@@ -34,10 +34,25 @@ export const getEnvelopeItemsByTokenRoute = maybeAuthenticatedProcedure
});
}
return await handleGetEnvelopeItemsByUser({ envelopeId, userId: user.id, teamId });
const { envelopeItems: data } = await handleGetEnvelopeItemsByUser({
envelopeId,
userId: user.id,
teamId,
});
return {
data,
};
}
return await handleGetEnvelopeItemsByToken({ envelopeId, token: access.token });
const { envelopeItems: data } = await handleGetEnvelopeItemsByToken({
envelopeId,
token: access.token,
});
return {
data,
};
});
const handleGetEnvelopeItemsByToken = async ({
@@ -1,6 +1,5 @@
import { z } from 'zod';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
export const ZGetEnvelopeItemsByTokenRequestSchema = z.object({
@@ -17,18 +16,10 @@ export const ZGetEnvelopeItemsByTokenRequestSchema = z.object({
});
export const ZGetEnvelopeItemsByTokenResponseSchema = z.object({
envelopeItems: EnvelopeItemSchema.pick({
data: EnvelopeItemSchema.pick({
id: true,
envelopeId: true,
title: true,
order: true,
})
.extend({
documentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true,
}),
})
.array(),
}).array(),
});
@@ -50,6 +50,6 @@ export const getEnvelopeItemsRoute = authenticatedProcedure
}
return {
envelopeItems: envelope.envelopeItems,
data: envelope.envelopeItems,
};
});
@@ -8,7 +8,7 @@ export const ZGetEnvelopeItemsRequestSchema = z.object({
});
export const ZGetEnvelopeItemsResponseSchema = z.object({
envelopeItems: EnvelopeItemSchema.pick({
data: EnvelopeItemSchema.pick({
id: true,
title: true,
order: true,
@@ -1,10 +1,14 @@
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { authenticatedProcedure } from '../trpc';
import { ZGetEnvelopeRequestSchema, ZGetEnvelopeResponseSchema } from './get-envelope.types';
import {
ZGetEnvelopeRequestSchema,
ZGetEnvelopeResponseSchema,
getEnvelopeMeta,
} from './get-envelope.types';
export const getEnvelopeRoute = authenticatedProcedure
// .meta(getEnvelopeMeta)
.meta(getEnvelopeMeta)
.input(ZGetEnvelopeRequestSchema)
.output(ZGetEnvelopeResponseSchema)
.query(async ({ input, ctx }) => {
@@ -2,15 +2,17 @@ import { z } from 'zod';
import { ZEnvelopeSchema } from '@documenso/lib/types/envelope';
// export const getEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'GET',
// path: '/envelope/{envelopeId}',
// summary: 'Get envelope',
// description: 'Returns a envelope given an ID',
// tags: ['Envelope'],
// },
// };
import type { TrpcRouteMeta } from '../trpc';
export const getEnvelopeMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/envelope/{envelopeId}',
summary: 'Get envelope',
description: 'Returns an envelope given an ID',
tags: ['Envelope'],
},
};
export const ZGetEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
@@ -4,10 +4,11 @@ import { authenticatedProcedure } from '../trpc';
import {
ZRedistributeEnvelopeRequestSchema,
ZRedistributeEnvelopeResponseSchema,
redistributeEnvelopeMeta,
} from './redistribute-envelope.types';
export const redistributeEnvelopeRoute = authenticatedProcedure
// .meta(redistributeEnvelopeMeta)
.meta(redistributeEnvelopeMeta)
.input(ZRedistributeEnvelopeRequestSchema)
.output(ZRedistributeEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
@@ -1,15 +1,17 @@
import { z } from 'zod';
// export const redistributeEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/redistribute',
// summary: 'Redistribute document',
// description:
// 'Redistribute the document to the provided recipients who have not actioned the document. Will use the distribution method set in the document',
// tags: ['Envelope'],
// },
// };
import type { TrpcRouteMeta } from '../trpc';
export const redistributeEnvelopeMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/redistribute',
summary: 'Redistribute envelope',
description:
'Redistribute the envelope to the provided recipients who have not actioned the envelope. Will use the distribution method set in the envelope',
tags: ['Envelope'],
},
};
export const ZRedistributeEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
+36 -15
View File
@@ -9,6 +9,14 @@ import { deleteEnvelopeRoute } from './delete-envelope';
import { deleteEnvelopeItemRoute } from './delete-envelope-item';
import { distributeEnvelopeRoute } from './distribute-envelope';
import { duplicateEnvelopeRoute } from './duplicate-envelope';
import { createEnvelopeFieldsRoute } from './envelope-fields/create-envelope-fields';
import { deleteEnvelopeFieldRoute } from './envelope-fields/delete-envelope-field';
import { getEnvelopeFieldRoute } from './envelope-fields/get-envelope-field';
import { updateEnvelopeFieldsRoute } from './envelope-fields/update-envelope-fields';
import { createEnvelopeRecipientsRoute } from './envelope-recipients/create-envelope-recipients';
import { deleteEnvelopeRecipientRoute } from './envelope-recipients/delete-envelope-recipient';
import { getEnvelopeRecipientRoute } from './envelope-recipients/get-envelope-recipient';
import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-envelope-recipients';
import { getEnvelopeRoute } from './get-envelope';
import { getEnvelopeItemsRoute } from './get-envelope-items';
import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token';
@@ -18,17 +26,20 @@ import { setEnvelopeRecipientsRoute } from './set-envelope-recipients';
import { signEnvelopeFieldRoute } from './sign-envelope-field';
import { updateEnvelopeRoute } from './update-envelope';
import { updateEnvelopeItemsRoute } from './update-envelope-items';
import { useEnvelopeRoute } from './use-envelope';
/**
* Note: The order of the routes is important for public API routes.
*
* Example: GET /envelope/attachment must appear before GET /envelope/:id
*/
export const envelopeRouter = router({
get: getEnvelopeRoute,
create: createEnvelopeRoute,
update: updateEnvelopeRoute,
delete: deleteEnvelopeRoute,
duplicate: duplicateEnvelopeRoute,
distribute: distributeEnvelopeRoute,
redistribute: redistributeEnvelopeRoute,
// share: shareEnvelopeRoute,
attachment: {
find: findAttachmentsRoute,
create: createAttachmentRoute,
update: updateAttachmentRoute,
delete: deleteAttachmentRoute,
},
item: {
getMany: getEnvelopeItemsRoute,
getManyByToken: getEnvelopeItemsByTokenRoute,
@@ -37,16 +48,26 @@ export const envelopeRouter = router({
delete: deleteEnvelopeItemRoute,
},
recipient: {
get: getEnvelopeRecipientRoute,
createMany: createEnvelopeRecipientsRoute,
updateMany: updateEnvelopeRecipientsRoute,
delete: deleteEnvelopeRecipientRoute,
set: setEnvelopeRecipientsRoute,
},
field: {
get: getEnvelopeFieldRoute,
createMany: createEnvelopeFieldsRoute,
updateMany: updateEnvelopeFieldsRoute,
delete: deleteEnvelopeFieldRoute,
set: setEnvelopeFieldsRoute,
sign: signEnvelopeFieldRoute,
},
attachment: {
find: findAttachmentsRoute,
create: createAttachmentRoute,
update: updateAttachmentRoute,
delete: deleteAttachmentRoute,
},
get: getEnvelopeRoute,
create: createEnvelopeRoute,
use: useEnvelopeRoute,
update: updateEnvelopeRoute,
delete: deleteEnvelopeRoute,
duplicate: duplicateEnvelopeRoute,
distribute: distributeEnvelopeRoute,
redistribute: redistributeEnvelopeRoute,
});
@@ -65,7 +65,7 @@ export const setEnvelopeFieldsRoute = authenticatedProcedure
.exhaustive();
return {
fields: result.fields.map((field) => ({
data: result.fields.map((field) => ({
id: field.id,
formId: field.formId,
})),
@@ -1,12 +1,19 @@
import { EnvelopeType, FieldType } from '@prisma/client';
import { z } from 'zod';
import {
ZClampedFieldHeightSchema,
ZClampedFieldPositionXSchema,
ZClampedFieldPositionYSchema,
ZClampedFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
export const ZSetEnvelopeFieldsRequestSchema = z.object({
envelopeId: z.string(),
envelopeType: z.nativeEnum(EnvelopeType),
fields: z.array(
// Todo: Envelopes - Use strict schema for types + field meta.
z.object({
id: z
.number()
@@ -20,34 +27,17 @@ export const ZSetEnvelopeFieldsRequestSchema = z.object({
.number()
.min(1)
.describe('The page number of the field on the envelope. Starts from 1.'),
// Todo: Envelopes - Extract these 0-100 schemas with better descriptions.
positionX: z
.number()
.min(0)
.max(100)
.describe('The percentage based X position of the field on the envelope.'),
positionY: z
.number()
.min(0)
.max(100)
.describe('The percentage based Y position of the field on the envelope.'),
width: z
.number()
.min(0)
.max(100)
.describe('The percentage based width of the field on the envelope.'),
height: z
.number()
.min(0)
.max(100)
.describe('The percentage based height of the field on the envelope.'),
fieldMeta: ZFieldMetaSchema, // Todo: Envelopes - Use a more strict form?
positionX: ZClampedFieldPositionXSchema,
positionY: ZClampedFieldPositionYSchema,
width: ZClampedFieldWidthSchema,
height: ZClampedFieldHeightSchema,
fieldMeta: ZFieldMetaSchema,
}),
),
});
export const ZSetEnvelopeFieldsResponseSchema = z.object({
fields: z
data: z
.object({
id: z.number(),
formId: z.string().optional(),
@@ -23,7 +23,7 @@ export const setEnvelopeRecipientsRoute = authenticatedProcedure
},
});
return await match(envelopeType)
const { recipients: data } = await match(envelopeType)
.with(EnvelopeType.DOCUMENT, async () =>
setDocumentRecipients({
userId: ctx.user.id,
@@ -48,4 +48,8 @@ export const setEnvelopeRecipientsRoute = authenticatedProcedure
}),
)
.exhaustive();
return {
data,
};
});
@@ -20,7 +20,7 @@ export const ZSetEnvelopeRecipientsRequestSchema = z.object({
});
export const ZSetEnvelopeRecipientsResponseSchema = z.object({
recipients: ZRecipientLiteSchema.omit({
data: ZRecipientLiteSchema.omit({
documentId: true,
templateId: true,
}).array(),
@@ -133,6 +133,49 @@ export const signEnvelopeFieldRoute = procedure
const insertionValues = extractFieldInsertionValues({ fieldValue, field, documentMeta });
// Early return for uninserting fields.
if (!insertionValues.inserted) {
return await prisma.$transaction(async (tx) => {
const updatedField = await tx.field.update({
where: {
id: field.id,
},
data: {
customText: '',
inserted: false,
},
});
await tx.signature.deleteMany({
where: {
fieldId: field.id,
},
});
if (recipient.role !== RecipientRole.ASSISTANT) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
envelopeId: envelope.id,
user: {
name: recipient.name,
email: recipient.email,
},
requestMetadata: metadata.requestMetadata,
data: {
field: field.type,
fieldId: field.secondaryId,
},
}),
});
}
return {
signedField: updatedField,
};
});
}
const derivedRecipientActionAuth = await validateFieldAuth({
documentAuthOptions: envelope.authOptions,
recipient,
@@ -7,9 +7,11 @@ import { authenticatedProcedure } from '../trpc';
import {
ZUpdateEnvelopeItemsRequestSchema,
ZUpdateEnvelopeItemsResponseSchema,
updateEnvelopeItemsMeta,
} from './update-envelope-items.types';
export const updateEnvelopeItemsRoute = authenticatedProcedure
.meta(updateEnvelopeItemsMeta)
.input(ZUpdateEnvelopeItemsRequestSchema)
.output(ZUpdateEnvelopeItemsResponseSchema)
.mutation(async ({ input, ctx }) => {
@@ -93,6 +95,6 @@ export const updateEnvelopeItemsRoute = authenticatedProcedure
// Todo: Envelope [AUDIT_LOGS]
return {
updatedEnvelopeItems,
data: updatedEnvelopeItems,
};
});
@@ -3,6 +3,17 @@ import { z } from 'zod';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import { ZDocumentTitleSchema } from '../document-router/schema';
import type { TrpcRouteMeta } from '../trpc';
export const updateEnvelopeItemsMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/item/update-many',
summary: 'Update envelope items',
description: 'Update multiple envelope items for an envelope',
tags: ['Envelope Items'],
},
};
export const ZUpdateEnvelopeItemsRequestSchema = z.object({
envelopeId: z.string(),
@@ -17,7 +28,7 @@ export const ZUpdateEnvelopeItemsRequestSchema = z.object({
});
export const ZUpdateEnvelopeItemsResponseSchema = z.object({
updatedEnvelopeItems: EnvelopeItemSchema.pick({
data: EnvelopeItemSchema.pick({
id: true,
order: true,
title: true,
@@ -4,10 +4,11 @@ import { authenticatedProcedure } from '../trpc';
import {
ZUpdateEnvelopeRequestSchema,
ZUpdateEnvelopeResponseSchema,
updateEnvelopeMeta,
} from './update-envelope.types';
export const updateEnvelopeRoute = authenticatedProcedure
// .meta(updateEnvelopeTrpcMeta)
.meta(updateEnvelopeMeta)
.input(ZUpdateEnvelopeRequestSchema)
.output(ZUpdateEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
@@ -1,5 +1,3 @@
import { EnvelopeType } from '@prisma/client';
// import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import {
@@ -14,19 +12,19 @@ import {
ZDocumentTitleSchema,
ZDocumentVisibilitySchema,
} from '../document-router/schema';
import type { TrpcRouteMeta } from '../trpc';
// export const updateEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/update',
// summary: 'Update envelope',
// tags: ['Envelope'],
// },
// };
export const updateEnvelopeMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/update',
summary: 'Update envelope',
tags: ['Envelope'],
},
};
export const ZUpdateEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
envelopeType: z.nativeEnum(EnvelopeType),
data: z
.object({
title: ZDocumentTitleSchema.optional(),
@@ -0,0 +1,170 @@
import { EnvelopeType } from '@prisma/client';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { authenticatedProcedure } from '../trpc';
import {
ZUseEnvelopeRequestSchema,
ZUseEnvelopeResponseSchema,
useEnvelopeMeta,
} from './use-envelope.types';
export const useEnvelopeRoute = authenticatedProcedure
.meta(useEnvelopeMeta)
.input(ZUseEnvelopeRequestSchema)
.output(ZUseEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { payload, files = [] } = input;
const {
envelopeId,
externalId,
recipients,
distributeDocument,
customDocumentData = [],
folderId,
prefillFields,
override,
attachments,
} = payload;
ctx.logger.info({
input: {
envelopeId,
folderId,
},
});
const limits = await getServerLimits({ userId: user.id, teamId });
if (limits.remaining.documents === 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your document limit.',
});
}
// Verify the template exists and get envelope items
const envelope = await getEnvelopeById({
id: {
type: 'envelopeId',
id: envelopeId,
},
type: EnvelopeType.TEMPLATE,
userId: user.id,
teamId,
});
if (files.length > envelope.envelopeItems.length) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `You cannot upload more than ${envelope.envelopeItems.length} envelope items per envelope`,
});
}
const filesToUpload = files.filter(
(file, index) =>
payload.customDocumentData &&
payload.customDocumentData.some(
(mapping) => mapping.identifier === file.name || mapping.identifier === index,
),
);
// Process uploaded files and create document data for them
const uploadedFiles = await Promise.all(
filesToUpload.map(async (file) => {
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
return {
name: file.name,
documentDataId,
};
}),
);
// Map custom document data using identifiers
const customDocumentDataMapped = customDocumentData?.map((mapping) => {
let documentDataId: string | undefined;
// Find the uploaded file by identifier
if (typeof mapping.identifier === 'string') {
documentDataId = uploadedFiles.find(
(file) => file.name === mapping.identifier,
)?.documentDataId;
}
if (typeof mapping.identifier === 'number') {
documentDataId = uploadedFiles.at(mapping.identifier)?.documentDataId;
}
if (mapping.identifier === undefined) {
documentDataId = uploadedFiles.at(0)?.documentDataId;
}
if (!documentDataId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `File with identifier "${mapping.identifier}" not found in uploaded files`,
});
}
// Verify that the envelopeItemId exists in the template
const envelopeItem = envelope.envelopeItems.find(
(item) => item.id === mapping.envelopeItemId,
);
if (!envelopeItem) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Envelope item with ID "${mapping.envelopeItemId}" not found in template`,
});
}
return {
documentDataId,
envelopeItemId: mapping.envelopeItemId,
};
});
// Create document from template
const createdEnvelope = await createDocumentFromTemplate({
id: {
type: 'envelopeId',
id: envelopeId,
},
externalId,
teamId,
userId: user.id,
recipients,
customDocumentData: customDocumentDataMapped,
requestMetadata: ctx.metadata,
folderId,
prefillFields,
override,
attachments,
});
// Distribute document if requested
if (distributeDocument) {
await sendDocument({
id: {
type: 'envelopeId',
id: createdEnvelope.id,
},
userId: user.id,
teamId,
requestMetadata: ctx.metadata,
}).catch((err) => {
console.error(err);
throw new AppError('DOCUMENT_SEND_FAILED');
});
}
return {
id: createdEnvelope.id,
};
});
@@ -0,0 +1,121 @@
import { z } from 'zod';
import { zfd } from 'zod-form-data';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
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 { zodFormData } from '../../utils/zod-form-data';
import type { TrpcRouteMeta } from '../trpc';
export const useEnvelopeMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/use',
contentTypes: ['multipart/form-data'],
summary: 'Use envelope',
description:
'Create a document envelope from a template envelope. Upload custom files to replace the template PDFs and map them to specific envelope items using identifiers.',
tags: ['Envelope'],
},
};
export const ZUseEnvelopePayloadSchema = z.object({
envelopeId: z.string().describe('The ID of the template envelope to use.'),
externalId: z.string().optional().describe('External ID for the created document.'),
recipients: z
.array(
z.object({
id: z.number().describe('The ID of the recipient in the template.'),
email: z.string().email().max(254),
name: z.string().max(255).optional(),
signingOrder: z.number().optional(),
}),
)
.describe('The information of the recipients to create the document with.'),
distributeDocument: z
.boolean()
.describe('Whether to create the document as pending and distribute it to recipients.')
.optional(),
customDocumentData: z
.array(
z.object({
identifier: z
.union([z.string(), z.number()])
.describe(
'Either the filename or the index of the file that was uploaded. This maps to which envelope item in the template should use this file.',
),
envelopeItemId: z
.string()
.describe('The envelope item ID from the template to replace with the uploaded file.'),
}),
)
.describe(
'Map uploaded files to specific envelope items in the template. If not provided, files will be ignored.',
)
.optional(),
folderId: z
.string()
.describe(
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
)
.optional(),
prefillFields: z
.array(ZFieldMetaPrefillFieldsSchema)
.describe(
'The fields to prefill on the document before sending it out. Useful when you want to create a document from an existing template and pre-fill the fields with specific values.',
)
.optional(),
override: z
.object({
title: z.string().min(1).max(255).optional(),
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
allowDictateNextSigner: z.boolean().optional(),
})
.describe('Override values from the template for the created document.')
.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 ZUseEnvelopeRequestSchema = zodFormData({
payload: zfd.json(ZUseEnvelopePayloadSchema),
files: zfd.repeatableOfType(zfd.file()).optional(),
});
export const ZUseEnvelopeResponseSchema = z.object({
id: z.string().describe('The ID of the created envelope.'),
});
export type TUseEnvelopePayload = z.infer<typeof ZUseEnvelopePayloadSchema>;
export type TUseEnvelopeRequest = z.infer<typeof ZUseEnvelopeRequestSchema>;
export type TUseEnvelopeResponse = z.infer<typeof ZUseEnvelopeResponseSchema>;