From 51e54a6badfe984a5384a9444bd33e0586f6e82a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 19 Nov 2025 18:24:24 +0100 Subject: [PATCH] :bug: Fix incorrect project restoration on restoring file (#7778) --- backend/src/app/rpc/commands/files.clj | 42 ++++++++--- backend/src/app/rpc/commands/projects.clj | 11 ++- backend/test/backend_tests/helpers.clj | 19 +++-- backend/test/backend_tests/rpc_file_test.clj | 77 +++++++++++++++++++- 4 files changed, 127 insertions(+), 22 deletions(-) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index f6b262eecc..6fadfae4fe 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -1209,7 +1209,7 @@ ;; --- MUTATION COMMAND: restore-files-immediatelly (def ^:private sql:resolve-editable-files - "SELECT f.id + "SELECT f.id, f.project_id FROM file AS f JOIN project AS p ON (p.id = f.project_id) JOIN team AS t ON (t.id = p.team_id) @@ -1250,18 +1250,38 @@ {:file-id file-id} {::db/return-keys false})) +(def ^:private sql:restore-projects + "UPDATE project SET deleted_at = null WHERE id = ANY(?::uuid[])") + +(defn- restore-projects + [conn project-ids] + (let [project-ids (db/create-array conn "uuid" project-ids)] + (->> (db/exec-one! conn [sql:restore-projects project-ids]) + (db/get-update-count)))) + (defn- restore-deleted-team-files [{:keys [::db/conn]} {:keys [::rpc/profile-id team-id ids]}] (teams/check-edition-permissions! conn profile-id team-id) + (let [total-files + (count ids) - (reduce (fn [affected {:keys [id]}] - (let [index (inc (count affected))] - (events/tap :progress {:file-id id :index index :total (count ids)}) - (restore-file conn id) - (conj affected id))) - #{} - (db/plan conn [sql:resolve-editable-files team-id - (db/create-array conn "uuid" ids)]))) + {:keys [files projects]} + (reduce (fn [result {:keys [id project-id]}] + (let [index (-> result :files count)] + (events/tap :progress {:file-id id :index index :total total-files}) + (restore-file conn id) + + (-> result + (update :files conj id) + (update :projects conj project-id)))) + + {:files #{} :projectes #{}} + (db/plan conn [sql:resolve-editable-files team-id + (db/create-array conn "uuid" ids)]))] + + (restore-projects conn projects) + + files)) (def ^:private schema:restore-deleted-team-files [:map {:title "restore-deleted-team-files"} @@ -1269,8 +1289,8 @@ [:ids [::sm/set ::sm/uuid]]]) (sv/defmethod ::restore-deleted-team-files - "Removes the deletion mark from the specified files (and respective projects)." - + "Removes the deletion mark from the specified files (and respective + projects) on the specified team." {::doc/added "2.12" ::sse/stream? true ::sm/params schema:restore-deleted-team-files} diff --git a/backend/src/app/rpc/commands/projects.clj b/backend/src/app/rpc/commands/projects.clj index c99c4f27d7..94cbab2a98 100644 --- a/backend/src/app/rpc/commands/projects.clj +++ b/backend/src/app/rpc/commands/projects.clj @@ -169,12 +169,19 @@ ;; --- MUTATION: Create Project (defn- create-project - [{:keys [::db/conn] :as cfg} {:keys [profile-id team-id] :as params}] - (let [project (teams/create-project conn params)] + [{:keys [::db/conn] :as cfg} {:keys [::rpc/request-at profile-id team-id] :as params}] + (assert (ct/inst? request-at) "expect request-at assigned") + (let [params (-> params + (assoc :created-at request-at) + (assoc :modified-at request-at)) + project (teams/create-project conn params) + timestamp (::rpc/request-at params)] (teams/create-project-role conn profile-id (:id project) :owner) (db/insert! conn :team-project-profile-rel {:project-id (:id project) :profile-id profile-id + :created-at timestamp + :modified-at timestamp :team-id team-id :is-pinned false}) (assoc project :is-pinned false))) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 4bacb15613..934a671e03 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -30,6 +30,7 @@ [app.rpc.commands.files :as files] [app.rpc.commands.files-create :as files.create] [app.rpc.commands.files-update :as files.update] + [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] [app.rpc.helpers :as rph] [app.util.blob :as blob] @@ -185,15 +186,17 @@ (defn create-project* ([i params] (create-project* *system* i params)) ([system i {:keys [profile-id team-id] :as params}] - (us/assert uuid? profile-id) - (us/assert uuid? team-id) - (db/run! system - (fn [{:keys [::db/conn]}] - (->> (merge {:id (mk-uuid "project" i) - :name (str "project" i)} - params) - (#'teams/create-project conn)))))) + (assert (uuid? profile-id)) + (assert (uuid? team-id)) + (let [timestamp (ct/now)] + (db/run! system + (fn [cfg] + (->> (merge {:id (mk-uuid "project" i) + :name (str "project" i)} + params + {::rpc/request-at timestamp}) + (#'projects/create-project cfg))))))) (defn create-file* ([i params] diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index d34077bc2d..8e8df81071 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -1925,7 +1925,7 @@ (let [row (th/db-exec-one! ["select * from file where id = ?" file-id])] (t/is (= (:deleted-at row) now))))))) -(t/deftest deleted-files-restore +(t/deftest restore-deleted-files (let [prof (th/create-profile* 1 {:is-active true}) team-id (:default-team-id prof) proj-id (:default-project-id prof) @@ -1988,3 +1988,78 @@ (let [row (th/db-exec-one! ["select * from file where id = ?" file-id])] (t/is (nil? (:deleted-at row))))))) + + +(t/deftest restore-deleted-files-and-projets + (let [profile (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id profile) + now (ct/inst "2025-10-31T00:00:00Z")] + + (binding [ct/*clock* (clock/fixed now)] + (let [project (th/create-project* 1 {:profile-id (:id profile) + :team-id team-id}) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:id project)}) + + data {::th/type :delete-project + :id (:id project) + ::rpc/profile-id (:id profile)} + out (th/command! data)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))) + + (th/run-pending-tasks!) + + ;; get deleted files + (let [data {::th/type :get-team-deleted-files + ::rpc/profile-id (:id profile) + :team-id team-id} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [[row1 :as result] (:result out)] + (t/is (= 1 (count result))) + (t/is (= (:will-be-deleted-at row1) #penpot/inst "2025-11-07T00:00:00Z")) + (t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z")) + (t/is (= (:modified-at row1) #penpot/inst "2025-10-31T00:00:00Z")))) + + ;; Check if project is deleted + (let [[row1 :as rows] (th/db-query :project {:id (:id project)})] + ;; (pp/pprint rows) + (t/is (= 1 (count rows))) + (t/is (= (:deleted-at row1) #penpot/inst "2025-11-07T00:00:00Z")) + (t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z")) + (t/is (= (:modified-at row1) #penpot/inst "2025-10-31T00:00:00Z"))) + + ;; Restore files + (let [data {::th/type :restore-deleted-team-files + ::rpc/profile-id (:id profile) + :team-id team-id + :ids #{(:id file)}} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (fn? result)) + (let [events (th/consume-sse result)] + ;; (pp/pprint events) + (t/is (= 2 (count events))) + (t/is (= :end (first (last events)))) + (t/is (= (:ids data) (last (last events))))))) + + + (let [[row1 :as rows] (th/db-query :file {:project-id (:id project)})] + ;; (pp/pprint rows) + (t/is (= 1 (count rows))) + (t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z")) + (t/is (nil? (:deleted-at row1)))) + + + ;; Check if project is restored + (let [[row1 :as rows] (th/db-query :project {:id (:id project)})] + ;; (pp/pprint rows) + (t/is (= 1 (count rows))) + (t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z")) + (t/is (nil? (:deleted-at row1))))))))