mirror of
https://github.com/logseq/logseq.git
synced 2026-02-01 22:47:36 +00:00
018-logseq-cli-add-tags-builtin-properties.md (2)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user