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,6 +1,6 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/field-router/schema';
|
||||
import type { Field, Signature } from '@prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
@@ -18,23 +18,14 @@ import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentCreatedFromDirectTemplateEmailTemplate } from '@documenso/email/templates/document-created-from-direct-template';
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/field-router/schema';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE, RECIPIENT_DIFF_TYPE } from '../../types/document-audit-logs';
|
||||
import type { TRecipientActionAuthTypes } from '../../types/document-auth';
|
||||
import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { ZFieldMetaSchema } from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
@@ -48,12 +39,10 @@ import {
|
||||
extractDocumentAuthMethods,
|
||||
} from '../../utils/document-auth';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
import { sendDocument } from '../document/send-document';
|
||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { incrementDocumentId } from '../envelope/increment-id';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentFromDirectTemplateOptions = {
|
||||
@@ -87,9 +76,7 @@ export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({
|
||||
recipientId: z.number(),
|
||||
});
|
||||
|
||||
export type TCreateDocumentFromDirectTemplateResponse = z.infer<
|
||||
typeof ZCreateDocumentFromDirectTemplateResponseSchema
|
||||
>;
|
||||
export type TCreateDocumentFromDirectTemplateResponse = z.infer<typeof ZCreateDocumentFromDirectTemplateResponseSchema>;
|
||||
|
||||
export const createDocumentFromDirectTemplate = async ({
|
||||
directRecipientName: initialDirectRecipientName,
|
||||
@@ -141,14 +128,11 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
directTemplateEnvelope.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'You need to enable allowDictateNextSigner and sequential signing to dictate the next signer',
|
||||
message: 'You need to enable allowDictateNextSigner and sequential signing to dictate the next signer',
|
||||
});
|
||||
}
|
||||
|
||||
const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId(
|
||||
directTemplateEnvelope.secondaryId,
|
||||
);
|
||||
const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId(directTemplateEnvelope.secondaryId);
|
||||
|
||||
if (directTemplateEnvelope.envelopeItems.length < 1) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
@@ -156,19 +140,14 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const { branding, settings, senderEmail, emailLanguage } = await getEmailContext({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: directTemplateEnvelope.teamId,
|
||||
},
|
||||
const settings = await getTeamSettings({
|
||||
userId: directTemplateEnvelope.userId,
|
||||
teamId: directTemplateEnvelope.teamId,
|
||||
});
|
||||
|
||||
const { recipients, directLink, user: templateOwner } = directTemplateEnvelope;
|
||||
const { recipients, directLink } = directTemplateEnvelope;
|
||||
|
||||
const directTemplateRecipient = recipients.find(
|
||||
(recipient) => recipient.id === directLink.directTemplateRecipientId,
|
||||
);
|
||||
const directTemplateRecipient = recipients.find((recipient) => recipient.id === directLink.directTemplateRecipientId);
|
||||
|
||||
if (!directTemplateRecipient || directTemplateRecipient.role === RecipientRole.CC) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
@@ -180,10 +159,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
|
||||
}
|
||||
|
||||
const { derivedRecipientAccessAuth, documentAuthOption: templateAuthOptions } =
|
||||
extractDocumentAuthMethods({
|
||||
documentAuth: directTemplateEnvelope.authOptions,
|
||||
});
|
||||
const { derivedRecipientAccessAuth, documentAuthOption: templateAuthOptions } = extractDocumentAuthMethods({
|
||||
documentAuth: directTemplateEnvelope.authOptions,
|
||||
});
|
||||
|
||||
let directRecipientName = user?.name || initialDirectRecipientName;
|
||||
|
||||
@@ -198,17 +176,12 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You must be logged in' });
|
||||
}
|
||||
|
||||
const directTemplateRecipientAuthOptions = ZRecipientAuthOptionsSchema.parse(
|
||||
directTemplateRecipient.authOptions,
|
||||
);
|
||||
const directTemplateRecipientAuthOptions = ZRecipientAuthOptionsSchema.parse(directTemplateRecipient.authOptions);
|
||||
|
||||
const nonDirectTemplateRecipients = directTemplateEnvelope.recipients.filter(
|
||||
(recipient) => recipient.id !== directTemplateRecipient.id,
|
||||
);
|
||||
const derivedDocumentMeta = extractDerivedDocumentMeta(
|
||||
settings,
|
||||
directTemplateEnvelope.documentMeta,
|
||||
);
|
||||
const derivedDocumentMeta = extractDerivedDocumentMeta(settings, directTemplateEnvelope.documentMeta);
|
||||
|
||||
// Associate, validate and map to a query every direct template recipient field with the provided fields.
|
||||
// Only process fields that are either required or have been signed by the user
|
||||
@@ -227,9 +200,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
const createDirectRecipientFieldArgs = await Promise.all(
|
||||
fieldsToProcess.map(async (templateField) => {
|
||||
const signedFieldValue = signedFieldValues.find(
|
||||
(value) => value.fieldId === templateField.id,
|
||||
);
|
||||
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
|
||||
|
||||
if (isRequiredField(templateField) && !signedFieldValue) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
@@ -265,8 +236,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const { value, isBase64 } = signedFieldValue;
|
||||
|
||||
const isSignatureField =
|
||||
templateField.type === FieldType.SIGNATURE ||
|
||||
templateField.type === FieldType.FREE_SIGNATURE;
|
||||
templateField.type === FieldType.SIGNATURE || templateField.type === FieldType.FREE_SIGNATURE;
|
||||
|
||||
let customText = !isSignatureField ? value : '';
|
||||
|
||||
@@ -274,9 +244,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const typedSignature = isSignatureField && !isBase64 ? value : undefined;
|
||||
|
||||
if (templateField.type === FieldType.DATE) {
|
||||
customText = DateTime.now()
|
||||
.setZone(derivedDocumentMeta.timezone)
|
||||
.toFormat(derivedDocumentMeta.dateFormat);
|
||||
customText = DateTime.now().setZone(derivedDocumentMeta.timezone).toFormat(derivedDocumentMeta.dateFormat);
|
||||
}
|
||||
|
||||
if (isSignatureField && !signatureImageAsBase64 && !typedSignature) {
|
||||
@@ -297,13 +265,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
}),
|
||||
);
|
||||
|
||||
const directTemplateNonSignatureFields = createDirectRecipientFieldArgs.filter(
|
||||
({ signature }) => signature === null,
|
||||
);
|
||||
const directTemplateNonSignatureFields = createDirectRecipientFieldArgs.filter(({ signature }) => signature === null);
|
||||
|
||||
const directTemplateSignatureFields = createDirectRecipientFieldArgs.filter(
|
||||
({ signature }) => signature !== null,
|
||||
);
|
||||
const directTemplateSignatureFields = createDirectRecipientFieldArgs.filter(({ signature }) => signature !== null);
|
||||
|
||||
const initialRequestTime = new Date();
|
||||
|
||||
@@ -318,20 +282,12 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
const titleToUse = item.title || directTemplateEnvelope.title;
|
||||
|
||||
const duplicatedFile = await putPdfFileServerSide({
|
||||
const { documentData: newDocumentData } = await putPdfFileServerSide({
|
||||
name: titleToUse,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(buffer),
|
||||
});
|
||||
|
||||
const newDocumentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: duplicatedFile.type,
|
||||
data: duplicatedFile.data,
|
||||
initialData: duplicatedFile.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
const newEnvelopeItemId = prefixedId('envelope_item');
|
||||
|
||||
oldEnvelopeItemToNewEnvelopeItemIdMap[item.id] = newEnvelopeItemId;
|
||||
@@ -391,12 +347,8 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
accessAuth: authOptions.accessAuth,
|
||||
actionAuth: authOptions.actionAuth,
|
||||
}),
|
||||
sendStatus:
|
||||
recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC
|
||||
? SigningStatus.SIGNED
|
||||
: SigningStatus.NOT_SIGNED,
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus: recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
};
|
||||
@@ -423,9 +375,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
let nonDirectRecipientFieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
|
||||
|
||||
Object.values(nonDirectTemplateRecipients).forEach((templateRecipient) => {
|
||||
const recipient = createdEnvelope.recipients.find(
|
||||
(recipient) => recipient.email === templateRecipient.email,
|
||||
);
|
||||
const recipient = createdEnvelope.recipients.find((recipient) => recipient.email === templateRecipient.email);
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found.');
|
||||
@@ -507,45 +457,43 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
// Create any direct recipient signature fields.
|
||||
// Note: It's done like this because we can't nest things in createMany.
|
||||
const createdDirectRecipientSignatureFields: CreatedDirectRecipientField[] = await Promise.all(
|
||||
directTemplateSignatureFields.map(
|
||||
async ({ templateField, signature, derivedRecipientActionAuth }) => {
|
||||
if (!signature) {
|
||||
throw new Error('Not possible.');
|
||||
}
|
||||
directTemplateSignatureFields.map(async ({ templateField, signature, derivedRecipientActionAuth }) => {
|
||||
if (!signature) {
|
||||
throw new Error('Not possible.');
|
||||
}
|
||||
|
||||
const field = await tx.field.create({
|
||||
data: {
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
||||
recipientId: createdDirectRecipient.id,
|
||||
type: templateField.type,
|
||||
page: templateField.page,
|
||||
positionX: templateField.positionX,
|
||||
positionY: templateField.positionY,
|
||||
width: templateField.width,
|
||||
height: templateField.height,
|
||||
customText: '',
|
||||
inserted: true,
|
||||
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
||||
signature: {
|
||||
create: {
|
||||
recipientId: createdDirectRecipient.id,
|
||||
signatureImageAsBase64: signature.signatureImageAsBase64,
|
||||
typedSignature: signature.typedSignature,
|
||||
},
|
||||
const field = await tx.field.create({
|
||||
data: {
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
||||
recipientId: createdDirectRecipient.id,
|
||||
type: templateField.type,
|
||||
page: templateField.page,
|
||||
positionX: templateField.positionX,
|
||||
positionY: templateField.positionY,
|
||||
width: templateField.width,
|
||||
height: templateField.height,
|
||||
customText: '',
|
||||
inserted: true,
|
||||
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
||||
signature: {
|
||||
create: {
|
||||
recipientId: createdDirectRecipient.id,
|
||||
signatureImageAsBase64: signature.signatureImageAsBase64,
|
||||
typedSignature: signature.typedSignature,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
field,
|
||||
derivedRecipientActionAuth,
|
||||
};
|
||||
},
|
||||
),
|
||||
return {
|
||||
field,
|
||||
derivedRecipientActionAuth,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const createdDirectRecipientFields: CreatedDirectRecipientField[] = [
|
||||
@@ -617,8 +565,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
field: match(field.type)
|
||||
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, (type) => ({
|
||||
type,
|
||||
data:
|
||||
field.signature?.signatureImageAsBase64 || field.signature?.typedSignature || '',
|
||||
data: field.signature?.signatureImageAsBase64 || field.signature?.typedSignature || '',
|
||||
}))
|
||||
.with(
|
||||
FieldType.DATE,
|
||||
@@ -723,6 +670,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
where: { id: nextRecipient.id },
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
sentAt: new Date(),
|
||||
...(nextSigner && documentMeta?.allowDictateNextSigner
|
||||
? {
|
||||
name: nextSigner.name,
|
||||
@@ -755,37 +703,6 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// Send email to template owner.
|
||||
const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, {
|
||||
recipientName: directRecipientEmail,
|
||||
recipientRole: directTemplateRecipient.role,
|
||||
documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(createdEnvelope.team?.url)}/${
|
||||
createdEnvelope.id
|
||||
}`,
|
||||
documentName: createdEnvelope.title,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailTemplate, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(emailTemplate, { lang: emailLanguage, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
{
|
||||
name: templateOwner.name || '',
|
||||
address: templateOwner.email,
|
||||
},
|
||||
],
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`Document created from direct template`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
return {
|
||||
createdEnvelope,
|
||||
token: createdDirectRecipient.token,
|
||||
@@ -793,6 +710,18 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
};
|
||||
});
|
||||
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(documentMeta);
|
||||
|
||||
if (emailSettings.ownerDocumentCreated) {
|
||||
await jobs.triggerJob({
|
||||
name: 'send.document.created.from.direct.template.email',
|
||||
payload: {
|
||||
envelopeId: createdEnvelope.id,
|
||||
recipientId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// This handles sending emails and sealing the document if required.
|
||||
await sendDocument({
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
@@ -13,10 +15,8 @@ import {
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
||||
import type { TEnvelopeExpirationPeriod } from '../../constants/envelope-expiration';
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { ZDefaultRecipientsSchema } from '../../types/default-recipients';
|
||||
@@ -32,16 +32,8 @@ import type {
|
||||
TRadioFieldMeta,
|
||||
TTextFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZFieldMetaSchema,
|
||||
ZRadioFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { ZCheckboxFieldMeta, ZDropdownFieldMeta, ZFieldMetaSchema, ZRadioFieldMeta } from '../../types/field-meta';
|
||||
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putNormalizedPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
@@ -60,11 +52,9 @@ import { incrementDocumentId } from '../envelope/increment-id';
|
||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getOrganisationTemplateWhereInput } from './get-organisation-template-by-id';
|
||||
|
||||
type FinalRecipient = Pick<
|
||||
Recipient,
|
||||
'name' | 'email' | 'role' | 'authOptions' | 'signingOrder' | 'token'
|
||||
> & {
|
||||
type FinalRecipient = Pick<Recipient, 'name' | 'email' | 'role' | 'authOptions' | 'signingOrder' | 'token'> & {
|
||||
templateRecipientId: number;
|
||||
fields: Field[];
|
||||
};
|
||||
@@ -119,6 +109,7 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
typedSignatureEnabled?: boolean;
|
||||
uploadSignatureEnabled?: boolean;
|
||||
drawSignatureEnabled?: boolean;
|
||||
envelopeExpirationPeriod?: TEnvelopeExpirationPeriod | null;
|
||||
};
|
||||
|
||||
formValues?: TDocumentFormValues;
|
||||
@@ -310,29 +301,43 @@ export const createDocumentFromTemplate = async ({
|
||||
attachments,
|
||||
formValues,
|
||||
}: CreateDocumentFromTemplateOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
const templateInclude = {
|
||||
recipients: {
|
||||
include: {
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
} as const;
|
||||
|
||||
const { envelopeWhereInput, team: callerTeam } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const template = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
include: {
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
const [teamTemplate, organisationTemplate] = await Promise.all([
|
||||
prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: templateInclude,
|
||||
}),
|
||||
prisma.envelope.findFirst({
|
||||
where: getOrganisationTemplateWhereInput({
|
||||
id,
|
||||
organisationId: callerTeam.organisationId,
|
||||
teamRole: callerTeam.currentTeamRole,
|
||||
}),
|
||||
include: templateInclude,
|
||||
}),
|
||||
]);
|
||||
|
||||
const template = teamTemplate ?? organisationTemplate;
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@@ -372,9 +377,7 @@ export const createDocumentFromTemplate = async ({
|
||||
|
||||
// Check that all the passed in recipient IDs can be associated with a template recipient.
|
||||
recipients.forEach((recipient) => {
|
||||
const foundRecipient = template.recipients.find(
|
||||
(templateRecipient) => templateRecipient.id === recipient.id,
|
||||
);
|
||||
const foundRecipient = template.recipients.find((templateRecipient) => templateRecipient.id === recipient.id);
|
||||
|
||||
if (!foundRecipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
@@ -513,18 +516,15 @@ export const createDocumentFromTemplate = async ({
|
||||
emailSettings: override?.emailSettings || template.documentMeta?.emailSettings,
|
||||
signingOrder: override?.signingOrder || template.documentMeta?.signingOrder,
|
||||
language: override?.language || template.documentMeta?.language || settings.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
override?.typedSignatureEnabled ?? template.documentMeta?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled:
|
||||
override?.uploadSignatureEnabled ?? template.documentMeta?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled:
|
||||
override?.drawSignatureEnabled ?? template.documentMeta?.drawSignatureEnabled,
|
||||
allowDictateNextSigner:
|
||||
override?.allowDictateNextSigner ?? template.documentMeta?.allowDictateNextSigner,
|
||||
typedSignatureEnabled: override?.typedSignatureEnabled ?? template.documentMeta?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: override?.uploadSignatureEnabled ?? template.documentMeta?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: override?.drawSignatureEnabled ?? template.documentMeta?.drawSignatureEnabled,
|
||||
allowDictateNextSigner: override?.allowDictateNextSigner ?? template.documentMeta?.allowDictateNextSigner,
|
||||
envelopeExpirationPeriod: override?.envelopeExpirationPeriod ?? template.documentMeta?.envelopeExpirationPeriod,
|
||||
}),
|
||||
});
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const { envelope, createdEnvelope } = await prisma.$transaction(async (tx) => {
|
||||
const envelope = await tx.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
@@ -537,7 +537,7 @@ export const createDocumentFromTemplate = async ({
|
||||
templateId: legacyTemplateId, // The template this envelope was created from.
|
||||
userId,
|
||||
folderId,
|
||||
teamId: template.teamId,
|
||||
teamId,
|
||||
title: finalEnvelopeTitle,
|
||||
envelopeItems: {
|
||||
createMany: {
|
||||
@@ -565,12 +565,8 @@ export const createDocumentFromTemplate = async ({
|
||||
accessAuth: authOptions.accessAuth,
|
||||
actionAuth: authOptions.actionAuth,
|
||||
}),
|
||||
sendStatus:
|
||||
recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC
|
||||
? SigningStatus.SIGNED
|
||||
: SigningStatus.NOT_SIGNED,
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus: recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: recipient.token,
|
||||
};
|
||||
@@ -595,9 +591,7 @@ export const createDocumentFromTemplate = async ({
|
||||
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId'>[] = [];
|
||||
|
||||
// Get all template field IDs first so we can validate later
|
||||
const allTemplateFieldIds = finalRecipients.flatMap((recipient) =>
|
||||
recipient.fields.map((field) => field.id),
|
||||
);
|
||||
const allTemplateFieldIds = finalRecipients.flatMap((recipient) => recipient.fields.map((field) => field.id));
|
||||
|
||||
if (prefillFields?.length) {
|
||||
// Validate that all prefill field IDs exist in the template
|
||||
@@ -757,13 +751,25 @@ export const createDocumentFromTemplate = async ({
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
return { envelope, createdEnvelope };
|
||||
});
|
||||
|
||||
// Trigger webhook outside the transaction to avoid holding the connection
|
||||
// open during network I/O.
|
||||
await Promise.allSettled([
|
||||
triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}),
|
||||
triggerWebhook({
|
||||
event: WebhookTriggerEvents.TEMPLATE_USED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
}),
|
||||
]);
|
||||
|
||||
return envelope;
|
||||
});
|
||||
return envelope;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { EnvelopeType, type Recipient } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import {
|
||||
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
DIRECT_TEMPLATE_RECIPIENT_NAME,
|
||||
} from '@documenso/lib/constants/direct-templates';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { EnvelopeType, type Recipient } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { type EnvelopeIdOptions, mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
@@ -47,18 +46,13 @@ export const createTemplateDirectLink = async ({
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, { message: 'Direct template already exists' });
|
||||
}
|
||||
|
||||
if (
|
||||
directRecipientId &&
|
||||
!envelope.recipients.find((recipient) => recipient.id === directRecipientId)
|
||||
) {
|
||||
if (directRecipientId && !envelope.recipients.find((recipient) => recipient.id === directRecipientId)) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Recipient not found' });
|
||||
}
|
||||
|
||||
if (
|
||||
!directRecipientId &&
|
||||
envelope.recipients.find(
|
||||
(recipient) => recipient.email.toLowerCase() === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
)
|
||||
envelope.recipients.find((recipient) => recipient.email.toLowerCase() === DIRECT_TEMPLATE_RECIPIENT_EMAIL)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Cannot generate placeholder direct recipient',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { generateAvaliableRecipientPlaceholder } from '@documenso/lib/utils/templates';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { EnvelopeType, WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { type EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type DeleteTemplateOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
@@ -19,7 +20,21 @@ export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptio
|
||||
teamId,
|
||||
});
|
||||
|
||||
return await prisma.envelope.delete({
|
||||
const templateToDelete = await prisma.envelope.findUniqueOrThrow({
|
||||
where: envelopeWhereInput,
|
||||
include: { documentMeta: true, recipients: true },
|
||||
});
|
||||
|
||||
const deletedTemplate = await prisma.envelope.delete({
|
||||
where: envelopeWhereInput,
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.TEMPLATE_DELETED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(templateToDelete)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return deletedTemplate;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { EnvelopeType, type Prisma, TemplateType } from '@prisma/client';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type FindOrganisationTemplatesOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
export const findOrganisationTemplates = async ({
|
||||
userId,
|
||||
teamId,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
}: FindOrganisationTemplatesOptions) => {
|
||||
const [team, { teamRole }] = await Promise.all([
|
||||
getTeamById({ teamId, userId }),
|
||||
getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const where: Prisma.EnvelopeWhereInput = {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: TemplateType.ORGANISATION,
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
},
|
||||
team: {
|
||||
organisationId: team.organisationId,
|
||||
},
|
||||
};
|
||||
|
||||
const templateInclude = {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
directLink: {
|
||||
select: {
|
||||
token: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.envelope.findMany({
|
||||
where,
|
||||
include: templateInclude,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@@ -1,10 +1,9 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TemplateType } from '@prisma/client';
|
||||
import { EnvelopeType, type Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { type FindResultResponse } from '../../types/search-params';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
|
||||
export type FindTemplatesOptions = {
|
||||
@@ -24,8 +23,6 @@ export const findTemplates = async ({
|
||||
perPage = 10,
|
||||
folderId,
|
||||
}: FindTemplatesOptions) => {
|
||||
const whereFilter: Prisma.EnvelopeWhereInput[] = [];
|
||||
|
||||
const { teamRole } = await getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
@@ -34,63 +31,55 @@ export const findTemplates = async ({
|
||||
},
|
||||
});
|
||||
|
||||
whereFilter.push(
|
||||
{ teamId },
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
const where: Prisma.EnvelopeWhereInput = {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: type,
|
||||
AND: [
|
||||
{ teamId },
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ userId, teamId },
|
||||
],
|
||||
},
|
||||
);
|
||||
{ userId, teamId },
|
||||
],
|
||||
},
|
||||
folderId ? { folderId } : { folderId: null },
|
||||
],
|
||||
};
|
||||
|
||||
if (folderId) {
|
||||
whereFilter.push({ folderId });
|
||||
} else {
|
||||
whereFilter.push({ folderId: null });
|
||||
}
|
||||
const templateInclude = {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
directLink: {
|
||||
select: {
|
||||
token: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.envelope.findMany({
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: type,
|
||||
AND: whereFilter,
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
directLink: {
|
||||
select: {
|
||||
token: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where,
|
||||
include: templateInclude,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: type,
|
||||
AND: whereFilter,
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma, TeamMemberRole } from '@prisma/client';
|
||||
import { EnvelopeType, TemplateType } from '@prisma/client';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetOrganisationTemplateByIdOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an organisation template by ID.
|
||||
*
|
||||
* This validates that the caller's team belongs to the same organisation as the template's team,
|
||||
* that the template is of type ORGANISATION, and that the template's visibility is permitted
|
||||
* for the caller's role on their own team.
|
||||
*/
|
||||
export const getOrganisationTemplateById = async ({ id, userId, teamId }: GetOrganisationTemplateByIdOptions) => {
|
||||
const [callerTeam, { teamRole }] = await Promise.all([
|
||||
getTeamById({ teamId, userId }),
|
||||
getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.TEMPLATE),
|
||||
templateType: TemplateType.ORGANISATION,
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
},
|
||||
team: {
|
||||
organisationId: callerTeam.organisationId,
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
directLink: {
|
||||
select: {
|
||||
directTemplateRecipientId: true,
|
||||
enabled: true,
|
||||
id: true,
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Organisation template not found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...envelope,
|
||||
user: {
|
||||
id: envelope.user.id,
|
||||
name: envelope.user.name || '',
|
||||
email: envelope.user.email,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a where input for querying an organisation template.
|
||||
*
|
||||
* Matches a TEMPLATE envelope with templateType ORGANISATION belonging to any team
|
||||
* within the provided organisation, respecting the caller's team role visibility.
|
||||
*/
|
||||
export const getOrganisationTemplateWhereInput = ({
|
||||
id,
|
||||
organisationId,
|
||||
teamRole,
|
||||
}: {
|
||||
id: EnvelopeIdOptions;
|
||||
organisationId: string;
|
||||
teamRole: TeamMemberRole;
|
||||
}): Prisma.EnvelopeWhereInput => {
|
||||
return {
|
||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.TEMPLATE),
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: TemplateType.ORGANISATION,
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
},
|
||||
team: {
|
||||
organisationId,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
@@ -9,9 +8,7 @@ export interface GetTemplateByDirectLinkTokenOptions {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const getTemplateByDirectLinkToken = async ({
|
||||
token,
|
||||
}: GetTemplateByDirectLinkTokenOptions) => {
|
||||
export const getTemplateByDirectLinkToken = async ({ token }: GetTemplateByDirectLinkTokenOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
@@ -90,6 +87,7 @@ export const getTemplateByDirectLinkToken = async ({
|
||||
envelopeItems: envelope.envelopeItems.map((item) => ({
|
||||
id: item.id,
|
||||
envelopeId: item.envelopeId,
|
||||
documentDataId: item.documentDataId,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { formatTemplatesPath, getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { DocumentVisibility, EnvelopeType, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { getUserTeamGroups } from '../team/get-user-team-groups';
|
||||
|
||||
export type SearchTemplatesWithKeywordOptions = {
|
||||
query: string;
|
||||
userId: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export const searchTemplatesWithKeyword = async ({ query, userId, limit = 20 }: SearchTemplatesWithKeywordOptions) => {
|
||||
if (!query.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [user, teamGroupsByTeamId] = await Promise.all([
|
||||
prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
}),
|
||||
getUserTeamGroups({ userId }),
|
||||
]);
|
||||
|
||||
const teamIds = [...teamGroupsByTeamId.keys()];
|
||||
|
||||
const titleOrRecipientMatch: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{
|
||||
recipients: {
|
||||
some: { email: { contains: query, mode: 'insensitive' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const filters: Prisma.EnvelopeWhereInput[] = [
|
||||
// Templates owned by the user matching title or recipient email.
|
||||
{
|
||||
userId,
|
||||
deletedAt: null,
|
||||
...titleOrRecipientMatch,
|
||||
},
|
||||
];
|
||||
|
||||
// Team templates the user has access to.
|
||||
if (teamIds.length > 0) {
|
||||
filters.push({
|
||||
teamId: { in: teamIds },
|
||||
deletedAt: null,
|
||||
...titleOrRecipientMatch,
|
||||
});
|
||||
}
|
||||
|
||||
const envelopes = await prisma.envelope.findMany({
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
OR: filters,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
title: true,
|
||||
secondaryId: true,
|
||||
visibility: true,
|
||||
recipients: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
distinct: ['id'],
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
// Over-fetch to compensate for post-query visibility filtering on team templates.
|
||||
take: limit * 3,
|
||||
});
|
||||
|
||||
const results = envelopes
|
||||
.filter((envelope) => {
|
||||
if (!envelope.teamId || envelope.userId === user.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const teamGroups = teamGroupsByTeamId.get(envelope.teamId) ?? [];
|
||||
const teamMemberRole = getHighestTeamRoleInGroup(teamGroups);
|
||||
|
||||
if (!teamMemberRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return match([envelope.visibility, teamMemberRole])
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true)
|
||||
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.ADMIN], () => true)
|
||||
.with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.MANAGER], () => true)
|
||||
.with([DocumentVisibility.ADMIN, TeamMemberRole.ADMIN], () => true)
|
||||
.otherwise(() => false);
|
||||
})
|
||||
.slice(0, limit)
|
||||
.map((envelope) => {
|
||||
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
|
||||
|
||||
const path = `${formatTemplatesPath(envelope.team.url)}/${legacyTemplateId}`;
|
||||
|
||||
return {
|
||||
title: envelope.title,
|
||||
path,
|
||||
value: [envelope.id, envelope.title, ...envelope.recipients.map((r) => r.email)].join(' '),
|
||||
};
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
|
||||
Reference in New Issue
Block a user