From d0813af686befe18c1dc733762304c818277863e Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Mon, 26 Jan 2026 23:18:10 +0800 Subject: [PATCH] ensure no missing :block/parent --- src/main/frontend/worker/db_sync.cljs | 50 ++++++++++++++++++- src/test/frontend/worker/db_sync_test.cljs | 57 ++++++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/src/main/frontend/worker/db_sync.cljs b/src/main/frontend/worker/db_sync.cljs index d807982ab6..0d950ea20c 100644 --- a/src/main/frontend/worker/db_sync.cljs +++ b/src/main/frontend/worker/db_sync.cljs @@ -763,6 +763,50 @@ (take 3 item) item))))) +(defn- ensure-block-parents + "Ensure block entities don't lose :block/parent without becoming pages." + [db tx-data] + (let [last-parent-op (atom {}) + page-retracts (atom #{}) + name-adds (atom #{}) + retract-entities (atom #{})] + (doseq [item tx-data] + (when (vector? item) + (let [op (first item)] + (cond + (= op :db/retractEntity) + (swap! retract-entities conj (second item)) + + (and (contains? #{:db/add :db/retract} op) + (>= (count item) 4)) + (let [e (second item) + a (nth item 2)] + (cond + (= a :block/parent) + (swap! last-parent-op assoc e {:op op :v (nth item 3)}) + + (and (= op :db/retract) (= a :block/page)) + (swap! page-retracts conj e) + + (and (= op :db/add) (= a :block/name)) + (swap! name-adds conj e))))))) + (let [candidates (->> @last-parent-op + (keep (fn [[e {:keys [op]}]] + (when (= op :db/retract) e))) + (remove @retract-entities) + (remove @page-retracts) + (remove @name-adds)) + fixes (keep (fn [e] + (when-let [ent (d/entity db e)] + (when (and (:block/page ent) + (not (ldb/page? ent))) + (when-let [page-uuid (:block/uuid (:block/page ent))] + [:db/add e :block/parent [:block/uuid page-uuid]])))) + candidates)] + (if (seq fixes) + (into tx-data fixes) + tx-data)))) + (defn- sanitize-tx-data [db tx-data local-deleted-ids] (let [sanitized-tx-data (->> tx-data @@ -773,7 +817,8 @@ (contains? #{:block/created-at :block/updated-at :block/title} (nth item 2))) (contains? local-deleted-ids (get-lookup-id (last item)))))) - keep-last-update)] + keep-last-update) + sanitized-tx-data (ensure-block-parents db sanitized-tx-data)] ;; (when (not= tx-data sanitized-tx-data) ;; (prn :debug :tx-data tx-data) ;; (prn :debug :sanitized-tx-data sanitized-tx-data)) @@ -796,7 +841,8 @@ tx-data (->> txs (db-normalize/remove-retract-entity-ref @conn) keep-last-update - distinct)] + distinct) + tx-data (ensure-block-parents @conn tx-data)] ;; (prn :debug :before-keep-last-update txs) ;; (prn :debug :upload :tx-data tx-data) (when (seq txs) diff --git a/src/test/frontend/worker/db_sync_test.cljs b/src/test/frontend/worker/db_sync_test.cljs index 161e9b2700..0aa75c498c 100644 --- a/src/test/frontend/worker/db_sync_test.cljs +++ b/src/test/frontend/worker/db_sync_test.cljs @@ -67,6 +67,63 @@ (is (= (:db/id child1') (:db/id (:block/parent parent')))) (is (= (:db/id page') (:db/id (:block/parent child1')))))))))) +(deftest ensure-block-parents-test + (let [{:keys [conn child1 parent]} (setup-parent-child) + child-uuid (:block/uuid child1) + parent-uuid (:block/uuid parent) + page-uuid (:block/uuid (:block/page child1))] + (testing "adds parent when a block loses parent without becoming a page" + (let [tx-data [[:db/retract [:block/uuid child-uuid] :block/parent [:block/uuid parent-uuid] 1]] + fixed (#'db-sync/ensure-block-parents @conn tx-data)] + (is (some (fn [item] + (and (= :db/add (first item)) + (= [:block/uuid child-uuid] (second item)) + (= :block/parent (nth item 2)) + (= [:block/uuid page-uuid] (nth item 3)))) + fixed)))) + (testing "does not add parent when converting to page" + (let [tx-data [[:db/retract [:block/uuid child-uuid] :block/parent [:block/uuid parent-uuid] 1] + [:db/add [:block/uuid child-uuid] :block/name "page" 1]] + fixed (#'db-sync/ensure-block-parents @conn tx-data)] + (is (= tx-data fixed)))) + (testing "does not add parent when entity is retracted" + (let [tx-data [[:db/retract [:block/uuid child-uuid] :block/parent [:block/uuid parent-uuid] 1] + [:db/retractEntity [:block/uuid child-uuid]]] + fixed (#'db-sync/ensure-block-parents @conn tx-data)] + (is (= tx-data fixed)))))) + +(deftest ensure-block-parents-last-op-retract-test + (let [{:keys [conn child1 parent]} (setup-parent-child) + child-uuid (:block/uuid child1) + parent-uuid (:block/uuid parent) + page-uuid (:block/uuid (:block/page child1))] + (testing "retract after add still reparents to page" + (let [tx-data [[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid parent-uuid] 1] + [:db/retract [:block/uuid child-uuid] :block/parent [:block/uuid parent-uuid] 2]] + fixed (#'db-sync/ensure-block-parents @conn tx-data)] + (is (some (fn [item] + (and (= :db/add (first item)) + (= [:block/uuid child-uuid] (second item)) + (= :block/parent (nth item 2)) + (= [:block/uuid page-uuid] (nth item 3)))) + fixed)))))) + +(deftest sanitize-tx-data-ensures-parent-test + (let [{:keys [conn child1 parent]} (setup-parent-child) + child-uuid (:block/uuid child1) + parent-uuid (:block/uuid parent) + page-uuid (:block/uuid (:block/page child1))] + (testing "sanitize-tx-data preserves parent for blocks when last op retracts parent" + (let [tx-data [[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid parent-uuid] 1] + [:db/retract [:block/uuid child-uuid] :block/parent [:block/uuid parent-uuid] 2]] + fixed (#'db-sync/sanitize-tx-data @conn tx-data #{})] + (is (some (fn [item] + (and (= :db/add (first item)) + (= [:block/uuid child-uuid] (second item)) + (= :block/parent (nth item 2)) + (= [:block/uuid page-uuid] (nth item 3)))) + fixed)))))) + (deftest encrypt-decrypt-tx-data-test (async done (-> (p/let [aes-key (crypt/