Explorar el Código

enhance: improve error handling for exports + imports

Display user friendly notification when unexpected failures occur.
Also actively validate export to ensure it can even import
Gabriel Horner hace 8 meses
padre
commit
7da4f7a9b7

+ 30 - 16
deps/db/src/logseq/db/sqlite/build.cljs

@@ -121,7 +121,7 @@
                      [property-map v])))))
                      [property-map v])))))
        (db-property-build/build-property-values-tx-m new-block)))
        (db-property-build/build-property-values-tx-m new-block)))
 
 
-(defn extract-content-refs
+(defn- extract-content-refs
   "Extracts basic refs from :block/title like `[[foo]]` or `[[UUID]]`. Can't
   "Extracts basic refs from :block/title like `[[foo]]` or `[[UUID]]`. Can't
   use db-content/get-matched-ids because of named ref support.  Adding more ref
   use db-content/get-matched-ids because of named ref support.  Adding more ref
   support would require parsing each block with mldoc and extracting with
   support would require parsing each block with mldoc and extracting with
@@ -352,20 +352,6 @@
                              ((fn [x] (update-vals x #(mapv second %)))))]
                              ((fn [x] (update-vals x #(mapv second %)))))]
     props-to-values))
     props-to-values))
 
 
-(defn- validate-options
-  [{:keys [properties] :as options}]
-  (when-let [errors (->> options (m/explain Options) me/humanize)]
-    (println "The build-blocks-tx has the following options errors:")
-    (pprint/pprint errors)
-    (throw (ex-info "Options validation failed" {:errors errors})))
-  (when-not (:auto-create-ontology? options)
-    (let [used-properties (get-used-properties-from-options options)
-          undeclared-properties (-> (set (keys used-properties))
-                                    (set/difference (set (keys properties)))
-                                    ((fn [x] (remove db-property/logseq-property? x))))]
-      (assert (empty? undeclared-properties)
-              (str "The following properties used in EDN were not declared in :properties: " undeclared-properties)))))
-
 ;; TODO: How to detect these idents don't conflict with existing? :db/add?
 ;; TODO: How to detect these idents don't conflict with existing? :db/add?
 (defn- create-all-idents
 (defn- create-all-idents
   [properties classes graph-namespace]
   [properties classes graph-namespace]
@@ -484,7 +470,7 @@
       (println "Building additional pages from content refs:" (pr-str (mapv #(get-in % [:page :block/title]) new-pages-from-refs))))
       (println "Building additional pages from content refs:" (pr-str (mapv #(get-in % [:page :block/title]) new-pages-from-refs))))
     (concat pages-and-blocks new-pages-from-refs)))
     (concat pages-and-blocks new-pages-from-refs)))
 
 
-(defn add-new-pages-from-properties
+(defn- add-new-pages-from-properties
   [properties pages-and-blocks]
   [properties pages-and-blocks]
   (let [used-properties (get-used-properties-from-options {:pages-and-blocks pages-and-blocks :properties properties})
   (let [used-properties (get-used-properties-from-options {:pages-and-blocks pages-and-blocks :properties properties})
         existing-pages (->> pages-and-blocks (keep #(select-keys (:page %) [:build/journal :block/title])) set)
         existing-pages (->> pages-and-blocks (keep #(select-keys (:page %) [:build/journal :block/title])) set)
@@ -623,6 +609,34 @@
                              classes-tx
                              classes-tx
                              pages-and-blocks-tx))))
                              pages-and-blocks-tx))))
 
 
+;; Public API
+;; ==========
+
+(defn extract-from-blocks
+  "Given a vec of blocks and a fn which applied to a block returns a coll, this
+  returns the coll produced by applying f to all blocks including :build/children blocks"
+  [blocks f]
+  (let [apply-to-block-and-all-children
+        (fn apply-to-block-and-all-children [m f]
+          (into (f m)
+                (when-let [children (seq (:build/children m))]
+                  (mapcat #(apply-to-block-and-all-children % f) children))))]
+    (mapcat #(apply-to-block-and-all-children % f) blocks)))
+
+(defn validate-options
+  [{:keys [properties] :as options}]
+  (when-let [errors (->> options (m/explain Options) me/humanize)]
+    (println "The build-blocks-tx has the following options errors:")
+    (pprint/pprint errors)
+    (throw (ex-info "Options validation failed" {:errors errors})))
+  (when-not (:auto-create-ontology? options)
+    (let [used-properties (get-used-properties-from-options options)
+          undeclared-properties (-> (set (keys used-properties))
+                                    (set/difference (set (keys properties)))
+                                    ((fn [x] (remove db-property/logseq-property? x))))]
+      (assert (empty? undeclared-properties)
+              (str "The following properties used in EDN were not declared in :properties: " undeclared-properties)))))
+
 (defn ^:large-vars/doc-var build-blocks-tx
 (defn ^:large-vars/doc-var build-blocks-tx
   "Given an EDN map for defining pages, blocks and properties, this creates a map
   "Given an EDN map for defining pages, blocks and properties, this creates a map
  with two keys of transactable data for use with d/transact!. The :init-tx key
  with two keys of transactable data for use with d/transact!. The :init-tx key

+ 41 - 8
deps/db/src/logseq/db/sqlite/export.cljs

@@ -1,7 +1,8 @@
 (ns logseq.db.sqlite.export
 (ns logseq.db.sqlite.export
   "Builds sqlite.build EDN to represent nodes in a graph-agnostic way.
   "Builds sqlite.build EDN to represent nodes in a graph-agnostic way.
    Useful for exporting and importing across DB graphs"
    Useful for exporting and importing across DB graphs"
-  (:require [datascript.core :as d]
+  (:require [clojure.set :as set]
+            [datascript.core :as d]
             [datascript.impl.entity :as de]
             [datascript.impl.entity :as de]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
             [logseq.db.frontend.class :as db-class]
             [logseq.db.frontend.class :as db-class]
@@ -354,16 +355,48 @@
       (seq classes)
       (seq classes)
       (assoc :classes classes))))
       (assoc :classes classes))))
 
 
+(defn- ensure-export-is-valid
+  "Checks that export map is usable by sqlite.build including checking that
+   all referenced properties and classes are defined"
+  [{:keys [classes properties pages-and-blocks] :as export-map}]
+  (sqlite-build/validate-options export-map)
+  (let [referenced-classes
+        (->> (concat (mapcat :build/property-classes (vals properties))
+                     (keep :class/parent (vals classes))
+                     (mapcat (comp :block/tags :page) pages-and-blocks)
+                     (mapcat #(sqlite-build/extract-from-blocks (:blocks %) :build/tags) pages-and-blocks))
+             (remove db-class/logseq-class?)
+             set)
+        undefined-classes (set/difference referenced-classes (set (keys classes)))
+        referenced-properties
+        (->> (concat (mapcat :build/class-properties (vals classes))
+              (mapcat (comp keys :build/properties :page) pages-and-blocks)
+                     (mapcat #(sqlite-build/extract-from-blocks (:blocks %) (comp keys :build/properties)) pages-and-blocks))
+             (remove db-property/logseq-property?)
+             set)
+        undefined-properties (set/difference referenced-properties (set (keys properties)))
+        undefined (cond-> {}
+                    (seq undefined-classes) (assoc :classes undefined-classes)
+                    (seq undefined-properties) (assoc :properties undefined-properties))]
+    ;; (prn :rclasses referenced-classes)
+    ;; (prn :rproperties referenced-properties)
+    (when (seq undefined)
+      (throw (ex-info (str "The following classes and properties are not defined: " (pr-str undefined))
+                      undefined)))))
+
 (defn build-export
 (defn build-export
   "Handles exporting db by given export-type"
   "Handles exporting db by given export-type"
   [db {:keys [export-type] :as options}]
   [db {:keys [export-type] :as options}]
-  (case export-type
-    :block
-    (build-block-export db (:block-id options))
-    :page
-    (build-page-export db (:page-id options))
-    :graph-ontology
-    (build-graph-ontology-export db)))
+  (let [export-map
+        (case export-type
+          :block
+          (build-block-export db (:block-id options))
+          :page
+          (build-page-export db (:page-id options))
+          :graph-ontology
+          (build-graph-ontology-export db))]
+    (ensure-export-is-valid (dissoc export-map ::block))
+    export-map))
 
 
 ;; Import fns
 ;; Import fns
 ;; ==========
 ;; ==========

+ 7 - 3
src/main/frontend/handler/db_based/export.cljs

@@ -59,9 +59,13 @@
     (if (= ::invalid-import export-map)
     (if (= ::invalid-import export-map)
       (notification/show! "The submitted EDN data is invalid! Fix and try again." :warning)
       (notification/show! "The submitted EDN data is invalid! Fix and try again." :warning)
       (let [{:keys [init-tx block-props-tx error] :as txs}
       (let [{:keys [init-tx block-props-tx error] :as txs}
-            (sqlite-export/build-import export-map
-                                        (db/get-db)
-                                        (when block {:current-block block}))]
+            (try
+              (sqlite-export/build-import export-map
+                                          (db/get-db)
+                                          (when block {:current-block block}))
+              (catch :default e
+                (js/console.error "Import EDN error: " e)
+                {:error "An unexpected error occurred during import. See the javascript console for details."}))]
         (pprint/pprint txs)
         (pprint/pprint txs)
         (if error
         (if error
           (notification/show! error :error)
           (notification/show! error :error)

+ 10 - 3
src/main/frontend/worker/db_worker.cljs

@@ -911,9 +911,16 @@
 
 
   (export-edn
   (export-edn
    [_this repo options]
    [_this repo options]
-   (let [conn (worker-state/get-datascript-conn repo)
-         result (sqlite-export/build-export @conn (ldb/read-transit-str options))]
-     (ldb/write-transit-str result)))
+   (let [conn (worker-state/get-datascript-conn repo)]
+     (try
+       (->> (ldb/read-transit-str options)
+            (sqlite-export/build-export @conn)
+            ldb/write-transit-str)
+       (catch :default e
+         (js/console.error "export-edn error: " e)
+         (worker-util/post-message :notification
+                                   ["An unexpected error occurred during export. See the javascript console for details."
+                                    :error])))))
 
 
   (dangerousRemoveAllDbs
   (dangerousRemoveAllDbs
    [this repo]
    [this repo]