This commit is contained in:
David Nguyen
2025-08-22 22:30:02 +10:00
parent e7e2aa9bd8
commit e1464ac2d3
17 changed files with 787 additions and 258 deletions

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import type { Document, Role, Subscription } from '@prisma/client'; import type { Role, Subscription } from '@prisma/client';
import { Edit, Loader } from 'lucide-react'; import { Edit, Loader } from 'lucide-react';
import { Link } from 'react-router'; import { Link } from 'react-router';
@ -20,7 +20,7 @@ type UserData = {
email: string; email: string;
roles: Role[]; roles: Role[];
subscriptions?: SubscriptionLite[] | null; subscriptions?: SubscriptionLite[] | null;
documents: DocumentLite[]; documentCount: number;
}; };
type SubscriptionLite = Pick< type SubscriptionLite = Pick<
@ -28,8 +28,6 @@ type SubscriptionLite = Pick<
'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd' 'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd'
>; >;
type DocumentLite = Pick<Document, 'id'>;
type AdminDashboardUsersTableProps = { type AdminDashboardUsersTableProps = {
users: UserData[]; users: UserData[];
totalPages: number; totalPages: number;
@ -74,10 +72,7 @@ export const AdminDashboardUsersTable = ({
}, },
{ {
header: _(msg`Documents`), header: _(msg`Documents`),
accessorKey: 'documents', accessorKey: 'documentCount',
cell: ({ row }) => {
return <div>{row.original.documents?.length}</div>;
},
}, },
{ {
header: '', header: '',

View File

@ -1,39 +1,32 @@
import type { Prisma } from '@prisma/client';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { DocumentVisibility } from '../../types/document-visibility'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { getTeamById } from '../team/get-team';
export type GetDocumentByIdOptions = { export type GetDocumentByIdOptions = {
documentId: number; documentId: number;
userId: number; userId: number;
teamId: number; teamId: number;
folderId?: string;
}; };
export const getDocumentById = async ({ export const getDocumentById = async ({ documentId, userId, teamId }: GetDocumentByIdOptions) => {
documentId, const { documentWhereInput } = await getEnvelopeWhereInput({
userId, id: {
teamId, type: 'documentId',
folderId, id: documentId,
}: GetDocumentByIdOptions) => { },
const { documentWhereInput } = await getDocumentWhereInput({ validatedUserId: userId,
documentId, unvalidatedTeamId: teamId,
userId,
teamId,
}); });
const document = await prisma.document.findFirst({ const envelope = await prisma.envelope.findFirst({
where: { where: documentWhereInput,
...documentWhereInput,
folderId,
},
include: { include: {
documentData: true, documents: {
include: {
documentData: true,
},
},
documentMeta: true, documentMeta: true,
user: { user: {
select: { select: {
@ -56,7 +49,7 @@ export const getDocumentById = async ({
}, },
}); });
if (!document) { if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, { throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document could not be found', message: 'Document could not be found',
}); });
@ -64,93 +57,3 @@ export const getDocumentById = async ({
return document; return document;
}; };
export type GetDocumentWhereInputOptions = {
documentId: number;
userId: number;
teamId: number;
};
/**
* Generate the where input for a given Prisma document query.
*
* This will return a query that allows a user to get a document if they have valid access to it.
*/
export const getDocumentWhereInput = async ({
documentId,
userId,
teamId,
}: GetDocumentWhereInputOptions) => {
const team = await getTeamById({ teamId, userId });
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const teamVisibilityFilters = match(team.currentTeamRole)
.with(TeamMemberRole.ADMIN, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.ADMIN,
])
.with(TeamMemberRole.MANAGER, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
])
.otherwise(() => [DocumentVisibility.EVERYONE]);
const documentOrInput: Prisma.DocumentWhereInput[] = [
// Allow access if they own the document.
{
userId,
},
// Or, if they belong to the team that the document is associated with.
{
visibility: {
in: teamVisibilityFilters,
},
teamId: team.id,
},
// Or, if they are a recipient of the document.
{
status: {
not: DocumentStatus.DRAFT,
},
recipients: {
some: {
email: user.email,
},
},
},
];
// Allow access to documents sent to or from the team email.
if (team.teamEmail) {
documentOrInput.push(
{
recipients: {
some: {
email: team.teamEmail.email,
},
},
},
{
user: {
email: team.teamEmail.email,
},
},
);
}
const documentWhereInput: Prisma.DocumentWhereUniqueInput = {
id: documentId,
OR: documentOrInput,
};
return {
documentWhereInput,
team,
};
};

View File

@ -0,0 +1,157 @@
import type { Prisma } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DocumentVisibility } from '../../types/document-visibility';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { buildEnvelopeIdQuery } from '../../utils/envelope';
import { getTeamById } from '../team/get-team';
export type GetEnvelopeByIdOptions = {
id: EnvelopeIdOptions;
userId: number;
teamId: number;
};
export const getEnvelopeById = async ({ id, userId, teamId }: GetEnvelopeByIdOptions) => {
const { documentWhereInput } = await getEnvelopeWhereInput({
id,
validatedUserId: userId,
unvalidatedTeamId: teamId,
});
const document = await prisma.envelope.findFirst({
where: documentWhereInput,
include: {
documents: {
include: {
documentData: true,
},
},
documentMeta: true,
user: {
select: {
id: true,
name: true,
email: true,
},
},
recipients: {
select: {
email: true,
},
},
team: {
select: {
id: true,
url: true,
},
},
},
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document could not be found',
});
}
return document;
};
export type GetEnvelopeWhereInputOptions = {
id: EnvelopeIdOptions;
/**
* The user ID who has been authenticated.
*/
validatedUserId: number;
/**
* The unknown teamId from the request.
*/
unvalidatedTeamId: number;
};
/**
* Generate the where input for a given Prisma envelope query.
*
* This will return a query that allows a user to get a document if they have valid access to it.
*/
export const getEnvelopeWhereInput = async ({
id,
validatedUserId,
unvalidatedTeamId,
}: GetEnvelopeWhereInputOptions) => {
const team = await getTeamById({ teamId: unvalidatedTeamId, userId: validatedUserId });
const teamVisibilityFilters = match(team.currentTeamRole)
.with(TeamMemberRole.ADMIN, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.ADMIN,
])
.with(TeamMemberRole.MANAGER, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
])
.otherwise(() => [DocumentVisibility.EVERYONE]);
const documentOrInput: Prisma.EnvelopeWhereInput[] = [
// Allow access if they own the document.
{
userId: validatedUserId,
},
// Or, if they belong to the team that the document is associated with.
{
visibility: {
in: teamVisibilityFilters,
},
teamId: team.id,
},
// Or, if they are a recipient of the document.
// ????????????? should recipients be able to do X?
// {
// status: {
// not: DocumentStatus.DRAFT,
// },
// recipients: {
// some: {
// email: user.email,
// },
// },
// },
];
// Allow access to documents sent to or from the team email.
if (team.teamEmail) {
documentOrInput.push(
{
recipients: {
some: {
email: team.teamEmail.email,
},
},
},
{
user: {
email: team.teamEmail.email,
},
},
);
}
const documentWhereInput: Prisma.EnvelopeWhereUniqueInput = {
...buildEnvelopeIdQuery(id),
OR: documentOrInput,
};
return {
documentWhereInput,
team,
};
};

View File

@ -1,4 +1,4 @@
import { DocumentStatus } from '@prisma/client'; import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -25,9 +25,10 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
const serviceAccount = await deletedAccountServiceAccount(); const serviceAccount = await deletedAccountServiceAccount();
// TODO: Send out cancellations for all pending docs // TODO: Send out cancellations for all pending docs
await prisma.document.updateMany({ await prisma.envelope.updateMany({
where: { where: {
userId: user.id, userId: user.id,
type: EnvelopeType.DOCUMENT,
status: { status: {
in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED], in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED],
}, },

View File

@ -1,4 +1,4 @@
import { Prisma } from '@prisma/client'; import { EnvelopeType, Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -34,12 +34,20 @@ export const findUsers = async ({
const [users, count] = await Promise.all([ const [users, count] = await Promise.all([
prisma.user.findMany({ prisma.user.findMany({
include: { select: {
documents: { _count: {
select: { select: {
id: true, envelopes: {
where: {
type: EnvelopeType.DOCUMENT,
},
},
}, },
}, },
id: true,
name: true,
email: true,
roles: true,
}, },
where: whereClause, where: whereClause,
skip: Math.max(page - 1, 0) * perPage, skip: Math.max(page - 1, 0) * perPage,
@ -51,7 +59,10 @@ export const findUsers = async ({
]); ]);
return { return {
users, users: users.map((user) => ({
...user,
documentCount: user._count.envelopes,
})),
totalPages: Math.ceil(count / perPage), totalPages: Math.ceil(count / perPage),
}; };
}; };

View File

@ -1,11 +1,11 @@
import type { z } from 'zod'; import { z } from 'zod';
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema'; import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema'; import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import { DocumentSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentSchema';
import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema'; import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
import { LegacyDocumentSchema } from '@documenso/prisma/types/document-legacy-schema';
import { ZFieldSchema } from './field'; import { ZFieldSchema } from './field';
import { ZRecipientLiteSchema } from './recipient'; import { ZRecipientLiteSchema } from './recipient';
@ -15,7 +15,7 @@ import { ZRecipientLiteSchema } from './recipient';
* *
* Mainly used for returning a single document from the API. * Mainly used for returning a single document from the API.
*/ */
export const ZDocumentSchema = DocumentSchema.pick({ export const ZDocumentSchema = LegacyDocumentSchema.pick({
visibility: true, visibility: true,
status: true, status: true,
source: true, source: true,
@ -31,9 +31,12 @@ export const ZDocumentSchema = DocumentSchema.pick({
completedAt: true, completedAt: true,
deletedAt: true, deletedAt: true,
teamId: true, teamId: true,
templateId: true,
folderId: true, folderId: true,
}).extend({ }).extend({
// Which "Template" the document was created from. Legacy field for backwards compatibility.
// The actual field is now called `createdFromDocumentId`.
templateId: z.number().optional(),
// Todo: Maybe we want to alter this a bit since this returns a lot of data. // Todo: Maybe we want to alter this a bit since this returns a lot of data.
documentData: DocumentDataSchema.pick({ documentData: DocumentDataSchema.pick({
type: true, type: true,
@ -82,7 +85,7 @@ export type TDocument = z.infer<typeof ZDocumentSchema>;
/** /**
* A lite version of the document response schema without relations. * A lite version of the document response schema without relations.
*/ */
export const ZDocumentLiteSchema = DocumentSchema.pick({ export const ZDocumentLiteSchema = LegacyDocumentSchema.pick({
visibility: true, visibility: true,
status: true, status: true,
source: true, source: true,
@ -98,9 +101,12 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({
completedAt: true, completedAt: true,
deletedAt: true, deletedAt: true,
teamId: true, teamId: true,
templateId: true,
folderId: true, folderId: true,
useLegacyFieldInsertion: true, useLegacyFieldInsertion: true,
}).extend({
// Which "Template" the document was created from. Legacy field for backwards compatibility.
// The actual field is now called `createdFromDocumentId`.
templateId: z.number().optional(),
}); });
export type TDocumentLite = z.infer<typeof ZDocumentLiteSchema>; export type TDocumentLite = z.infer<typeof ZDocumentLiteSchema>;
@ -108,7 +114,7 @@ export type TDocumentLite = z.infer<typeof ZDocumentLiteSchema>;
/** /**
* A version of the document response schema when returning multiple documents at once from a single API endpoint. * A version of the document response schema when returning multiple documents at once from a single API endpoint.
*/ */
export const ZDocumentManySchema = DocumentSchema.pick({ export const ZDocumentManySchema = LegacyDocumentSchema.pick({
visibility: true, visibility: true,
status: true, status: true,
source: true, source: true,
@ -124,10 +130,13 @@ export const ZDocumentManySchema = DocumentSchema.pick({
completedAt: true, completedAt: true,
deletedAt: true, deletedAt: true,
teamId: true, teamId: true,
templateId: true,
folderId: true, folderId: true,
useLegacyFieldInsertion: true, useLegacyFieldInsertion: true,
}).extend({ }).extend({
// Which "Template" the document was created from. Legacy field for backwards compatibility.
// The actual field is now called `createdFromDocumentId`.
templateId: z.number().optional(),
user: UserSchema.pick({ user: UserSchema.pick({
id: true, id: true,
name: true, name: true,

View File

@ -18,8 +18,6 @@ export const ZFieldSchema = FieldSchema.pick({
type: true, type: true,
id: true, id: true,
secondaryId: true, secondaryId: true,
documentId: true,
templateId: true,
recipientId: true, recipientId: true,
page: true, page: true,
positionX: true, positionX: true,
@ -29,6 +27,10 @@ export const ZFieldSchema = FieldSchema.pick({
customText: true, customText: true,
inserted: true, inserted: true,
fieldMeta: true, fieldMeta: true,
}).extend({
// Todo: Decide whether to make these two IDs backwards compatible.
documentId: z.number().optional(),
templateId: z.number().optional(),
}); });
export const ZFieldPageNumberSchema = z export const ZFieldPageNumberSchema = z

View File

@ -1,3 +1,5 @@
import { z } from 'zod';
import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema'; import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
@ -15,8 +17,6 @@ export const ZRecipientSchema = RecipientSchema.pick({
signingStatus: true, signingStatus: true,
sendStatus: true, sendStatus: true,
id: true, id: true,
documentId: true,
templateId: true,
email: true, email: true,
name: true, name: true,
token: true, token: true,
@ -28,6 +28,10 @@ export const ZRecipientSchema = RecipientSchema.pick({
rejectionReason: true, rejectionReason: true,
}).extend({ }).extend({
fields: ZFieldSchema.array(), fields: ZFieldSchema.array(),
// Todo: Decide whether to make these two IDs backwards compatible.
documentId: z.number().optional(),
templateId: z.number().optional(),
}); });
/** /**
@ -39,8 +43,6 @@ export const ZRecipientLiteSchema = RecipientSchema.pick({
signingStatus: true, signingStatus: true,
sendStatus: true, sendStatus: true,
id: true, id: true,
documentId: true,
templateId: true,
email: true, email: true,
name: true, name: true,
token: true, token: true,
@ -50,6 +52,10 @@ export const ZRecipientLiteSchema = RecipientSchema.pick({
authOptions: true, authOptions: true,
signingOrder: true, signingOrder: true,
rejectionReason: true, rejectionReason: true,
}).extend({
// Todo: Decide whether to make these two IDs backwards compatible.
documentId: z.number().optional(),
templateId: z.number().optional(),
}); });
/** /**
@ -61,8 +67,6 @@ export const ZRecipientManySchema = RecipientSchema.pick({
signingStatus: true, signingStatus: true,
sendStatus: true, sendStatus: true,
id: true, id: true,
documentId: true,
templateId: true,
email: true, email: true,
name: true, name: true,
token: true, token: true,
@ -83,4 +87,8 @@ export const ZRecipientManySchema = RecipientSchema.pick({
id: true, id: true,
url: true, url: true,
}).nullable(), }).nullable(),
// Todo: Decide whether to make these two IDs backwards compatible.
documentId: z.number().optional(),
templateId: z.number().optional(),
}); });

View File

@ -1,12 +1,12 @@
import type { z } from 'zod'; import { z } from 'zod';
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema'; import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema'; import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema'; import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema'; import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema';
import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateSchema';
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
import { TemplateSchema } from '@documenso/prisma/types/template-legacy-schema';
import { ZFieldSchema } from './field'; import { ZFieldSchema } from './field';
import { ZRecipientLiteSchema } from './recipient'; import { ZRecipientLiteSchema } from './recipient';
@ -51,13 +51,17 @@ export const ZTemplateSchema = TemplateSchema.pick({
drawSignatureEnabled: true, drawSignatureEnabled: true,
allowDictateNextSigner: true, allowDictateNextSigner: true,
distributionMethod: true, distributionMethod: true,
templateId: true,
redirectUrl: true, redirectUrl: true,
language: true, language: true,
emailSettings: true, emailSettings: true,
emailId: true, emailId: true,
emailReplyTo: true, emailReplyTo: true,
}).nullable(), })
.extend({
// Legacy field for backwards compatibility. Needs to refer to the Envelope `secondaryTemplateId`.
templateId: z.number(),
})
.nullable(),
directLink: TemplateDirectLinkSchema.nullable(), directLink: TemplateDirectLinkSchema.nullable(),
user: UserSchema.pick({ user: UserSchema.pick({
id: true, id: true,

View File

@ -1,10 +1,10 @@
import type { Document, DocumentMeta, OrganisationGlobalSettings } from '@prisma/client'; import type { DocumentMeta, Envelope, OrganisationGlobalSettings } from '@prisma/client';
import { DocumentDistributionMethod, DocumentSigningOrder, DocumentStatus } from '@prisma/client'; import { DocumentDistributionMethod, DocumentSigningOrder, DocumentStatus } from '@prisma/client';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones';
import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email'; import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email';
export const isDocumentCompleted = (document: Pick<Document, 'status'> | DocumentStatus) => { export const isDocumentCompleted = (document: Pick<Envelope, 'status'> | DocumentStatus) => {
const status = typeof document === 'string' ? document : document.status; const status = typeof document === 'string' ? document : document.status;
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED; return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
@ -53,5 +53,5 @@ export const extractDerivedDocumentMeta = (
emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo, emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo,
emailSettings: emailSettings:
meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS, meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS,
} satisfies Omit<DocumentMeta, 'id' | 'documentId' | 'templateId'>; } satisfies Omit<DocumentMeta, 'id' | 'envelopeId'>;
}; };

View File

@ -0,0 +1,92 @@
import { EnvelopeType } from '@prisma/client';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { AppError, AppErrorCode } from '../errors/app-error';
const envelopeDocumentPrefixId = 'document';
const envelopeTemplatePrefixId = 'template';
const envelopePrefixId = 'envelope';
const ZDocumentIdSchema = z.string().regex(/^document_\d+$/);
const ZTemplateIdSchema = z.string().regex(/^template_\d+$/);
const ZEnvelopeIdSchema = z.string().regex(/^envelope_\d+$/);
export type EnvelopeIdOptions =
| {
type: 'envelopeId';
id: string;
}
| {
type: 'documentId';
id: string | number;
}
| {
type: 'templateId';
id: string | number;
};
/**
* Parses an unknown document or template ID.
*
* @param id
* @param type
* @returns
*/
export const buildEnvelopeIdQuery = (options: EnvelopeIdOptions) => {
return match(options)
.with({ type: 'envelopeId' }, (value) => {
const parsed = ZEnvelopeIdSchema.safeParse(value.id);
if (!parsed.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid envelope ID',
});
}
return {
id: value.id,
};
})
.with({ type: 'documentId' }, (value) => ({
type: EnvelopeType.DOCUMENT,
secondaryId: parseDocumentIdToEnvelopeSecondaryId(value.id),
}))
.with({ type: 'templateId' }, (value) => ({
type: EnvelopeType.TEMPLATE,
secondaryId: parseTemplateIdToEnvelopeSecondaryId(value.id),
}))
.exhaustive();
};
export const parseDocumentIdToEnvelopeSecondaryId = (documentId: string | number) => {
if (typeof documentId === 'number') {
return `${envelopeDocumentPrefixId}_${documentId}`;
}
const parsed = ZDocumentIdSchema.safeParse(documentId);
if (!parsed.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid document ID',
});
}
return parsed.data;
};
export const parseTemplateIdToEnvelopeSecondaryId = (templateId: string | number) => {
if (typeof templateId === 'number') {
return `${envelopeTemplatePrefixId}_${templateId}`;
}
const parsed = ZTemplateIdSchema.safeParse(templateId);
if (!parsed.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid template ID',
});
}
return parsed.data;
};

View File

@ -1,4 +1,4 @@
import type { Recipient } from '@prisma/client'; import { type Recipient } from '@prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';

View File

@ -0,0 +1,261 @@
/*
Warnings:
- You are about to drop the column `authOptions` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `completedAt` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `createdAt` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `deletedAt` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `externalId` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `folderId` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `formValues` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `source` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `status` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `teamId` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `templateId` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `updatedAt` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `useLegacyFieldInsertion` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `userId` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `visibility` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `documentId` on the `DocumentAuditLog` table. All the data in the column will be lost.
- You are about to drop the column `documentId` on the `DocumentMeta` table. All the data in the column will be lost.
- You are about to drop the column `templateId` on the `DocumentMeta` table. All the data in the column will be lost.
- You are about to drop the column `documentId` on the `DocumentShareLink` table. All the data in the column will be lost.
- You are about to drop the column `documentId` on the `Field` table. All the data in the column will be lost.
- You are about to drop the column `templateId` on the `Field` table. All the data in the column will be lost.
- You are about to drop the column `documentId` on the `Recipient` table. All the data in the column will be lost.
- You are about to drop the column `templateId` on the `Recipient` table. All the data in the column will be lost.
- You are about to drop the column `templateId` on the `TemplateDirectLink` table. All the data in the column will be lost.
- You are about to drop the `Template` table. If the table is not empty, all the data it contains will be lost.
- A unique constraint covering the columns `[envelopeId,email]` on the table `DocumentShareLink` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[envelopeId,email]` on the table `Recipient` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[envelopeId]` on the table `TemplateDirectLink` will be added. If there are existing duplicate values, this will fail.
- Added the required column `envelopeId` to the `DocumentAuditLog` table without a default value. This is not possible if the table is not empty.
- Added the required column `envelopeId` to the `DocumentShareLink` table without a default value. This is not possible if the table is not empty.
- Added the required column `envelopeId` to the `Field` table without a default value. This is not possible if the table is not empty.
- Added the required column `envelopeId` to the `Recipient` table without a default value. This is not possible if the table is not empty.
- Added the required column `envelopeId` to the `TemplateDirectLink` table without a default value. This is not possible if the table is not empty.
- Added the required column `type` to the `TemplateDirectLink` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "EnvelopeType" AS ENUM ('DOCUMENT', 'TEMPLATE');
-- CreateEnum
CREATE TYPE "TemplateDirectLinkType" AS ENUM ('PUBLIC', 'PRIVATE');
-- DropForeignKey
ALTER TABLE "Document" DROP CONSTRAINT "Document_folderId_fkey";
-- DropForeignKey
ALTER TABLE "Document" DROP CONSTRAINT "Document_teamId_fkey";
-- DropForeignKey
ALTER TABLE "Document" DROP CONSTRAINT "Document_templateId_fkey";
-- DropForeignKey
ALTER TABLE "Document" DROP CONSTRAINT "Document_userId_fkey";
-- DropForeignKey
ALTER TABLE "DocumentAuditLog" DROP CONSTRAINT "DocumentAuditLog_documentId_fkey";
-- DropForeignKey
ALTER TABLE "DocumentMeta" DROP CONSTRAINT "DocumentMeta_documentId_fkey";
-- DropForeignKey
ALTER TABLE "DocumentMeta" DROP CONSTRAINT "DocumentMeta_templateId_fkey";
-- DropForeignKey
ALTER TABLE "DocumentShareLink" DROP CONSTRAINT "DocumentShareLink_documentId_fkey";
-- DropForeignKey
ALTER TABLE "Field" DROP CONSTRAINT "Field_documentId_fkey";
-- DropForeignKey
ALTER TABLE "Field" DROP CONSTRAINT "Field_templateId_fkey";
-- DropForeignKey
ALTER TABLE "Recipient" DROP CONSTRAINT "Recipient_documentId_fkey";
-- DropForeignKey
ALTER TABLE "Recipient" DROP CONSTRAINT "Recipient_templateId_fkey";
-- DropForeignKey
ALTER TABLE "Template" DROP CONSTRAINT "Template_folderId_fkey";
-- DropForeignKey
ALTER TABLE "Template" DROP CONSTRAINT "Template_teamId_fkey";
-- DropForeignKey
ALTER TABLE "Template" DROP CONSTRAINT "Template_templateDocumentDataId_fkey";
-- DropForeignKey
ALTER TABLE "Template" DROP CONSTRAINT "Template_userId_fkey";
-- DropForeignKey
ALTER TABLE "TemplateDirectLink" DROP CONSTRAINT "TemplateDirectLink_templateId_fkey";
-- DropIndex
DROP INDEX "Document_folderId_idx";
-- DropIndex
DROP INDEX "Document_status_idx";
-- DropIndex
DROP INDEX "Document_userId_idx";
-- DropIndex
DROP INDEX "DocumentMeta_documentId_key";
-- DropIndex
DROP INDEX "DocumentMeta_templateId_key";
-- DropIndex
DROP INDEX "DocumentShareLink_documentId_email_key";
-- DropIndex
DROP INDEX "Field_documentId_idx";
-- DropIndex
DROP INDEX "Field_templateId_idx";
-- DropIndex
DROP INDEX "Recipient_documentId_email_key";
-- DropIndex
DROP INDEX "Recipient_documentId_idx";
-- DropIndex
DROP INDEX "Recipient_templateId_email_key";
-- DropIndex
DROP INDEX "Recipient_templateId_idx";
-- DropIndex
DROP INDEX "TemplateDirectLink_templateId_key";
-- AlterTable
ALTER TABLE "Document" DROP COLUMN "authOptions",
DROP COLUMN "completedAt",
DROP COLUMN "createdAt",
DROP COLUMN "deletedAt",
DROP COLUMN "externalId",
DROP COLUMN "folderId",
DROP COLUMN "formValues",
DROP COLUMN "source",
DROP COLUMN "status",
DROP COLUMN "teamId",
DROP COLUMN "templateId",
DROP COLUMN "updatedAt",
DROP COLUMN "useLegacyFieldInsertion",
DROP COLUMN "userId",
DROP COLUMN "visibility";
-- AlterTable
ALTER TABLE "DocumentAuditLog" DROP COLUMN "documentId",
ADD COLUMN "envelopeId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "DocumentMeta" DROP COLUMN "documentId",
DROP COLUMN "templateId",
ADD COLUMN "envelopeId" TEXT;
-- AlterTable
ALTER TABLE "DocumentShareLink" DROP COLUMN "documentId",
ADD COLUMN "envelopeId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "Field" DROP COLUMN "documentId",
DROP COLUMN "templateId",
ADD COLUMN "envelopeId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "Recipient" DROP COLUMN "documentId",
DROP COLUMN "templateId",
ADD COLUMN "envelopeId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "TemplateDirectLink" DROP COLUMN "templateId",
ADD COLUMN "envelopeId" TEXT NOT NULL,
ADD COLUMN "type" "TemplateDirectLinkType" NOT NULL;
-- DropTable
DROP TABLE "Template";
-- DropEnum
DROP TYPE "TemplateType";
-- CreateTable
CREATE TABLE "Envelope" (
"id" TEXT NOT NULL,
"secondaryId" TEXT NOT NULL,
"externalId" TEXT,
"type" "EnvelopeType" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"completedAt" TIMESTAMP(3),
"deletedAt" TIMESTAMP(3),
"title" TEXT NOT NULL,
"status" "DocumentStatus" NOT NULL DEFAULT 'DRAFT',
"source" "DocumentSource" NOT NULL,
"useLegacyFieldInsertion" BOOLEAN NOT NULL DEFAULT false,
"authOptions" JSONB,
"formValues" JSONB,
"visibility" "DocumentVisibility" NOT NULL DEFAULT 'EVERYONE',
"publicTitle" TEXT NOT NULL DEFAULT '',
"publicDescription" TEXT NOT NULL DEFAULT '',
"templateId" INTEGER,
"userId" INTEGER NOT NULL,
"teamId" INTEGER NOT NULL,
"folderId" TEXT,
"documentMetaId" TEXT NOT NULL,
CONSTRAINT "Envelope_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Envelope_secondaryId_key" ON "Envelope"("secondaryId");
-- CreateIndex
CREATE UNIQUE INDEX "Envelope_documentMetaId_key" ON "Envelope"("documentMetaId");
-- CreateIndex
CREATE UNIQUE INDEX "DocumentShareLink_envelopeId_email_key" ON "DocumentShareLink"("envelopeId", "email");
-- CreateIndex
CREATE INDEX "Field_envelopeId_idx" ON "Field"("envelopeId");
-- CreateIndex
CREATE INDEX "Recipient_envelopeId_idx" ON "Recipient"("envelopeId");
-- CreateIndex
CREATE UNIQUE INDEX "Recipient_envelopeId_email_key" ON "Recipient"("envelopeId", "email");
-- CreateIndex
CREATE UNIQUE INDEX "TemplateDirectLink_envelopeId_key" ON "TemplateDirectLink"("envelopeId");
-- AddForeignKey
ALTER TABLE "Envelope" ADD CONSTRAINT "Envelope_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Envelope" ADD CONSTRAINT "Envelope_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Envelope" ADD CONSTRAINT "Envelope_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Envelope" ADD CONSTRAINT "Envelope_documentMetaId_fkey" FOREIGN KEY ("documentMetaId") REFERENCES "DocumentMeta"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DocumentAuditLog" ADD CONSTRAINT "DocumentAuditLog_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Recipient" ADD CONSTRAINT "Recipient_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Field" ADD CONSTRAINT "Field_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DocumentShareLink" ADD CONSTRAINT "DocumentShareLink_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TemplateDirectLink" ADD CONSTRAINT "TemplateDirectLink_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,11 @@
/*
Warnings:
- Added the required column `envelopeId` to the `Document` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Document" ADD COLUMN "envelopeId" TEXT NOT NULL;
-- AddForeignKey
ALTER TABLE "Document" ADD CONSTRAINT "Document_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -64,8 +64,7 @@ model User {
twoFactorBackupCodes String? twoFactorBackupCodes String?
folders Folder[] folders Folder[]
documents Document[] envelopes Envelope[]
templates Template[]
verificationTokens VerificationToken[] verificationTokens VerificationToken[]
apiTokens ApiToken[] apiTokens ApiToken[]
@ -348,8 +347,7 @@ model Folder {
pinned Boolean @default(false) pinned Boolean @default(false)
parentId String? parentId String?
parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: Cascade) parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: Cascade)
documents Document[] envelopes Envelope[]
templates Template[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
subfolders Folder[] @relation("FolderToFolder") subfolders Folder[] @relation("FolderToFolder")
@ -362,53 +360,79 @@ model Folder {
@@index([type]) @@index([type])
} }
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"]) enum EnvelopeType {
model Document { DOCUMENT
id Int @id @default(autoincrement()) TEMPLATE
qrToken String? /// @zod.string.describe("The token for viewing the document using the QR code on the certificate.") }
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
model Envelope {
id String @id @default(cuid())
secondaryId String @unique
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
type EnvelopeType
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
completedAt DateTime?
deletedAt DateTime?
title String
status DocumentStatus @default(DRAFT)
source DocumentSource
useLegacyFieldInsertion Boolean @default(false)
documents Document[]
recipients Recipient[]
fields Field[]
shareLinks DocumentShareLink[]
auditLogs DocumentAuditLog[]
// Envelope settings
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
visibility DocumentVisibility @default(EVERYONE)
// Template specific fields.
publicTitle String @default("")
publicDescription String @default("")
directLink TemplateDirectLink?
templateId Int? // Todo: Migrate from templateId -> This @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// Relations
userId Int /// @zod.number.describe("The ID of the user that created this document.") userId Int /// @zod.number.describe("The ID of the user that created this document.")
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId Int teamId Int
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema) folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema) folderId String?
visibility DocumentVisibility @default(EVERYONE)
title String documentMetaId String @unique
status DocumentStatus @default(DRAFT) documentMeta DocumentMeta @relation(fields: [documentMetaId], references: [id])
recipients Recipient[] }
fields Field[]
shareLinks DocumentShareLink[] /// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
model Document {
id Int @id @default(autoincrement())
qrToken String? /// @zod.string.describe("The token for viewing the document using the QR code on the certificate.")
title String
documentDataId String documentDataId String
documentMeta DocumentMeta? documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
completedAt DateTime?
deletedAt DateTime?
templateId Int?
source DocumentSource
useLegacyFieldInsertion Boolean @default(false) envelopeId String
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
auditLogs DocumentAuditLog[]
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
folderId String?
@@unique([documentDataId]) @@unique([documentDataId])
@@index([userId])
@@index([status])
@@index([folderId])
} }
model DocumentAuditLog { model DocumentAuditLog {
id String @id @default(cuid()) id String @id @default(cuid())
documentId Int envelopeId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
type String type String
data Json data Json
@ -420,7 +444,7 @@ model DocumentAuditLog {
userAgent String? userAgent String?
ipAddress String? ipAddress String?
Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
} }
enum DocumentDataType { enum DocumentDataType {
@ -440,7 +464,6 @@ model DocumentData {
data String data String
initialData String initialData String
document Document? document Document?
template Template?
} }
enum DocumentDistributionMethod { enum DocumentDistributionMethod {
@ -471,11 +494,8 @@ model DocumentMeta {
emailReplyTo String? emailReplyTo String?
emailId String? emailId String?
documentId Int? @unique envelopeId String?
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) envelope Envelope?
templateId Int? @unique
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
} }
enum ReadStatus { enum ReadStatus {
@ -505,8 +525,7 @@ enum RecipientRole {
/// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"]) /// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])
model Recipient { model Recipient {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
documentId Int? envelopeId String
templateId Int?
email String @db.VarChar(255) email String @db.VarChar(255)
name String @default("") @db.VarChar(255) name String @default("") @db.VarChar(255)
token String token String
@ -520,15 +539,12 @@ model Recipient {
readStatus ReadStatus @default(NOT_OPENED) readStatus ReadStatus @default(NOT_OPENED)
signingStatus SigningStatus @default(NOT_SIGNED) signingStatus SigningStatus @default(NOT_SIGNED)
sendStatus SendStatus @default(NOT_SENT) sendStatus SendStatus @default(NOT_SENT)
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
fields Field[] fields Field[]
signatures Signature[] signatures Signature[]
@@unique([documentId, email]) @@unique([envelopeId, email])
@@unique([templateId, email]) @@index([envelopeId])
@@index([documentId])
@@index([templateId])
@@index([token]) @@index([token])
} }
@ -550,8 +566,7 @@ enum FieldType {
model Field { model Field {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
secondaryId String @unique @default(cuid()) secondaryId String @unique @default(cuid())
documentId Int? envelopeId String
templateId Int?
recipientId Int recipientId Int
type FieldType type FieldType
page Int /// @zod.number.describe("The page number of the field on the document. Starts from 1.") page Int /// @zod.number.describe("The page number of the field on the document. Starts from 1.")
@ -561,14 +576,12 @@ model Field {
height Decimal @default(-1) height Decimal @default(-1)
customText String customText String
inserted Boolean inserted Boolean
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade) recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
signature Signature? signature Signature?
fieldMeta Json? /// [FieldMeta] @zod.custom.use(ZFieldMetaNotOptionalSchema) fieldMeta Json? /// [FieldMeta] @zod.custom.use(ZFieldMetaNotOptionalSchema)
@@index([documentId]) @@index([envelopeId])
@@index([templateId])
@@index([recipientId]) @@index([recipientId])
} }
@ -590,13 +603,13 @@ model DocumentShareLink {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
email String email String
slug String @unique slug String @unique
documentId Int envelopeId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
@@unique([documentId, email]) @@unique([envelopeId, email])
} }
enum OrganisationType { enum OrganisationType {
@ -807,8 +820,7 @@ model Team {
profile TeamProfile? profile TeamProfile?
documents Document[] envelopes Envelope[]
templates Template[]
folders Folder[] folders Folder[]
apiTokens ApiToken[] apiTokens ApiToken[]
webhooks Webhook[] webhooks Webhook[]
@ -841,58 +853,26 @@ model TeamEmailVerification {
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
} }
enum TemplateType { // TODO: USE THIS
// TODO: USE THIS
// TODO: USE THIS
// TODO: USE THIS
enum TemplateDirectLinkType {
PUBLIC PUBLIC
PRIVATE PRIVATE
} }
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])
model Template {
id Int @id @default(autoincrement())
externalId String?
type TemplateType @default(PRIVATE)
title String
visibility DocumentVisibility @default(EVERYONE)
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
templateMeta DocumentMeta?
templateDocumentDataId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
publicTitle String @default("")
publicDescription String @default("")
useLegacyFieldInsertion Boolean @default(false)
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId Int
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade)
recipients Recipient[]
fields Field[]
directLink TemplateDirectLink?
documents Document[]
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
folderId String?
@@unique([templateDocumentDataId])
@@index([userId])
}
model TemplateDirectLink { model TemplateDirectLink {
id String @id @unique @default(cuid()) id String @id @unique @default(cuid())
templateId Int @unique envelopeId String @unique
token String @unique token String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
enabled Boolean enabled Boolean
type TemplateDirectLinkType
directTemplateRecipientId Int directTemplateRecipientId Int
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade) envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
} }
model SiteSettings { model SiteSettings {

View File

@ -0,0 +1,59 @@
/**
* Legacy Document schema to confirm backwards API compatibility since
* we migrated Documents to Envelopes.
*/
import { z } from 'zod';
import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import DocumentStatusSchema from '../generated/zod/inputTypeSchemas/DocumentStatusSchema';
import DocumentVisibilitySchema from '../generated/zod/inputTypeSchemas/DocumentVisibilitySchema';
const DocumentSourceSchema = z.enum(['DOCUMENT', 'TEMPLATE', 'TEMPLATE_DIRECT_LINK']);
const DocumentTypeSchema = z.enum(['DOCUMENT', 'PUBLIC_TEMPLATE', 'PRIVATE_TEMPLATE']);
/////////////////////////////////////////
// DOCUMENT SCHEMA
/////////////////////////////////////////
export const LegacyDocumentSchema = z.object({
type: DocumentTypeSchema,
visibility: DocumentVisibilitySchema,
status: DocumentStatusSchema,
source: DocumentSourceSchema,
id: z.number(),
qrToken: z
.string()
.describe('The token for viewing the document using the QR code on the certificate.')
.nullable(),
externalId: z
.string()
.describe('A custom external ID you can use to identify the document.')
.nullable(),
secondaryDocumentId: z.number(),
secondaryTemplateId: z.number(),
publicTitle: z.string(),
publicDescription: z.string(),
createdFromDocumentId: z.number().nullable(),
userId: z.number().describe('The ID of the user that created this document.'),
teamId: z.number(),
/**
* [DocumentAuthOptions]
*/
authOptions: ZDocumentAuthOptionsSchema.nullable(),
/**
* [DocumentFormValues]
*/
formValues: ZDocumentFormValuesSchema.nullable(),
title: z.string(),
documentDataId: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
completedAt: z.coerce.date().nullable(),
deletedAt: z.coerce.date().nullable(),
useLegacyFieldInsertion: z.boolean(),
folderId: z.string().nullable(),
});
export type Document = z.infer<typeof LegacyDocumentSchema>;

View File

@ -0,0 +1,36 @@
/**
* Legacy Template schema to confirm backwards API compatibility since
* we removed the "Template" prisma schema model.
*/
import { z } from 'zod';
import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { DocumentVisibilitySchema } from '../generated/zod/inputTypeSchemas/DocumentVisibilitySchema';
export const TemplateTypeSchema = z.enum(['PUBLIC', 'PRIVATE']);
export type TemplateTypeType = `${z.infer<typeof TemplateTypeSchema>}`;
export const TemplateSchema = z.object({
type: TemplateTypeSchema,
visibility: DocumentVisibilitySchema,
id: z.number(),
externalId: z.string().nullable(),
title: z.string(),
/**
* [DocumentAuthOptions]
*/
authOptions: ZDocumentAuthOptionsSchema.nullable(),
templateDocumentDataId: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
publicTitle: z.string(),
publicDescription: z.string(),
useLegacyFieldInsertion: z.boolean(),
userId: z.number(),
teamId: z.number(),
folderId: z.string().nullable(),
});
export type Template = z.infer<typeof TemplateSchema>;