mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 18:04:55 +10:00
feat: add embedded envelopes (#2564)
## Description Add envelopes V2 embedded support --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
import { router } from '../trpc';
|
||||
import { createEmbeddingDocumentRoute } from './create-embedding-document';
|
||||
import { createEmbeddingEnvelopeRoute } from './create-embedding-envelope';
|
||||
import { createEmbeddingPresignTokenRoute } from './create-embedding-presign-token';
|
||||
import { createEmbeddingTemplateRoute } from './create-embedding-template';
|
||||
import { getMultiSignDocumentRoute } from './get-multi-sign-document';
|
||||
import { updateEmbeddingDocumentRoute } from './update-embedding-document';
|
||||
import { updateEmbeddingEnvelopeRoute } from './update-embedding-envelope';
|
||||
import { updateEmbeddingTemplateRoute } from './update-embedding-template';
|
||||
import { verifyEmbeddingPresignTokenRoute } from './verify-embedding-presign-token';
|
||||
|
||||
export const embeddingPresignRouter = router({
|
||||
createEmbeddingPresignToken: createEmbeddingPresignTokenRoute,
|
||||
verifyEmbeddingPresignToken: verifyEmbeddingPresignTokenRoute,
|
||||
createEmbeddingEnvelope: createEmbeddingEnvelopeRoute,
|
||||
createEmbeddingDocument: createEmbeddingDocumentRoute,
|
||||
createEmbeddingTemplate: createEmbeddingTemplateRoute,
|
||||
updateEmbeddingEnvelope: updateEmbeddingEnvelopeRoute,
|
||||
updateEmbeddingDocument: updateEmbeddingDocumentRoute,
|
||||
updateEmbeddingTemplate: updateEmbeddingTemplateRoute,
|
||||
// applyMultiSignSignature: applyMultiSignSignatureRoute,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
|
||||
import { createEnvelopeRouteCaller } from '../envelope-router/create-envelope';
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
ZCreateEmbeddingEnvelopeRequestSchema,
|
||||
ZCreateEmbeddingEnvelopeResponseSchema,
|
||||
} from './create-embedding-envelope.types';
|
||||
|
||||
export const createEmbeddingEnvelopeRoute = procedure
|
||||
.input(ZCreateEmbeddingEnvelopeRequestSchema)
|
||||
.output(ZCreateEmbeddingEnvelopeResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { req } = ctx;
|
||||
|
||||
const authorizationHeader = req.headers.get('authorization');
|
||||
|
||||
const [presignToken] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
|
||||
|
||||
if (!presignToken) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'No presign token provided',
|
||||
});
|
||||
}
|
||||
|
||||
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
||||
|
||||
const { userId, teamId } = apiToken;
|
||||
|
||||
return await createEnvelopeRouteCaller({
|
||||
userId,
|
||||
teamId,
|
||||
input,
|
||||
options: {
|
||||
// Default recipients should be added on the frontend automatically for embeds.
|
||||
bypassDefaultRecipients: true,
|
||||
},
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import {
|
||||
ZCreateEnvelopeRequestSchema,
|
||||
ZCreateEnvelopeResponseSchema,
|
||||
} from '../envelope-router/create-envelope.types';
|
||||
|
||||
export const ZCreateEmbeddingEnvelopeRequestSchema = ZCreateEnvelopeRequestSchema;
|
||||
|
||||
export const ZCreateEmbeddingEnvelopeResponseSchema = ZCreateEnvelopeResponseSchema;
|
||||
@@ -24,7 +24,9 @@ export const ZCreateEmbeddingPresignTokenRequestSchema = z.object({
|
||||
scope: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Resource restriction. Example: documentId:1, templateId:2'),
|
||||
.describe(
|
||||
'Resource restriction. V1 embeds only support documentId:1, templateId:2. V2 embeds only support envelopeId:envelope_123',
|
||||
),
|
||||
});
|
||||
|
||||
export const ZCreateEmbeddingPresignTokenResponseSchema = z.object({
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import pMap from 'p-map';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import { UNSAFE_createEnvelopeItems } from '@documenso/lib/server-only/envelope-item/create-envelope-items';
|
||||
import { UNSAFE_deleteEnvelopeItem } from '@documenso/lib/server-only/envelope-item/delete-envelope-item';
|
||||
import { UNSAFE_updateEnvelopeItems } from '@documenso/lib/server-only/envelope-item/update-envelope-items';
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope';
|
||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
||||
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
|
||||
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
|
||||
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { PRESIGNED_ENVELOPE_ITEM_ID_PREFIX } from '@documenso/lib/utils/embed-config';
|
||||
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateEmbeddingEnvelopeRequestSchema,
|
||||
ZUpdateEmbeddingEnvelopeResponseSchema,
|
||||
} from './update-embedding-envelope.types';
|
||||
|
||||
export const updateEmbeddingEnvelopeRoute = procedure
|
||||
.input(ZUpdateEmbeddingEnvelopeRequestSchema)
|
||||
.output(ZUpdateEmbeddingEnvelopeResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { payload, files } = input;
|
||||
const { envelopeId, data, meta } = payload;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
envelopeId,
|
||||
},
|
||||
});
|
||||
|
||||
const authorizationHeader = ctx.req.headers.get('authorization');
|
||||
|
||||
const [presignToken] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
|
||||
|
||||
if (!presignToken) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'No presign token provided',
|
||||
});
|
||||
}
|
||||
|
||||
const apiToken = await verifyEmbeddingPresignToken({
|
||||
token: presignToken,
|
||||
scope: `envelopeId:${envelopeId}`,
|
||||
});
|
||||
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
type: null, // Allow updating both documents and templates.
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
envelopeItems: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
envelopeAttachments: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.status === DocumentStatus.COMPLETED) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Cannot modify completed envelope',
|
||||
});
|
||||
}
|
||||
|
||||
// Step 1: Update the envelope items.
|
||||
const envelopeItemsToUpdate: EnvelopeItemUpdateOptions[] = [];
|
||||
const envelopeItemsToCreate: EnvelopeItemCreateOptions[] = [];
|
||||
|
||||
// Sort and group envelope items to update and create.
|
||||
data.envelopeItems.forEach((item) => {
|
||||
const isNewEnvelopeItem = item.id.startsWith(PRESIGNED_ENVELOPE_ITEM_ID_PREFIX);
|
||||
|
||||
// Handle existing envelope items.
|
||||
if (!isNewEnvelopeItem) {
|
||||
const envelopeItem = envelope.envelopeItems.find(
|
||||
(envelopeItem) => envelopeItem.id === item.id,
|
||||
);
|
||||
|
||||
if (!envelopeItem) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope item not found',
|
||||
});
|
||||
}
|
||||
|
||||
const hasEnvelopeItemChanged =
|
||||
envelopeItem.title !== item.title || envelopeItem.order !== item.order;
|
||||
|
||||
if (hasEnvelopeItemChanged) {
|
||||
envelopeItemsToUpdate.push({
|
||||
envelopeItemId: envelopeItem.id,
|
||||
title: item.title,
|
||||
order: item.order,
|
||||
});
|
||||
}
|
||||
|
||||
// Return to continue loop.
|
||||
return;
|
||||
}
|
||||
|
||||
const newEnvelopeItemFile = item.index !== undefined ? files[item.index] : undefined;
|
||||
|
||||
if (!newEnvelopeItemFile) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid envelope item index',
|
||||
});
|
||||
}
|
||||
|
||||
// Handle not yet uploaded envelope items.
|
||||
envelopeItemsToCreate.push({
|
||||
embeddedEnvelopeItemId: item.id,
|
||||
title: item.title,
|
||||
order: item.order,
|
||||
file: newEnvelopeItemFile,
|
||||
});
|
||||
});
|
||||
|
||||
// Delete envelope items that have been removed from the payload.
|
||||
const envelopeItemIdsToDelete = envelope.envelopeItems
|
||||
.filter((item) => !data.envelopeItems.some((i) => i.id === item.id))
|
||||
.map((item) => item.id);
|
||||
|
||||
const willEnvelopeItemsBeModified =
|
||||
envelopeItemIdsToDelete.length > 0 ||
|
||||
envelopeItemsToCreate.length > 0 ||
|
||||
envelopeItemsToUpdate.length > 0;
|
||||
|
||||
const organisationClaim = envelope.team.organisation.organisationClaim;
|
||||
const resultingEnvelopeItemCount =
|
||||
envelope.envelopeItems.length - envelopeItemIdsToDelete.length + envelopeItemsToCreate.length;
|
||||
|
||||
if (resultingEnvelopeItemCount > organisationClaim.envelopeItemCount) {
|
||||
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
|
||||
message: `You cannot upload more than ${organisationClaim.envelopeItemCount} envelope items`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// Should be safe to use stale envelope.recipients since only signed or sent
|
||||
// recipients affect the outcome.
|
||||
if (willEnvelopeItemsBeModified && !canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item is not editable',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelopeItemIdsToDelete.length > 0) {
|
||||
await pMap(
|
||||
envelopeItemIdsToDelete,
|
||||
async (envelopeItemId) => {
|
||||
await UNSAFE_deleteEnvelopeItem({
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId,
|
||||
user: apiToken.user,
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
},
|
||||
{ concurrency: 2 },
|
||||
);
|
||||
}
|
||||
|
||||
// Mapping for the client side embedded prefix envelope item IDs to the real envelope item IDs.
|
||||
const embeddedEnvelopeItemIdMapping: Record<string, string> = {};
|
||||
|
||||
// Create new envelope items.
|
||||
if (envelopeItemsToCreate.length > 0) {
|
||||
const createdEnvelopeItems = await UNSAFE_createEnvelopeItems({
|
||||
files: envelopeItemsToCreate.map((item) => ({
|
||||
clientId: item.embeddedEnvelopeItemId,
|
||||
file: item.file,
|
||||
orderOverride: item.order,
|
||||
})),
|
||||
envelope: {
|
||||
...envelope,
|
||||
// Purposefully putting empty recipients here since placeholders should automatically injected on the client side for
|
||||
// embedded purposes. Todo: Embeds - (Not implemeneted yet)
|
||||
recipients: [],
|
||||
},
|
||||
user: {
|
||||
id: apiToken.user.id,
|
||||
name: apiToken.user.name,
|
||||
email: apiToken.user.email,
|
||||
},
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
// Build the map from the envelope item order.
|
||||
createdEnvelopeItems.forEach((item) => {
|
||||
if (!item.clientId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Client ID not found',
|
||||
});
|
||||
}
|
||||
|
||||
embeddedEnvelopeItemIdMapping[item.clientId] = item.id;
|
||||
});
|
||||
}
|
||||
|
||||
if (envelopeItemsToUpdate.length > 0) {
|
||||
await UNSAFE_updateEnvelopeItems({
|
||||
envelopeId: envelope.id,
|
||||
data: envelopeItemsToUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
// Step 2: Update the general envelope data and meta.
|
||||
await updateEnvelope({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility,
|
||||
globalAccessAuth: data.globalAccessAuth,
|
||||
globalActionAuth: data.globalActionAuth,
|
||||
folderId: data.folderId,
|
||||
},
|
||||
meta,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
// Step 3: Update the recipients
|
||||
const recipientsWithClientId = data.recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
clientId: nanoid(),
|
||||
}));
|
||||
|
||||
const { recipients: updatedRecipients } = await match(envelope.type)
|
||||
.with(EnvelopeType.DOCUMENT, async () =>
|
||||
setDocumentRecipients({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelope.id,
|
||||
},
|
||||
recipients: recipientsWithClientId.map((recipient) => ({
|
||||
id: recipient.id,
|
||||
clientId: recipient.clientId,
|
||||
email: recipient.email,
|
||||
name: recipient.name ?? '',
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
actionAuth: recipient.actionAuth,
|
||||
})),
|
||||
requestMetadata: ctx.metadata,
|
||||
}),
|
||||
)
|
||||
.with(EnvelopeType.TEMPLATE, async () =>
|
||||
setTemplateRecipients({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelope.id,
|
||||
},
|
||||
recipients: recipientsWithClientId.map((recipient) => ({
|
||||
id: recipient.id,
|
||||
clientId: recipient.clientId,
|
||||
email: recipient.email,
|
||||
name: recipient.name ?? '',
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
actionAuth: recipient.actionAuth,
|
||||
})),
|
||||
}),
|
||||
)
|
||||
.exhaustive();
|
||||
|
||||
// Step 4: Update the fields.
|
||||
const fields = recipientsWithClientId.flatMap((recipient) => {
|
||||
const recipientId = updatedRecipients.find((r) => r.clientId === recipient.clientId)?.id;
|
||||
|
||||
if (!recipientId) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
return (recipient.fields ?? []).map((field) => {
|
||||
let envelopeItemId = field.envelopeItemId;
|
||||
|
||||
if (envelopeItemId.startsWith(PRESIGNED_ENVELOPE_ITEM_ID_PREFIX)) {
|
||||
envelopeItemId = embeddedEnvelopeItemIdMapping[envelopeItemId];
|
||||
}
|
||||
|
||||
if (!envelopeItemId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope item not found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
recipientId,
|
||||
envelopeItemId,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
await match(envelope.type)
|
||||
.with(EnvelopeType.DOCUMENT, async () =>
|
||||
setFieldsForDocument({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
fields: fields.map((field) => ({
|
||||
...field,
|
||||
pageNumber: field.page,
|
||||
pageX: field.positionX,
|
||||
pageY: field.positionY,
|
||||
pageWidth: field.width,
|
||||
pageHeight: field.height,
|
||||
})),
|
||||
requestMetadata: ctx.metadata,
|
||||
}),
|
||||
)
|
||||
.with(EnvelopeType.TEMPLATE, async () =>
|
||||
setFieldsForTemplate({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
fields: fields.map((field) => ({
|
||||
...field,
|
||||
pageNumber: field.page,
|
||||
pageX: field.positionX,
|
||||
pageY: field.positionY,
|
||||
pageWidth: field.width,
|
||||
pageHeight: field.height,
|
||||
})),
|
||||
}),
|
||||
)
|
||||
.exhaustive();
|
||||
|
||||
// Step 5: Handle attachments (set semantics: delete all existing, create new).
|
||||
let hasEnvelopeAttachmentsChanged =
|
||||
envelope.envelopeAttachments.length !== data.attachments.length;
|
||||
|
||||
data.attachments.forEach((attachment) => {
|
||||
const foundAttachment = envelope.envelopeAttachments.find((a) => a.id === attachment.id);
|
||||
|
||||
if (!foundAttachment) {
|
||||
hasEnvelopeAttachmentsChanged = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const hasAttachmentChanged =
|
||||
foundAttachment.label !== attachment.label ||
|
||||
foundAttachment.data !== attachment.data ||
|
||||
foundAttachment.type !== attachment.type;
|
||||
|
||||
if (hasAttachmentChanged) {
|
||||
hasEnvelopeAttachmentsChanged = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasEnvelopeAttachmentsChanged) {
|
||||
await prisma.envelopeAttachment.deleteMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.attachments.length > 0) {
|
||||
await prisma.envelopeAttachment.createMany({
|
||||
data: data.attachments.map((attachment) => ({
|
||||
envelopeId: envelope.id,
|
||||
label: attachment.label,
|
||||
data: attachment.data,
|
||||
type: attachment.type,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type EnvelopeItemUpdateOptions = {
|
||||
envelopeItemId: string;
|
||||
title?: string;
|
||||
order?: number;
|
||||
};
|
||||
|
||||
type EnvelopeItemCreateOptions = {
|
||||
embeddedEnvelopeItemId: string;
|
||||
title: string;
|
||||
order: number;
|
||||
file: File;
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import { z } from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta';
|
||||
import {
|
||||
ZClampedFieldHeightSchema,
|
||||
ZClampedFieldPositionXSchema,
|
||||
ZClampedFieldPositionYSchema,
|
||||
ZClampedFieldWidthSchema,
|
||||
ZFieldPageNumberSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { EnvelopeAttachmentSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeAttachmentSchema';
|
||||
import { ZSetEnvelopeRecipientSchema } from '@documenso/trpc/server/envelope-router/set-envelope-recipients.types';
|
||||
|
||||
import { zodFormData } from '../../utils/zod-form-data';
|
||||
import {
|
||||
ZDocumentExternalIdSchema,
|
||||
ZDocumentTitleSchema,
|
||||
ZDocumentVisibilitySchema,
|
||||
} from '../document-router/schema';
|
||||
|
||||
export const ZUpdateEmbeddingEnvelopePayloadSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
data: z.object({
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
folderId: z.string().nullish(),
|
||||
|
||||
/**
|
||||
* The list of envelope items that are part of the envelope.
|
||||
*
|
||||
* Any missing IDs will be treated as deleting the envelope item.
|
||||
*/
|
||||
envelopeItems: z
|
||||
.object({
|
||||
/**
|
||||
* This is not necesssarily a real id, it can be a temporary id for the envelope item.
|
||||
*/
|
||||
id: z.string(),
|
||||
|
||||
/**
|
||||
* The title of the envelope item.
|
||||
*/
|
||||
title: z.string(),
|
||||
|
||||
/**
|
||||
* The order of the envelope item in the envelope.
|
||||
*/
|
||||
order: z.number().int().min(0),
|
||||
|
||||
/**
|
||||
* The file index for items that are not yet uploaded.
|
||||
*/
|
||||
index: z.number().int().min(0).optional(),
|
||||
})
|
||||
.array(),
|
||||
|
||||
/**
|
||||
* This is a set command.
|
||||
*/
|
||||
recipients: ZSetEnvelopeRecipientSchema.extend({
|
||||
fields: ZEnvelopeFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
page: ZFieldPageNumberSchema,
|
||||
positionX: ZClampedFieldPositionXSchema,
|
||||
positionY: ZClampedFieldPositionYSchema,
|
||||
width: ZClampedFieldWidthSchema,
|
||||
height: ZClampedFieldHeightSchema,
|
||||
envelopeItemId: z.string(),
|
||||
}),
|
||||
).array(),
|
||||
}).array(),
|
||||
|
||||
/**
|
||||
* The list of attachments for the envelope.
|
||||
*
|
||||
* This is a set command: when provided, all existing attachments are deleted
|
||||
* and replaced with the provided list.
|
||||
*/
|
||||
attachments: EnvelopeAttachmentSchema.pick({
|
||||
type: true,
|
||||
label: true,
|
||||
data: true,
|
||||
})
|
||||
.extend({
|
||||
id: z.string().optional(),
|
||||
})
|
||||
.array(),
|
||||
}),
|
||||
|
||||
meta: ZDocumentMetaUpdateSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateEmbeddingEnvelopeRequestSchema = zodFormData({
|
||||
payload: zfd.json(ZUpdateEmbeddingEnvelopePayloadSchema),
|
||||
files: zfd.repeatableOfType(zfd.file()),
|
||||
});
|
||||
|
||||
export const ZUpdateEmbeddingEnvelopeResponseSchema = z.void();
|
||||
|
||||
export type TUpdateEmbeddingEnvelopePayload = z.infer<typeof ZUpdateEmbeddingEnvelopePayloadSchema>;
|
||||
export type TUpdateEmbeddingEnvelopeRequest = z.infer<typeof ZUpdateEmbeddingEnvelopeRequestSchema>;
|
||||
@@ -1,16 +1,6 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { UNSAFE_createEnvelopeItems } from '@documenso/lib/server-only/envelope-item/create-envelope-items';
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import {
|
||||
convertPlaceholdersToFieldInputs,
|
||||
extractPdfPlaceholders,
|
||||
} from '@documenso/lib/server-only/pdf/auto-place-fields';
|
||||
import { findRecipientByPlaceholder } from '@documenso/lib/server-only/pdf/helpers';
|
||||
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
|
||||
import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import { prefixedId } from '@documenso/lib/universal/id';
|
||||
import { putPdfFileServerSide } 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';
|
||||
|
||||
@@ -91,130 +81,17 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
// For each file: normalize, extract & clean placeholders, then upload.
|
||||
const envelopeItems = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
let buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
if (envelope.formValues) {
|
||||
buffer = await insertFormValuesInPdf({ pdf: buffer, formValues: envelope.formValues });
|
||||
}
|
||||
|
||||
const normalized = await normalizePdf(buffer, {
|
||||
flattenForm: envelope.type !== 'TEMPLATE',
|
||||
});
|
||||
|
||||
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
|
||||
|
||||
const { id: documentDataId } = await putPdfFileServerSide({
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(cleanedPdf),
|
||||
});
|
||||
|
||||
return {
|
||||
title: file.name,
|
||||
documentDataId,
|
||||
placeholders,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const currentHighestOrderValue =
|
||||
envelope.envelopeItems[envelope.envelopeItems.length - 1]?.order ?? 1;
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const createdItems = await tx.envelopeItem.createManyAndReturn({
|
||||
data: envelopeItems.map((item) => ({
|
||||
id: prefixedId('envelope_item'),
|
||||
envelopeId,
|
||||
title: item.title,
|
||||
documentDataId: item.documentDataId,
|
||||
order: currentHighestOrderValue + 1,
|
||||
})),
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: createdItems.map((item) =>
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
envelopeItemId: item.id,
|
||||
envelopeItemTitle: item.title,
|
||||
},
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
requestMetadata: metadata.requestMetadata,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
// Create fields from placeholders if the envelope already has recipients.
|
||||
if (envelope.recipients.length > 0) {
|
||||
const orderedRecipients = [...envelope.recipients].sort((a, b) => {
|
||||
const aOrder = a.signingOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
const bOrder = b.signingOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
|
||||
if (aOrder !== bOrder) {
|
||||
return aOrder - bOrder;
|
||||
}
|
||||
|
||||
return a.id - b.id;
|
||||
});
|
||||
|
||||
for (const uploadedItem of envelopeItems) {
|
||||
if (!uploadedItem.placeholders || uploadedItem.placeholders.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const createdItem = createdItems.find(
|
||||
(ci) => ci.documentDataId === uploadedItem.documentDataId,
|
||||
);
|
||||
|
||||
if (!createdItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldsToCreate = convertPlaceholdersToFieldInputs(
|
||||
uploadedItem.placeholders,
|
||||
(recipientPlaceholder, placeholder) =>
|
||||
findRecipientByPlaceholder(
|
||||
recipientPlaceholder,
|
||||
placeholder,
|
||||
orderedRecipients,
|
||||
orderedRecipients,
|
||||
),
|
||||
createdItem.id,
|
||||
);
|
||||
|
||||
if (fieldsToCreate.length > 0) {
|
||||
await tx.field.createMany({
|
||||
data: fieldsToCreate.map((field) => ({
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId: createdItem.id,
|
||||
recipientId: field.recipientId,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta || undefined,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createdItems;
|
||||
const result = await UNSAFE_createEnvelopeItems({
|
||||
files: files.map((file) => ({
|
||||
file,
|
||||
})),
|
||||
envelope,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
apiRequestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,10 +5,12 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
||||
import { extractPdfPlaceholders } from '@documenso/lib/server-only/pdf/auto-place-fields';
|
||||
import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
|
||||
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import type { TCreateEnvelopeRequest } from './create-envelope.types';
|
||||
import {
|
||||
ZCreateEnvelopeRequestSchema,
|
||||
ZCreateEnvelopeResponseSchema,
|
||||
@@ -20,150 +22,183 @@ export const createEnvelopeRoute = authenticatedProcedure
|
||||
.input(ZCreateEnvelopeRequestSchema)
|
||||
.output(ZCreateEnvelopeResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { user, teamId } = ctx;
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
folderId: input.payload.folderId,
|
||||
},
|
||||
});
|
||||
|
||||
const { payload, files } = input;
|
||||
return await createEnvelopeRouteCaller({
|
||||
userId: ctx.user.id,
|
||||
teamId: ctx.teamId,
|
||||
input,
|
||||
apiRequestMetadata: ctx.metadata,
|
||||
});
|
||||
});
|
||||
|
||||
const {
|
||||
title,
|
||||
type CreateEnvelopeRouteOptions = {
|
||||
/**
|
||||
* Verified user ID.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* Unverified team ID.
|
||||
*/
|
||||
teamId: number;
|
||||
input: TCreateEnvelopeRequest;
|
||||
apiRequestMetadata: ApiRequestMetadata;
|
||||
|
||||
options?: {
|
||||
bypassDefaultRecipients?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const createEnvelopeRouteCaller = async ({
|
||||
userId,
|
||||
teamId,
|
||||
input,
|
||||
apiRequestMetadata,
|
||||
options = {},
|
||||
}: CreateEnvelopeRouteOptions) => {
|
||||
const { payload, files } = input;
|
||||
|
||||
const {
|
||||
title,
|
||||
type,
|
||||
externalId,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
formValues,
|
||||
recipients,
|
||||
folderId,
|
||||
meta,
|
||||
attachments,
|
||||
delegatedDocumentOwner,
|
||||
} = payload;
|
||||
|
||||
const { remaining, maximumEnvelopeItemCount } = await getServerLimits({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (remaining.documents <= 0) {
|
||||
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||
message: 'You have reached your document limit for this month. Please upgrade your plan.',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (files.length > maximumEnvelopeItemCount) {
|
||||
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
|
||||
message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (files.some((file) => !file.type.startsWith('application/pdf'))) {
|
||||
throw new AppError('INVALID_DOCUMENT_FILE', {
|
||||
message: 'You cannot upload non-PDF files',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// For each file: normalize, extract & clean placeholders, then upload.
|
||||
const envelopeItems = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
let pdf = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
if (formValues) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
pdf = await insertFormValuesInPdf({
|
||||
pdf,
|
||||
formValues,
|
||||
});
|
||||
}
|
||||
|
||||
const normalized = await normalizePdf(pdf, {
|
||||
flattenForm: type !== EnvelopeType.TEMPLATE,
|
||||
});
|
||||
|
||||
// Todo: Embeds - Might need to add this for client-side embeds in the future.
|
||||
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
|
||||
|
||||
const { id: documentDataId } = await putPdfFileServerSide({
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(cleanedPdf),
|
||||
});
|
||||
|
||||
return {
|
||||
title: file.name,
|
||||
documentDataId,
|
||||
placeholders,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
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,
|
||||
teamId,
|
||||
internalVersion: 2,
|
||||
data: {
|
||||
type,
|
||||
title,
|
||||
externalId,
|
||||
formValues,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
formValues,
|
||||
recipients,
|
||||
recipients: recipientsToCreate,
|
||||
folderId,
|
||||
meta,
|
||||
attachments,
|
||||
envelopeItems,
|
||||
delegatedDocumentOwner,
|
||||
} = payload;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
|
||||
const { remaining, maximumEnvelopeItemCount } = await getServerLimits({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (remaining.documents <= 0) {
|
||||
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||
message: 'You have reached your document limit for this month. Please upgrade your plan.',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (files.length > maximumEnvelopeItemCount) {
|
||||
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
|
||||
message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (files.some((file) => !file.type.startsWith('application/pdf'))) {
|
||||
throw new AppError('INVALID_DOCUMENT_FILE', {
|
||||
message: 'You cannot upload non-PDF files',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// For each file: normalize, extract & clean placeholders, then upload.
|
||||
const envelopeItems = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
let pdf = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
if (formValues) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
pdf = await insertFormValuesInPdf({
|
||||
pdf,
|
||||
formValues,
|
||||
});
|
||||
}
|
||||
|
||||
const normalized = await normalizePdf(pdf, {
|
||||
flattenForm: type !== EnvelopeType.TEMPLATE,
|
||||
});
|
||||
|
||||
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
|
||||
|
||||
const { id: documentDataId } = await putPdfFileServerSide({
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(cleanedPdf),
|
||||
});
|
||||
|
||||
return {
|
||||
title: file.name,
|
||||
documentDataId,
|
||||
placeholders,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
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,
|
||||
internalVersion: 2,
|
||||
data: {
|
||||
type,
|
||||
title,
|
||||
externalId,
|
||||
formValues,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
recipients: recipientsToCreate,
|
||||
folderId,
|
||||
envelopeItems,
|
||||
delegatedDocumentOwner,
|
||||
},
|
||||
attachments,
|
||||
meta,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
id: envelope.id,
|
||||
};
|
||||
},
|
||||
attachments,
|
||||
meta,
|
||||
requestMetadata: apiRequestMetadata,
|
||||
bypassDefaultRecipients: options.bypassDefaultRecipients,
|
||||
});
|
||||
|
||||
return {
|
||||
id: envelope.id,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { UNSAFE_deleteEnvelopeItem } from '@documenso/lib/server-only/envelope-item/delete-envelope-item';
|
||||
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 { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@@ -57,49 +56,11 @@ export const deleteEnvelopeItemRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const deletedEnvelopeItem = await tx.envelopeItem.delete({
|
||||
where: {
|
||||
id: envelopeItemId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
documentData: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_DELETED,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
envelopeItemId: deletedEnvelopeItem.id,
|
||||
envelopeItemTitle: deletedEnvelopeItem.title,
|
||||
},
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
requestMetadata: metadata.requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return deletedEnvelopeItem;
|
||||
});
|
||||
|
||||
await prisma.documentData.delete({
|
||||
where: {
|
||||
id: result.documentData.id,
|
||||
envelopeItem: {
|
||||
is: null,
|
||||
},
|
||||
},
|
||||
await UNSAFE_deleteEnvelopeItem({
|
||||
envelopeId,
|
||||
envelopeItemId,
|
||||
user,
|
||||
apiRequestMetadata: metadata,
|
||||
});
|
||||
|
||||
return ZGenericSuccessResponse;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { getEditorEnvelopeById } from '@documenso/lib/server-only/envelope/get-editor-envelope-by-id';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetEditorEnvelopeRequestSchema,
|
||||
ZGetEditorEnvelopeResponseSchema,
|
||||
} from './get-editor-envelope.types';
|
||||
|
||||
export const getEditorEnvelopeRoute = authenticatedProcedure
|
||||
.input(ZGetEditorEnvelopeRequestSchema)
|
||||
.output(ZGetEditorEnvelopeResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { envelopeId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
envelopeId,
|
||||
},
|
||||
});
|
||||
|
||||
return await getEditorEnvelopeById({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
type: null,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZEditorEnvelopeSchema } from '@documenso/lib/types/envelope-editor';
|
||||
|
||||
export const ZGetEditorEnvelopeRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
});
|
||||
|
||||
export const ZGetEditorEnvelopeResponseSchema = ZEditorEnvelopeSchema;
|
||||
|
||||
export type TGetEditorEnvelopeRequest = z.infer<typeof ZGetEditorEnvelopeRequestSchema>;
|
||||
export type TGetEditorEnvelopeResponse = z.infer<typeof ZGetEditorEnvelopeResponseSchema>;
|
||||
@@ -22,6 +22,7 @@ import { getEnvelopeRecipientRoute } from './envelope-recipients/get-envelope-re
|
||||
import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-envelope-recipients';
|
||||
import { findEnvelopeAuditLogsRoute } from './find-envelope-audit-logs';
|
||||
import { findEnvelopesRoute } from './find-envelopes';
|
||||
import { getEditorEnvelopeRoute } from './get-editor-envelope';
|
||||
import { getEnvelopeRoute } from './get-envelope';
|
||||
import { getEnvelopeItemsRoute } from './get-envelope-items';
|
||||
import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token';
|
||||
@@ -78,6 +79,9 @@ export const envelopeRouter = router({
|
||||
move: bulkMoveEnvelopesRoute,
|
||||
delete: bulkDeleteEnvelopesRoute,
|
||||
},
|
||||
editor: {
|
||||
get: getEditorEnvelopeRoute,
|
||||
},
|
||||
get: getEnvelopeRoute,
|
||||
getMany: getEnvelopesByIdsRoute,
|
||||
create: createEnvelopeRoute,
|
||||
|
||||
@@ -4,19 +4,19 @@ import { z } from 'zod';
|
||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
import { ZRecipientEmailSchema, ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
||||
|
||||
export const ZSetEnvelopeRecipientSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
email: ZRecipientEmailSchema,
|
||||
name: z.string().max(255),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
});
|
||||
|
||||
export const ZSetEnvelopeRecipientsRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
envelopeType: z.nativeEnum(EnvelopeType),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
email: ZRecipientEmailSchema,
|
||||
name: z.string().max(255),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
}),
|
||||
),
|
||||
recipients: ZSetEnvelopeRecipientSchema.array(),
|
||||
});
|
||||
|
||||
export const ZSetEnvelopeRecipientsResponseSchema = z.object({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { UNSAFE_updateEnvelopeItems } from '@documenso/lib/server-only/envelope-item/update-envelope-items';
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@@ -54,6 +55,9 @@ export const updateEnvelopeItemsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
// Note: This logic is duplicated in many places. If we plan to allow changing title/order
|
||||
// even after the envelope has been sent, make sure to update it everywhere including
|
||||
// embedding routes.
|
||||
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item is not editable',
|
||||
@@ -71,28 +75,10 @@ export const updateEnvelopeItemsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
const updatedEnvelopeItems = await Promise.all(
|
||||
data.map(async ({ envelopeItemId, order, title }) =>
|
||||
prisma.envelopeItem.update({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
id: envelopeItemId,
|
||||
},
|
||||
data: {
|
||||
order,
|
||||
title,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
order: true,
|
||||
title: true,
|
||||
envelopeId: true,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Todo: Envelope [AUDIT_LOGS]
|
||||
const updatedEnvelopeItems = await UNSAFE_updateEnvelopeItems({
|
||||
envelopeId,
|
||||
data,
|
||||
});
|
||||
|
||||
return {
|
||||
data: updatedEnvelopeItems,
|
||||
|
||||
Reference in New Issue
Block a user