Skip to content
bitzorcas
中EN

Reference

DocumentStructure favorites, recycle bin, and object security

A deep guide to favorite relations, uniqueness and soft delete, dangling references, owner-resource authorization, and Documents recycle-bin query, restore, purge, and cascade boundaries.

Last updated

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.

not resolved today

Current user

DocsFavorite relation

TargetType + TargetId

Documents or another owner

existence + current authorization

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

Add a document 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.

Natural key whose lifecycle must be specified
-- 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.

Remove a favorite and preserve typed errors
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.

List one knowledge base's recycle bin
GET /api/v1/document-structures/recycle-bin?knowledgeBaseId=kb-100&pageIndex=1&pageSize=20
Authorization: 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

ScenarioGA behavior
favorite target missingvalidation/not found; no relation
favorite target later forbiddenno detail leakage in list
cross-tenant FavoriteIdnot found
remove another user’s favoriteforbidden
query unauthorized knowledge baseforbidden/not found
Category ID supplied as Documentowner/type validation rejects
purge inside retention periodpolicy denied
purge object under legal holdhard denial plus audit
concurrent restore and purgeone 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

Terminal window
# 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

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%