From 45a0937266494061b8d1b366a158014972c92164 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 24 Jun 2026 11:16:16 +0000 Subject: [PATCH] :recycle: Remove deleted-at from demo profile creation, use delayed worker task instead Demo profiles were created with deleted-at set to a future date, causing conflicts with db/is-row-deleted? which treats any non-nil deleted-at as deleted. This required ::db/remove-deleted false workarounds scattered across the codebase. Now demo profiles are created with deleted_at = NULL (fully active during their lifetime). A delayed :demo-purge worker task is scheduled at creation time. When it fires after the configured deletion-delay, it marks the profile with deleted_at = now() and submits the standard delete-object cascade (teams -> projects -> files -> media). - New: app.tasks.demo-purge handler (delayed worker task) - Modified: demo.clj removes :deleted-at, adds delayed task submission - Modified: profile.clj removes ::db/remove-deleted false workaround - Modified: main.clj registers demo-purge handler --- backend/src/app/main.clj | 5 +++ backend/src/app/rpc/commands/demo.clj | 10 ++++- backend/src/app/rpc/commands/profile.clj | 4 +- backend/src/app/tasks/demo_purge.clj | 41 +++++++++++++++++++++ backend/test/backend_tests/demo_test.clj | 47 ++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 backend/src/app/tasks/demo_purge.clj create mode 100644 backend/test/backend_tests/demo_test.clj diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index ca8fba8af1..a6048a970d 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -391,6 +391,8 @@ :delete-object (ig/ref :app.tasks.delete-object/handler) + :demo-purge + (ig/ref :app.tasks.demo-purge/handler) :process-webhook-event (ig/ref ::webhooks/process-event-handler) :run-webhook @@ -428,6 +430,9 @@ :app.tasks.delete-object/handler {::db/pool (ig/ref ::db/pool)} + :app.tasks.demo-purge/handler + {::db/pool (ig/ref ::db/pool)} + :app.tasks.file-gc/handler {::db/pool (ig/ref ::db/pool) ::sto/storage (ig/ref ::sto/storage)} diff --git a/backend/src/app/rpc/commands/demo.clj b/backend/src/app/rpc/commands/demo.clj index be34fc71fe..17fba7f1b9 100644 --- a/backend/src/app/rpc/commands/demo.clj +++ b/backend/src/app/rpc/commands/demo.clj @@ -9,7 +9,6 @@ (:require [app.auth :refer [derive-password-weak]] [app.common.exceptions :as ex] - [app.common.time :as ct] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -18,6 +17,7 @@ [app.rpc.commands.auth :as auth] [app.rpc.doc :as-alias doc] [app.util.services :as sv] + [app.worker :as wrk] [buddy.core.codecs :as bc] [buddy.core.nonce :as bn])) @@ -47,12 +47,18 @@ :fullname fullname :is-active true :is-demo true - :deleted-at (ct/in-future (cf/get-deletion-delay)) :password (derive-password-weak password) :props {}} + profile (db/tx-run! cfg (fn [cfg] (->> (auth/create-profile cfg params) (auth/create-profile-rels cfg))))] + + (wrk/submit! (-> cfg + (assoc ::wrk/task :demo-purge) + (assoc ::wrk/delay (cf/get-deletion-delay)) + (assoc ::wrk/params {:profile-id (:id profile)}))) + (with-meta {:email email :password password} {::audit/profile-id (:id profile)}))) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 9941638196..6cb6fb0041 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -122,9 +122,7 @@ (defn get-profile "Get profile by id. Throws not-found exception if no profile found." [conn id & {:as opts}] - ;; NOTE: We need to set ::db/remove-deleted to false because demo profiles - ;; are created with a set deleted-at value - (-> (db/get-by-id conn :profile id (assoc opts ::db/remove-deleted false)) + (-> (db/get-by-id conn :profile id opts) (decode-row))) ;; --- MUTATION: Update Profile (own) diff --git a/backend/src/app/tasks/demo_purge.clj b/backend/src/app/tasks/demo_purge.clj new file mode 100644 index 0000000000..429816c053 --- /dev/null +++ b/backend/src/app/tasks/demo_purge.clj @@ -0,0 +1,41 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns app.tasks.demo-purge + "Task handler for delayed demo profile deletion. Submitted at demo + creation time with a delay matching the configured deletion-delay." + (:require + [app.common.logging :as l] + [app.common.time :as ct] + [app.db :as db] + [app.worker :as wrk] + [integrant.core :as ig])) + +(defmethod ig/assert-key ::handler + [_ params] + (assert (db/pool? (::db/pool params)) "expected a valid database pool")) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [{:keys [props]}] + (let [profile-id (get props :profile-id) + now (ct/now)] + + (l/trc :hint "demo-purge" :profile-id (str profile-id)) + + ;; Mark the profile for immediate deletion + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (db/update! conn :profile + {:deleted-at now} + {:id profile-id} + {::db/return-keys false}) + (wrk/submit! + (-> cfg + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :profile + :deleted-at now + :id profile-id})))))))) diff --git a/backend/test/backend_tests/demo_test.clj b/backend/test/backend_tests/demo_test.clj new file mode 100644 index 0000000000..da1cc342c9 --- /dev/null +++ b/backend/test/backend_tests/demo_test.clj @@ -0,0 +1,47 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns backend-tests.demo-test + (:require + [app.common.time :as ct] + [app.db :as db] + [app.rpc.commands.profile :as profile] + [app.tasks.demo-purge :as demo-purge] + [app.worker :as wrk] + [backend-tests.helpers :as th] + [clojure.test :as t] + [integrant.core :as ig])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest demo-profile-created-without-deleted-at + (let [profile (th/create-profile* 999 {:is-demo true})] + (t/is (true? (:is-demo profile))) + (t/is (nil? (:deleted-at profile))) + (t/is (some? (:id profile))))) + +(t/deftest get-profile-finds-demo-user-without-override + (let [profile (th/create-profile* 998 {:is-demo true}) + found (db/run! th/*pool* + (fn [{:keys [::db/conn]}] + (profile/get-profile conn (:id profile))))] + (t/is (some? found)) + (t/is (= (:id profile) (:id found))))) + +(t/deftest demo-purge-handler-submits-delete-object + (let [profile (th/create-profile* 996 {:is-demo true}) + handler (ig/init-key :app.tasks.demo-purge/handler + {::db/pool th/*pool*}) + submitted (atom nil)] + (with-redefs [wrk/submit! (fn [& {:keys [::wrk/task ::wrk/params]}] + (reset! submitted {:task task :params params}))] + (handler {:props {:profile-id (:id profile) + :deleted-at (ct/now)}})) + (t/is (= :delete-object (:task @submitted))) + (t/is (= :profile (:object (:params @submitted)))) + (t/is (= (:id profile) (:id (:params @submitted)))) + (t/is (some? (:deleted-at (:params @submitted))))))