🐛 Cascade profile deletion in objects-gc task

The objects-gc task was performing a hard delete on profiles
without cascading the soft-delete to owned teams, projects, and files.
This left orphaned objects that were never cleaned up.

Now the task invokes delete-object before the hard delete, ensuring
all owned resources are properly marked for deletion and cleaned up
in subsequent GC iterations.

AI-assisted-by: qwen3.7-plus
This commit is contained in:
Andrey Antukh 2026-08-27 10:28:59 +00:00
parent c9a2b8f12f
commit 6feccaebb3
2 changed files with 38 additions and 0 deletions

View File

@ -13,6 +13,7 @@
[app.db :as db]
[app.features.fdata :as fdata]
[app.storage :as sto]
[app.tasks.delete-object :as dobj]
[integrant.core :as ig]))
(def ^:private sql:get-profiles
@ -33,6 +34,11 @@
;; Mark as deleted the storage object
(some->> photo-id (sto/touch-object! storage))
;; Cascade soft-delete to owned teams, projects, files, etc.
(dobj/delete-object cfg {:object :profile
:id id
:deleted-at timestamp})
(let [affected (-> (db/delete! conn :profile {:id id})
(db/get-update-count))]
(+ total affected)))

View File

@ -424,6 +424,38 @@
(let [count-after (:count (th/db-exec-one! ["SELECT count(*) FROM http_session_v2 WHERE profile_id = ?" (:id prof)]))]
(t/is (= 0 count-after)))))
(t/deftest profile-deletion-via-gc-cascades
(let [prof (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
team-id (:default-team-id prof)
project-id (:default-project-id prof)
file-id (:id file)
deleted-at (ct/minus (ct/now) (ct/duration {:days 1}))]
(th/db-update! :profile
{:deleted-at deleted-at}
{:id (:id prof)})
(let [team-before (th/db-get :team {:id team-id} {::db/remove-deleted false})]
(t/is (nil? (:deleted-at team-before))))
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (pos? (:processed result))))
(let [profile-after (th/db-get :profile {:id (:id prof)} {::db/remove-deleted false})]
(t/is (nil? profile-after)))
(let [team-after (th/db-get :team {:id team-id} {::db/remove-deleted false})]
(t/is (nil? team-after)))
(let [project-after (th/db-get :project {:id project-id} {::db/remove-deleted false})]
(t/is (nil? project-after)))
(let [file-after (th/db-get :file {:id file-id} {::db/remove-deleted false})]
(t/is (nil? file-after)))))
(t/deftest email-blacklist-1
(t/is (false? (email.blacklist/enabled? th/*system*)))