From 4a567c28de3aacfecc9d1d0ecf9b19f5817cae9d Mon Sep 17 00:00:00 2001
From: Case Nelson <case@metabase.com>
Date: Wed, 25 May 2022 11:54:14 -0600
Subject: [PATCH] Persist model integration changes for front end (#22933)

* Add can-manage to databases to help front-end

* Only return persisted true when the state agrees

* Add event for persisted-info to catch new models and turn on peristence when appropriate

* Fix bad threading

* Add comment about the case being detected to add the persisted-info

* Fix pre-existing bug in expand-db-details where a function was used as the condition
---
 src/metabase/api/database.clj                |  9 ++--
 src/metabase/events/persisted_info.clj       | 45 ++++++++++++++++++++
 src/metabase/models/persisted_info.clj       |  4 +-
 test/metabase/api/database_test.clj          |  2 +-
 test/metabase/events/persisted_info_test.clj | 19 +++++++++
 5 files changed, 74 insertions(+), 5 deletions(-)
 create mode 100644 src/metabase/events/persisted_info.clj
 create mode 100644 test/metabase/events/persisted_info_test.clj

diff --git a/src/metabase/api/database.clj b/src/metabase/api/database.clj
index 6cb115ca8db..fa199b4beee 100644
--- a/src/metabase/api/database.clj
+++ b/src/metabase/api/database.clj
@@ -330,14 +330,17 @@
   {include (s/maybe (s/enum "tables" "tables.fields"))}
   (let [include-editable-data-model? (Boolean/parseBoolean include_editable_data_model)
         exclude-uneditable-details?  (Boolean/parseBoolean exclude_uneditable_details)
-        filter-by-data-access?       (not (or include-editable-data-model? exclude-uneditable-details?))]
-    (cond-> (api/check-404 (Database id))
+        filter-by-data-access?       (not (or include-editable-data-model? exclude-uneditable-details?))
+        database                     (api/check-404 (Database id))]
+    (cond-> database
       filter-by-data-access?       api/read-check
       exclude-uneditable-details?  api/write-check
       true                         add-expanded-schedules
       true                         (get-database-hydrate-include include)
       include-editable-data-model? check-db-data-model-perms
-      mi/can-write?                secret/expand-db-details-inferred-secret-values)))
+      (mi/can-write? database)     (->
+                                     secret/expand-db-details-inferred-secret-values
+                                     (assoc :can-manage true)))))
 
 
 ;;; ----------------------------------------- GET /api/database/:id/metadata -----------------------------------------
diff --git a/src/metabase/events/persisted_info.clj b/src/metabase/events/persisted_info.clj
new file mode 100644
index 00000000000..4f7c3ca7f5b
--- /dev/null
+++ b/src/metabase/events/persisted_info.clj
@@ -0,0 +1,45 @@
+(ns metabase.events.persisted-info
+  (:require [clojure.core.async :as a]
+            [clojure.tools.logging :as log]
+            [metabase.events :as events]
+            [metabase.models :refer [Database PersistedInfo]]
+            [metabase.models.persisted-info :as persisted-info]
+            [metabase.public-settings :as public-settings]
+            [toucan.db :as db]))
+
+(def ^:private ^:const persisted-info-topics
+  "The `Set` of event topics which are subscribed to add persisted-info to new models."
+  #{:card-create
+    :card-update})
+
+
+(defonce ^:private ^{:doc "Channel for receiving event notifications we want to subscribe to persist new models"}
+  persisted-info-channel
+  (a/chan))
+
+
+;;; ## ---------------------------------------- EVENT PROCESSING ----------------------------------------
+
+
+(defn process-event
+  "Handle processing for a single event notification received on the persisted-info-channel"
+  [{_topic :topic card :item :as event}]
+  ;; try/catch here to prevent individual topic processing exceptions from bubbling up.  better to handle them here.
+  (try
+    ;; We only want to add a persisted-info for newly created models where dataset is being set to true.
+    ;; If there is already a PersistedInfo, even in "off" or "deletable" state, we skip it as this
+    ;; is only supposed to be that initial edge when the dataset is being changed.
+    (when (and (:dataset card)
+               (public-settings/persisted-models-enabled)
+               (get-in (Database (:database_id card)) [:options :persist-models-enabled])
+               (nil? (db/select-one-field :id PersistedInfo :card_id (:id card))))
+      (persisted-info/turn-on! (:actor_id card) card))
+    (catch Throwable e
+      (log/warn (format "Failed to process persisted-info event. %s" (:topic event)) e))))
+
+
+;;; ## ---------------------------------------- LIFECYLE ----------------------------------------
+
+(defmethod events/init! ::PersistedInfo
+  [_]
+  (events/start-event-listener! persisted-info-topics persisted-info-channel process-event))
diff --git a/src/metabase/models/persisted_info.clj b/src/metabase/models/persisted_info.clj
index 2bf48550bb7..e982385f329 100644
--- a/src/metabase/models/persisted_info.clj
+++ b/src/metabase/models/persisted_info.clj
@@ -74,7 +74,9 @@
   {:batched-hydrate :persisted}
   [cards]
   (when (seq cards)
-    (let [existing-ids (db/select-field :card_id PersistedInfo :card_id [:in (map :id cards)])]
+    (let [existing-ids (db/select-field :card_id PersistedInfo
+                                        :card_id [:in (map :id cards)]
+                                        :state [:not-in ["off" "deletable"]])]
       (map (fn [{id :id :as card}]
              (assoc card :persisted (contains? existing-ids id)))
            cards))))
diff --git a/test/metabase/api/database_test.clj b/test/metabase/api/database_test.clj
index b1ec229c41d..6567a2e3fc4 100644
--- a/test/metabase/api/database_test.clj
+++ b/test/metabase/api/database_test.clj
@@ -87,7 +87,7 @@
                    (dissoc :schedules)))))
 
       (testing "Superusers should see DB details"
-        (is (= (db-details)
+        (is (= (assoc (db-details) :can-manage true)
                (-> (mt/user-http-request :crowberto :get 200 (format "database/%d" (mt/id)))
                    (dissoc :schedules))))))
 
diff --git a/test/metabase/events/persisted_info_test.clj b/test/metabase/events/persisted_info_test.clj
new file mode 100644
index 00000000000..54d4847225a
--- /dev/null
+++ b/test/metabase/events/persisted_info_test.clj
@@ -0,0 +1,19 @@
+(ns metabase.events.persisted-info-test
+  (:require [clojure.test :refer [deftest is]]
+            [metabase.events.persisted-info :as events.persisted-info]
+            [metabase.models :refer [Card Database PersistedInfo]]
+            [metabase.test :as mt]
+            [metabase.util :as u]
+            [toucan.db :as db]))
+
+(deftest event-test
+  (mt/with-temporary-setting-values [persisted-models-enabled true]
+    (mt/with-temp* [Database [db {:options {:persist-models-enabled true}}]
+                    Card     [card {:database_id (u/the-id db)}]]
+      (events.persisted-info/process-event {:topic :card-create
+                                            :item  card})
+      (is (zero? (count (db/select PersistedInfo :card_id (u/the-id card)))))
+
+      (events.persisted-info/process-event {:topic :card-update
+                                            :item (assoc card :dataset true)})
+      (is (= "creating" (:state (db/select-one PersistedInfo :card_id (u/the-id card))))))))
-- 
GitLab