Compare commits

..

1 Commits

Author SHA1 Message Date
Catalin Pit
9d21913b90 feat: add default recipients for teams and orgs 2025-11-26 12:16:38 +02:00
17 changed files with 73 additions and 381 deletions

View File

@@ -11,6 +11,7 @@ import {
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf'; import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { nanoid, prefixedId } from '@documenso/lib/universal/id'; import { nanoid, prefixedId } from '@documenso/lib/universal/id';
@@ -306,8 +307,22 @@ export const createEnvelope = async ({
const firstEnvelopeItem = envelope.envelopeItems[0]; const firstEnvelopeItem = envelope.envelopeItems[0];
const defaultRecipients = settings.defaultRecipients
? ZDefaultRecipientsSchema.parse(settings.defaultRecipients)
: [];
const mappedDefaultRecipients: CreateEnvelopeRecipientOptions[] = defaultRecipients.map(
(recipient) => ({
email: recipient.email,
name: recipient.name,
role: recipient.role,
}),
);
const allRecipients = [...(data.recipients || []), ...mappedDefaultRecipients];
await Promise.all( await Promise.all(
(data.recipients || []).map(async (recipient) => { allRecipients.map(async (recipient) => {
const recipientAuthOptions = createRecipientAuthOptions({ const recipientAuthOptions = createRecipientAuthOptions({
accessAuth: recipient.accessAuth ?? [], accessAuth: recipient.accessAuth ?? [],
actionAuth: recipient.actionAuth ?? [], actionAuth: recipient.actionAuth ?? [],

View File

@@ -19,6 +19,7 @@ import { prisma } from '@documenso/prisma';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats'; import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
import type { SupportedLanguageCodes } from '../../constants/i18n'; import type { SupportedLanguageCodes } from '../../constants/i18n';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { ZDefaultRecipientsSchema } from '../../types/default-recipients';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth'; import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
import type { TDocumentEmailSettings } from '../../types/document-email'; import type { TDocumentEmailSettings } from '../../types/document-email';
@@ -396,6 +397,30 @@ export const createDocumentFromTemplate = async ({
}; };
}); });
const defaultRecipients = settings.defaultRecipients
? ZDefaultRecipientsSchema.parse(settings.defaultRecipients)
: [];
const defaultRecipientsFinal: FinalRecipient[] = defaultRecipients.map((recipient) => {
const authOptions = ZRecipientAuthOptionsSchema.parse({});
return {
templateRecipientId: -1,
fields: [],
name: recipient.name || recipient.email,
email: recipient.email,
role: recipient.role,
signingOrder: null,
authOptions: createRecipientAuthOptions({
accessAuth: authOptions.accessAuth,
actionAuth: authOptions.actionAuth,
}),
token: nanoid(),
};
});
const allFinalRecipients = [...finalRecipients, ...defaultRecipientsFinal];
// Key = original envelope item ID // Key = original envelope item ID
// Value = duplicated envelope item ID. // Value = duplicated envelope item ID.
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {}; const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
@@ -515,7 +540,7 @@ export const createDocumentFromTemplate = async ({
documentMetaId: documentMeta.id, documentMetaId: documentMeta.id,
recipients: { recipients: {
createMany: { createMany: {
data: finalRecipients.map((recipient) => { data: allFinalRecipients.map((recipient) => {
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient?.authOptions); const authOptions = ZRecipientAuthOptionsSchema.parse(recipient?.authOptions);
return { return {
@@ -596,7 +621,7 @@ export const createDocumentFromTemplate = async ({
} }
} }
Object.values(finalRecipients).forEach(({ token, fields }) => { Object.values(allFinalRecipients).forEach(({ token, fields }) => {
const recipient = envelope.recipients.find((recipient) => recipient.token === token); const recipient = envelope.recipients.find((recipient) => recipient.token === token);
if (!recipient) { if (!recipient) {

View File

@@ -0,0 +1,12 @@
import { RecipientRole } from '@prisma/client';
import { z } from 'zod';
export const ZDefaultRecipientSchema = z.object({
email: z.string().email(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
});
export const ZDefaultRecipientsSchema = z.array(ZDefaultRecipientSchema);
export type TDefaultRecipients = z.infer<typeof ZDefaultRecipientsSchema>;

View File

@@ -135,5 +135,7 @@ export const generateDefaultOrganisationSettings = (): Omit<
emailReplyTo: null, emailReplyTo: null,
// emailReplyToName: null, // emailReplyToName: null,
emailDocumentSettings: DEFAULT_DOCUMENT_EMAIL_SETTINGS, emailDocumentSettings: DEFAULT_DOCUMENT_EMAIL_SETTINGS,
defaultRecipients: null,
}; };
}; };

View File

@@ -202,6 +202,8 @@ export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | '
emailId: null, emailId: null,
emailReplyTo: null, emailReplyTo: null,
// emailReplyToName: null, // emailReplyToName: null,
defaultRecipients: null,
}; };
}; };

View File

@@ -797,7 +797,7 @@ enum OrganisationMemberInviteStatus {
DECLINED DECLINED
} }
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"]) /// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';", "import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';"])
model OrganisationGlobalSettings { model OrganisationGlobalSettings {
id String @id id String @id
organisation Organisation? organisation Organisation?
@@ -814,6 +814,8 @@ model OrganisationGlobalSettings {
uploadSignatureEnabled Boolean @default(true) uploadSignatureEnabled Boolean @default(true)
drawSignatureEnabled Boolean @default(true) drawSignatureEnabled Boolean @default(true)
defaultRecipients Json? /// [DefaultRecipient[]] @zod.custom.use(ZDefaultRecipientsSchema)
emailId String? emailId String?
email OrganisationEmail? @relation(fields: [emailId], references: [id]) email OrganisationEmail? @relation(fields: [emailId], references: [id])
@@ -827,7 +829,7 @@ model OrganisationGlobalSettings {
brandingCompanyDetails String @default("") brandingCompanyDetails String @default("")
} }
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"]) /// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';", "import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';"])
model TeamGlobalSettings { model TeamGlobalSettings {
id String @id id String @id
team Team? team Team?
@@ -845,6 +847,8 @@ model TeamGlobalSettings {
uploadSignatureEnabled Boolean? uploadSignatureEnabled Boolean?
drawSignatureEnabled Boolean? drawSignatureEnabled Boolean?
defaultRecipients Json? /// [DefaultRecipient[]] @zod.custom.use(ZDefaultRecipientsSchema)
emailId String? emailId String?
email OrganisationEmail? @relation(fields: [emailId], references: [id]) email OrganisationEmail? @relation(fields: [emailId], references: [id])

View File

@@ -1,74 +0,0 @@
import { EnvelopeType } from '@prisma/client';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document';
import { mapDocumentIdToSecondaryId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZGetDocumentsByIdsRequestSchema,
ZGetDocumentsByIdsResponseSchema,
getDocumentsByIdsMeta,
} from './get-documents-by-ids.types';
export const getDocumentsByIdsRoute = authenticatedProcedure
.meta(getDocumentsByIdsMeta)
.input(ZGetDocumentsByIdsRequestSchema)
.output(ZGetDocumentsByIdsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentIds } = input;
ctx.logger.info({
input: {
documentIds,
},
});
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'documentId',
id: documentIds[0],
},
userId: user.id,
teamId,
type: EnvelopeType.DOCUMENT,
});
const envelopeOrInput = envelopeWhereInput.OR!;
const secondaryIds = documentIds.map((documentId) => mapDocumentIdToSecondaryId(documentId));
const envelopes = await prisma.envelope.findMany({
where: {
type: EnvelopeType.DOCUMENT,
secondaryId: {
in: secondaryIds,
},
OR: envelopeOrInput,
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
recipients: {
orderBy: {
id: 'asc',
},
},
team: {
select: {
id: true,
url: true,
},
},
},
});
return envelopes.map((envelope) => mapEnvelopesToDocumentMany(envelope));
});

View File

@@ -1,24 +0,0 @@
import { z } from 'zod';
import { ZDocumentManySchema } from '@documenso/lib/types/document';
import type { TrpcRouteMeta } from '../trpc';
export const getDocumentsByIdsMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/document/get-many',
summary: 'Get multiple documents',
description: 'Retrieve multiple documents by their IDs',
tags: ['Document'],
},
};
export const ZGetDocumentsByIdsRequestSchema = z.object({
documentIds: z.array(z.number()).min(1),
});
export const ZGetDocumentsByIdsResponseSchema = z.array(ZDocumentManySchema);
export type TGetDocumentsByIdsRequest = z.infer<typeof ZGetDocumentsByIdsRequestSchema>;
export type TGetDocumentsByIdsResponse = z.infer<typeof ZGetDocumentsByIdsResponseSchema>;

View File

@@ -19,7 +19,6 @@ import { findDocumentsInternalRoute } from './find-documents-internal';
import { findInboxRoute } from './find-inbox'; import { findInboxRoute } from './find-inbox';
import { getDocumentRoute } from './get-document'; import { getDocumentRoute } from './get-document';
import { getDocumentByTokenRoute } from './get-document-by-token'; import { getDocumentByTokenRoute } from './get-document-by-token';
import { getDocumentsByIdsRoute } from './get-documents-by-ids';
import { getInboxCountRoute } from './get-inbox-count'; import { getInboxCountRoute } from './get-inbox-count';
import { redistributeDocumentRoute } from './redistribute-document'; import { redistributeDocumentRoute } from './redistribute-document';
import { searchDocumentRoute } from './search-document'; import { searchDocumentRoute } from './search-document';
@@ -28,7 +27,6 @@ import { updateDocumentRoute } from './update-document';
export const documentRouter = router({ export const documentRouter = router({
get: getDocumentRoute, get: getDocumentRoute,
getMany: getDocumentsByIdsRoute,
find: findDocumentsRoute, find: findDocumentsRoute,
create: createDocumentRoute, create: createDocumentRoute,
update: updateDocumentRoute, update: updateDocumentRoute,

View File

@@ -1,93 +0,0 @@
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZGetEnvelopesByIdsRequestSchema,
ZGetEnvelopesByIdsResponseSchema,
getEnvelopesByIdsMeta,
} from './get-envelopes-by-ids.types';
export const getEnvelopesByIdsRoute = authenticatedProcedure
.meta(getEnvelopesByIdsMeta)
.input(ZGetEnvelopesByIdsRequestSchema)
.output(ZGetEnvelopesByIdsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { envelopeIds } = input;
ctx.logger.info({
input: {
envelopeIds,
},
});
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: envelopeIds[0],
},
userId: user.id,
teamId,
type: null,
});
const envelopeOrInput = envelopeWhereInput.OR!;
const envelopes = await prisma.envelope.findMany({
where: {
id: {
in: envelopeIds,
},
OR: envelopeOrInput,
},
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,
},
},
},
});
return envelopes.map((envelope) => ({
...envelope,
user: {
id: envelope.user.id,
name: envelope.user.name || '',
email: envelope.user.email,
},
}));
});

View File

@@ -1,24 +0,0 @@
import { z } from 'zod';
import { ZEnvelopeSchema } from '@documenso/lib/types/envelope';
import type { TrpcRouteMeta } from '../trpc';
export const getEnvelopesByIdsMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/get-many',
summary: 'Get multiple envelopes',
description: 'Retrieve multiple envelopes by their IDs',
tags: ['Envelope'],
},
};
export const ZGetEnvelopesByIdsRequestSchema = z.object({
envelopeIds: z.array(z.string()).min(1),
});
export const ZGetEnvelopesByIdsResponseSchema = z.array(ZEnvelopeSchema);
export type TGetEnvelopesByIdsRequest = z.infer<typeof ZGetEnvelopesByIdsRequestSchema>;
export type TGetEnvelopesByIdsResponse = z.infer<typeof ZGetEnvelopesByIdsResponseSchema>;

View File

@@ -21,7 +21,6 @@ import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-enve
import { getEnvelopeRoute } from './get-envelope'; import { getEnvelopeRoute } from './get-envelope';
import { getEnvelopeItemsRoute } from './get-envelope-items'; import { getEnvelopeItemsRoute } from './get-envelope-items';
import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token'; import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token';
import { getEnvelopesByIdsRoute } from './get-envelopes-by-ids';
import { redistributeEnvelopeRoute } from './redistribute-envelope'; import { redistributeEnvelopeRoute } from './redistribute-envelope';
import { setEnvelopeFieldsRoute } from './set-envelope-fields'; import { setEnvelopeFieldsRoute } from './set-envelope-fields';
import { setEnvelopeRecipientsRoute } from './set-envelope-recipients'; import { setEnvelopeRecipientsRoute } from './set-envelope-recipients';
@@ -67,7 +66,6 @@ export const envelopeRouter = router({
sign: signEnvelopeFieldRoute, sign: signEnvelopeFieldRoute,
}, },
get: getEnvelopeRoute, get: getEnvelopeRoute,
getMany: getEnvelopesByIdsRoute,
create: createEnvelopeRoute, create: createEnvelopeRoute,
use: useEnvelopeRoute, use: useEnvelopeRoute,
update: updateEnvelopeRoute, update: updateEnvelopeRoute,

View File

@@ -51,6 +51,9 @@ export const updateTeamSettingsRoute = authenticatedProcedure
emailReplyTo, emailReplyTo,
// emailReplyToName, // emailReplyToName,
emailDocumentSettings, emailDocumentSettings,
// Default recipients settings.
defaultRecipients,
} = data; } = data;
if (Object.values(data).length === 0) { if (Object.values(data).length === 0) {
@@ -160,6 +163,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
// emailReplyToName, // emailReplyToName,
emailDocumentSettings: emailDocumentSettings:
emailDocumentSettings === null ? Prisma.DbNull : emailDocumentSettings, emailDocumentSettings === null ? Prisma.DbNull : emailDocumentSettings,
defaultRecipients: defaultRecipients === null ? Prisma.DbNull : defaultRecipients,
}, },
}, },
}, },

View File

@@ -1,6 +1,7 @@
import { z } from 'zod'; import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n'; import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { import {
ZDocumentMetaDateFormatSchema, ZDocumentMetaDateFormatSchema,
@@ -38,6 +39,9 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
emailReplyTo: z.string().email().nullish(), emailReplyTo: z.string().email().nullish(),
// emailReplyToName: z.string().nullish(), // emailReplyToName: z.string().nullish(),
emailDocumentSettings: ZDocumentEmailSettingsSchema.nullish(), emailDocumentSettings: ZDocumentEmailSettingsSchema.nullish(),
// Default recipients settings.
defaultRecipients: ZDefaultRecipientsSchema.nullish(),
}), }),
}); });

View File

@@ -1,127 +0,0 @@
import { EnvelopeType } from '@prisma/client';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import {
mapSecondaryIdToTemplateId,
mapTemplateIdToSecondaryId,
} from '@documenso/lib/utils/envelope';
import { mapFieldToLegacyField } from '@documenso/lib/utils/fields';
import { mapRecipientToLegacyRecipient } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZGetTemplatesByIdsRequestSchema,
ZGetTemplatesByIdsResponseSchema,
getTemplatesByIdsMeta,
} from './get-templates-by-ids.types';
export const getTemplatesByIdsRoute = authenticatedProcedure
.meta(getTemplatesByIdsMeta)
.input(ZGetTemplatesByIdsRequestSchema)
.output(ZGetTemplatesByIdsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { templateIds } = input;
ctx.logger.info({
input: {
templateIds,
},
});
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id: templateIds[0],
},
userId: user.id,
teamId,
type: EnvelopeType.TEMPLATE,
});
const envelopeOrInput = envelopeWhereInput.OR!;
const secondaryIds = templateIds.map((templateId) => mapTemplateIdToSecondaryId(templateId));
const envelopes = await prisma.envelope.findMany({
where: {
type: EnvelopeType.TEMPLATE,
secondaryId: {
in: secondaryIds,
},
OR: envelopeOrInput,
},
include: {
recipients: {
orderBy: {
id: 'asc',
},
},
fields: true,
team: {
select: {
id: true,
url: true,
},
},
documentMeta: {
select: {
signingOrder: true,
distributionMethod: true,
},
},
directLink: {
select: {
token: true,
enabled: true,
},
},
},
});
return envelopes.map((envelope) => {
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
return {
id: legacyTemplateId,
envelopeId: envelope.id,
type: envelope.templateType,
visibility: envelope.visibility,
externalId: envelope.externalId,
title: envelope.title,
userId: envelope.userId,
teamId: envelope.teamId,
authOptions: envelope.authOptions,
createdAt: envelope.createdAt,
updatedAt: envelope.updatedAt,
publicTitle: envelope.publicTitle,
publicDescription: envelope.publicDescription,
folderId: envelope.folderId,
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
team: envelope.team
? {
id: envelope.team.id,
url: envelope.team.url,
}
: null,
fields: envelope.fields.map((field) => mapFieldToLegacyField(field, envelope)),
recipients: envelope.recipients.map((recipient) =>
mapRecipientToLegacyRecipient(recipient, envelope),
),
templateMeta: envelope.documentMeta
? {
signingOrder: envelope.documentMeta.signingOrder,
distributionMethod: envelope.documentMeta.distributionMethod,
}
: null,
directLink: envelope.directLink
? {
token: envelope.directLink.token,
enabled: envelope.directLink.enabled,
}
: null,
templateDocumentDataId: '', // Backwards compatibility.
};
});
});

View File

@@ -1,24 +0,0 @@
import { z } from 'zod';
import { ZTemplateManySchema } from '@documenso/lib/types/template';
import type { TrpcRouteMeta } from '../trpc';
export const getTemplatesByIdsMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/template/get-many',
summary: 'Get multiple templates',
description: 'Retrieve multiple templates by their IDs',
tags: ['Template'],
},
};
export const ZGetTemplatesByIdsRequestSchema = z.object({
templateIds: z.array(z.number()).min(1),
});
export const ZGetTemplatesByIdsResponseSchema = z.array(ZTemplateManySchema);
export type TGetTemplatesByIdsRequest = z.infer<typeof ZGetTemplatesByIdsRequestSchema>;
export type TGetTemplatesByIdsResponse = z.infer<typeof ZGetTemplatesByIdsResponseSchema>;

View File

@@ -30,7 +30,6 @@ import { mapEnvelopeToTemplateLite } from '@documenso/lib/utils/templates';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../schema'; import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../schema';
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc'; import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
import { getTemplatesByIdsRoute } from './get-templates-by-ids';
import { import {
ZBulkSendTemplateMutationSchema, ZBulkSendTemplateMutationSchema,
ZCreateDocumentFromDirectTemplateRequestSchema, ZCreateDocumentFromDirectTemplateRequestSchema,
@@ -155,11 +154,6 @@ export const templateRouter = router({
}); });
}), }),
/**
* @public
*/
getMany: getTemplatesByIdsRoute,
/** /**
* Wait until RR7 so we can passthrough documents. * Wait until RR7 so we can passthrough documents.
* *