Browse Source

enhance: validate export EDN for most export types

and error explicitly if they can't import. This is much better
for the user so they aren't finding out later that EDN is invalid
on import. Added both to the export menu and to export EDN commands that
aren't graph-wide. Related to
https://github.com/logseq/db-test/issues/549
Gabriel Horner 1 day ago
parent
commit
134dc0f2a8

+ 2 - 0
deps/db/.carve/ignore

@@ -27,6 +27,8 @@ logseq.db.sqlite.build/create-blocks
 ;; API
 logseq.db.sqlite.export/build-export
 ;; API
+logseq.db.sqlite.export/validate-export
+;; API
 logseq.db.sqlite.export/build-import
 ;; API
 logseq.db.common.view/get-property-values

+ 27 - 7
deps/db/src/logseq/db/sqlite/export.cljs

@@ -16,7 +16,10 @@
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.sqlite.build :as sqlite-build]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [logseq.db.test.helper :as db-test]
+            [logseq.db.frontend.validate :as db-validate]
+            [cljs.pprint :as pprint]))
 
 ;; Export fns
 ;; ==========
@@ -492,12 +495,12 @@
           (build-mixed-properties-and-classes-export db [page-entity] {:include-uuid? true}))
         class-page-properties-export
         (when-let [props
-                     (and (not (:ontology-page? options))
-                          (entity-util/class? page-entity)
-                          (->> (:logseq.property.class/properties page-entity)
-                               (map :db/ident)
-                               seq))]
-            {:properties (build-export-properties db props {:shallow-copy? true})})
+                   (and (not (:ontology-page? options))
+                        (entity-util/class? page-entity)
+                        (->> (:logseq.property.class/properties page-entity)
+                             (map :db/ident)
+                             seq))]
+          {:properties (build-export-properties db props {:shallow-copy? true})})
         page-block-options (cond-> blocks-export
                              ontology-page-export
                              (merge-export-maps ontology-page-export class-page-properties-export)
@@ -1042,3 +1045,20 @@
             (assoc :misc-tx (vec (concat (::graph-files export-map'')
                                          (::kv-values export-map'')))))
         (sqlite-build/build-blocks-tx (remove-namespaced-keys export-map''))))))
+
+(defn validate-export
+  "Validates an export by creating an in-memory DB graph, importing the EDN and validating the graph.
+   Returns a map with a readable :error key if any error occurs"
+  [export-edn]
+  (try
+    (let [import-conn (db-test/create-conn)
+          {:keys [init-tx block-props-tx misc-tx] :as _txs} (build-import export-edn @import-conn {})
+          _ (d/transact! import-conn (concat init-tx block-props-tx misc-tx))
+          validation (db-validate/validate-db! @import-conn)]
+      (when-let [errors (seq (:errors validation))]
+        (js/console.error "Exported edn has the following invalid errors when imported into a new graph:")
+        (pprint/pprint errors)
+        {:error (str "The exported EDN has " (count errors) " error(s). See the javascript console for more details.")}))
+    (catch :default e
+      (js/console.error "Unexpected export-edn validation error:" e)
+      {:error (str "The exported EDN is unexpectedly invalid: " (pr-str (ex-message e)))})))

+ 16 - 5
src/main/frontend/components/export.cljs

@@ -20,7 +20,8 @@
             [logseq.db :as ldb]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [logseq.db.sqlite.export :as sqlite-export]))
 
 (rum/defcs auto-backup < rum/reactive
   {:init (fn [state]
@@ -177,9 +178,18 @@
                       :selected-nodes
                       {:node-ids (mapv #(vector :block/uuid %) root-block-uuids-or-page-uuid)}
                       {})]
-    (state/<invoke-db-worker :thread-api/export-edn
-                             (state/get-current-repo)
-                             (merge {:export-type export-type} export-args))))
+    (p/let [export-edn (state/<invoke-db-worker :thread-api/export-edn
+                                                (state/get-current-repo)
+                                                (merge {:export-type export-type} export-args))]
+      ;; Don't validate :block for now b/c it requires more setup
+      (if (#{:page :selected-nodes} export-type)
+        (if-let [error (:error (sqlite-export/validate-export export-edn))]
+          (do
+            (js/console.log "Invalid export EDN:")
+            (pprint/pprint export-edn)
+            {:export-edn-error error})
+          export-edn)
+        export-edn))))
 
 (defn- get-zoom-level
   [page-uuid]
@@ -283,7 +293,8 @@
                       :on-click #(do (reset! *export-block-type :edn)
                                      (p/let [result (<export-edn-helper top-level-uuids export-type)
                                              pull-data (with-out-str (pprint/pprint result))]
-                                       (when-not (:export-edn-error result)
+                                       (if (:export-edn-error result)
+                                         (notification/show! (:export-edn-error result) :error)
                                          (reset! *content pull-data))))))])
       (if (= :png tp)
         [:div.flex.items-center.justify-center.relative

+ 27 - 17
src/main/frontend/handler/db_based/export.cljs

@@ -8,7 +8,19 @@
             [frontend.util :as util]
             [frontend.util.page :as page-util]
             [goog.dom :as gdom]
-            [promesa.core :as p]))
+            [promesa.core :as p]
+            [logseq.db.sqlite.export :as sqlite-export]))
+
+(defn- <export-edn-helper
+  "Gets export-edn and validates export for smaller exports. Copied from component.export/<export-edn-helper"
+  [export-args]
+  (p/let [export-edn (state/<invoke-db-worker :thread-api/export-edn (state/get-current-repo) export-args)]
+    (if-let [error (:error (sqlite-export/validate-export export-edn))]
+      (do
+        (js/console.log "Invalid export EDN:")
+        (pprint/pprint export-edn)
+        {:export-edn-error error})
+      export-edn)))
 
 (defn ^:export export-block-data []
   ;; Use editor state to locate most recent block
@@ -24,27 +36,25 @@
     (notification/show! "No block found" :warning)))
 
 (defn export-view-nodes-data [rows {:keys [group-by?]}]
-  (p/let [result (state/<invoke-db-worker :thread-api/export-edn
-                                          (state/get-current-repo)
-                                          {:export-type :view-nodes
-                                           :rows rows
-                                           :group-by? group-by?})
+  (p/let [result (<export-edn-helper {:export-type :view-nodes
+                                      :rows rows
+                                      :group-by? group-by?})
           pull-data (with-out-str (pprint/pprint result))]
-    (when-not (:export-edn-error result)
-      (.writeText js/navigator.clipboard pull-data)
-      (println pull-data)
-      (notification/show! "Copied view nodes' data!" :success))))
+    (if (:export-edn-error result)
+        (notification/show! (:export-edn-error result) :error)
+        (do (.writeText js/navigator.clipboard pull-data)
+            (println pull-data)
+            (notification/show! "Copied view nodes' data!" :success)))))
 
 (defn ^:export export-page-data []
   (if-let [page-id (page-util/get-current-page-id)]
-    (p/let [result (state/<invoke-db-worker :thread-api/export-edn
-                                            (state/get-current-repo)
-                                            {:export-type :page :page-id page-id})
+    (p/let [result (<export-edn-helper {:export-type :page :page-id page-id})
             pull-data (with-out-str (pprint/pprint result))]
-      (when-not (:export-edn-error result)
-        (.writeText js/navigator.clipboard pull-data)
-        (println pull-data)
-        (notification/show! "Copied page's data!" :success)))
+      (if (:export-edn-error result)
+        (notification/show! (:export-edn-error result) :error)
+        (do (.writeText js/navigator.clipboard pull-data)
+            (println pull-data)
+            (notification/show! "Copied page's data!" :success))))
     (notification/show! "No page found" :warning)))
 
 (defn ^:export export-graph-ontology-data []