Favorites and recycle-bin operations both reference Documents resources, but their risks differ sharply. A favorite is only a user shortcut and must never grant access. Purge is irreversible and must be protected by owner-object policy, retention, legal hold, and audit.
1. Favorite data model
DocsFavorite stores TenantId, UserId, TargetType, TargetId, and GroupName, with soft-delete metadata. Its unique index is (TenantId, UserId, TargetType, TargetId). The same target cannot be favorited twice by one user, and GroupName cannot be used to duplicate it into several groups.
Avoiding a cross-bounded-context navigation property is correct. The application still needs an owner resolver to turn the weak reference into a safe view.
2. Adding a favorite
var result = await mediator.Send(new ManageFavorites.AddCommand( TargetType: "Document", TargetId: "doc-100", GroupName: "This week"), cancellationToken);
if (result.IsFailure) return result.Error;
// Success proves only that the relation was saved.// It does not prove that doc-100 exists or is readable by the caller.return result.Value!;The handler checks only nonblank TargetType and TargetId. It has no Document/Category allowlist, does not call the owner, and uses UserId?.ToString() ?? "0". HTTP authentication does not replace an application-level RequireUserId invariant.
3. Duplicate and soft-deleted favorites
There is no pre-check or upsert; duplicate behavior depends on the database unique index. Re-adding a soft-deleted natural key is uncertain because the unique index excludes IsDeleted and the database usually still sees the old row.
GA must define whether duplicate active add returns the existing item, whether a deleted row is restored or replaced, and whether concurrent duplicate requests return identical behavior on both ORMs.
-- Conceptual only: inspect generated provider DDL before relying on syntax.UNIQUE (TenantId, UserId, TargetType, TargetId)4. Listing favorites safely
List filters by TenantId, UserId, optional GroupName, and descending CreateTime. It has no paging. It neither batch-resolves owner objects nor filters targets that were deleted or became forbidden. The DTO contains only type, ID, group, and time.
A safe API can return ResolvedFavorite: batch owner lookups for supported types, disclose no details for forbidden targets, and mark Unavailable or clean up according to policy. The frontend must not treat an ID as permission and fetch through an unguarded endpoint.
5. Removing the caller’s favorite
Remove reads the row by primary key inside EffectiveTenant, compares favorite.UserId with the current user, and soft-deletes only after that check. The explicit ownership check is sound, but the same "0" identity fallback remains. A second concurrent delete may return NotFound because idempotent delete semantics are not specified.
var removed = await mediator.Send( new ManageFavorites.RemoveCommand(favoriteId), cancellationToken);
// A different owner yields DocsErrors.Favorite.NotOwner;// a missing/currently invisible record yields NotFound.if (removed.IsFailure) return removed.Error;
return Result.Success();6. Permission-catalog gap
DocumentStructurePermissions declares template and recycle-bin permissions, but no FavoriteView/Create/Delete entries. Requests omit an explicit Resource and rely on request-type conventions. Authentication alone does not demonstrate which resource/action the authorization pipeline evaluated. HTTP tests and an explicit favorite permission model must lock this down.
7. Recycle-bin port ownership
The port lives in DocumentStructure Application while the adapter lives in Documents Infrastructure and works with Documents soft-delete records. This arrangement avoids direct table access from the coordinating module, but the contract still needs to express owner semantics rather than only tenantId plus item ID.
8. Querying the recycle bin
Query receives KnowledgeBaseId, PageIndex, and PageSize and forwards them unchanged. Application does not normalize page bounds or prove knowledge-base existence and permission. The store must merge soft-deleted Document and Category rows into a stable page; ordering and merge semantics should be fixed by implementation tests.
GET /api/v1/document-structures/recycle-bin?knowledgeBaseId=kb-100&pageIndex=1&pageSize=20Authorization: Bearer <token>Name, ParentId, and DeletedBy are sensitive metadata. A cross-knowledge-base or unauthorized request must be rejected before invoking the store, not merely filtered by tenant.
9. Restore semantics
ItemType dispatch accepts exact Document and Category; anything else yields InvalidItemType. The handler does not load the item, prove that it is deleted, or return an affected count. A port comment says category restore is recursive, but parent restoration, name collisions, attachments, versions, and search indexes require Documents-store evidence.
Repeated Restore may be a no-op that still returns success. Define idempotency and audit explicitly. Restoring under a deleted parent or into an occupied name also needs a stable conflict policy.
10. Purge governance
Purge uses the same type dispatch, but permanent deletion should not be authorized by an ordinary Delete action plus an ID. Minimum controls include retention, legal hold, step-up confirmation, reason, high privilege or approval, impact preview, nonrepudiable audit, and controlled background execution.
A category cascade can remove many categories and documents. A synchronous HTTP transaction is prone to timeouts and oversized locks. A commercial design creates a PurgeJob, scans and freezes scope, processes bounded batches, emits a report, and lets operations resume an interrupted job.
11. Cross-module side effects
Restore and purge affect Files, Search, Comments, Chat attachment references, Guidance/RAG indexes, and favorites. The current port returns only Task, with no affected IDs, event, or report. The coordinator cannot prove that all consumers converged.
Documents should publish versioned owner facts through an Outbox. Consumers process them idempotently. DocumentStructure should not call every downstream module directly.
12. Security matrix
| Scenario | GA behavior |
|---|---|
| favorite target missing | validation/not found; no relation |
| favorite target later forbidden | no detail leakage in list |
| cross-tenant FavoriteId | not found |
| remove another user’s favorite | forbidden |
| query unauthorized knowledge base | forbidden/not found |
| Category ID supplied as Document | owner/type validation rejects |
| purge inside retention period | policy denied |
| purge object under legal hold | hard denial plus audit |
| concurrent restore and purge | one stable terminal state |
13. Operational signals
For favorites, measure add, conflict, restore, remove, unresolved/denied target, and list size. For recycle bin, measure item age/type, restore/purge count, cascade size, duration, failure, legal-hold denial, and event lag. Logs should include tenant, knowledge base, item, operation, actor, reason, and correlation—not document content.
14. Review commands
# Favorite input, owner check, and uniqueness surfaces.rg -n "AddFavoriteAsync|ListFavoritesAsync|NotOwner|UX_DocsFavorite" \ src/Platform/DocumentStructure -g '*.cs'
# Port and Documents adapter locations.rg -n "IDocumentRecycleBinStore|DocumentRecycleBinStore|RestoreCategoryAsync|PurgeCategoryAsync" \ src/Platform/DocumentStructure src/Platform/Documents -g '*.cs'DocumentStructure overview · Template preview and apply · Testing and GA