forked from EduCraft/curriculum-project-hub
d072e9ec1e
- 回收站:listBin(祖先全活跃的已删顶点;管理员/直连 MANAGE 可见)、 restore(与 D15 对称只清本节点,落审计)、purge(仅管理员,pathIds 枚举 子树按深度降序分批硬删,绕过 self-FK RESTRICT) - 最近打开:FileLibRecentVisit 表(filePath='' 兜底 PG 唯一索引),客户端 成功打开后上报(VIEW 门禁,upsert 刷新),列表 20 条,D8/D15 可见性过滤 - 前端:/app 左栏(文件库/最近打开/回收站);RecentView/BinView; GridLibraryView 埋点 + navTarget 跳转(breadcrumb 建栈,role 已捎带) - 测试:filelib-nav 集成 4 例;全套 79 例绿
59 lines
2.9 KiB
Markdown
59 lines
2.9 KiB
Markdown
# ADR 0031: File Library Recycle Bin And Recent-Visit Tracking
|
|
|
|
## Status
|
|
|
|
Accepted.
|
|
|
|
## Context
|
|
|
|
The teacher app (`/app`) gains a left navigation rail with three entries: 文件库 /
|
|
最近打开 / 回收站. Two of them need semantics that no prior decision covers:
|
|
|
|
- **回收站 (recycle bin)**: D15 defined soft delete (mark `deletedAt` on the node only;
|
|
a node is invisible when any ancestor is deleted) but never defined listing, restore,
|
|
or permanent deletion.
|
|
- **最近打开 (recent visits)**: nothing tracks opens.
|
|
|
|
## Decision
|
|
|
|
### Recycle bin
|
|
|
|
- **List**: shows nodes with `deletedAt != null` whose **ancestors are all active**
|
|
(the topmost deleted node per branch; descendants of a deleted node are represented
|
|
by it and not listed separately).
|
|
- **Visibility/auth**: a bin entry is visible to (a) the website administrator, or
|
|
(b) any actor holding an active MANAGE grant **on the deleted node itself**
|
|
(grants stay live through soft delete, so this is a plain grant query — no chain
|
|
walk, no inheritance; the bin is a management surface, not a browsing surface).
|
|
- **Restore** clears `deletedAt` on that node only (D15 symmetry: delete marks one
|
|
node, restore unmarks one node). The subtree becomes visible again immediately.
|
|
Same auth as the list entry. Audited (`folder_restore` / `project_restore`).
|
|
- **Permanent delete (彻底删除)** is **website-administrator only**: hard-deletes the
|
|
node **and its whole subtree** (descendants enumerated via the `pathIds` materialized
|
|
path, deleted deepest-first because the self-FK is `ON DELETE RESTRICT`), in one
|
|
transaction, with one audit entry (`node_purge`, detail carries removed count).
|
|
Grants/settings/export-jobs cascade. There is no recovery; the UI must confirm
|
|
explicitly.
|
|
|
|
### Recent visits
|
|
|
|
- **Model**: `FileLibRecentVisit(organizationId, userId, nodeId, filePath, openedAt)`,
|
|
unique on `(organizationId, userId, nodeId, filePath)` with `filePath` defaulting to
|
|
`""` (Postgres unique indexes treat NULLs as distinct). `filePath = ""` means the
|
|
visit is the node itself (drill into folder/project); non-empty means a file preview
|
|
inside that project.
|
|
- **Recording is client-driven**: the teacher app POSTs after a successful open
|
|
(folder drill, project open, file preview). The endpoint requires VIEW on the node
|
|
(D8: no VIEW → 404, leaking nothing). Upsert semantics: re-opening refreshes
|
|
`openedAt`. No audit entries — this is a per-user read model, not a权限-sensitive
|
|
mutation.
|
|
- **List**: the actor's own most recent 20, `openedAt` desc. Entries whose node is
|
|
deleted **or has any deleted ancestor** are filtered out (D8/D15 visibility holds
|
|
on every surface). Names are read live from `FileLibNode` (no denormalization).
|
|
|
|
## Consequences
|
|
|
|
- No change to existing permission algebra; both features are additive surfaces.
|
|
- The bin deliberately does not offer per-owner bins or inherited-MANAGE visibility —
|
|
if real usage demands it, that is a new decision.
|