ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Documenso 状态筛选设计解析:REJECTED 标签页、EXPIRED 伪状态与 hasExpiredRecipients API 过滤器

Documenso 状态筛选设计解析:REJECTED 标签页、EXPIRED 伪状态与 hasExpiredRecipients API 过滤器 Documenso 状态筛选设计解析REJECTED 标签页、EXPIRED 伪状态与 hasExpiredRecipients API 过滤器【免费下载链接】documensoThe Open Source DocuSign Alternative.项目地址: https://gitcode.com/GitHub_Trending/do/documenso本文基于 Documenso 仓库中的设计方案文档.agents/plans/wild-indigo-wave-rejected-expired-recipient-filters.md展开解析该功能在文档中心列表与公共 API 中的完整落地如何在 UI 中新增 REJECTED已拒绝与 EXPIRED已过期两个筛选标签如何用一个共享的 EXISTS 谓词在数据库层面判定存在已过期收件人以及如何通过hasExpiredRecipients布尔查询参数向 API 消费者暴露正交过滤能力。读完本文你将掌握伪状态标签页与正交布尔过滤器这两种筛选模式的实现原理、权限访问控制的镜像约束以及避免布尔查询参数陷阱如z.coerce.boolean()的 false → true 问题的实用技巧。背景两个未被满足的筛选需求该设计文档的出发点是两个真实的客户场景客户需要找到处于REJECTED被拒绝状态的信封/文档客户需要找到至少有一个收件人的签署链接已过期的信封。而现状是UI 只暴露INBOX / PENDING / COMPLETED / DRAFT / ALL标签页公共 API 也无法按已过期收件人过滤——API 消费者只能被迫拉取全部 PENDING 文档再逐个检查收件人。设计文档通过前期探索确认了两个关键事实它们直接决定了方案形态REJECTED在后端已完全就绪where 子句find-documents.ts、统计计数get-stats.ts、tRPC 响应 schema、ExtendedDocumentStatus枚举、以及FRIENDLY_STATUS_MAP展示层都已支持唯独缺少 UI 标签页数组中的一项。续期过期链接的机制已存在resendDocument会为未签署、非 CC 的收件人刷新expiresAt并清空expirationNotifiedAt见 resend-document.ts对外通过POST /api/v2/document/redistribute与/api/v2/envelope/redistribute暴露。因此续期不需要新功能只需要文档说明。核心概念过期是收件人级条件不是信封状态设计文档明确了一个建模关键Expiration 是逐收件人per-recipient的条件而非信封envelope状态。因此方案采用双轨制UI 侧把过期建模为EXPIRED伪状态标签页复用现有的标签页机制与REJECTED的做法一致API 侧暴露一个与status正交的布尔参数hasExpiredRecipients。两侧共享同一个 EXISTS 谓词。过期收件人的严格定义与 recipients.ts 中的isRecipientExpired保持一致是expiresAt IS NOT NULL AND expiresAt now() AND signingStatus NOT_SIGNED AND role ! CC即设置了过期时间、且已过期、尚未签署、且角色不是抄送CC的收件人。源码中该判定的实现为// packages/lib/utils/recipients.ts (L170-172) export const isRecipientExpired (recipient: { expiresAt: Date | null }) { return Boolean(recipient.expiresAt new Date(recipient.expiresAt) new Date()); };配套的assertRecipientNotExpired会在签名流程中抛出RECIPIENT_EXPIRED错误拦截已过期链接的签署请求。共享 EXISTS 谓词hasExpiredRecipient设计文档最初提议把hasExpiredRecipient(eb)局部添加到find-documents.ts、get-stats.ts、find-envelopes.ts三个文件仿照各文件内已有的recipientExists/senderEmailIs助手模式。当前仓库的最终实现更进一步将该谓词抽取为跨模块共享的 query-helpers.ts成为唯一的判定源// packages/lib/server-only/envelope/query-helpers.ts (L17-27) 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)), );文件头注释明确要求它必须与isRecipientExpired保持同步。这里有两处值得注意的工程细节new Date()直接作为 now 的取值与同文件中 period 过滤器使用.toJSDate()的风格一致避免了引入额外时区换算谓词是 Kysely 的EXISTS子查询只判定是否存在至少一个过期收件人不产生重复行——这正是把它当作独立 AND 条件叠加到任意基查询上的前提。find-documents.ts中同样保留了本地定义的recipientExists与senderEmailIs助手L82-L109新谓词与它们共同构成该文件的可复用 EXISTS 子查询工具箱。枚举扩展EXPIRED作为内部专用状态extended-document-status.ts 在 Prisma 生成的DocumentStatus之上扩展了三个内部状态export const ExtendedDocumentStatus { ...DocumentStatus, // 含 DRAFT / PENDING / COMPLETED / REJECTED / CANCELLED INBOX: INBOX, ALL: ALL, EXPIRED: EXPIRED, } as const;设计文档特别强调EXPIRED是内部专用值公共DocumentStatus枚举不受影响——API 消费者不会把EXPIRED当作合法的status值。同时枚举扩展会故意在类型层面炸出所有RecordExtendedDocumentStatus, ...穷举点强制开发者逐一处理新状态。当前仓库中这些穷举点均已补齐例如内部统计响应 schema find-documents-internal.types.ts 中的每个状态都映射到z.number()。查询层EXPIRED 分支如何叠加访问控制findDocumentsfind-documents.ts按个人路径 / 团队路径分别用ts-pattern的match().with(...).exhaustive()处理各状态。EXPIRED 分支的设计要点与设计文档 C.2 一致镜像 COMPLETED 分支的访问控制删除过滤 可见性 拥有者/收件人权限再 AND 上hasExpiredRecipient(eb)不约束Envelope.status——因为 EXISTS 谓词本身已把范围限定到未签署收件人身上过期的文档实际上仍是 PENDING再约束状态反而会漏掉数据。个人路径L320-L328.with(ExtendedDocumentStatus.EXPIRED, () qb.where((eb) eb.and([ personalDeletedFilter(eb), hasExpiredRecipient(eb), eb.or([eb(Envelope.userId, , user.id), recipientExists(eb, user.email)]), ]), ), )团队路径L479-L490则叠加teamDeletedFilter、visibilityFilter基于团队角色阈值的可见性与teamId归属分支最后 AND 上同一个hasExpiredRecipient(eb)。REJECTED分支则约束Envelope.status REJECTED并要求当前用户是发送者或该信封的拒绝签署收件人个人路径 L295-L309团队路径 L451-L466。统计计数expiredQuery为什么不计入 ALLget-stats.ts 为每个标签页构建独立的计数查询并通过Promise.all并行执行每个计数都套上cappedCount上限STATS_COUNT_CAP 1的 LIMIT 子查询再取 COUNT防止计数本身成为慢查询。expiredQueryL259-L268完全镜像findDocuments中 EXPIRED 分支的访问控制注释明确说明权限控制必须与列表查询镜像保证计数与列表一致。最微妙的一处决策expired不计入all总和L301-L302// expired is intentionally excluded from all — it overlaps PENDING. const all Math.min(draft pending completed rejected cancelled inbox, STATS_COUNT_CAP);因为一个有已过期收件人的信封同时必然处于 PENDING 状态若把它加进all会导致 ALL 标签页计数虚高。这是伪状态 ≠ 真实状态这一概念在数据层的直接体现。UI 层标签页数组与状态展示状态展示document-status.tsx 中FRIENDLY_STATUS_MAP为EXPIRED配置了TimerOff图标与text-orange-500颜色与红色text-red-500的REJECTED形成视觉区分——这正是设计文档开放问题中建议TimerOff/text-orange-500的落地结果。标签页初始化documents._index.tsx 的stats状态初始化器为REJECTED与EXPIRED补上了0初值标签页机制getTabHref、计数徽章、个人组织的.filter过滤无需改动即可驱动新标签页?statusREJECTED/?statusEXPIRED的深度链接也随之生效。空状态文案设计文档中的可选项documents-table-empty-state.tsx 为 REJECTED / EXPIRED 定制空状态文案替代落入.otherwise()的通用文案也包含在本次改动范围内。公共 API正交布尔参数hasExpiredRecipients设计文档 D 部分要求在文档与信封两个 v2 API 上新增该参数。当前实现要点选项透传find-documents.ts 的FindDocumentsOptions新增hasExpiredRecipients?: boolean并带有注释与status正交——叠加应用在buildBaseQuery内L209-L212以.where((eb) hasExpiredRecipient(eb))形式叠加可与任意status组合。信封侧 find-envelopes.ts 做了对称改造。Zod 参数的正确姿势find-documents.types.ts 中参数被定义为字符串枚举再转换hasExpiredRecipients: z .enum([true, false]) .describe(Filter for documents that have at least one recipient whose signing link has expired.) .transform((value) value true) .optional(),设计文档特别警告了要避免的陷阱直接使用z.coerce.boolean()会把查询串false强制转换成true因为非空字符串均为 truthy。字符串白名单 显式比较既规避了这个坑又让 OpenAPI 生成器能正确描述该参数为 string 类型。信封 API 的 find-envelopes.types.ts 采用相同定义参数会随之自动出现在生成的/api/v2/openapi.json中。REJECTED 无需 API 改动REJECTED本来就是合法的公共status值DocumentStatus.REJECTEDGET /api/v2/document?statusREJECTED直接可用。v1 保持不动REST v1GET /api/v1/documents已弃用且不支持状态过滤本次刻意不改动。续期过期链接纯文档说明不引入新机制设计文档 E 部分明确无功能变更——因为resendDocument/redistribute链路早已具备续期能力重发时会用resolveExpiresAt(envelopeExpirationPeriod)重新计算过期时间并对目标收件人写入新的expiresAt同时清空expirationNotifiedAtresend-document.ts。实际落地是对 API 描述的补充例如 redistribute-document.types.ts 的 description 现已写明This also refreshes the signing-link expiration for the targeted unsigned recipients, renewing any expired links. 信封侧的redistribute-envelope.types.ts做了同样的说明。验证策略与 E2E 证据设计文档给出的验证清单typecheck 穷举强制、种子数据 UI 操作、API 行为、续期回归、翻译文件纪律在当前仓库中均有对应产物E2E 覆盖已就位find-documents.spec.ts 包含多条针对hasExpiredRecipients的用例——true时只返回存在过期、未签署、非 CC 收件人的文档false与省略时不做过期过滤跨账号tokenA / tokenB场景验证了过期过滤与访问权限的叠加find-envelopes.spec.ts 对信封 API 做了镜像验证。续期回归路径对过期文档执行POST /api/v2/document/redistribute或 UI 重发对话框后收件人expiresAt被刷新该文档随即离开 Expired 标签页签署链接不再重定向到/sign/$token/expired。翻译纪律packages/lib/translations/*.po为生成文件仅在确有新的msg/Trans字符串时才运行npm run translate且不把生成的.po变更带入分支。改动文件清单领域文件枚举extended-document-status.ts共享过期谓词query-helpers.ts文档 where 子句 API 选项find-documents.ts统计计数get-stats.ts信封查询APIfind-envelopes.ts内部 tRPC 统计 schemafind-documents-internal.types.ts公共文档 API schema 处理器find-documents.types.ts、find-documents.ts公共信封 API schema 处理器find-envelopes.types.ts、find-envelopes.ts状态展示document-status.tsx标签页 统计初始化documents._index.tsx空状态可选项documents-table-empty-state.tsx续期说明redistribute-document.types.ts、redistribute-envelope.types.ts小结这套方案的价值在于最小改动、最大复用REJECTED 的过滤能力完全来自对已有后端机制的 UI 暴露EXPIRED 伪状态复用标签页机器与访问控制模式仅新增一个被四处find、stats 列表、stats 计数、公共 API 双端点复用的 EXISTS 谓词过期链接的修复则回归到既有的 redistribute 续期链路只补充了文档措辞。同时expired不计入 ALL、布尔参数用字符串白名单而非coerce.boolean、统计与列表的权限条件严格镜像等细节展示了在真实代码库中做筛选类功能时容易踩坑的边界——理解这些取舍比单纯照抄枚举与标签页更有工程价值。【免费下载链接】documensoThe Open Source DocuSign Alternative.项目地址: https://gitcode.com/GitHub_Trending/do/documenso创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表