fix: remove contents from public envelope create route

This commit is contained in:
David Nguyen
2026-10-01 15:39:21 +10:00
parent 110f307d36
commit 656d1347b9
14 changed files with 235 additions and 100 deletions
@@ -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
@@ -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}
/>
</FormControl>
@@ -270,7 +270,7 @@ export const EnvelopeEditorFieldsPage = () => {
const { allowContents } = editorConfig.actions;
if (!editorConfig.general?.allowAddFieldsStep) {
if (!editorConfig.general.allowAddFieldsStep) {
return null;
}
@@ -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<TEditorEnvelope, 'id'>,
): { 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<string, number>();
@@ -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,
@@ -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<typeof ZCreateEmbeddingEnvelopePayloadSchema>;
type CreateEnvelopeWithContentsOptions = {
request: APIRequestContext;
token: string;
payload?: Partial<Omit<TCreateEnvelopePayload, 'contents'>>;
/**
* A presign token, since only the embedded editor may create an envelope
* with contents.
*/
presignToken: string;
payload?: Partial<Omit<TCreateEmbeddingEnvelopePayloadInput, 'contents'>>;
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']);
});
});
@@ -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);
});
@@ -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;
@@ -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.
*
@@ -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,
@@ -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,
+2 -1
View File
@@ -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),
@@ -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<typeof ZCreateEmbeddingEnvelopePayloadSchema>;
export type TCreateEmbeddingEnvelopeRequest = z.infer<typeof ZCreateEmbeddingEnvelopeRequestSchema>;
@@ -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<TCreateEmbeddingEnvelopeRequest, 'contentImages'> & {
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,
@@ -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({