chore: merged main

This commit is contained in:
Catalin Documenso
2025-05-07 11:17:15 +03:00
150 changed files with 14851 additions and 616 deletions

View File

@ -107,8 +107,17 @@ export const documentRouter = router({
.query(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { query, templateId, page, perPage, orderByDirection, orderByColumn, source, status } =
input;
const {
query,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
folderId,
} = input;
const documents = await findDocuments({
userId: user.id,
@ -119,6 +128,7 @@ export const documentRouter = router({
status,
page,
perPage,
folderId,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
});
@ -147,12 +157,14 @@ export const documentRouter = router({
status,
period,
senderIds,
folderId,
} = input;
const getStatOptions: GetStatsInput = {
user,
period,
search: query,
folderId,
};
if (teamId) {
@ -181,6 +193,7 @@ export const documentRouter = router({
status,
period,
senderIds,
folderId,
orderBy: orderByColumn
? { column: orderByColumn, direction: orderByDirection }
: undefined,
@ -212,12 +225,13 @@ export const documentRouter = router({
.output(ZGetDocumentWithDetailsByIdResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId } = input;
const { documentId, folderId } = input;
return await getDocumentWithDetailsById({
userId: user.id,
teamId,
documentId,
folderId,
});
}),
@ -290,6 +304,7 @@ export const documentRouter = router({
return {
document: createdDocument,
folder: createdDocument.folder,
uploadUrl: url,
};
}),
@ -311,7 +326,7 @@ export const documentRouter = router({
.input(ZCreateDocumentRequestSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { title, documentDataId, timezone } = input;
const { title, documentDataId, timezone, folderId } = input;
const { remaining } = await getServerLimits({ email: ctx.user.email, teamId });
@ -330,6 +345,7 @@ export const documentRouter = router({
normalizePdf: true,
timezone,
requestMetadata: ctx.metadata,
folderId,
});
}),

View File

@ -130,6 +130,7 @@ export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
.nativeEnum(DocumentStatus)
.describe('Filter documents by the current status')
.optional(),
folderId: z.string().describe('Filter documents by folder ID').optional(),
orderByColumn: z.enum(['createdAt']).optional(),
orderByDirection: z.enum(['asc', 'desc']).describe('').default('desc'),
});
@ -144,6 +145,7 @@ export const ZFindDocumentsInternalRequestSchema = ZFindDocumentsRequestSchema.e
period: z.enum(['7d', '14d', '30d']).optional(),
senderIds: z.array(z.number()).optional(),
status: z.nativeEnum(ExtendedDocumentStatus).optional(),
folderId: z.string().optional(),
});
export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({
@ -188,6 +190,7 @@ export type TGetDocumentByTokenQuerySchema = z.infer<typeof ZGetDocumentByTokenQ
export const ZGetDocumentWithDetailsByIdRequestSchema = z.object({
documentId: z.number(),
folderId: z.string().describe('Filter documents by folder ID').optional(),
});
export const ZGetDocumentWithDetailsByIdResponseSchema = ZDocumentSchema;
@ -196,6 +199,7 @@ export const ZCreateDocumentRequestSchema = z.object({
title: ZDocumentTitleSchema,
documentDataId: z.string().min(1),
timezone: ZDocumentMetaTimezoneSchema.optional(),
folderId: z.string().describe('The ID of the folder to create the document in').optional(),
});
export const ZCreateDocumentV2RequestSchema = z.object({

View File

@ -2,7 +2,8 @@ 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 { updateEmbeddingDocumentRoute } from './update-embedding-document';
import { updateEmbeddingTemplateRoute } from './update-embedding-template';
import { verifyEmbeddingPresignTokenRoute } from './verify-embedding-presign-token';
export const embeddingPresignRouter = router({
@ -10,5 +11,6 @@ export const embeddingPresignRouter = router({
verifyEmbeddingPresignToken: verifyEmbeddingPresignTokenRoute,
createEmbeddingDocument: createEmbeddingDocumentRoute,
createEmbeddingTemplate: createEmbeddingTemplateRoute,
getEmbeddingDocument: getEmbeddingDocumentRoute,
updateEmbeddingDocument: updateEmbeddingDocumentRoute,
updateEmbeddingTemplate: updateEmbeddingTemplateRoute,
});

View File

@ -1,10 +1,10 @@
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 { prisma } from '@documenso/prisma';
import { procedure } from '../trpc';
import {
@ -42,13 +42,24 @@ export const createEmbeddingPresignTokenRoute = procedure
});
}
const [hasCommunityPlan, hasPlatformPlan, hasEnterprisePlan] = await Promise.all([
const [hasCommunityPlan, 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) {
let hasTeamAuthoringFlag = false;
if (token.teamId) {
const teamGlobalSettings = await prisma.teamGlobalSettings.findFirst({
where: {
teamId: token.teamId,
},
});
hasTeamAuthoringFlag = teamGlobalSettings?.allowEmbeddedAuthoring ?? false;
}
if (!hasCommunityPlan && !hasEnterprisePlan && !hasTeamAuthoringFlag) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to create embedding presign tokens',
});

View File

@ -1,63 +0,0 @@
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

@ -1,34 +0,0 @@
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,118 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
import { nanoid } from '@documenso/lib/universal/id';
import { procedure } from '../trpc';
import {
ZUpdateEmbeddingDocumentRequestSchema,
ZUpdateEmbeddingDocumentResponseSchema,
} from './update-embedding-document.types';
export const updateEmbeddingDocumentRoute = procedure
.input(ZUpdateEmbeddingDocumentRequestSchema)
.output(ZUpdateEmbeddingDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
try {
const authorizationHeader = ctx.req.headers.get('authorization');
const [presignToken] = (authorizationHeader || '')
.split('Bearer ')
.filter((s) => s.length > 0);
if (!presignToken) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'No presign token provided',
});
}
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
const { documentId, title, externalId, recipients, meta } = input;
if (meta && Object.values(meta).length > 0) {
await upsertDocumentMeta({
documentId: documentId,
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
...meta,
requestMetadata: ctx.metadata,
});
}
await updateDocument({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
documentId: documentId,
data: {
title,
externalId,
},
requestMetadata: ctx.metadata,
});
const recipientsWithClientId = recipients.map((recipient) => ({
...recipient,
clientId: nanoid(),
}));
const { recipients: updatedRecipients } = await setDocumentRecipients({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
documentId: documentId,
recipients: recipientsWithClientId.map((recipient) => ({
id: recipient.id,
clientId: recipient.clientId,
email: recipient.email,
name: recipient.name ?? '',
role: recipient.role,
signingOrder: recipient.signingOrder,
})),
requestMetadata: ctx.metadata,
});
const fields = recipientsWithClientId.flatMap((recipient) => {
const recipientId = updatedRecipients.find((r) => r.clientId === recipient.clientId)?.id;
if (!recipientId) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Recipient not found',
});
}
return (recipient.fields ?? []).map((field) => ({
...field,
recipientId,
// !: Temp property to be removed once we don't link based on signer email
signerEmail: recipient.email,
}));
});
await setFieldsForDocument({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
documentId,
fields: fields.map((field) => ({
...field,
pageWidth: field.width,
pageHeight: field.height,
})),
requestMetadata: ctx.metadata,
});
return {
documentId,
};
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to update document',
});
}
});

View File

@ -0,0 +1,87 @@
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, RecipientRole } from '@documenso/prisma/generated/types';
import {
ZDocumentExternalIdSchema,
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
} from '../document-router/schema';
export const ZUpdateEmbeddingDocumentRequestSchema = z.object({
documentId: z.number(),
title: ZDocumentTitleSchema,
externalId: ZDocumentExternalIdSchema.optional(),
recipients: z
.array(
z.object({
id: z.number().optional(),
email: z.string().toLowerCase().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
fields: ZFieldAndMetaSchema.and(
z.object({
id: z.number().optional(),
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' },
),
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 ZUpdateEmbeddingDocumentResponseSchema = z.object({
documentId: z.number(),
});
export type TUpdateEmbeddingDocumentRequestSchema = z.infer<
typeof ZUpdateEmbeddingDocumentRequestSchema
>;

View File

@ -0,0 +1,104 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
import { updateTemplate } from '@documenso/lib/server-only/template/update-template';
import { nanoid } from '@documenso/lib/universal/id';
import { procedure } from '../trpc';
import {
ZUpdateEmbeddingTemplateRequestSchema,
ZUpdateEmbeddingTemplateResponseSchema,
} from './update-embedding-template.types';
export const updateEmbeddingTemplateRoute = procedure
.input(ZUpdateEmbeddingTemplateRequestSchema)
.output(ZUpdateEmbeddingTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
try {
const authorizationHeader = ctx.req.headers.get('authorization');
const [presignToken] = (authorizationHeader || '')
.split('Bearer ')
.filter((s) => s.length > 0);
if (!presignToken) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'No presign token provided',
});
}
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
const { templateId, title, externalId, recipients, meta } = input;
await updateTemplate({
templateId,
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
data: {
title,
externalId,
},
meta,
});
const recipientsWithClientId = recipients.map((recipient) => ({
...recipient,
clientId: nanoid(),
}));
const { recipients: updatedRecipients } = await setTemplateRecipients({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
templateId,
recipients: recipientsWithClientId.map((recipient) => ({
id: recipient.id,
email: recipient.email,
name: recipient.name ?? '',
role: recipient.role ?? 'SIGNER',
signingOrder: recipient.signingOrder,
})),
});
const fields = recipientsWithClientId.flatMap((recipient) => {
const recipientId = updatedRecipients.find((r) => r.email === recipient.email)?.id;
if (!recipientId) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Recipient not found',
});
}
return (recipient.fields ?? []).map((field) => ({
...field,
recipientId,
// !: Temp property to be removed once we don't link based on signer email
signerEmail: recipient.email,
}));
});
await setFieldsForTemplate({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
templateId,
fields: fields.map((field) => ({
...field,
pageWidth: field.width,
pageHeight: field.height,
})),
});
return {
templateId,
};
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to update template',
});
}
});

View File

@ -0,0 +1,77 @@
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({
id: z.number().optional(),
type: z.nativeEnum(FieldType),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
fieldMeta: ZFieldMetaSchema.optional(),
});
export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
templateId: z.number(),
title: ZDocumentTitleSchema.optional(),
externalId: z.string().optional(),
recipients: z.array(
z.object({
id: z.number().optional(),
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 ZUpdateEmbeddingTemplateResponseSchema = z.object({
templateId: z.number(),
});
export type TUpdateEmbeddingTemplateRequestSchema = z.infer<
typeof ZUpdateEmbeddingTemplateRequestSchema
>;

View File

@ -0,0 +1,354 @@
import { TRPCError } from '@trpc/server';
import { createFolder } from '@documenso/lib/server-only/folder/create-folder';
import { deleteFolder } from '@documenso/lib/server-only/folder/delete-folder';
import { findFolders } from '@documenso/lib/server-only/folder/find-folders';
import { getFolderBreadcrumbs } from '@documenso/lib/server-only/folder/get-folder-breadcrumbs';
import { getFolderById } from '@documenso/lib/server-only/folder/get-folder-by-id';
import { moveDocumentToFolder } from '@documenso/lib/server-only/folder/move-document-to-folder';
import { moveFolder } from '@documenso/lib/server-only/folder/move-folder';
import { moveTemplateToFolder } from '@documenso/lib/server-only/folder/move-template-to-folder';
import { pinFolder } from '@documenso/lib/server-only/folder/pin-folder';
import { unpinFolder } from '@documenso/lib/server-only/folder/unpin-folder';
import { updateFolder } from '@documenso/lib/server-only/folder/update-folder';
import { FolderType } from '@documenso/lib/types/folder-type';
import { authenticatedProcedure, router } from '../trpc';
import {
ZCreateFolderSchema,
ZDeleteFolderSchema,
ZFindFoldersRequestSchema,
ZFindFoldersResponseSchema,
ZGenericSuccessResponse,
ZGetFoldersResponseSchema,
ZGetFoldersSchema,
ZMoveDocumentToFolderSchema,
ZMoveFolderSchema,
ZMoveTemplateToFolderSchema,
ZPinFolderSchema,
ZSuccessResponseSchema,
ZUnpinFolderSchema,
ZUpdateFolderSchema,
} from './schema';
export const folderRouter = router({
/**
* @private
*/
getFolders: authenticatedProcedure
.input(ZGetFoldersSchema)
.output(ZGetFoldersResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { parentId, type } = input;
const folders = await findFolders({
userId: user.id,
teamId,
parentId,
type,
});
const breadcrumbs = parentId
? await getFolderBreadcrumbs({
userId: user.id,
teamId,
folderId: parentId,
type,
})
: [];
return {
folders,
breadcrumbs,
type,
};
}),
/**
* @private
*/
findFolders: authenticatedProcedure
.input(ZFindFoldersRequestSchema)
.output(ZFindFoldersResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { parentId, type } = input;
const folders = await findFolders({
userId: user.id,
teamId,
parentId,
type,
});
const breadcrumbs = parentId
? await getFolderBreadcrumbs({
userId: user.id,
teamId,
folderId: parentId,
type,
})
: [];
return {
data: folders,
breadcrumbs,
type,
};
}),
/**
* @private
*/
createFolder: authenticatedProcedure
.input(ZCreateFolderSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { name, parentId, type } = input;
if (parentId) {
try {
await getFolderById({
userId: user.id,
teamId,
folderId: parentId,
type,
});
} catch (error) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Parent folder not found',
});
}
}
const result = await createFolder({
userId: user.id,
teamId,
name,
parentId,
type,
});
return {
...result,
type,
};
}),
/**
* @private
*/
updateFolder: authenticatedProcedure
.input(ZUpdateFolderSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { id, name, visibility } = input;
const currentFolder = await getFolderById({
userId: user.id,
teamId,
folderId: id,
});
const result = await updateFolder({
userId: user.id,
teamId,
folderId: id,
name,
visibility,
type: currentFolder.type,
});
return {
...result,
type: currentFolder.type,
};
}),
/**
* @private
*/
deleteFolder: authenticatedProcedure
.input(ZDeleteFolderSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { id } = input;
await deleteFolder({
userId: user.id,
teamId,
folderId: id,
});
return ZGenericSuccessResponse;
}),
/**
* @private
*/
moveFolder: authenticatedProcedure.input(ZMoveFolderSchema).mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { id, parentId } = input;
const currentFolder = await getFolderById({
userId: user.id,
teamId,
folderId: id,
});
if (parentId !== null) {
try {
await getFolderById({
userId: user.id,
teamId,
folderId: parentId,
type: currentFolder.type,
});
} catch (error) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Parent folder not found',
});
}
}
const result = await moveFolder({
userId: user.id,
teamId,
folderId: id,
parentId,
requestMetadata: ctx.metadata,
});
return {
...result,
type: currentFolder.type,
};
}),
/**
* @private
*/
moveDocumentToFolder: authenticatedProcedure
.input(ZMoveDocumentToFolderSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId, folderId } = input;
if (folderId !== null) {
try {
await getFolderById({
userId: user.id,
teamId,
folderId,
type: FolderType.DOCUMENT,
});
} catch (error) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Folder not found',
});
}
}
const result = await moveDocumentToFolder({
userId: user.id,
teamId,
documentId,
folderId,
requestMetadata: ctx.metadata,
});
return {
...result,
type: FolderType.DOCUMENT,
};
}),
/**
* @private
*/
moveTemplateToFolder: authenticatedProcedure
.input(ZMoveTemplateToFolderSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { templateId, folderId } = input;
if (folderId !== null) {
try {
await getFolderById({
userId: user.id,
teamId,
folderId,
type: FolderType.TEMPLATE,
});
} catch (error) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Folder not found',
});
}
}
const result = await moveTemplateToFolder({
userId: user.id,
teamId,
templateId,
folderId,
});
return {
...result,
type: FolderType.TEMPLATE,
};
}),
/**
* @private
*/
pinFolder: authenticatedProcedure.input(ZPinFolderSchema).mutation(async ({ ctx, input }) => {
const currentFolder = await getFolderById({
userId: ctx.user.id,
teamId: ctx.teamId,
folderId: input.folderId,
});
const result = await pinFolder({
userId: ctx.user.id,
teamId: ctx.teamId,
folderId: input.folderId,
type: currentFolder.type,
});
return {
...result,
type: currentFolder.type,
};
}),
/**
* @private
*/
unpinFolder: authenticatedProcedure.input(ZUnpinFolderSchema).mutation(async ({ ctx, input }) => {
const currentFolder = await getFolderById({
userId: ctx.user.id,
teamId: ctx.teamId,
folderId: input.folderId,
});
const result = await unpinFolder({
userId: ctx.user.id,
teamId: ctx.teamId,
folderId: input.folderId,
type: currentFolder.type,
});
return {
...result,
type: currentFolder.type,
};
}),
});

View File

@ -0,0 +1,132 @@
import { z } from 'zod';
import { ZFolderTypeSchema } from '@documenso/lib/types/folder-type';
import { DocumentVisibility } from '@documenso/prisma/generated/types';
/**
* Required for empty responses since we currently can't 201 requests for our openapi setup.
*
* Without this it will throw an error in Speakeasy SDK when it tries to parse an empty response.
*/
export const ZSuccessResponseSchema = z.object({
success: z.boolean(),
type: ZFolderTypeSchema.optional(),
});
export const ZGenericSuccessResponse = {
success: true,
} satisfies z.infer<typeof ZSuccessResponseSchema>;
export const ZFolderSchema = z.object({
id: z.string(),
name: z.string(),
userId: z.number(),
teamId: z.number().nullable(),
parentId: z.string().nullable(),
pinned: z.boolean(),
createdAt: z.date(),
updatedAt: z.date(),
visibility: z.nativeEnum(DocumentVisibility),
type: ZFolderTypeSchema,
});
export type TFolder = z.infer<typeof ZFolderSchema>;
const ZFolderCountSchema = z.object({
documents: z.number(),
templates: z.number(),
subfolders: z.number(),
});
const ZSubfolderSchema = ZFolderSchema.extend({
subfolders: z.array(z.any()),
_count: ZFolderCountSchema,
});
export const ZFolderWithSubfoldersSchema = ZFolderSchema.extend({
subfolders: z.array(ZSubfolderSchema),
_count: ZFolderCountSchema,
});
export type TFolderWithSubfolders = z.infer<typeof ZFolderWithSubfoldersSchema>;
export const ZCreateFolderSchema = z.object({
name: z.string(),
parentId: z.string().optional(),
type: ZFolderTypeSchema.optional(),
});
export const ZUpdateFolderSchema = z.object({
id: z.string(),
name: z.string(),
visibility: z.nativeEnum(DocumentVisibility),
type: ZFolderTypeSchema.optional(),
});
export type TUpdateFolderSchema = z.infer<typeof ZUpdateFolderSchema>;
export const ZDeleteFolderSchema = z.object({
id: z.string(),
type: ZFolderTypeSchema.optional(),
});
export const ZMoveFolderSchema = z.object({
id: z.string(),
parentId: z.string().nullable(),
type: ZFolderTypeSchema.optional(),
});
export const ZMoveDocumentToFolderSchema = z.object({
documentId: z.number(),
folderId: z.string().nullable().optional(),
type: z.enum(['DOCUMENT']).optional(),
});
export const ZMoveTemplateToFolderSchema = z.object({
templateId: z.number(),
folderId: z.string().nullable().optional(),
type: z.enum(['TEMPLATE']).optional(),
});
export const ZPinFolderSchema = z.object({
folderId: z.string(),
type: ZFolderTypeSchema.optional(),
});
export const ZUnpinFolderSchema = z.object({
folderId: z.string(),
type: ZFolderTypeSchema.optional(),
});
export const ZGetFoldersSchema = z.object({
parentId: z.string().nullable().optional(),
type: ZFolderTypeSchema.optional(),
});
export const ZGetFoldersResponseSchema = z.object({
folders: z.array(ZFolderWithSubfoldersSchema),
breadcrumbs: z.array(ZFolderSchema),
type: ZFolderTypeSchema.optional(),
});
export type TGetFoldersResponse = z.infer<typeof ZGetFoldersResponseSchema>;
export const ZFindSearchParamsSchema = z.object({
query: z.string().optional(),
page: z.number().optional(),
perPage: z.number().optional(),
type: ZFolderTypeSchema.optional(),
});
export const ZFindFoldersRequestSchema = ZFindSearchParamsSchema.extend({
parentId: z.string().nullable().optional(),
type: ZFolderTypeSchema.optional(),
});
export const ZFindFoldersResponseSchema = z.object({
data: z.array(ZFolderWithSubfoldersSchema),
breadcrumbs: z.array(ZFolderSchema),
type: ZFolderTypeSchema.optional(),
});
export type TFindFoldersResponse = z.infer<typeof ZFindFoldersResponseSchema>;

View File

@ -30,7 +30,7 @@ export const ZCreateRecipientSchema = z.object({
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
});
const ZUpdateRecipientSchema = z.object({
export const ZUpdateRecipientSchema = z.object({
id: z.number().describe('The ID of the recipient to update.'),
email: z.string().toLowerCase().email().min(1).optional(),
name: z.string().optional(),

View File

@ -4,6 +4,7 @@ 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 { folderRouter } from './folder-router/router';
import { profileRouter } from './profile-router/router';
import { recipientRouter } from './recipient-router/router';
import { shareLinkRouter } from './share-link-router/router';
@ -17,6 +18,7 @@ export const appRouter = router({
profile: profileRouter,
document: documentRouter,
field: fieldRouter,
folder: folderRouter,
recipient: recipientRouter,
admin: adminRouter,
shareLink: shareLinkRouter,

View File

@ -0,0 +1,61 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { prisma } from '@documenso/prisma';
import { procedure } from '../trpc';
import {
ZGetDocumentInternalUrlForQRCodeInput,
ZGetDocumentInternalUrlForQRCodeOutput,
} from './get-document-internal-url-for-qr-code.types';
export const getDocumentInternalUrlForQRCodeRoute = procedure
.input(ZGetDocumentInternalUrlForQRCodeInput)
.output(ZGetDocumentInternalUrlForQRCodeOutput)
.query(async ({ input, ctx }) => {
const { documentId } = input;
if (!ctx.user) {
return null;
}
const document = await prisma.document.findFirst({
where: {
OR: [
{
id: documentId,
userId: ctx.user.id,
},
{
id: documentId,
team: {
members: {
some: {
userId: ctx.user.id,
},
},
},
},
],
},
include: {
team: {
where: {
members: {
some: {
userId: ctx.user.id,
},
},
},
},
},
});
if (!document) {
return null;
}
if (document.team) {
return `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${document.id}`;
}
return `${NEXT_PUBLIC_WEBAPP_URL()}/documents/${document.id}`;
});

View File

@ -0,0 +1,15 @@
import { z } from 'zod';
export const ZGetDocumentInternalUrlForQRCodeInput = z.object({
documentId: z.number(),
});
export type TGetDocumentInternalUrlForQRCodeInput = z.infer<
typeof ZGetDocumentInternalUrlForQRCodeInput
>;
export const ZGetDocumentInternalUrlForQRCodeOutput = z.string().nullable();
export type TGetDocumentInternalUrlForQRCodeOutput = z.infer<
typeof ZGetDocumentInternalUrlForQRCodeOutput
>;

View File

@ -1,6 +1,7 @@
import { createOrGetShareLink } from '@documenso/lib/server-only/share/create-or-get-share-link';
import { procedure, router } from '../trpc';
import { getDocumentInternalUrlForQRCodeRoute } from './get-document-internal-url-for-qr-code';
import { ZCreateOrGetShareLinkMutationSchema } from './schema';
export const shareLinkRouter = router({
@ -21,4 +22,6 @@ export const shareLinkRouter = router({
return await createOrGetShareLink({ documentId, userId: ctx.user.id });
}),
getDocumentInternalUrlForQRCode: getDocumentInternalUrlForQRCodeRoute,
});

View File

@ -121,13 +121,14 @@ export const templateRouter = router({
.output(ZCreateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { title, templateDocumentDataId } = input;
const { title, templateDocumentDataId, folderId } = input;
return await createTemplate({
userId: ctx.user.id,
teamId,
title,
templateDocumentDataId,
folderId,
});
}),

View File

@ -34,6 +34,7 @@ import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
export const ZCreateTemplateMutationSchema = z.object({
title: z.string().min(1).trim(),
templateDocumentDataId: z.string().min(1),
folderId: z.string().optional(),
});
export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({
@ -190,6 +191,7 @@ export const ZUpdateTemplateResponseSchema = ZTemplateLiteSchema;
export const ZFindTemplatesRequestSchema = ZFindSearchParamsSchema.extend({
type: z.nativeEnum(TemplateType).describe('Filter templates by type.').optional(),
folderId: z.string().describe('The ID of the folder to filter templates by.').optional(),
});
export const ZFindTemplatesResponseSchema = ZFindResultResponse.extend({