feat: support embedded authoring for creation (#1741)

Adds support for creating documents and templates
using our embed components.

Support is super primitive at the moment and is being polished.
This commit is contained in:
Lucas Smith
2025-04-11 00:20:39 +10:00
committed by GitHub
parent 95aae52fa4
commit e613e0e347
42 changed files with 3849 additions and 137 deletions

View File

@ -0,0 +1,14 @@
import { router } from '../trpc';
import { createEmbeddingDocumentRoute } from './create-embedding-document';
import { createEmbeddingPresignTokenRoute } from './create-embedding-presign-token';
import { createEmbeddingTemplateRoute } from './create-embedding-template';
import { getEmbeddingDocumentRoute } from './get-embedding-document';
import { verifyEmbeddingPresignTokenRoute } from './verify-embedding-presign-token';
export const embeddingPresignRouter = router({
createEmbeddingPresignToken: createEmbeddingPresignTokenRoute,
verifyEmbeddingPresignToken: verifyEmbeddingPresignTokenRoute,
createEmbeddingDocument: createEmbeddingDocumentRoute,
createEmbeddingTemplate: createEmbeddingTemplateRoute,
getEmbeddingDocument: getEmbeddingDocumentRoute,
});

View File

@ -0,0 +1,63 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createDocumentV2 } from '@documenso/lib/server-only/document/create-document-v2';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { procedure } from '../trpc';
import {
ZCreateEmbeddingDocumentRequestSchema,
ZCreateEmbeddingDocumentResponseSchema,
} from './create-embedding-document.types';
export const createEmbeddingDocumentRoute = procedure
.input(ZCreateEmbeddingDocumentRequestSchema)
.output(ZCreateEmbeddingDocumentResponseSchema)
.mutation(async ({ input, ctx: { req, metadata } }) => {
try {
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 { title, documentDataId, externalId, recipients, meta } = input;
const document = await createDocumentV2({
data: {
title,
externalId,
recipients,
},
meta,
documentDataId,
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
requestMetadata: metadata,
});
if (!document.id) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to create document: missing document ID',
});
}
return {
documentId: document.id,
};
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to create document',
});
}
});

View File

@ -0,0 +1,83 @@
import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { DocumentSigningOrder } from '@documenso/prisma/generated/types';
import {
ZDocumentExternalIdSchema,
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
} from '../document-router/schema';
import { ZCreateRecipientSchema } from '../recipient-router/schema';
export const ZCreateEmbeddingDocumentRequestSchema = z.object({
title: ZDocumentTitleSchema,
documentDataId: z.string(),
externalId: ZDocumentExternalIdSchema.optional(),
recipients: z
.array(
ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and(
z.object({
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.optional(),
}),
)
.refine(
(recipients) => {
const emails = recipients.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
},
{ message: 'Recipients must have unique emails' },
)
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export const ZCreateEmbeddingDocumentResponseSchema = z.object({
documentId: z.number(),
});
export type TCreateEmbeddingDocumentRequestSchema = z.infer<
typeof ZCreateEmbeddingDocumentRequestSchema
>;

View File

@ -0,0 +1,73 @@
import { isCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/create-embedding-presign-token';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import { procedure } from '../trpc';
import {
ZCreateEmbeddingPresignTokenRequestSchema,
ZCreateEmbeddingPresignTokenResponseSchema,
createEmbeddingPresignTokenMeta,
} from './create-embedding-presign-token.types';
/**
* Route to create embedding presign tokens.
*/
export const createEmbeddingPresignTokenRoute = procedure
.meta(createEmbeddingPresignTokenMeta)
.input(ZCreateEmbeddingPresignTokenRequestSchema)
.output(ZCreateEmbeddingPresignTokenResponseSchema)
.mutation(async ({ input, ctx: { req } }) => {
try {
const authorizationHeader = req.headers.get('authorization');
const [apiToken] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
if (!apiToken) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'No API token provided',
});
}
const { expiresIn } = input;
if (IS_BILLING_ENABLED()) {
const token = await getApiTokenByToken({ token: apiToken });
if (!token.userId) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Invalid API token',
});
}
const [hasCommunityPlan, hasPlatformPlan, hasEnterprisePlan] = await Promise.all([
isCommunityPlan({ userId: token.userId, teamId: token.teamId ?? undefined }),
isDocumentPlatform({ userId: token.userId, teamId: token.teamId }),
isUserEnterprise({ userId: token.userId, teamId: token.teamId ?? undefined }),
]);
if (!hasCommunityPlan && !hasPlatformPlan && !hasEnterprisePlan) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to create embedding presign tokens',
});
}
}
const presignToken = await createEmbeddingPresignToken({
apiToken,
expiresIn,
});
return { ...presignToken };
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to create embedding presign token',
});
}
});

View File

@ -0,0 +1,38 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../trpc';
export const createEmbeddingPresignTokenMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/embedding/create-presign-token',
summary: 'Create embedding presign token',
description:
'Creates a presign token for embedding operations with configurable expiration time',
tags: ['Embedding'],
},
};
export const ZCreateEmbeddingPresignTokenRequestSchema = z.object({
expiresIn: z
.number()
.min(0)
.max(10080)
.optional()
.default(60)
.describe('Expiration time in minutes (default: 60, max: 10,080)'),
});
export const ZCreateEmbeddingPresignTokenResponseSchema = z.object({
token: z.string(),
expiresAt: z.date(),
expiresIn: z.number().describe('Expiration time in seconds'),
});
export type TCreateEmbeddingPresignTokenRequestSchema = z.infer<
typeof ZCreateEmbeddingPresignTokenRequestSchema
>;
export type TCreateEmbeddingPresignTokenResponseSchema = z.infer<
typeof ZCreateEmbeddingPresignTokenResponseSchema
>;

View File

@ -0,0 +1,112 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { createTemplate } from '@documenso/lib/server-only/template/create-template';
import { prisma } from '@documenso/prisma';
import { procedure } from '../trpc';
import {
ZCreateEmbeddingTemplateRequestSchema,
ZCreateEmbeddingTemplateResponseSchema,
} from './create-embedding-template.types';
export const createEmbeddingTemplateRoute = procedure
.input(ZCreateEmbeddingTemplateRequestSchema)
.output(ZCreateEmbeddingTemplateResponseSchema)
.mutation(async ({ input, ctx: { req } }) => {
try {
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 { title, documentDataId, recipients, meta } = input;
// First create the template
const template = await createTemplate({
userId: apiToken.userId,
title,
templateDocumentDataId: documentDataId,
teamId: apiToken.teamId ?? undefined,
});
await Promise.all(
recipients.map(async (recipient) => {
const createdRecipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: recipient.email,
name: recipient.name || '',
role: recipient.role || 'SIGNER',
token: `template-${template.id}-${recipient.email}`,
signingOrder: recipient.signingOrder,
},
});
const fields = recipient.fields ?? [];
const createdFields = await prisma.field.createMany({
data: fields.map((field) => ({
recipientId: createdRecipient.id,
type: field.type,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
templateId: template.id,
})),
});
return {
...createdRecipient,
fields: createdFields,
};
}),
);
// Update the template meta if needed
if (meta) {
await prisma.templateMeta.upsert({
where: {
templateId: template.id,
},
create: {
templateId: template.id,
...meta,
},
update: {
...meta,
},
});
}
if (!template.id) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to create template: missing template ID',
});
}
return {
templateId: template.id,
};
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to create template',
});
}
});

View File

@ -0,0 +1,74 @@
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
} from '../document-router/schema';
const ZFieldSchema = z.object({
type: z.nativeEnum(FieldType),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
fieldMeta: ZFieldMetaSchema.optional(),
});
export const ZCreateEmbeddingTemplateRequestSchema = z.object({
title: ZDocumentTitleSchema,
documentDataId: z.string(),
recipients: z.array(
z.object({
email: z.string().email(),
name: z.string().optional(),
role: z.nativeEnum(RecipientRole).optional(),
signingOrder: z.number().optional(),
fields: z.array(ZFieldSchema).optional(),
}),
),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export const ZCreateEmbeddingTemplateResponseSchema = z.object({
templateId: z.number(),
});
export type TCreateEmbeddingTemplateRequestSchema = z.infer<
typeof ZCreateEmbeddingTemplateRequestSchema
>;

View File

@ -0,0 +1,63 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { prisma } from '@documenso/prisma';
import { procedure } from '../trpc';
import {
ZGetEmbeddingDocumentRequestSchema,
ZGetEmbeddingDocumentResponseSchema,
} from './get-embedding-document.types';
export const getEmbeddingDocumentRoute = procedure
.input(ZGetEmbeddingDocumentRequestSchema)
.output(ZGetEmbeddingDocumentResponseSchema)
.query(async ({ input, ctx: { req } }) => {
try {
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 { documentId } = input;
const document = await prisma.document.findFirst({
where: {
id: documentId,
userId: apiToken.userId,
...(apiToken.teamId ? { teamId: apiToken.teamId } : {}),
},
include: {
documentData: true,
recipients: true,
fields: true,
},
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
return {
document,
};
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to get document',
});
}
});

View File

@ -0,0 +1,34 @@
import { DocumentDataType, type Field, type Recipient } from '@prisma/client';
import { z } from 'zod';
export const ZGetEmbeddingDocumentRequestSchema = z.object({
documentId: z.number(),
});
export const ZGetEmbeddingDocumentResponseSchema = z.object({
document: z
.object({
id: z.number(),
title: z.string(),
status: z.string(),
documentDataId: z.string(),
userId: z.number(),
teamId: z.number().nullable(),
createdAt: z.date(),
updatedAt: z.date(),
documentData: z.object({
id: z.string(),
type: z.nativeEnum(DocumentDataType),
data: z.string(),
initialData: z.string(),
}),
recipients: z.array(z.custom<Recipient>()),
fields: z.array(z.custom<Field>()),
})
.nullable(),
});
export type TGetEmbeddingDocumentRequestSchema = z.infer<typeof ZGetEmbeddingDocumentRequestSchema>;
export type TGetEmbeddingDocumentResponseSchema = z.infer<
typeof ZGetEmbeddingDocumentResponseSchema
>;

View File

@ -0,0 +1,36 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { procedure } from '../trpc';
import {
ZVerifyEmbeddingPresignTokenRequestSchema,
ZVerifyEmbeddingPresignTokenResponseSchema,
verifyEmbeddingPresignTokenMeta,
} from './verify-embedding-presign-token.types';
/**
* Public route.
*/
export const verifyEmbeddingPresignTokenRoute = procedure
.meta(verifyEmbeddingPresignTokenMeta)
.input(ZVerifyEmbeddingPresignTokenRequestSchema)
.output(ZVerifyEmbeddingPresignTokenResponseSchema)
.mutation(async ({ input }) => {
try {
const { token } = input;
const apiToken = await verifyEmbeddingPresignToken({
token,
}).catch(() => null);
return { success: !!apiToken };
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to verify embedding presign token',
});
}
});

View File

@ -0,0 +1,33 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../trpc';
export const verifyEmbeddingPresignTokenMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/embedding/verify-presign-token',
summary: 'Verify embedding presign token',
description:
'Verifies a presign token for embedding operations and returns the associated API token',
tags: ['Embedding'],
},
};
export const ZVerifyEmbeddingPresignTokenRequestSchema = z.object({
token: z
.string()
.min(1, { message: 'Token is required' })
.describe('The presign token to verify'),
});
export const ZVerifyEmbeddingPresignTokenResponseSchema = z.object({
success: z.boolean(),
});
export type TVerifyEmbeddingPresignTokenRequestSchema = z.infer<
typeof ZVerifyEmbeddingPresignTokenRequestSchema
>;
export type TVerifyEmbeddingPresignTokenResponseSchema = z.infer<
typeof ZVerifyEmbeddingPresignTokenResponseSchema
>;

View File

@ -2,6 +2,7 @@ import { adminRouter } from './admin-router/router';
import { apiTokenRouter } from './api-token-router/router';
import { authRouter } from './auth-router/router';
import { documentRouter } from './document-router/router';
import { embeddingPresignRouter } from './embedding-router/_router';
import { fieldRouter } from './field-router/router';
import { profileRouter } from './profile-router/router';
import { recipientRouter } from './recipient-router/router';
@ -23,6 +24,7 @@ export const appRouter = router({
team: teamRouter,
template: templateRouter,
webhook: webhookRouter,
embeddingPresign: embeddingPresignRouter,
});
export type AppRouter = typeof appRouter;