mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 08:42:12 +10:00
feat: migrate templates and documents to envelope model
This commit is contained in:
@ -22,7 +22,7 @@ import { ZRecipientAuthOptionsSchema } from '../types/document-auth';
|
||||
import type { ApiRequestMetadata, RequestMetadata } from '../universal/extract-request-metadata';
|
||||
|
||||
type CreateDocumentAuditLogDataOptions<T = TDocumentAuditLog['type']> = {
|
||||
documentId: number;
|
||||
envelopeId: string;
|
||||
type: T;
|
||||
data: Extract<TDocumentAuditLog, { type: T }>['data'];
|
||||
user?: { email?: string | null; id?: number | null; name?: string | null } | null;
|
||||
@ -32,13 +32,13 @@ type CreateDocumentAuditLogDataOptions<T = TDocumentAuditLog['type']> = {
|
||||
|
||||
export type CreateDocumentAuditLogDataResponse = Pick<
|
||||
DocumentAuditLog,
|
||||
'type' | 'ipAddress' | 'userAgent' | 'email' | 'userId' | 'name' | 'documentId'
|
||||
'type' | 'ipAddress' | 'userAgent' | 'email' | 'userId' | 'name' | 'envelopeId'
|
||||
> & {
|
||||
data: TDocumentAuditLog['data'];
|
||||
};
|
||||
|
||||
export const createDocumentAuditLogData = <T extends TDocumentAuditLog['type']>({
|
||||
documentId,
|
||||
envelopeId,
|
||||
type,
|
||||
data,
|
||||
user,
|
||||
@ -62,7 +62,7 @@ export const createDocumentAuditLogData = <T extends TDocumentAuditLog['type']>(
|
||||
return {
|
||||
type,
|
||||
data,
|
||||
documentId,
|
||||
envelopeId,
|
||||
userId,
|
||||
email,
|
||||
name,
|
||||
@ -203,7 +203,6 @@ export const diffDocumentMetaChanges = (
|
||||
const oldMessage = oldData?.message ?? '';
|
||||
const oldSubject = oldData?.subject ?? '';
|
||||
const oldTimezone = oldData?.timezone ?? '';
|
||||
const oldPassword = oldData?.password ?? null;
|
||||
const oldRedirectUrl = oldData?.redirectUrl ?? '';
|
||||
const oldEmailId = oldData?.emailId || null;
|
||||
const oldEmailReplyTo = oldData?.emailReplyTo || null;
|
||||
@ -258,12 +257,6 @@ export const diffDocumentMetaChanges = (
|
||||
});
|
||||
}
|
||||
|
||||
if (oldPassword !== newData.password) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.PASSWORD,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldEmailId !== newEmailId) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.EMAIL_ID,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Document, Recipient } from '@prisma/client';
|
||||
import type { Envelope, Recipient } from '@prisma/client';
|
||||
|
||||
import type {
|
||||
TDocumentAuthOptions,
|
||||
@ -10,7 +10,7 @@ import { DocumentAuth } from '../types/document-auth';
|
||||
import { ZDocumentAuthOptionsSchema, ZRecipientAuthOptionsSchema } from '../types/document-auth';
|
||||
|
||||
type ExtractDocumentAuthMethodsOptions = {
|
||||
documentAuth: Document['authOptions'];
|
||||
documentAuth: Envelope['authOptions'];
|
||||
recipientAuth?: Recipient['authOptions'];
|
||||
};
|
||||
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
|
||||
export const determineDocumentVisibility = (
|
||||
globalVisibility: DocumentVisibility | null | undefined,
|
||||
userRole: TeamMemberRole,
|
||||
): DocumentVisibility => {
|
||||
if (globalVisibility) {
|
||||
return globalVisibility;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.ADMIN) {
|
||||
return DocumentVisibility.ADMIN;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.MANAGER) {
|
||||
return DocumentVisibility.MANAGER_AND_ABOVE;
|
||||
}
|
||||
|
||||
return DocumentVisibility.EVERYONE;
|
||||
};
|
||||
@ -1,10 +1,19 @@
|
||||
import type { Document, DocumentMeta, OrganisationGlobalSettings } from '@prisma/client';
|
||||
import type {
|
||||
DocumentMeta,
|
||||
Envelope,
|
||||
OrganisationGlobalSettings,
|
||||
Recipient,
|
||||
Team,
|
||||
User,
|
||||
} from '@prisma/client';
|
||||
import { DocumentDistributionMethod, DocumentSigningOrder, DocumentStatus } from '@prisma/client';
|
||||
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones';
|
||||
import type { TDocumentLite, TDocumentMany } from '../types/document';
|
||||
import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email';
|
||||
import { mapSecondaryIdToDocumentId } from './envelope';
|
||||
|
||||
export const isDocumentCompleted = (document: Pick<Document, 'status'> | DocumentStatus) => {
|
||||
export const isDocumentCompleted = (document: Pick<Envelope, 'status'> | DocumentStatus) => {
|
||||
const status = typeof document === 'string' ? document : document.status;
|
||||
|
||||
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
|
||||
@ -36,7 +45,6 @@ export const extractDerivedDocumentMeta = (
|
||||
dateFormat: meta.dateFormat || settings.documentDateFormat,
|
||||
message: meta.message || null,
|
||||
subject: meta.subject || null,
|
||||
password: meta.password || null,
|
||||
redirectUrl: meta.redirectUrl || null,
|
||||
|
||||
signingOrder: meta.signingOrder || DocumentSigningOrder.PARALLEL,
|
||||
@ -53,5 +61,81 @@ export const extractDerivedDocumentMeta = (
|
||||
emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo,
|
||||
emailSettings:
|
||||
meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS,
|
||||
} satisfies Omit<DocumentMeta, 'id' | 'documentId' | 'templateId'>;
|
||||
} satisfies Omit<DocumentMeta, 'id'>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map an envelope to a legacy document lite response entity.
|
||||
*
|
||||
* Do not use spread operator here to avoid unexpected behavior.
|
||||
*/
|
||||
export const mapEnvelopeToDocumentLite = (envelope: Envelope): TDocumentLite => {
|
||||
const documentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
return {
|
||||
id: documentId, // Use legacy ID.
|
||||
visibility: envelope.visibility,
|
||||
status: envelope.status,
|
||||
source: envelope.source,
|
||||
externalId: envelope.externalId,
|
||||
userId: envelope.userId,
|
||||
authOptions: envelope.authOptions,
|
||||
formValues: envelope.formValues,
|
||||
title: envelope.title,
|
||||
createdAt: envelope.createdAt,
|
||||
updatedAt: envelope.updatedAt,
|
||||
completedAt: envelope.completedAt,
|
||||
deletedAt: envelope.deletedAt,
|
||||
teamId: envelope.teamId,
|
||||
folderId: envelope.folderId,
|
||||
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
|
||||
templateId: envelope.templateId,
|
||||
};
|
||||
};
|
||||
|
||||
type MapEnvelopeToDocumentManyOptions = Envelope & {
|
||||
user: Pick<User, 'id' | 'name' | 'email'>;
|
||||
team: Pick<Team, 'id' | 'url'>;
|
||||
recipients: Recipient[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Map an envelope to a legacy document many response entity.
|
||||
*
|
||||
* Do not use spread operator here to avoid unexpected behavior.
|
||||
*/
|
||||
export const mapEnvelopesToDocumentMany = (
|
||||
envelope: MapEnvelopeToDocumentManyOptions,
|
||||
): TDocumentMany => {
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
return {
|
||||
id: legacyDocumentId, // Use legacy ID.
|
||||
visibility: envelope.visibility,
|
||||
status: envelope.status,
|
||||
source: envelope.source,
|
||||
externalId: envelope.externalId,
|
||||
userId: envelope.userId,
|
||||
authOptions: envelope.authOptions,
|
||||
formValues: envelope.formValues,
|
||||
title: envelope.title,
|
||||
createdAt: envelope.createdAt,
|
||||
updatedAt: envelope.updatedAt,
|
||||
completedAt: envelope.completedAt,
|
||||
deletedAt: envelope.deletedAt,
|
||||
teamId: envelope.teamId,
|
||||
folderId: envelope.folderId,
|
||||
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
|
||||
templateId: envelope.templateId,
|
||||
user: {
|
||||
id: envelope.userId,
|
||||
name: envelope.user.name,
|
||||
email: envelope.user.email,
|
||||
},
|
||||
team: {
|
||||
id: envelope.teamId,
|
||||
url: envelope.team.url,
|
||||
},
|
||||
recipients: envelope.recipients,
|
||||
};
|
||||
};
|
||||
|
||||
141
packages/lib/utils/envelope.ts
Normal file
141
packages/lib/utils/envelope.ts
Normal file
@ -0,0 +1,141 @@
|
||||
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 ZDocumentIdSchema = z.string().regex(/^document_\d+$/);
|
||||
const ZTemplateIdSchema = z.string().regex(/^template_\d+$/);
|
||||
const ZEnvelopeIdSchema = z.string().regex(/^envelope_.{2,}$/);
|
||||
|
||||
export type EnvelopeIdOptions =
|
||||
| {
|
||||
type: 'envelopeId';
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'documentId';
|
||||
id: number;
|
||||
}
|
||||
| {
|
||||
type: 'templateId';
|
||||
id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses an unknown document or template ID.
|
||||
*
|
||||
* This is UNSAFE because does not validate access, it just builds the query for ID and TYPE.
|
||||
*/
|
||||
export const unsafeBuildEnvelopeIdQuery = (
|
||||
options: EnvelopeIdOptions,
|
||||
expectedEnvelopeType: EnvelopeType | null,
|
||||
) => {
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectedEnvelopeType) {
|
||||
return {
|
||||
id: value.id,
|
||||
type: expectedEnvelopeType,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: value.id,
|
||||
};
|
||||
})
|
||||
.with({ type: 'documentId' }, (value) => {
|
||||
if (expectedEnvelopeType && expectedEnvelopeType !== EnvelopeType.DOCUMENT) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid document ID',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
secondaryId: mapDocumentIdToSecondaryId(value.id),
|
||||
};
|
||||
})
|
||||
.with({ type: 'templateId' }, (value) => {
|
||||
if (expectedEnvelopeType && expectedEnvelopeType !== EnvelopeType.TEMPLATE) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid template ID',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
secondaryId: mapTemplateIdToSecondaryId(value.id),
|
||||
};
|
||||
})
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps a legacy document ID number to an envelope secondary ID.
|
||||
*
|
||||
* @returns The formatted envelope secondary ID (document_123)
|
||||
*/
|
||||
export const mapDocumentIdToSecondaryId = (documentId: number) => {
|
||||
return `${envelopeDocumentPrefixId}_${documentId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps a legacy template ID number to an envelope secondary ID.
|
||||
*
|
||||
* @returns The formatted envelope secondary ID (template_123)
|
||||
*/
|
||||
export const mapTemplateIdToSecondaryId = (templateId: number) => {
|
||||
return `${envelopeTemplatePrefixId}_${templateId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps an envelope secondary ID to a legacy document ID number.
|
||||
*
|
||||
* Throws an error if the secondary ID is not a document ID.
|
||||
*
|
||||
* @param secondaryId The envelope secondary ID (document_123)
|
||||
* @returns The legacy document ID number (123)
|
||||
*/
|
||||
export const mapSecondaryIdToDocumentId = (secondaryId: string) => {
|
||||
const parsed = ZDocumentIdSchema.safeParse(secondaryId);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid document ID',
|
||||
});
|
||||
}
|
||||
|
||||
return parseInt(parsed.data.split('_')[1]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps an envelope secondary ID to a legacy template ID number.
|
||||
*
|
||||
* Throws an error if the secondary ID is not a template ID.
|
||||
*
|
||||
* @param secondaryId The envelope secondary ID (template_123)
|
||||
* @returns The legacy template ID number (123)
|
||||
*/
|
||||
export const mapSecondaryIdToTemplateId = (secondaryId: string) => {
|
||||
const parsed = ZTemplateIdSchema.safeParse(secondaryId);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid template ID',
|
||||
});
|
||||
}
|
||||
|
||||
return parseInt(parsed.data.split('_')[1]);
|
||||
};
|
||||
@ -1,14 +1,14 @@
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import type { DocumentWithRecipients } from '@documenso/prisma/types/document-with-recipient';
|
||||
import type { EnvelopeWithRecipients } from '@documenso/prisma/types/document-with-recipient';
|
||||
|
||||
export type MaskRecipientTokensForDocumentOptions<T extends DocumentWithRecipients> = {
|
||||
export type MaskRecipientTokensForDocumentOptions<T extends EnvelopeWithRecipients> = {
|
||||
document: T;
|
||||
user?: Pick<User, 'id' | 'email' | 'name'>;
|
||||
user?: Pick<User, 'id' | 'email'>;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export const maskRecipientTokensForDocument = <T extends DocumentWithRecipients>({
|
||||
export const maskRecipientTokensForDocument = <T extends EnvelopeWithRecipients>({
|
||||
document,
|
||||
user,
|
||||
token,
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import type { OrganisationGlobalSettings, Prisma, TeamGlobalSettings } from '@prisma/client';
|
||||
import type {
|
||||
DocumentVisibility,
|
||||
OrganisationGlobalSettings,
|
||||
Prisma,
|
||||
TeamGlobalSettings,
|
||||
} from '@prisma/client';
|
||||
|
||||
import type { TeamGroup } from '@documenso/prisma/generated/types';
|
||||
import type { TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
@ -6,6 +11,7 @@ import type { TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||
import {
|
||||
LOWEST_TEAM_ROLE,
|
||||
TEAM_DOCUMENT_VISIBILITY_MAP,
|
||||
TEAM_MEMBER_ROLE_HIERARCHY,
|
||||
TEAM_MEMBER_ROLE_PERMISSIONS_MAP,
|
||||
} from '../constants/teams';
|
||||
@ -48,6 +54,17 @@ export const canExecuteTeamAction = (
|
||||
return TEAM_MEMBER_ROLE_PERMISSIONS_MAP[action].some((i) => i === role);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether a team role can access the visibility of a document.
|
||||
*
|
||||
* @param action The action the user is trying to execute.
|
||||
* @param role The current role of the user.
|
||||
* @returns Whether the user can execute the action.
|
||||
*/
|
||||
export const canAccessTeamDocument = (role: TeamMemberRole, visibility: DocumentVisibility) => {
|
||||
return TEAM_DOCUMENT_VISIBILITY_MAP[role].some((i) => i === visibility);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares the provided `currentUserRole` with the provided `roleToCheck` to determine
|
||||
* whether the `currentUserRole` has permission to modify the `roleToCheck`.
|
||||
@ -106,7 +123,7 @@ export const extractTeamSignatureSettings = (
|
||||
return signatureTypes;
|
||||
};
|
||||
|
||||
type BuildTeamWhereQueryOptions = {
|
||||
export type BuildTeamWhereQueryOptions = {
|
||||
teamId: number | undefined;
|
||||
userId: number;
|
||||
roles?: TeamMemberRole[];
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import type { Envelope } from '@prisma/client';
|
||||
import { type Recipient } from '@prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||
import type { TTemplateLite } from '../types/template';
|
||||
import { mapSecondaryIdToTemplateId } from './envelope';
|
||||
|
||||
export const formatDirectTemplatePath = (token: string) => {
|
||||
return `${NEXT_PUBLIC_WEBAPP_URL()}/d/${token}`;
|
||||
@ -42,3 +45,24 @@ export const generateAvaliableRecipientPlaceholder = (currentRecipients: Recipie
|
||||
|
||||
return recipientPlaceholder;
|
||||
};
|
||||
|
||||
export const mapEnvelopeToTemplateLite = (envelope: Envelope): TTemplateLite => {
|
||||
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
|
||||
|
||||
return {
|
||||
id: legacyTemplateId,
|
||||
type: envelope.templateType,
|
||||
visibility: envelope.visibility,
|
||||
externalId: envelope.externalId,
|
||||
title: envelope.title,
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
authOptions: envelope.authOptions,
|
||||
createdAt: envelope.createdAt,
|
||||
updatedAt: envelope.updatedAt,
|
||||
publicTitle: envelope.publicTitle,
|
||||
publicDescription: envelope.publicDescription,
|
||||
folderId: envelope.folderId,
|
||||
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user