mirror of
https://github.com/logseq/logseq.git
synced 2026-05-18 01:42:19 +00:00
043-logseq-cli-tag-property-management.md
This commit is contained in:
@@ -34,13 +34,9 @@
|
||||
:tags {:desc "EDN vector of tags. Identifiers can be id, :db/ident, or :block/title."}
|
||||
:properties {:desc "EDN map of built-in properties. Identifiers can be id, :db/ident, or :block/title."}})
|
||||
|
||||
(def ^:private add-tag-spec
|
||||
{:name {:desc "Tag name"}})
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["add" "block"] :add-block "Add blocks" content-add-spec)
|
||||
(core/command-entry ["add" "page"] :add-page "Create page" add-page-spec)
|
||||
(core/command-entry ["add" "tag"] :add-tag "Create tag" add-tag-spec)])
|
||||
(core/command-entry ["add" "page"] :add-page "Create page" add-page-spec)])
|
||||
|
||||
(defn- today-page-title
|
||||
[config repo]
|
||||
@@ -1011,43 +1007,6 @@
|
||||
:error {:code :missing-page-name
|
||||
:message "page name is required"}}))))
|
||||
|
||||
(defn- normalize-tag-name-option
|
||||
[value]
|
||||
(let [normalized (normalize-tag-value value)]
|
||||
(when (string? normalized)
|
||||
(let [name (string/trim normalized)]
|
||||
(when (seq name)
|
||||
name)))))
|
||||
|
||||
(defn build-add-tag-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for add"}}
|
||||
(let [name (normalize-tag-name-option (:name options))]
|
||||
(if (seq name)
|
||||
{:ok? true
|
||||
:action {:type :add-tag
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:name name}}
|
||||
{:ok? false
|
||||
:error {:code :missing-tag-name
|
||||
:message "tag name is required"}}))))
|
||||
|
||||
(defn- pull-page-by-name
|
||||
[config repo page-name]
|
||||
(pull-entity config repo
|
||||
[:db/id :block/name :block/title :block/uuid
|
||||
{:block/tags [:db/id :db/ident :block/name :block/title]}]
|
||||
[:block/name (common-util/page-name-sanity-lc page-name)]))
|
||||
|
||||
(defn- tag-entity?
|
||||
[entity]
|
||||
(some #(= :logseq.class/Tag (:db/ident %))
|
||||
(:block/tags entity)))
|
||||
|
||||
(defn execute-add-block
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
@@ -1134,33 +1093,3 @@
|
||||
created-ids (resolve-created-page-ids cfg (:repo action) (:page action) create-result)]
|
||||
{:status :ok
|
||||
:data {:result created-ids}})))
|
||||
|
||||
(defn execute-add-tag
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
existing (pull-page-by-name cfg (:repo action) (:name action))
|
||||
existing-id (:db/id existing)
|
||||
_ (when (and existing-id (not (tag-entity? existing)))
|
||||
(throw (ex-info "tag already exists as a page and is not a tag"
|
||||
{:code :tag-name-conflict
|
||||
:name (:name action)})))
|
||||
_ (when-not existing-id
|
||||
(transport/invoke cfg :thread-api/apply-outliner-ops false
|
||||
[(:repo action)
|
||||
[[:create-page [(:name action) {:class? true}]]]
|
||||
{}]))
|
||||
page (or (when existing-id existing)
|
||||
(pull-page-by-name cfg (:repo action) (:name action)))
|
||||
page-id (:db/id page)
|
||||
_ (when-not page-id
|
||||
(throw (ex-info "tag not found after create"
|
||||
{:code :tag-not-found
|
||||
:name (:name action)})))
|
||||
_ (when-not (tag-entity? page)
|
||||
(throw (ex-info "created entity is not tagged as :logseq.class/Tag"
|
||||
{:code :tag-create-not-tag
|
||||
:name (:name action)
|
||||
:id page-id})))
|
||||
created-ids (normalize-created-ids [page-id])]
|
||||
{:status :ok
|
||||
:data {:result created-ids}})))
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
(defn top-level-summary
|
||||
[table]
|
||||
(let [groups [{:title "Graph Inspect and Edit"
|
||||
:commands #{"list" "add" "remove" "update" "query" "show"}}
|
||||
:commands #{"list" "add" "upsert" "remove" "update" "query" "show"}}
|
||||
{:title "Graph Management"
|
||||
:commands #{"graph" "server" "doctor"}}]
|
||||
render-group (fn [{:keys [title commands]}]
|
||||
|
||||
@@ -8,27 +8,66 @@
|
||||
[logseq.common.util :as common-util]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private remove-spec
|
||||
(def ^:private remove-block-spec
|
||||
{:id {:desc "Block db/id or EDN vector of ids"}
|
||||
:uuid {:desc "Block UUID"}
|
||||
:page {:desc "Page name"}})
|
||||
:uuid {:desc "Block UUID"}})
|
||||
|
||||
(def ^:private remove-page-spec
|
||||
{:name {:desc "Page name"}})
|
||||
|
||||
(def ^:private remove-entity-spec
|
||||
{:id {:desc "Entity db/id"
|
||||
:coerce :long}
|
||||
:name {:desc "Entity name"}})
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["remove"] :remove "Remove blocks or pages" remove-spec)])
|
||||
[(core/command-entry ["remove" "block"] :remove-block "Remove blocks" remove-block-spec)
|
||||
(core/command-entry ["remove" "page"] :remove-page "Remove page" remove-page-spec)
|
||||
(core/command-entry ["remove" "tag"] :remove-tag "Remove tag" remove-entity-spec)
|
||||
(core/command-entry ["remove" "property"] :remove-property "Remove property" remove-entity-spec)])
|
||||
|
||||
(defn invalid-options?
|
||||
[opts]
|
||||
(let [id-result (id-command/parse-id-option (:id opts))]
|
||||
(cond
|
||||
(and (some? (:id opts)) (not (:ok? id-result)))
|
||||
(:message id-result)
|
||||
[command opts]
|
||||
(case command
|
||||
:remove-block
|
||||
(let [id-result (id-command/parse-id-option (:id opts))
|
||||
selectors (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])]
|
||||
(cond
|
||||
(and (some? (:id opts)) (not (:ok? id-result)))
|
||||
(:message id-result)
|
||||
|
||||
:else
|
||||
nil)))
|
||||
(> (count selectors) 1)
|
||||
"only one of --id or --uuid is allowed"
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
(:remove-tag :remove-property)
|
||||
(let [name (some-> (:name opts) string/trim)
|
||||
selectors (filter some? [(:id opts) name])]
|
||||
(cond
|
||||
(> (count selectors) 1)
|
||||
"only one of --id or --name is allowed"
|
||||
|
||||
(and (contains? opts :name) (string/blank? (or (:name opts) "")))
|
||||
"name must be non-empty"
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
nil))
|
||||
|
||||
(def ^:private block-id-selector
|
||||
[:db/id :block/uuid])
|
||||
|
||||
(def ^:private page-id-selector
|
||||
[:db/id :block/uuid :block/name :block/title])
|
||||
|
||||
(def ^:private entity-selector
|
||||
[:db/id :db/ident :block/uuid :block/name :block/title
|
||||
:logseq.property/type :logseq.property/public? :logseq.property/built-in?
|
||||
{:block/tags [:db/id :db/ident :block/title :block/name]}])
|
||||
|
||||
(defn- fetch-block-by-id
|
||||
[config repo id]
|
||||
(transport/invoke config :thread-api/pull false
|
||||
@@ -48,6 +87,11 @@
|
||||
(transport/invoke config :thread-api/apply-outliner-ops false
|
||||
[repo [[:delete-blocks [ids {}]]] {}]))
|
||||
|
||||
(defn- delete-page-by-uuid
|
||||
[config repo page-uuid]
|
||||
(transport/invoke config :thread-api/apply-outliner-ops false
|
||||
[repo [[:delete-page [page-uuid]]] {}]))
|
||||
|
||||
(defn- remove-block-id
|
||||
[config repo id]
|
||||
(p/let [entity (fetch-block-by-id config repo id)]
|
||||
@@ -74,8 +118,8 @@
|
||||
:missing-ids missing-ids
|
||||
:result result}))
|
||||
|
||||
(defn- perform-remove
|
||||
[config {:keys [repo ids multi-id? uuid page]}]
|
||||
(defn- perform-remove-block
|
||||
[config {:keys [repo ids multi-id? uuid]}]
|
||||
(cond
|
||||
(and (seq ids) multi-id?)
|
||||
(remove-block-ids-best-effort config repo ids)
|
||||
@@ -91,59 +135,288 @@
|
||||
(delete-block-ids config repo [id])
|
||||
(throw (ex-info "block not found" {:code :block-not-found})))))
|
||||
|
||||
(seq page)
|
||||
(p/let [entity (transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id :block/uuid] [:block/name page]])]
|
||||
(if-let [page-uuid (:block/uuid entity)]
|
||||
(transport/invoke config :thread-api/apply-outliner-ops false
|
||||
[repo [[:delete-page [page-uuid]]] {}])
|
||||
(throw (ex-info "page not found" {:code :page-not-found}))))
|
||||
:else
|
||||
(p/rejected (ex-info "block is required" {:code :missing-target}))))
|
||||
|
||||
(defn- resolve-page-by-name
|
||||
[config repo name]
|
||||
(transport/invoke config :thread-api/pull false
|
||||
[repo page-id-selector [:block/name (common-util/page-name-sanity-lc name)]]))
|
||||
|
||||
(defn- item-id
|
||||
[item]
|
||||
(or (:db/id item) (:id item)))
|
||||
|
||||
(defn- item-name
|
||||
[item]
|
||||
(or (:block/title item) (:title item) (:name item) (:block/name item)))
|
||||
|
||||
(defn- normalize-name
|
||||
[value]
|
||||
(common-util/page-name-sanity-lc (or value "")))
|
||||
|
||||
(defn- tag-entity?
|
||||
[entity]
|
||||
(some #(= :logseq.class/Tag (:db/ident %))
|
||||
(:block/tags entity)))
|
||||
|
||||
(defn- property-entity?
|
||||
[entity]
|
||||
(some? (:logseq.property/type entity)))
|
||||
|
||||
(defn- list-matches-by-name
|
||||
[config repo method name]
|
||||
(let [normalized (normalize-name name)]
|
||||
(p/let [items (transport/invoke config method false [repo {:include-built-in true :expand true}])
|
||||
matches (->> (or items [])
|
||||
(filter (fn [item]
|
||||
(= normalized (normalize-name (item-name item)))))
|
||||
vec)]
|
||||
matches)))
|
||||
|
||||
(defn- ambiguous-error
|
||||
[code label name matches]
|
||||
(let [candidates (->> matches
|
||||
(map (fn [item]
|
||||
{:id (item-id item)
|
||||
:name (item-name item)}))
|
||||
(filter :id)
|
||||
vec)]
|
||||
{:code code
|
||||
:message (str "multiple " label "s match name: " name "; rerun with --id")
|
||||
:candidates candidates}))
|
||||
|
||||
(defn- resolve-target
|
||||
[config repo {:keys [id name]} {:keys [list-method not-found-code ambiguous-code label]}]
|
||||
(cond
|
||||
(some? id)
|
||||
(p/resolved {:ok? true
|
||||
:lookup id
|
||||
:id id})
|
||||
|
||||
(seq name)
|
||||
(p/let [matches (list-matches-by-name config repo list-method name)]
|
||||
(cond
|
||||
(empty? matches)
|
||||
{:ok? false
|
||||
:error {:code not-found-code
|
||||
:message (str label " not found")}}
|
||||
|
||||
(> (count matches) 1)
|
||||
{:ok? false
|
||||
:error (ambiguous-error ambiguous-code label name matches)}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:lookup [:block/name (normalize-name (or (item-name (first matches)) name))]
|
||||
:id (item-id (first matches))
|
||||
:name (item-name (first matches))}))
|
||||
|
||||
:else
|
||||
(p/rejected (ex-info "block or page required" {:code :missing-target}))))
|
||||
(p/resolved {:ok? false
|
||||
:error {:code :missing-target
|
||||
:message (str label " name or id is required")}})))
|
||||
|
||||
(defn- validate-tag-target
|
||||
[entity]
|
||||
(cond
|
||||
(nil? (:db/id entity))
|
||||
{:ok? false
|
||||
:error {:code :tag-not-found
|
||||
:message "tag not found"}}
|
||||
|
||||
(not (tag-entity? entity))
|
||||
{:ok? false
|
||||
:error {:code :invalid-tag-target
|
||||
:message "target is not a tag"}}
|
||||
|
||||
(true? (:logseq.property/built-in? entity))
|
||||
{:ok? false
|
||||
:error {:code :tag-built-in
|
||||
:message "built-in tag cannot be removed"}}
|
||||
|
||||
(false? (:logseq.property/public? entity))
|
||||
{:ok? false
|
||||
:error {:code :tag-hidden
|
||||
:message "hidden tag cannot be removed"}}
|
||||
|
||||
(nil? (:block/uuid entity))
|
||||
{:ok? false
|
||||
:error {:code :tag-not-found
|
||||
:message "tag uuid not found"}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:entity entity}))
|
||||
|
||||
(defn- validate-property-target
|
||||
[entity]
|
||||
(cond
|
||||
(nil? (:db/id entity))
|
||||
{:ok? false
|
||||
:error {:code :property-not-found
|
||||
:message "property not found"}}
|
||||
|
||||
(not (property-entity? entity))
|
||||
{:ok? false
|
||||
:error {:code :invalid-property-target
|
||||
:message "target is not a property"}}
|
||||
|
||||
(true? (:logseq.property/built-in? entity))
|
||||
{:ok? false
|
||||
:error {:code :property-built-in
|
||||
:message "built-in property cannot be removed"}}
|
||||
|
||||
(false? (:logseq.property/public? entity))
|
||||
{:ok? false
|
||||
:error {:code :property-hidden
|
||||
:message "hidden property cannot be removed"}}
|
||||
|
||||
(nil? (:block/uuid entity))
|
||||
{:ok? false
|
||||
:error {:code :property-not-found
|
||||
:message "property uuid not found"}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:entity entity}))
|
||||
|
||||
(defn- perform-remove-entity
|
||||
[config action {:keys [list-method not-found-code ambiguous-code label validate-fn]}]
|
||||
(p/let [resolved (resolve-target config (:repo action) action
|
||||
{:list-method list-method
|
||||
:not-found-code not-found-code
|
||||
:ambiguous-code ambiguous-code
|
||||
:label label})]
|
||||
(if-not (:ok? resolved)
|
||||
{:status :error
|
||||
:error (:error resolved)}
|
||||
(p/let [entity (transport/invoke config :thread-api/pull false
|
||||
[(:repo action) entity-selector (:lookup resolved)])
|
||||
validation (validate-fn entity)]
|
||||
(if-not (:ok? validation)
|
||||
{:status :error
|
||||
:error (:error validation)}
|
||||
(p/let [result (delete-page-by-uuid config (:repo action) (:block/uuid entity))]
|
||||
{:status :ok
|
||||
:data {:result result
|
||||
:id (:db/id entity)
|
||||
:name (or (:name resolved) (:block/title entity) (:block/name entity))}}))))))
|
||||
|
||||
(defn build-action
|
||||
[options repo]
|
||||
[command options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for remove"}}
|
||||
(let [id-result (id-command/parse-id-option (:id options))
|
||||
ids (:value id-result)
|
||||
multi-id? (:multi? id-result)
|
||||
uuid (some-> (:uuid options) string/trim)
|
||||
page (some-> (:page options) string/trim)
|
||||
selectors (filter some? [(:id options) uuid page])]
|
||||
(cond
|
||||
(empty? selectors)
|
||||
{:ok? false
|
||||
:error {:code :missing-target
|
||||
:message "block or page is required"}}
|
||||
(case command
|
||||
:remove-block
|
||||
(let [id-result (id-command/parse-id-option (:id options))
|
||||
ids (:value id-result)
|
||||
multi-id? (:multi? id-result)
|
||||
uuid (some-> (:uuid options) string/trim)
|
||||
selectors (filter some? [(:id options) uuid])]
|
||||
(cond
|
||||
(empty? selectors)
|
||||
{:ok? false
|
||||
:error {:code :missing-target
|
||||
:message "block is required"}}
|
||||
|
||||
(> (count selectors) 1)
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message "only one of --id, --uuid, or --page is allowed"}}
|
||||
(> (count selectors) 1)
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message "only one of --id or --uuid is allowed"}}
|
||||
|
||||
(and (some? (:id options)) (not (:ok? id-result)))
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message (:message id-result)}}
|
||||
(and (some? (:id options)) (not (:ok? id-result)))
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message (:message id-result)}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:action {:type :remove
|
||||
:repo repo
|
||||
:id (when (and (seq ids) (not multi-id?)) (first ids))
|
||||
:ids ids
|
||||
:multi-id? multi-id?
|
||||
:uuid uuid
|
||||
:page page}}))))
|
||||
:else
|
||||
{:ok? true
|
||||
:action {:type :remove-block
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:id (when (and (seq ids) (not multi-id?)) (first ids))
|
||||
:ids ids
|
||||
:multi-id? multi-id?
|
||||
:uuid uuid}}))
|
||||
|
||||
(defn execute-remove
|
||||
:remove-page
|
||||
(let [name (some-> (:name options) string/trim)]
|
||||
(if (seq name)
|
||||
{:ok? true
|
||||
:action {:type :remove-page
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:name name}}
|
||||
{:ok? false
|
||||
:error {:code :missing-page-name
|
||||
:message "page name is required"}}))
|
||||
|
||||
(:remove-tag :remove-property)
|
||||
(let [name (some-> (:name options) string/trim)
|
||||
id (:id options)
|
||||
selectors (filter some? [id name])]
|
||||
(cond
|
||||
(empty? selectors)
|
||||
{:ok? false
|
||||
:error {:code :missing-target
|
||||
:message "name or id is required"}}
|
||||
|
||||
(> (count selectors) 1)
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message "only one of --id or --name is allowed"}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:action {:type command
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:id id
|
||||
:name name}}))
|
||||
|
||||
{:ok? false
|
||||
:error {:code :unknown-command
|
||||
:message (str "unknown remove command: " command)}})))
|
||||
|
||||
(defn execute-remove-block
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
result (perform-remove cfg action)]
|
||||
result (perform-remove-block cfg action)]
|
||||
{:status :ok
|
||||
:data (cond-> {:result result}
|
||||
(map? result) (merge (dissoc result :result)))})))
|
||||
|
||||
(defn execute-remove-page
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
entity (resolve-page-by-name cfg (:repo action) (:name action))]
|
||||
(if-let [page-uuid (:block/uuid entity)]
|
||||
(p/let [result (delete-page-by-uuid cfg (:repo action) page-uuid)]
|
||||
{:status :ok
|
||||
:data {:result result}})
|
||||
{:status :error
|
||||
:error {:code :page-not-found
|
||||
:message "page not found"}}))))
|
||||
|
||||
(defn execute-remove-tag
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))]
|
||||
(perform-remove-entity cfg action
|
||||
{:list-method :thread-api/api-list-tags
|
||||
:not-found-code :tag-not-found
|
||||
:ambiguous-code :ambiguous-tag-name
|
||||
:label "tag"
|
||||
:validate-fn validate-tag-target}))))
|
||||
|
||||
(defn execute-remove-property
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))]
|
||||
(perform-remove-entity cfg action
|
||||
{:list-method :thread-api/api-list-properties
|
||||
:not-found-code :property-not-found
|
||||
:ambiguous-code :ambiguous-property-name
|
||||
:label "property"
|
||||
:validate-fn validate-property-target}))))
|
||||
|
||||
227
src/main/logseq/cli/command/upsert.cljs
Normal file
227
src/main/logseq/cli/command/upsert.cljs
Normal file
@@ -0,0 +1,227 @@
|
||||
(ns logseq.cli.command.upsert
|
||||
"Upsert-related CLI commands."
|
||||
(:require [clojure.string :as string]
|
||||
[logseq.cli.command.core :as core]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.transport :as transport]
|
||||
[logseq.common.util :as common-util]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private upsert-tag-spec
|
||||
{:name {:desc "Tag name"}})
|
||||
|
||||
(def ^:private upsert-property-spec
|
||||
{:name {:desc "Property name"}
|
||||
:type {:desc "Property type (default, number, date, datetime, checkbox, url, node, json, string)"}
|
||||
:cardinality {:desc "Property cardinality (one, many)"}
|
||||
:hide {:desc "Hide property"
|
||||
:coerce :boolean}
|
||||
:public {:desc "Set property public visibility"
|
||||
:coerce :boolean}})
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["upsert" "tag"] :upsert-tag "Upsert tag" upsert-tag-spec)
|
||||
(core/command-entry ["upsert" "property"] :upsert-property "Upsert property" upsert-property-spec)])
|
||||
|
||||
(def ^:private property-types
|
||||
#{"default" "number" "date" "datetime" "checkbox" "url" "node" "json" "string"})
|
||||
|
||||
(def ^:private property-cardinalities
|
||||
#{"one" "many"})
|
||||
|
||||
(defn- normalize-tag-name
|
||||
[value]
|
||||
(let [text (some-> value string/trim (string/replace #"^#+" ""))]
|
||||
(when (seq text)
|
||||
text)))
|
||||
|
||||
(defn- normalize-property-name
|
||||
[value]
|
||||
(let [text (some-> value string/trim)]
|
||||
(when (seq text)
|
||||
text)))
|
||||
|
||||
(defn- normalize-property-type
|
||||
[value]
|
||||
(some-> value string/trim string/lower-case))
|
||||
|
||||
(defn- normalize-property-cardinality
|
||||
[value]
|
||||
(let [v (some-> value string/trim string/lower-case)]
|
||||
(case v
|
||||
"db.cardinality/one" "one"
|
||||
"db.cardinality/many" "many"
|
||||
v)))
|
||||
|
||||
(defn invalid-options?
|
||||
[command opts]
|
||||
(case command
|
||||
:upsert-property
|
||||
(let [type' (normalize-property-type (:type opts))
|
||||
cardinality' (normalize-property-cardinality (:cardinality opts))]
|
||||
(cond
|
||||
(and (seq (:type opts)) (not (contains? property-types type')))
|
||||
(str "invalid type: " (:type opts))
|
||||
|
||||
(and (seq (:cardinality opts)) (not (contains? property-cardinalities cardinality')))
|
||||
(str "invalid cardinality: " (:cardinality opts))
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
nil))
|
||||
|
||||
(defn build-tag-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for upsert"}}
|
||||
(let [name (normalize-tag-name (:name options))]
|
||||
(if (seq name)
|
||||
{:ok? true
|
||||
:action {:type :upsert-tag
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:name name}}
|
||||
{:ok? false
|
||||
:error {:code :missing-tag-name
|
||||
:message "tag name is required"}}))))
|
||||
|
||||
(defn- cardinality->db
|
||||
[value]
|
||||
(when-let [v (normalize-property-cardinality value)]
|
||||
(case v
|
||||
"many" :db.cardinality/many
|
||||
"one" :db.cardinality/one
|
||||
nil)))
|
||||
|
||||
(defn- property-schema
|
||||
[options]
|
||||
(cond-> {}
|
||||
(seq (:type options))
|
||||
(assoc :logseq.property/type (keyword (normalize-property-type (:type options))))
|
||||
|
||||
(seq (:cardinality options))
|
||||
(assoc :db/cardinality (cardinality->db (:cardinality options)))
|
||||
|
||||
(contains? options :hide)
|
||||
(assoc :logseq.property/hide? (boolean (:hide options)))
|
||||
|
||||
(contains? options :public)
|
||||
(assoc :logseq.property/public? (boolean (:public options)))))
|
||||
|
||||
(defn build-property-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for upsert"}}
|
||||
(let [name (normalize-property-name (:name options))
|
||||
invalid-message (invalid-options? :upsert-property options)]
|
||||
(cond
|
||||
(not (seq name))
|
||||
{:ok? false
|
||||
:error {:code :missing-property-name
|
||||
:message "property name is required"}}
|
||||
|
||||
(seq invalid-message)
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message invalid-message}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:action {:type :upsert-property
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:name name
|
||||
:schema (property-schema options)}}))))
|
||||
|
||||
(defn- pull-page-by-name
|
||||
[config repo page-name selector]
|
||||
(transport/invoke config :thread-api/pull false
|
||||
[repo selector [:block/name (common-util/page-name-sanity-lc page-name)]]))
|
||||
|
||||
(defn- tag-entity?
|
||||
[entity]
|
||||
(some #(= :logseq.class/Tag (:db/ident %))
|
||||
(:block/tags entity)))
|
||||
|
||||
(defn execute-upsert-tag
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
existing (pull-page-by-name cfg (:repo action) (:name action)
|
||||
[:db/id :block/name :block/title
|
||||
{:block/tags [:db/ident]}])
|
||||
existing-id (:db/id existing)]
|
||||
(cond
|
||||
(and existing-id (not (tag-entity? existing)))
|
||||
{:status :error
|
||||
:error {:code :tag-name-conflict
|
||||
:message "tag already exists as a page and is not a tag"}}
|
||||
|
||||
:else
|
||||
(p/let [_ (when-not existing-id
|
||||
(transport/invoke cfg :thread-api/apply-outliner-ops false
|
||||
[(:repo action)
|
||||
[[:create-page [(:name action) {:class? true}]]]
|
||||
{}]))
|
||||
page (or (when existing-id existing)
|
||||
(pull-page-by-name cfg (:repo action) (:name action)
|
||||
[:db/id :block/name :block/title
|
||||
{:block/tags [:db/ident]}]))
|
||||
page-id (:db/id page)]
|
||||
(cond
|
||||
(not page-id)
|
||||
{:status :error
|
||||
:error {:code :tag-not-found
|
||||
:message "tag not found after upsert"}}
|
||||
|
||||
(not (tag-entity? page))
|
||||
{:status :error
|
||||
:error {:code :tag-create-not-tag
|
||||
:message "created entity is not tagged as :logseq.class/Tag"}}
|
||||
|
||||
:else
|
||||
{:status :ok
|
||||
:data {:result [page-id]}}))))))
|
||||
|
||||
(def ^:private property-selector
|
||||
[:db/id :db/ident :block/name :block/title :logseq.property/type])
|
||||
|
||||
(defn- property-entity?
|
||||
[entity]
|
||||
(some? (:logseq.property/type entity)))
|
||||
|
||||
(defn execute-upsert-property
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
existing (pull-page-by-name cfg (:repo action) (:name action) property-selector)
|
||||
existing-id (:db/id existing)]
|
||||
(cond
|
||||
(and existing-id (not (property-entity? existing)))
|
||||
{:status :error
|
||||
:error {:code :property-name-conflict
|
||||
:message "property already exists as a page and is not a property"}}
|
||||
|
||||
:else
|
||||
(p/let [property-ident (when (property-entity? existing)
|
||||
(:db/ident existing))
|
||||
property-opts (cond-> {}
|
||||
(nil? property-ident)
|
||||
(assoc :property-name (:name action)))
|
||||
_ (transport/invoke cfg :thread-api/apply-outliner-ops false
|
||||
[(:repo action)
|
||||
[[:upsert-property [property-ident
|
||||
(:schema action)
|
||||
property-opts]]]
|
||||
{}])
|
||||
property (pull-page-by-name cfg (:repo action) (:name action) property-selector)
|
||||
property-id (:db/id property)]
|
||||
(if property-id
|
||||
{:status :ok
|
||||
:data {:result [property-id]}}
|
||||
{:status :error
|
||||
:error {:code :property-not-found
|
||||
:message "property not found after upsert"}}))))))
|
||||
@@ -11,6 +11,7 @@
|
||||
[logseq.cli.command.remove :as remove-command]
|
||||
[logseq.cli.command.server :as server-command]
|
||||
[logseq.cli.command.show :as show-command]
|
||||
[logseq.cli.command.upsert :as upsert-command]
|
||||
[logseq.cli.command.update :as update-command]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[promesa.core :as p]))
|
||||
@@ -69,6 +70,13 @@
|
||||
:message "tag name is required"}
|
||||
:summary summary})
|
||||
|
||||
(defn- missing-property-name-result
|
||||
[summary]
|
||||
{:ok? false
|
||||
:error {:code :missing-property-name
|
||||
:message "property name is required"}
|
||||
:summary summary})
|
||||
|
||||
(defn- missing-type-result
|
||||
[summary]
|
||||
{:ok? false
|
||||
@@ -106,6 +114,7 @@
|
||||
server-command/entries
|
||||
list-command/entries
|
||||
add-command/entries
|
||||
upsert-command/entries
|
||||
remove-command/entries
|
||||
update-command/entries
|
||||
query-command/entries
|
||||
@@ -153,9 +162,6 @@
|
||||
(seq (:blocks-file opts))
|
||||
has-args?)
|
||||
show-targets (filter some? [(:id opts) (:uuid opts) (:page opts)])
|
||||
remove-targets (filter some? [(:id opts)
|
||||
(some-> (:uuid opts) string/trim)
|
||||
(some-> (:page opts) string/trim)])
|
||||
update-sources (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])]
|
||||
(cond
|
||||
(:help opts)
|
||||
@@ -174,17 +180,24 @@
|
||||
(and (= command :add-page) (not (seq (:page opts))))
|
||||
(missing-page-name-result summary)
|
||||
|
||||
(and (= command :add-tag) (not (seq (some-> (:name opts) string/trim))))
|
||||
(and (= command :upsert-tag) (not (seq (some-> (:name opts) string/trim))))
|
||||
(missing-tag-name-result summary)
|
||||
|
||||
(and (= command :remove) (seq args))
|
||||
(command-core/invalid-options-result summary "remove does not accept subcommands")
|
||||
(and (= command :upsert-property) (not (seq (some-> (:name opts) string/trim))))
|
||||
(missing-property-name-result summary)
|
||||
|
||||
(and (= command :remove) (empty? remove-targets))
|
||||
(and (= command :upsert-property) (upsert-command/invalid-options? command opts))
|
||||
(command-core/invalid-options-result summary (upsert-command/invalid-options? command opts))
|
||||
|
||||
(and (= command :remove-block) (empty? (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])))
|
||||
(missing-target-result summary)
|
||||
|
||||
(and (= command :remove) (> (count remove-targets) 1))
|
||||
(command-core/invalid-options-result summary "only one of --id, --uuid, or --page is allowed")
|
||||
(and (= command :remove-page) (not (seq (some-> (:name opts) string/trim))))
|
||||
(missing-page-name-result summary)
|
||||
|
||||
(and (#{:remove-tag :remove-property} command)
|
||||
(empty? (filter some? [(:id opts) (some-> (:name opts) string/trim)])))
|
||||
(missing-target-result summary)
|
||||
|
||||
(and (= command :update-block) (update-command/invalid-options? opts))
|
||||
(command-core/invalid-options-result summary (update-command/invalid-options? opts))
|
||||
@@ -207,8 +220,9 @@
|
||||
(list-command/invalid-options? command opts))
|
||||
(command-core/invalid-options-result summary (list-command/invalid-options? command opts))
|
||||
|
||||
(and (= command :remove) (remove-command/invalid-options? opts))
|
||||
(command-core/invalid-options-result summary (remove-command/invalid-options? opts))
|
||||
(and (#{:remove-block :remove-page :remove-tag :remove-property} command)
|
||||
(remove-command/invalid-options? command opts))
|
||||
(command-core/invalid-options-result summary (remove-command/invalid-options? command opts))
|
||||
|
||||
(and (= command :show) (show-command/invalid-options? opts))
|
||||
(command-core/invalid-options-result summary (show-command/invalid-options? opts))
|
||||
@@ -268,7 +282,7 @@
|
||||
:message "missing command"}
|
||||
:summary summary})
|
||||
|
||||
(and (= 1 (count args)) (#{"graph" "server" "list" "add" "query"} (first args)))
|
||||
(and (= 1 (count args)) (#{"graph" "server" "list" "add" "upsert" "remove" "query"} (first args)))
|
||||
(command-core/help-result (command-core/group-summary (first args) table))
|
||||
|
||||
:else
|
||||
@@ -372,14 +386,17 @@
|
||||
:add-page
|
||||
(add-command/build-add-page-action options repo)
|
||||
|
||||
:add-tag
|
||||
(add-command/build-add-tag-action options repo)
|
||||
:upsert-tag
|
||||
(upsert-command/build-tag-action options repo)
|
||||
|
||||
:upsert-property
|
||||
(upsert-command/build-property-action options repo)
|
||||
|
||||
:update-block
|
||||
(update-command/build-action options repo)
|
||||
|
||||
:remove
|
||||
(remove-command/build-action options repo)
|
||||
(:remove-block :remove-page :remove-tag :remove-property)
|
||||
(remove-command/build-action command options repo)
|
||||
|
||||
:query
|
||||
(query-command/build-action options repo config)
|
||||
@@ -423,9 +440,13 @@
|
||||
:list-property (list-command/execute-list-property action config)
|
||||
:add-block (add-command/execute-add-block action config)
|
||||
:add-page (add-command/execute-add-page action config)
|
||||
:add-tag (add-command/execute-add-tag action config)
|
||||
:upsert-tag (upsert-command/execute-upsert-tag action config)
|
||||
:upsert-property (upsert-command/execute-upsert-property action config)
|
||||
:update-block (update-command/execute-update action config)
|
||||
:remove (remove-command/execute-remove action config)
|
||||
:remove-block (remove-command/execute-remove-block action config)
|
||||
:remove-page (remove-command/execute-remove-page action config)
|
||||
:remove-tag (remove-command/execute-remove-tag action config)
|
||||
:remove-property (remove-command/execute-remove-property action config)
|
||||
:query (query-command/execute-query action config)
|
||||
:query-list (query-command/execute-query-list action config)
|
||||
:show (show-command/execute-show action config)
|
||||
@@ -440,8 +461,8 @@
|
||||
:message "unknown action"}}))]
|
||||
(assoc result
|
||||
:command (or (:command action) (:type action))
|
||||
:context (select-keys action [:repo :graph :page :id :ids :uuid :block :blocks
|
||||
:name
|
||||
:context (select-keys action [:repo :graph :page :name :id :ids :uuid :block :blocks
|
||||
:schema
|
||||
:source :target :update-tags :update-properties
|
||||
:remove-tags :remove-properties
|
||||
:export-type :output :import-type :input])))))
|
||||
|
||||
@@ -93,17 +93,30 @@
|
||||
:missing-tag-name "Use --name <tag-name>"
|
||||
:missing-query "Use --query <edn>"
|
||||
:unknown-query "Use `logseq query list` to see available queries"
|
||||
:ambiguous-tag-name "Retry with --id <tag-id>"
|
||||
:ambiguous-property-name "Retry with --id <property-id>"
|
||||
:data-dir-permission "Check filesystem permissions or set LOGSEQ_CLI_DATA_DIR"
|
||||
:server-owned-by-other "Retry from the process owner that started the server"
|
||||
:server-start-timeout-orphan "Check and stop lingering db-worker-node processes, then retry"
|
||||
nil))
|
||||
|
||||
(defn- format-candidates
|
||||
[candidates]
|
||||
(when (seq candidates)
|
||||
(str "\nCandidates:\n"
|
||||
(string/join "\n"
|
||||
(map (fn [{:keys [id name]}]
|
||||
(str " " id " " (or name "-")))
|
||||
candidates)))))
|
||||
|
||||
(defn- format-error
|
||||
[error]
|
||||
(let [{:keys [code message]} error
|
||||
(let [{:keys [code message candidates]} error
|
||||
hint (error-hint error)
|
||||
message* (style/bold-keywords message ["option" "command" "argument"])]
|
||||
message* (style/bold-keywords message ["option" "command" "argument"])
|
||||
candidates* (format-candidates candidates)]
|
||||
(cond-> (str "Error (" (name (or code :error)) "): " message*)
|
||||
candidates* (str candidates*)
|
||||
hint (str "\nHint: " hint))))
|
||||
|
||||
(defn- maybe-ident-header
|
||||
@@ -248,14 +261,37 @@
|
||||
[_context ids]
|
||||
(str "Added tag:\n" (pr-str (vec (or ids [])))))
|
||||
|
||||
(defn- format-remove
|
||||
[{:keys [repo page uuid id ids]}]
|
||||
(defn- format-upsert-tag
|
||||
[_context ids]
|
||||
(str "Upserted tag:\n" (pr-str (vec (or ids [])))))
|
||||
|
||||
(defn- format-upsert-property
|
||||
[_context ids]
|
||||
(str "Upserted property:\n" (pr-str (vec (or ids [])))))
|
||||
|
||||
(defn- format-remove-block
|
||||
[{:keys [repo uuid id ids]}]
|
||||
(cond
|
||||
(seq page) (str "Removed page: " page " (repo: " repo ")")
|
||||
(seq uuid) (str "Removed block: " uuid " (repo: " repo ")")
|
||||
(seq ids) (str "Removed blocks: " (count ids) " (repo: " repo ")")
|
||||
(some? id) (str "Removed block: " id " (repo: " repo ")")
|
||||
:else (str "Removed item (repo: " repo ")")))
|
||||
:else (str "Removed block (repo: " repo ")")))
|
||||
|
||||
(defn- format-remove-page
|
||||
[{:keys [repo name]}]
|
||||
(str "Removed page: " name " (repo: " repo ")"))
|
||||
|
||||
(defn- format-remove-tag
|
||||
[{:keys [repo name id]}]
|
||||
(if (seq name)
|
||||
(str "Removed tag: " name " (repo: " repo ")")
|
||||
(str "Removed tag: " id " (repo: " repo ")")))
|
||||
|
||||
(defn- format-remove-property
|
||||
[{:keys [repo name id]}]
|
||||
(if (seq name)
|
||||
(str "Removed property: " name " (repo: " repo ")")
|
||||
(str "Removed property: " id " (repo: " repo ")")))
|
||||
|
||||
(defn- format-update-block
|
||||
[{:keys [repo source target update-tags update-properties remove-tags remove-properties]}]
|
||||
@@ -319,7 +355,12 @@
|
||||
:add-block (format-add-block context (:result data))
|
||||
:add-page (format-add-page context (:result data))
|
||||
:add-tag (format-add-tag context (:result data))
|
||||
:remove (format-remove context)
|
||||
:upsert-tag (format-upsert-tag context (:result data))
|
||||
:upsert-property (format-upsert-property context (:result data))
|
||||
:remove-block (format-remove-block context)
|
||||
:remove-page (format-remove-page context)
|
||||
:remove-tag (format-remove-tag context)
|
||||
:remove-property (format-remove-property context)
|
||||
:update-block (format-update-block context)
|
||||
:graph-export (format-graph-export context)
|
||||
:graph-import (format-graph-import context)
|
||||
|
||||
Reference in New Issue
Block a user