mirror of
https://github.com/logseq/logseq.git
synced 2026-05-19 18:32:41 +00:00
impl 007-logseq-cli-thread-api-and-command-split.md
This commit is contained in:
@@ -68,6 +68,22 @@
|
||||
payload
|
||||
(ldb/write-transit-str payload)))
|
||||
|
||||
(defn- normalize-method-kw
|
||||
[method]
|
||||
(cond
|
||||
(keyword? method) method
|
||||
(string? method) (keyword method)
|
||||
(nil? method) nil
|
||||
:else (keyword (str method))))
|
||||
|
||||
(defn- normalize-method-str
|
||||
[method]
|
||||
(cond
|
||||
(keyword? method) (subs (str method) 1)
|
||||
(string? method) method
|
||||
(nil? method) nil
|
||||
:else (str method)))
|
||||
|
||||
(defn- handle-event!
|
||||
[type payload]
|
||||
(let [event (js/JSON.stringify (clj->js {:type type}
|
||||
@@ -90,7 +106,7 @@
|
||||
(swap! *sse-clients disj res))))
|
||||
|
||||
(defn- <invoke!
|
||||
[^js proxy method direct-pass? args]
|
||||
[^js proxy method-str method-kw direct-pass? args]
|
||||
(let [args' (if direct-pass?
|
||||
(into-array (or args []))
|
||||
(if (string? args)
|
||||
@@ -100,22 +116,24 @@
|
||||
timeout-id (js/setTimeout
|
||||
(fn []
|
||||
(log/warn :db-worker-node-invoke-timeout
|
||||
{:method method
|
||||
{:method (or method-kw method-str)
|
||||
:elapsed-ms (- (js/Date.now) started-at)}))
|
||||
10000)]
|
||||
(-> (.remoteInvoke proxy method (boolean direct-pass?) args')
|
||||
(-> (.remoteInvoke proxy method-str (boolean direct-pass?) args')
|
||||
(p/finally (fn []
|
||||
(js/clearTimeout timeout-id))))))
|
||||
|
||||
(defn- <init-worker!
|
||||
[proxy rtc-ws-url]
|
||||
(<invoke! proxy "thread-api/init" true #js [rtc-ws-url]))
|
||||
(let [method-kw :thread-api/init
|
||||
method-str (normalize-method-str method-kw)]
|
||||
(<invoke! proxy method-str method-kw true #js [rtc-ws-url])))
|
||||
|
||||
(def ^:private non-repo-methods
|
||||
#{"thread-api/init"
|
||||
"thread-api/list-db"
|
||||
"thread-api/get-version"
|
||||
"thread-api/set-infer-worker-proxy"})
|
||||
#{:thread-api/init
|
||||
:thread-api/list-db
|
||||
:thread-api/get-version
|
||||
:thread-api/set-infer-worker-proxy})
|
||||
|
||||
(defn- repo-arg
|
||||
[args]
|
||||
@@ -126,23 +144,24 @@
|
||||
|
||||
(defn- repo-error
|
||||
[method args bound-repo]
|
||||
(when-not (contains? non-repo-methods method)
|
||||
(let [repo (repo-arg args)]
|
||||
(cond
|
||||
(not (seq repo))
|
||||
{:status 400
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required"}}
|
||||
(let [method-kw (normalize-method-kw method)]
|
||||
(when-not (contains? non-repo-methods method-kw)
|
||||
(let [repo (repo-arg args)]
|
||||
(cond
|
||||
(not (seq repo))
|
||||
{:status 400
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required"}}
|
||||
|
||||
(not= repo bound-repo)
|
||||
{:status 409
|
||||
:error {:code :repo-mismatch
|
||||
:message "repo does not match bound repo"
|
||||
:repo repo
|
||||
:bound-repo bound-repo}}
|
||||
(not= repo bound-repo)
|
||||
{:status 409
|
||||
:error {:code :repo-mismatch
|
||||
:message "repo does not match bound repo"
|
||||
:repo repo
|
||||
:bound-repo bound-repo}}
|
||||
|
||||
:else
|
||||
nil))))
|
||||
:else
|
||||
nil)))))
|
||||
|
||||
(defn- set-main-thread-stub!
|
||||
[]
|
||||
@@ -177,6 +196,8 @@
|
||||
(-> (p/let [body (<read-body req)
|
||||
payload (js/JSON.parse body)
|
||||
{:keys [method directPass argsTransit args]} (js->clj payload :keywordize-keys true)
|
||||
method-kw (normalize-method-kw method)
|
||||
method-str (normalize-method-str method)
|
||||
direct-pass? (boolean directPass)
|
||||
args' (if direct-pass?
|
||||
args
|
||||
@@ -186,9 +207,9 @@
|
||||
(if (string? args')
|
||||
(ldb/read-transit-str args')
|
||||
args'))]
|
||||
(if-let [{:keys [status error]} (repo-error method args-for-validation bound-repo)]
|
||||
(if-let [{:keys [status error]} (repo-error method-kw args-for-validation bound-repo)]
|
||||
(send-json! res status {:ok false :error error})
|
||||
(p/let [result (<invoke! proxy method direct-pass? args')]
|
||||
(p/let [result (<invoke! proxy method-str method-kw direct-pass? args')]
|
||||
(send-json! res 200 (if direct-pass?
|
||||
{:ok true :result result}
|
||||
{:ok true :resultTransit result})))))
|
||||
@@ -311,7 +332,9 @@
|
||||
:host host
|
||||
:port port})
|
||||
_ (reset! *lock-info {:path path :lock lock})
|
||||
_ (<invoke! proxy "thread-api/create-or-open-db" false [repo {}])]
|
||||
_ (let [method-kw :thread-api/create-or-open-db
|
||||
method-str (normalize-method-str method-kw)]
|
||||
(<invoke! proxy method-str method-kw false [repo {}]))]
|
||||
(let [stop!* (atom nil)
|
||||
server (make-server proxy {:auth-token auth-token
|
||||
:bound-repo repo
|
||||
|
||||
147
src/main/logseq/cli/command/add.cljs
Normal file
147
src/main/logseq/cli/command/add.cljs
Normal file
@@ -0,0 +1,147 @@
|
||||
(ns logseq.cli.command.add
|
||||
"Add-related CLI commands."
|
||||
(:require ["fs" :as fs]
|
||||
[cljs-time.coerce :as tc]
|
||||
[cljs.reader :as reader]
|
||||
[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]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private content-add-spec
|
||||
{:content {:desc "Block content for add"}
|
||||
:blocks {:desc "EDN vector of blocks for add"}
|
||||
:blocks-file {:desc "EDN file of blocks for add"}
|
||||
:page {:desc "Page name"}
|
||||
:parent {:desc "Parent block UUID for add"}})
|
||||
|
||||
(def ^:private add-page-spec
|
||||
{:page {:desc "Page 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)])
|
||||
|
||||
(defn- today-page-title
|
||||
[config repo]
|
||||
(p/let [journal (transport/invoke config :thread-api/pull false
|
||||
[repo [:logseq.property.journal/title-format] :logseq.class/Journal])
|
||||
formatter (or (:logseq.property.journal/title-format journal) "MMM do, yyyy")
|
||||
now (tc/from-date (js/Date.))]
|
||||
(date-time-util/format now formatter)))
|
||||
|
||||
(defn- ensure-page!
|
||||
[config repo page-name]
|
||||
(p/let [page (transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id :block/uuid :block/name :block/title] [:block/name page-name]])]
|
||||
(if (:db/id page)
|
||||
page
|
||||
(p/let [_ (transport/invoke config :thread-api/apply-outliner-ops false
|
||||
[repo [[:create-page [page-name {}]]] {}])]
|
||||
(transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id :block/uuid :block/name :block/title] [:block/name page-name]])))))
|
||||
|
||||
(defn- resolve-add-target
|
||||
[config {:keys [repo page parent]}]
|
||||
(if (seq parent)
|
||||
(if-not (common-util/uuid-string? parent)
|
||||
(p/rejected (ex-info "parent must be a uuid" {:code :invalid-parent}))
|
||||
(p/let [block (transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id :block/uuid :block/title] [:block/uuid (uuid parent)]])]
|
||||
(if-let [id (:db/id block)]
|
||||
id
|
||||
(throw (ex-info "parent block not found" {:code :parent-not-found})))))
|
||||
(p/let [page-name (if (seq page) page (today-page-title config repo))
|
||||
page-entity (ensure-page! config repo page-name)]
|
||||
(or (:db/id page-entity)
|
||||
(throw (ex-info "page not found" {:code :page-not-found}))))))
|
||||
|
||||
(defn- read-blocks
|
||||
[options command-args]
|
||||
(cond
|
||||
(seq (:blocks options))
|
||||
{:ok? true :value (reader/read-string (:blocks options))}
|
||||
|
||||
(seq (:blocks-file options))
|
||||
(let [contents (.toString (fs/readFileSync (:blocks-file options)) "utf8")]
|
||||
{:ok? true :value (reader/read-string contents)})
|
||||
|
||||
(seq (:content options))
|
||||
{:ok? true :value [{:block/title (:content options)}]}
|
||||
|
||||
(seq command-args)
|
||||
{:ok? true :value [{:block/title (string/join " " command-args)}]}
|
||||
|
||||
:else
|
||||
{:ok? false
|
||||
:error {:code :missing-content
|
||||
:message "content is required"}}))
|
||||
|
||||
(defn- ensure-blocks
|
||||
[value]
|
||||
(if (vector? value)
|
||||
{:ok? true :value value}
|
||||
{:ok? false
|
||||
:error {:code :invalid-blocks
|
||||
:message "blocks must be a vector"}}))
|
||||
|
||||
(defn build-add-block-action
|
||||
[options args repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for add"}}
|
||||
(let [blocks-result (read-blocks options args)]
|
||||
(if-not (:ok? blocks-result)
|
||||
blocks-result
|
||||
(let [vector-result (ensure-blocks (:value blocks-result))]
|
||||
(if-not (:ok? vector-result)
|
||||
vector-result
|
||||
{:ok? true
|
||||
:action {:type :add-block
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:page (:page options)
|
||||
:parent (:parent options)
|
||||
:blocks (:value vector-result)}}))))))
|
||||
|
||||
(defn build-add-page-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for add"}}
|
||||
(let [page (some-> (:page options) string/trim)]
|
||||
(if (seq page)
|
||||
{:ok? true
|
||||
:action {:type :add-page
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:page page}}
|
||||
{:ok? false
|
||||
:error {:code :missing-page-name
|
||||
:message "page name is required"}}))))
|
||||
|
||||
(defn execute-add-block
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
target-id (resolve-add-target cfg action)
|
||||
ops [[:insert-blocks [(:blocks action)
|
||||
target-id
|
||||
{:sibling? false
|
||||
:bottom? true
|
||||
:outliner-op :insert-blocks}]]]
|
||||
result (transport/invoke cfg :thread-api/apply-outliner-ops false [(:repo action) ops {}])]
|
||||
{:status :ok
|
||||
:data {:result result}})))
|
||||
|
||||
(defn execute-add-page
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
ops [[:create-page [(:page action) {}]]]
|
||||
result (transport/invoke cfg :thread-api/apply-outliner-ops false [(:repo action) ops {}])]
|
||||
{:status :ok
|
||||
:data {:result result}})))
|
||||
216
src/main/logseq/cli/command/core.cljs
Normal file
216
src/main/logseq/cli/command/core.cljs
Normal file
@@ -0,0 +1,216 @@
|
||||
(ns logseq.cli.command.core
|
||||
"Shared CLI parsing utilities."
|
||||
(:require [babashka.cli :as cli]
|
||||
[clojure.string :as string]
|
||||
[logseq.common.config :as common-config]))
|
||||
|
||||
(def ^:private global-spec*
|
||||
{:help {:alias :h
|
||||
:desc "Show help"
|
||||
:coerce :boolean}
|
||||
:config {:desc "Path to cli.edn"}
|
||||
:auth-token {:desc "Auth token for db-worker-node"}
|
||||
:repo {:desc "Graph name"}
|
||||
:data-dir {:desc "Path to db-worker data dir"}
|
||||
:timeout-ms {:desc "Request timeout in ms"
|
||||
:coerce :long}
|
||||
:retries {:desc "Retry count for requests"
|
||||
:coerce :long}
|
||||
:output {:desc "Output format (human, json, edn)"}})
|
||||
|
||||
(defn global-spec
|
||||
[]
|
||||
global-spec*)
|
||||
|
||||
(defn- merge-spec
|
||||
[spec]
|
||||
(merge global-spec* (or spec {})))
|
||||
|
||||
(defn command-entry
|
||||
[cmds command desc spec]
|
||||
(let [spec* (merge-spec spec)]
|
||||
{:cmds cmds
|
||||
:command command
|
||||
:desc desc
|
||||
:spec spec*
|
||||
:restrict true
|
||||
:fn (fn [{:keys [opts args]}]
|
||||
{:command command
|
||||
:cmds cmds
|
||||
:spec spec*
|
||||
:opts opts
|
||||
:args args})}))
|
||||
|
||||
(defn- format-commands
|
||||
[table]
|
||||
(let [rows (->> table
|
||||
(filter (comp seq :cmds))
|
||||
(map (fn [{:keys [cmds desc spec]}]
|
||||
(let [command (str (string/join " " cmds)
|
||||
(when (seq spec) " [options]"))]
|
||||
{:command command
|
||||
:desc desc}))))
|
||||
width (apply max 0 (map (comp count :command) rows))]
|
||||
(->> rows
|
||||
(map (fn [{:keys [command desc]}]
|
||||
(let [padding (apply str (repeat (- width (count command)) " "))]
|
||||
(cond-> (str " " command padding)
|
||||
(seq desc) (str " " desc)))))
|
||||
(string/join "\n"))))
|
||||
|
||||
(defn group-summary
|
||||
[group table]
|
||||
(let [group-table (filter #(= group (first (:cmds %))) table)]
|
||||
(string/join "\n"
|
||||
[(str "Usage: logseq " group " <subcommand> [options]")
|
||||
""
|
||||
"Subcommands:"
|
||||
(format-commands group-table)
|
||||
""
|
||||
"Global options:"
|
||||
(cli/format-opts {:spec global-spec*})
|
||||
""
|
||||
"Command options:"
|
||||
(str " See `logseq " group " <subcommand> --help`")])))
|
||||
|
||||
(defn top-level-summary
|
||||
[table]
|
||||
(let [groups [{:title "Graph Inspect and Edit"
|
||||
:commands #{"list" "add" "remove" "search" "show"}}
|
||||
{:title "Graph Management"
|
||||
:commands #{"graph" "server"}}]
|
||||
render-group (fn [{:keys [title commands]}]
|
||||
(let [entries (filter #(contains? commands (first (:cmds %))) table)]
|
||||
(string/join "\n" [title (format-commands entries)])))]
|
||||
(string/join "\n"
|
||||
["Usage: logseq <command> [options]"
|
||||
""
|
||||
"Commands:"
|
||||
(string/join "\n\n" (map render-group groups))
|
||||
""
|
||||
"Global options:"
|
||||
(cli/format-opts {:spec global-spec*})
|
||||
""
|
||||
"Command options:"
|
||||
" See `logseq <command> --help`"])))
|
||||
|
||||
(defn command-summary
|
||||
[{:keys [cmds spec]}]
|
||||
(let [command-spec (apply dissoc spec (keys global-spec*))]
|
||||
(string/join "\n"
|
||||
[(str "Usage: logseq " (string/join " " cmds) " [options]")
|
||||
""
|
||||
"Global options:"
|
||||
(cli/format-opts {:spec global-spec*})
|
||||
""
|
||||
"Command options:"
|
||||
(cli/format-opts {:spec command-spec})])))
|
||||
|
||||
(defn normalize-opts
|
||||
[opts]
|
||||
(cond-> opts
|
||||
(:config opts) (-> (assoc :config-path (:config opts))
|
||||
(dissoc :config))))
|
||||
|
||||
(defn ok-result
|
||||
[command opts args summary]
|
||||
{:ok? true
|
||||
:command command
|
||||
:options (normalize-opts opts)
|
||||
:args (vec args)
|
||||
:summary summary})
|
||||
|
||||
(defn help-result
|
||||
[summary]
|
||||
{:ok? false
|
||||
:help? true
|
||||
:summary summary})
|
||||
|
||||
(defn invalid-options-result
|
||||
[summary message]
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message message}
|
||||
:summary summary})
|
||||
|
||||
(defn unknown-command-result
|
||||
[summary message]
|
||||
{:ok? false
|
||||
:error {:code :unknown-command
|
||||
:message message}
|
||||
:summary summary})
|
||||
|
||||
(def ^:private global-aliases
|
||||
(->> global-spec*
|
||||
(keep (fn [[k {:keys [alias]}]]
|
||||
(when alias
|
||||
[alias k])))
|
||||
(into {})))
|
||||
|
||||
(def ^:private global-flag-options
|
||||
(->> global-spec*
|
||||
(keep (fn [[k {:keys [coerce]}]]
|
||||
(when (= coerce :boolean) k)))
|
||||
(set)))
|
||||
|
||||
(defn- global-opt-key
|
||||
[token]
|
||||
(cond
|
||||
(string/starts-with? token "--")
|
||||
(keyword (subs token 2))
|
||||
|
||||
(and (string/starts-with? token "-")
|
||||
(= 2 (count token)))
|
||||
(get global-aliases (keyword (subs token 1)))
|
||||
|
||||
:else nil))
|
||||
|
||||
(defn parse-leading-global-opts
|
||||
[args]
|
||||
(loop [remaining args
|
||||
opts {}]
|
||||
(if (empty? remaining)
|
||||
{:opts opts :args []}
|
||||
(let [token (first remaining)]
|
||||
(if-let [opt-key (global-opt-key token)]
|
||||
(if (contains? global-flag-options opt-key)
|
||||
(recur (rest remaining) (assoc opts opt-key true))
|
||||
(if-let [value (second remaining)]
|
||||
(recur (drop 2 remaining) (assoc opts opt-key value))
|
||||
{:opts opts :args (rest remaining)}))
|
||||
{:opts opts :args remaining})))))
|
||||
|
||||
(defn legacy-graph-opt?
|
||||
[raw-args]
|
||||
(some (fn [token]
|
||||
(or (= token "--graph")
|
||||
(string/starts-with? token "--graph=")))
|
||||
raw-args))
|
||||
|
||||
(defn cli-error->result
|
||||
[summary {:keys [msg]}]
|
||||
(invalid-options-result summary (or msg "invalid options")))
|
||||
|
||||
(defn graph->repo
|
||||
[graph]
|
||||
(when (seq graph)
|
||||
(if (string/starts-with? graph common-config/db-version-prefix)
|
||||
graph
|
||||
(str common-config/db-version-prefix graph))))
|
||||
|
||||
(defn repo->graph
|
||||
[repo]
|
||||
(when (seq repo)
|
||||
(string/replace-first repo common-config/db-version-prefix "")))
|
||||
|
||||
(defn resolve-repo
|
||||
[graph]
|
||||
(let [graph (some-> graph string/trim)]
|
||||
(when (seq graph)
|
||||
(graph->repo graph))))
|
||||
|
||||
(defn pick-graph
|
||||
[options command-args config]
|
||||
(or (:repo options)
|
||||
(first command-args)
|
||||
(:repo config)))
|
||||
226
src/main/logseq/cli/command/graph.cljs
Normal file
226
src/main/logseq/cli/command/graph.cljs
Normal file
@@ -0,0 +1,226 @@
|
||||
(ns logseq.cli.command.graph
|
||||
"Graph-related CLI commands."
|
||||
(:require [clojure.string :as string]
|
||||
[logseq.cli.command.core :as core]
|
||||
[logseq.cli.config :as cli-config]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.transport :as transport]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private graph-export-spec
|
||||
{:type {:desc "Export type (edn, sqlite)"}
|
||||
:output {:desc "Output path"}})
|
||||
|
||||
(def ^:private graph-import-spec
|
||||
{:type {:desc "Import type (edn, sqlite)"}
|
||||
:input {:desc "Input path"}})
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["graph" "list"] :graph-list "List graphs" {})
|
||||
(core/command-entry ["graph" "create"] :graph-create "Create graph" {})
|
||||
(core/command-entry ["graph" "switch"] :graph-switch "Switch current graph" {})
|
||||
(core/command-entry ["graph" "remove"] :graph-remove "Remove graph" {})
|
||||
(core/command-entry ["graph" "validate"] :graph-validate "Validate graph" {})
|
||||
(core/command-entry ["graph" "info"] :graph-info "Graph metadata" {})
|
||||
(core/command-entry ["graph" "export"] :graph-export "Export graph" graph-export-spec)
|
||||
(core/command-entry ["graph" "import"] :graph-import "Import graph" graph-import-spec)])
|
||||
|
||||
(def ^:private import-export-types*
|
||||
#{"edn" "sqlite"})
|
||||
|
||||
(defn import-export-types
|
||||
[]
|
||||
import-export-types*)
|
||||
|
||||
(defn normalize-import-export-type
|
||||
[value]
|
||||
(some-> value string/lower-case string/trim))
|
||||
|
||||
(defn- missing-graph-error
|
||||
[]
|
||||
{:ok? false
|
||||
:error {:code :missing-graph
|
||||
:message "graph name is required"}})
|
||||
|
||||
(defn build-graph-action
|
||||
[command graph repo]
|
||||
(case command
|
||||
:graph-list
|
||||
{:ok? true
|
||||
:action {:type :graph-list
|
||||
:command :graph-list}}
|
||||
|
||||
:graph-create
|
||||
(if-not (seq graph)
|
||||
(missing-graph-error)
|
||||
{:ok? true
|
||||
:action {:type :invoke
|
||||
:command :graph-create
|
||||
:method :thread-api/create-or-open-db
|
||||
:direct-pass? false
|
||||
:args [repo {}]
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:allow-missing-graph true
|
||||
:persist-repo (core/repo->graph repo)}})
|
||||
|
||||
:graph-switch
|
||||
(if-not (seq graph)
|
||||
(missing-graph-error)
|
||||
{:ok? true
|
||||
:action {:type :graph-switch
|
||||
:command :graph-switch
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)}})
|
||||
|
||||
:graph-remove
|
||||
(if-not (seq graph)
|
||||
(missing-graph-error)
|
||||
{:ok? true
|
||||
:action {:type :invoke
|
||||
:command :graph-remove
|
||||
:method :thread-api/unsafe-unlink-db
|
||||
:direct-pass? false
|
||||
:args [repo]
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)}})
|
||||
|
||||
:graph-validate
|
||||
(if-not (seq repo)
|
||||
(missing-graph-error)
|
||||
{:ok? true
|
||||
:action {:type :invoke
|
||||
:command :graph-validate
|
||||
:method :thread-api/validate-db
|
||||
:direct-pass? false
|
||||
:args [repo]
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)}})
|
||||
|
||||
:graph-info
|
||||
(if-not (seq repo)
|
||||
(missing-graph-error)
|
||||
{:ok? true
|
||||
:action {:type :graph-info
|
||||
:command :graph-info
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)}})))
|
||||
|
||||
(defn build-export-action
|
||||
[repo export-type output]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for export"}}
|
||||
{:ok? true
|
||||
:action {:type :graph-export
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:export-type export-type
|
||||
:output output}}))
|
||||
|
||||
(defn build-import-action
|
||||
[repo import-type input]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for import"}}
|
||||
{:ok? true
|
||||
:action {:type :graph-import
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:import-type import-type
|
||||
:input input
|
||||
:allow-missing-graph true}}))
|
||||
|
||||
(defn execute-graph-list
|
||||
[_action config]
|
||||
(let [graphs (cli-server/list-graphs config)]
|
||||
{:status :ok
|
||||
:data {:graphs graphs}}))
|
||||
|
||||
(defn execute-invoke
|
||||
[action config]
|
||||
(-> (p/let [cfg (if-let [repo (:repo action)]
|
||||
(cli-server/ensure-server! config repo)
|
||||
(p/resolved config))
|
||||
result (transport/invoke cfg
|
||||
(:method action)
|
||||
(:direct-pass? action)
|
||||
(:args action))]
|
||||
(when-let [repo (:persist-repo action)]
|
||||
(cli-config/update-config! config {:repo repo}))
|
||||
(if-let [write (:write action)]
|
||||
(let [{:keys [format path]} write]
|
||||
(transport/write-output {:format format :path path :data result})
|
||||
{:status :ok
|
||||
:data {:message (str "wrote " path)}})
|
||||
{:status :ok :data {:result result}}))))
|
||||
|
||||
(defn execute-graph-switch
|
||||
[action config]
|
||||
(-> (p/let [graphs (cli-server/list-graphs config)
|
||||
graph (:graph action)]
|
||||
(if-not (some #(= graph %) graphs)
|
||||
{:status :error
|
||||
:error {:code :graph-not-found
|
||||
:message (str "graph not found: " graph)}}
|
||||
(p/let [_ (cli-server/ensure-server! config (:repo action))]
|
||||
(cli-config/update-config! config {:repo graph})
|
||||
{:status :ok
|
||||
:data {:message (str "switched to " graph)}})))))
|
||||
|
||||
(defn execute-graph-info
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
created (transport/invoke cfg :thread-api/pull false [(:repo action) [:kv/value] :logseq.kv/graph-created-at])
|
||||
schema (transport/invoke cfg :thread-api/pull false [(:repo action) [:kv/value] :logseq.kv/schema-version])]
|
||||
{:status :ok
|
||||
:data {:graph (:graph action)
|
||||
:logseq.kv/graph-created-at (:kv/value created)
|
||||
:logseq.kv/schema-version (:kv/value schema)}})))
|
||||
|
||||
(defn execute-graph-export
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
export-type (:export-type action)
|
||||
export-result (case export-type
|
||||
"edn"
|
||||
(transport/invoke cfg
|
||||
:thread-api/export-edn
|
||||
false
|
||||
[(:repo action) {:export-type :graph}])
|
||||
"sqlite"
|
||||
(transport/invoke cfg
|
||||
:thread-api/export-db-base64
|
||||
true
|
||||
[(:repo action)])
|
||||
(throw (ex-info "unsupported export type" {:export-type export-type})))
|
||||
data (if (= export-type "sqlite")
|
||||
(js/Buffer.from export-result "base64")
|
||||
export-result)
|
||||
format (if (= export-type "sqlite") :sqlite :edn)]
|
||||
(transport/write-output {:format format :path (:output action) :data data})
|
||||
{:status :ok
|
||||
:data {:message (str "wrote " (:output action))}})))
|
||||
|
||||
(defn execute-graph-import
|
||||
[action config]
|
||||
(-> (p/let [_ (cli-server/stop-server! config (:repo action))
|
||||
cfg (cli-server/ensure-server! config (:repo action))
|
||||
import-type (:import-type action)
|
||||
input-data (case import-type
|
||||
"edn" (transport/read-input {:format :edn :path (:input action)})
|
||||
"sqlite" (transport/read-input {:format :sqlite :path (:input action)})
|
||||
(throw (ex-info "unsupported import type" {:import-type import-type})))
|
||||
payload (if (= import-type "sqlite")
|
||||
(.toString (js/Buffer.from input-data) "base64")
|
||||
input-data)
|
||||
method (if (= import-type "sqlite")
|
||||
:thread-api/import-db-base64
|
||||
:thread-api/import-edn)
|
||||
direct-pass? (= import-type "sqlite")
|
||||
_ (transport/invoke cfg method direct-pass? [(:repo action) payload])
|
||||
_ (cli-server/restart-server! config (:repo action))]
|
||||
{:status :ok
|
||||
:data {:message (str "imported " import-type " from " (:input action))}})))
|
||||
212
src/main/logseq/cli/command/list.cljs
Normal file
212
src/main/logseq/cli/command/list.cljs
Normal file
@@ -0,0 +1,212 @@
|
||||
(ns logseq.cli.command.list
|
||||
"List-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]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private list-common-spec
|
||||
{:expand {:desc "Include expanded metadata"
|
||||
:coerce :boolean}
|
||||
:limit {:desc "Limit results"
|
||||
:coerce :long}
|
||||
:offset {:desc "Offset results"
|
||||
:coerce :long}
|
||||
:sort {:desc "Sort field"}
|
||||
:order {:desc "Sort order (asc, desc)"}})
|
||||
|
||||
(def ^:private list-page-spec
|
||||
(merge list-common-spec
|
||||
{:include-journal {:desc "Include journal pages"
|
||||
:coerce :boolean}
|
||||
:journal-only {:desc "Only journal pages"
|
||||
:coerce :boolean}
|
||||
:include-hidden {:desc "Include hidden pages"
|
||||
:coerce :boolean}
|
||||
:updated-after {:desc "Filter by updated-at (ISO8601)"}
|
||||
:created-after {:desc "Filter by created-at (ISO8601)"}
|
||||
:fields {:desc "Select output fields (comma separated)"}}))
|
||||
|
||||
(def ^:private list-tag-spec
|
||||
(merge list-common-spec
|
||||
{:include-built-in {:desc "Include built-in tags"
|
||||
:coerce :boolean}
|
||||
:with-properties {:desc "Include tag properties"
|
||||
:coerce :boolean}
|
||||
:with-extends {:desc "Include tag extends"
|
||||
:coerce :boolean}
|
||||
:fields {:desc "Select output fields (comma separated)"}}))
|
||||
|
||||
(def ^:private list-property-spec
|
||||
(merge list-common-spec
|
||||
{:include-built-in {:desc "Include built-in properties"
|
||||
:coerce :boolean}
|
||||
:with-classes {:desc "Include property classes"
|
||||
:coerce :boolean}
|
||||
:with-type {:desc "Include property type"
|
||||
:coerce :boolean}
|
||||
:fields {:desc "Select output fields (comma separated)"}}))
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["list" "page"] :list-page "List pages" list-page-spec)
|
||||
(core/command-entry ["list" "tag"] :list-tag "List tags" list-tag-spec)
|
||||
(core/command-entry ["list" "property"] :list-property "List properties" list-property-spec)])
|
||||
|
||||
(def ^:private list-sort-fields
|
||||
{:list-page #{"title" "created-at" "updated-at"}
|
||||
:list-tag #{"name" "title"}
|
||||
:list-property #{"name" "title"}})
|
||||
|
||||
(defn invalid-options?
|
||||
[command opts]
|
||||
(let [{:keys [order include-journal journal-only]} opts
|
||||
sort-field (:sort opts)
|
||||
allowed (get list-sort-fields command)]
|
||||
(cond
|
||||
(and include-journal journal-only)
|
||||
"include-journal and journal-only are mutually exclusive"
|
||||
|
||||
(and (seq sort-field) (not (contains? allowed sort-field)))
|
||||
(str "invalid sort field: " sort-field)
|
||||
|
||||
(and (seq order) (not (#{"asc" "desc"} order)))
|
||||
(str "invalid order: " order)
|
||||
|
||||
:else nil)))
|
||||
|
||||
(defn build-action
|
||||
[command options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for list"}}
|
||||
{:ok? true
|
||||
:action {:type command
|
||||
:repo repo
|
||||
:options options}}))
|
||||
|
||||
(def ^:private list-page-field-map
|
||||
{"title" :block/title
|
||||
"uuid" :block/uuid
|
||||
"created-at" :block/created-at
|
||||
"updated-at" :block/updated-at})
|
||||
|
||||
(def ^:private list-tag-field-map
|
||||
{"name" :block/title
|
||||
"title" :block/title
|
||||
"uuid" :block/uuid
|
||||
"properties" :logseq.property.class/properties
|
||||
"extends" :logseq.property.class/extends
|
||||
"description" :logseq.property/description})
|
||||
|
||||
(def ^:private list-property-field-map
|
||||
{"name" :block/title
|
||||
"title" :block/title
|
||||
"uuid" :block/uuid
|
||||
"classes" :logseq.property/classes
|
||||
"type" :logseq.property/type
|
||||
"description" :logseq.property/description})
|
||||
|
||||
(defn- parse-field-list
|
||||
[fields]
|
||||
(when (seq fields)
|
||||
(->> (string/split fields #",")
|
||||
(map string/trim)
|
||||
(remove string/blank?)
|
||||
vec)))
|
||||
|
||||
(defn- apply-fields
|
||||
[items fields field-map]
|
||||
(if (seq fields)
|
||||
(let [keys (->> fields
|
||||
(map #(get field-map %))
|
||||
(remove nil?)
|
||||
vec)]
|
||||
(if (seq keys)
|
||||
(mapv #(select-keys % keys) items)
|
||||
items))
|
||||
items))
|
||||
|
||||
(defn- apply-sort
|
||||
[items sort-field order field-map]
|
||||
(if (seq sort-field)
|
||||
(let [sort-key (get field-map sort-field)
|
||||
sorted (if sort-key
|
||||
(sort-by #(get % sort-key) items)
|
||||
items)
|
||||
sorted (if (= "desc" order) (reverse sorted) sorted)]
|
||||
(vec sorted))
|
||||
(vec items)))
|
||||
|
||||
(defn- apply-offset-limit
|
||||
[items offset limit]
|
||||
(cond-> items
|
||||
(some? offset) (->> (drop offset) vec)
|
||||
(some? limit) (->> (take limit) vec)))
|
||||
|
||||
(defn- prepare-tag-item
|
||||
[item {:keys [expand with-properties with-extends]}]
|
||||
(if expand
|
||||
(cond-> item
|
||||
(not with-properties) (dissoc :logseq.property.class/properties)
|
||||
(not with-extends) (dissoc :logseq.property.class/extends))
|
||||
item))
|
||||
|
||||
(defn- prepare-property-item
|
||||
[item {:keys [expand with-classes with-type]}]
|
||||
(if expand
|
||||
(cond-> item
|
||||
(not with-classes) (dissoc :logseq.property/classes)
|
||||
(not with-type) (dissoc :logseq.property/type))
|
||||
item))
|
||||
|
||||
(defn execute-list-page
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
options (:options action)
|
||||
items (transport/invoke cfg :thread-api/api-list-pages false
|
||||
[(:repo action) options])
|
||||
order (or (:order options) "asc")
|
||||
fields (parse-field-list (:fields options))
|
||||
sorted (apply-sort items (:sort options) order list-page-field-map)
|
||||
limited (apply-offset-limit sorted (:offset options) (:limit options))
|
||||
final (if (:expand options)
|
||||
(apply-fields limited fields list-page-field-map)
|
||||
limited)]
|
||||
{:status :ok
|
||||
:data {:items final}})))
|
||||
|
||||
(defn execute-list-tag
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
options (:options action)
|
||||
items (transport/invoke cfg :thread-api/api-list-tags false
|
||||
[(:repo action) options])
|
||||
order (or (:order options) "asc")
|
||||
fields (parse-field-list (:fields options))
|
||||
prepared (mapv #(prepare-tag-item % options) items)
|
||||
sorted (apply-sort prepared (:sort options) order list-tag-field-map)
|
||||
limited (apply-offset-limit sorted (:offset options) (:limit options))
|
||||
final (if (:expand options)
|
||||
(apply-fields limited fields list-tag-field-map)
|
||||
limited)]
|
||||
{:status :ok
|
||||
:data {:items final}})))
|
||||
|
||||
(defn execute-list-property
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
options (:options action)
|
||||
items (transport/invoke cfg :thread-api/api-list-properties false
|
||||
[(:repo action) options])
|
||||
order (or (:order options) "asc")
|
||||
fields (parse-field-list (:fields options))
|
||||
prepared (mapv #(prepare-property-item % options) items)
|
||||
sorted (apply-sort prepared (:sort options) order list-property-field-map)
|
||||
limited (apply-offset-limit sorted (:offset options) (:limit options))
|
||||
final (if (:expand options)
|
||||
(apply-fields limited fields list-property-field-map)
|
||||
limited)]
|
||||
{:status :ok
|
||||
:data {:items final}})))
|
||||
81
src/main/logseq/cli/command/remove.cljs
Normal file
81
src/main/logseq/cli/command/remove.cljs
Normal file
@@ -0,0 +1,81 @@
|
||||
(ns logseq.cli.command.remove
|
||||
"Remove-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 remove-block-spec
|
||||
{:block {:desc "Block UUID"}})
|
||||
|
||||
(def ^:private remove-page-spec
|
||||
{:page {:desc "Page name"}})
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["remove" "block"] :remove-block "Remove block" remove-block-spec)
|
||||
(core/command-entry ["remove" "page"] :remove-page "Remove page" remove-page-spec)])
|
||||
|
||||
(defn- perform-remove
|
||||
[config {:keys [repo block page]}]
|
||||
(cond
|
||||
(seq block)
|
||||
(if-not (common-util/uuid-string? block)
|
||||
(p/rejected (ex-info "block must be a uuid" {:code :invalid-block}))
|
||||
(p/let [entity (transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id :block/uuid] [:block/uuid (uuid block)]])]
|
||||
(if-let [id (:db/id entity)]
|
||||
(transport/invoke config :thread-api/apply-outliner-ops false
|
||||
[repo [[:delete-blocks [[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 or page required" {:code :missing-target}))))
|
||||
|
||||
(defn build-remove-block-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for remove"}}
|
||||
(let [block (some-> (:block options) string/trim)]
|
||||
(if (seq block)
|
||||
{:ok? true
|
||||
:action {:type :remove-block
|
||||
:repo repo
|
||||
:block block}}
|
||||
{:ok? false
|
||||
:error {:code :missing-target
|
||||
:message "block is required"}}))))
|
||||
|
||||
(defn build-remove-page-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for remove"}}
|
||||
(let [page (some-> (:page options) string/trim)]
|
||||
(if (seq page)
|
||||
{:ok? true
|
||||
:action {:type :remove-page
|
||||
:repo repo
|
||||
:page page}}
|
||||
{:ok? false
|
||||
:error {:code :missing-target
|
||||
:message "page is required"}}))))
|
||||
|
||||
(defn execute-remove
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
result (perform-remove cfg action)]
|
||||
{:status :ok
|
||||
:data {:result result}})))
|
||||
240
src/main/logseq/cli/command/search.cljs
Normal file
240
src/main/logseq/cli/command/search.cljs
Normal file
@@ -0,0 +1,240 @@
|
||||
(ns logseq.cli.command.search
|
||||
"Search-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]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private search-spec
|
||||
{:text {:desc "Search text"}
|
||||
:type {:desc "Search types (page, block, tag, property, all)"}
|
||||
:tag {:desc "Restrict to a specific tag"}
|
||||
:limit {:desc "Limit results"
|
||||
:coerce :long}
|
||||
:case-sensitive {:desc "Case sensitive search"
|
||||
:coerce :boolean}
|
||||
:include-content {:desc "Search block content"
|
||||
:coerce :boolean}
|
||||
:sort {:desc "Sort field (updated-at, created-at)"}
|
||||
:order {:desc "Sort order (asc, desc)"}})
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["search"] :search "Search graph" search-spec)])
|
||||
|
||||
(def ^:private search-types
|
||||
#{"page" "block" "tag" "property" "all"})
|
||||
|
||||
(defn invalid-options?
|
||||
[opts]
|
||||
(let [type (:type opts)
|
||||
order (:order opts)
|
||||
sort-field (:sort opts)]
|
||||
(cond
|
||||
(and (seq type) (not (contains? search-types type)))
|
||||
(str "invalid type: " type)
|
||||
|
||||
(and (seq sort-field) (not (#{"updated-at" "created-at"} sort-field)))
|
||||
(str "invalid sort field: " sort-field)
|
||||
|
||||
(and (seq order) (not (#{"asc" "desc"} order)))
|
||||
(str "invalid order: " order)
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
(defn build-action
|
||||
[options args repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for search"}}
|
||||
(let [text (or (:text options) (string/join " " args))]
|
||||
(if (seq text)
|
||||
{:ok? true
|
||||
:action {:type :search
|
||||
:repo repo
|
||||
:text text
|
||||
:search-type (:type options)
|
||||
:tag (:tag options)
|
||||
:limit (:limit options)
|
||||
:case-sensitive (:case-sensitive options)
|
||||
:include-content (:include-content options)
|
||||
:sort (:sort options)
|
||||
:order (:order options)}}
|
||||
{:ok? false
|
||||
:error {:code :missing-search-text
|
||||
:message "search text is required"}}))))
|
||||
|
||||
(defn- query-pages
|
||||
[cfg repo text case-sensitive?]
|
||||
(let [query (if case-sensitive?
|
||||
'[:find ?e ?title ?uuid ?updated ?created
|
||||
:in $ ?q
|
||||
:where
|
||||
[?e :block/name ?name]
|
||||
[?e :block/title ?title]
|
||||
[?e :block/uuid ?uuid]
|
||||
[(get-else $ ?e :block/updated-at 0) ?updated]
|
||||
[(get-else $ ?e :block/created-at 0) ?created]
|
||||
[(clojure.string/includes? ?title ?q)]]
|
||||
'[:find ?e ?title ?uuid ?updated ?created
|
||||
:in $ ?q
|
||||
:where
|
||||
[?e :block/name ?name]
|
||||
[?e :block/title ?title]
|
||||
[?e :block/uuid ?uuid]
|
||||
[(get-else $ ?e :block/updated-at 0) ?updated]
|
||||
[(get-else $ ?e :block/created-at 0) ?created]
|
||||
[(clojure.string/includes? (clojure.string/lower-case ?title) ?q)]])
|
||||
q* (if case-sensitive? text (string/lower-case text))]
|
||||
(transport/invoke cfg :thread-api/q false [repo [query q*]])))
|
||||
|
||||
#_{:clj-kondo/ignore [:aliased-namespace-symbol]}
|
||||
(defn- query-blocks
|
||||
[cfg repo text case-sensitive? tag include-content?]
|
||||
(let [has-tag? (seq tag)
|
||||
content-attr (if include-content? :block/content :block/title)
|
||||
query (cond
|
||||
(and case-sensitive? has-tag?)
|
||||
`[:find ?e ?value ?uuid ?updated ?created
|
||||
:in $ ?q ?tag-name
|
||||
:where
|
||||
[?tag :block/name ?tag-name]
|
||||
[?e :block/tags ?tag]
|
||||
[?e ~content-attr ?value]
|
||||
[(missing? $ ?e :block/name)]
|
||||
[?e :block/uuid ?uuid]
|
||||
[(get-else $ ?e :block/updated-at 0) ?updated]
|
||||
[(get-else $ ?e :block/created-at 0) ?created]
|
||||
[(clojure.string/includes? ?value ?q)]]
|
||||
|
||||
case-sensitive?
|
||||
`[:find ?e ?value ?uuid ?updated ?created
|
||||
:in $ ?q
|
||||
:where
|
||||
[?e ~content-attr ?value]
|
||||
[(missing? $ ?e :block/name)]
|
||||
[?e :block/uuid ?uuid]
|
||||
[(get-else $ ?e :block/updated-at 0) ?updated]
|
||||
[(get-else $ ?e :block/created-at 0) ?created]
|
||||
[(clojure.string/includes? ?value ?q)]]
|
||||
|
||||
has-tag?
|
||||
`[:find ?e ?value ?uuid ?updated ?created
|
||||
:in $ ?q ?tag-name
|
||||
:where
|
||||
[?tag :block/name ?tag-name]
|
||||
[?e :block/tags ?tag]
|
||||
[?e ~content-attr ?value]
|
||||
[(missing? $ ?e :block/name)]
|
||||
[?e :block/uuid ?uuid]
|
||||
[(get-else $ ?e :block/updated-at 0) ?updated]
|
||||
[(get-else $ ?e :block/created-at 0) ?created]
|
||||
[(clojure.string/includes? (clojure.string/lower-case ?value) ?q)]]
|
||||
|
||||
:else
|
||||
`[:find ?e ?value ?uuid ?updated ?created
|
||||
:in $ ?q
|
||||
:where
|
||||
[?e ~content-attr ?value]
|
||||
[(missing? $ ?e :block/name)]
|
||||
[?e :block/uuid ?uuid]
|
||||
[(get-else $ ?e :block/updated-at 0) ?updated]
|
||||
[(get-else $ ?e :block/created-at 0) ?created]
|
||||
[(clojure.string/includes? (clojure.string/lower-case ?value) ?q)]])
|
||||
q* (if case-sensitive? text (string/lower-case text))
|
||||
tag-name (some-> tag string/lower-case)]
|
||||
(if has-tag?
|
||||
(transport/invoke cfg :thread-api/q false [repo [query q* tag-name]])
|
||||
(transport/invoke cfg :thread-api/q false [repo [query q*]]))))
|
||||
|
||||
(defn- normalize-search-types
|
||||
[type]
|
||||
(let [type (or type "all")]
|
||||
(case type
|
||||
"page" [:page]
|
||||
"block" [:block]
|
||||
"tag" [:tag]
|
||||
"property" [:property]
|
||||
[:page :block :tag :property])))
|
||||
|
||||
(defn- search-sort-key
|
||||
[item sort-field]
|
||||
(case sort-field
|
||||
"updated-at" (:updated-at item)
|
||||
"created-at" (:created-at item)
|
||||
nil))
|
||||
|
||||
(defn execute-search
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
types (normalize-search-types (:search-type action))
|
||||
case-sensitive? (boolean (:case-sensitive action))
|
||||
text (:text action)
|
||||
tag (:tag action)
|
||||
page-results (when (some #{:page} types)
|
||||
(p/let [rows (query-pages cfg (:repo action) text case-sensitive?)]
|
||||
(mapv (fn [[id title uuid updated created]]
|
||||
{:type "page"
|
||||
:db/id id
|
||||
:title title
|
||||
:uuid (str uuid)
|
||||
:updated-at updated
|
||||
:created-at created})
|
||||
rows)))
|
||||
include-content? (boolean (:include-content action))
|
||||
block-results (when (some #{:block} types)
|
||||
(p/let [rows (query-blocks cfg (:repo action) text case-sensitive? tag include-content?)]
|
||||
(mapv (fn [[id content uuid updated created]]
|
||||
{:type "block"
|
||||
:db/id id
|
||||
:content content
|
||||
:uuid (str uuid)
|
||||
:updated-at updated
|
||||
:created-at created})
|
||||
rows)))
|
||||
tag-results (when (some #{:tag} types)
|
||||
(p/let [items (transport/invoke cfg :thread-api/api-list-tags false
|
||||
[(:repo action) {:expand true :include-built-in true}])
|
||||
q* (if case-sensitive? text (string/lower-case text))]
|
||||
(->> items
|
||||
(filter (fn [item]
|
||||
(let [title (:block/title item)]
|
||||
(if case-sensitive?
|
||||
(string/includes? title q*)
|
||||
(string/includes? (string/lower-case title) q*)))))
|
||||
(mapv (fn [item]
|
||||
{:type "tag"
|
||||
:title (:block/title item)
|
||||
:uuid (:block/uuid item)})))))
|
||||
property-results (when (some #{:property} types)
|
||||
(p/let [items (transport/invoke cfg :thread-api/api-list-properties false
|
||||
[(:repo action) {:expand true :include-built-in true}])
|
||||
q* (if case-sensitive? text (string/lower-case text))]
|
||||
(->> items
|
||||
(filter (fn [item]
|
||||
(let [title (:block/title item)]
|
||||
(if case-sensitive?
|
||||
(string/includes? title q*)
|
||||
(string/includes? (string/lower-case title) q*)))))
|
||||
(mapv (fn [item]
|
||||
{:type "property"
|
||||
:title (:block/title item)
|
||||
:uuid (:block/uuid item)})))))
|
||||
results (->> (concat (or page-results [])
|
||||
(or block-results [])
|
||||
(or tag-results [])
|
||||
(or property-results []))
|
||||
(distinct)
|
||||
vec)
|
||||
sorted (if-let [sort-field (:sort action)]
|
||||
(let [order (or (:order action) "desc")]
|
||||
(->> results
|
||||
(sort-by #(search-sort-key % sort-field))
|
||||
(cond-> (= order "desc") reverse)
|
||||
vec))
|
||||
results)
|
||||
limited (if (some? (:limit action)) (vec (take (:limit action) sorted)) sorted)]
|
||||
{:status :ok
|
||||
:data {:results limited}})))
|
||||
96
src/main/logseq/cli/command/server.cljs
Normal file
96
src/main/logseq/cli/command/server.cljs
Normal file
@@ -0,0 +1,96 @@
|
||||
(ns logseq.cli.command.server
|
||||
"Server-related CLI commands."
|
||||
(:require [logseq.cli.command.core :as core]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private server-spec
|
||||
{:repo {:desc "Graph name"}})
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["server" "list"] :server-list "List db-worker-node servers" {})
|
||||
(core/command-entry ["server" "status"] :server-status "Show server status for a graph" server-spec)
|
||||
(core/command-entry ["server" "start"] :server-start "Start db-worker-node for a graph" server-spec)
|
||||
(core/command-entry ["server" "stop"] :server-stop "Stop db-worker-node for a graph" server-spec)
|
||||
(core/command-entry ["server" "restart"] :server-restart "Restart db-worker-node for a graph" server-spec)])
|
||||
|
||||
(defn build-action
|
||||
[command repo]
|
||||
(case command
|
||||
:server-list
|
||||
{:ok? true
|
||||
:action {:type :server-list}}
|
||||
|
||||
:server-status
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for server status"}}
|
||||
{:ok? true
|
||||
:action {:type :server-status
|
||||
:repo repo}})
|
||||
|
||||
:server-start
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for server start"}}
|
||||
{:ok? true
|
||||
:action {:type :server-start
|
||||
:repo repo}})
|
||||
|
||||
:server-stop
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for server stop"}}
|
||||
{:ok? true
|
||||
:action {:type :server-stop
|
||||
:repo repo}})
|
||||
|
||||
:server-restart
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for server restart"}}
|
||||
{:ok? true
|
||||
:action {:type :server-restart
|
||||
:repo repo}})
|
||||
|
||||
{:ok? false
|
||||
:error {:code :unknown-command
|
||||
:message (str "unknown server command: " command)}}))
|
||||
|
||||
(defn- server-result->response
|
||||
[result]
|
||||
(if (:ok? result)
|
||||
{:status :ok
|
||||
:data (:data result)}
|
||||
{:status :error
|
||||
:error (:error result)}))
|
||||
|
||||
(defn execute-list
|
||||
[_action config]
|
||||
(-> (p/let [servers (cli-server/list-servers config)]
|
||||
{:status :ok
|
||||
:data {:servers servers}})))
|
||||
|
||||
(defn execute-status
|
||||
[action config]
|
||||
(-> (p/let [result (cli-server/server-status config (:repo action))]
|
||||
(server-result->response result))))
|
||||
|
||||
(defn execute-start
|
||||
[action config]
|
||||
(-> (p/let [result (cli-server/start-server! config (:repo action))]
|
||||
(server-result->response result))))
|
||||
|
||||
(defn execute-stop
|
||||
[action config]
|
||||
(-> (p/let [result (cli-server/stop-server! config (:repo action))]
|
||||
(server-result->response result))))
|
||||
|
||||
(defn execute-restart
|
||||
[action config]
|
||||
(-> (p/let [result (cli-server/restart-server! config (:repo action))]
|
||||
(server-result->response result))))
|
||||
189
src/main/logseq/cli/command/show.cljs
Normal file
189
src/main/logseq/cli/command/show.cljs
Normal file
@@ -0,0 +1,189 @@
|
||||
(ns logseq.cli.command.show
|
||||
"Show-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 show-spec
|
||||
{:id {:desc "Block db/id"
|
||||
:coerce :long}
|
||||
:uuid {:desc "Block UUID"}
|
||||
:page-name {:desc "Page name"}
|
||||
:level {:desc "Limit tree depth"
|
||||
:coerce :long}
|
||||
:format {:desc "Output format (text, json, edn)"}})
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["show"] :show "Show tree" show-spec)])
|
||||
|
||||
(def ^:private show-formats
|
||||
#{"text" "json" "edn"})
|
||||
|
||||
(defn invalid-options?
|
||||
[opts]
|
||||
(let [format (:format opts)
|
||||
level (:level opts)]
|
||||
(cond
|
||||
(and (seq format) (not (contains? show-formats (string/lower-case format))))
|
||||
(str "invalid format: " format)
|
||||
|
||||
(and (some? level) (< level 1))
|
||||
"level must be >= 1"
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
(def ^:private tree-block-selector
|
||||
[:db/id :block/uuid :block/title :block/order {:block/parent [:db/id]}])
|
||||
|
||||
(defn- fetch-blocks-for-page
|
||||
[config repo page-id]
|
||||
(let [query [:find (list 'pull '?b tree-block-selector)
|
||||
:in '$ '?page-id
|
||||
:where ['?b :block/page '?page-id]]]
|
||||
(p/let [rows (transport/invoke config :thread-api/q false [repo [query page-id]])]
|
||||
(mapv first rows))))
|
||||
|
||||
(defn- build-tree
|
||||
[blocks root-id max-depth]
|
||||
(let [parent->children (group-by #(get-in % [:block/parent :db/id]) blocks)
|
||||
sort-children (fn [children]
|
||||
(vec (sort-by :block/order children)))
|
||||
build (fn build [parent-id depth]
|
||||
(mapv (fn [b]
|
||||
(let [children (build (:db/id b) (inc depth))]
|
||||
(cond-> b
|
||||
(seq children) (assoc :block/children children))))
|
||||
(if (and max-depth (>= depth max-depth))
|
||||
[]
|
||||
(sort-children (get parent->children parent-id)))))]
|
||||
(build root-id 1)))
|
||||
|
||||
(defn- fetch-tree
|
||||
[config {:keys [repo id page-name level] :as opts}]
|
||||
(let [max-depth (or level 10)
|
||||
uuid-str (:uuid opts)]
|
||||
(cond
|
||||
(some? id)
|
||||
(p/let [entity (transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id :block/name :block/uuid :block/title {:block/page [:db/id :block/title]}] id])]
|
||||
(if-let [page-id (get-in entity [:block/page :db/id])]
|
||||
(p/let [blocks (fetch-blocks-for-page config repo page-id)
|
||||
children (build-tree blocks (:db/id entity) max-depth)]
|
||||
{:root (assoc entity :block/children children)})
|
||||
(if (:db/id entity)
|
||||
(p/let [blocks (fetch-blocks-for-page config repo (:db/id entity))
|
||||
children (build-tree blocks (:db/id entity) max-depth)]
|
||||
{:root (assoc entity :block/children children)})
|
||||
(throw (ex-info "block not found" {:code :block-not-found})))))
|
||||
|
||||
(seq uuid-str)
|
||||
(if-not (common-util/uuid-string? uuid-str)
|
||||
(p/rejected (ex-info "block must be a uuid" {:code :invalid-block}))
|
||||
(p/let [entity (transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id :block/name :block/uuid :block/title {:block/page [:db/id :block/title]}]
|
||||
[:block/uuid (uuid uuid-str)]])
|
||||
entity (if (:db/id entity)
|
||||
entity
|
||||
(transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id :block/name :block/uuid :block/title {:block/page [:db/id :block/title]}]
|
||||
[:block/uuid uuid-str]]))]
|
||||
(if-let [page-id (get-in entity [:block/page :db/id])]
|
||||
(p/let [blocks (fetch-blocks-for-page config repo page-id)
|
||||
children (build-tree blocks (:db/id entity) max-depth)]
|
||||
{:root (assoc entity :block/children children)})
|
||||
(if (:db/id entity)
|
||||
(p/let [blocks (fetch-blocks-for-page config repo (:db/id entity))
|
||||
children (build-tree blocks (:db/id entity) max-depth)]
|
||||
{:root (assoc entity :block/children children)})
|
||||
(throw (ex-info "block not found" {:code :block-not-found}))))))
|
||||
|
||||
(seq page-name)
|
||||
(p/let [page-entity (transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id :block/uuid :block/title] [:block/name page-name]])]
|
||||
(if-let [page-id (:db/id page-entity)]
|
||||
(p/let [blocks (fetch-blocks-for-page config repo page-id)
|
||||
children (build-tree blocks page-id max-depth)]
|
||||
{:root (assoc page-entity :block/children children)})
|
||||
(throw (ex-info "page not found" {:code :page-not-found}))))
|
||||
|
||||
:else
|
||||
(p/rejected (ex-info "block or page required" {:code :missing-target})))))
|
||||
|
||||
(defn tree->text
|
||||
[{:keys [root]}]
|
||||
(let [label (fn [node]
|
||||
(or (:block/title node) (:block/name node) (str (:block/uuid node))))
|
||||
node-id (fn [node]
|
||||
(or (:db/id node) "-"))
|
||||
id-padding (fn [node]
|
||||
(apply str (repeat (inc (count (str (node-id node)))) " ")))
|
||||
split-lines (fn [value]
|
||||
(string/split (or value "") #"\n"))
|
||||
lines (atom [])
|
||||
walk (fn walk [node prefix]
|
||||
(let [children (:block/children node)
|
||||
total (count children)]
|
||||
(doseq [[idx child] (map-indexed vector children)]
|
||||
(let [last-child? (= idx (dec total))
|
||||
branch (if last-child? "└── " "├── ")
|
||||
next-prefix (str prefix (if last-child? " " "│ "))
|
||||
rows (split-lines (label child))
|
||||
first-row (first rows)
|
||||
rest-rows (rest rows)
|
||||
line (str (node-id child) " " prefix branch first-row)]
|
||||
(swap! lines conj line)
|
||||
(doseq [row rest-rows]
|
||||
(swap! lines conj (str (id-padding child) next-prefix row)))
|
||||
(walk child next-prefix)))))]
|
||||
(let [rows (split-lines (label root))
|
||||
first-row (first rows)
|
||||
rest-rows (rest rows)]
|
||||
(swap! lines conj (str (node-id root) " " first-row))
|
||||
(doseq [row rest-rows]
|
||||
(swap! lines conj (str (id-padding root) row))))
|
||||
(walk root "")
|
||||
(string/join "\n" @lines)))
|
||||
|
||||
(defn build-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for show"}}
|
||||
(let [format (some-> (:format options) string/lower-case)
|
||||
targets (filter some? [(:id options) (:uuid options) (:page-name options)])]
|
||||
(if (empty? targets)
|
||||
{:ok? false
|
||||
:error {:code :missing-target
|
||||
:message "block or page is required"}}
|
||||
{:ok? true
|
||||
:action {:type :show
|
||||
:repo repo
|
||||
:id (:id options)
|
||||
:uuid (:uuid options)
|
||||
:page-name (:page-name options)
|
||||
:level (:level options)
|
||||
:format format}}))))
|
||||
|
||||
(defn execute-show
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
tree-data (fetch-tree cfg action)
|
||||
format (:format action)]
|
||||
(case format
|
||||
"edn"
|
||||
{:status :ok
|
||||
:data tree-data
|
||||
:output-format :edn}
|
||||
|
||||
"json"
|
||||
{:status :ok
|
||||
:data tree-data
|
||||
:output-format :json}
|
||||
|
||||
{:status :ok
|
||||
:data {:message (tree->text tree-data)}}))))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -68,7 +68,7 @@
|
||||
(defn- error-hint
|
||||
[{:keys [code]}]
|
||||
(case code
|
||||
:missing-graph "Use --graph <name>"
|
||||
:missing-graph "Use --repo <name>"
|
||||
:missing-repo "Use --repo <name>"
|
||||
:missing-content "Use --content or pass content as args"
|
||||
:missing-search-text "Provide search text or --text"
|
||||
|
||||
@@ -90,11 +90,16 @@
|
||||
[{:keys [base-url auth-token timeout-ms retries]}
|
||||
method direct-pass? args]
|
||||
(let [url (str (string/replace base-url #"/$" "") "/v1/invoke")
|
||||
method* (cond
|
||||
(keyword? method) (subs (str method) 1)
|
||||
(string? method) method
|
||||
(nil? method) nil
|
||||
:else (str method))
|
||||
payload (if direct-pass?
|
||||
{:method method
|
||||
{:method method*
|
||||
:directPass true
|
||||
:args args}
|
||||
{:method method
|
||||
{:method method*
|
||||
:directPass false
|
||||
:argsTransit (ldb/write-transit-str args)})
|
||||
body (js/JSON.stringify (clj->js payload))]
|
||||
|
||||
@@ -180,6 +180,22 @@
|
||||
(is (= "logseq_db_parse_args" (:repo result)))
|
||||
(is (= "/tmp/db-worker" (:data-dir result)))))
|
||||
|
||||
(deftest db-worker-node-repo-error-handles-keyword-methods
|
||||
(let [repo-error #'db-worker-node/repo-error
|
||||
bound-repo "logseq_db_bound"]
|
||||
(is (nil? (repo-error :thread-api/list-db [] bound-repo)))
|
||||
(is (nil? (repo-error "thread-api/list-db" [] bound-repo)))
|
||||
(is (= {:status 400
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required"}}
|
||||
(repo-error :thread-api/create-or-open-db [] bound-repo)))
|
||||
(is (= {:status 409
|
||||
:error {:code :repo-mismatch
|
||||
:message "repo does not match bound repo"
|
||||
:repo "other"
|
||||
:bound-repo bound-repo}}
|
||||
(repo-error :thread-api/create-or-open-db ["other"] bound-repo)))))
|
||||
|
||||
(deftest db-worker-node-daemon-smoke-test
|
||||
(async done
|
||||
(let [daemon (atom nil)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns logseq.cli.commands-test
|
||||
(:require [cljs.test :refer [async deftest is testing]]
|
||||
[clojure.string :as string]
|
||||
[logseq.cli.command.show :as show-command]
|
||||
[logseq.cli.commands :as commands]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.transport :as transport]
|
||||
@@ -127,6 +128,13 @@
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :unknown-command (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-parse-args-rejects-graph-option
|
||||
(testing "rejects legacy --graph option"
|
||||
(let [result (commands/parse-args ["--graph" "demo" "graph" "list"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))
|
||||
(is (= "unknown option: --graph" (get-in result [:error :message]))))))
|
||||
|
||||
(deftest test-parse-args-global-options
|
||||
(testing "global output option is accepted"
|
||||
(let [result (commands/parse-args ["--output" "json" "graph" "list"])]
|
||||
@@ -135,7 +143,7 @@
|
||||
|
||||
(deftest test-tree->text-format
|
||||
(testing "show tree text uses db/id with tree glyphs"
|
||||
(let [tree->text #'commands/tree->text
|
||||
(let [tree->text #'show-command/tree->text
|
||||
tree-data {:root {:db/id 1
|
||||
:block/title "Root"
|
||||
:block/children [{:db/id 2
|
||||
@@ -152,7 +160,7 @@
|
||||
|
||||
(deftest test-tree->text-multiline
|
||||
(testing "show tree text renders multiline blocks under glyph column"
|
||||
(let [tree->text #'commands/tree->text
|
||||
(let [tree->text #'show-command/tree->text
|
||||
tree-data {:root {:db/id 168
|
||||
:block/title "Jan 18th, 2026"
|
||||
:block/children [{:db/id 169
|
||||
@@ -244,7 +252,7 @@
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-verb-subcommand-parse
|
||||
(deftest test-verb-subcommand-parse-add-remove
|
||||
(testing "add block requires content source"
|
||||
(let [result (commands/parse-args ["add" "block"])]
|
||||
(is (false? (:ok? result)))
|
||||
@@ -282,8 +290,9 @@
|
||||
(let [result (commands/parse-args ["remove" "page" "--page" "Home"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :remove-page (:command result)))
|
||||
(is (= "Home" (get-in result [:options :page])))))
|
||||
(is (= "Home" (get-in result [:options :page]))))))
|
||||
|
||||
(deftest test-verb-subcommand-parse-search-show
|
||||
(testing "search requires text"
|
||||
(let [result (commands/parse-args ["search"])]
|
||||
(is (false? (:ok? result)))
|
||||
@@ -304,8 +313,9 @@
|
||||
(let [result (commands/parse-args ["show" "--page-name" "Home"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :show (:command result)))
|
||||
(is (= "Home" (get-in result [:options :page-name])))))
|
||||
(is (= "Home" (get-in result [:options :page-name]))))))
|
||||
|
||||
(deftest test-verb-subcommand-parse-graph-import-export
|
||||
(testing "graph export parses with type and output"
|
||||
(let [result (commands/parse-args ["graph" "export"
|
||||
"--type" "edn"
|
||||
@@ -349,8 +359,9 @@
|
||||
"--input" "import.zip"
|
||||
"--repo" "demo"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
(is (= :invalid-options (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-verb-subcommand-parse-flags
|
||||
(testing "verb subcommands reject unknown flags"
|
||||
(doseq [args [["list" "page" "--wat"]
|
||||
["add" "block" "--wat"]
|
||||
@@ -517,7 +528,7 @@
|
||||
(assoc config :base-url "http://127.0.0.1:9999")))
|
||||
(set! transport/invoke (fn [_ method direct-pass? args]
|
||||
(swap! invoke-calls conj [method direct-pass? args])
|
||||
(if (= method "thread-api/export-db-base64")
|
||||
(if (= method :thread-api/export-db-base64)
|
||||
"c3FsaXRl"
|
||||
{:exported true})))
|
||||
(set! transport/write-output (fn [opts]
|
||||
@@ -538,8 +549,8 @@
|
||||
{})]
|
||||
(is (= :ok (:status edn-result)))
|
||||
(is (= :ok (:status sqlite-result)))
|
||||
(is (= [["thread-api/export-edn" false ["logseq_db_demo" {:export-type :graph}]]
|
||||
["thread-api/export-db-base64" true ["logseq_db_demo"]]]
|
||||
(is (= [[:thread-api/export-edn false ["logseq_db_demo" {:export-type :graph}]]
|
||||
[:thread-api/export-db-base64 true ["logseq_db_demo"]]]
|
||||
@invoke-calls))
|
||||
(is (= 2 (count @write-calls)))
|
||||
(let [[edn-write sqlite-write] @write-calls]
|
||||
@@ -605,8 +616,8 @@
|
||||
(is (= [[:edn "/tmp/import.edn"]
|
||||
[:sqlite "/tmp/import.sqlite"]]
|
||||
@read-calls))
|
||||
(is (= [["thread-api/import-edn" ["logseq_db_demo" {:page "Import Page"}]]
|
||||
["thread-api/import-db-base64" ["logseq_db_demo" "c3FsaXRl"]]]
|
||||
(is (= [[:thread-api/import-edn ["logseq_db_demo" {:page "Import Page"}]]
|
||||
[:thread-api/import-db-base64 ["logseq_db_demo" "c3FsaXRl"]]]
|
||||
@invoke-calls))
|
||||
(is (= ["logseq_db_demo" "logseq_db_demo"] @stop-calls))
|
||||
(is (= ["logseq_db_demo" "logseq_db_demo"] @restart-calls)))
|
||||
|
||||
@@ -193,5 +193,5 @@
|
||||
:message "graph name is required"}}
|
||||
{:output-format nil})]
|
||||
(is (= (str "Error (missing-graph): graph name is required\n"
|
||||
"Hint: Use --graph <name>")
|
||||
"Hint: Use --repo <name>")
|
||||
result)))))
|
||||
|
||||
@@ -72,23 +72,46 @@
|
||||
(is false (str "unexpected error: " e))
|
||||
(done))))))
|
||||
|
||||
(deftest test-invoke-accepts-keyword-method
|
||||
(async done
|
||||
(let [received (atom nil)]
|
||||
(-> (p/let [{:keys [url stop!]} (start-server
|
||||
(fn [^js req ^js res]
|
||||
(let [chunks (array)]
|
||||
(.on req "data" (fn [chunk] (.push chunks chunk)))
|
||||
(.on req "end" (fn []
|
||||
(let [buf (js/Buffer.concat chunks)
|
||||
payload (js/JSON.parse (.toString buf "utf8"))]
|
||||
(reset! received (js->clj payload :keywordize-keys true))
|
||||
(.writeHead res 200 #js {"Content-Type" "application/json"})
|
||||
(.end res (js/JSON.stringify #js {:result "ok"}))))))))
|
||||
result (transport/invoke {:base-url url} :thread-api/pull true ["repo" [:block/title]])]
|
||||
(is (= "ok" result))
|
||||
(is (= "thread-api/pull" (:method @received)))
|
||||
(is (= true (:directPass @received)))
|
||||
(p/let [_ (stop!)]
|
||||
(done)))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))
|
||||
(done)))))))
|
||||
|
||||
(deftest test-read-input
|
||||
(testing "reads edn input"
|
||||
(let [path (temp-path "input.edn")]
|
||||
(.writeFileSync fs path "{:a 1}")
|
||||
(is (= {:a 1} (transport/read-input {:format :edn :path path})))))
|
||||
(let [file-path (temp-path "input.edn")]
|
||||
(.writeFileSync fs file-path "{:a 1}")
|
||||
(is (= {:a 1} (transport/read-input {:format :edn :path file-path})))))
|
||||
|
||||
(testing "reads sqlite input as buffer"
|
||||
(let [path (temp-path "input.sqlite")
|
||||
(let [file-path (temp-path "input.sqlite")
|
||||
buffer (js/Buffer.from "sqlite-data")]
|
||||
(.writeFileSync fs path buffer)
|
||||
(let [result (transport/read-input {:format :sqlite :path path})]
|
||||
(.writeFileSync fs file-path buffer)
|
||||
(let [result (transport/read-input {:format :sqlite :path file-path})]
|
||||
(is (instance? js/Buffer result))
|
||||
(is (= "sqlite-data" (.toString result "utf8")))))))
|
||||
|
||||
(deftest test-write-output
|
||||
(testing "writes sqlite output as buffer"
|
||||
(let [path (temp-path "output.sqlite")
|
||||
(let [file-path (temp-path "output.sqlite")
|
||||
buffer (js/Buffer.from "sqlite-export")]
|
||||
(transport/write-output {:format :sqlite :path path :data buffer})
|
||||
(is (= "sqlite-export" (.toString (.readFileSync fs path) "utf8"))))))
|
||||
(transport/write-output {:format :sqlite :path file-path :data buffer})
|
||||
(is (= "sqlite-export" (.toString (.readFileSync fs file-path) "utf8"))))))
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
(require '[babashka.curl :as curl]
|
||||
'[cheshire.core :as json]
|
||||
'[cognitect.transit :as transit]
|
||||
'[clojure.pprint :as pprint]
|
||||
'[clojure.string :as string])
|
||||
|
||||
(def base-url (or (System/getenv "DB_WORKER_URL") "http://127.0.0.1:9101"))
|
||||
|
||||
(defn write-transit [v]
|
||||
(let [out (java.io.ByteArrayOutputStream.)
|
||||
w (transit/writer out :json)]
|
||||
(transit/write w v)
|
||||
(.toString out "UTF-8")))
|
||||
|
||||
(defn read-transit [s]
|
||||
(let [in (java.io.ByteArrayInputStream. (.getBytes s "UTF-8"))
|
||||
r (transit/reader in :json)]
|
||||
(transit/read r)))
|
||||
|
||||
(defn invoke [method direct-pass? args]
|
||||
(let [payload (if direct-pass?
|
||||
{:method method :directPass true :args args}
|
||||
{:method method :directPass false :argsTransit (write-transit args)})
|
||||
resp (curl/post (str base-url "/v1/invoke")
|
||||
{:headers {"Content-Type" "application/json"}
|
||||
:body (json/generate-string payload)})
|
||||
body (json/parse-string (:body resp) true)]
|
||||
(if (<= 200 (:status resp) 299)
|
||||
(if direct-pass?
|
||||
(:result body)
|
||||
(read-transit (:resultTransit body)))
|
||||
(throw (ex-info "db-worker invoke failed" {:status (:status resp) :body (:body resp)})))))
|
||||
|
||||
(def suffix (subs (str (random-uuid)) 0 8))
|
||||
(def repo (str "logseq_db_smoke_" suffix))
|
||||
(def page-uuid (random-uuid))
|
||||
(def block-uuid (random-uuid))
|
||||
(def now (long (System/currentTimeMillis)))
|
||||
|
||||
(println "== db-worker-node smoke test ==")
|
||||
(println "Base URL:" base-url)
|
||||
(println "Repo:" repo)
|
||||
(println "Step 1/4: list-db (before)")
|
||||
(println "Result:" (json/generate-string (invoke "thread-api/list-db" false [])
|
||||
{:pretty true}))
|
||||
|
||||
(println "Step 2/4: create-or-open-db")
|
||||
(invoke "thread-api/create-or-open-db" false [repo {}])
|
||||
(println "Step 3/4: list-db (after)")
|
||||
(println "Result:" (json/generate-string (invoke "thread-api/list-db" false [])
|
||||
{:pretty true}))
|
||||
|
||||
(println "Step 4/4: transact + q")
|
||||
(invoke "thread-api/transact" false
|
||||
[repo
|
||||
[{:block/uuid page-uuid
|
||||
:block/title "Smoke Page"
|
||||
:block/name "smoke-page"
|
||||
:block/tags #{:logseq.class/Page}
|
||||
:block/created-at now
|
||||
:block/updated-at now}
|
||||
{:block/uuid block-uuid
|
||||
:block/title "Smoke Test"
|
||||
:block/page [:block/uuid page-uuid]
|
||||
:block/parent [:block/uuid page-uuid]
|
||||
:block/order "a0"
|
||||
:block/created-at now
|
||||
:block/updated-at now}]
|
||||
{}
|
||||
nil])
|
||||
|
||||
(let [query '[:find ?e
|
||||
:in $ ?uuid
|
||||
:where [?e :block/uuid ?uuid]]
|
||||
result (invoke "thread-api/q" false [repo [query block-uuid]])]
|
||||
(println "Query result:" result)
|
||||
(when (empty? result)
|
||||
(throw (ex-info "Query returned no results" {:uuid block-uuid}))))
|
||||
|
||||
(let [page-query '[:find (pull ?e [:db/id :block/uuid :block/title :block/name :block/tags])
|
||||
:in $ ?uuid
|
||||
:where [?e :block/uuid ?uuid]]
|
||||
blocks-query '[:find (pull ?e [:db/id :block/uuid :block/title :block/order :block/parent])
|
||||
:in $ ?page-uuid
|
||||
:where [?page :block/uuid ?page-uuid]
|
||||
[?e :block/page ?page]]
|
||||
page-result (invoke "thread-api/q" false [repo [page-query page-uuid]])
|
||||
blocks-result (invoke "thread-api/q" false [repo [blocks-query page-uuid]])]
|
||||
(println "Page + blocks (pretty):")
|
||||
(pprint/pprint {:page page-result
|
||||
:blocks blocks-result}))
|
||||
|
||||
(println "Smoke test OK")
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env bb
|
||||
(require '[babashka.process :as process]
|
||||
'[clojure.java.io :as io]
|
||||
'[clojure.string :as string])
|
||||
|
||||
(def base-url (or (System/getenv "DB_WORKER_URL")
|
||||
"http://127.0.0.1:9101"))
|
||||
(def auth-token (System/getenv "DB_WORKER_AUTH_TOKEN"))
|
||||
(def events-url (str (string/replace base-url #"/$" "") "/v1/events"))
|
||||
|
||||
(defn- open-sse-connection
|
||||
[url token]
|
||||
(let [^java.net.HttpURLConnection conn (.openConnection (java.net.URL. url))]
|
||||
(.setRequestMethod conn "GET")
|
||||
(.setRequestProperty conn "Accept" "text/event-stream")
|
||||
(when (seq token)
|
||||
(.setRequestProperty conn "Authorization" (str "Bearer " token)))
|
||||
(.setDoInput conn true)
|
||||
(.connect conn)
|
||||
conn))
|
||||
|
||||
(defn- wait-for-sse!
|
||||
[^java.net.HttpURLConnection conn timeout-ms]
|
||||
(let [event-seen (promise)
|
||||
reader (future
|
||||
(try
|
||||
(with-open [rdr (io/reader (.getInputStream conn))]
|
||||
(doseq [line (line-seq rdr)]
|
||||
(when (string/starts-with? line "data:")
|
||||
(deliver event-seen line)
|
||||
(reduced nil))))
|
||||
(catch Exception _ nil)))]
|
||||
(try
|
||||
(let [result (deref event-seen timeout-ms ::timeout)]
|
||||
(when (= result ::timeout)
|
||||
(throw (ex-info "No SSE events captured" {:url events-url})))
|
||||
result)
|
||||
(finally
|
||||
(.disconnect conn)
|
||||
(future-cancel reader)))))
|
||||
|
||||
(defn- run-smoke-test!
|
||||
[]
|
||||
(let [{:keys [exit]} (process/shell {:inherit true}
|
||||
"bb" "tmp_scripts/db-worker-smoke-test.clj")]
|
||||
(when-not (zero? exit)
|
||||
(throw (ex-info "Smoke test failed" {:exit exit})))))
|
||||
|
||||
(comment
|
||||
(let [conn (open-sse-connection events-url auth-token)]
|
||||
(run-smoke-test!)
|
||||
(wait-for-sse! conn 2000)
|
||||
(println "SSE smoke test OK")))
|
||||
Reference in New Issue
Block a user