diff --git a/src/main/frontend/db/model.cljs b/src/main/frontend/db/model.cljs index 38d1b4379f..8876c003cb 100644 --- a/src/main/frontend/db/model.cljs +++ b/src/main/frontend/db/model.cljs @@ -511,24 +511,6 @@ independent of format as format specific heading characters are stripped" (not= parent-id (:db/id node))) node)) lefts)))) -(defn- get-next-outdented-block - "Get the next outdented block of the block that has the `id`. - e.g. - - a - - b - - c - - d - - The next outdented block of `c` is `d`." - [db id] - (when-let [block (db-utils/entity db id)] - (let [parent (:block/parent block)] - (if-let [parent-sibling (get-by-parent-&-left db - (:db/id (:block/parent parent)) - (:db/id parent))] - parent-sibling - (get-next-outdented-block db (:db/id parent)))))) - (defn top-block? [block] (= (:db/id (:block/parent block)) diff --git a/src/main/frontend/db/validate.cljs b/src/main/frontend/db/validate.cljs new file mode 100644 index 0000000000..1f330d9b0f --- /dev/null +++ b/src/main/frontend/db/validate.cljs @@ -0,0 +1,50 @@ +(ns frontend.db.validate + "DB validation. + For pages: + 1. Each block should has a unique [:block/parent :block/left] position. + 2. For any block, its children should be connected by :block/left." + (:require [datascript.core :as d] + [medley.core :as medley])) + +(defn- broken-chain? + [page parent-left->eid] + (let [parents (->> (:block/_page page) + (filter #(seq (:block/_parent %))) + (cons page))] + (some + (fn [parent] + (let [parent-id (:db/id parent) + blocks (:block/_parent parent)] + (when (seq blocks) + (when-let [start (parent-left->eid [parent-id parent-id])] + (let [chain (loop [current start + chain [start]] + (let [next (parent-left->eid [parent-id current])] + (if next + (recur next (conj chain next)) + chain)))] + (when (not= (count chain) (count blocks)) + {:parent parent + :chain chain + :broken-blocks (remove (set chain) (map :db/id blocks)) + :blocks blocks})))))) + parents))) + +(defn broken-page? + "Whether `page` is broken." + [db page-id] + (prn :debug " Validate page: " page-id) + (let [parent-left-f (fn [b] + [(get-in b [:block/parent :db/id]) + (get-in b [:block/left :db/id])]) + page (d/entity db page-id) + blocks (:block/_page page) + parent-left->es (group-by parent-left-f blocks) + conflicted (filter #(> (count (second %)) 1) parent-left->es)] + (if (seq conflicted) + [:conflict-parent-left conflicted] + + (let [parent-left->eid (medley/map-vals (fn [c] (:db/id (first c))) parent-left->es)] + (if-let [result (broken-chain? page parent-left->eid)] + [:broken-chain result] + false))))) diff --git a/src/main/frontend/modules/outliner/datascript.cljs b/src/main/frontend/modules/outliner/datascript.cljs index 476936acdd..87b46cb439 100644 --- a/src/main/frontend/modules/outliner/datascript.cljs +++ b/src/main/frontend/modules/outliner/datascript.cljs @@ -1,19 +1,20 @@ (ns frontend.modules.outliner.datascript (:require [datascript.core :as d] - [frontend.db.conn :as conn] - [frontend.db :as db] - [frontend.db.react :as react] - [frontend.modules.outliner.pipeline :as pipelines] - [frontend.modules.editor.undo-redo :as undo-redo] - [frontend.state :as state] - [frontend.config :as config] - [logseq.graph-parser.util :as gp-util] - [lambdaisland.glogi :as log] - [frontend.search :as search] - [clojure.string :as string] - [frontend.util :as util] - [frontend.util.property-edit :as property-edit] - [logseq.graph-parser.util.block-ref :as block-ref])) + [frontend.db.conn :as conn] + [frontend.db :as db] + [frontend.db.react :as react] + [frontend.modules.outliner.pipeline :as pipelines] + [frontend.modules.editor.undo-redo :as undo-redo] + [frontend.state :as state] + [frontend.config :as config] + [logseq.graph-parser.util :as gp-util] + [lambdaisland.glogi :as log] + [frontend.search :as search] + [clojure.string :as string] + [frontend.util :as util] + [frontend.util.property-edit :as property-edit] + [logseq.graph-parser.util.block-ref :as block-ref] + [frontend.db.validate :as db-validate])) (defn new-outliner-txs-state [] (atom [])) @@ -113,6 +114,29 @@ (concat txs retracted-tx')) txs)) +(defn validate-db! + [{:keys [db-before db-after tx-data]}] + (let [changed-pages (->> (filter (fn [d] (contains? #{:block/left :block/parent} (:a d))) tx-data) + (map :e) + distinct + (map (fn [id] + (-> (or (d/entity db-after id) + (d/entity db-before id)) + :block/page + :db/id))) + (remove nil?) + (distinct))] + (reduce + (fn [_ page-id] + (if-let [result (db-validate/broken-page? db-after page-id)] + (do + ;; revert db changes + (assert (false? result) (str "Broken page: " result)) + (reduced false)) + true)) + true + changed-pages))) + (defn transact! [txs opts before-editor-cursor] (let [repo (state/get-current-repo) @@ -148,6 +172,8 @@ conn (conn/get-db repo false) rs (d/transact! conn txs (assoc opts :outliner/transact? true)) tx-id (get-tx-id rs)] + ;; TODO: disable this when db is stable + (validate-db! rs) (state/update-state! :history/tx->editor-cursor (fn [m] (assoc m tx-id before-editor-cursor)))