diff --git a/docs/agent-guide/018-logseq-cli-add-tags-builtin-properties.md b/docs/agent-guide/018-logseq-cli-add-tags-builtin-properties.md index 44d617761f..f9158c0b4b 100644 --- a/docs/agent-guide/018-logseq-cli-add-tags-builtin-properties.md +++ b/docs/agent-guide/018-logseq-cli-add-tags-builtin-properties.md @@ -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 diff --git a/src/main/frontend/worker/db_worker_node_lock.cljs b/src/main/frontend/worker/db_worker_node_lock.cljs index d272bb4b08..41cbe87711 100644 --- a/src/main/frontend/worker/db_worker_node_lock.cljs +++ b/src/main/frontend/worker/db_worker_node_lock.cljs @@ -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") diff --git a/src/main/logseq/cli/command/add.cljs b/src/main/logseq/cli/command/add.cljs index 46d6438308..98d4b45333 100644 --- a/src/main/logseq/cli/command/add.cljs +++ b/src/main/logseq/cli/command/add.cljs @@ -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] diff --git a/src/main/logseq/cli/server.cljs b/src/main/logseq/cli/server.cljs index dfadd11647..ac01e79d3c 100644 --- a/src/main/logseq/cli/server.cljs +++ b/src/main/logseq/cli/server.cljs @@ -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 diff --git a/src/test/logseq/cli/commands_test.cljs b/src/test/logseq/cli/commands_test.cljs index 3b2f94877c..2adad4054b 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -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}} diff --git a/src/test/logseq/cli/integration_test.cljs b/src/test/logseq/cli/integration_test.cljs index bc42047d4e..d3b4b626cc 100644 --- a/src/test/logseq/cli/integration_test.cljs +++ b/src/test/logseq/cli/integration_test.cljs @@ -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]