diff --git a/packages/lib/server-only/embedding-presign/verify-embedding-presign-token.ts b/packages/lib/server-only/embedding-presign/verify-embedding-presign-token.ts index 5d0fb6556..ae09e5c5a 100644 --- a/packages/lib/server-only/embedding-presign/verify-embedding-presign-token.ts +++ b/packages/lib/server-only/embedding-presign/verify-embedding-presign-token.ts @@ -60,6 +60,15 @@ export const verifyEmbeddingPresignToken = async ({ where: { id: tokenId, }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, }); if (!apiToken) { @@ -69,7 +78,7 @@ export const verifyEmbeddingPresignToken = async ({ } // This should never happen but we need to narrow types - if (!apiToken.userId) { + if (!apiToken.userId || !apiToken.user) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'Invalid presign token: API token does not have a user attached', }); @@ -119,5 +128,10 @@ export const verifyEmbeddingPresignToken = async ({ return { ...apiToken, userId, + user: { + id: apiToken.user.id, + name: apiToken.user.name, + email: apiToken.user.email, + }, }; }; diff --git a/packages/lib/server-only/envelope/create-envelope.ts b/packages/lib/server-only/envelope/create-envelope.ts index f810389e7..4c2ed5756 100644 --- a/packages/lib/server-only/envelope/create-envelope.ts +++ b/packages/lib/server-only/envelope/create-envelope.ts @@ -97,6 +97,14 @@ export type CreateEnvelopeOptions = { data: string; type?: TEnvelopeAttachmentType; }>; + + /** + * Whether to bypass adding default recipients. + * + * Defaults to false. + */ + bypassDefaultRecipients?: boolean; + meta?: Partial>; requestMetadata: ApiRequestMetadata; }; @@ -110,6 +118,7 @@ export const createEnvelope = async ({ meta, requestMetadata, internalVersion, + bypassDefaultRecipients = false, }: CreateEnvelopeOptions) => { const { type, @@ -355,9 +364,10 @@ export const createEnvelope = async ({ const firstEnvelopeItem = envelope.envelopeItems[0]; - const defaultRecipients = settings.defaultRecipients - ? ZDefaultRecipientsSchema.parse(settings.defaultRecipients) - : []; + const defaultRecipients = + settings.defaultRecipients && !bypassDefaultRecipients + ? ZDefaultRecipientsSchema.parse(settings.defaultRecipients) + : []; const mappedDefaultRecipients: CreateEnvelopeRecipientOptions[] = defaultRecipients.map( (recipient) => ({ diff --git a/packages/lib/server-only/recipient/set-template-recipients.ts b/packages/lib/server-only/recipient/set-template-recipients.ts index 1743968c1..4376f4d7c 100644 --- a/packages/lib/server-only/recipient/set-template-recipients.ts +++ b/packages/lib/server-only/recipient/set-template-recipients.ts @@ -21,14 +21,7 @@ export type SetTemplateRecipientsOptions = { userId: number; teamId: number; id: EnvelopeIdOptions; - recipients: { - id?: number; - email: string; - name: string; - role: RecipientRole; - signingOrder?: number | null; - actionAuth?: TRecipientActionAuthTypes[]; - }[]; + recipients: RecipientData[]; }; export const setTemplateRecipients = async ({ @@ -183,7 +176,10 @@ export const setTemplateRecipients = async ({ }); } - return upsertedRecipient; + return { + ...upsertedRecipient, + clientId: recipient.clientId, + }; }), ); }); @@ -199,7 +195,7 @@ export const setTemplateRecipients = async ({ } // Filter out recipients that have been removed or have been updated. - const filteredRecipients: Recipient[] = existingRecipients.filter((recipient) => { + const filteredRecipients: RecipientDataWithClientId[] = existingRecipients.filter((recipient) => { const isRemoved = removedRecipients.find( (removedRecipient) => removedRecipient.id === recipient.id, ); @@ -218,3 +214,17 @@ export const setTemplateRecipients = async ({ })), }; }; + +type RecipientData = { + id?: number; + clientId?: string | null; + email: string; + name: string; + role: RecipientRole; + signingOrder?: number | null; + actionAuth?: TRecipientActionAuthTypes[]; +}; + +type RecipientDataWithClientId = Recipient & { + clientId?: string | null; +}; diff --git a/packages/lib/types/envelope-editor.ts b/packages/lib/types/envelope-editor.ts new file mode 100644 index 000000000..068ec5530 --- /dev/null +++ b/packages/lib/types/envelope-editor.ts @@ -0,0 +1,240 @@ +import { EnvelopeType } from '@prisma/client'; +import { z } from 'zod'; + +import { ZEnvelopeFieldSchema } from '@documenso/lib/types/field'; +import { ZEnvelopeRecipientLiteSchema } from '@documenso/lib/types/recipient'; +import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema'; +import { EnvelopeItemSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema'; +import { EnvelopeSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema'; +import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; +import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema'; +import { ZBaseEmbedDataSchema } from '@documenso/remix/app/types/embed-base-schemas'; + +export const ZEnvelopeEditorSettingsSchema = z.object({ + /** + * Generic editor related configurations. + */ + general: z.object({ + allowConfigureEnvelopeTitle: z.boolean(), + allowUploadAndRecipientStep: z.boolean(), + allowAddFieldsStep: z.boolean(), + allowPreviewStep: z.boolean(), + minimizeLeftSidebar: z.boolean(), + }), + + /** + * Envelope meta/settings related configuration + * + * If null, the settings will not be available to be seen/updated. + */ + settings: z + .object({ + allowConfigureSignatureTypes: z.boolean(), + allowConfigureLanguage: z.boolean(), + allowConfigureDateFormat: z.boolean(), + allowConfigureTimezone: z.boolean(), + allowConfigureRedirectUrl: z.boolean(), + allowConfigureDistribution: z.boolean(), + }) + .nullable(), + + /** + * Action related configurations. + */ + actions: z.object({ + allowAttachments: z.boolean(), + allowDistributing: z.boolean(), + allowDirectLink: z.boolean(), + allowDuplication: z.boolean(), + allowDownloadPDF: z.boolean(), + allowDeletion: z.boolean(), + allowReturnToPreviousPage: z.boolean(), + }), + + /** + * Envelope items related configurations. + * + * If null, no adjustments to envelope items will be allowed. + */ + envelopeItems: z + .object({ + allowConfigureTitle: z.boolean(), + allowConfigureOrder: z.boolean(), + allowUpload: z.boolean(), + allowDelete: z.boolean(), + }) + .nullable(), + + /** + * Recipient related configurations. + * + * If null, recipients will not be configurable at all. + */ + recipients: z + .object({ + allowAIDetection: z.boolean(), + allowConfigureSigningOrder: z.boolean(), + allowConfigureDictateNextSigner: z.boolean(), + allowApproverRole: z.boolean(), + allowViewerRole: z.boolean(), + allowCCerRole: z.boolean(), + allowAssistantRole: z.boolean(), + }) + .nullable(), + + /** + * Fields related configurations. + */ + fields: z.object({ + allowAIDetection: z.boolean(), + }), +}); + +export type TEnvelopeEditorSettings = z.infer; + +export const ZEmbedCreateEnvelopeAuthoringSchema = ZBaseEmbedDataSchema.extend({ + externalId: z.string().optional(), + type: z.nativeEnum(EnvelopeType), + features: ZEnvelopeEditorSettingsSchema, +}); + +export const ZEmbedEditEnvelopeAuthoringSchema = ZBaseEmbedDataSchema.extend({ + externalId: z.string().optional(), + features: ZEnvelopeEditorSettingsSchema, +}); + +export type TEmbedCreateEnvelopeAuthoring = z.infer; +export type TEmbedEditEnvelopeAuthoring = z.infer; + +/** + * A subset of the full envelope response schema used for the envelope editor. + * + * Internal usage only. + */ +export const ZEditorEnvelopeSchema = EnvelopeSchema.pick({ + internalVersion: true, + type: true, + status: true, + source: true, + visibility: true, + templateType: true, + id: true, + secondaryId: true, + externalId: true, + completedAt: true, + deletedAt: true, + title: true, + authOptions: true, + publicTitle: true, + publicDescription: true, + userId: true, + teamId: true, + folderId: true, +}).extend({ + documentMeta: DocumentMetaSchema.pick({ + signingOrder: true, + distributionMethod: true, + id: true, + subject: true, + message: true, + timezone: true, + dateFormat: true, + redirectUrl: true, + typedSignatureEnabled: true, + uploadSignatureEnabled: true, + drawSignatureEnabled: true, + allowDictateNextSigner: true, + language: true, + emailSettings: true, + emailId: true, + emailReplyTo: true, + }), + recipients: ZEnvelopeRecipientLiteSchema.array(), + fields: ZEnvelopeFieldSchema.array(), + envelopeItems: EnvelopeItemSchema.pick({ + envelopeId: true, + id: true, + title: true, + order: true, + }) + .extend({ + data: z.instanceof(Uint8Array).optional(), + }) + .array(), + directLink: TemplateDirectLinkSchema.pick({ + directTemplateRecipientId: true, + enabled: true, + id: true, + token: true, + }).nullable(), + team: TeamSchema.pick({ + id: true, + url: true, + }), + user: z.object({ + id: z.number(), + name: z.string(), + email: z.string(), + }), +}); + +export type TEditorEnvelope = z.infer; + +export type EnvelopeEditorConfig = TEnvelopeEditorSettings & { + embeded?: { + presignToken: string; + mode: 'create' | 'edit'; + onCreate?: (envelope: Omit) => void; + onUpdate?: (envelope: TEditorEnvelope) => void; + customBrandingLogo?: boolean; + }; +}; + +/** + * The default editor configuration for normal flows. + */ +export const DEFAULT_EDITOR_CONFIG: EnvelopeEditorConfig = { + general: { + allowConfigureEnvelopeTitle: true, + allowUploadAndRecipientStep: true, + allowAddFieldsStep: true, + allowPreviewStep: true, + minimizeLeftSidebar: false, + }, + settings: { + allowConfigureSignatureTypes: true, + allowConfigureLanguage: true, + allowConfigureDateFormat: true, + allowConfigureTimezone: true, + allowConfigureRedirectUrl: true, + allowConfigureDistribution: true, + }, + actions: { + allowAttachments: true, + allowDistributing: true, + allowDirectLink: true, + allowDuplication: true, + allowDownloadPDF: true, + allowDeletion: true, + allowReturnToPreviousPage: true, + }, + envelopeItems: { + allowConfigureTitle: true, + allowConfigureOrder: true, + allowUpload: true, + allowDelete: true, + }, + recipients: { + allowAIDetection: true, + allowConfigureSigningOrder: true, + allowConfigureDictateNextSigner: true, + + allowApproverRole: true, + allowViewerRole: true, + allowCCerRole: true, + allowAssistantRole: true, + }, + fields: { + allowAIDetection: true, + }, +}; diff --git a/packages/lib/types/field-meta.ts b/packages/lib/types/field-meta.ts index e4ed9be40..d5b3cbe29 100644 --- a/packages/lib/types/field-meta.ts +++ b/packages/lib/types/field-meta.ts @@ -419,4 +419,4 @@ export const ZEnvelopeFieldAndMetaSchema = z.discriminatedUnion('type', [ }), ]); -type TEnvelopeFieldAndMeta = z.infer; +export type TEnvelopeFieldAndMeta = z.infer; diff --git a/packages/lib/utils/embed-config.ts b/packages/lib/utils/embed-config.ts new file mode 100644 index 000000000..9740eeee6 --- /dev/null +++ b/packages/lib/utils/embed-config.ts @@ -0,0 +1,162 @@ +import type { EnvelopeEditorConfig, TEnvelopeEditorSettings } from '../types/envelope-editor'; + +export const PRESIGNED_ENVELOPE_ITEM_ID_PREFIX = 'PRESIGNED_'; + +/** + * Embedded-mode defaults for the editor config. + * + * - `general`: All true, `minimizeLeftSidebar: true` + * - `settings`: All true + * - `actions`: `allowAttachments: true`; everything else false (parent app controls lifecycle) + * - `envelopeItems`: All true + * - `recipients`: All true + */ +const EMBEDDED_DEFAULTS = { + general: { + allowConfigureEnvelopeTitle: true, + allowUploadAndRecipientStep: true, + allowAddFieldsStep: true, + allowPreviewStep: true, + minimizeLeftSidebar: true, + }, + settings: { + allowConfigureSignatureTypes: true, + allowConfigureLanguage: true, + allowConfigureDateFormat: true, + allowConfigureTimezone: true, + allowConfigureRedirectUrl: true, + allowConfigureDistribution: true, + }, + actions: { + allowAttachments: true, + allowDistributing: false, + allowDirectLink: false, + allowDuplication: false, + allowDownloadPDF: false, + allowDeletion: false, + allowReturnToPreviousPage: false, + }, + envelopeItems: { + allowConfigureTitle: true, + allowConfigureOrder: true, + allowUpload: true, + allowDelete: true, + }, + recipients: { + allowAIDetection: true, + allowConfigureSigningOrder: true, + allowConfigureDictateNextSigner: true, + allowApproverRole: true, + allowViewerRole: true, + allowCCerRole: true, + allowAssistantRole: true, + }, + fields: { + allowAIDetection: true, + }, +} as const satisfies TEnvelopeEditorSettings; + +/** + * Takes parsed `features` from the embedding hash and an `embeded` config, + * and produces a complete `EnvelopeEditorConfig` with sensible embedded-mode defaults. + * + * Any explicitly provided feature flag overrides the embedded default. + */ +export function buildEditorConfigFromFeatures( + features: TEnvelopeEditorSettings, + embeded: EnvelopeEditorConfig['embeded'], +): EnvelopeEditorConfig { + return { + embeded, + + general: { + allowConfigureEnvelopeTitle: + features.general.allowConfigureEnvelopeTitle ?? + EMBEDDED_DEFAULTS.general.allowConfigureEnvelopeTitle, + allowUploadAndRecipientStep: + features.general.allowUploadAndRecipientStep ?? + EMBEDDED_DEFAULTS.general.allowUploadAndRecipientStep, + allowAddFieldsStep: + features.general.allowAddFieldsStep ?? EMBEDDED_DEFAULTS.general.allowAddFieldsStep, + allowPreviewStep: + features.general.allowPreviewStep ?? EMBEDDED_DEFAULTS.general.allowPreviewStep, + minimizeLeftSidebar: + features.general.minimizeLeftSidebar ?? EMBEDDED_DEFAULTS.general.minimizeLeftSidebar, + }, + + settings: { + allowConfigureSignatureTypes: + features.settings?.allowConfigureSignatureTypes ?? + EMBEDDED_DEFAULTS.settings.allowConfigureSignatureTypes, + allowConfigureLanguage: + features.settings?.allowConfigureLanguage ?? + EMBEDDED_DEFAULTS.settings.allowConfigureLanguage, + allowConfigureDateFormat: + features.settings?.allowConfigureDateFormat ?? + EMBEDDED_DEFAULTS.settings.allowConfigureDateFormat, + allowConfigureTimezone: + features.settings?.allowConfigureTimezone ?? + EMBEDDED_DEFAULTS.settings.allowConfigureTimezone, + allowConfigureRedirectUrl: + features.settings?.allowConfigureRedirectUrl ?? + EMBEDDED_DEFAULTS.settings.allowConfigureRedirectUrl, + allowConfigureDistribution: + features.settings?.allowConfigureDistribution ?? + EMBEDDED_DEFAULTS.settings.allowConfigureDistribution, + }, + + actions: { + allowAttachments: + features.actions.allowAttachments ?? EMBEDDED_DEFAULTS.actions.allowAttachments, + allowDistributing: + features.actions.allowDistributing ?? EMBEDDED_DEFAULTS.actions.allowDistributing, + allowDirectLink: + features.actions.allowDirectLink ?? EMBEDDED_DEFAULTS.actions.allowDirectLink, + allowDuplication: + features.actions.allowDuplication ?? EMBEDDED_DEFAULTS.actions.allowDuplication, + allowDownloadPDF: + features.actions.allowDownloadPDF ?? EMBEDDED_DEFAULTS.actions.allowDownloadPDF, + allowDeletion: features.actions.allowDeletion ?? EMBEDDED_DEFAULTS.actions.allowDeletion, + allowReturnToPreviousPage: + features.actions.allowReturnToPreviousPage ?? + EMBEDDED_DEFAULTS.actions.allowReturnToPreviousPage, + }, + + envelopeItems: { + allowConfigureTitle: + features.envelopeItems?.allowConfigureTitle ?? + EMBEDDED_DEFAULTS.envelopeItems.allowConfigureTitle, + allowConfigureOrder: + features.envelopeItems?.allowConfigureOrder ?? + EMBEDDED_DEFAULTS.envelopeItems.allowConfigureOrder, + allowUpload: + features.envelopeItems?.allowUpload ?? EMBEDDED_DEFAULTS.envelopeItems.allowUpload, + allowDelete: + features.envelopeItems?.allowDelete ?? EMBEDDED_DEFAULTS.envelopeItems.allowDelete, + }, + + recipients: { + allowAIDetection: + features.recipients?.allowAIDetection ?? EMBEDDED_DEFAULTS.recipients.allowAIDetection, + allowConfigureSigningOrder: + features.recipients?.allowConfigureSigningOrder ?? + EMBEDDED_DEFAULTS.recipients.allowConfigureSigningOrder, + allowConfigureDictateNextSigner: + features.recipients?.allowConfigureDictateNextSigner ?? + EMBEDDED_DEFAULTS.recipients.allowConfigureDictateNextSigner, + allowApproverRole: + features.recipients?.allowApproverRole ?? EMBEDDED_DEFAULTS.recipients.allowApproverRole, + allowViewerRole: + features.recipients?.allowViewerRole ?? EMBEDDED_DEFAULTS.recipients.allowViewerRole, + allowCCerRole: + features.recipients?.allowCCerRole ?? EMBEDDED_DEFAULTS.recipients.allowCCerRole, + allowAssistantRole: + features.recipients?.allowAssistantRole ?? EMBEDDED_DEFAULTS.recipients.allowAssistantRole, + }, + + fields: { + allowAIDetection: + features.fields?.allowAIDetection ?? EMBEDDED_DEFAULTS.fields.allowAIDetection, + }, + }; +} diff --git a/packages/trpc/server/embedding-router/create-embedding-envelope.ts b/packages/trpc/server/embedding-router/create-embedding-envelope.ts index c52ecb852..b4ae1709f 100644 --- a/packages/trpc/server/embedding-router/create-embedding-envelope.ts +++ b/packages/trpc/server/embedding-router/create-embedding-envelope.ts @@ -32,6 +32,10 @@ export const createEmbeddingEnvelopeRoute = procedure userId, teamId, input, - requestMetadata: ctx.metadata, + options: { + // Default recipients should be added on the frontend automatically for embeds. + bypassDefaultRecipients: true, + }, + apiRequestMetadata: ctx.metadata, }); }); diff --git a/packages/trpc/server/envelope-router/create-envelope.ts b/packages/trpc/server/envelope-router/create-envelope.ts index e95d7eff4..0e41ec3d4 100644 --- a/packages/trpc/server/envelope-router/create-envelope.ts +++ b/packages/trpc/server/envelope-router/create-envelope.ts @@ -32,7 +32,7 @@ export const createEnvelopeRoute = authenticatedProcedure userId: ctx.user.id, teamId: ctx.teamId, input, - requestMetadata: ctx.metadata, + apiRequestMetadata: ctx.metadata, }); }); @@ -47,14 +47,19 @@ type CreateEnvelopeRouteOptions = { */ teamId: number; input: TCreateEnvelopeRequest; - requestMetadata: ApiRequestMetadata; + apiRequestMetadata: ApiRequestMetadata; + + options?: { + bypassDefaultRecipients?: boolean; + }; }; export const createEnvelopeRouteCaller = async ({ userId, teamId, input, - requestMetadata, + apiRequestMetadata, + options = {}, }: CreateEnvelopeRouteOptions) => { const { payload, files } = input; @@ -116,6 +121,7 @@ export const createEnvelopeRouteCaller = async ({ flattenForm: type !== EnvelopeType.TEMPLATE, }); + // Todo: Embeds - Does this need to be done on the frontend? const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized); const { id: documentDataId } = await putPdfFileServerSide({ @@ -188,7 +194,8 @@ export const createEnvelopeRouteCaller = async ({ }, attachments, meta, - requestMetadata, + requestMetadata: apiRequestMetadata, + bypassDefaultRecipients: options.bypassDefaultRecipients, }); return { diff --git a/packages/trpc/server/envelope-router/delete-envelope-item.ts b/packages/trpc/server/envelope-router/delete-envelope-item.ts index 024bdf7e1..0bbbc9859 100644 --- a/packages/trpc/server/envelope-router/delete-envelope-item.ts +++ b/packages/trpc/server/envelope-router/delete-envelope-item.ts @@ -1,6 +1,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 type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; @@ -57,50 +58,75 @@ 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; }); + +type UnsafeDeleteEnvelopeItemOptions = { + envelopeId: string; + envelopeItemId: string; + user: { + id: number; + name: string | null; + email: string; + }; + apiRequestMetadata: ApiRequestMetadata; +}; + +export const UNSAFE_deleteEnvelopeItem = async ({ + envelopeId, + envelopeItemId, + user, + apiRequestMetadata, +}: UnsafeDeleteEnvelopeItemOptions) => { + const result = await prisma.$transaction(async (tx) => { + const deletedEnvelopeItem = await tx.envelopeItem.delete({ + where: { + id: envelopeItemId, + envelopeId, + }, + select: { + id: true, + title: true, + documentData: { + select: { + id: true, + }, + }, + }, + }); + + await tx.documentAuditLog.create({ + data: createDocumentAuditLogData({ + type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_DELETED, + envelopeId, + data: { + envelopeItemId: deletedEnvelopeItem.id, + envelopeItemTitle: deletedEnvelopeItem.title, + }, + user: { + name: user.name, + email: user.email, + }, + requestMetadata: apiRequestMetadata.requestMetadata, + }), + }); + + return deletedEnvelopeItem; + }); + + await prisma.documentData.delete({ + where: { + id: result.documentData.id, + envelopeItem: { + is: null, + }, + }, + }); +}; diff --git a/packages/trpc/server/envelope-router/update-envelope-items.ts b/packages/trpc/server/envelope-router/update-envelope-items.ts index 2e25ad2d9..ac6713059 100644 --- a/packages/trpc/server/envelope-router/update-envelope-items.ts +++ b/packages/trpc/server/envelope-router/update-envelope-items.ts @@ -54,6 +54,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,30 +74,51 @@ 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, }; }); + +type UnsafeUpdateEnvelopeItemsOptions = { + envelopeId: string; + data: { + envelopeItemId: string; + order?: number; + title?: string; + }[]; +}; + +export const UNSAFE_updateEnvelopeItems = async ({ + envelopeId, + data, +}: UnsafeUpdateEnvelopeItemsOptions) => { + // Todo: Envelope [AUDIT_LOGS] + + const updatedEnvelopeItems = await Promise.all( + data.map(async ({ envelopeItemId, order, title }) => + prisma.envelopeItem.update({ + where: { + envelopeId, + id: envelopeItemId, + }, + data: { + order, + title, + }, + select: { + id: true, + order: true, + title: true, + envelopeId: true, + }, + }), + ), + ); + + return updatedEnvelopeItems; +};