From 656d1347b903760d163fe37ca232d5127e3d2147 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 1 Oct 2026 15:39:21 +1000 Subject: [PATCH] fix: remove contents from public envelope create route --- .../docs/developers/embedding/editor/v2.mdx | 7 +- .../forms/editor/editor-content-text-form.tsx | 2 + .../envelope-editor-fields-page.tsx | 2 +- .../v2+/authoring+/envelope.create._index.tsx | 6 +- ...reate-embedding-envelope-contents.spec.ts} | 155 +++++++++++++----- .../trpc/set-envelope-contents-limits.spec.ts | 21 ++- .../e2e/api/v2/envelope-copy-contents.spec.ts | 35 +++- packages/lib/constants/envelope-content.ts | 9 + .../server-only/envelope/create-envelope.ts | 6 - .../envelope/duplicate-envelope.ts | 9 - packages/lib/types/envelope-content-meta.ts | 3 +- .../create-embedding-envelope.types.ts | 40 ++++- .../server/envelope-router/create-envelope.ts | 15 +- .../envelope-router/create-envelope.types.ts | 25 +-- 14 files changed, 235 insertions(+), 100 deletions(-) rename packages/app-tests/e2e/api/{v2/envelope-create-contents.spec.ts => trpc/create-embedding-envelope-contents.spec.ts} (80%) diff --git a/apps/docs/content/docs/developers/embedding/editor/v2.mdx b/apps/docs/content/docs/developers/embedding/editor/v2.mdx index 6c8fc91f4..ca0e01b7b 100644 --- a/apps/docs/content/docs/developers/embedding/editor/v2.mdx +++ b/apps/docs/content/docs/developers/embedding/editor/v2.mdx @@ -190,9 +190,10 @@ Controls envelope configuration options. Set to `null` to hide envelope settings Controls available actions during editing: -| Property | Type | Default | Description | -| ------------------ | --------- | ------- | ------------------------ | -| `allowAttachments` | `boolean` | `true` | Allow adding attachments | +| Property | Type | Default | Description | +| ------------------ | --------- | ------- | -------------------------------------------------------------------------------------------- | +| `allowContents` | `boolean` | `true` | Allow adding contents (text, lines, shapes, highlights and images) alongside fields in the add fields step | +| `allowAttachments` | `boolean` | `true` | Allow adding attachments | ### Envelope Items diff --git a/apps/remix/app/components/forms/editor/editor-content-text-form.tsx b/apps/remix/app/components/forms/editor/editor-content-text-form.tsx index 2747df240..51735f68e 100644 --- a/apps/remix/app/components/forms/editor/editor-content-text-form.tsx +++ b/apps/remix/app/components/forms/editor/editor-content-text-form.tsx @@ -1,3 +1,4 @@ +import { CONTENT_TEXT_MAX_LENGTH } from '@documenso/lib/constants/envelope-content'; import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form'; import { Textarea } from '@documenso/ui/primitives/textarea'; import { Trans, useLingui } from '@lingui/react/macro'; @@ -47,6 +48,7 @@ export const EditorContentTextForm = () => { className="h-auto" placeholder={t`Add text to the document`} rows={3} + maxLength={CONTENT_TEXT_MAX_LENGTH} {...field} /> diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx index 4d18ca3cd..85d28761c 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx @@ -270,7 +270,7 @@ export const EnvelopeEditorFieldsPage = () => { const { allowContents } = editorConfig.actions; - if (!editorConfig.general?.allowAddFieldsStep) { + if (!editorConfig.general.allowAddFieldsStep) { return null; } diff --git a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx index 158b97ca3..e4424f3fe 100644 --- a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx +++ b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx @@ -20,7 +20,7 @@ import { } from '@documenso/lib/utils/embed-config'; import { prisma } from '@documenso/prisma'; import { trpc } from '@documenso/trpc/react'; -import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types'; +import type { TCreateEmbeddingEnvelopePayload } from '@documenso/trpc/server/embedding-router/create-embedding-envelope.types'; import { Spinner } from '@documenso/ui/primitives/spinner'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { Trans, useLingui } from '@lingui/react/macro'; @@ -176,7 +176,7 @@ const EnvelopeCreatePage = ({ embedAuthoringOptions }: EnvelopeCreatePageProps) const buildCreateEnvelopeRequest = ( envelope: Omit, - ): { payload: TCreateEnvelopePayload; files: File[]; contentImages: File[] } => { + ): { payload: TCreateEmbeddingEnvelopePayload; files: File[]; contentImages: File[] } => { const sortedItems = [...envelope.envelopeItems].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); const itemIdToIndex = new Map(); @@ -248,7 +248,7 @@ const EnvelopeCreatePage = ({ embedAuthoringOptions }: EnvelopeCreatePageProps) const contentImages = imagesToUpload.map((image) => image.file); - const payload: TCreateEnvelopePayload = { + const payload: TCreateEmbeddingEnvelopePayload = { title: envelope.title, type: envelope.type, externalId: envelope.externalId ?? undefined, diff --git a/packages/app-tests/e2e/api/v2/envelope-create-contents.spec.ts b/packages/app-tests/e2e/api/trpc/create-embedding-envelope-contents.spec.ts similarity index 80% rename from packages/app-tests/e2e/api/v2/envelope-create-contents.spec.ts rename to packages/app-tests/e2e/api/trpc/create-embedding-envelope-contents.spec.ts index 9c82b1ad7..de5bc01fe 100644 --- a/packages/app-tests/e2e/api/v2/envelope-create-contents.spec.ts +++ b/packages/app-tests/e2e/api/trpc/create-embedding-envelope-contents.spec.ts @@ -10,13 +10,12 @@ import { import { nanoid } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; import { seedUser } from '@documenso/prisma/seed/users'; -import type { - TCreateEnvelopePayload, - TCreateEnvelopeResponse, -} from '@documenso/trpc/server/envelope-router/create-envelope.types'; +import type { ZCreateEmbeddingEnvelopePayloadSchema } from '@documenso/trpc/server/embedding-router/create-embedding-envelope.types'; +import type { TCreateEnvelopeResponse } from '@documenso/trpc/server/envelope-router/create-envelope.types'; import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types'; -import { type APIRequestContext, expect, test } from '@playwright/test'; +import { type APIRequestContext, type APIResponse, expect, test } from '@playwright/test'; import { EnvelopeType, FieldType, RecipientRole, type Team } from '@prisma/client'; +import type { z } from 'zod'; import { createGifLabelledAsPng, createImageFile, type TestImageFile } from '../../fixtures/contents'; @@ -35,21 +34,32 @@ type TCreateContentInput = { imageIndex?: number; }; +/** + * The payload as sent, i.e. before any defaults are applied. + */ +type TCreateEmbeddingEnvelopePayloadInput = z.input; + type CreateEnvelopeWithContentsOptions = { request: APIRequestContext; - token: string; - payload?: Partial>; + + /** + * A presign token, since only the embedded editor may create an envelope + * with contents. + */ + presignToken: string; + payload?: Partial>; contents: TCreateContentInput[]; pdfNames?: string[]; images?: TestImageFile[]; }; /** - * Create an envelope with contents through the public create route. + * Create an envelope with contents through the embedded create route, as the + * embedded editor does. */ const createEnvelopeWithContents = async ({ request, - token, + presignToken, payload = {}, contents, pdfNames = ['example.pdf'], @@ -75,12 +85,38 @@ const createEnvelopeWithContents = async ({ formData.append('contentImages', new File([image.buffer], image.name, { type: image.mimeType })); } - return await request.post(`${baseUrl}/envelope/create`, { - headers: { Authorization: `Bearer ${token}` }, + return await request.post(`${WEBAPP_BASE_URL}/api/trpc/embeddingPresign.createEmbeddingEnvelope`, { + headers: { authorization: `Bearer ${presignToken}` }, multipart: formData, }); }; +/** + * The ID of the envelope a successful embedded create returned. tRPC wraps + * the result, unlike the OpenAPI routes. + */ +const getCreatedEnvelopeId = async (res: APIResponse) => { + const body: { result: { data: { json: TCreateEnvelopeResponse } } } = await res.json(); + + return body.result.data.json.id; +}; + +/** + * A presign token for the team, as the embedded editor is given. + */ +const createPresignToken = async (request: APIRequestContext, apiToken: string) => { + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2/embedding/create-presign-token`, { + headers: { Authorization: `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, + data: {}, + }); + + expect(res.ok(), await res.text()).toBeTruthy(); + + const { token }: { token: string } = await res.json(); + + return token; +}; + const getEnvelope = async (request: APIRequestContext, token: string, envelopeId: string) => { const res = await request.get(`${baseUrl}/envelope/${envelopeId}`, { headers: { Authorization: `Bearer ${token}` }, @@ -154,14 +190,13 @@ const imageMeta = (positionY: number): TEnvelopeContentMetaInput => ({ test.describe('Create envelope with contents', () => { let team: Team; - let userId: number; let token: string; + let presignToken: string; - test.beforeEach(async () => { + test.beforeEach(async ({ request }) => { const seeded = await seedUser(); team = seeded.team; - userId = seeded.user.id; ({ token } = await createApiToken({ userId: seeded.user.id, @@ -169,6 +204,8 @@ test.describe('Create envelope with contents', () => { tokenName: 'test-envelope-create-contents', expiresIn: null, })); + + presignToken = await createPresignToken(request, token); }); for (const envelopeType of [EnvelopeType.DOCUMENT, EnvelopeType.TEMPLATE]) { @@ -177,7 +214,7 @@ test.describe('Create envelope with contents', () => { const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { type: envelopeType }, contents: [ { contentMeta: textMeta('Hello') }, @@ -193,7 +230,7 @@ test.describe('Create envelope with contents', () => { expect(res.ok(), await res.text()).toBeTruthy(); - const { id } = (await res.json()) as TCreateEnvelopeResponse; + const id = await getCreatedEnvelopeId(res); // Contents are returned by the public get route. const envelope = await getEnvelope(request, token, id); @@ -234,7 +271,7 @@ test.describe('Create envelope with contents', () => { test('places each content on the file it identifies', async ({ request }) => { const res = await createEnvelopeWithContents({ request, - token, + presignToken, pdfNames: ['first.pdf', 'second.pdf'], contents: [ { contentMeta: textMeta('Default') }, @@ -245,7 +282,7 @@ test.describe('Create envelope with contents', () => { expect(res.ok(), await res.text()).toBeTruthy(); - const { id } = (await res.json()) as TCreateEnvelopeResponse; + const id = await getCreatedEnvelopeId(res); const envelope = await getEnvelope(request, token, id); const [first, second] = [...envelope.envelopeItems].sort((a, b) => a.order - b.order); @@ -276,7 +313,7 @@ test.describe('Create envelope with contents', () => { const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { externalId }, contents: [ { contentMeta: imageMeta(40), imageIndex: 0 }, @@ -302,7 +339,7 @@ test.describe('Create envelope with contents', () => { // The example PDF has a single page. const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { externalId }, contents: [{ contentMeta: { ...imageMeta(40), page: 2 }, imageIndex: 0 }], images: [image], @@ -322,7 +359,7 @@ test.describe('Create envelope with contents', () => { const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { externalId }, contents: [{ contentMeta: imageMeta(40), imageIndex: 1 }], images: [await createImageFile(`index-${nanoid()}.png`, 50, 50)], @@ -340,7 +377,7 @@ test.describe('Create envelope with contents', () => { const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { externalId }, contents: [{ contentMeta: textMeta('Not an image'), imageIndex: 0 }], images: [image], @@ -364,7 +401,7 @@ test.describe('Create envelope with contents', () => { // Only "example.pdf" is uploaded, at index 0. const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { externalId }, contents: [{ identifier, contentMeta: imageMeta(40), imageIndex: 0 }], images: [image], @@ -391,7 +428,7 @@ test.describe('Create envelope with contents', () => { const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { externalId }, contents: [{ identifier, contentMeta: textMeta('Bad index') }], }); @@ -405,7 +442,7 @@ test.describe('Create envelope with contents', () => { const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { externalId, recipients: [ @@ -441,7 +478,7 @@ test.describe('Create envelope with contents', () => { const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { recipients: [ { @@ -460,7 +497,7 @@ test.describe('Create envelope with contents', () => { expect(res.ok(), await res.text()).toBeTruthy(); - const { id } = (await res.json()) as TCreateEnvelopeResponse; + const id = await getCreatedEnvelopeId(res); const envelope = await prisma.envelope.findUniqueOrThrow({ where: { id }, @@ -474,9 +511,13 @@ test.describe('Create envelope with contents', () => { const fieldLogs = auditLogs.filter((log) => log.type === 'FIELD_CREATED'); const contentLogs = auditLogs.filter((log) => log.type === 'CONTENT_CREATED'); - // Attributed to the user who made the request. + // Attributed from the request metadata like the editor's own logs. The + // embedded routes carry no audit user, so these are anonymous, the same + // as contents saved through the embedded update route. for (const log of auditLogs) { - expect(log.userId).toBe(userId); + expect(log.userId).toBeNull(); + expect(log.email).toBeNull(); + expect(log.name).toBeNull(); } // One entry per field, carrying the same identifiers the editor's logs do. @@ -522,7 +563,7 @@ test.describe('Create envelope with contents', () => { test('does not log fields or contents on a template', async ({ request }) => { const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { type: EnvelopeType.TEMPLATE, recipients: [ @@ -539,7 +580,7 @@ test.describe('Create envelope with contents', () => { expect(res.ok(), await res.text()).toBeTruthy(); - const { id } = (await res.json()) as TCreateEnvelopeResponse; + const id = await getCreatedEnvelopeId(res); expect(await prisma.documentAuditLog.count({ where: { envelopeId: id } })).toBe(0); }); @@ -550,7 +591,7 @@ test.describe('Create envelope with contents', () => { const res = await createEnvelopeWithContents({ request, - token, + presignToken, payload: { externalId }, contents: [{ contentMeta: imageMeta(40), imageIndex: 0 }], images: [image], @@ -563,7 +604,41 @@ test.describe('Create envelope with contents', () => { expect(await countDataContentsNamed(image.name)).toBe(0); }); - test('documents contents on the public create route', async ({ request }) => { + test('the public create route ignores contents', async ({ request }) => { + const externalId = `e2e-public-contents-${nanoid()}`; + const image = await createImageFile(`public-${nanoid()}.png`, 50, 50); + + const formData = new FormData(); + + // Sent in the embedded shape, which the public route does not accept. + formData.append( + 'payload', + JSON.stringify({ + type: EnvelopeType.DOCUMENT, + title: 'Public Envelope', + externalId, + contents: [{ contentMeta: textMeta('Ignored') }, { contentMeta: imageMeta(40), imageIndex: 0 }], + }), + ); + + formData.append('files', new File([examplePdfBuffer], 'example.pdf', { type: 'application/pdf' })); + formData.append('contentImages', new File([image.buffer], image.name, { type: image.mimeType })); + + const res = await request.post(`${baseUrl}/envelope/create`, { + headers: { Authorization: `Bearer ${token}` }, + multipart: formData, + }); + + expect(res.ok(), await res.text()).toBeTruthy(); + + const { id } = (await res.json()) as TCreateEnvelopeResponse; + + // The envelope is created without them, and the image is never stored. + expect(await prisma.envelopeContent.count({ where: { envelopeId: id } })).toBe(0); + expect(await countDataContentsNamed(image.name)).toBe(0); + }); + + test('does not document contents on the public create route', async ({ request }) => { const res = await request.get(`${WEBAPP_BASE_URL}/api/v2/openapi.json`); expect(res.ok()).toBeTruthy(); @@ -581,16 +656,8 @@ test.describe('Create envelope with contents', () => { 'properties', ]; - expect(openApiDocument).toHaveProperty([...requestSchemaPath, 'contentImages']); - - expect(openApiDocument).toHaveProperty([ - ...requestSchemaPath, - 'payload', - 'properties', - 'contents', - 'items', - 'properties', - 'imageIndex', - ]); + expect(openApiDocument).toHaveProperty([...requestSchemaPath, 'files']); + expect(openApiDocument).not.toHaveProperty([...requestSchemaPath, 'contentImages']); + expect(openApiDocument).not.toHaveProperty([...requestSchemaPath, 'payload', 'properties', 'contents']); }); }); diff --git a/packages/app-tests/e2e/api/trpc/set-envelope-contents-limits.spec.ts b/packages/app-tests/e2e/api/trpc/set-envelope-contents-limits.spec.ts index bf2a56c91..4b1aa378b 100644 --- a/packages/app-tests/e2e/api/trpc/set-envelope-contents-limits.spec.ts +++ b/packages/app-tests/e2e/api/trpc/set-envelope-contents-limits.spec.ts @@ -1,4 +1,5 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { CONTENT_TEXT_MAX_LENGTH } from '@documenso/lib/constants/envelope-content'; import { CONTENT_MAX_Z_INDEX, EnvelopeContentType, @@ -26,7 +27,7 @@ test.describe.configure({ * its organisation's allowance. */ -const textMeta = (index: number, overrides: { zIndex?: number } = {}): TEnvelopeContentMetaInput => ({ +const textMeta = (index: number, overrides: { zIndex?: number; text?: string } = {}): TEnvelopeContentMetaInput => ({ type: EnvelopeContentType.TEXT, page: 1, positionX: 10, @@ -216,3 +217,21 @@ test('rejects a stacking order above the authored ceiling', async ({ page }) => expect(res.ok()).toBeFalsy(); expect(await getContentCount(envelope.id)).toBe(0); }); + +test('rejects a text longer than the cap', async ({ page }) => { + const { envelope, envelopeItemId, teamId } = await setupEnvelope(page, {}); + + const atCap = await setContents(page, teamId, envelope.id, [ + { envelopeItemId, contentMeta: textMeta(0, { text: 'a'.repeat(CONTENT_TEXT_MAX_LENGTH) }) }, + ]); + + expect(atCap.ok()).toBeTruthy(); + expect(await getContentCount(envelope.id)).toBe(1); + + const overCap = await setContents(page, teamId, envelope.id, [ + { envelopeItemId, contentMeta: textMeta(0, { text: 'a'.repeat(CONTENT_TEXT_MAX_LENGTH + 1) }) }, + ]); + + expect(overCap.ok()).toBeFalsy(); + expect(await getContentCount(envelope.id)).toBe(1); +}); diff --git a/packages/app-tests/e2e/api/v2/envelope-copy-contents.spec.ts b/packages/app-tests/e2e/api/v2/envelope-copy-contents.spec.ts index 0cf3df241..d627f4db5 100644 --- a/packages/app-tests/e2e/api/v2/envelope-copy-contents.spec.ts +++ b/packages/app-tests/e2e/api/v2/envelope-copy-contents.spec.ts @@ -53,8 +53,12 @@ const IMAGE_META: TEnvelopeContentMetaInput = { }; /** - * Create an envelope with two files through the public create route, with a - * text on each file and an image on the second. + * Create an envelope with two files, with a text on each file and an image on + * the second. + * + * Goes through the embedded create route, since only the embedded editor may + * create an envelope with contents, using a presign token minted from the + * API token. */ const createSourceEnvelope = async ( request: APIRequestContext, @@ -62,6 +66,15 @@ const createSourceEnvelope = async ( type: EnvelopeType, options: { withSignerFields?: boolean } = {}, ) => { + const presignRes = await request.post(`${WEBAPP_BASE_URL}/api/v2/embedding/create-presign-token`, { + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + data: {}, + }); + + expect(presignRes.ok(), await presignRes.text()).toBeTruthy(); + + const { token: presignToken }: { token: string } = await presignRes.json(); + const image = await createImageFile(`copy-${nanoid()}.png`, 120, 60); const formData = new FormData(); @@ -98,16 +111,17 @@ const createSourceEnvelope = async ( formData.append('contentImages', new File([image.buffer], image.name, { type: image.mimeType })); - const res = await request.post(`${baseUrl}/envelope/create`, { - headers: { Authorization: `Bearer ${token}` }, + const res = await request.post(`${WEBAPP_BASE_URL}/api/trpc/embeddingPresign.createEmbeddingEnvelope`, { + headers: { authorization: `Bearer ${presignToken}` }, multipart: formData, }); expect(res.ok(), await res.text()).toBeTruthy(); - const { id } = (await res.json()) as TCreateEnvelopeResponse; + // tRPC wraps the result, unlike the OpenAPI routes. + const body: { result: { data: { json: TCreateEnvelopeResponse } } } = await res.json(); - return id; + return body.result.data.json.id; }; /** @@ -226,10 +240,13 @@ const getCopyRoute = (name: string): CopyRoute => { test.describe('Copy contents to new envelopes', () => { let token: string; + let teamName: string; test.beforeEach(async () => { const { user, team } = await seedUser(); + teamName = team.name; + ({ token } = await createApiToken({ userId: user.id, teamId: team.id, @@ -284,6 +301,12 @@ test.describe('Copy contents to new envelopes', () => { expect(logsOfType('DOCUMENT_CREATED')).toHaveLength(1); expect(logsOfType('DOCUMENT_CREATED')[0].data).toMatchObject({ title: copy.title }); + // Attributed from the request metadata, which for a team API token is the team. + for (const log of auditLogs) { + expect(log.userId).toBeNull(); + expect(log.name).toBe(teamName); + } + // One entry per copied field, naming the copy's own field and recipient IDs. const [signer] = copy.recipients; diff --git a/packages/lib/constants/envelope-content.ts b/packages/lib/constants/envelope-content.ts index 44ed395bc..b3429c811 100644 --- a/packages/lib/constants/envelope-content.ts +++ b/packages/lib/constants/envelope-content.ts @@ -29,6 +29,15 @@ export const CONTENT_IMAGE_MAX_EDGE = 2048; */ export const CONTENT_IMAGE_MAX_INPUT_PIXELS = 25_000_000; +/** + * The longest text a text content may hold. + * + * Text is the only free-form value on a content, and it is recorded in full + * in the audit log on every save, so it is capped the same as a document + * message. + */ +export const CONTENT_TEXT_MAX_LENGTH = 5_000; + /** * The number of contents allowed on a single envelope. * diff --git a/packages/lib/server-only/envelope/create-envelope.ts b/packages/lib/server-only/envelope/create-envelope.ts index 52d046b75..7d1b3ef36 100644 --- a/packages/lib/server-only/envelope/create-envelope.ts +++ b/packages/lib/server-only/envelope/create-envelope.ts @@ -726,9 +726,6 @@ export const createEnvelope = async ({ return createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED, envelopeId: envelope.id, - user: { - id: userId, - }, metadata: requestMetadata, data: { fieldId: field.secondaryId, @@ -743,9 +740,6 @@ export const createEnvelope = async ({ createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.CONTENT_CREATED, envelopeId: envelope.id, - user: { - id: userId, - }, metadata: requestMetadata, data: { contentId: content.id, diff --git a/packages/lib/server-only/envelope/duplicate-envelope.ts b/packages/lib/server-only/envelope/duplicate-envelope.ts index a5a0fc0f1..fe94dbd2f 100644 --- a/packages/lib/server-only/envelope/duplicate-envelope.ts +++ b/packages/lib/server-only/envelope/duplicate-envelope.ts @@ -209,9 +209,6 @@ export const duplicateEnvelope = async ({ createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED, envelopeId: duplicatedEnvelope.id, - user: { - id: userId, - }, metadata: requestMetadata, data: { title: duplicatedEnvelope.title, @@ -268,9 +265,6 @@ export const duplicateEnvelope = async ({ createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED, envelopeId: duplicatedEnvelope.id, - user: { - id: userId, - }, metadata: requestMetadata, data: { fieldId: field.secondaryId, @@ -304,9 +298,6 @@ export const duplicateEnvelope = async ({ createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.CONTENT_CREATED, envelopeId: duplicatedEnvelope.id, - user: { - id: userId, - }, metadata: requestMetadata, data: { contentId: content.id, diff --git a/packages/lib/types/envelope-content-meta.ts b/packages/lib/types/envelope-content-meta.ts index 931da8f8d..776e411fd 100644 --- a/packages/lib/types/envelope-content-meta.ts +++ b/packages/lib/types/envelope-content-meta.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { CONTENT_TEXT_MAX_LENGTH } from '../constants/envelope-content'; import { DataContentType } from './data-content-meta'; // Note: The default is different to fields (12), the range is the same. @@ -237,7 +238,7 @@ export const ZBasePositionalContentMetaSchema = z.object({ export const ZContentTextMetaSchema = ZBasePositionalContentMetaSchema.extend({ type: z.literal(EnvelopeContentType.TEXT), - text: z.string().optional().default(''), + text: z.string().max(CONTENT_TEXT_MAX_LENGTH).optional().default(''), textAlign: ZContentTextAlignSchema.optional().default(DEFAULT_CONTENT_TEXT_ALIGN), verticalAlign: ZContentVerticalAlignSchema.optional().default(DEFAULT_CONTENT_VERTICAL_ALIGN), lineHeight: ZContentLineHeightSchema.optional().default(DEFAULT_CONTENT_LINE_HEIGHT), diff --git a/packages/trpc/server/embedding-router/create-embedding-envelope.types.ts b/packages/trpc/server/embedding-router/create-embedding-envelope.types.ts index 4e178178b..51d068f22 100644 --- a/packages/trpc/server/embedding-router/create-embedding-envelope.types.ts +++ b/packages/trpc/server/embedding-router/create-embedding-envelope.types.ts @@ -1,5 +1,41 @@ -import { ZCreateEnvelopeRequestSchema, ZCreateEnvelopeResponseSchema } from '../envelope-router/create-envelope.types'; +import { ZEnvelopeContentMetaSchema } from '@documenso/lib/types/envelope-content-meta'; +import { z } from 'zod'; +import { zfd } from 'zod-form-data'; -export const ZCreateEmbeddingEnvelopeRequestSchema = ZCreateEnvelopeRequestSchema; +import { zfdContentImageFile, zfdFile, zodFormData } from '../../utils/zod-form-data'; +import { ZCreateEnvelopePayloadSchema, ZCreateEnvelopeResponseSchema } from '../envelope-router/create-envelope.types'; + +/** + * Extend the create envelope route with contents since embeds set all the data at once. + */ +export const ZCreateEmbeddingEnvelopePayloadSchema = ZCreateEnvelopePayloadSchema.extend({ + contents: z + .object({ + identifier: z + .union([z.string(), z.number().int().min(0)]) + .describe('Either the filename or the zero-based index of the file that was uploaded to attach the content to.') + .optional(), + contentMeta: ZEnvelopeContentMetaSchema.describe('All the properties of the content being placed.'), + imageIndex: z + .number() + .int() + .min(0) + .describe( + 'The index of the file in `contentImages` to show in the content. Only image contents can hold an image.', + ) + .optional(), + }) + .array() + .optional(), +}); + +export const ZCreateEmbeddingEnvelopeRequestSchema = zodFormData({ + payload: zfd.json(ZCreateEmbeddingEnvelopePayloadSchema), + files: zfd.repeatableOfType(zfdFile()), + contentImages: zfd.repeatableOfType(zfdContentImageFile()), +}); export const ZCreateEmbeddingEnvelopeResponseSchema = ZCreateEnvelopeResponseSchema; + +export type TCreateEmbeddingEnvelopePayload = z.infer; +export type TCreateEmbeddingEnvelopeRequest = z.infer; diff --git a/packages/trpc/server/envelope-router/create-envelope.ts b/packages/trpc/server/envelope-router/create-envelope.ts index e220a581e..4e9d0edfa 100644 --- a/packages/trpc/server/envelope-router/create-envelope.ts +++ b/packages/trpc/server/envelope-router/create-envelope.ts @@ -13,8 +13,8 @@ import type { Logger } from 'pino'; import { match, P } from 'ts-pattern'; import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf'; +import type { TCreateEmbeddingEnvelopeRequest } from '../embedding-router/create-embedding-envelope.types'; import { authenticatedProcedure } from '../trpc'; -import type { TCreateEnvelopeRequest } from './create-envelope.types'; import { createEnvelopeMeta, ZCreateEnvelopeRequestSchema, @@ -51,7 +51,16 @@ type CreateEnvelopeRouteOptions = { * Unverified team ID. */ teamId: number; - input: TCreateEnvelopeRequest; + + /** + * This function is used by both embeds and normal flows. + * + * Since embeds also allow setting contents, we need to remap the type to allow + * undefined contents for normal flows. + */ + input: Omit & { + contentImages?: File[]; + }; apiRequestMetadata: ApiRequestMetadata; /** @@ -73,7 +82,7 @@ export const createEnvelopeRouteCaller = async ({ logger, options = {}, }: CreateEnvelopeRouteOptions) => { - const { payload, files, contentImages } = input; + const { payload, files, contentImages = [] } = input; const { title, diff --git a/packages/trpc/server/envelope-router/create-envelope.types.ts b/packages/trpc/server/envelope-router/create-envelope.types.ts index d16919866..de6d3991f 100644 --- a/packages/trpc/server/envelope-router/create-envelope.types.ts +++ b/packages/trpc/server/envelope-router/create-envelope.types.ts @@ -2,7 +2,6 @@ import { ZDocumentAccessAuthTypesSchema, ZDocumentActionAuthTypesSchema } from ' import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values'; import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta'; import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; -import { ZEnvelopeContentMetaSchema } from '@documenso/lib/types/envelope-content-meta'; import { ZClampedFieldHeightSchema, ZClampedFieldPositionXSchema, @@ -16,7 +15,7 @@ import { EnvelopeType } from '@prisma/client'; import { z } from 'zod'; import { zfd } from 'zod-form-data'; -import { zfdContentImageFile, zfdFile, zodFormData } from '../../utils/zod-form-data'; +import { zfdFile, zodFormData } from '../../utils/zod-form-data'; import { ZDocumentExternalIdSchema, ZDocumentTitleSchema, ZDocumentVisibilitySchema } from '../document-router/schema'; import type { TrpcRouteMeta } from '../trpc'; import { ZCreateEnvelopeRecipientSchema } from './envelope-recipients/create-envelope-recipients.types'; @@ -32,6 +31,9 @@ export const createEnvelopeMeta: TrpcRouteMeta = { }, }; +/** + * Note: This is reused in the embedding routes. + */ export const ZCreateEnvelopePayloadSchema = z.object({ title: ZDocumentTitleSchema, type: z.nativeEnum(EnvelopeType), @@ -70,24 +72,6 @@ export const ZCreateEnvelopePayloadSchema = z.object({ }), ) .optional(), - contents: z - .object({ - identifier: z - .union([z.string(), z.number().int().min(0)]) - .describe('Either the filename or the zero-based index of the file that was uploaded to attach the content to.') - .optional(), - contentMeta: ZEnvelopeContentMetaSchema.describe('All the properties of the content being placed.'), - imageIndex: z - .number() - .int() - .min(0) - .describe( - 'The index of the file in `contentImages` to show in the content. Only image contents can hold an image.', - ) - .optional(), - }) - .array() - .optional(), meta: ZDocumentMetaCreateSchema.optional(), attachments: z .array( @@ -103,7 +87,6 @@ export const ZCreateEnvelopePayloadSchema = z.object({ export const ZCreateEnvelopeRequestSchema = zodFormData({ payload: zfd.json(ZCreateEnvelopePayloadSchema), files: zfd.repeatableOfType(zfdFile()), - contentImages: zfd.repeatableOfType(zfdContentImageFile()), }); export const ZCreateEnvelopeResponseSchema = z.object({