018-logseq-cli-add-tags-builtin-properties.md (2)

This commit is contained in:
rcmerci
2026-01-27 19:59:40 +08:00
parent 2b614ab8e7
commit 7aba504052
6 changed files with 421 additions and 178 deletions

View File

@@ -38,6 +38,7 @@ Add block supports setting tags on all inserted blocks.
Add page supports setting tags on the created page.
Add block supports setting built-in properties with correct type coercion.
Add page supports setting built-in properties with correct type coercion.
Add block and add page accept tag and property references by id, :block/title, or :db/ident for --tags and --properties.
CLI rejects non built-in properties for these new options.
CLI rejects built-in properties that are not public and provides a clear error message.
CLI rejects adding non public tags to blocks.
@@ -110,6 +111,8 @@ Add new options to content-add-spec in src/main/logseq/cli/command/add.cljs for
Add new options to add-page-spec in src/main/logseq/cli/command/add.cljs for tags and properties.
Use a single EDN map option like --properties for multiple properties and a repeated option like --property for key value pairs if needed.
Use a single EDN vector option like --tags and a repeated option like --tag for convenience.
For --tags, allow each entry to be either a tag id, :db/ident, or the tag page's :block/title.
For --properties, allow each property key to be a property id, :db/ident, or the property's :block/title.
Update command summary and help output in src/main/logseq/cli/command/core.cljs if needed to reflect new options.
### 2. Tag resolution helpers

View File

@@ -26,13 +26,17 @@
[data-dir repo]
(node-path/join (repo-dir data-dir repo) "db-worker.lock"))
(defn- pid-alive?
(defn- pid-status
[pid]
(when (number? pid)
(try
(.kill js/process pid 0)
true
(catch :default _ false))))
:alive
(catch :default e
(case (.-code e)
"ESRCH" :not-found
"EPERM" :no-permission
:error)))))
(defn read-lock
[path]
@@ -53,9 +57,9 @@
(let [data-dir (resolve-data-dir data-dir)
path (lock-path data-dir repo)
existing (read-lock path)]
(when (and existing (pid-alive? (:pid existing)))
(when (and existing (contains? #{:alive :no-permission} (pid-status (:pid existing))))
(throw (ex-info "graph already locked" {:code :repo-locked :lock existing})))
(when existing
(when (and existing (= :not-found (pid-status (:pid existing))))
(remove-lock! path))
(fs/mkdirSync (node-path/dirname path) #js {:recursive true})
(let [fd (fs/openSync path "wx")

View File

@@ -18,8 +18,8 @@
{:content {:desc "Block content for add"}
:blocks {:desc "EDN vector of blocks for add"}
:blocks-file {:desc "EDN file of blocks for add"}
:tags {:desc "EDN vector of tags"}
:properties {:desc "EDN map of built-in properties"}
:tags {:desc "EDN vector of tags (id, :db/ident, or :block/title)"}
:properties {:desc "EDN map of built-in properties (id, :db/ident, or :block/title)"}
:target-id {:desc "Target block db/id"
:coerce :long}
:target-uuid {:desc "Target block UUID"}
@@ -29,8 +29,8 @@
(def ^:private add-page-spec
{:page {:desc "Page name"}
:tags {:desc "EDN vector of tags"}
:properties {:desc "EDN map of built-in properties"}})
:tags {:desc "EDN vector of tags (id, :db/ident, or :block/title)"}
:properties {:desc "EDN map of built-in properties (id, :db/ident, or :block/title)"}})
(def entries
[(core/command-entry ["add" "block"] :add-block "Add blocks" content-add-spec)
@@ -120,19 +120,20 @@
(when (seq value)
(common-util/safe-read-string {:log-error? false} value)))
(defn- keyword->name
[value]
(subs (str value) 1))
(defn- normalize-tag-value
[value]
(cond
(uuid? value) value
(number? value) value
(and (string? value) (common-util/uuid-string? (string/trim value)))
(uuid (string/trim value))
(keyword? value) (keyword->name value)
(keyword? value) value
(string? value) (let [text (-> value string/trim (string/replace #"^#+" ""))]
(when (seq text) text))
(cond
(string/blank? text) nil
(common-util/valid-edn-keyword? text)
(common-util/safe-read-string {:log-error? false} text)
:else text))
:else nil))
(defn- parse-tags-option
@@ -153,7 +154,7 @@
:else
(let [tags (mapv normalize-tag-value parsed)]
(if (some nil? tags)
(invalid-options-result "tags must be strings, keywords, or uuids")
(invalid-options-result "tags must be strings, keywords, uuids, or ids")
{:ok? true :value tags}))))))
(defn- normalize-property-key
@@ -169,6 +170,39 @@
:else (keyword text)))
:else nil))
(def ^:private built-in-properties-by-title
(into {}
(keep (fn [[ident {:keys [title]}]]
(when (string? title)
[(common-util/page-name-sanity-lc title) ident])))
db-property/built-in-properties))
(defn- property-title->ident
[value]
(when (string? value)
(let [text (string/trim value)]
(when (seq text)
(get built-in-properties-by-title (common-util/page-name-sanity-lc text))))))
(defn- normalize-property-key-input
[value]
(cond
(keyword? value) {:type :ident :value value}
(number? value) {:type :id :value value}
(string? value)
(let [text (string/trim value)]
(cond
(string/blank? text) nil
(common-util/valid-edn-keyword? text)
(let [parsed (common-util/safe-read-string {:log-error? false} text)]
(when (keyword? parsed)
{:type :ident :value parsed}))
:else
(if-let [ident (property-title->ident text)]
{:type :ident :value ident}
{:type :ident :value (keyword text)})))
:else nil))
(defn- parse-boolean-value
[value]
(cond
@@ -320,27 +354,32 @@
(invalid-options-result "properties must be a non-empty map")
:else
(loop [entries (seq parsed)
(loop [prop-entries (seq parsed)
acc {}]
(if (empty? entries)
(if (empty? prop-entries)
{:ok? true :value acc}
(let [[k v] (first entries)
key* (normalize-property-key k)]
(if-not key*
(let [[k v] (first prop-entries)
key-result (normalize-property-key-input k)]
(if-not key-result
(invalid-options-result (str "invalid property key: " k))
(let [property (get db-property/built-in-properties key*)]
(cond
(nil? property)
(invalid-options-result (str "unknown built-in property: " key*))
(let [{:keys [type value]} key-result
key-ident value]
(if (= type :id)
(recur (rest prop-entries) (assoc acc key-ident v))
(let [property (get db-property/built-in-properties key-ident)]
(cond
(nil? property)
(invalid-options-result (str "unknown built-in property: " key-ident))
(not (property-public? property))
(invalid-options-result (str "property is not public: " key*))
(not (property-public? property))
(invalid-options-result (str "property is not public: " key-ident))
:else
(let [{:keys [ok? value message]} (normalize-property-values property v)]
(if-not ok?
(invalid-options-result (str "invalid value for " key* ": " message))
(recur (rest entries) (assoc acc key* value))))))))))))))
:else
(let [{:keys [ok? value message]} (normalize-property-values property v)
normalized-value value]
(if-not ok?
(invalid-options-result (str "invalid value for " key-ident ": " message))
(recur (rest prop-entries) (assoc acc key-ident normalized-value))))))))))))))))
(defn invalid-options?
[opts]
@@ -375,8 +414,10 @@
(defn- tag-lookup-ref
[tag]
(cond
(number? tag) tag
(uuid? tag) [:block/uuid tag]
(and (string? tag) (common-util/uuid-string? (string/trim tag))) [:block/uuid (uuid (string/trim tag))]
(keyword? tag) [:db/ident tag]
(string? tag) [:block/name (common-util/page-name-sanity-lc tag)]
:else nil))
@@ -475,11 +516,11 @@
:class (resolve-class-id config repo value)
:property (resolve-property-id config repo value)
:entity (resolve-entity-id config repo (cond
(number? value) value
(uuid? value) [:block/uuid value]
(and (string? value) (common-util/uuid-string? (string/trim value)))
[:block/uuid (uuid (string/trim value))]
:else value))
(number? value) value
(uuid? value) [:block/uuid value]
(and (string? value) (common-util/uuid-string? (string/trim value)))
[:block/uuid (uuid (string/trim value))]
:else value))
:node (resolve-node-id config repo value)
:date (resolve-date-page-id config repo value)
(p/resolved value))))
@@ -488,18 +529,70 @@
[config repo properties]
(if-not (seq properties)
(p/resolved nil)
(p/let [entries (p/all
(map (fn [[k v]]
(let [property (get db-property/built-in-properties k)
many? (= :many (get-in property [:schema :cardinality]))
values (if many?
(if (and (coll? v) (not (string? v))) v [v])
[v])]
(p/let [resolved (p/all (map #(resolve-property-value config repo property %) values))
final-value (if many? (vec resolved) (first resolved))]
[k final-value])))
properties))]
(into {} entries))))
(p/let [resolved-entries (p/all
(map (fn [[k v]]
(p/let [{:keys [ident property]}
(cond
(keyword? k)
(let [property (get db-property/built-in-properties k)]
(when-not property
(throw (ex-info "unknown built-in property"
{:code :unknown-property :property k})))
(when-not (property-public? property)
(throw (ex-info "property is not public"
{:code :property-not-public :property k})))
(p/resolved {:ident k :property property}))
(number? k)
(p/let [entity (pull-entity config repo [:db/ident] k)
ident (:db/ident entity)
property (get db-property/built-in-properties ident)]
(cond
(nil? ident)
(throw (ex-info "property not found"
{:code :property-not-found :property k}))
(nil? property)
(throw (ex-info "unknown built-in property"
{:code :unknown-property :property ident}))
(not (property-public? property))
(throw (ex-info "property is not public"
{:code :property-not-public :property ident}))
:else
{:ident ident :property property}))
(string? k)
(let [ident (or (property-title->ident k)
(normalize-property-key k))
property (get db-property/built-in-properties ident)]
(when-not property
(throw (ex-info "unknown built-in property"
{:code :unknown-property :property k})))
(when-not (property-public? property)
(throw (ex-info "property is not public"
{:code :property-not-public :property ident})))
(p/resolved {:ident ident :property property}))
:else
(p/rejected (ex-info "invalid property key"
{:code :invalid-property :property k})))
{:keys [ok? value message]} (normalize-property-values property v)]
(when-not ok?
(throw (ex-info "invalid property value"
{:code :invalid-property-value
:property ident
:message message})))
(let [many? (= :many (get-in property [:schema :cardinality]))
values (if many?
(if (and (coll? value) (not (string? value))) value [value])
[value])]
(p/let [resolved (p/all (map #(resolve-property-value config repo property %) values))
final-value (if many? (vec resolved) (first resolved))]
[ident final-value]))))
properties))]
(into {} resolved-entries))))
(defn- resolve-add-target
[config {:keys [repo target-id target-uuid target-page-name]}]
@@ -582,26 +675,26 @@
properties-result
:else
(if-not (:ok? blocks-result)
blocks-result
(let [vector-result (ensure-blocks (:value blocks-result))]
(if-not (:ok? vector-result)
vector-result
(let [blocks (cond-> (:value vector-result)
ensure-uuids?
ensure-block-uuids)]
{:ok? true
:action {:type :add-block
:repo repo
:graph (core/repo->graph repo)
:target-id (:target-id options)
:target-uuid (some-> (:target-uuid options) string/trim)
:target-page-name (some-> (:target-page-name options) string/trim)
:pos (or (some-> (:pos options) string/trim string/lower-case) "last-child")
:status status
:tags tags
:properties properties
:blocks blocks}}))))))))
(if-not (:ok? blocks-result)
blocks-result
(let [vector-result (ensure-blocks (:value blocks-result))]
(if-not (:ok? vector-result)
vector-result
(let [blocks (cond-> (:value vector-result)
ensure-uuids?
ensure-block-uuids)]
{:ok? true
:action {:type :add-block
:repo repo
:graph (core/repo->graph repo)
:target-id (:target-id options)
:target-uuid (some-> (:target-uuid options) string/trim)
:target-page-name (some-> (:target-page-name options) string/trim)
:pos (or (some-> (:pos options) string/trim string/lower-case) "last-child")
:status status
:tags tags
:properties properties
:blocks blocks}}))))))))
(defn build-add-page-action
[options repo]

View File

@@ -29,13 +29,17 @@
[data-dir repo]
(node-path/join (repo-dir data-dir repo) "db-worker.lock"))
(defn- pid-alive?
(defn- pid-status
[pid]
(when (number? pid)
(try
(.kill js/process pid 0)
true
(catch :default _ false))))
:alive
(catch :default e
(case (.-code e)
"ESRCH" :not-found
"EPERM" :no-permission
:error)))))
(defn- read-lock
[path]
@@ -117,7 +121,7 @@
(nil? lock)
(p/resolved nil)
(not (pid-alive? (:pid lock)))
(= :not-found (pid-status (:pid lock)))
(do
(remove-lock! path)
(p/resolved nil))
@@ -219,13 +223,13 @@
{:ok? true
:data {:repo repo}})
(p/catch (fn [_]
(when (and (pid-alive? (:pid lock))
(when (and (= :alive (pid-status (:pid lock)))
(not= (:pid lock) (.-pid js/process)))
(try
(.kill js/process (:pid lock) "SIGTERM")
(catch :default e
(log/warn :cli-server-stop-sigterm-failed e))))
(when-not (pid-alive? (:pid lock))
(when (= :not-found (pid-status (:pid lock)))
(remove-lock! path))
(if (fs/existsSync path)
{:ok? false

View File

@@ -1,6 +1,7 @@
(ns logseq.cli.commands-test
(:require [cljs.test :refer [async deftest is testing]]
[clojure.string :as string]
[logseq.cli.command.add :as add-command]
[logseq.cli.command.show :as show-command]
[logseq.cli.commands :as commands]
[logseq.cli.server :as cli-server]
@@ -402,6 +403,63 @@
(is (= :invalid-options (get-in result [:error :code]))))))
(deftest test-verb-subcommand-parse-add-remove
(testing "remove requires target"
(let [result (commands/parse-args ["remove"])]
(is (false? (:ok? result)))
(is (= :missing-target (get-in result [:error :code])))))
(testing "remove parses with id"
(let [result (commands/parse-args ["remove" "--id" "10"])]
(is (true? (:ok? result)))
(is (= :remove (:command result)))
(is (= 10 (get-in result [:options :id])))))
(testing "remove parses with uuid"
(let [result (commands/parse-args ["remove" "--uuid" "abc"])]
(is (true? (:ok? result)))
(is (= :remove (:command result)))
(is (= "abc" (get-in result [:options :uuid])))))
(testing "remove parses with page"
(let [result (commands/parse-args ["remove" "--page" "Home"])]
(is (true? (:ok? result)))
(is (= :remove (:command result)))
(is (= "Home" (get-in result [:options :page])))))
(testing "remove rejects multiple selectors"
(let [result (commands/parse-args ["remove" "--id" "1" "--page" "Home"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "remove rejects empty id vector"
(let [result (commands/parse-args ["remove" "--id" "[]"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "remove rejects invalid id vector"
(let [result (commands/parse-args ["remove" "--id" "[1 \"no\"]"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "move requires source selector"
(let [result (commands/parse-args ["move" "--target-id" "10"])]
(is (false? (:ok? result)))
(is (= :missing-source (get-in result [:error :code])))))
(testing "move requires target selector"
(let [result (commands/parse-args ["move" "--id" "1"])]
(is (false? (:ok? result)))
(is (= :missing-target (get-in result [:error :code])))))
(testing "move parses with source and target"
(let [result (commands/parse-args ["move" "--uuid" "abc" "--target-uuid" "def" "--pos" "last-child"])]
(is (true? (:ok? result)))
(is (= :move-block (:command result)))
(is (= "abc" (get-in result [:options :uuid])))
(is (= "def" (get-in result [:options :target-uuid])))
(is (= "last-child" (get-in result [:options :pos]))))))
(deftest test-verb-subcommand-parse-add
(testing "add block requires content source"
(let [result (commands/parse-args ["add" "block"])]
(is (false? (:ok? result)))
@@ -473,63 +531,7 @@
(is (true? (:ok? result)))
(is (= :add-page (:command result)))
(is (= "[\"TagA\"]" (get-in result [:options :tags])))
(is (= "{:logseq.property/publishing-public? true}" (get-in result [:options :properties])))))
(testing "remove requires target"
(let [result (commands/parse-args ["remove"])]
(is (false? (:ok? result)))
(is (= :missing-target (get-in result [:error :code])))))
(testing "remove parses with id"
(let [result (commands/parse-args ["remove" "--id" "10"])]
(is (true? (:ok? result)))
(is (= :remove (:command result)))
(is (= 10 (get-in result [:options :id])))))
(testing "remove parses with uuid"
(let [result (commands/parse-args ["remove" "--uuid" "abc"])]
(is (true? (:ok? result)))
(is (= :remove (:command result)))
(is (= "abc" (get-in result [:options :uuid])))))
(testing "remove parses with page"
(let [result (commands/parse-args ["remove" "--page" "Home"])]
(is (true? (:ok? result)))
(is (= :remove (:command result)))
(is (= "Home" (get-in result [:options :page])))))
(testing "remove rejects multiple selectors"
(let [result (commands/parse-args ["remove" "--id" "1" "--page" "Home"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "remove rejects empty id vector"
(let [result (commands/parse-args ["remove" "--id" "[]"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "remove rejects invalid id vector"
(let [result (commands/parse-args ["remove" "--id" "[1 \"no\"]"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "move requires source selector"
(let [result (commands/parse-args ["move" "--target-id" "10"])]
(is (false? (:ok? result)))
(is (= :missing-source (get-in result [:error :code])))))
(testing "move requires target selector"
(let [result (commands/parse-args ["move" "--id" "1"])]
(is (false? (:ok? result)))
(is (= :missing-target (get-in result [:error :code])))))
(testing "move parses with source and target"
(let [result (commands/parse-args ["move" "--uuid" "abc" "--target-uuid" "def" "--pos" "last-child"])]
(is (true? (:ok? result)))
(is (= :move-block (:command result)))
(is (= "abc" (get-in result [:options :uuid])))
(is (= "def" (get-in result [:options :target-uuid])))
(is (= "last-child" (get-in result [:options :pos]))))))
(is (= "{:logseq.property/publishing-public? true}" (get-in result [:options :properties]))))))
(deftest test-verb-subcommand-parse-move-target-page
(testing "move parses with target page"
@@ -775,6 +777,15 @@
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "add block accepts property title key"
(let [parsed (commands/parse-args ["add" "block"
"--content" "hello"
"--properties" "{\"Publishing Public?\" true}"])
result (commands/build-action parsed {:repo "demo"})]
(is (true? (:ok? result)))
(is (= :logseq.property/publishing-public?
(-> result :action :properties keys first)))))
(testing "add block rejects non-public built-in property"
(let [parsed (commands/parse-args ["add" "block"
"--content" "hello"
@@ -791,6 +802,23 @@
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code]))))))
(deftest test-build-action-add-accepts-tag-ids
(testing "add block accepts numeric tag ids"
(let [parsed (commands/parse-args ["add" "block"
"--content" "hello"
"--tags" "[42]"])
result (commands/build-action parsed {:repo "demo"})]
(is (true? (:ok? result)))
(is (= [42] (get-in result [:action :tags]))))))
(deftest test-tag-lookup-ref-accepts-id
(let [tag-lookup-ref #'add-command/tag-lookup-ref]
(is (= 42 (tag-lookup-ref 42)))))
(deftest test-normalize-property-key-input-accepts-id
(let [normalize-property-key-input #'add-command/normalize-property-key-input]
(is (= {:type :id :value 42} (normalize-property-key-input 42)))))
(deftest test-build-action-move
(testing "move requires source selector"
(let [parsed {:ok? true :command :move-block :options {:target-id 2}}

View File

@@ -6,6 +6,7 @@
[clojure.string :as string]
[frontend.test.node-helper :as node-helper]
[logseq.cli.main :as cli-main]
[logseq.db.frontend.property :as db-property]
[promesa.core :as p]))
(defn- run-cli
@@ -42,6 +43,14 @@
[node]
(or (:block/children node) (:children node)))
(defn- item-id
[item]
(or (:db/id item) (:id item)))
(defn- item-title
[item]
(or (:block/title item) (:block/name item) (:title item) (:name item)))
(defn- find-block-by-title
[node title]
(when node
@@ -49,6 +58,55 @@
node
(some #(find-block-by-title % title) (node-children node)))))
(defn- setup-tags-graph
[data-dir]
(p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
_ (fs/writeFileSync cfg-path "{:output-format :json}")
_ (run-cli ["graph" "create" "--repo" "tags-graph"] data-dir cfg-path)
_ (run-cli ["--repo" "tags-graph" "add" "page" "--page" "Home"] data-dir cfg-path)]
{:cfg-path cfg-path :repo "tags-graph"}))
(defn- stop-repo!
[data-dir cfg-path repo]
(p/let [result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path)]
(parse-json-output result)))
(defn- run-query
[data-dir cfg-path repo query inputs]
(p/let [result (run-cli ["--repo" repo "query" "--query" query "--inputs" inputs]
data-dir cfg-path)]
(parse-json-output result)))
(defn- query-tags
[data-dir cfg-path repo title]
(p/let [payload (run-query data-dir cfg-path repo
"[:find ?tag :in $ ?title :where [?b :block/title ?title] [?b :block/tags ?t] [?t :block/title ?tag]]"
(pr-str [title]))]
(->> (get-in payload [:data :result])
(map first)
set)))
(defn- query-property
[data-dir cfg-path repo title property]
(p/let [payload (run-query data-dir cfg-path repo
(str "[:find ?value :in $ ?title :where [?e :block/title ?title] [?e "
property
" ?value]]")
(pr-str [title]))]
(first (first (get-in payload [:data :result])))))
(defn- list-items
[data-dir cfg-path repo list-type]
(p/let [result (run-cli ["--repo" repo "list" list-type] data-dir cfg-path)]
(parse-json-output result)))
(defn- find-item-id
[items title]
(->> items
(some (fn [item]
(when (= title (item-title item)) item)))
item-id))
(deftest test-cli-graph-list
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker")]
@@ -127,13 +185,10 @@
(is false (str "unexpected error: " e))
(done)))))))
(deftest test-cli-add-tags-and-properties
(deftest test-cli-add-tags-and-properties-by-name
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-tags")]
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
_ (fs/writeFileSync cfg-path "{:output-format :json}")
_ (run-cli ["graph" "create" "--repo" "tags-graph"] data-dir cfg-path)
_ (run-cli ["--repo" "tags-graph" "add" "page" "--page" "Home"] data-dir cfg-path)
(-> (p/let [{:keys [cfg-path repo]} (setup-tags-graph data-dir)
add-page-result (run-cli ["--repo" "tags-graph"
"add" "page"
"--page" "TaggedPage"
@@ -149,49 +204,105 @@
"--properties" "{:logseq.property/deadline \"2026-01-25T12:00:00Z\"}"]
data-dir cfg-path)
add-block-payload (parse-json-output add-block-result)
_ (p/delay 100)
query-block-tags-result (run-cli ["--repo" "tags-graph"
"query"
"--query" "[:find ?tag :in $ ?title :where [?b :block/title ?title] [?b :block/tags ?t] [?t :block/title ?tag]]"
"--inputs" "[\"Tagged block\"]"]
data-dir cfg-path)
query-block-tags-payload (parse-json-output query-block-tags-result)
block-tag-names (->> (get-in query-block-tags-payload [:data :result])
(map first)
set)
query-page-tags-result (run-cli ["--repo" "tags-graph"
"query"
"--query" "[:find ?tag :in $ ?title :where [?p :block/title ?title] [?p :block/tags ?t] [?t :block/title ?tag]]"
"--inputs" "[\"TaggedPage\"]"]
add-block-ident-result (run-cli ["--repo" "tags-graph"
"add" "block"
"--target-page-name" "Home"
"--content" "Tagged block ident"
"--tags" "[:logseq.class/Quote-block]"]
data-dir cfg-path)
query-page-tags-payload (parse-json-output query-page-tags-result)
page-tag-names (->> (get-in query-page-tags-payload [:data :result])
(map first)
set)
query-page-result (run-cli ["--repo" "tags-graph"
"query"
"--query" "[:find ?value :in $ ?title :where [?p :block/title ?title] [?p :logseq.property/publishing-public? ?value]]"
"--inputs" "[\"TaggedPage\"]"]
data-dir cfg-path)
query-page-payload (parse-json-output query-page-result)
page-value (first (first (get-in query-page-payload [:data :result])))
query-block-result (run-cli ["--repo" "tags-graph"
"query"
"--query" "[:find ?value :in $ ?title :where [?b :block/title ?title] [?b :logseq.property/deadline ?value]]"
"--inputs" "[\"Tagged block\"]"]
add-block-ident-payload (parse-json-output add-block-ident-result)
deadline-prop-title (get-in db-property/built-in-properties [:logseq.property/deadline :title])
publishing-prop-title (get-in db-property/built-in-properties [:logseq.property/publishing-public? :title])
add-page-title-result (run-cli ["--repo" "tags-graph"
"add" "page"
"--page" "TaggedPageTitle"
"--properties" (str "{\"" publishing-prop-title "\" true}")]
data-dir cfg-path)
add-page-title-payload (parse-json-output add-page-title-result)
add-block-title-result (run-cli ["--repo" "tags-graph"
"add" "block"
"--target-page-name" "Home"
"--content" "Tagged block title"
"--properties" (str "{\"" deadline-prop-title "\" \"2026-01-25T12:00:00Z\"}")]
data-dir cfg-path)
add-block-title-payload (parse-json-output add-block-title-result)
_ (p/delay 100)
block-tag-names (query-tags data-dir cfg-path repo "Tagged block")
block-ident-tag-names (query-tags data-dir cfg-path repo "Tagged block ident")
page-tag-names (query-tags data-dir cfg-path repo "TaggedPage")
page-value (query-property data-dir cfg-path repo "TaggedPage" ":logseq.property/publishing-public?")
page-title-value (query-property data-dir cfg-path repo "TaggedPageTitle" ":logseq.property/publishing-public?")
block-deadline (query-property data-dir cfg-path repo "Tagged block" ":logseq.property/deadline")
block-deadline-title (query-property data-dir cfg-path repo "Tagged block title" ":logseq.property/deadline")
stop-payload (stop-repo! data-dir cfg-path repo)]
(is (= 0 (:exit-code add-page-result)))
(is (= "ok" (:status add-page-payload)))
(is (= 0 (:exit-code add-block-result)))
(is (= "ok" (:status add-block-payload)))
(is (= 0 (:exit-code add-block-ident-result)))
(is (= "ok" (:status add-block-ident-payload)))
(is (string? deadline-prop-title))
(is (string? publishing-prop-title))
(is (= 0 (:exit-code add-page-title-result)))
(is (= "ok" (:status add-page-title-payload)))
(is (= 0 (:exit-code add-block-title-result)))
(is (= "ok" (:status add-block-title-payload)))
(is (contains? block-tag-names "Quote"))
(is (contains? block-ident-tag-names "Quote"))
(is (contains? page-tag-names "Quote"))
(is (true? page-value))
(is (true? page-title-value))
(is (number? block-deadline))
(is (number? block-deadline-title))
(is (= "ok" (:status stop-payload)))
(done))
(p/catch (fn [e]
(is false (str "unexpected error: " e))
(done)))))))
(deftest test-cli-add-tags-and-properties-by-id
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-tags-id")]
(-> (p/let [{:keys [cfg-path repo]} (setup-tags-graph data-dir)
list-tag-payload (list-items data-dir cfg-path repo "tag")
quote-tag-id (find-item-id (get-in list-tag-payload [:data :items]) "Quote")
list-property-payload (list-items data-dir cfg-path repo "property")
deadline-title (get-in db-property/built-in-properties [:logseq.property/deadline :title])
publishing-title (get-in db-property/built-in-properties [:logseq.property/publishing-public? :title])
deadline-id (find-item-id (get-in list-property-payload [:data :items]) deadline-title)
publishing-id (find-item-id (get-in list-property-payload [:data :items]) publishing-title)
add-page-id-result (run-cli ["--repo" repo
"add" "page"
"--page" "TaggedPageId"
"--tags" (pr-str [quote-tag-id])
"--properties" (pr-str {publishing-id true})]
data-dir cfg-path)
query-block-payload (parse-json-output query-block-result)
block-deadline (first (first (get-in query-block-payload [:data :result])))
stop-result (run-cli ["server" "stop" "--repo" "tags-graph"] data-dir cfg-path)
stop-payload (parse-json-output stop-result)]
(is (= 0 (:exit-code add-page-result)))
(is (= "ok" (:status add-page-payload)))
(is (= 0 (:exit-code add-block-result)))
(is (= "ok" (:status add-block-payload)))
(is (contains? block-tag-names "Quote"))
(is (contains? page-tag-names "Quote"))
(is (true? page-value))
(is (number? block-deadline))
add-page-id-payload (parse-json-output add-page-id-result)
add-block-id-result (run-cli ["--repo" repo
"add" "block"
"--target-page-name" "Home"
"--content" "Tagged block id"
"--tags" (pr-str [quote-tag-id])
"--properties" (pr-str {deadline-id "2026-01-25T12:00:00Z"})]
data-dir cfg-path)
add-block-id-payload (parse-json-output add-block-id-result)
_ (p/delay 100)
page-id-value (query-property data-dir cfg-path repo "TaggedPageId" ":logseq.property/publishing-public?")
block-deadline-id (query-property data-dir cfg-path repo "Tagged block id" ":logseq.property/deadline")
stop-payload (stop-repo! data-dir cfg-path repo)]
(is (= "ok" (:status list-tag-payload)))
(is (number? quote-tag-id))
(is (= "ok" (:status list-property-payload)))
(is (number? deadline-id))
(is (number? publishing-id))
(is (= 0 (:exit-code add-page-id-result))
(pr-str (:error add-page-id-payload)))
(is (= "ok" (:status add-page-id-payload)))
(is (= 0 (:exit-code add-block-id-result))
(pr-str (:error add-block-id-payload)))
(is (= "ok" (:status add-block-id-payload)))
(is (true? page-id-value))
(is (number? block-deadline-id))
(is (= "ok" (:status stop-payload)))
(done))
(p/catch (fn [e]