feat(backend): per-file review status with DSL filter and bulk endpoint

Replaces the old "untagged" sentinel tag with a proper per-file workflow
status: needs_review starts true on upload/import and is cleared by an
explicit action (no auto-clear on tagging). Surfaced as a filter token
(r=1 needs review, r=0 done) so it combines with tag/MIME conditions, and
toggled via POST /files/bulk/review (single id or many, edit-ACL enforced,
audit-logged as file_review).

needs_review lives on data.files (column added to the original 003 migration,
partial index in 006, action type seeded in 007).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 21:16:47 +03:00
parent 4d11beb296
commit 48e901cac1
12 changed files with 215 additions and 9 deletions
+41
View File
@@ -558,6 +558,47 @@ func (s *FileService) BulkDelete(ctx context.Context, fileIDs []uuid.UUID) error
return nil
}
// SetNeedsReview sets the review status ("needs tagging" vs marked done) on the
// given files. Each file is checked against edit ACL; files the caller cannot
// edit, or that do not exist, are skipped (same forgiving semantics as
// BulkDelete). Authorized files are updated in a single statement and each is
// audit-logged. Works for one file (single-element slice) or many.
func (s *FileService) SetNeedsReview(ctx context.Context, ids []uuid.UUID, value bool) error {
userID, isAdmin, _ := domain.UserFromContext(ctx)
authorized := make([]uuid.UUID, 0, len(ids))
for _, id := range ids {
f, err := s.files.GetByID(ctx, id)
if err != nil {
if err == domain.ErrNotFound {
continue
}
return err
}
ok, err := s.acl.CanEdit(ctx, userID, isAdmin, f.CreatorID, fileObjectTypeID, id)
if err != nil {
return err
}
if ok {
authorized = append(authorized, id)
}
}
if len(authorized) == 0 {
return nil
}
if err := s.files.SetNeedsReview(ctx, authorized, value); err != nil {
return err
}
objType := fileObjectType
details := map[string]any{"needs_review": value}
for i := range authorized {
_ = s.audit.Log(ctx, "file_review", &objType, &authorized[i], details)
}
return nil
}
// ---------------------------------------------------------------------------
// Import
// ---------------------------------------------------------------------------