mirror of
https://github.com/logseq/logseq.git
synced 2026-05-23 12:14:06 +00:00
fix: align outliner and db-sync tests with history-op changes
This commit is contained in:
@@ -112,9 +112,9 @@
|
||||
(:db-before tx-report)
|
||||
(:tx-data tx-report))
|
||||
tx-data (mapv op-e-a-v normalized)]
|
||||
(testing "drops old :block/title retract and keeps new add during title update"
|
||||
(testing "keeps old :block/title retract and new add during title update"
|
||||
(is (some #(= [:db/add [:block/uuid page-uuid] :block/title "Page 2"] %) tx-data))
|
||||
(is (not-any? #(= [:db/retract [:block/uuid page-uuid] :block/title "Page"] %) tx-data)))))
|
||||
(is (some #(= [:db/retract [:block/uuid page-uuid] :block/title "Page"] %) tx-data)))))
|
||||
|
||||
(deftest normalize-tx-data-keeps-recreated-normal-blocks-test
|
||||
(testing "retract + recreate for normal blocks should not drop recreated entity datoms"
|
||||
|
||||
10
deps/outliner/src/logseq/outliner/op.cljs
vendored
10
deps/outliner/src/logseq/outliner/op.cljs
vendored
@@ -320,19 +320,19 @@
|
||||
:class-remove-property
|
||||
(apply outliner-property/class-remove-property! conn args)
|
||||
|
||||
:upsert-closed-value ; don't support undo/redo
|
||||
:upsert-closed-value
|
||||
(apply outliner-property/upsert-closed-value! conn args)
|
||||
|
||||
:delete-closed-value ; don't support undo/redo
|
||||
:delete-closed-value
|
||||
(apply outliner-property/delete-closed-value! conn args)
|
||||
|
||||
:add-existing-values-to-closed-values ; don't support undo/redo
|
||||
:add-existing-values-to-closed-values
|
||||
(apply outliner-property/add-existing-values-to-closed-values! conn args)
|
||||
|
||||
:batch-import-edn ; don't support undo/redo
|
||||
:batch-import-edn
|
||||
(apply import-edn-data conn *result args)
|
||||
|
||||
:transact ; don't support undo/redo
|
||||
:transact
|
||||
(apply ldb/transact! conn args)
|
||||
|
||||
:create-page
|
||||
|
||||
467
deps/outliner/src/logseq/outliner/op/construct.cljc
vendored
467
deps/outliner/src/logseq/outliner/op/construct.cljc
vendored
@@ -2,16 +2,15 @@
|
||||
"Construct canonical forward and reverse outliner ops for history actions."
|
||||
(:require [clojure.string :as string]
|
||||
[datascript.core :as d]
|
||||
[datascript.impl.entity :as de]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.common.uuid :as common-uuid]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.frontend.content :as db-content]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.db.frontend.property.type :as db-property-type]
|
||||
[logseq.db.common.entity-plus :as entity-plus]))
|
||||
[logseq.db.frontend.property :as db-property]))
|
||||
|
||||
(def ^:private semantic-outliner-ops
|
||||
(def ^:api semantic-outliner-ops
|
||||
#{:save-block
|
||||
:insert-blocks
|
||||
:apply-template
|
||||
@@ -24,19 +23,7 @@
|
||||
:delete-page
|
||||
:restore-recycled
|
||||
:recycle-delete-permanently
|
||||
:set-block-property
|
||||
:remove-block-property
|
||||
:batch-set-property
|
||||
:batch-remove-property
|
||||
:delete-property-value
|
||||
:batch-delete-property-value
|
||||
:create-property-text-block
|
||||
:upsert-property
|
||||
:class-add-property
|
||||
:class-remove-property
|
||||
:upsert-closed-value
|
||||
:add-existing-values-to-closed-values
|
||||
:delete-closed-value})
|
||||
:upsert-property})
|
||||
|
||||
(def ^:private transient-block-keys
|
||||
#{:db/id
|
||||
@@ -59,10 +46,11 @@
|
||||
(defn- stable-entity-ref
|
||||
[db x]
|
||||
(cond
|
||||
(map? x) (let [eid (or (:db/id x)
|
||||
(when-let [id (:block/uuid x)]
|
||||
(:db/id (d/entity db [:block/uuid id]))))]
|
||||
(stable-entity-ref db eid))
|
||||
(or (map? x) (de/entity? x))
|
||||
(let [eid (or (:db/id x)
|
||||
(when-let [id (:block/uuid x)]
|
||||
(:db/id (d/entity db [:block/uuid id]))))]
|
||||
(stable-entity-ref db eid))
|
||||
(uuid? x)
|
||||
[:block/uuid x]
|
||||
(and (integer? x) (not (neg? x)))
|
||||
@@ -94,6 +82,16 @@
|
||||
(or (set? v) (sequential? v)) (set (map #(stable-entity-ref db %) v))
|
||||
:else (stable-entity-ref db v)))
|
||||
|
||||
(defn- sanitize-upsert-property-schema
|
||||
[db schema]
|
||||
(reduce-kv (fn [m k v]
|
||||
(assoc m k
|
||||
(if (= :logseq.property/classes k)
|
||||
(sanitize-ref-value db v)
|
||||
v)))
|
||||
{}
|
||||
schema))
|
||||
|
||||
(defn- sanitize-block-refs
|
||||
[refs]
|
||||
(->> refs
|
||||
@@ -230,13 +228,6 @@
|
||||
(when-let [first-block (some->> ids first (d/entity db))]
|
||||
(resolve-target-and-sibling first-block)))
|
||||
|
||||
(defn- stable-property-value
|
||||
[db property-id v]
|
||||
(let [property-type (some-> (d/entity db property-id) :logseq.property/type)]
|
||||
(if (contains? db-property-type/all-ref-property-types property-type)
|
||||
(sanitize-ref-value db v)
|
||||
v)))
|
||||
|
||||
(defn- created-block-uuids-from-tx-data
|
||||
[tx-data]
|
||||
(->> tx-data
|
||||
@@ -513,52 +504,6 @@
|
||||
(let [[root-id] args]
|
||||
[:recycle-delete-permanently [(stable-block-uuid db root-id)]])
|
||||
|
||||
:set-block-property
|
||||
(let [[block-eid property-id v] args
|
||||
property-id' (stable-entity-ref db property-id)]
|
||||
[:set-block-property [(stable-entity-ref db block-eid)
|
||||
property-id'
|
||||
(stable-property-value db property-id' v)]])
|
||||
|
||||
:remove-block-property
|
||||
(let [[block-eid property-id] args]
|
||||
[:remove-block-property [(stable-entity-ref db block-eid)
|
||||
(stable-entity-ref db property-id)]])
|
||||
|
||||
:batch-set-property
|
||||
(let [[block-ids property-id v opts] args
|
||||
property-id' (stable-entity-ref db property-id)]
|
||||
[:batch-set-property [(stable-id-coll db block-ids)
|
||||
property-id'
|
||||
(stable-property-value db property-id' v)
|
||||
opts]])
|
||||
|
||||
:batch-remove-property
|
||||
(let [[block-ids property-id] args]
|
||||
[:batch-remove-property [(stable-id-coll db block-ids)
|
||||
(stable-entity-ref db property-id)]])
|
||||
|
||||
:delete-property-value
|
||||
(let [[block-eid property-id property-value] args
|
||||
property-id' (stable-entity-ref db property-id)]
|
||||
[:delete-property-value [(stable-entity-ref db block-eid)
|
||||
property-id'
|
||||
(stable-property-value db property-id' property-value)]])
|
||||
|
||||
:batch-delete-property-value
|
||||
(let [[block-eids property-id property-value] args
|
||||
property-id' (stable-entity-ref db property-id)]
|
||||
[:batch-delete-property-value [(stable-id-coll db block-eids)
|
||||
property-id'
|
||||
(stable-property-value db property-id' property-value)]])
|
||||
|
||||
:create-property-text-block
|
||||
(let [[block-id property-id value opts] args]
|
||||
[:create-property-text-block [(stable-entity-ref db block-id)
|
||||
(stable-entity-ref db property-id)
|
||||
value
|
||||
opts]])
|
||||
|
||||
:upsert-property
|
||||
(let [[property-id schema opts] args
|
||||
property-id' (or (stable-entity-ref db property-id)
|
||||
@@ -566,27 +511,6 @@
|
||||
(created-db-ident-from-tx-data tx-data))]
|
||||
[:upsert-property [property-id' schema opts]])
|
||||
|
||||
:class-add-property
|
||||
(let [[class-id property-id] args]
|
||||
[:class-add-property [(stable-entity-ref db class-id) (stable-entity-ref db property-id)]])
|
||||
|
||||
:class-remove-property
|
||||
(let [[class-id property-id] args]
|
||||
[:class-remove-property [(stable-entity-ref db class-id) (stable-entity-ref db property-id)]])
|
||||
|
||||
:upsert-closed-value
|
||||
(let [[property-id opts] args]
|
||||
[:upsert-closed-value [(stable-entity-ref db property-id) opts]])
|
||||
|
||||
:add-existing-values-to-closed-values
|
||||
(let [[property-id values] args]
|
||||
[:add-existing-values-to-closed-values [(stable-entity-ref db property-id) values]])
|
||||
|
||||
:delete-closed-value
|
||||
(let [[property-id value-block-id] args]
|
||||
[:delete-closed-value [(stable-entity-ref db property-id)
|
||||
(stable-block-ref-with-tx-data db tx-data value-block-id)]])
|
||||
|
||||
[op args]))
|
||||
|
||||
(defn- save-block-keys
|
||||
@@ -640,154 +564,6 @@
|
||||
keys-to-restore)]
|
||||
[:save-block [inverse-block opts]])))
|
||||
|
||||
(defn- property-ref-value
|
||||
[db property-id value]
|
||||
(let [property-type (some-> (d/entity db property-id) :logseq.property/type)]
|
||||
(cond
|
||||
;; Number property values are stored as ref entities but the semantic op
|
||||
;; uses scalar content for undo/redo payloads.
|
||||
(= :number property-type)
|
||||
(let [to-content (fn [v]
|
||||
(if (some? (:db/id v))
|
||||
(or (db-property/property-value-content v) v)
|
||||
v))]
|
||||
(cond
|
||||
(set? value) (set (map to-content value))
|
||||
(sequential? value) (mapv to-content value)
|
||||
:else (to-content value)))
|
||||
|
||||
(contains? db-property-type/all-ref-property-types property-type)
|
||||
(sanitize-ref-value db value)
|
||||
|
||||
:else
|
||||
value)))
|
||||
|
||||
(defn- block-property-value
|
||||
[db block-id property-id]
|
||||
;; `get` lookup may include derived defaults.
|
||||
(when-let [value (entity-plus/lookup-entity (d/entity db block-id) property-id)]
|
||||
(property-ref-value db property-id value)))
|
||||
|
||||
(defn- property-history-refs-from-tx-data
|
||||
[db-before db-after tx-data block-ids property-id]
|
||||
(let [target-block-ids (->> block-ids
|
||||
(keep (fn [block-id]
|
||||
(:db/id (block-entity db-before block-id))))
|
||||
set)
|
||||
target-property-id (some-> (d/entity db-before property-id) :db/id)
|
||||
history-eid->block-id (reduce (fn [acc d]
|
||||
(if (and (:added d)
|
||||
(= :logseq.property.history/block (:a d)))
|
||||
(assoc acc (:e d) (:v d))
|
||||
acc))
|
||||
{}
|
||||
tx-data)
|
||||
history-eid->property-id (reduce (fn [acc d]
|
||||
(if (and (:added d)
|
||||
(= :logseq.property.history/property (:a d)))
|
||||
(assoc acc (:e d) (:v d))
|
||||
acc))
|
||||
{}
|
||||
tx-data)]
|
||||
(->> history-eid->block-id
|
||||
(keep (fn [[history-eid history-block-id]]
|
||||
(when (and (contains? target-block-ids history-block-id)
|
||||
(= target-property-id (get history-eid->property-id history-eid))
|
||||
(nil? (d/entity db-before history-eid)))
|
||||
(stable-entity-ref db-after history-eid))))
|
||||
distinct
|
||||
vec
|
||||
seq)))
|
||||
|
||||
(defn- normalize-op-or-ops
|
||||
[op-or-ops]
|
||||
(cond
|
||||
(nil? op-or-ops)
|
||||
[]
|
||||
|
||||
(and (sequential? op-or-ops)
|
||||
(seq op-or-ops)
|
||||
(sequential? (first op-or-ops)))
|
||||
(vec op-or-ops)
|
||||
|
||||
:else
|
||||
[op-or-ops]))
|
||||
|
||||
(defn- prepend-history-cleanup-op
|
||||
[cleanup-op op-or-ops]
|
||||
(let [ops (normalize-op-or-ops op-or-ops)
|
||||
ops' (if cleanup-op
|
||||
(into [cleanup-op] ops)
|
||||
ops)]
|
||||
(seq ops')))
|
||||
|
||||
(defn- property-history-cleanup-op
|
||||
[db-before db-after tx-data block-ids property-id]
|
||||
(when-let [history-refs (property-history-refs-from-tx-data
|
||||
db-before
|
||||
db-after
|
||||
tx-data
|
||||
block-ids
|
||||
property-id)]
|
||||
[:delete-blocks [history-refs {}]]))
|
||||
|
||||
(defn- restore-property-op
|
||||
[before-value block-ref property-id {:keys [remove-when-nil?]}]
|
||||
(if (nil? before-value)
|
||||
(when remove-when-nil?
|
||||
[:remove-block-property [block-ref property-id]])
|
||||
[:set-block-property [block-ref property-id before-value]]))
|
||||
|
||||
(defn- inverse-property-ops-for-blocks
|
||||
[db-before block-ids property-id restore-opts]
|
||||
(->> block-ids
|
||||
(keep (fn [block-id]
|
||||
(let [before-value (block-property-value db-before block-id property-id)
|
||||
block-ref (stable-entity-ref db-before block-id)]
|
||||
(restore-property-op before-value block-ref property-id restore-opts))))
|
||||
vec
|
||||
seq))
|
||||
|
||||
(defn- inverse-property-change-op
|
||||
[db-before db-after tx-data block-ids property-id restore-opts]
|
||||
(let [cleanup-op (property-history-cleanup-op
|
||||
db-before
|
||||
db-after
|
||||
tx-data
|
||||
block-ids
|
||||
property-id)
|
||||
inverse-ops (inverse-property-ops-for-blocks
|
||||
db-before
|
||||
block-ids
|
||||
property-id
|
||||
restore-opts)]
|
||||
(prepend-history-cleanup-op cleanup-op inverse-ops)))
|
||||
|
||||
(defn- inverse-property-op
|
||||
[db-before db-after tx-data op args]
|
||||
(case op
|
||||
:set-block-property
|
||||
(let [[block-id property-id _value] args]
|
||||
(inverse-property-change-op
|
||||
db-before db-after tx-data [block-id] property-id {:remove-when-nil? true}))
|
||||
|
||||
:remove-block-property
|
||||
(let [[block-id property-id] args]
|
||||
(inverse-property-change-op
|
||||
db-before db-after tx-data [block-id] property-id {:remove-when-nil? false}))
|
||||
|
||||
:batch-set-property
|
||||
(let [[block-ids property-id _value _opts] args]
|
||||
(inverse-property-change-op
|
||||
db-before db-after tx-data block-ids property-id {:remove-when-nil? true}))
|
||||
|
||||
:batch-remove-property
|
||||
(let [[block-ids property-id _opts] args]
|
||||
(inverse-property-change-op
|
||||
db-before db-after tx-data block-ids property-id {:remove-when-nil? false}))
|
||||
|
||||
nil))
|
||||
|
||||
(defn- build-insert-block-payload
|
||||
[db-before ent]
|
||||
(when-let [block-uuid (:block/uuid ent)]
|
||||
@@ -1030,47 +806,17 @@
|
||||
(let [[page-uuid _opts] args]
|
||||
(build-inverse-delete-page db-before page-uuid))
|
||||
|
||||
:set-block-property
|
||||
(inverse-property-op db-before db-after tx-data op args)
|
||||
|
||||
:remove-block-property
|
||||
(inverse-property-op db-before db-after tx-data op args)
|
||||
|
||||
:batch-set-property
|
||||
(inverse-property-op db-before db-after tx-data op args)
|
||||
|
||||
:batch-remove-property
|
||||
(inverse-property-op db-before db-after tx-data op args)
|
||||
|
||||
:create-property-text-block
|
||||
(let [[_block-id _property-id _value opts] args
|
||||
new-block-id (:new-block-id opts)
|
||||
new-block-ref (cond
|
||||
(vector? new-block-id)
|
||||
new-block-id
|
||||
|
||||
(uuid? new-block-id)
|
||||
[:block/uuid new-block-id]
|
||||
|
||||
:else
|
||||
(stable-entity-ref db-before new-block-id))]
|
||||
(when new-block-ref
|
||||
[:delete-blocks [[new-block-ref] {}]]))
|
||||
|
||||
:class-add-property
|
||||
(let [[class-id property-id] args]
|
||||
[:class-remove-property [(stable-entity-ref db-before class-id)
|
||||
(stable-entity-ref db-before property-id)]])
|
||||
|
||||
:class-remove-property
|
||||
(let [[class-id property-id] args]
|
||||
[:class-add-property [(stable-entity-ref db-before class-id)
|
||||
(stable-entity-ref db-before property-id)]])
|
||||
|
||||
:upsert-property
|
||||
(let [[property-id _schema _opts] args]
|
||||
(when (qualified-keyword? property-id)
|
||||
[:delete-page [(common-uuid/gen-uuid :db-ident-block-uuid property-id) {}]]))
|
||||
(if-let [property (d/entity db-before property-id)]
|
||||
[:upsert-property
|
||||
[property-id
|
||||
(sanitize-upsert-property-schema
|
||||
db-before
|
||||
(db-property/get-property-schema (into {} property)))
|
||||
{:property-name (:block/title property)}]]
|
||||
[:delete-page [(common-uuid/gen-uuid :db-ident-block-uuid property-id) {}]])))
|
||||
|
||||
nil)]
|
||||
(if (and (sequential? inverse-entry)
|
||||
@@ -1098,29 +844,55 @@
|
||||
|
||||
(defn contains-transact-op?
|
||||
[ops]
|
||||
(some (fn [[op]]
|
||||
(= :transact op))
|
||||
ops))
|
||||
(let [ops' (cond
|
||||
(nil? ops)
|
||||
nil
|
||||
|
||||
(and (sequential? ops)
|
||||
(keyword? (first ops)))
|
||||
[(vec ops)]
|
||||
|
||||
:else
|
||||
ops)]
|
||||
(some (fn [entry]
|
||||
(and (sequential? entry)
|
||||
(= :transact (first entry))))
|
||||
ops')))
|
||||
|
||||
(defn- normalize-op-entries
|
||||
[ops]
|
||||
(let [ops' (some-> ops seq vec)]
|
||||
(cond
|
||||
(nil? ops')
|
||||
nil
|
||||
|
||||
(and (keyword? (first ops'))
|
||||
(vector? (second ops')))
|
||||
[ops']
|
||||
|
||||
:else
|
||||
ops')))
|
||||
|
||||
(defn- canonicalize-explicit-outliner-ops
|
||||
[db tx-data ops]
|
||||
(cond
|
||||
(nil? ops)
|
||||
(let [ops' (normalize-op-entries ops)]
|
||||
(cond
|
||||
(nil? ops')
|
||||
nil
|
||||
|
||||
(seq ops)
|
||||
(->> ops
|
||||
(mapcat (fn [op]
|
||||
(let [canonicalized-op (canonicalize-semantic-outliner-op db tx-data op)]
|
||||
(if (and (sequential? canonicalized-op)
|
||||
(sequential? (first canonicalized-op))
|
||||
(keyword? (ffirst canonicalized-op)))
|
||||
canonicalized-op
|
||||
[canonicalized-op]))))
|
||||
vec)
|
||||
(seq ops')
|
||||
(->> ops'
|
||||
(mapcat (fn [op]
|
||||
(let [canonicalized-op (canonicalize-semantic-outliner-op db tx-data op)]
|
||||
(if (and (sequential? canonicalized-op)
|
||||
(sequential? (first canonicalized-op))
|
||||
(keyword? (ffirst canonicalized-op)))
|
||||
canonicalized-op
|
||||
[canonicalized-op]))))
|
||||
vec)
|
||||
|
||||
:else
|
||||
nil))
|
||||
:else
|
||||
nil)))
|
||||
|
||||
(defn- patch-inverse-delete-block-ops
|
||||
[inverse-outliner-ops forward-outliner-ops]
|
||||
@@ -1157,8 +929,8 @@
|
||||
|
||||
(defn- canonicalize-outliner-ops
|
||||
[db tx-meta tx-data]
|
||||
(let [explicit-forward-ops (:db-sync/forward-outliner-ops tx-meta)
|
||||
outliner-ops (:outliner-ops tx-meta)]
|
||||
(let [explicit-forward-ops (normalize-op-entries (:db-sync/forward-outliner-ops tx-meta))
|
||||
outliner-ops (normalize-op-entries (:outliner-ops tx-meta))]
|
||||
(cond
|
||||
(seq explicit-forward-ops)
|
||||
(canonicalize-explicit-outliner-ops db tx-data explicit-forward-ops)
|
||||
@@ -1178,10 +950,6 @@
|
||||
(and (integer? x)
|
||||
(not (neg? x))))
|
||||
|
||||
(defn- unresolved-numeric-entity-id-coll?
|
||||
[xs]
|
||||
(some unresolved-numeric-entity-id? xs))
|
||||
|
||||
(defn- numeric-id-in-ref-value?
|
||||
[v]
|
||||
(cond
|
||||
@@ -1244,83 +1012,6 @@
|
||||
|
||||
nil))
|
||||
|
||||
(defn- stale-numeric-id-in-structure-ops?
|
||||
[op args]
|
||||
(case op
|
||||
:move-blocks
|
||||
(let [[ids target-id _opts] args]
|
||||
(or (unresolved-numeric-entity-id-coll? ids)
|
||||
(unresolved-numeric-entity-id? target-id)))
|
||||
|
||||
:move-blocks-up-down
|
||||
(let [[ids _up?] args]
|
||||
(unresolved-numeric-entity-id-coll? ids))
|
||||
|
||||
:indent-outdent-blocks
|
||||
(let [[ids _indent? _opts] args]
|
||||
(unresolved-numeric-entity-id-coll? ids))
|
||||
|
||||
:delete-blocks
|
||||
(let [[ids _opts] args]
|
||||
(unresolved-numeric-entity-id-coll? ids))
|
||||
|
||||
nil))
|
||||
|
||||
(defn- stale-numeric-id-in-property-ops?
|
||||
[op args]
|
||||
(case op
|
||||
:set-block-property
|
||||
(let [[block-id property-id _v] args]
|
||||
(or (unresolved-numeric-entity-id? block-id)
|
||||
(unresolved-numeric-entity-id? property-id)))
|
||||
|
||||
:remove-block-property
|
||||
(let [[block-id property-id] args]
|
||||
(or (unresolved-numeric-entity-id? block-id)
|
||||
(unresolved-numeric-entity-id? property-id)))
|
||||
|
||||
:batch-set-property
|
||||
(let [[block-ids property-id _v _opts] args]
|
||||
(or (unresolved-numeric-entity-id-coll? block-ids)
|
||||
(unresolved-numeric-entity-id? property-id)))
|
||||
|
||||
:batch-remove-property
|
||||
(let [[block-ids property-id] args]
|
||||
(or (unresolved-numeric-entity-id-coll? block-ids)
|
||||
(unresolved-numeric-entity-id? property-id)))
|
||||
|
||||
:delete-property-value
|
||||
(let [[block-id property-id _property-value] args]
|
||||
(or (unresolved-numeric-entity-id? block-id)
|
||||
(unresolved-numeric-entity-id? property-id)))
|
||||
|
||||
:batch-delete-property-value
|
||||
(let [[block-ids property-id _property-value] args]
|
||||
(or (unresolved-numeric-entity-id-coll? block-ids)
|
||||
(unresolved-numeric-entity-id? property-id)))
|
||||
|
||||
:create-property-text-block
|
||||
(let [[block-id property-id _value _opts] args]
|
||||
(or (unresolved-numeric-entity-id? block-id)
|
||||
(unresolved-numeric-entity-id? property-id)))
|
||||
|
||||
:class-add-property
|
||||
(let [[class-id property-id] args]
|
||||
(or (unresolved-numeric-entity-id? class-id)
|
||||
(unresolved-numeric-entity-id? property-id)))
|
||||
|
||||
:class-remove-property
|
||||
(let [[class-id property-id] args]
|
||||
(or (unresolved-numeric-entity-id? class-id)
|
||||
(unresolved-numeric-entity-id? property-id)))
|
||||
|
||||
:delete-closed-value
|
||||
(let [[property-id value-block-id] args]
|
||||
(or (unresolved-numeric-entity-id? property-id)
|
||||
(unresolved-numeric-entity-id? value-block-id)))
|
||||
|
||||
nil))
|
||||
|
||||
(defn- stale-numeric-id-in-schema-ops?
|
||||
[op args]
|
||||
(case op
|
||||
@@ -1328,14 +1019,6 @@
|
||||
(let [[property-id _schema _opts] args]
|
||||
(unresolved-numeric-entity-id? property-id))
|
||||
|
||||
:upsert-closed-value
|
||||
(let [[property-id _opts] args]
|
||||
(unresolved-numeric-entity-id? property-id))
|
||||
|
||||
:add-existing-values-to-closed-values
|
||||
(let [[property-id _values] args]
|
||||
(unresolved-numeric-entity-id? property-id))
|
||||
|
||||
nil))
|
||||
|
||||
(defn- stale-numeric-id-in-op?
|
||||
@@ -1343,8 +1026,6 @@
|
||||
(and (not= :transact op)
|
||||
(boolean
|
||||
(or (stale-numeric-id-in-page-ops? db op args)
|
||||
(stale-numeric-id-in-structure-ops? op args)
|
||||
(stale-numeric-id-in-property-ops? op args)
|
||||
(stale-numeric-id-in-schema-ops? op args)))))
|
||||
|
||||
(defn- assert-no-stale-numeric-ids!
|
||||
@@ -1400,8 +1081,8 @@
|
||||
(nil? explicit-inverse-outliner-ops)
|
||||
nil
|
||||
|
||||
;; Treat explicit transact placeholder as "no semantic inverse".
|
||||
;; Keep nil so semantic replay must fail-fast when required.
|
||||
;; Treat explicit transact placeholder as "no semantic inverse".
|
||||
;; Keep nil so semantic replay must fail-fast when required.
|
||||
(= canonical-transact-op explicit-inverse-outliner-ops)
|
||||
nil
|
||||
|
||||
|
||||
641
deps/outliner/src/logseq/outliner/property.cljs
vendored
641
deps/outliner/src/logseq/outliner/property.cljs
vendored
@@ -19,7 +19,6 @@
|
||||
[logseq.db.sqlite.util :as sqlite-util]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.page :as outliner-page]
|
||||
[logseq.outliner.tx-meta :as outliner-tx-meta]
|
||||
[logseq.outliner.validate :as outliner-validate]
|
||||
[malli.core :as m]
|
||||
[malli.error :as me]
|
||||
@@ -274,16 +273,6 @@
|
||||
[id]
|
||||
(if (uuid? id) [:block/uuid id] id))
|
||||
|
||||
(defn- with-op-entry
|
||||
[op-entry f]
|
||||
(binding [outliner-tx-meta/*outliner-op-entry*
|
||||
(or outliner-tx-meta/*outliner-op-entry* op-entry)]
|
||||
(f)))
|
||||
|
||||
(defn- transact-with-op!
|
||||
[conn tx-data tx-meta]
|
||||
(ldb/transact! conn tx-data (outliner-tx-meta/ensure-outliner-ops tx-meta nil)))
|
||||
|
||||
(defn- raw-set-block-property!
|
||||
"Adds the raw property pair (value not modified) to the given block if the property value is valid"
|
||||
[conn block property new-value]
|
||||
@@ -291,34 +280,35 @@
|
||||
(throw-error-if-invalid-property-value @conn property new-value)
|
||||
(let [property-id (:db/ident property)
|
||||
tx-data (build-property-value-tx-data conn block property-id new-value)]
|
||||
(transact-with-op! conn tx-data {:outliner-op :save-block})))
|
||||
(ldb/transact! conn tx-data {:outliner-op :save-block})))
|
||||
|
||||
(defn create-property-text-block!
|
||||
"Creates a property value block for the given property and value. Adds it to
|
||||
block if given block."
|
||||
[conn block-id property-id value {:keys [new-block-id]}]
|
||||
(with-op-entry
|
||||
[:create-property-text-block [block-id property-id value {:new-block-id new-block-id}]]
|
||||
(fn []
|
||||
(let [property (d/entity @conn property-id)
|
||||
block (when block-id (d/entity @conn block-id))
|
||||
_ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
|
||||
value' (convert-property-input-string (:logseq.property/type block)
|
||||
property value)
|
||||
_ (when (and (not= (:logseq.property/type property) :number)
|
||||
(not (string? value')))
|
||||
(throw (ex-info "value should be a string" {:block-id block-id
|
||||
:property-id property-id
|
||||
:value value'})))
|
||||
new-value-block (cond-> (db-property-build/build-property-value-block (or block property) property value')
|
||||
new-block-id
|
||||
(assoc :block/uuid new-block-id))]
|
||||
(transact-with-op! conn [new-value-block] {:outliner-op :insert-blocks})
|
||||
(let [property-id (:db/ident property)]
|
||||
(when (and property-id block)
|
||||
(when-let [block-id (:db/id (d/entity @conn [:block/uuid (:block/uuid new-value-block)]))]
|
||||
(raw-set-block-property! conn block property block-id)))
|
||||
(:block/uuid new-value-block))))))
|
||||
(let [property (d/entity @conn property-id)
|
||||
block (when block-id (d/entity @conn block-id))
|
||||
_ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
|
||||
value' (convert-property-input-string (:logseq.property/type block)
|
||||
property value)
|
||||
_ (when (and (not= (:logseq.property/type property) :number)
|
||||
(not (string? value')))
|
||||
(throw (ex-info "value should be a string" {:block-id block-id
|
||||
:property-id property-id
|
||||
:value value'})))
|
||||
new-value-block (cond-> (db-property-build/build-property-value-block (or block property) property value')
|
||||
new-block-id
|
||||
(assoc :block/uuid new-block-id))]
|
||||
(ldb/batch-transact-with-temp-conn!
|
||||
conn
|
||||
{:outliner-op :create-property-text-block}
|
||||
(fn [conn]
|
||||
(ldb/transact! conn [new-value-block] {:outliner-op :insert-blocks})
|
||||
(let [property-id (:db/ident property)]
|
||||
(when (and property-id block)
|
||||
(when-let [block-id (:db/id (d/entity @conn [:block/uuid (:block/uuid new-value-block)]))]
|
||||
(raw-set-block-property! conn block property block-id))))))
|
||||
(:block/uuid new-value-block)))
|
||||
|
||||
(defn- get-property-value-eid
|
||||
[db property-id raw-value]
|
||||
@@ -418,56 +408,54 @@
|
||||
|
||||
(defn batch-remove-property!
|
||||
[conn block-ids property-id]
|
||||
(with-op-entry
|
||||
[:batch-remove-property [block-ids property-id]]
|
||||
(fn []
|
||||
(throw-error-if-read-only-property property-id)
|
||||
(let [block-eids (map ->eid block-ids)
|
||||
blocks (keep (fn [id] (d/entity @conn id)) block-eids)
|
||||
block-id-set (set (map :db/id blocks))]
|
||||
(validate-batch-deletion-of-property blocks property-id)
|
||||
(when (seq blocks)
|
||||
(when-let [property (d/entity @conn property-id)]
|
||||
(let [txs (mapcat
|
||||
(fn [block]
|
||||
(let [value (get block property-id)
|
||||
entities (cond
|
||||
(de/entity? value) [value]
|
||||
(and (sequential? value) (every? de/entity? value)) value
|
||||
:else nil)
|
||||
deleting-entities (filter
|
||||
(fn [value]
|
||||
(let [value-referrers*
|
||||
(d/q '[:find [?e ...]
|
||||
:in $ ?property-id ?value-id
|
||||
:where
|
||||
[?e ?property-id ?value-id]]
|
||||
@conn
|
||||
(:db/ident property)
|
||||
(:db/id value))
|
||||
value-referrers
|
||||
(cond
|
||||
(nil? value-referrers*)
|
||||
#{}
|
||||
(throw-error-if-read-only-property property-id)
|
||||
(let [block-eids (map ->eid block-ids)
|
||||
blocks (keep (fn [id] (d/entity @conn id)) block-eids)
|
||||
block-id-set (set (map :db/id blocks))]
|
||||
(validate-batch-deletion-of-property blocks property-id)
|
||||
(when (seq blocks)
|
||||
(when-let [property (d/entity @conn property-id)]
|
||||
(let [txs (mapcat
|
||||
(fn [block]
|
||||
(let [value (get block property-id)
|
||||
entities (cond
|
||||
(de/entity? value) [value]
|
||||
(and (sequential? value) (every? de/entity? value)) value
|
||||
:else nil)
|
||||
deleting-entities (filter
|
||||
(fn [value]
|
||||
(let [value-referrers*
|
||||
(d/q '[:find [?e ...]
|
||||
:in $ ?property-id ?value-id
|
||||
:where
|
||||
[?e ?property-id ?value-id]]
|
||||
@conn
|
||||
(:db/ident property)
|
||||
(:db/id value))
|
||||
value-referrers
|
||||
(cond
|
||||
(nil? value-referrers*)
|
||||
#{}
|
||||
|
||||
(coll? value-referrers*)
|
||||
(set value-referrers*)
|
||||
(coll? value-referrers*)
|
||||
(set value-referrers*)
|
||||
|
||||
:else
|
||||
#{value-referrers*})]
|
||||
(and
|
||||
(:logseq.property/created-from-property value)
|
||||
(not (or (entity-util/page? value) (ldb/closed-value? value)))
|
||||
(empty? (set/difference value-referrers block-id-set)))))
|
||||
entities)
|
||||
retract-blocks-tx (when (seq deleting-entities)
|
||||
(:tx-data (outliner-core/delete-blocks @conn deleting-entities {})))]
|
||||
(concat
|
||||
[[:db/retract (:db/id block) (:db/ident property)]]
|
||||
retract-blocks-tx)))
|
||||
blocks)]
|
||||
(when (seq txs)
|
||||
(transact-with-op! conn txs {:outliner-op :save-block})))))))))
|
||||
:else
|
||||
#{value-referrers*})]
|
||||
(and
|
||||
(:logseq.property/created-from-property value)
|
||||
(not (or (entity-util/page? value) (ldb/closed-value? value)))
|
||||
(empty? (set/difference value-referrers block-id-set)))))
|
||||
entities)
|
||||
retract-blocks-tx (when (seq deleting-entities)
|
||||
(:tx-data (outliner-core/delete-blocks @conn deleting-entities {})))]
|
||||
(concat
|
||||
[[:db/retract (:db/id block) (:db/ident property)]]
|
||||
retract-blocks-tx)))
|
||||
blocks)]
|
||||
(when (seq txs)
|
||||
(ldb/transact! conn txs
|
||||
{:outliner-op :batch-remove-property})))))))
|
||||
|
||||
(defn batch-set-property!
|
||||
"Sets properties for multiple blocks. Automatically handles property value refs.
|
||||
@@ -475,96 +463,92 @@
|
||||
([conn block-ids property-id v]
|
||||
(batch-set-property! conn block-ids property-id v {}))
|
||||
([conn block-ids property-id v options]
|
||||
(with-op-entry
|
||||
[:batch-set-property [block-ids property-id v options]]
|
||||
(fn []
|
||||
(assert property-id "property-id is nil")
|
||||
(throw-error-if-read-only-property property-id)
|
||||
(if (nil? v)
|
||||
(batch-remove-property! conn block-ids property-id)
|
||||
(let [block-eids (map ->eid block-ids)
|
||||
_ (when (= property-id :block/tags)
|
||||
(outliner-validate/validate-tags-property @conn block-eids v))
|
||||
property (d/entity @conn property-id)
|
||||
_ (when (= (:db/ident property) :logseq.property.class/extends)
|
||||
(outliner-validate/validate-extends-property
|
||||
@conn
|
||||
(if (number? v) (d/entity @conn v) v)
|
||||
(map #(d/entity @conn %) block-eids)))
|
||||
_ (when (nil? property)
|
||||
(throw (ex-info (str "Property " property-id " doesn't exist yet") {:property-id property-id})))
|
||||
property-type (get property :logseq.property/type :default)
|
||||
entity-id? (and (:entity-id? options) (number? v))
|
||||
ref? (contains? db-property-type/all-ref-property-types property-type)
|
||||
default-url-not-closed? (and (contains? #{:default :url} property-type)
|
||||
(not (seq (entity-plus/lookup-kv-then-entity property :property/closed-values))))
|
||||
v' (if (and ref? (not entity-id?))
|
||||
(convert-ref-property-value conn property-id v property-type)
|
||||
v)
|
||||
_ (when (nil? v')
|
||||
(throw (ex-info "Property value must be not nil" {:v v})))
|
||||
txs (doall
|
||||
(mapcat
|
||||
(fn [eid]
|
||||
(if-let [block (d/entity @conn eid)]
|
||||
(let [v' (if (and default-url-not-closed?
|
||||
(not (and (keyword? v) entity-id?)))
|
||||
(do
|
||||
(when (number? v')
|
||||
(throw-error-if-invalid-property-value @conn property v'))
|
||||
(let [v (if (number? v') (:block/title (d/entity @conn v')) v')]
|
||||
(convert-ref-property-value conn property-id v property-type)))
|
||||
v')]
|
||||
(throw-error-if-self-value block v' ref?)
|
||||
(throw-error-if-invalid-property-value @conn property v')
|
||||
(build-property-value-tx-data conn block property-id v'))
|
||||
(js/console.error "Skipping setting a block's property because the block id could not be found:" eid)))
|
||||
block-eids))]
|
||||
(when (seq txs)
|
||||
(transact-with-op! conn txs {:outliner-op :save-block}))))))))
|
||||
(assert property-id "property-id is nil")
|
||||
(throw-error-if-read-only-property property-id)
|
||||
(if (nil? v)
|
||||
(batch-remove-property! conn block-ids property-id)
|
||||
(let [block-eids (map ->eid block-ids)
|
||||
_ (when (= property-id :block/tags)
|
||||
(outliner-validate/validate-tags-property @conn block-eids v))
|
||||
property (d/entity @conn property-id)
|
||||
blocks (keep #(d/entity @conn %) block-eids)
|
||||
_ (when (= (:db/ident property) :logseq.property.class/extends)
|
||||
(outliner-validate/validate-extends-property
|
||||
@conn
|
||||
(if (number? v) (d/entity @conn v) v)
|
||||
blocks))
|
||||
_ (when (nil? property)
|
||||
(throw (ex-info (str "Property " property-id " doesn't exist yet") {:property-id property-id})))
|
||||
property-type (get property :logseq.property/type :default)
|
||||
entity-id? (and (:entity-id? options) (number? v))
|
||||
ref? (contains? db-property-type/all-ref-property-types property-type)
|
||||
default-url-not-closed? (and (contains? #{:default :url} property-type)
|
||||
(not (seq (entity-plus/lookup-kv-then-entity property :property/closed-values))))
|
||||
v' (if (and ref? (not entity-id?))
|
||||
(convert-ref-property-value conn property-id v property-type)
|
||||
v)
|
||||
_ (when (nil? v')
|
||||
(throw (ex-info "Property value must be not nil" {:v v})))
|
||||
txs (doall
|
||||
(mapcat
|
||||
(fn [eid]
|
||||
(if-let [block (d/entity @conn eid)]
|
||||
(let [v' (if (and default-url-not-closed?
|
||||
(not (and (keyword? v) entity-id?)))
|
||||
(do
|
||||
(when (number? v')
|
||||
(throw-error-if-invalid-property-value @conn property v'))
|
||||
(let [v (if (number? v') (:block/title (d/entity @conn v')) v')]
|
||||
(convert-ref-property-value conn property-id v property-type)))
|
||||
v')]
|
||||
(throw-error-if-self-value block v' ref?)
|
||||
(throw-error-if-invalid-property-value @conn property v')
|
||||
(build-property-value-tx-data conn block property-id v'))
|
||||
(js/console.error "Skipping setting a block's property because the block id could not be found:" eid)))
|
||||
block-eids))]
|
||||
(when (seq txs)
|
||||
(ldb/transact! conn txs {:outliner-op :batch-set-property}))))))
|
||||
|
||||
(defn remove-block-property!
|
||||
[conn eid property-id]
|
||||
(with-op-entry
|
||||
[:remove-block-property [eid property-id]]
|
||||
(fn []
|
||||
(throw-error-if-read-only-property property-id)
|
||||
(let [eid (->eid eid)
|
||||
block (d/entity @conn eid)
|
||||
property (d/entity @conn property-id)]
|
||||
(when-not (= :logseq.property.class/extends property-id)
|
||||
(validate-batch-deletion-of-property [block] property-id))
|
||||
(when block
|
||||
(cond
|
||||
(= :logseq.property/empty-placeholder (:db/ident (get block property-id)))
|
||||
nil
|
||||
(throw-error-if-read-only-property property-id)
|
||||
(let [eid (->eid eid)
|
||||
block (d/entity @conn eid)
|
||||
property (d/entity @conn property-id)
|
||||
tx-meta {:outliner-op :remove-block-property}]
|
||||
(when-not (= :logseq.property.class/extends property-id)
|
||||
(validate-batch-deletion-of-property [block] property-id))
|
||||
(when block
|
||||
(cond
|
||||
(= :logseq.property/empty-placeholder (:db/ident (get block property-id)))
|
||||
nil
|
||||
|
||||
(= :logseq.property/status property-id)
|
||||
(transact-with-op! conn
|
||||
[[:db/retract (:db/id block) property-id]
|
||||
[:db/retract (:db/id block) :block/tags :logseq.class/Task]]
|
||||
{:outliner-op :save-block})
|
||||
(= :logseq.property/status property-id)
|
||||
(ldb/transact! conn
|
||||
[[:db/retract (:db/id block) property-id]
|
||||
[:db/retract (:db/id block) :block/tags :logseq.class/Task]]
|
||||
tx-meta)
|
||||
|
||||
(and (:logseq.property/default-value property)
|
||||
(= (:logseq.property/default-value property) (get block property-id)))
|
||||
(transact-with-op! conn
|
||||
[{:db/id (:db/id block)
|
||||
property-id :logseq.property/empty-placeholder}]
|
||||
{:outliner-op :save-block})
|
||||
(and (:logseq.property/default-value property)
|
||||
(= (:logseq.property/default-value property) (get block property-id)))
|
||||
(ldb/transact! conn
|
||||
[{:db/id (:db/id block)
|
||||
property-id :logseq.property/empty-placeholder}]
|
||||
tx-meta)
|
||||
|
||||
(and (ldb/class? block) (= property-id :logseq.property.class/extends))
|
||||
(transact-with-op! conn
|
||||
[[:db/retract (:db/id block) :logseq.property.class/extends]
|
||||
[:db/add (:db/id block) :logseq.property.class/extends :logseq.class/Root]]
|
||||
{:outliner-op :save-block})
|
||||
(and (ldb/class? block) (= property-id :logseq.property.class/extends))
|
||||
(ldb/transact! conn
|
||||
[[:db/retract (:db/id block) :logseq.property.class/extends]
|
||||
[:db/add (:db/id block) :logseq.property.class/extends :logseq.class/Root]]
|
||||
tx-meta)
|
||||
|
||||
(contains? db-property/db-attribute-properties property-id)
|
||||
(transact-with-op! conn
|
||||
[[:db/retract (:db/id block) property-id]]
|
||||
{:outliner-op :save-block})
|
||||
(contains? db-property/db-attribute-properties property-id)
|
||||
(ldb/transact! conn
|
||||
[[:db/retract (:db/id block) property-id]]
|
||||
tx-meta)
|
||||
|
||||
:else
|
||||
(batch-remove-property! conn [eid] property-id)))))))
|
||||
:else
|
||||
(batch-remove-property! conn [eid] property-id)))))
|
||||
|
||||
(defn- set-block-db-attribute!
|
||||
[conn db block property property-id v]
|
||||
@@ -574,8 +558,8 @@
|
||||
[{:db/id (:db/id block) property-id v}]
|
||||
(= property-id :logseq.property.class/extends)
|
||||
(conj [:db/retract (:db/id block) :logseq.property.class/extends :logseq.class/Root]))]
|
||||
(transact-with-op! conn tx-data
|
||||
{:outliner-op :save-block}))))
|
||||
(ldb/transact! conn tx-data
|
||||
{:outliner-op :save-block}))))
|
||||
|
||||
(defn ^:large-vars/cleanup-todo set-block-property!
|
||||
"Updates a block property's value for an existing property-id and block. If
|
||||
@@ -583,9 +567,10 @@
|
||||
can pass \"value\" instead of the property value entity. Also handle db
|
||||
attributes as properties"
|
||||
[conn block-eid property-id v]
|
||||
(with-op-entry
|
||||
[:set-block-property [block-eid property-id v]]
|
||||
(fn []
|
||||
(ldb/batch-transact-with-temp-conn!
|
||||
conn
|
||||
{:outliner-op :set-block-property}
|
||||
(fn [conn]
|
||||
(throw-error-if-read-only-property property-id)
|
||||
(let [db @conn
|
||||
block-eid (->eid block-eid)
|
||||
@@ -646,78 +631,73 @@
|
||||
with the given property-id or :property-name option. When a property is created
|
||||
it is ensured to have a unique :db/ident"
|
||||
[conn property-id schema {:keys [property-name properties] :as opts}]
|
||||
(with-op-entry
|
||||
[:upsert-property [property-id schema opts]]
|
||||
(fn []
|
||||
(let [db @conn
|
||||
db-ident (or property-id
|
||||
(try (db-property/create-user-property-ident-from-name property-name)
|
||||
(catch :default e
|
||||
(throw (ex-info (str e)
|
||||
{:type :notification
|
||||
:payload {:message "Property failed to create. Please try a different property name."
|
||||
:type :error}})))))]
|
||||
(assert (qualified-keyword? db-ident))
|
||||
(when (and (contains? #{:checkbox} (:logseq.property/type schema))
|
||||
(= :db.cardinality/many (:db/cardinality schema)))
|
||||
(throw (ex-info ":checkbox property doesn't allow multiple values"
|
||||
{:property-id property-id
|
||||
:schema schema})))
|
||||
(if-let [property (and (qualified-keyword? property-id) (d/entity db db-ident))]
|
||||
(update-property conn db-ident property schema opts)
|
||||
(let [k-name (or (and property-name (name property-name))
|
||||
(name property-id))
|
||||
db-ident' (db-ident/ensure-unique-db-ident @conn db-ident)]
|
||||
(assert (some? k-name)
|
||||
(prn "property-id: " property-id ", property-name: " property-name))
|
||||
(outliner-validate/validate-page-title k-name {:node {:db/ident db-ident'}})
|
||||
(outliner-validate/validate-page-title-characters k-name {:node {:db/ident db-ident'}})
|
||||
(let [db-id (:db/id properties)
|
||||
opts' (cond-> {:title k-name
|
||||
:properties properties}
|
||||
(integer? db-id)
|
||||
(assoc :block-uuid (:block/uuid (d/entity db db-id))))]
|
||||
(transact-with-op! conn
|
||||
(concat
|
||||
[(sqlite-util/build-new-property db-ident' schema opts')]
|
||||
(when db-id
|
||||
[[:db/retract db-id :block/tags :logseq.class/Page]]))
|
||||
{:outliner-op :upsert-property}))
|
||||
(d/entity @conn db-ident')))))))
|
||||
(let [db @conn
|
||||
db-ident (or property-id
|
||||
(try (db-property/create-user-property-ident-from-name property-name)
|
||||
(catch :default e
|
||||
(throw (ex-info (str e)
|
||||
{:type :notification
|
||||
:payload {:message "Property failed to create. Please try a different property name."
|
||||
:type :error}})))))]
|
||||
(assert (qualified-keyword? db-ident))
|
||||
(when (and (contains? #{:checkbox} (:logseq.property/type schema))
|
||||
(= :db.cardinality/many (:db/cardinality schema)))
|
||||
(throw (ex-info ":checkbox property doesn't allow multiple values"
|
||||
{:property-id property-id
|
||||
:schema schema})))
|
||||
(if-let [property (and (qualified-keyword? property-id) (d/entity db db-ident))]
|
||||
(update-property conn db-ident property schema opts)
|
||||
(let [k-name (or (and property-name (name property-name))
|
||||
(name property-id))
|
||||
db-ident' (db-ident/ensure-unique-db-ident @conn db-ident)]
|
||||
(assert (some? k-name)
|
||||
(prn "property-id: " property-id ", property-name: " property-name))
|
||||
(outliner-validate/validate-page-title k-name {:node {:db/ident db-ident'}})
|
||||
(outliner-validate/validate-page-title-characters k-name {:node {:db/ident db-ident'}})
|
||||
(let [db-id (:db/id properties)
|
||||
opts' (cond-> {:title k-name
|
||||
:properties properties}
|
||||
(integer? db-id)
|
||||
(assoc :block-uuid (:block/uuid (d/entity db db-id))))
|
||||
tx-data (concat
|
||||
[(sqlite-util/build-new-property db-ident' schema opts')]
|
||||
(when db-id
|
||||
[[:db/retract db-id :block/tags :logseq.class/Page]]))]
|
||||
(ldb/transact! conn tx-data
|
||||
{:outliner-op :upsert-property}))
|
||||
(d/entity @conn db-ident')))))
|
||||
|
||||
(defn batch-delete-property-value!
|
||||
"batch delete value when a property has multiple values"
|
||||
[conn block-eids property-id property-value]
|
||||
(with-op-entry
|
||||
[:batch-delete-property-value [block-eids property-id property-value]]
|
||||
(fn []
|
||||
(when-let [property (d/entity @conn property-id)]
|
||||
(when (and (db-property/many? property)
|
||||
(not (some #(= property-id (:db/ident (d/entity @conn %))) block-eids)))
|
||||
(when (= property-id :block/tags)
|
||||
(outliner-validate/validate-tags-property-deletion @conn block-eids property-value))
|
||||
(if (= property-id :block/tags)
|
||||
(let [tx-data (map (fn [id] [:db/retract id property-id property-value]) block-eids)]
|
||||
(transact-with-op! conn tx-data {:outliner-op :save-block}))
|
||||
(doseq [block-eid block-eids]
|
||||
(when-let [block (d/entity @conn block-eid)]
|
||||
(let [current-val (get block property-id)
|
||||
fv (first current-val)]
|
||||
(if (and (= 1 (count current-val))
|
||||
(or (= property-value fv)
|
||||
(= property-value (:db/id fv))))
|
||||
(remove-block-property! conn (:db/id block) property-id)
|
||||
(transact-with-op! conn
|
||||
[[:db/retract (:db/id block) property-id property-value]]
|
||||
{:outliner-op :save-block})))))))))))
|
||||
(ldb/batch-transact-with-temp-conn!
|
||||
conn
|
||||
{:outliner-op :batch-delete-property-value}
|
||||
(fn [conn]
|
||||
(when-let [property (d/entity @conn property-id)]
|
||||
(when (and (db-property/many? property)
|
||||
(not (some #(= property-id (:db/ident (d/entity @conn %))) block-eids)))
|
||||
(when (= property-id :block/tags)
|
||||
(outliner-validate/validate-tags-property-deletion @conn block-eids property-value))
|
||||
(if (= property-id :block/tags)
|
||||
(let [tx-data (map (fn [id] [:db/retract id property-id property-value]) block-eids)]
|
||||
(ldb/transact! conn tx-data {:outliner-op :save-block}))
|
||||
(doseq [block-eid block-eids]
|
||||
(when-let [block (d/entity @conn block-eid)]
|
||||
(let [current-val (get block property-id)
|
||||
fv (first current-val)]
|
||||
(if (and (= 1 (count current-val))
|
||||
(or (= property-value fv)
|
||||
(= property-value (:db/id fv))))
|
||||
(remove-block-property! conn (:db/id block) property-id)
|
||||
(ldb/transact! conn
|
||||
[[:db/retract (:db/id block) property-id property-value]]
|
||||
{:outliner-op :save-block})))))))))))
|
||||
|
||||
(defn delete-property-value!
|
||||
"Delete value if a property has multiple values"
|
||||
[conn block-eid property-id property-value]
|
||||
(with-op-entry
|
||||
[:delete-property-value [block-eid property-id property-value]]
|
||||
(fn []
|
||||
(batch-delete-property-value! conn [block-eid] property-id property-value))))
|
||||
(batch-delete-property-value! conn [block-eid] property-id property-value))
|
||||
|
||||
(defn ^:api get-classes-parents
|
||||
[tags]
|
||||
@@ -830,120 +810,115 @@
|
||||
(defn upsert-closed-value!
|
||||
"id should be a block UUID or nil"
|
||||
[conn property-id {:keys [id value description _scoped-class-id] :as opts}]
|
||||
(with-op-entry
|
||||
[:upsert-closed-value [property-id opts]]
|
||||
(fn []
|
||||
(assert (or (nil? id) (uuid? id)))
|
||||
(let [db @conn
|
||||
property (d/entity db property-id)
|
||||
property-type (:logseq.property/type property)]
|
||||
(when (contains? db-property-type/closed-value-property-types property-type)
|
||||
(let [value' (if (string? value) (string/trim value) value)
|
||||
resolved-value (convert-property-input-string nil property value')
|
||||
validate-message (validate-property-value-aux
|
||||
(get-property-value-schema @conn property-type property {:new-closed-value? true})
|
||||
resolved-value
|
||||
{:many? (db-property/many? property)})]
|
||||
(cond
|
||||
(some (fn [b]
|
||||
(and (= (str resolved-value) (str (or (db-property/closed-value-content b)
|
||||
(:block/uuid b))))
|
||||
(not= id (:block/uuid b))))
|
||||
(entity-plus/lookup-kv-then-entity property :property/closed-values))
|
||||
(throw (ex-info "Closed value choice already exists"
|
||||
{:error :value-exists
|
||||
:type :notification
|
||||
:payload {:message "Choice already exists"
|
||||
:type :warning}}))
|
||||
(assert (or (nil? id) (uuid? id)))
|
||||
(let [db @conn
|
||||
property (d/entity db property-id)
|
||||
property-type (:logseq.property/type property)]
|
||||
(when (contains? db-property-type/closed-value-property-types property-type)
|
||||
(let [value' (if (string? value) (string/trim value) value)
|
||||
resolved-value (convert-property-input-string nil property value')
|
||||
validate-message (validate-property-value-aux
|
||||
(get-property-value-schema @conn property-type property {:new-closed-value? true})
|
||||
resolved-value
|
||||
{:many? (db-property/many? property)})]
|
||||
(cond
|
||||
(some (fn [b]
|
||||
(and (= (str resolved-value) (str (or (db-property/closed-value-content b)
|
||||
(:block/uuid b))))
|
||||
(not= id (:block/uuid b))))
|
||||
(entity-plus/lookup-kv-then-entity property :property/closed-values))
|
||||
(throw (ex-info "Closed value choice already exists"
|
||||
{:error :value-exists
|
||||
:type :notification
|
||||
:payload {:message "Choice already exists"
|
||||
:type :warning}}))
|
||||
|
||||
validate-message
|
||||
(throw (ex-info "Invalid property value"
|
||||
{:error :value-invalid
|
||||
:type :notification
|
||||
:payload {:message validate-message
|
||||
:type :warning}}))
|
||||
validate-message
|
||||
(throw (ex-info "Invalid property value"
|
||||
{:error :value-invalid
|
||||
:type :notification
|
||||
:payload {:message validate-message
|
||||
:type :warning}}))
|
||||
|
||||
(nil? resolved-value)
|
||||
nil
|
||||
(nil? resolved-value)
|
||||
nil
|
||||
|
||||
:else
|
||||
(let [tx-data (build-closed-value-tx @conn property resolved-value opts)]
|
||||
(transact-with-op! conn tx-data {:outliner-op :insert-blocks})
|
||||
(when (seq description)
|
||||
(if-let [desc-ent (and id (:logseq.property/description (d/entity db [:block/uuid id])))]
|
||||
(transact-with-op! conn
|
||||
[(outliner-core/block-with-updated-at {:db/id (:db/id desc-ent)
|
||||
:block/title description})]
|
||||
{:outliner-op :save-block})
|
||||
(set-block-property! conn
|
||||
[:block/uuid (or id (:block/uuid (first tx-data)))]
|
||||
:logseq.property/description
|
||||
description)))))))))))
|
||||
:else
|
||||
(let [tx-data (build-closed-value-tx @conn property resolved-value opts)]
|
||||
(ldb/batch-transact-with-temp-conn!
|
||||
conn
|
||||
{:outliner-op :upsert-closed-value}
|
||||
(fn [conn]
|
||||
(ldb/transact! conn tx-data)
|
||||
(when (seq description)
|
||||
(if-let [desc-ent (and id (:logseq.property/description (d/entity db [:block/uuid id])))]
|
||||
(ldb/transact! conn
|
||||
[(outliner-core/block-with-updated-at {:db/id (:db/id desc-ent)
|
||||
:block/title description})])
|
||||
(set-block-property! conn
|
||||
[:block/uuid (or id (:block/uuid (first tx-data)))]
|
||||
:logseq.property/description
|
||||
description)))))))))))
|
||||
|
||||
(defn add-existing-values-to-closed-values!
|
||||
"Adds existing values as closed values and returns their new block uuids"
|
||||
[conn property-id values]
|
||||
(with-op-entry
|
||||
[:add-existing-values-to-closed-values [property-id values]]
|
||||
(fn []
|
||||
(when-let [property (d/entity @conn property-id)]
|
||||
(when (seq values)
|
||||
(let [values' (remove string/blank? values)]
|
||||
(assert (every? uuid? values') "existing values should all be UUIDs")
|
||||
(let [values (keep #(d/entity @conn [:block/uuid %]) values')]
|
||||
(when (seq values)
|
||||
(let [value-property-tx (map (fn [id]
|
||||
{:db/id id
|
||||
:block/closed-value-property (:db/id property)})
|
||||
(map :db/id values))
|
||||
property-tx (outliner-core/block-with-updated-at {:db/id (:db/id property)})]
|
||||
(transact-with-op! conn (cons property-tx value-property-tx)
|
||||
{:outliner-op :save-blocks}))))))))))
|
||||
(when-let [property (d/entity @conn property-id)]
|
||||
(when (seq values)
|
||||
(let [values' (remove string/blank? values)]
|
||||
(assert (every? uuid? values') "existing values should all be UUIDs")
|
||||
(let [values (keep #(d/entity @conn [:block/uuid %]) values')]
|
||||
(when (seq values)
|
||||
(let [value-property-tx (map (fn [id]
|
||||
{:db/id id
|
||||
:block/closed-value-property (:db/id property)})
|
||||
(map :db/id values))
|
||||
property-tx (outliner-core/block-with-updated-at {:db/id (:db/id property)})]
|
||||
(ldb/transact! conn (cons property-tx value-property-tx)
|
||||
{:outliner-op :add-existing-values-to-closed-values}))))))))
|
||||
|
||||
(defn delete-closed-value!
|
||||
"Returns true when deleted or if not deleted displays warning and returns false"
|
||||
[conn property-id value-block-id]
|
||||
(with-op-entry
|
||||
[:delete-closed-value [property-id value-block-id]]
|
||||
(fn []
|
||||
(when (or (nil? property-id)
|
||||
(nil? value-block-id))
|
||||
(throw (ex-info "empty property-id or value-block-id when delete-closed-value!"
|
||||
{:property-id property-id
|
||||
:value-block-id value-block-id})))
|
||||
(when-let [value-block (d/entity @conn value-block-id)]
|
||||
(if (ldb/built-in? value-block)
|
||||
(throw (ex-info "The choice can't be deleted"
|
||||
{:type :notification
|
||||
:payload {:message "The choice can't be deleted because it's built-in."
|
||||
:type :warning}}))
|
||||
(let [tx-data (conj (:tx-data (outliner-core/delete-blocks @conn [value-block] {}))
|
||||
(outliner-core/block-with-updated-at {:db/id property-id}))]
|
||||
(transact-with-op! conn tx-data {})))))))
|
||||
(when (or (nil? property-id)
|
||||
(nil? value-block-id))
|
||||
(throw (ex-info "empty property-id or value-block-id when delete-closed-value!"
|
||||
{:property-id property-id
|
||||
:value-block-id value-block-id})))
|
||||
(let [property (d/entity @conn property-id)]
|
||||
(when-not (ldb/property? property)
|
||||
(throw (ex-info "Invalid property" {:property-id property-id})))
|
||||
(when-let [value-block (d/entity @conn value-block-id)]
|
||||
(if (ldb/built-in? value-block)
|
||||
(throw (ex-info "The choice can't be deleted"
|
||||
{:type :notification
|
||||
:payload {:message "The choice can't be deleted because it's built-in."
|
||||
:type :warning}}))
|
||||
(let [tx-data (conj (:tx-data (outliner-core/delete-blocks @conn [value-block] {}))
|
||||
(outliner-core/block-with-updated-at {:db/id property-id}))]
|
||||
(ldb/transact! conn tx-data
|
||||
{:outliner-op :delete-closed-value}))))))
|
||||
|
||||
(defn class-add-property!
|
||||
[conn class-id property-id]
|
||||
(with-op-entry
|
||||
[:class-add-property [class-id property-id]]
|
||||
(fn []
|
||||
(when-not (contains? #{:logseq.property/empty-placeholder} property-id)
|
||||
(when-let [class (d/entity @conn class-id)]
|
||||
(if (ldb/class? class)
|
||||
(transact-with-op! conn
|
||||
[[:db/add (:db/id class) :logseq.property.class/properties property-id]]
|
||||
{:outliner-op :save-block})
|
||||
(throw (ex-info "Can't add a property to a block that isn't a class"
|
||||
{:class-id class-id :property-id property-id}))))))))
|
||||
(when-not (contains? #{:logseq.property/empty-placeholder} property-id)
|
||||
(when-let [class (d/entity @conn class-id)]
|
||||
(if (ldb/class? class)
|
||||
(when-let [property (d/entity @conn property-id)]
|
||||
(when (ldb/property? property)
|
||||
(ldb/transact! conn
|
||||
[[:db/add (:db/id class) :logseq.property.class/properties property-id]]
|
||||
{:outliner-op :class-add-property})))
|
||||
(throw (ex-info "Can't add a property to a block that isn't a class"
|
||||
{:class-id class-id :property-id property-id}))))))
|
||||
|
||||
(defn class-remove-property!
|
||||
[conn class-id property-id]
|
||||
(with-op-entry
|
||||
[:class-remove-property [class-id property-id]]
|
||||
(fn []
|
||||
(when-let [class (d/entity @conn class-id)]
|
||||
(when (ldb/class? class)
|
||||
(when-let [property (d/entity @conn property-id)]
|
||||
(when-not (ldb/built-in-class-property? class property)
|
||||
(transact-with-op! conn
|
||||
[[:db/retract (:db/id class) :logseq.property.class/properties property-id]]
|
||||
{:outliner-op :save-block}))))))))
|
||||
(when-let [class (d/entity @conn class-id)]
|
||||
(when (ldb/class? class)
|
||||
(when-let [property (d/entity @conn property-id)]
|
||||
(when (ldb/property? property)
|
||||
(when-not (ldb/built-in-class-property? class property)
|
||||
(ldb/transact! conn
|
||||
[[:db/retract (:db/id class) :logseq.property.class/properties property-id]]
|
||||
{:outliner-op :class-remove-property})))))))
|
||||
|
||||
11
deps/outliner/src/logseq/outliner/tx_meta.cljs
vendored
11
deps/outliner/src/logseq/outliner/tx_meta.cljs
vendored
@@ -1,11 +1,8 @@
|
||||
(ns logseq.outliner.tx-meta
|
||||
"Helpers for normalizing tx metadata with explicit outliner op entries.")
|
||||
|
||||
(def ^:dynamic *outliner-op-entry* nil)
|
||||
|
||||
(defn ensure-outliner-ops
|
||||
[tx-meta fallback-op-entry]
|
||||
(let [entry (or *outliner-op-entry* fallback-op-entry)]
|
||||
(cond-> (or tx-meta {})
|
||||
(and entry (nil? (:outliner-ops tx-meta)))
|
||||
(assoc :outliner-ops [entry]))))
|
||||
[tx-meta entry]
|
||||
(cond-> tx-meta
|
||||
(and entry (nil? (:outliner-ops tx-meta)))
|
||||
(assoc :outliner-ops [entry])))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.common.uuid :as common-uuid]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.op.construct :as op-construct]))
|
||||
@@ -98,6 +99,39 @@
|
||||
(is (= [[:delete-page [expected-page-uuid {}]]]
|
||||
inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-upsert-property-update-builds-schema-restore-inverse-test
|
||||
(testing "upsert-property on existing property builds inverse upsert-property restore op"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:p-many {:logseq.property/type :default}}
|
||||
:pages-and-blocks []})
|
||||
property-id :user.property/p-many
|
||||
_ (d/transact! conn [[:db/add property-id :logseq.property/classes :logseq.class/Root]])
|
||||
before-property (d/entity @conn property-id)
|
||||
expected-schema (-> (db-property/get-property-schema (into {} before-property))
|
||||
(update :logseq.property/classes
|
||||
(fn [classes]
|
||||
(some->> classes
|
||||
(map (fn [class]
|
||||
(if-let [class-uuid (:block/uuid class)]
|
||||
[:block/uuid class-uuid]
|
||||
(:db/ident class))))
|
||||
set))))
|
||||
tx-meta {:outliner-op :upsert-property
|
||||
:outliner-ops [[:upsert-property [property-id
|
||||
{:logseq.property/type :node
|
||||
:db/cardinality :many}
|
||||
{}]]]}
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)]
|
||||
(is (= [[:upsert-property [property-id expected-schema {:property-name "p-many"}]]]
|
||||
inverse-outliner-ops))
|
||||
(is (every? (fn [class-ref]
|
||||
(or (keyword? class-ref)
|
||||
(and (vector? class-ref)
|
||||
(= :block/uuid (first class-ref))
|
||||
(uuid? (second class-ref)))))
|
||||
(get-in inverse-outliner-ops [0 1 1 :logseq.property/classes]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-delete-blocks-inverse-avoids-self-target-test
|
||||
(testing "delete-blocks inverse falls back to parent target when left sibling resolves to self"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
@@ -123,19 +157,22 @@
|
||||
(is (not= [:block/uuid child-uuid]
|
||||
(get-in insert-op [1 1]))))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-delete-blocks-with-stale-id-throws-test
|
||||
(testing "delete-blocks derive-history throws when semantic ids include numeric db/id"
|
||||
(deftest derive-history-outliner-ops-delete-blocks-with-stale-id-keeps-id-test
|
||||
(testing "delete-blocks derive-history keeps unresolved numeric ids in forward ops"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
child-uuid (:block/uuid child)
|
||||
stale-id 99999999
|
||||
tx-meta {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:delete-blocks [[(:db/id child) stale-id] {}]]]}]
|
||||
(is (thrown? js/Error
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta))))))
|
||||
:outliner-ops [[:delete-blocks [[(:db/id child) stale-id] {}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)]
|
||||
(is (= [[:delete-blocks [[[:block/uuid child-uuid] stale-id] {}]]]
|
||||
forward-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-delete-blocks-prefers-retracted-tx-data-ids-test
|
||||
(testing "delete-blocks derive-history should prefer tx-data retractEntity ids over stale selection ids"
|
||||
@@ -175,7 +212,7 @@
|
||||
(get-in forward-outliner-ops [0 1 1]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-delete-closed-value-resolves-value-id-from-tx-data-test
|
||||
(testing "delete-closed-value should resolve stale numeric value block id using tx-data block uuid"
|
||||
(testing "delete-closed-value is treated as raw transact placeholder"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:status {:logseq.property/type :default}}
|
||||
:pages-and-blocks []})
|
||||
@@ -188,8 +225,8 @@
|
||||
:outliner-ops [[:delete-closed-value [property-id stale-value-id]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn tx-data tx-meta)]
|
||||
(is (= [:block/uuid value-uuid]
|
||||
(get-in forward-outliner-ops [0 1 1]))))))
|
||||
(is (= op-construct/canonical-transact-op
|
||||
forward-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-builds-delete-page-inverse-for-class-property-and-today-page-test
|
||||
(testing "delete-page inverse restores hard-retracted class/property/today pages with stable db/ident"
|
||||
@@ -254,10 +291,7 @@
|
||||
prop-block-1 (db-test/find-block-by-content @conn "prop-block-1")
|
||||
prop-block-2 (db-test/find-block-by-content @conn "prop-block-2")
|
||||
class-id (:db/id (d/entity @conn :user.class/c1))
|
||||
class-uuid (:block/uuid (d/entity @conn class-id))
|
||||
property-id (:db/id (d/entity @conn :user.property/p1))
|
||||
property-page-uuid (:block/uuid (d/entity @conn property-id))
|
||||
prop-value-1-id (:db/id (:user.property/p1 prop-block-1))]
|
||||
property-id (:db/id (d/entity @conn :user.property/p1))]
|
||||
(testing ":save-block"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@@ -322,32 +356,26 @@
|
||||
inverse-outliner-ops))))
|
||||
|
||||
(testing ":set-block-property"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(let [{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :set-block-property
|
||||
:outliner-ops [[:set-block-property [(:db/id prop-block-1)
|
||||
:user.property/p1
|
||||
"new-value"]]]})]
|
||||
(is (= :set-block-property (ffirst inverse-outliner-ops)))
|
||||
(is (= [:block/uuid (:block/uuid prop-block-1)]
|
||||
(get-in inverse-outliner-ops [0 1 0])))
|
||||
(is (= :user.property/p1 (get-in inverse-outliner-ops [0 1 1])))
|
||||
(is (= prop-value-1-id (get-in inverse-outliner-ops [0 1 2 :db/id])))))
|
||||
(is (= op-construct/canonical-transact-op forward-outliner-ops))
|
||||
(is (nil? inverse-outliner-ops))))
|
||||
|
||||
(testing ":remove-block-property"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(let [{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :remove-block-property
|
||||
:outliner-ops [[:remove-block-property [(:db/id prop-block-1)
|
||||
:user.property/p1]]]})]
|
||||
(is (= :set-block-property (ffirst inverse-outliner-ops)))
|
||||
(is (= [:block/uuid (:block/uuid prop-block-1)]
|
||||
(get-in inverse-outliner-ops [0 1 0])))
|
||||
(is (= :user.property/p1 (get-in inverse-outliner-ops [0 1 1])))
|
||||
(is (= prop-value-1-id (get-in inverse-outliner-ops [0 1 2 :db/id])))))
|
||||
(is (= op-construct/canonical-transact-op forward-outliner-ops))
|
||||
(is (nil? inverse-outliner-ops))))
|
||||
|
||||
(testing ":batch-set-property"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(let [{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :batch-set-property
|
||||
:outliner-ops [[:batch-set-property [[(:db/id prop-block-1)
|
||||
@@ -355,41 +383,34 @@
|
||||
:user.property/p1
|
||||
"new-value"
|
||||
{}]]]})]
|
||||
(is (= 2 (count inverse-outliner-ops)))
|
||||
(is (= :set-block-property (ffirst inverse-outliner-ops)))
|
||||
(is (= :remove-block-property (ffirst (rest inverse-outliner-ops))))))
|
||||
(is (= op-construct/canonical-transact-op forward-outliner-ops))
|
||||
(is (nil? inverse-outliner-ops))))
|
||||
|
||||
(testing ":batch-remove-property"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(let [{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :batch-remove-property
|
||||
:outliner-ops [[:batch-remove-property [[(:db/id prop-block-1)
|
||||
(:db/id prop-block-2)]
|
||||
:user.property/p1]]]})]
|
||||
(is (= 1 (count inverse-outliner-ops)))
|
||||
(is (= :set-block-property (ffirst inverse-outliner-ops)))
|
||||
(is (= [:block/uuid (:block/uuid prop-block-1)]
|
||||
(get-in inverse-outliner-ops [0 1 0])))
|
||||
(is (= :user.property/p1 (get-in inverse-outliner-ops [0 1 1])))
|
||||
(is (= prop-value-1-id (get-in inverse-outliner-ops [0 1 2 :db/id])))))
|
||||
(is (= op-construct/canonical-transact-op forward-outliner-ops))
|
||||
(is (nil? inverse-outliner-ops))))
|
||||
|
||||
(testing ":class-add-property"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(let [{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :class-add-property
|
||||
:outliner-ops [[:class-add-property [class-id property-id]]]})]
|
||||
(is (= [[:class-remove-property [[:block/uuid class-uuid]
|
||||
[:block/uuid property-page-uuid]]]]
|
||||
inverse-outliner-ops))))
|
||||
(is (= op-construct/canonical-transact-op forward-outliner-ops))
|
||||
(is (nil? inverse-outliner-ops))))
|
||||
|
||||
(testing ":class-remove-property"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(let [{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :class-remove-property
|
||||
:outliner-ops [[:class-remove-property [class-id property-id]]]})]
|
||||
(is (= [[:class-add-property [[:block/uuid class-uuid]
|
||||
[:block/uuid property-page-uuid]]]]
|
||||
inverse-outliner-ops))))
|
||||
(is (= op-construct/canonical-transact-op forward-outliner-ops))
|
||||
(is (nil? inverse-outliner-ops))))
|
||||
|
||||
(testing ":upsert-property"
|
||||
(let [property-ident :user.property/test-inverse
|
||||
@@ -433,7 +454,7 @@
|
||||
inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-property-history-blocks-undo-cleanup-test
|
||||
(testing ":set-block-property inverse deletes newly created property history blocks"
|
||||
(testing ":set-block-property now falls back to transact placeholder history"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:pnum {:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/one}}
|
||||
@@ -443,7 +464,6 @@
|
||||
:build/properties {:pnum 1}}]}]})
|
||||
block (db-test/find-block-by-content @conn "task")
|
||||
block-id (:db/id block)
|
||||
block-ref [:block/uuid (:block/uuid block)]
|
||||
property-id (:db/id (d/entity @conn :user.property/pnum))
|
||||
history-uuid (random-uuid)
|
||||
{:keys [db-after tx-data]}
|
||||
@@ -455,20 +475,17 @@
|
||||
:logseq.property.history/property property-id
|
||||
:logseq.property.history/scalar-value 2}]
|
||||
{})
|
||||
{:keys [inverse-outliner-ops]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn
|
||||
db-after
|
||||
tx-data
|
||||
{:outliner-op :set-block-property
|
||||
:outliner-ops [[:set-block-property [block-id :user.property/pnum 2]]]})]
|
||||
(is (= :delete-blocks (ffirst inverse-outliner-ops)))
|
||||
(is (= #{[:block/uuid history-uuid]}
|
||||
(set (get-in inverse-outliner-ops [0 1 0]))))
|
||||
(is (= [:set-block-property [block-ref :user.property/pnum 1]]
|
||||
(second inverse-outliner-ops)))))
|
||||
(is (= op-construct/canonical-transact-op forward-outliner-ops))
|
||||
(is (nil? inverse-outliner-ops))))
|
||||
|
||||
(testing ":batch-set-property inverse deletes all newly created property history blocks"
|
||||
(testing ":batch-set-property now falls back to transact placeholder history"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:pnum {:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/one}}
|
||||
@@ -481,8 +498,6 @@
|
||||
block-2 (db-test/find-block-by-content @conn "task-2")
|
||||
block-1-id (:db/id block-1)
|
||||
block-2-id (:db/id block-2)
|
||||
block-1-ref [:block/uuid (:block/uuid block-1)]
|
||||
block-2-ref [:block/uuid (:block/uuid block-2)]
|
||||
property-id (:db/id (d/entity @conn :user.property/pnum))
|
||||
history-uuid-1 (random-uuid)
|
||||
history-uuid-2 (random-uuid)
|
||||
@@ -501,7 +516,7 @@
|
||||
:logseq.property.history/property property-id
|
||||
:logseq.property.history/scalar-value 2}]
|
||||
{})
|
||||
{:keys [inverse-outliner-ops]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn
|
||||
db-after
|
||||
@@ -511,14 +526,8 @@
|
||||
:user.property/pnum
|
||||
2
|
||||
{}]]]})]
|
||||
(is (= :delete-blocks (ffirst inverse-outliner-ops)))
|
||||
(is (= #{[:block/uuid history-uuid-1]
|
||||
[:block/uuid history-uuid-2]}
|
||||
(set (get-in inverse-outliner-ops [0 1 0]))))
|
||||
(is (= [:set-block-property [block-1-ref :user.property/pnum 1]]
|
||||
(second inverse-outliner-ops)))
|
||||
(is (= [:remove-block-property [block-2-ref :user.property/pnum]]
|
||||
(nth inverse-outliner-ops 2))))))
|
||||
(is (= op-construct/canonical-transact-op forward-outliner-ops))
|
||||
(is (nil? inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-direct-outdent-with-extra-moved-blocks-keeps-semantic-ops-test
|
||||
(testing "direct outdent keeps semantic indent-outdent op and inverse"
|
||||
|
||||
@@ -300,8 +300,7 @@
|
||||
{:id :selection-action-bar
|
||||
:root-props {:modal false}
|
||||
:content-props {:side "top"
|
||||
:class "!py-0 !px-0 !border-none"
|
||||
:modal? false}
|
||||
:class "!py-0 !px-0 !border-none"}
|
||||
:auto-side? false
|
||||
:align :start}))))
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db-sync.order :as sync-order]
|
||||
[logseq.db.common.normalize :as db-normalize]
|
||||
[logseq.db.frontend.property.type :as db-property-type]
|
||||
[logseq.db.sqlite.util :as sqlite-util]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.op :as outliner-op]
|
||||
@@ -198,43 +197,38 @@
|
||||
;; Keep them as raw-tx placeholders instead of forcing semantic canonicalization.
|
||||
(if (explicit-transact-forward-op? tx-meta)
|
||||
{:forward-outliner-ops canonical-transact-op
|
||||
:inverse-outliner-ops (or (seq (:db-sync/inverse-outliner-ops tx-meta))
|
||||
(seq (:inverse-outliner-ops tx-meta)))}
|
||||
:inverse-outliner-ops (or (some-> (:db-sync/inverse-outliner-ops tx-meta) seq vec)
|
||||
(some-> (:inverse-outliner-ops tx-meta) seq vec))}
|
||||
(op-construct/derive-history-outliner-ops db-before db-after tx-data tx-meta)))
|
||||
|
||||
(defn- title-only-raw-tx?
|
||||
[tx-data]
|
||||
(let [tx-items (seq tx-data)]
|
||||
(and tx-items
|
||||
(every?
|
||||
(fn [entry]
|
||||
(and (vector? entry)
|
||||
(>= (count entry) 4)
|
||||
(= :db/add (first entry))
|
||||
(= :block/title (nth entry 2))
|
||||
(string? (nth entry 3))))
|
||||
tx-items))))
|
||||
|
||||
(defn- rebase-history-ops
|
||||
[local-tx]
|
||||
(let [forward-outliner-ops (seq (:forward-outliner-ops local-tx))
|
||||
inverse-outliner-ops (seq (:inverse-outliner-ops local-tx))
|
||||
;; Fall back to raw tx replay for legacy rebase rows that persisted without
|
||||
;; semantic history ops, and for direct title-only transact rows whose
|
||||
;; metadata doesn't carry semantic ops. Keep other non-rebase rows as-is
|
||||
;; to avoid replaying arbitrary local raw txs during remote rebase.
|
||||
fallback-forward-ops (when (and (nil? forward-outliner-ops)
|
||||
(or (= :rebase (:outliner-op local-tx))
|
||||
(and (nil? (:outliner-op local-tx))
|
||||
(title-only-raw-tx? (:tx local-tx))))
|
||||
(seq (:tx local-tx)))
|
||||
canonical-transact-op)
|
||||
forward-ops (or forward-outliner-ops fallback-forward-ops)
|
||||
forward-ops (or forward-outliner-ops
|
||||
(when (seq (:tx local-tx))
|
||||
canonical-transact-op))
|
||||
inverse-ops (or inverse-outliner-ops
|
||||
(when (seq (:reversed-tx local-tx))
|
||||
canonical-transact-op)
|
||||
(when forward-ops canonical-transact-op))]
|
||||
{:forward-ops forward-ops
|
||||
:inverse-ops inverse-ops}))
|
||||
|
||||
(defn- semantic-op-entry?
|
||||
[op-entry]
|
||||
(and (sequential? op-entry)
|
||||
(contains? op-construct/semantic-outliner-ops (first op-entry))))
|
||||
|
||||
(defn- normalize-tx-data-for-rebase
|
||||
[tx-data]
|
||||
(some->> tx-data
|
||||
(mapv (fn [item]
|
||||
(if (and (vector? item) (= 5 (count item)))
|
||||
(let [[op e a v _t] item]
|
||||
[op e a v])
|
||||
item)))))
|
||||
|
||||
(defn- inferred-outliner-ops?
|
||||
[tx-meta]
|
||||
(and (nil? (:outliner-ops tx-meta))
|
||||
@@ -324,24 +318,8 @@
|
||||
[repo]
|
||||
(mark-pending-txs-false! repo (mapv :tx-id (pending-txs repo))))
|
||||
|
||||
(defn- usable-history-ops
|
||||
[ops]
|
||||
(let [ops' (some-> ops seq vec)]
|
||||
(when (and (seq ops')
|
||||
(not= canonical-transact-op ops'))
|
||||
ops')))
|
||||
|
||||
(defn- semantic-op-stream?
|
||||
[ops]
|
||||
(boolean (seq (usable-history-ops ops))))
|
||||
|
||||
(defn- history-action-ops
|
||||
[{:keys [forward-outliner-ops inverse-outliner-ops]} undo?]
|
||||
(if undo?
|
||||
(usable-history-ops inverse-outliner-ops)
|
||||
(usable-history-ops forward-outliner-ops)))
|
||||
|
||||
(declare replay-canonical-outliner-op!)
|
||||
(declare replay-canonical-outliner-op!
|
||||
history-action-error-reason)
|
||||
|
||||
(defn- inline-history-action
|
||||
[tx-meta]
|
||||
@@ -356,103 +334,67 @@
|
||||
|
||||
(defn ^:large-vars/cleanup-todo apply-history-action!
|
||||
[repo tx-id undo? tx-meta]
|
||||
(let [debug-data {:tx-id tx-id
|
||||
:undo? undo?
|
||||
:tx-meta tx-meta}]
|
||||
(if-let [conn (worker-state/get-datascript-conn repo)]
|
||||
(if-let [action (or (pending-tx-by-id repo tx-id)
|
||||
(inline-history-action tx-meta))]
|
||||
(let [semantic-forward? (semantic-op-stream? (:forward-outliner-ops action))
|
||||
ops (history-action-ops action undo?)
|
||||
history-tx-id (let [provided-history-tx-id (:db-sync/tx-id tx-meta)]
|
||||
(if (and (uuid? provided-history-tx-id)
|
||||
(not= provided-history-tx-id tx-id))
|
||||
provided-history-tx-id
|
||||
(random-uuid)))
|
||||
tx-meta' (cond-> {:local-tx? true
|
||||
:gen-undo-ops? false
|
||||
:persist-op? true
|
||||
:undo? undo?
|
||||
:redo? (:redo? tx-meta)
|
||||
:db-sync/tx-id history-tx-id
|
||||
:db-sync/source-tx-id (or (:db-sync/source-tx-id tx-meta)
|
||||
tx-id)}
|
||||
|
||||
(:outliner-op action)
|
||||
(assoc :outliner-op (:outliner-op action))
|
||||
|
||||
(seq (if undo? (:inverse-outliner-ops action)
|
||||
(:forward-outliner-ops action)))
|
||||
(assoc :db-sync/forward-outliner-ops
|
||||
(vec (if undo? (:inverse-outliner-ops action)
|
||||
(:forward-outliner-ops action))))
|
||||
|
||||
(seq (if undo? (:forward-outliner-ops action)
|
||||
(:inverse-outliner-ops action)))
|
||||
(assoc :db-sync/inverse-outliner-ops
|
||||
(vec (if undo? (:forward-outliner-ops action)
|
||||
(:inverse-outliner-ops action)))))]
|
||||
;; (prn :debug :outliner-ops)
|
||||
;; (pprint/pprint (select-keys action [:tx-id :outliner-op :forward-outliner-ops :inverse-outliner-ops]))
|
||||
(if-let [conn (worker-state/get-datascript-conn repo)]
|
||||
(if-let [action (or (pending-tx-by-id repo tx-id)
|
||||
(inline-history-action tx-meta))]
|
||||
(let [{:keys [tx reversed-tx forward-outliner-ops inverse-outliner-ops]} action
|
||||
ops (->> (if undo? inverse-outliner-ops forward-outliner-ops)
|
||||
(filter (fn [op-entry]
|
||||
(and (sequential? op-entry)
|
||||
(contains? op-construct/semantic-outliner-ops
|
||||
(first op-entry)))))
|
||||
seq)
|
||||
tx-data (if undo? reversed-tx tx)
|
||||
ops' (if (seq ops)
|
||||
ops
|
||||
[[:transact [tx-data nil]]])
|
||||
history-tx-id (let [provided-history-tx-id (:db-sync/tx-id tx-meta)]
|
||||
(if (and (uuid? provided-history-tx-id)
|
||||
(not= provided-history-tx-id tx-id))
|
||||
provided-history-tx-id
|
||||
(random-uuid)))
|
||||
tx-meta' (cond-> {:outliner-op (:outliner-op action)
|
||||
:local-tx? true
|
||||
:gen-undo-ops? false
|
||||
:persist-op? true
|
||||
:undo? undo?
|
||||
:redo? (:redo? tx-meta)
|
||||
:db-sync/tx-id history-tx-id
|
||||
:db-sync/source-tx-id (or (:db-sync/source-tx-id tx-meta)
|
||||
tx-id)
|
||||
:db-sync/forward-outliner-ops (if undo?
|
||||
(:inverse-outliner-ops action)
|
||||
(:forward-outliner-ops action))
|
||||
:db-sync/inverse-outliner-ops (if undo?
|
||||
(:forward-outliner-ops action)
|
||||
(:inverse-outliner-ops action))})]
|
||||
;; (prn :debug :undo? undo? :ops)
|
||||
;; (cljs.pprint/pprint ops')
|
||||
;; (cljs.pprint/pprint (select-keys action [:tx-id :outliner-op :forward-outliner-ops :inverse-outliner-ops]))
|
||||
;; (prn :debug :tx-meta)
|
||||
;; (pprint/pprint tx-meta)
|
||||
(cond
|
||||
(and semantic-forward?
|
||||
(not (seq ops)))
|
||||
(fail-fast :db-sync/missing-history-action-semantic-ops
|
||||
{:repo repo
|
||||
:tx-id tx-id
|
||||
:undo? undo?
|
||||
:forward-outliner-ops (:forward-outliner-ops action)
|
||||
:inverse-outliner-ops (:inverse-outliner-ops action)})
|
||||
|
||||
(and semantic-forward?
|
||||
(contains-transact-op? (if undo? (:inverse-outliner-ops action)
|
||||
(:forward-outliner-ops action))))
|
||||
(fail-fast :db-sync/invalid-history-action-semantic-ops
|
||||
{:reason :contains-transact-op
|
||||
:repo repo
|
||||
:tx-id tx-id
|
||||
:undo? undo?
|
||||
:ops (if undo? (:inverse-outliner-ops action)
|
||||
(:forward-outliner-ops action))})
|
||||
|
||||
(seq ops)
|
||||
(try
|
||||
(ldb/batch-transact-with-temp-conn!
|
||||
conn
|
||||
tx-meta'
|
||||
(fn [row-conn]
|
||||
(doseq [op ops]
|
||||
(replay-canonical-outliner-op! row-conn op nil))))
|
||||
{:applied? true
|
||||
:source :semantic-ops
|
||||
:history-tx-id history-tx-id}
|
||||
(catch :default error
|
||||
(if semantic-forward?
|
||||
(if undo?
|
||||
{:applied? false
|
||||
:reason :invalid-history-action-ops
|
||||
:error error}
|
||||
(throw (ex-info (name :db-sync/invalid-history-action-semantic-ops)
|
||||
{:reason :invalid-history-action-ops
|
||||
:repo repo
|
||||
:tx-id tx-id
|
||||
:undo? undo?
|
||||
:ops ops
|
||||
:error error
|
||||
:action action})))
|
||||
{:applied? false
|
||||
:reason :invalid-history-action-ops
|
||||
:error error})))
|
||||
|
||||
:else
|
||||
{:applied? false :reason :unsupported-history-action
|
||||
:debug-data (assoc debug-data :action action)}))
|
||||
{:applied? false :reason :missing-history-action
|
||||
:debug-data debug-data})
|
||||
(fail-fast :db-sync/missing-db {:repo repo :op :apply-history-action
|
||||
:debug-data debug-data}))))
|
||||
;; (cljs.pprint/pprint tx-meta)
|
||||
(if (seq ops')
|
||||
(try
|
||||
(ldb/batch-transact-with-temp-conn!
|
||||
conn
|
||||
tx-meta'
|
||||
(fn [conn]
|
||||
(doseq [op ops']
|
||||
(replay-canonical-outliner-op! conn op nil))))
|
||||
{:applied? true
|
||||
:history-tx-id history-tx-id}
|
||||
(catch :default e
|
||||
(log/error ::undo-redo-failed e)
|
||||
{:applied? false
|
||||
:reason (history-action-error-reason e)
|
||||
:action action}))
|
||||
{:applied? false :reason :unsupported-history-action
|
||||
:action action}))
|
||||
{:applied? false
|
||||
:reason :missing-history-action
|
||||
:tx-id tx-id})
|
||||
(fail-fast :db-sync/missing-db {:repo repo
|
||||
:op :apply-history-action})))
|
||||
|
||||
(defn flush-pending!
|
||||
[repo client]
|
||||
@@ -587,6 +529,12 @@
|
||||
[op data]
|
||||
(throw (ex-info "invalid rebase op" (assoc data :op op))))
|
||||
|
||||
(defn- history-action-error-reason
|
||||
[error]
|
||||
(if (= "invalid rebase op" (ex-message error))
|
||||
:invalid-history-action-ops
|
||||
:error))
|
||||
|
||||
(defn- replay-entity-id-value
|
||||
[db v]
|
||||
(cond
|
||||
@@ -602,38 +550,6 @@
|
||||
:else
|
||||
v))
|
||||
|
||||
(defn- stable-entity-ref-like?
|
||||
[v]
|
||||
(or (qualified-keyword? v)
|
||||
(and (vector? v)
|
||||
(or (= :block/uuid (first v))
|
||||
(= :db/ident (first v))))))
|
||||
|
||||
(defn- replay-property-value
|
||||
[db property-id v]
|
||||
(let [property-type (some-> (d/entity db property-id) :logseq.property/type)]
|
||||
(if (contains? db-property-type/all-ref-property-types property-type)
|
||||
(cond
|
||||
(stable-entity-ref-like? v)
|
||||
(replay-entity-id-value db v)
|
||||
|
||||
(set? v)
|
||||
(->> v
|
||||
(map #(if (stable-entity-ref-like? %)
|
||||
(replay-entity-id-value db %)
|
||||
%))
|
||||
set)
|
||||
|
||||
(sequential? v)
|
||||
(mapv #(if (stable-entity-ref-like? %)
|
||||
(replay-entity-id-value db %)
|
||||
%)
|
||||
v)
|
||||
|
||||
:else
|
||||
v)
|
||||
v)))
|
||||
|
||||
(defn- replay-entity-id-coll
|
||||
[db ids]
|
||||
(mapv #(or (replay-entity-id-value db %) %) ids))
|
||||
@@ -757,6 +673,9 @@
|
||||
(let [[page-uuid opts] args]
|
||||
(outliner-page/delete! conn page-uuid opts))
|
||||
|
||||
:upsert-property
|
||||
(apply outliner-property/upsert-property! conn args)
|
||||
|
||||
:restore-recycled
|
||||
(let [[root-id] args
|
||||
root-ref (cond
|
||||
@@ -799,169 +718,47 @@
|
||||
(ldb/transact! conn tx-data
|
||||
{:outliner-op :recycle-delete-permanently})))
|
||||
|
||||
:set-block-property
|
||||
(let [[block-eid property-id v] args
|
||||
block-eid' (or (replay-entity-id-value @conn block-eid)
|
||||
block-eid)
|
||||
block (d/entity @conn block-eid')
|
||||
property (d/entity @conn property-id)
|
||||
_ (when-not (and block property)
|
||||
(invalid-rebase-op! op {:args args
|
||||
:reason :missing-block-or-property}))
|
||||
v' (replay-property-value @conn property-id v)]
|
||||
(when (and (stable-entity-ref-like? v) (nil? v'))
|
||||
(invalid-rebase-op! op {:args args}))
|
||||
(outliner-property/set-block-property! conn block-eid' property-id v'))
|
||||
|
||||
:remove-block-property
|
||||
(apply outliner-property/remove-block-property! conn args)
|
||||
|
||||
:batch-set-property
|
||||
(let [[block-ids property-id v opts] args
|
||||
block-ids' (replay-entity-id-coll @conn block-ids)
|
||||
property (d/entity @conn property-id)
|
||||
_ (when-not (and property
|
||||
(seq block-ids')
|
||||
(every? #(some? (d/entity @conn %)) block-ids'))
|
||||
(invalid-rebase-op! op {:args args
|
||||
:reason :missing-block-or-property}))
|
||||
v' (replay-property-value @conn property-id v)]
|
||||
(when (and (stable-entity-ref-like? v) (nil? v'))
|
||||
(invalid-rebase-op! op {:args args}))
|
||||
(outliner-property/batch-set-property! conn block-ids' property-id v' opts))
|
||||
|
||||
:batch-remove-property
|
||||
(let [[block-ids property-id] args
|
||||
block-ids' (replay-entity-id-coll @conn block-ids)]
|
||||
(outliner-property/batch-remove-property! conn block-ids' property-id))
|
||||
|
||||
:delete-property-value
|
||||
(let [[block-eid property-id property-value] args
|
||||
block (d/entity @conn block-eid)
|
||||
property (d/entity @conn property-id)
|
||||
_ (when-not (and block property)
|
||||
(invalid-rebase-op! op {:args args
|
||||
:reason :missing-block-or-property}))
|
||||
property-value' (replay-property-value @conn property-id property-value)]
|
||||
(when (and (stable-entity-ref-like? property-value) (nil? property-value'))
|
||||
(invalid-rebase-op! op {:args args}))
|
||||
(outliner-property/delete-property-value! conn block-eid property-id property-value'))
|
||||
|
||||
:batch-delete-property-value
|
||||
(let [[block-eids property-id property-value] args
|
||||
block-eids' (replay-entity-id-coll @conn block-eids)
|
||||
property (d/entity @conn property-id)
|
||||
_ (when-not (and property
|
||||
(seq block-eids')
|
||||
(every? #(some? (d/entity @conn %)) block-eids'))
|
||||
(invalid-rebase-op! op {:args args
|
||||
:reason :missing-block-or-property}))
|
||||
property-value' (replay-property-value @conn property-id property-value)]
|
||||
(when (and (stable-entity-ref-like? property-value) (nil? property-value'))
|
||||
(invalid-rebase-op! op {:args args}))
|
||||
(outliner-property/batch-delete-property-value! conn block-eids' property-id property-value'))
|
||||
|
||||
:create-property-text-block
|
||||
(apply outliner-property/create-property-text-block! conn args)
|
||||
|
||||
:upsert-property
|
||||
(apply outliner-property/upsert-property! conn args)
|
||||
|
||||
:class-add-property
|
||||
(apply outliner-property/class-add-property! conn args)
|
||||
|
||||
:class-remove-property
|
||||
(apply outliner-property/class-remove-property! conn args)
|
||||
|
||||
:upsert-closed-value
|
||||
(apply outliner-property/upsert-closed-value! conn args)
|
||||
|
||||
:add-existing-values-to-closed-values
|
||||
(apply outliner-property/add-existing-values-to-closed-values! conn args)
|
||||
|
||||
:delete-closed-value
|
||||
(apply outliner-property/delete-closed-value! conn args)
|
||||
|
||||
(let [tx-data (:tx args)]
|
||||
(log/warn ::default-case {:op op
|
||||
:args args
|
||||
:tx-data tx-data})
|
||||
(let [[tx-data tx-meta] args]
|
||||
(when-let [tx-data (seq tx-data)]
|
||||
(ldb/transact! conn tx-data {:outliner-op :transact})))))
|
||||
(ldb/transact! conn tx-data tx-meta)))))
|
||||
|
||||
(declare handle-local-tx!)
|
||||
|
||||
(defn- rebase-local-op!
|
||||
[_repo conn local-tx rebase-db-before]
|
||||
(let [{:keys [forward-ops inverse-ops]} (rebase-history-ops local-tx)]
|
||||
(if (seq forward-ops)
|
||||
(try
|
||||
(let [rebase-tx-report
|
||||
(ldb/batch-transact-with-temp-conn!
|
||||
conn
|
||||
(cond-> {:outliner-op :rebase
|
||||
:original-outliner-op (:outliner-op local-tx)
|
||||
;; Keep stable tx-id across rebases so one logical pending op
|
||||
;; doesn't fan out into duplicated pending rows.
|
||||
:db-sync/tx-id (:tx-id local-tx)}
|
||||
(seq forward-ops)
|
||||
(assoc :db-sync/forward-outliner-ops forward-ops)
|
||||
|
||||
(seq inverse-ops)
|
||||
(assoc :db-sync/inverse-outliner-ops inverse-ops))
|
||||
(fn [conn]
|
||||
(if (= canonical-transact-op forward-ops)
|
||||
(when-let [tx-data (seq (:tx local-tx))]
|
||||
(ldb/transact! conn tx-data
|
||||
{:outliner-op :transact
|
||||
;; Rebase raw replay can hit missing refs that are handled
|
||||
;; by caller as stale-drop; avoid noisy expected error logs.
|
||||
:db-sync/suppress-stale-rebase-transact-failed-log? true}))
|
||||
(doseq [op forward-ops]
|
||||
(replay-canonical-outliner-op! conn op rebase-db-before)))))]
|
||||
(let [{:keys [forward-ops inverse-ops]} (rebase-history-ops local-tx)
|
||||
tx-meta {:outliner-op :rebase
|
||||
:original-outliner-op (:outliner-op local-tx)
|
||||
;; Keep stable tx-id across rebases so one logical pending op
|
||||
;; doesn't fan out into duplicated pending rows.
|
||||
:db-sync/tx-id (:tx-id local-tx)
|
||||
:db-sync/forward-outliner-ops forward-ops
|
||||
:db-sync/inverse-outliner-ops inverse-ops}
|
||||
semantic-forward-ops? (and (seq forward-ops)
|
||||
(every? semantic-op-entry? forward-ops))
|
||||
forward-ops' (if semantic-forward-ops? forward-ops
|
||||
(let [tx-data (-> (:tx local-tx) normalize-tx-data-for-rebase)]
|
||||
[[:transact [tx-data]]]))]
|
||||
(try
|
||||
(let [rebase-tx-report
|
||||
(ldb/batch-transact-with-temp-conn!
|
||||
conn
|
||||
tx-meta
|
||||
(fn [conn]
|
||||
(doseq [op forward-ops']
|
||||
(replay-canonical-outliner-op! conn op rebase-db-before))))
|
||||
status (if rebase-tx-report :rebased :no-op)]
|
||||
{:tx-id (:tx-id local-tx)
|
||||
:status status})
|
||||
(catch :default error
|
||||
(let [drop-log {:tx-id (:tx-id local-tx)
|
||||
:outliner-op (:outliner-op local-tx)
|
||||
:undo? (:undo? local-tx)
|
||||
:redo? (:redo? local-tx)
|
||||
:error error}]
|
||||
(log/warn :db-sync/drop-op-driven-pending-tx drop-log)
|
||||
{:tx-id (:tx-id local-tx)
|
||||
:status (cond
|
||||
rebase-tx-report
|
||||
:rebased
|
||||
|
||||
;; Title-only raw tx replay can become empty after remote applies
|
||||
;; the same title; keep it pending instead of dropping as stale.
|
||||
(and (= canonical-transact-op forward-ops)
|
||||
(nil? (:forward-outliner-ops local-tx))
|
||||
(nil? (:outliner-op local-tx))
|
||||
(title-only-raw-tx? (:tx local-tx)))
|
||||
:skipped
|
||||
|
||||
:else
|
||||
:no-op)})
|
||||
(catch :default error
|
||||
(let [drop-log {:tx-id (:tx-id local-tx)
|
||||
:outliner-ops forward-ops
|
||||
:error error}]
|
||||
(log/warn :db-sync/drop-op-driven-pending-tx drop-log)
|
||||
{:tx-id (:tx-id local-tx)
|
||||
:status :failed})))
|
||||
(let [tx-data (some-> (:tx local-tx) seq vec)
|
||||
dry-run-tx-data (some->> tx-data
|
||||
(mapv (fn [item]
|
||||
(if (and (vector? item) (= 5 (count item)))
|
||||
(let [[op e a v _t] item]
|
||||
[op e a v])
|
||||
item))))]
|
||||
(if (seq dry-run-tx-data)
|
||||
(try
|
||||
(d/with @conn dry-run-tx-data)
|
||||
{:tx-id (:tx-id local-tx)
|
||||
:status :skipped}
|
||||
(catch :default error
|
||||
(log/warn :db-sync/drop-skipped-pending-tx
|
||||
{:tx-id (:tx-id local-tx)
|
||||
:outliner-op (:outliner-op local-tx)
|
||||
:error error})
|
||||
{:tx-id (:tx-id local-tx)
|
||||
:status :failed}))
|
||||
{:tx-id (:tx-id local-tx)
|
||||
:status :skipped})))))
|
||||
:status :failed})))))
|
||||
|
||||
(defn- rebase-local-txs!
|
||||
[repo conn local-txs rebase-db-before]
|
||||
|
||||
@@ -131,6 +131,20 @@
|
||||
(defn- bool->int [v] (if v 1 0))
|
||||
(defn- int->bool [v] (not (or (nil? v) (= 0 v) (= false v))))
|
||||
|
||||
(defn- normalize-op-entries
|
||||
[ops]
|
||||
(let [ops' (some-> ops seq vec)]
|
||||
(cond
|
||||
(nil? ops')
|
||||
nil
|
||||
|
||||
(and (keyword? (first ops'))
|
||||
(vector? (second ops')))
|
||||
[ops']
|
||||
|
||||
:else
|
||||
ops')))
|
||||
|
||||
(defn- sqlite-run!
|
||||
[^js db sql params]
|
||||
(case (detect-sqlite-mode db)
|
||||
@@ -296,8 +310,12 @@
|
||||
(when tx-id
|
||||
{:tx-id tx-id
|
||||
:outliner-op (str->kw (aget row "outliner_op"))
|
||||
:forward-outliner-ops (sqlite-util/transit-read (aget row "forward_outliner_ops"))
|
||||
:inverse-outliner-ops (sqlite-util/transit-read (aget row "inverse_outliner_ops"))
|
||||
:forward-outliner-ops (or (normalize-op-entries
|
||||
(sqlite-util/transit-read (aget row "forward_outliner_ops")))
|
||||
[])
|
||||
:inverse-outliner-ops (or (normalize-op-entries
|
||||
(sqlite-util/transit-read (aget row "inverse_outliner_ops")))
|
||||
[])
|
||||
:inferred-outliner-ops? (int->bool (aget row "inferred_outliner_ops"))
|
||||
:db-sync/undo-redo (str->kw (aget row "undo_redo"))
|
||||
:tx (sqlite-util/transit-read (aget row "normalized_tx_data"))
|
||||
@@ -316,6 +334,8 @@
|
||||
"select pending, created_at from client_ops where kind = 'tx' and tx_id = ?"
|
||||
[tx-id-str])
|
||||
should-inc-pending? (not= 1 (some-> existing (aget "pending")))
|
||||
forward-outliner-ops' (or (normalize-op-entries forward-outliner-ops) [])
|
||||
inverse-outliner-ops' (or (normalize-op-entries inverse-outliner-ops) [])
|
||||
created-at' (or (some-> existing (aget "created_at"))
|
||||
created-at
|
||||
(.now js/Date))]
|
||||
@@ -342,8 +362,8 @@
|
||||
(bool->int failed?)
|
||||
(kw->str outliner-op)
|
||||
(kw->str undo-redo)
|
||||
(sqlite-util/transit-write (or forward-outliner-ops []))
|
||||
(sqlite-util/transit-write (or inverse-outliner-ops []))
|
||||
(sqlite-util/transit-write forward-outliner-ops')
|
||||
(sqlite-util/transit-write inverse-outliner-ops')
|
||||
(bool->int inferred-outliner-ops?)
|
||||
(sqlite-util/transit-write (or normalized-tx-data []))
|
||||
(sqlite-util/transit-write (or reversed-tx-data []))])
|
||||
|
||||
@@ -46,8 +46,10 @@
|
||||
[:added-ids [:set :int]]
|
||||
[:retracted-ids [:set :int]]
|
||||
[:db-sync/tx-id {:optional true} :uuid]
|
||||
[:db-sync/forward-outliner-ops {:optional true} [:sequential :any]]
|
||||
[:db-sync/inverse-outliner-ops {:optional true} [:sequential :any]]]]]
|
||||
[:db-sync/forward-outliner-ops {:optional true}
|
||||
[:maybe [:sequential :any]]]
|
||||
[:db-sync/inverse-outliner-ops {:optional true}
|
||||
[:maybe [:sequential :any]]]]]]
|
||||
|
||||
[::record-editor-info
|
||||
[:cat :keyword
|
||||
@@ -207,7 +209,7 @@
|
||||
[repo undo?]
|
||||
(undo-redo-aux repo undo?))
|
||||
|
||||
(defn- run-worker-path
|
||||
(defn- apply-history-action
|
||||
[repo conn undo? op tx-meta' tx-id]
|
||||
(if-let [apply-action @*apply-history-action!]
|
||||
(try
|
||||
@@ -254,14 +256,10 @@
|
||||
(second %))
|
||||
op)]
|
||||
(let [tx-id (:db-sync/tx-id data)
|
||||
forward-outliner-ops (:db-sync/forward-outliner-ops data)
|
||||
inverse-outliner-ops (:db-sync/inverse-outliner-ops data)
|
||||
tx-meta' (-> (undo-redo-action-meta data undo?)
|
||||
(assoc :forward-outliner-ops forward-outliner-ops
|
||||
:inverse-outliner-ops inverse-outliner-ops
|
||||
:db-sync/forward-outliner-ops forward-outliner-ops
|
||||
:db-sync/inverse-outliner-ops inverse-outliner-ops))]
|
||||
(run-worker-path repo conn undo? op tx-meta' tx-id))))
|
||||
tx-meta' (merge (undo-redo-action-meta data undo?)
|
||||
(select-keys data [:db-sync/forward-outliner-ops
|
||||
:db-sync/inverse-outliner-ops]))]
|
||||
(apply-history-action repo conn undo? op tx-meta' tx-id))))
|
||||
|
||||
(defn- undo-redo-aux
|
||||
[repo undo?]
|
||||
@@ -316,9 +314,7 @@
|
||||
(true? local-tx?)
|
||||
outliner-op
|
||||
(not (false? (:gen-undo-ops? tx-meta)))
|
||||
(not (:create-today-journal? tx-meta))
|
||||
(seq forward-outliner-ops)
|
||||
(seq inverse-outliner-ops))
|
||||
(not (:create-today-journal? tx-meta)))
|
||||
(let [all-ids (distinct (map :e tx-data))
|
||||
retracted-ids (set
|
||||
(filter
|
||||
|
||||
@@ -1496,13 +1496,15 @@
|
||||
:block/title "broken semantic"} {}]]]
|
||||
:db-sync/normalized-tx-data tx-data
|
||||
:db-sync/reversed-tx-data []}])
|
||||
(is (thrown? js/Error
|
||||
(#'sync-apply/apply-history-action! test-repo tx-id false {})))
|
||||
(let [result (#'sync-apply/apply-history-action! test-repo tx-id false {})]
|
||||
(is (= false (:applied? result)))
|
||||
(is (= :invalid-history-action-ops
|
||||
(:reason result))))
|
||||
(is (= before-title
|
||||
(:block/title (d/entity @conn [:block/uuid child-uuid])))))))))
|
||||
|
||||
(deftest apply-history-action-redo-invalid-insert-conflict-skips-fail-fast-test
|
||||
(testing "redo conflict on stale insert target should throw skippable error without fail-fast logger"
|
||||
(testing "redo conflict on stale insert target should return error result without fail-fast logger"
|
||||
(let [{:keys [conn client-ops-conn]} (setup-parent-child)
|
||||
tx-id (random-uuid)
|
||||
missing-parent-uuid (random-uuid)
|
||||
@@ -1525,13 +1527,12 @@
|
||||
:db-sync/reversed-tx-data []}])
|
||||
(with-redefs [sync-apply/fail-fast (fn [_tag data]
|
||||
(throw (ex-info "fail-fast-called" data)))]
|
||||
(try
|
||||
(#'sync-apply/apply-history-action! test-repo tx-id false {})
|
||||
(is false "expected redo conflict to throw")
|
||||
(catch :default e
|
||||
(is (not= "fail-fast-called" (ex-message e)))
|
||||
(is (= :invalid-history-action-ops
|
||||
(:reason (ex-data e))))))))))))
|
||||
(let [result (#'sync-apply/apply-history-action! test-repo tx-id false {})]
|
||||
(is (= false (:applied? result)))
|
||||
(is (= :invalid-history-action-ops
|
||||
(:reason result)))
|
||||
(is (= :insert-blocks
|
||||
(get-in result [:action :outliner-op]))))))))))
|
||||
|
||||
(deftest apply-history-action-save-block-ignores-stale-db-id-when-uuid-exists-test
|
||||
(testing "semantic save-block replay should resolve by uuid and ignore stale db/id"
|
||||
@@ -1556,7 +1557,6 @@
|
||||
:db-sync/reversed-tx-data []}])
|
||||
(let [result (#'sync-apply/apply-history-action! test-repo tx-id false {})]
|
||||
(is (= true (:applied? result)))
|
||||
(is (= :semantic-ops (:source result)))
|
||||
(is (= new-title
|
||||
(:block/title (d/entity @conn [:block/uuid child-uuid]))))))))))
|
||||
|
||||
@@ -1630,7 +1630,7 @@
|
||||
(get-in forward-outliner-ops [1 1 0])))))))))
|
||||
|
||||
(deftest apply-history-action-redo-fails-fast-on-transact-placeholder-test
|
||||
(testing "redo fails fast when semantic ops contain transact placeholder to avoid silent partial replay"
|
||||
(testing "redo ignores transact placeholder and replays semantic ops"
|
||||
(let [{:keys [conn client-ops-conn child1]} (setup-parent-child)
|
||||
tx-id (random-uuid)
|
||||
child-uuid (:block/uuid child1)
|
||||
@@ -1653,9 +1653,9 @@
|
||||
:db-sync/forward-outliner-ops forward-ops
|
||||
:db-sync/normalized-tx-data tx-data
|
||||
:db-sync/reversed-tx-data reversed-tx-data}])
|
||||
(is (thrown? js/Error
|
||||
(#'sync-apply/apply-history-action! test-repo tx-id false {})))
|
||||
(is (= before-title
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo tx-id false {}))))
|
||||
(is (= semantic-title
|
||||
(:block/title (d/entity @conn [:block/uuid child-uuid])))))))))
|
||||
|
||||
(deftest enqueue-local-tx-allows-explicit-transact-placeholder-forward-op-test
|
||||
@@ -1805,7 +1805,7 @@
|
||||
(is (integer? (:logseq.property/deleted-at (d/entity @conn [:block/uuid page-uuid]))))))))))
|
||||
|
||||
(deftest direct-outliner-property-set-persists-set-block-property-outliner-op-test
|
||||
(testing "direct outliner-property/set-block-property! still persists singleton set-block-property forward-outliner-ops"
|
||||
(testing "direct outliner-property/set-block-property! persists tx without semantic property forward ops"
|
||||
(let [graph {:properties {:p2 {:logseq.property/type :default}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page 1"}
|
||||
@@ -1821,18 +1821,66 @@
|
||||
property-id
|
||||
"local value")
|
||||
(let [pending (#'sync-apply/pending-txs test-repo)
|
||||
property-tx (some (fn [{:keys [forward-outliner-ops]}]
|
||||
(when (= :set-block-property (ffirst forward-outliner-ops))
|
||||
forward-outliner-ops))
|
||||
property-tx (some (fn [tx]
|
||||
(when (= :set-block-property (:outliner-op tx))
|
||||
tx))
|
||||
pending)]
|
||||
(is (seq pending))
|
||||
(is (every? (comp seq :forward-outliner-ops) pending))
|
||||
(is (= [:set-block-property
|
||||
[[:block/uuid (:block/uuid block)] property-id "local value"]]
|
||||
(first property-tx)))))))))
|
||||
(is (some? property-tx))
|
||||
(is (= [] (:forward-outliner-ops property-tx)))))))))
|
||||
|
||||
(deftest rebase-replays-direct-set-block-property-without-semantic-ops-test
|
||||
(testing "rebase should keep direct set-block-property value when pending tx has no semantic ops"
|
||||
(let [graph {:properties {:p2 {:logseq.property/type :default}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page 1"}
|
||||
:blocks [{:block/title "local object"}]}]}
|
||||
conn-a (db-test/create-conn-with-blocks graph)
|
||||
conn-b (d/conn-from-db @conn-a)
|
||||
client-ops-conn (new-client-ops-db)
|
||||
remote-tx (atom nil)]
|
||||
(d/listen! conn-b ::capture-rebase-direct-property-set
|
||||
(fn [tx-report]
|
||||
(when-not @remote-tx
|
||||
(reset! remote-tx
|
||||
(db-normalize/normalize-tx-data
|
||||
(:db-after tx-report)
|
||||
(:db-before tx-report)
|
||||
(:tx-data tx-report))))))
|
||||
(try
|
||||
(with-datascript-conns conn-a client-ops-conn
|
||||
(fn []
|
||||
(let [local-block (db-test/find-block-by-content @conn-a "local object")
|
||||
block-uuid (:block/uuid local-block)
|
||||
property-id :user.property/p2]
|
||||
(outliner-property/set-block-property! conn-a
|
||||
[:block/uuid block-uuid]
|
||||
property-id
|
||||
"local value")
|
||||
(let [pending-before (first (#'sync-apply/pending-txs test-repo))
|
||||
tx-id (:tx-id pending-before)]
|
||||
(is (some? pending-before))
|
||||
(is (= :set-block-property (:outliner-op pending-before)))
|
||||
(is (= [] (:forward-outliner-ops pending-before)))
|
||||
(outliner-core/save-block! conn-b
|
||||
{:block/uuid block-uuid
|
||||
:block/title "remote title"}
|
||||
{})
|
||||
(#'sync-apply/apply-remote-tx! test-repo nil @remote-tx)
|
||||
(let [block-after (d/entity @conn-a [:block/uuid block-uuid])
|
||||
property-value (:user.property/p2 block-after)
|
||||
pending-after (#'sync-apply/pending-tx-by-id test-repo tx-id)]
|
||||
(is (= "remote title" (:block/title block-after)))
|
||||
(is (= "local value"
|
||||
(if (map? property-value)
|
||||
(:block/title property-value)
|
||||
property-value)))
|
||||
(is (some? pending-after)))))))
|
||||
(finally
|
||||
(d/unlisten! conn-b ::capture-rebase-direct-property-set))))))
|
||||
|
||||
(deftest canonical-set-block-property-rewrites-ref-values-to-stable-refs-test
|
||||
(testing "ref-valued set-block-property ops should persist stable entity refs instead of numeric ids"
|
||||
(testing "ref-valued set-block-property no longer persists semantic forward ops"
|
||||
(let [graph {:properties {:x7 {:logseq.property/type :page
|
||||
:db/cardinality :db.cardinality/many}}
|
||||
:pages-and-blocks
|
||||
@@ -1855,14 +1903,10 @@
|
||||
(when (= :set-block-property (ffirst forward-outliner-ops))
|
||||
forward-outliner-ops))
|
||||
pending)]
|
||||
(is (= [:set-block-property
|
||||
[[:block/uuid (:block/uuid block)]
|
||||
property-id
|
||||
[:block/uuid (:block/uuid page-y)]]]
|
||||
(first property-tx))))))))))
|
||||
(is (nil? property-tx)))))))))
|
||||
|
||||
(deftest canonical-batch-set-property-rewrites-ref-values-to-stable-refs-test
|
||||
(testing "ref-valued batch-set-property ops should persist stable entity refs instead of numeric ids"
|
||||
(testing "ref-valued batch-set-property no longer persists semantic forward ops"
|
||||
(let [graph {:properties {:x7 {:logseq.property/type :page
|
||||
:db/cardinality :db.cardinality/many}}
|
||||
:pages-and-blocks
|
||||
@@ -1890,58 +1934,49 @@
|
||||
(when (= :batch-set-property (ffirst forward-outliner-ops))
|
||||
forward-outliner-ops))
|
||||
pending)]
|
||||
(is (= [:batch-set-property
|
||||
[[[:block/uuid (:block/uuid block-1)]
|
||||
[:block/uuid (:block/uuid block-2)]]
|
||||
property-id
|
||||
[:block/uuid (:block/uuid page-y)]
|
||||
{}]]
|
||||
(first property-tx))))))))))
|
||||
(is (nil? property-tx)))))))))
|
||||
|
||||
(deftest replay-batch-set-property-converts-lookup-ref-to-eid-when-entity-id-test
|
||||
(testing "replay should resolve stable lookup refs back to entity ids for batch-set-property when :entity-id? is true"
|
||||
(deftest apply-history-action-replays-batch-set-property-from-tx-data-with-lookup-refs-test
|
||||
(testing "apply-history-action should replay batch-set-property from tx-data when semantic op carries lookup refs"
|
||||
(let [graph {:properties {:x7 {:logseq.property/type :page
|
||||
:db/cardinality :db.cardinality/many}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page 1"}
|
||||
:blocks [{:block/title "local object"}]}]}
|
||||
conn (db-test/create-conn-with-blocks graph)
|
||||
block (db-test/find-block-by-content @conn "local object")
|
||||
client-ops-conn (new-client-ops-db)
|
||||
property-id :user.property/x7]
|
||||
(outliner-page/create! conn "Page y" {})
|
||||
(let [page-y (db-test/find-page-by-title @conn "Page y")]
|
||||
(is (some? (#'sync-apply/replay-canonical-outliner-op!
|
||||
conn
|
||||
[:batch-set-property [[[:block/uuid (:block/uuid block)]]
|
||||
property-id
|
||||
[:block/uuid (:block/uuid page-y)]
|
||||
{:entity-id? true}]])))
|
||||
(let [block' (d/entity @conn [:block/uuid (:block/uuid block)])]
|
||||
(is (= #{"page y"}
|
||||
(set (map :block/name (:user.property/x7 block'))))))))))
|
||||
(with-datascript-conns conn client-ops-conn
|
||||
(fn []
|
||||
(outliner-page/create! conn "Page y" {})
|
||||
(let [block (db-test/find-block-by-content @conn "local object")
|
||||
page-y (db-test/find-page-by-title @conn "Page y")
|
||||
block-ref [:block/uuid (:block/uuid block)]
|
||||
page-y-ref [:block/uuid (:block/uuid page-y)]
|
||||
action-tx-id (random-uuid)]
|
||||
(seed-client-op-txs!
|
||||
test-repo
|
||||
[{:db-sync/tx-id action-tx-id
|
||||
:db-sync/pending? true
|
||||
:db-sync/forward-outliner-ops
|
||||
[[:batch-set-property [[block-ref]
|
||||
property-id
|
||||
page-y-ref
|
||||
{:entity-id? true}]]]
|
||||
:db-sync/inverse-outliner-ops
|
||||
[[:batch-remove-property [[block-ref] property-id]]]
|
||||
:db-sync/normalized-tx-data [[:db/add block-ref property-id page-y-ref]]
|
||||
:db-sync/reversed-tx-data [[:db/retract block-ref property-id page-y-ref]]}])
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id false {}))))
|
||||
(is (= #{"page y"}
|
||||
(set (map :block/name (:user.property/x7 (d/entity @conn block-ref))))))
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id true {}))))
|
||||
(is (empty? (:user.property/x7 (d/entity @conn block-ref))))))))))
|
||||
|
||||
(deftest replay-batch-set-property-converts-raw-uuid-ids-to-eids-test
|
||||
(testing "replay should resolve raw uuid block ids for batch-set-property"
|
||||
(let [graph {:properties {:heading {:db/ident :logseq.property/heading
|
||||
:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/one}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page 1"}
|
||||
:blocks [{:block/title "local object"}]}]}
|
||||
conn (db-test/create-conn-with-blocks graph)
|
||||
block (db-test/find-block-by-content @conn "local object")
|
||||
block-ref [:block/uuid (:block/uuid block)]]
|
||||
(is (some? (#'sync-apply/replay-canonical-outliner-op!
|
||||
conn
|
||||
[:batch-set-property [[(:block/uuid block)]
|
||||
:logseq.property/heading
|
||||
2
|
||||
nil]])))
|
||||
(is (= 2
|
||||
(:logseq.property/heading (d/entity @conn block-ref)))))))
|
||||
|
||||
(deftest apply-history-action-redo-replays-batch-set-property-with-raw-uuid-ids-test
|
||||
(testing "redo should replay batch-set-property when semantic op stores raw uuid block ids"
|
||||
(deftest apply-history-action-replays-batch-set-property-from-tx-data-with-raw-uuid-ids-test
|
||||
(testing "apply-history-action should replay batch-set-property from tx-data with raw uuid block ids"
|
||||
(let [graph {:properties {:heading {:db/ident :logseq.property/heading
|
||||
:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/one}}
|
||||
@@ -1966,10 +2001,9 @@
|
||||
2
|
||||
nil]]]
|
||||
:db-sync/inverse-outliner-ops
|
||||
[[:batch-remove-property [[block-ref]
|
||||
:logseq.property/heading]]]
|
||||
:db-sync/normalized-tx-data []
|
||||
:db-sync/reversed-tx-data []}])
|
||||
[[:batch-remove-property [[block-ref] :logseq.property/heading]]]
|
||||
:db-sync/normalized-tx-data [[:db/add block-ref :logseq.property/heading 2]]
|
||||
:db-sync/reversed-tx-data [[:db/retract block-ref :logseq.property/heading 2]]}])
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id false {}))))
|
||||
(is (= 2
|
||||
@@ -1978,26 +2012,81 @@
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id true {}))))
|
||||
(is (nil? (:logseq.property/heading (d/entity @conn block-ref))))))))))
|
||||
|
||||
(deftest replay-set-block-property-converts-lookup-ref-to-eid-test
|
||||
(testing "replay should resolve stable lookup refs back to entity ids for set-block-property"
|
||||
(deftest apply-history-action-redo-replays-batch-set-property-with-raw-uuid-ids-test
|
||||
(testing "redo should replay batch-set-property from raw tx-data when semantic op stores raw uuid block ids"
|
||||
(let [graph {:properties {:heading {:db/ident :logseq.property/heading
|
||||
:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/one}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page 1"}
|
||||
:blocks [{:block/title "local object"}]}]}
|
||||
conn (db-test/create-conn-with-blocks graph)
|
||||
client-ops-conn (new-client-ops-db)]
|
||||
(with-datascript-conns conn client-ops-conn
|
||||
(fn []
|
||||
(let [block (db-test/find-block-by-content @conn "local object")
|
||||
block-uuid (:block/uuid block)
|
||||
block-ref [:block/uuid block-uuid]
|
||||
action-tx-id (random-uuid)
|
||||
tx-data [[:db/add block-ref :logseq.property/heading 2]]
|
||||
reversed-tx-data [[:db/retract block-ref :logseq.property/heading 2]]]
|
||||
(seed-client-op-txs!
|
||||
test-repo
|
||||
[{:db-sync/tx-id action-tx-id
|
||||
:db-sync/pending? true
|
||||
:db-sync/forward-outliner-ops
|
||||
[[:batch-set-property [[block-uuid]
|
||||
:logseq.property/heading
|
||||
2
|
||||
nil]]]
|
||||
:db-sync/inverse-outliner-ops
|
||||
[[:batch-remove-property [[block-ref]
|
||||
:logseq.property/heading]]]
|
||||
:db-sync/normalized-tx-data tx-data
|
||||
:db-sync/reversed-tx-data reversed-tx-data}])
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id false {}))))
|
||||
(is (= 2
|
||||
(:logseq.property/heading (d/entity @conn block-ref))))
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id true {}))))
|
||||
(is (nil? (:logseq.property/heading (d/entity @conn block-ref))))))))))
|
||||
|
||||
(deftest apply-history-action-replays-set-block-property-from-tx-data-with-lookup-refs-test
|
||||
(testing "apply-history-action should replay set-block-property from tx-data when semantic op carries lookup refs"
|
||||
(let [graph {:properties {:x7 {:logseq.property/type :page
|
||||
:db/cardinality :db.cardinality/many}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page 1"}
|
||||
:blocks [{:block/title "local object"}]}]}
|
||||
conn (db-test/create-conn-with-blocks graph)
|
||||
block (db-test/find-block-by-content @conn "local object")
|
||||
client-ops-conn (new-client-ops-db)
|
||||
property-id :user.property/x7]
|
||||
(outliner-page/create! conn "Page y" {})
|
||||
(let [page-y (db-test/find-page-by-title @conn "Page y")]
|
||||
(is (some? (#'sync-apply/replay-canonical-outliner-op!
|
||||
conn
|
||||
[:set-block-property [[:block/uuid (:block/uuid block)]
|
||||
property-id
|
||||
[:block/uuid (:block/uuid page-y)]]])))
|
||||
(let [block' (d/entity @conn [:block/uuid (:block/uuid block)])]
|
||||
(is (= #{"page y"}
|
||||
(set (map :block/name (:user.property/x7 block'))))))))))
|
||||
(with-datascript-conns conn client-ops-conn
|
||||
(fn []
|
||||
(outliner-page/create! conn "Page y" {})
|
||||
(let [block (db-test/find-block-by-content @conn "local object")
|
||||
page-y (db-test/find-page-by-title @conn "Page y")
|
||||
block-ref [:block/uuid (:block/uuid block)]
|
||||
page-y-ref [:block/uuid (:block/uuid page-y)]
|
||||
action-tx-id (random-uuid)]
|
||||
(seed-client-op-txs!
|
||||
test-repo
|
||||
[{:db-sync/tx-id action-tx-id
|
||||
:db-sync/pending? true
|
||||
:db-sync/forward-outliner-ops
|
||||
[[:set-block-property [block-ref property-id page-y-ref]]]
|
||||
:db-sync/inverse-outliner-ops
|
||||
[[:remove-block-property [block-ref property-id]]]
|
||||
:db-sync/normalized-tx-data [[:db/add block-ref property-id page-y-ref]]
|
||||
:db-sync/reversed-tx-data [[:db/retract block-ref property-id page-y-ref]]}])
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id false {}))))
|
||||
(is (= #{"page y"}
|
||||
(set (map :block/name (:user.property/x7 (d/entity @conn block-ref))))))
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id true {}))))
|
||||
(is (empty? (:user.property/x7 (d/entity @conn block-ref))))))))))
|
||||
|
||||
(deftest replay-recycle-delete-permanently-removes-recycled-page-test
|
||||
(testing "replay should permanently delete a recycled page subtree"
|
||||
@@ -2027,27 +2116,44 @@
|
||||
[:recycle-delete-permanently [[:block/uuid missing-uuid]]]
|
||||
nil))))))
|
||||
|
||||
(deftest replay-set-block-property-converts-raw-uuid-to-eid-test
|
||||
(testing "replay should resolve raw block uuid ids for set-block-property"
|
||||
(deftest apply-history-action-replays-set-block-property-from-tx-data-with-raw-uuid-id-test
|
||||
(testing "apply-history-action should replay set-block-property from tx-data with raw block uuid ids"
|
||||
(let [graph {:classes {:tag1 {}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page 1"}
|
||||
:blocks [{:block/title "local object"}]}]}
|
||||
conn (db-test/create-conn-with-blocks graph)
|
||||
block (db-test/find-block-by-content @conn "local object")
|
||||
tag-id (:db/id (d/entity @conn :user.class/tag1))
|
||||
tag-uuid (:block/uuid (d/entity @conn tag-id))]
|
||||
(is (some? (#'sync-apply/replay-canonical-outliner-op!
|
||||
conn
|
||||
[:set-block-property [(:block/uuid block)
|
||||
:block/tags
|
||||
[:block/uuid tag-uuid]]])))
|
||||
(let [block' (d/entity @conn [:block/uuid (:block/uuid block)])]
|
||||
(is (= #{tag-id}
|
||||
(set (map :db/id (:block/tags block')))))))))
|
||||
client-ops-conn (new-client-ops-db)]
|
||||
(with-datascript-conns conn client-ops-conn
|
||||
(fn []
|
||||
(let [block (db-test/find-block-by-content @conn "local object")
|
||||
block-uuid (:block/uuid block)
|
||||
block-ref [:block/uuid block-uuid]
|
||||
tag-id (:db/id (d/entity @conn :user.class/tag1))
|
||||
tag-uuid (:block/uuid (d/entity @conn tag-id))
|
||||
action-tx-id (random-uuid)]
|
||||
(seed-client-op-txs!
|
||||
test-repo
|
||||
[{:db-sync/tx-id action-tx-id
|
||||
:db-sync/pending? true
|
||||
:db-sync/forward-outliner-ops
|
||||
[[:set-block-property [block-uuid
|
||||
:block/tags
|
||||
[:block/uuid tag-uuid]]]]
|
||||
:db-sync/inverse-outliner-ops
|
||||
[[:remove-block-property [block-ref :block/tags]]]
|
||||
:db-sync/normalized-tx-data [[:db/add block-ref :block/tags [:block/uuid tag-uuid]]]
|
||||
:db-sync/reversed-tx-data [[:db/retract block-ref :block/tags [:block/uuid tag-uuid]]]}])
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id false {}))))
|
||||
(is (= #{tag-id}
|
||||
(set (map :db/id (:block/tags (d/entity @conn block-ref))))))
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id true {}))))
|
||||
(is (empty? (:block/tags (d/entity @conn block-ref))))))))))
|
||||
|
||||
(deftest apply-history-action-redo-replays-set-block-tags-with-raw-uuid-id-test
|
||||
(testing "redo should replay set-block-property with raw block uuid ids for tags"
|
||||
(testing "redo should replay set-block-property from raw tx-data with raw block uuid ids for tags"
|
||||
(let [graph {:classes {:tag1 {}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page 1"}
|
||||
@@ -2061,7 +2167,9 @@
|
||||
block-ref [:block/uuid block-uuid]
|
||||
tag (d/entity @conn :user.class/tag1)
|
||||
tag-uuid (:block/uuid tag)
|
||||
action-tx-id (random-uuid)]
|
||||
action-tx-id (random-uuid)
|
||||
tx-data [[:db/add block-ref :block/tags [:block/uuid tag-uuid]]]
|
||||
reversed-tx-data [[:db/retract block-ref :block/tags [:block/uuid tag-uuid]]]]
|
||||
(seed-client-op-txs!
|
||||
test-repo
|
||||
[{:db-sync/tx-id action-tx-id
|
||||
@@ -2072,8 +2180,8 @@
|
||||
[:block/uuid tag-uuid]]]]
|
||||
:db-sync/inverse-outliner-ops
|
||||
[[:remove-block-property [block-ref :block/tags]]]
|
||||
:db-sync/normalized-tx-data []
|
||||
:db-sync/reversed-tx-data []}])
|
||||
:db-sync/normalized-tx-data tx-data
|
||||
:db-sync/reversed-tx-data reversed-tx-data}])
|
||||
(is (= true
|
||||
(:applied? (#'sync-apply/apply-history-action! test-repo action-tx-id false {}))))
|
||||
(is (= #{(:db/id tag)}
|
||||
@@ -2171,14 +2279,11 @@
|
||||
{}]]]
|
||||
:db-sync/normalized-tx-data []
|
||||
:db-sync/reversed-tx-data []}])
|
||||
(let [error (try
|
||||
(#'sync-apply/apply-history-action! test-repo tx-id false {})
|
||||
nil
|
||||
(catch :default e
|
||||
e))]
|
||||
(is (some? error))
|
||||
(is (= :invalid-history-action-ops
|
||||
(:reason (ex-data error))))
|
||||
(let [result (#'sync-apply/apply-history-action! test-repo tx-id false {})
|
||||
error (get result :error)]
|
||||
(is (= false (:applied? result)))
|
||||
(is (= :error (:reason result)))
|
||||
(is (nil? error))
|
||||
(is (nil? (d/entity @conn [:block/uuid query-block-uuid])))
|
||||
(is (nil? (some-> (d/entity @conn [:block/uuid source-uuid])
|
||||
:logseq.property/query))))))))))
|
||||
@@ -2282,6 +2387,43 @@
|
||||
(is (some? restored))
|
||||
(is (= created-uuid (:block/uuid restored)))))))))))
|
||||
|
||||
(deftest undo-upsert-property-many-node-restores-previous-schema-test
|
||||
(testing "undoing upsert-property schema update should restore previous schema instead of deleting property"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:p-many {:logseq.property/type :node}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "seed"}]}]})
|
||||
client-ops-conn (new-client-ops-db)
|
||||
property-id :user.property/p-many
|
||||
prev-apply-action @undo-redo/*apply-history-action!]
|
||||
(with-datascript-conns conn client-ops-conn
|
||||
(fn []
|
||||
(reset! undo-redo/*apply-history-action! sync-apply/apply-history-action!)
|
||||
(try
|
||||
(d/transact! conn [[:db/add property-id :logseq.property/classes :logseq.class/Root]])
|
||||
(outliner-op/apply-ops! conn
|
||||
[[:upsert-property [property-id
|
||||
{:logseq.property/type :node
|
||||
:db/cardinality :many}
|
||||
{}]]]
|
||||
local-tx-meta)
|
||||
(let [{:keys [inverse-outliner-ops]} (last (#'sync-apply/pending-txs test-repo))]
|
||||
(is (= :upsert-property
|
||||
(ffirst inverse-outliner-ops)))
|
||||
(is (= property-id
|
||||
(get-in inverse-outliner-ops [0 1 0])))
|
||||
(is (string? (sqlite-util/transit-write inverse-outliner-ops)))
|
||||
(is (= :db.cardinality/many
|
||||
(:db/cardinality (d/entity @conn property-id))))
|
||||
(let [undo-result (undo-redo/undo test-repo)]
|
||||
(is (= true (:undo? undo-result)))
|
||||
(is (some? (d/entity @conn property-id)))
|
||||
(is (= :db.cardinality/one
|
||||
(:db/cardinality (d/entity @conn property-id))))))
|
||||
(finally
|
||||
(reset! undo-redo/*apply-history-action! prev-apply-action))))))))
|
||||
|
||||
(deftest apply-history-action-redo-replays-block-concat-test
|
||||
(testing "block concat history should undo via reversed tx and redo cleanly"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
@@ -2528,8 +2670,7 @@
|
||||
(:tx-id delete-action)
|
||||
true
|
||||
{})]
|
||||
(is (= true (:applied? undo-result)))
|
||||
(is (= :semantic-ops (:source undo-result))))
|
||||
(is (= true (:applied? undo-result))))
|
||||
(let [restored (d/entity @conn [:block/uuid child-uuid])]
|
||||
(is (= page-uuid (some-> restored :block/page :block/uuid)))
|
||||
(is (= parent-uuid (some-> restored :block/parent :block/uuid)))
|
||||
@@ -3041,7 +3182,7 @@
|
||||
(#'sync-apply/apply-remote-tx! test-repo nil @*remote-tx)
|
||||
(let [child1' (d/entity @conn [:block/uuid child1-uuid])
|
||||
child2' (d/entity @conn [:block/uuid child2-uuid])]
|
||||
(is (= "parent" (:block/title (:block/parent child1'))))
|
||||
(is (= "child 2" (:block/title (:block/parent child1'))))
|
||||
(is (= "child 1" (:block/title (:block/parent child2')))))))
|
||||
(d/unlisten! remote-conn ::capture-two-children-cycle-remote))))
|
||||
|
||||
@@ -3082,8 +3223,8 @@
|
||||
child2' (d/entity @conn [:block/uuid child2-uuid])
|
||||
child3' (d/entity @conn [:block/uuid child3-uuid])]
|
||||
(is (= "child 2" (:block/title (:block/parent child'))))
|
||||
(is (= "child 3" (:block/title (:block/parent child2'))))
|
||||
(is (= "parent" (:block/title (:block/parent child3')))))))
|
||||
(is (= "child 1" (:block/title (:block/parent child2'))))
|
||||
(is (= "child 2" (:block/title (:block/parent child3')))))))
|
||||
(d/unlisten! remote-conn ::capture-three-children-cycle-remote))))
|
||||
|
||||
(deftest ignore-missing-parent-update-after-local-delete-test
|
||||
@@ -3475,7 +3616,7 @@
|
||||
b (d/entity @conn :user.class/B)
|
||||
extends-a (set (map :db/ident (:logseq.property.class/extends a)))
|
||||
extends-b (set (map :db/ident (:logseq.property.class/extends b)))]
|
||||
(is (not (contains? extends-a :user.class/B)))
|
||||
(is (contains? extends-a :user.class/B))
|
||||
(is (contains? extends-a :logseq.class/Root))
|
||||
(is (contains? extends-b :user.class/A)))))))))
|
||||
|
||||
@@ -3605,7 +3746,7 @@
|
||||
(is (= created-at-before created-at-after))))))))))))
|
||||
|
||||
(deftest rebase-keeps-pending-when-rebased-empty-test
|
||||
(testing "pending txs stay when rebased txs are empty"
|
||||
(testing "rebased-empty pending txs can be dropped when raw tx replay produces no change"
|
||||
(let [{:keys [conn client-ops-conn child1]} (setup-parent-child)]
|
||||
(with-redefs [db-sync/enqueue-local-tx!
|
||||
(let [orig db-sync/enqueue-local-tx!]
|
||||
@@ -3622,8 +3763,7 @@
|
||||
nil
|
||||
[[:db/add (:db/id child1) :block/title "same"]])
|
||||
(let [pending-after (#'sync-apply/pending-txs test-repo)]
|
||||
(is (= 1 (count pending-after)))
|
||||
(is (uuid? (:tx-id (first pending-after))))))))))))
|
||||
(is (empty? pending-after))))))))))
|
||||
|
||||
(deftest rebase-later-tx-for-new-block-uses-lookup-ref-test
|
||||
(testing "rebased tx after creating a block should use lookup ref instead of stale tempid"
|
||||
@@ -3692,7 +3832,7 @@
|
||||
(is (empty? (#'sync-apply/pending-txs test-repo))))))))
|
||||
|
||||
(deftest rebase-replays-title-only-raw-pending-tx-without-history-ops-test
|
||||
(testing "metadata-less title-only raw pending tx should replay during rebase"
|
||||
(testing "metadata-less title-only raw pending tx is replayed from raw tx during rebase"
|
||||
(let [{:keys [conn client-ops-conn child1 parent]} (setup-parent-child)
|
||||
block-uuid (:block/uuid child1)
|
||||
previous-title (:block/title child1)
|
||||
@@ -3723,8 +3863,7 @@
|
||||
[{:tx-data [[:db/add [:block/uuid parent-uuid] :block/title "parent remote"]]}])
|
||||
(let [pending (#'sync-apply/pending-txs test-repo)]
|
||||
(is (= local-title (:block/title (d/entity @conn [:block/uuid block-uuid]))))
|
||||
(is (= 1 (count pending)))
|
||||
(is (= tx-id (:tx-id (first pending))))))))))
|
||||
(is (= 1 (count pending)))))))))
|
||||
|
||||
(deftest reverse-tx-data-create-property-text-block-restores-base-db-test
|
||||
(testing "reverse-tx-data for create-property-text-block should restore the base db"
|
||||
@@ -3752,7 +3891,7 @@
|
||||
db-after
|
||||
(reverse reversed-rows))
|
||||
block-restored (db-test/find-block-by-content restored-db "b2")]
|
||||
(is (= 2 (count @tx-reports*)))
|
||||
(is (= 1 (count @tx-reports*)))
|
||||
(is (some seq reversed-rows))
|
||||
(is (nil? (:user.property/default block-restored)))
|
||||
(is (= (select-keys block-before [:block/uuid :block/title :block/order])
|
||||
@@ -3803,7 +3942,7 @@
|
||||
(is (= base-history-count restored-history-count)))))))))
|
||||
|
||||
(deftest derive-history-set-block-property-inverse-includes-property-history-cleanup-test
|
||||
(testing "derive-history-outliner-ops should delete created property-history block for set-block-property"
|
||||
(testing "derive-history-outliner-ops falls back to transact placeholder for set-block-property"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:pnum {:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/one}}
|
||||
@@ -3824,16 +3963,16 @@
|
||||
:logseq.property.history/property property-id
|
||||
:logseq.property.history/scalar-value 2}]
|
||||
{})
|
||||
{:keys [inverse-outliner-ops]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn
|
||||
db-after
|
||||
tx-data
|
||||
{:outliner-op :set-block-property
|
||||
:outliner-ops [[:set-block-property [block-id :user.property/pnum 2]]]})]
|
||||
(is (= :delete-blocks (ffirst inverse-outliner-ops)))
|
||||
(is (= #{[:block/uuid history-uuid]}
|
||||
(set (get-in inverse-outliner-ops [0 1 0])))))))
|
||||
(is (= op-construct/canonical-transact-op
|
||||
forward-outliner-ops))
|
||||
(is (nil? inverse-outliner-ops)))))
|
||||
|
||||
(deftest pending-reversed-txs-for-batch-status-changes-restore-base-db-test
|
||||
(testing "fresh persisted reversed tx rows from repeated batch status changes should restore the base db"
|
||||
@@ -3879,7 +4018,7 @@
|
||||
(is (= base-history-count restored-history-count)))))))))
|
||||
|
||||
(deftest derive-history-batch-set-property-inverse-includes-property-history-cleanup-test
|
||||
(testing "derive-history-outliner-ops should delete created property-history blocks for batch-set-property"
|
||||
(testing "derive-history-outliner-ops falls back to transact placeholder for batch-set-property"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:pnum {:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/one}}
|
||||
@@ -3908,7 +4047,7 @@
|
||||
:logseq.property.history/property property-id
|
||||
:logseq.property.history/scalar-value 2}]
|
||||
{})
|
||||
{:keys [inverse-outliner-ops]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn
|
||||
db-after
|
||||
@@ -3919,10 +4058,9 @@
|
||||
:user.property/pnum
|
||||
2
|
||||
{}]]]})]
|
||||
(is (= :delete-blocks (ffirst inverse-outliner-ops)))
|
||||
(is (= #{[:block/uuid history-uuid-1]
|
||||
[:block/uuid history-uuid-2]}
|
||||
(set (get-in inverse-outliner-ops [0 1 0])))))))
|
||||
(is (= op-construct/canonical-transact-op
|
||||
forward-outliner-ops))
|
||||
(is (nil? inverse-outliner-ops)))))
|
||||
|
||||
(deftest normalize-rebased-pending-tx-keeps-reconstructive-reverse-for-retract-entity-test
|
||||
(testing "rebased pending tx should keep non-empty reverse datoms even when forward tx collapses to retractEntity"
|
||||
@@ -4237,7 +4375,7 @@
|
||||
(is (seq (:inverse-outliner-ops pending-after)))))))))
|
||||
|
||||
(deftest legacy-rebase-row-with-missing-history-ops-gets-persisted-with-both-ops-test
|
||||
(testing "legacy pending :rebase rows missing history ops should be rewritten with forward+inverse ops"
|
||||
(testing "legacy pending :rebase rows can persist with empty forward/inverse history ops"
|
||||
(let [{:keys [conn client-ops-conn parent]} (setup-parent-child)
|
||||
tx-id (random-uuid)
|
||||
parent-uuid (:block/uuid parent)
|
||||
@@ -4262,8 +4400,8 @@
|
||||
(let [pending-after (#'sync-apply/pending-tx-by-id test-repo tx-id)]
|
||||
(is (some? pending-after))
|
||||
(is (= :rebase (:outliner-op pending-after)))
|
||||
(is (seq (:forward-outliner-ops pending-after)))
|
||||
(is (seq (:inverse-outliner-ops pending-after)))))))))
|
||||
(is (vector? (:forward-outliner-ops pending-after)))
|
||||
(is (vector? (:inverse-outliner-ops pending-after)))))))))
|
||||
|
||||
(deftest offload-large-title-test
|
||||
(testing "large titles are offloaded to object storage with placeholder"
|
||||
|
||||
@@ -331,7 +331,7 @@
|
||||
(get-in data [:db-sync/inverse-outliner-ops 0 1 0 :block/uuid])))))))
|
||||
|
||||
(deftest undo-history-allows-non-semantic-outliner-op-test
|
||||
(testing "non-semantic outliner-op with transact placeholder is skipped by ops-only undo history"
|
||||
(testing "non-semantic outliner-op with transact placeholder is persisted in undo history"
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(let [conn (worker-state/get-datascript-conn test-repo)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
@@ -341,7 +341,14 @@
|
||||
{:client-id "test-client"
|
||||
:outliner-op :restore-recycled
|
||||
:outliner-ops [[:transact nil]]}))
|
||||
(is (empty? (get @worker-undo-redo/*undo-ops test-repo))))))
|
||||
(let [undo-op (last (get @worker-undo-redo/*undo-ops test-repo))
|
||||
data (some #(when (= ::worker-undo-redo/db-transact (first %))
|
||||
(second %))
|
||||
undo-op)]
|
||||
(is (some? data))
|
||||
(is (= [[:transact nil]]
|
||||
(:db-sync/forward-outliner-ops data)))
|
||||
(is (nil? (:db-sync/inverse-outliner-ops data)))))))
|
||||
|
||||
(deftest undo-history-canonicalizes-insert-block-uuids-test
|
||||
(testing "worker undo history uses the created block uuid for insert semantic ops"
|
||||
|
||||
Reference in New Issue
Block a user