mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 09:25:08 +10:00
chore: merge main, resolve biome formatting conflicts
Merge origin/main into feat/external-2fa-codes. Resolve formatting conflicts caused by biome rollout; preserve both feature streams: PR's external 2FA token + signing-session 2FA proof additions plus main's RateLimit/RecipientExpired/signingReminders/date-auto-insert. In complete-document-with-token.ts, drop the duplicate early field-fetching block introduced when main moved that logic later with date auto-insert support; keep the EXTERNAL_TWO_FACTOR_AUTH check using derivedRecipientActionAuth.
This commit is contained in:
@@ -1,14 +1,3 @@
|
||||
import type { DocumentMeta, DocumentVisibility, TemplateType } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
EnvelopeType,
|
||||
FolderType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { PlaceholderInfo } from '@documenso/lib/server-only/pdf/auto-place-fields';
|
||||
import { convertPlaceholdersToFieldInputs } from '@documenso/lib/server-only/pdf/auto-place-fields';
|
||||
@@ -20,6 +9,16 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentMeta, DocumentVisibility, TemplateType } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
EnvelopeType,
|
||||
FolderType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
|
||||
import type {
|
||||
TDocumentAccessAuthTypes,
|
||||
@@ -30,10 +29,7 @@ import type {
|
||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||
import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment';
|
||||
import type { TFieldAndMeta } from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
@@ -97,6 +93,14 @@ export type CreateEnvelopeOptions = {
|
||||
data: string;
|
||||
type?: TEnvelopeAttachmentType;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Whether to bypass adding default recipients.
|
||||
*
|
||||
* Defaults to false.
|
||||
*/
|
||||
bypassDefaultRecipients?: boolean;
|
||||
|
||||
meta?: Partial<Omit<DocumentMeta, 'id'>>;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
@@ -110,6 +114,7 @@ export const createEnvelope = async ({
|
||||
meta,
|
||||
requestMetadata,
|
||||
internalVersion,
|
||||
bypassDefaultRecipients = false,
|
||||
}: CreateEnvelopeOptions) => {
|
||||
const {
|
||||
type,
|
||||
@@ -198,7 +203,7 @@ export const createEnvelope = async ({
|
||||
|
||||
const titleToUse = item.title || title;
|
||||
|
||||
const newDocumentData = await putPdfFileServerSide({
|
||||
const { documentData: newDocumentData } = await putPdfFileServerSide({
|
||||
name: titleToUse,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(normalizedPdf),
|
||||
@@ -256,24 +261,8 @@ export const createEnvelope = async ({
|
||||
// for uploads from the frontend
|
||||
const timezoneToUse = meta?.timezone || settings.documentTimezone || userTimezone;
|
||||
|
||||
const documentMeta = await prisma.documentMeta.create({
|
||||
data: extractDerivedDocumentMeta(settings, {
|
||||
...meta,
|
||||
timezone: timezoneToUse,
|
||||
}),
|
||||
});
|
||||
|
||||
const secondaryId =
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? await incrementDocumentId().then((v) => v.formattedDocumentId)
|
||||
: await incrementTemplateId().then((v) => v.formattedTemplateId);
|
||||
|
||||
const getValidatedDelegatedOwner = async () => {
|
||||
if (
|
||||
!settings.delegateDocumentOwnership ||
|
||||
!delegatedDocumentOwner ||
|
||||
requestMetadata.source === 'app'
|
||||
) {
|
||||
if (!settings.delegateDocumentOwnership || !delegatedDocumentOwner || requestMetadata.source === 'app') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -302,7 +291,18 @@ export const createEnvelope = async ({
|
||||
return delegatedOwner;
|
||||
};
|
||||
|
||||
const delegatedOwner = await getValidatedDelegatedOwner();
|
||||
const [documentMeta, secondaryId, delegatedOwner] = await Promise.all([
|
||||
prisma.documentMeta.create({
|
||||
data: extractDerivedDocumentMeta(settings, {
|
||||
...meta,
|
||||
timezone: timezoneToUse,
|
||||
}),
|
||||
}),
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? incrementDocumentId().then((v) => v.formattedDocumentId)
|
||||
: incrementTemplateId().then((v) => v.formattedTemplateId),
|
||||
getValidatedDelegatedOwner(),
|
||||
]);
|
||||
const envelopeOwnerId = delegatedOwner?.id ?? userId;
|
||||
|
||||
const createdEnvelope = await prisma.$transaction(async (tx) => {
|
||||
@@ -355,17 +355,16 @@ export const createEnvelope = async ({
|
||||
|
||||
const firstEnvelopeItem = envelope.envelopeItems[0];
|
||||
|
||||
const defaultRecipients = settings.defaultRecipients
|
||||
? ZDefaultRecipientsSchema.parse(settings.defaultRecipients)
|
||||
: [];
|
||||
const defaultRecipients =
|
||||
settings.defaultRecipients && !bypassDefaultRecipients
|
||||
? ZDefaultRecipientsSchema.parse(settings.defaultRecipients)
|
||||
: [];
|
||||
|
||||
const mappedDefaultRecipients: CreateEnvelopeRecipientOptions[] = defaultRecipients.map(
|
||||
(recipient) => ({
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
}),
|
||||
);
|
||||
const mappedDefaultRecipients: CreateEnvelopeRecipientOptions[] = defaultRecipients.map((recipient) => ({
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
}));
|
||||
|
||||
const allRecipients = [...(data.recipients || []), ...mappedDefaultRecipients];
|
||||
|
||||
@@ -417,8 +416,7 @@ export const createEnvelope = async ({
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
signingStatus: recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
authOptions: recipientAuthOptions,
|
||||
fields: {
|
||||
createMany: {
|
||||
@@ -431,9 +429,7 @@ export const createEnvelope = async ({
|
||||
);
|
||||
|
||||
// Create fields from PDF placeholders (extracted at upload time).
|
||||
const itemsWithPlaceholders = envelopeItems.filter(
|
||||
(item) => item.placeholders && item.placeholders.length > 0,
|
||||
);
|
||||
const itemsWithPlaceholders = envelopeItems.filter((item) => item.placeholders && item.placeholders.length > 0);
|
||||
|
||||
if (itemsWithPlaceholders.length > 0) {
|
||||
// Collect all unique recipient placeholder references (e.g. "r1", "r2").
|
||||
@@ -463,23 +459,18 @@ export const createEnvelope = async ({
|
||||
|
||||
// If recipients were not provided, create placeholder recipients even when defaults exist.
|
||||
if (shouldCreatePlaceholderRecipients) {
|
||||
const existingRecipientEmails = new Set(
|
||||
availableRecipients.map((recipient) => recipient.email.toLowerCase()),
|
||||
);
|
||||
const existingRecipientEmails = new Set(availableRecipients.map((recipient) => recipient.email.toLowerCase()));
|
||||
|
||||
const placeholderRecipients = Array.from(
|
||||
uniqueRecipientRefs.entries(),
|
||||
([recipientIndex, name]) => ({
|
||||
envelopeId: envelope.id,
|
||||
email: `recipient.${recipientIndex}@documenso.com`,
|
||||
name,
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: recipientIndex,
|
||||
token: nanoid(),
|
||||
sendStatus: SendStatus.NOT_SENT,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
}),
|
||||
).filter((recipient) => !existingRecipientEmails.has(recipient.email.toLowerCase()));
|
||||
const placeholderRecipients = Array.from(uniqueRecipientRefs.entries(), ([recipientIndex, name]) => ({
|
||||
envelopeId: envelope.id,
|
||||
email: `recipient.${recipientIndex}@documenso.com`,
|
||||
name,
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: recipientIndex,
|
||||
token: nanoid(),
|
||||
sendStatus: SendStatus.NOT_SENT,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
})).filter((recipient) => !existingRecipientEmails.has(recipient.email.toLowerCase()));
|
||||
|
||||
if (placeholderRecipients.length > 0) {
|
||||
await tx.recipient.createMany({
|
||||
@@ -495,9 +486,7 @@ export const createEnvelope = async ({
|
||||
}
|
||||
|
||||
for (const item of itemsWithPlaceholders) {
|
||||
const envelopeItem = envelope.envelopeItems.find(
|
||||
(ei) => ei.documentDataId === item.documentDataId,
|
||||
);
|
||||
const envelopeItem = envelope.envelopeItems.find((ei) => ei.documentDataId === item.documentDataId);
|
||||
|
||||
if (!envelopeItem) {
|
||||
continue;
|
||||
@@ -572,7 +561,7 @@ export const createEnvelope = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// Only create audit logs and webhook events for documents.
|
||||
// Only create audit logs for documents.
|
||||
if (type === EnvelopeType.DOCUMENT) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
@@ -609,17 +598,28 @@ export const createEnvelope = async ({
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
return createdEnvelope;
|
||||
});
|
||||
|
||||
// Trigger webhook outside the transaction to avoid holding the connection
|
||||
// open during network I/O.
|
||||
if (type === EnvelopeType.DOCUMENT) {
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
} else if (type === EnvelopeType.TEMPLATE) {
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.TEMPLATE_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
return createdEnvelope;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSource, EnvelopeType, WebhookTriggerEvents } from '@prisma/client';
|
||||
import pMap from 'p-map';
|
||||
import { omit } from 'remeda';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import { nanoid, prefixedId } from '../../universal/id';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
@@ -18,9 +15,16 @@ export interface DuplicateEnvelopeOptions {
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
overrides?: {
|
||||
duplicateAsTemplate?: boolean;
|
||||
includeRecipients?: boolean;
|
||||
includeFields?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelopeOptions) => {
|
||||
export const duplicateEnvelope = async ({ id, userId, teamId, overrides }: DuplicateEnvelopeOptions) => {
|
||||
const { duplicateAsTemplate = false, includeRecipients = true, includeFields = true } = overrides ?? {};
|
||||
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: null,
|
||||
@@ -35,6 +39,9 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
|
||||
title: true,
|
||||
userId: true,
|
||||
internalVersion: true,
|
||||
templateType: true,
|
||||
publicTitle: true,
|
||||
publicDescription: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: {
|
||||
@@ -68,29 +75,42 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
|
||||
});
|
||||
}
|
||||
|
||||
const { legacyNumberId, secondaryId } =
|
||||
envelope.type === EnvelopeType.DOCUMENT
|
||||
? await incrementDocumentId().then(({ documentId, formattedDocumentId }) => ({
|
||||
if (duplicateAsTemplate && envelope.type !== EnvelopeType.DOCUMENT) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Only documents can be saved as templates',
|
||||
});
|
||||
}
|
||||
|
||||
const targetType = duplicateAsTemplate ? EnvelopeType.TEMPLATE : envelope.type;
|
||||
|
||||
const [{ legacyNumberId, secondaryId }, createdDocumentMeta] = await Promise.all([
|
||||
targetType === EnvelopeType.DOCUMENT
|
||||
? incrementDocumentId().then(({ documentId, formattedDocumentId }) => ({
|
||||
legacyNumberId: documentId,
|
||||
secondaryId: formattedDocumentId,
|
||||
}))
|
||||
: await incrementTemplateId().then(({ templateId, formattedTemplateId }) => ({
|
||||
: incrementTemplateId().then(({ templateId, formattedTemplateId }) => ({
|
||||
legacyNumberId: templateId,
|
||||
secondaryId: formattedTemplateId,
|
||||
}));
|
||||
})),
|
||||
prisma.documentMeta.create({
|
||||
data: {
|
||||
...omit(envelope.documentMeta, ['id']),
|
||||
emailSettings: envelope.documentMeta.emailSettings || undefined,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const createdDocumentMeta = await prisma.documentMeta.create({
|
||||
data: {
|
||||
...omit(envelope.documentMeta, ['id']),
|
||||
emailSettings: envelope.documentMeta.emailSettings || undefined,
|
||||
},
|
||||
});
|
||||
const duplicatedTemplateType =
|
||||
envelope.templateType === 'ORGANISATION' && envelope.teamId !== teamId
|
||||
? 'PRIVATE'
|
||||
: (envelope.templateType ?? undefined);
|
||||
|
||||
const duplicatedEnvelope = await prisma.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId,
|
||||
type: envelope.type,
|
||||
type: targetType,
|
||||
internalVersion: envelope.internalVersion,
|
||||
userId,
|
||||
teamId,
|
||||
@@ -98,8 +118,10 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
|
||||
documentMetaId: createdDocumentMeta.id,
|
||||
authOptions: envelope.authOptions || undefined,
|
||||
visibility: envelope.visibility,
|
||||
source:
|
||||
envelope.type === EnvelopeType.DOCUMENT ? DocumentSource.DOCUMENT : DocumentSource.TEMPLATE,
|
||||
templateType: duplicatedTemplateType,
|
||||
publicTitle: envelope.publicTitle ?? undefined,
|
||||
publicDescription: envelope.publicDescription ?? undefined,
|
||||
source: targetType === EnvelopeType.DOCUMENT ? DocumentSource.DOCUMENT : DocumentSource.TEMPLATE,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
@@ -136,34 +158,41 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
|
||||
}),
|
||||
);
|
||||
|
||||
for (const recipient of envelope.recipients) {
|
||||
await prisma.recipient.create({
|
||||
data: {
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
fields: {
|
||||
createMany: {
|
||||
data: recipient.fields.map((field) => ({
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
|
||||
})),
|
||||
if (includeRecipients) {
|
||||
await pMap(
|
||||
envelope.recipients,
|
||||
async (recipient) =>
|
||||
prisma.recipient.create({
|
||||
data: {
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
fields: includeFields
|
||||
? {
|
||||
createMany: {
|
||||
data: recipient.fields.map((field) => ({
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
|
||||
})),
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
{ concurrency: 5 },
|
||||
);
|
||||
}
|
||||
|
||||
if (duplicatedEnvelope.type === EnvelopeType.DOCUMENT) {
|
||||
@@ -189,7 +218,7 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
|
||||
id: duplicatedEnvelope.id,
|
||||
envelope: duplicatedEnvelope,
|
||||
legacyId: {
|
||||
type: envelope.type,
|
||||
type: duplicatedEnvelope.type,
|
||||
id: legacyNumberId,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import type {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
Envelope,
|
||||
EnvelopeType,
|
||||
Prisma,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { kyselyPrisma, prisma, sql } from '@documenso/prisma';
|
||||
import type { DB } from '@documenso/prisma/generated/types';
|
||||
import type { DocumentSource, DocumentStatus, Envelope, EnvelopeType } from '@prisma/client';
|
||||
import type { Expression, ExpressionBuilder, SelectQueryBuilder, SqlBool } from 'kysely';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
@@ -28,8 +23,77 @@ export type FindEnvelopesOptions = {
|
||||
};
|
||||
query?: string;
|
||||
folderId?: string;
|
||||
/**
|
||||
* When true (default), use a windowed count that caps early for faster pagination.
|
||||
* When false, use a full COUNT(*) for exact totals — preferred for external API consumers.
|
||||
*/
|
||||
useWindowedCount?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The number of pages ahead of the current page we'll scan for pagination.
|
||||
*
|
||||
* Instead of COUNT(*) over the entire result set (which must scan all qualifying rows),
|
||||
* we fetch at most `offset + COUNT_WINDOW_SIZE * perPage + 1` IDs. This lets Postgres
|
||||
* stop early once it has enough rows.
|
||||
*/
|
||||
const COUNT_WINDOW_SIZE = 100;
|
||||
|
||||
/**
|
||||
* Cap for the recipient search subquery. When searching by recipient email/name,
|
||||
* we pre-compute matching envelope IDs up to this limit to prevent pathological
|
||||
* heap scans on broad searches.
|
||||
*/
|
||||
const RECIPIENT_SEARCH_CAP = 1000;
|
||||
|
||||
// Kysely query builder type for Envelope queries.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type EnvelopeQueryBuilder = SelectQueryBuilder<DB, 'Envelope', any>;
|
||||
|
||||
// Expression builder type scoped to Envelope table context.
|
||||
type EnvelopeExpressionBuilder = ExpressionBuilder<DB, 'Envelope'>;
|
||||
type RecipientExpressionBuilder = ExpressionBuilder<DB, 'Recipient'>;
|
||||
|
||||
/**
|
||||
* Reusable EXISTS subquery: checks that a Recipient row exists for the given
|
||||
* envelope with the given email, plus optional extra conditions.
|
||||
*/
|
||||
const recipientExists = (
|
||||
eb: EnvelopeExpressionBuilder,
|
||||
email: string,
|
||||
extra?: (qb: RecipientExpressionBuilder) => Expression<SqlBool>,
|
||||
) => {
|
||||
let sub = eb
|
||||
.selectFrom('Recipient')
|
||||
.whereRef('Recipient.envelopeId', '=', 'Envelope.id')
|
||||
.where('Recipient.email', '=', email);
|
||||
|
||||
if (extra) {
|
||||
sub = sub.where(extra);
|
||||
}
|
||||
|
||||
return eb.exists(sub.select(sql.lit(1).as('one')));
|
||||
};
|
||||
|
||||
/**
|
||||
* Reusable EXISTS subquery: checks that the envelope's sender (User) has the given email.
|
||||
*/
|
||||
const senderEmailIs = (eb: EnvelopeExpressionBuilder, email: string) =>
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('User')
|
||||
.whereRef('User.id', '=', 'Envelope.userId')
|
||||
.where('User.email', '=', email)
|
||||
.select(sql.lit(1).as('one')),
|
||||
);
|
||||
|
||||
/**
|
||||
* Find envelopes visible to the requesting user within a team.
|
||||
*
|
||||
* Unlike `findDocuments` (used by the UI), being a recipient does NOT override
|
||||
* document visibility. A user will only see an envelope if its visibility level
|
||||
* is within their role's threshold, or they are the document owner.
|
||||
*/
|
||||
export const findEnvelopes = async ({
|
||||
userId,
|
||||
teamId,
|
||||
@@ -42,133 +106,166 @@ export const findEnvelopes = async ({
|
||||
orderBy,
|
||||
query = '',
|
||||
folderId,
|
||||
useWindowedCount = true,
|
||||
}: FindEnvelopesOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
where: { id: userId },
|
||||
select: { id: true, email: true, name: true },
|
||||
});
|
||||
|
||||
const team = await getTeamById({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
const searchQuery = query.trim();
|
||||
const hasSearch = searchQuery.length > 0;
|
||||
const searchPattern = `%${searchQuery}%`;
|
||||
|
||||
const searchFilter: Prisma.EnvelopeWhereInput = query
|
||||
? {
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ externalId: { contains: query, mode: 'insensitive' } },
|
||||
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||
],
|
||||
}
|
||||
: {};
|
||||
const teamEmail = team.teamEmail?.email ?? null;
|
||||
const allowedVisibilities = TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole];
|
||||
|
||||
const visibilityFilter: Prisma.EnvelopeWhereInput = {
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
};
|
||||
// ─── Build Kysely query ──────────────────────────────────────────────
|
||||
|
||||
const teamEmailFilters: Prisma.EnvelopeWhereInput[] = [];
|
||||
let qb: EnvelopeQueryBuilder = kyselyPrisma.$kysely
|
||||
.selectFrom('Envelope')
|
||||
.select(['Envelope.id', 'Envelope.createdAt']);
|
||||
|
||||
if (team.teamEmail) {
|
||||
teamEmailFilters.push(
|
||||
{
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Folder filter
|
||||
qb =
|
||||
folderId !== undefined ? qb.where('Envelope.folderId', '=', folderId) : qb.where('Envelope.folderId', 'is', null);
|
||||
|
||||
// Exclude soft-deleted envelopes
|
||||
qb = qb.where('Envelope.deletedAt', 'is', null);
|
||||
|
||||
// Type filter (enum cast)
|
||||
if (type) {
|
||||
qb = qb.where('Envelope.type', '=', sql.lit(type));
|
||||
}
|
||||
|
||||
// Template filter
|
||||
if (templateId) {
|
||||
qb = qb.where('Envelope.templateId', '=', templateId);
|
||||
}
|
||||
|
||||
// Source filter (enum cast)
|
||||
if (source) {
|
||||
qb = qb.where('Envelope.source', '=', sql.lit(source));
|
||||
}
|
||||
|
||||
// Status filter (enum cast)
|
||||
if (status) {
|
||||
qb = qb.where('Envelope.status', '=', sql.lit(status));
|
||||
}
|
||||
|
||||
// Search filter: title, externalId, or recipient match via capped subquery
|
||||
if (hasSearch) {
|
||||
qb = qb.where(({ or, eb }) =>
|
||||
or([
|
||||
eb('Envelope.title', 'ilike', searchPattern),
|
||||
eb('Envelope.externalId', 'ilike', searchPattern),
|
||||
eb(
|
||||
'Envelope.id',
|
||||
'in',
|
||||
eb
|
||||
.selectFrom('Recipient')
|
||||
.select('Recipient.envelopeId')
|
||||
.where(({ or: innerOr, eb: innerEb }) =>
|
||||
innerOr([
|
||||
innerEb('Recipient.email', 'ilike', searchPattern),
|
||||
innerEb('Recipient.name', 'ilike', searchPattern),
|
||||
]),
|
||||
)
|
||||
.distinct()
|
||||
.limit(RECIPIENT_SEARCH_CAP),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.EnvelopeWhereInput = {
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
teamId: team.id,
|
||||
...visibilityFilter,
|
||||
},
|
||||
{
|
||||
userId,
|
||||
},
|
||||
...teamEmailFilters,
|
||||
],
|
||||
},
|
||||
{
|
||||
folderId: folderId ?? null,
|
||||
deletedAt: null,
|
||||
},
|
||||
searchFilter,
|
||||
],
|
||||
};
|
||||
// ─── Access control ──────────────────────────────────────────────────
|
||||
//
|
||||
// An envelope is visible if ANY of:
|
||||
// 1. It belongs to this team AND (meets the visibility threshold OR the requesting user is the owner)
|
||||
// 2. (If team email) The sender's email matches the team email
|
||||
// 3. (If team email) A recipient's email matches the team email
|
||||
|
||||
if (type) {
|
||||
whereClause.type = type;
|
||||
const visibilityFilter = (eb: EnvelopeExpressionBuilder) =>
|
||||
eb.or([
|
||||
eb(
|
||||
'Envelope.visibility',
|
||||
'in',
|
||||
allowedVisibilities.map((v) => sql.lit(v)),
|
||||
),
|
||||
// Owner always sees their own docs within this team
|
||||
eb('Envelope.userId', '=', user.id),
|
||||
]);
|
||||
|
||||
qb = qb.where((eb) => {
|
||||
const accessBranches: Expression<SqlBool>[] = [
|
||||
// Team docs that pass visibility (or are owned by the user)
|
||||
eb.and([eb('Envelope.teamId', '=', team.id), visibilityFilter(eb)]),
|
||||
];
|
||||
|
||||
if (teamEmail) {
|
||||
// Docs sent by the team email user
|
||||
accessBranches.push(senderEmailIs(eb, teamEmail));
|
||||
// Docs received by the team email
|
||||
accessBranches.push(recipientExists(eb, teamEmail));
|
||||
}
|
||||
|
||||
return eb.or(accessBranches);
|
||||
});
|
||||
|
||||
// ─── Execute: paginated data + count ──────────────────────────────────
|
||||
|
||||
const offset = Math.max(page - 1, 0) * perPage;
|
||||
|
||||
const dataQuery = qb.orderBy(`Envelope.${orderByColumn}`, orderByDirection).limit(perPage).offset(offset);
|
||||
|
||||
// Count query: either windowed (fast, capped) or full (exact, for API consumers).
|
||||
const baseCountQuery = qb.clearSelect().select('Envelope.id');
|
||||
|
||||
const countQuery = useWindowedCount
|
||||
? kyselyPrisma.$kysely
|
||||
.selectFrom(baseCountQuery.limit(offset + COUNT_WINDOW_SIZE * perPage + 1).as('windowed'))
|
||||
.select(({ fn }) => fn.count<number>('id').as('total'))
|
||||
: kyselyPrisma.$kysely
|
||||
.selectFrom(baseCountQuery.as('filtered'))
|
||||
.select(({ fn }) => fn.count<number>('id').as('total'));
|
||||
|
||||
const [dataResult, countResult] = await Promise.all([dataQuery.execute(), countQuery.executeTakeFirstOrThrow()]);
|
||||
|
||||
const ids = dataResult.map((row) => row.id);
|
||||
|
||||
const totalCount = useWindowedCount
|
||||
? Math.min(Number(countResult.total ?? 0), offset + COUNT_WINDOW_SIZE * perPage)
|
||||
: Number(countResult.total ?? 0);
|
||||
|
||||
// ─── Hydrate with Prisma ─────────────────────────────────────────────
|
||||
|
||||
if (ids.length === 0) {
|
||||
return {
|
||||
data: [],
|
||||
count: totalCount,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(totalCount / perPage),
|
||||
} satisfies FindResultResponse<never[]>;
|
||||
}
|
||||
|
||||
if (templateId) {
|
||||
whereClause.templateId = templateId;
|
||||
}
|
||||
const data = await prisma.envelope.findMany({
|
||||
where: { id: { in: ids } },
|
||||
orderBy: { [orderByColumn]: orderByDirection },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
recipients: { orderBy: { id: 'asc' } },
|
||||
team: { select: { id: true, url: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (source) {
|
||||
whereClause.source = source;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause.status = status;
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.envelope.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
[orderByColumn]: orderByDirection,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
// Preserve ordering from the Kysely query
|
||||
const idOrder = new Map(ids.map((id, index) => [id, index]));
|
||||
data.sort((a, b) => (idOrder.get(a.id) ?? 0) - (idOrder.get(b.id) ?? 0));
|
||||
|
||||
const maskedData = data.map((envelope) =>
|
||||
maskRecipientTokensForDocument({
|
||||
@@ -189,9 +286,9 @@ export const findEnvelopes = async ({
|
||||
|
||||
return {
|
||||
data: mappedData,
|
||||
count,
|
||||
count: totalCount,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
totalPages: Math.ceil(totalCount / perPage),
|
||||
} satisfies FindResultResponse<typeof mappedData>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
|
||||
export type GetEditorEnvelopeByIdOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
|
||||
/**
|
||||
* The validated team ID.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The unvalidated team ID.
|
||||
*/
|
||||
teamId: number;
|
||||
|
||||
/**
|
||||
* The type of envelope to get.
|
||||
*
|
||||
* Set to null to bypass check.
|
||||
*/
|
||||
type: EnvelopeType | null;
|
||||
};
|
||||
|
||||
export const getEditorEnvelopeById = async ({ id, userId, teamId, type }: GetEditorEnvelopeByIdOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
folder: true,
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
organisationId: true,
|
||||
},
|
||||
},
|
||||
directLink: {
|
||||
select: {
|
||||
directTemplateRecipientId: true,
|
||||
enabled: true,
|
||||
id: true,
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
envelopeAttachments: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
label: true,
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope could not be found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...envelope,
|
||||
attachments: envelope.envelopeAttachments,
|
||||
user: {
|
||||
id: envelope.user.id,
|
||||
name: envelope.user.name || '',
|
||||
email: envelope.user.email,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import type { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { EnvelopeType, Prisma } from '@prisma/client';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
@@ -127,12 +125,7 @@ export type GetEnvelopeWhereInputOptions = {
|
||||
*
|
||||
* NOTE: Be extremely careful when modifying this function. Needs at minimum two reviewers to approve any changes.
|
||||
*/
|
||||
export const getEnvelopeWhereInput = async ({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
}: GetEnvelopeWhereInputOptions) => {
|
||||
export const getEnvelopeWhereInput = async ({ id, userId, teamId, type }: GetEnvelopeWhereInputOptions) => {
|
||||
// Backup validation incase something goes wrong.
|
||||
if (!id.id || !userId || !teamId || type === undefined) {
|
||||
console.error(`[CRTICAL ERROR]: MUST NEVER HAPPEN`);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
@@ -84,9 +83,7 @@ export const getEnvelopeForDirectTemplateSigning = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const recipient = (envelope?.recipients || []).find(
|
||||
(r) => r.id === envelope?.directLink?.directTemplateRecipientId,
|
||||
);
|
||||
const recipient = (envelope?.recipients || []).find((r) => r.id === envelope?.directLink?.directTemplateRecipientId);
|
||||
|
||||
if (!envelope || !recipient) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@@ -146,7 +143,7 @@ export const getEnvelopeForDirectTemplateSigning = async ({
|
||||
...recipient,
|
||||
directToken: envelope.directLink?.token || '',
|
||||
fields: recipient.fields.map((field) => {
|
||||
const autoInsertValue = extractFieldAutoInsertValues(field);
|
||||
const autoInsertValue = extractFieldAutoInsertValues(field, recipient);
|
||||
|
||||
if (!autoInsertValue) {
|
||||
return field;
|
||||
@@ -163,6 +160,7 @@ export const getEnvelopeForDirectTemplateSigning = async ({
|
||||
isRecipientsTurn: true,
|
||||
isCompleted: false,
|
||||
isRejected: false,
|
||||
isExpired: false,
|
||||
sender,
|
||||
settings: {
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { DocumentSigningOrder, DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import DocumentMetaSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
@@ -8,11 +5,14 @@ import EnvelopeSchema from '@documenso/prisma/generated/zod/modelSchema/Envelope
|
||||
import SignatureSchema from '@documenso/prisma/generated/zod/modelSchema/SignatureSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
import { DocumentSigningOrder, DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAuthMethods } from '../../types/document-auth';
|
||||
import { ZEnvelopeFieldSchema, ZFieldSchema } from '../../types/field';
|
||||
import { ZRecipientLiteSchema } from '../../types/recipient';
|
||||
import { isRecipientExpired } from '../../utils/recipients';
|
||||
import { isRecipientAuthorized } from '../document/is-recipient-authorized';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
|
||||
@@ -56,7 +56,9 @@ export const ZEnvelopeForSigningResponse = z.object({
|
||||
email: true,
|
||||
name: true,
|
||||
documentDeletedAt: true,
|
||||
expired: true,
|
||||
expired: true, //!: deprecated Not in use. To be removed in a future migration.
|
||||
expiresAt: true,
|
||||
expirationNotifiedAt: true,
|
||||
signedAt: true,
|
||||
authOptions: true,
|
||||
signingOrder: true,
|
||||
@@ -77,6 +79,7 @@ export const ZEnvelopeForSigningResponse = z.object({
|
||||
id: true,
|
||||
title: true,
|
||||
order: true,
|
||||
documentDataId: true,
|
||||
}).array(),
|
||||
|
||||
team: TeamSchema.pick({
|
||||
@@ -102,7 +105,8 @@ export const ZEnvelopeForSigningResponse = z.object({
|
||||
email: true,
|
||||
name: true,
|
||||
documentDeletedAt: true,
|
||||
expired: true,
|
||||
expiresAt: true,
|
||||
expirationNotifiedAt: true,
|
||||
signedAt: true,
|
||||
authOptions: true,
|
||||
token: true,
|
||||
@@ -126,6 +130,7 @@ export const ZEnvelopeForSigningResponse = z.object({
|
||||
|
||||
isCompleted: z.boolean(),
|
||||
isRejected: z.boolean(),
|
||||
isExpired: z.boolean(),
|
||||
isRecipientsTurn: z.boolean(),
|
||||
|
||||
sender: z.object({
|
||||
@@ -258,10 +263,7 @@ export const getEnvelopeForRecipientSigning = async ({
|
||||
|
||||
const currentRecipientIndex = envelope.recipients.findIndex((r) => r.token === token);
|
||||
|
||||
if (
|
||||
envelope.documentMeta.signingOrder === DocumentSigningOrder.SEQUENTIAL &&
|
||||
currentRecipientIndex !== -1
|
||||
) {
|
||||
if (envelope.documentMeta.signingOrder === DocumentSigningOrder.SEQUENTIAL && currentRecipientIndex !== -1) {
|
||||
for (let i = 0; i < currentRecipientIndex; i++) {
|
||||
if (envelope.recipients[i].signingStatus !== SigningStatus.SIGNED) {
|
||||
isRecipientsTurn = false;
|
||||
@@ -285,12 +287,9 @@ export const getEnvelopeForRecipientSigning = async ({
|
||||
recipient,
|
||||
recipientSignature,
|
||||
isRecipientsTurn,
|
||||
isCompleted:
|
||||
recipient.signingStatus === SigningStatus.SIGNED ||
|
||||
envelope.status === DocumentStatus.COMPLETED,
|
||||
isRejected:
|
||||
recipient.signingStatus === SigningStatus.REJECTED ||
|
||||
envelope.status === DocumentStatus.REJECTED,
|
||||
isCompleted: recipient.signingStatus === SigningStatus.SIGNED || envelope.status === DocumentStatus.COMPLETED,
|
||||
isRejected: recipient.signingStatus === SigningStatus.REJECTED || envelope.status === DocumentStatus.REJECTED,
|
||||
isExpired: isRecipientExpired(recipient),
|
||||
sender,
|
||||
settings: {
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { EnvelopeType, Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { EnvelopeType, Prisma } from '@prisma/client';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
@@ -42,12 +41,7 @@ export type GetEnvelopesByIdsOptions = {
|
||||
*
|
||||
* NOTE: Be extremely careful when modifying this function. Needs at minimum two reviewers to approve any changes.
|
||||
*/
|
||||
export const getEnvelopesByIds = async ({
|
||||
ids,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
}: GetEnvelopesByIdsOptions) => {
|
||||
export const getEnvelopesByIds = async ({ ids, userId, teamId, type }: GetEnvelopesByIdsOptions) => {
|
||||
const { envelopeWhereInput } = await getMultipleEnvelopeWhereInput({
|
||||
ids,
|
||||
userId,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { deletedAccountServiceAccount } from '../user/service-accounts/deleted-account';
|
||||
|
||||
|
||||
@@ -5,10 +5,7 @@ export type TransferTeamEnvelopesOptions = {
|
||||
targetTeamId: number;
|
||||
};
|
||||
|
||||
export const transferTeamEnvelopes = async ({
|
||||
sourceTeamId,
|
||||
targetTeamId,
|
||||
}: TransferTeamEnvelopesOptions) => {
|
||||
export const transferTeamEnvelopes = async ({ sourceTeamId, targetTeamId }: TransferTeamEnvelopesOptions) => {
|
||||
await prisma.envelope.updateMany({
|
||||
where: {
|
||||
teamId: sourceTeamId,
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import type { DocumentMeta, DocumentVisibility, Prisma, TemplateType } from '@prisma/client';
|
||||
import { EnvelopeType, FolderType } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentMeta, DocumentVisibility, Prisma, TemplateType } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType, FolderType, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
|
||||
import { recomputeNextReminderForEnvelope } from '../recipient/update-recipient-next-reminder';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getEnvelopeWhereInput } from './get-envelope-by-id';
|
||||
|
||||
export type UpdateEnvelopeOptions = {
|
||||
@@ -75,10 +76,7 @@ export const updateEnvelope = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
envelope.type !== EnvelopeType.TEMPLATE &&
|
||||
(data.publicTitle || data.publicDescription || data.templateType)
|
||||
) {
|
||||
if (envelope.type !== EnvelopeType.TEMPLATE && (data.publicTitle || data.publicDescription || data.templateType)) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'You cannot update the template fields for document type envelopes',
|
||||
});
|
||||
@@ -92,11 +90,7 @@ export const updateEnvelope = async ({
|
||||
const isEnvelopeOwner = envelope.userId === userId;
|
||||
|
||||
// Validate whether the new visibility setting is allowed for the current user.
|
||||
if (
|
||||
!isEnvelopeOwner &&
|
||||
data?.visibility &&
|
||||
!canAccessTeamDocument(team.currentTeamRole, data.visibility)
|
||||
) {
|
||||
if (!isEnvelopeOwner && data?.visibility && !canAccessTeamDocument(team.currentTeamRole, data.visibility)) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the envelope visibility',
|
||||
});
|
||||
@@ -110,10 +104,8 @@ export const updateEnvelope = async ({
|
||||
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
||||
|
||||
// If the new global auth values aren't passed in, fallback to the current document values.
|
||||
const newGlobalAccessAuth =
|
||||
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||
const newGlobalActionAuth =
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
const newGlobalAccessAuth = data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||
const newGlobalActionAuth = data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth.length > 0 && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
@@ -145,7 +137,7 @@ export const updateEnvelope = async ({
|
||||
}
|
||||
}
|
||||
|
||||
let folderUpdateQuery: Prisma.FolderUpdateOneWithoutEnvelopesNestedInput | undefined = undefined;
|
||||
let folderUpdateQuery: Prisma.FolderUpdateOneWithoutEnvelopesNestedInput | undefined;
|
||||
|
||||
// Validate folder ID.
|
||||
if (data.folderId) {
|
||||
@@ -186,26 +178,21 @@ export const updateEnvelope = async ({
|
||||
const isTitleSame = data.title === undefined || data.title === envelope.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === envelope.externalId;
|
||||
const isGlobalAccessSame =
|
||||
documentGlobalAccessAuth === undefined ||
|
||||
isDeepEqual(documentGlobalAccessAuth, newGlobalAccessAuth);
|
||||
documentGlobalAccessAuth === undefined || isDeepEqual(documentGlobalAccessAuth, newGlobalAccessAuth);
|
||||
const isGlobalActionSame =
|
||||
documentGlobalActionAuth === undefined ||
|
||||
isDeepEqual(documentGlobalActionAuth, newGlobalActionAuth);
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === envelope.visibility;
|
||||
documentGlobalActionAuth === undefined || isDeepEqual(documentGlobalActionAuth, newGlobalActionAuth);
|
||||
const isDocumentVisibilitySame = data.visibility === undefined || data.visibility === envelope.visibility;
|
||||
const isFolderSame = data.folderId === undefined || data.folderId === envelope.folderId;
|
||||
const isTemplateTypeSame =
|
||||
data.templateType === undefined || data.templateType === envelope.templateType;
|
||||
const isTemplateTypeSame = data.templateType === undefined || data.templateType === envelope.templateType;
|
||||
const isPublicDescriptionSame =
|
||||
data.publicDescription === undefined || data.publicDescription === envelope.publicDescription;
|
||||
const isPublicTitleSame =
|
||||
data.publicTitle === undefined || data.publicTitle === envelope.publicTitle;
|
||||
const isPublicTitleSame = data.publicTitle === undefined || data.publicTitle === envelope.publicTitle;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
if (!isTitleSame && envelope.status !== DocumentStatus.DRAFT) {
|
||||
if (!isTitleSame && envelope.status !== DocumentStatus.DRAFT && envelope.status !== DocumentStatus.PENDING) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'You cannot update the title if the envelope has been sent',
|
||||
message: 'Envelope title can only be updated while in draft or pending status',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -309,8 +296,8 @@ export const updateEnvelope = async ({
|
||||
// return envelope;
|
||||
// }
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const updatedEnvelope = await tx.envelope.update({
|
||||
const updatedEnvelope = await prisma.$transaction(async (tx) => {
|
||||
const result = await tx.envelope.update({
|
||||
where: {
|
||||
id: envelope.id,
|
||||
},
|
||||
@@ -331,6 +318,10 @@ export const updateEnvelope = async ({
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (envelope.type === EnvelopeType.DOCUMENT) {
|
||||
@@ -339,6 +330,25 @@ export const updateEnvelope = async ({
|
||||
});
|
||||
}
|
||||
|
||||
return updatedEnvelope;
|
||||
return result;
|
||||
});
|
||||
|
||||
// Recompute reminders for active recipients when reminder settings change.
|
||||
if (meta && 'reminderSettings' in meta) {
|
||||
await recomputeNextReminderForEnvelope(envelope.id);
|
||||
}
|
||||
|
||||
if (envelope.type === EnvelopeType.TEMPLATE) {
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.TEMPLATE_UPDATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
// deconstruct to remove the recipients and documentMeta from the returned object since they aren't needed and can be large.
|
||||
const { recipients: _recipients, documentMeta: _documentMeta, ...finalEnvelope } = updatedEnvelope;
|
||||
|
||||
return finalEnvelope;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user