feat: rejected and expired recipient filters (#2889)

This commit is contained in:
Ephraim Duncan
2026-07-30 10:41:45 +10:00
committed by GitHub
parent a457e1ef7d
commit 6ec67d1c4d
21 changed files with 867 additions and 10 deletions
@@ -7,6 +7,7 @@ import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
import type { FindResultResponse } from '../../types/search-params';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
import { getTeamById } from '../team/get-team';
import { hasExpiredRecipient } from './query-helpers';
export type FindEnvelopesOptions = {
userId: number;
@@ -23,6 +24,11 @@ export type FindEnvelopesOptions = {
};
query?: string;
folderId?: string;
/**
* When true, restrict results to envelopes with at least one recipient whose signing
* link has expired. Orthogonal to `status` — applied additively.
*/
hasExpiredRecipients?: boolean;
/**
* 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.
@@ -106,6 +112,7 @@ export const findEnvelopes = async ({
orderBy,
query = '',
folderId,
hasExpiredRecipients,
useWindowedCount = true,
}: FindEnvelopesOptions) => {
const user = await prisma.user.findFirstOrThrow({
@@ -182,6 +189,11 @@ export const findEnvelopes = async ({
);
}
// Expired recipient filter (orthogonal to status, additive)
if (hasExpiredRecipients) {
qb = qb.where((eb) => hasExpiredRecipient(eb));
}
// ─── Access control ──────────────────────────────────────────────────
//
// An envelope is visible if ANY of:
@@ -0,0 +1,27 @@
import { sql } from '@documenso/prisma';
import type { DB } from '@documenso/prisma/generated/types';
import { RecipientRole, SigningStatus } from '@prisma/client';
import type { ExpressionBuilder } from 'kysely';
// Expression builder type scoped to the Envelope table context.
type EnvelopeExpressionBuilder = ExpressionBuilder<DB, 'Envelope'>;
/**
* Reusable EXISTS subquery: checks that the envelope has at least one recipient whose
* signing link has expired — `expiresAt` in the past, still unsigned, and not a CC.
*
* This is the single source of truth for the "expired recipient" predicate used by
* `findDocuments`, `findEnvelopes`, and `getStats`. It must stay in sync with
* `isRecipientExpired` (packages/lib/utils/recipients.ts).
*/
export const hasExpiredRecipient = (eb: EnvelopeExpressionBuilder) =>
eb.exists(
eb
.selectFrom('Recipient')
.whereRef('Recipient.envelopeId', '=', 'Envelope.id')
.where('Recipient.expiresAt', 'is not', null)
.where('Recipient.expiresAt', '<=', new Date())
.where('Recipient.signingStatus', '=', sql.lit(SigningStatus.NOT_SIGNED))
.where('Recipient.role', '!=', sql.lit(RecipientRole.CC))
.select(sql.lit(1).as('one')),
);