import { formatDocumentsPath, getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams'; import { prisma } from '@documenso/prisma'; import type { Prisma } from '@prisma/client'; import { DocumentStatus, DocumentVisibility, EnvelopeType, TeamMemberRole } from '@prisma/client'; import { match } from 'ts-pattern'; import { mapSecondaryIdToDocumentId } from '../../utils/envelope'; import { getUserTeamGroups } from '../team/get-user-team-groups'; export type SearchDocumentsWithKeywordOptions = { query: string; userId: number; limit?: number; }; export const searchDocumentsWithKeyword = async ({ query, userId, limit = 20 }: SearchDocumentsWithKeywordOptions) => { if (!query.trim()) { return []; } const [user, teamGroupsByTeamId] = await Promise.all([ prisma.user.findFirstOrThrow({ where: { id: userId, }, }), getUserTeamGroups({ userId }), ]); const teamIds = [...teamGroupsByTeamId.keys()]; const filters: Prisma.EnvelopeWhereInput[] = [ // Documents owned by the user matching title, externalId, or recipient email. { userId, deletedAt: null, OR: [ { title: { contains: query, mode: 'insensitive' } }, { externalId: { contains: query, mode: 'insensitive' } }, { recipients: { some: { email: { contains: query, mode: 'insensitive' } }, }, }, ], }, // Documents where the user is a recipient (completed or pending). { status: { in: [DocumentStatus.COMPLETED, DocumentStatus.PENDING] }, recipients: { some: { email: user.email } }, title: { contains: query, mode: 'insensitive' }, deletedAt: null, }, ]; // Team documents the user has access to. if (teamIds.length > 0) { filters.push({ teamId: { in: teamIds }, deletedAt: null, OR: [ { title: { contains: query, mode: 'insensitive' } }, { externalId: { contains: query, mode: 'insensitive' } }, { recipients: { some: { email: { contains: query, mode: 'insensitive' } }, }, }, ], }); } const envelopes = await prisma.envelope.findMany({ where: { type: EnvelopeType.DOCUMENT, OR: filters, }, select: { id: true, userId: true, teamId: true, title: true, secondaryId: true, visibility: true, recipients: { select: { email: true, token: true, }, }, team: { select: { url: true, }, }, }, distinct: ['id'], orderBy: { createdAt: 'desc', }, // Over-fetch to compensate for post-query visibility filtering on team documents. 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 legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); let path: string; if (envelope.userId === user.id || (envelope.teamId && teamGroupsByTeamId.has(envelope.teamId))) { path = `${formatDocumentsPath(envelope.team.url)}/${legacyDocumentId}`; } else { const signingToken = envelope.recipients.find((r) => r.email === user.email)?.token; path = `/sign/${signingToken}`; } return { title: envelope.title, path, value: [envelope.id, envelope.title, ...envelope.recipients.map((r) => r.email)].join(' '), }; }); return results; };