mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 22:25:01 +00:00
impl 005-logseq-cli-output-and-db-worker-node-log.md (1)
This commit is contained in:
148
deps/cli/src/logseq/cli/common/mcp/tools.cljs
vendored
148
deps/cli/src/logseq/cli/common/mcp/tools.cljs
vendored
@@ -16,60 +16,71 @@
|
|||||||
[malli.core :as m]
|
[malli.core :as m]
|
||||||
[malli.error :as me]))
|
[malli.error :as me]))
|
||||||
|
|
||||||
|
(defn- ensure-db-graph
|
||||||
|
[db]
|
||||||
|
(when-not (ldb/db-based-graph? db)
|
||||||
|
(throw (ex-info "This tool must be called on a DB graph" {}))))
|
||||||
|
|
||||||
|
(defn- minimal-list-item
|
||||||
|
[e]
|
||||||
|
(cond-> {:db/id (:db/id e)
|
||||||
|
:block/title (:block/title e)
|
||||||
|
:block/created-at (:block/created-at e)
|
||||||
|
:block/updated-at (:block/updated-at e)}
|
||||||
|
(:db/ident e) (assoc :db/ident (:db/ident e))))
|
||||||
|
|
||||||
(defn list-properties
|
(defn list-properties
|
||||||
"Main fn for ListProperties tool"
|
"Main fn for ListProperties tool"
|
||||||
[db {:keys [expand include-built-in] :as options}]
|
[db {:keys [expand include-built-in] :as options}]
|
||||||
(ensure-db-graph db)
|
(ensure-db-graph db)
|
||||||
(let [include-built-in? (if (contains? options :include-built-in) include-built-in true)]
|
(let [include-built-in? (if (contains? options :include-built-in) include-built-in true)]
|
||||||
(->> (d/datoms db :avet :block/tags :logseq.class/Property)
|
(->> (d/datoms db :avet :block/tags :logseq.class/Property)
|
||||||
(map #(d/entity db (:e %)))
|
(map #(d/entity db (:e %)))
|
||||||
(remove (fn [e]
|
(remove (fn [e]
|
||||||
(and (not include-built-in?)
|
(and (not include-built-in?)
|
||||||
(ldb/built-in? e))))
|
(ldb/built-in? e))))
|
||||||
#_((fn [x] (prn :prop-keys (distinct (mapcat keys x))) x))
|
#_((fn [x] (prn :prop-keys (distinct (mapcat keys x))) x))
|
||||||
(map (fn [e]
|
(map (fn [e]
|
||||||
(if expand
|
(if expand
|
||||||
(cond-> (into {} e)
|
(cond-> (into {} e)
|
||||||
true
|
true
|
||||||
(dissoc e :block/tags :block/order :block/refs :block/name :db/index
|
(dissoc e :block/tags :block/order :block/refs :block/name :db/index
|
||||||
:logseq.property.embedding/hnsw-label-updated-at :logseq.property/default-value)
|
:logseq.property.embedding/hnsw-label-updated-at :logseq.property/default-value)
|
||||||
true
|
true
|
||||||
(update :block/uuid str)
|
(update :block/uuid str)
|
||||||
(:logseq.property/classes e)
|
(:logseq.property/classes e)
|
||||||
(update :logseq.property/classes #(mapv :db/ident %))
|
(update :logseq.property/classes #(mapv :db/ident %))
|
||||||
(:logseq.property/description e)
|
(:logseq.property/description e)
|
||||||
(update :logseq.property/description db-property/property-value-content))
|
(update :logseq.property/description db-property/property-value-content))
|
||||||
{:block/title (:block/title e)
|
(minimal-list-item e)))))))
|
||||||
:block/uuid (str (:block/uuid e))}))))))
|
|
||||||
|
|
||||||
(defn list-tags
|
(defn list-tags
|
||||||
"Main fn for ListTags tool"
|
"Main fn for ListTags tool"
|
||||||
[db {:keys [expand include-built-in] :as options}]
|
[db {:keys [expand include-built-in] :as options}]
|
||||||
(ensure-db-graph db)
|
(ensure-db-graph db)
|
||||||
(let [include-built-in? (if (contains? options :include-built-in) include-built-in true)]
|
(let [include-built-in? (if (contains? options :include-built-in) include-built-in true)]
|
||||||
(->> (d/datoms db :avet :block/tags :logseq.class/Tag)
|
(->> (d/datoms db :avet :block/tags :logseq.class/Tag)
|
||||||
(map #(d/entity db (:e %)))
|
(map #(d/entity db (:e %)))
|
||||||
(remove (fn [e]
|
(remove (fn [e]
|
||||||
(and (not include-built-in?)
|
(and (not include-built-in?)
|
||||||
(ldb/built-in? e))))
|
(ldb/built-in? e))))
|
||||||
(map (fn [e]
|
(map (fn [e]
|
||||||
(if expand
|
(if expand
|
||||||
(cond-> (into {} e)
|
(cond-> (into {} e)
|
||||||
true
|
true
|
||||||
(dissoc e :block/tags :block/order :block/refs :block/name
|
(dissoc e :block/tags :block/order :block/refs :block/name
|
||||||
:logseq.property.embedding/hnsw-label-updated-at)
|
:logseq.property.embedding/hnsw-label-updated-at)
|
||||||
true
|
true
|
||||||
(update :block/uuid str)
|
(update :block/uuid str)
|
||||||
(:logseq.property.class/extends e)
|
(:logseq.property.class/extends e)
|
||||||
(update :logseq.property.class/extends #(mapv :db/ident %))
|
(update :logseq.property.class/extends #(mapv :db/ident %))
|
||||||
(:logseq.property.class/properties e)
|
(:logseq.property.class/properties e)
|
||||||
(update :logseq.property.class/properties #(mapv :db/ident %))
|
(update :logseq.property.class/properties #(mapv :db/ident %))
|
||||||
(:logseq.property.view/type e)
|
(:logseq.property.view/type e)
|
||||||
(assoc :logseq.property.view/type (:db/ident (:logseq.property.view/type e)))
|
(assoc :logseq.property.view/type (:db/ident (:logseq.property.view/type e)))
|
||||||
(:logseq.property/description e)
|
(:logseq.property/description e)
|
||||||
(update :logseq.property/description db-property/property-value-content))
|
(update :logseq.property/description db-property/property-value-content))
|
||||||
{:block/title (:block/title e)
|
(minimal-list-item e)))))))
|
||||||
:block/uuid (str (:block/uuid e))}))))))
|
|
||||||
|
|
||||||
(defn- get-page-blocks
|
(defn- get-page-blocks
|
||||||
[db page-id]
|
[db page-id]
|
||||||
@@ -118,32 +129,31 @@
|
|||||||
journal-only? (boolean journal-only)
|
journal-only? (boolean journal-only)
|
||||||
created-after-ms (parse-time created-after)
|
created-after-ms (parse-time created-after)
|
||||||
updated-after-ms (parse-time updated-after)]
|
updated-after-ms (parse-time updated-after)]
|
||||||
(->> (d/datoms db :avet :block/name)
|
(->> (d/datoms db :avet :block/name)
|
||||||
(map #(d/entity db (:e %)))
|
(map #(d/entity db (:e %)))
|
||||||
(remove (fn [e]
|
(remove (fn [e]
|
||||||
(and (not include-hidden?)
|
(and (not include-hidden?)
|
||||||
(entity-util/hidden? e))))
|
(entity-util/hidden? e))))
|
||||||
(remove (fn [e]
|
(remove (fn [e]
|
||||||
(let [is-journal? (ldb/journal? e)]
|
(let [is-journal? (ldb/journal? e)]
|
||||||
(cond
|
(cond
|
||||||
journal-only? (not is-journal?)
|
journal-only? (not is-journal?)
|
||||||
(false? include-journal?) is-journal?
|
(false? include-journal?) is-journal?
|
||||||
:else false))))
|
:else false))))
|
||||||
(remove (fn [e]
|
(remove (fn [e]
|
||||||
(and created-after-ms
|
(and created-after-ms
|
||||||
(<= (:block/created-at e 0) created-after-ms))))
|
(<= (:block/created-at e 0) created-after-ms))))
|
||||||
(remove (fn [e]
|
(remove (fn [e]
|
||||||
(and updated-after-ms
|
(and updated-after-ms
|
||||||
(<= (:block/updated-at e 0) updated-after-ms))))
|
(<= (:block/updated-at e 0) updated-after-ms))))
|
||||||
(map (fn [e]
|
(map (fn [e]
|
||||||
(if expand
|
(if expand
|
||||||
(-> e
|
(-> e
|
||||||
;; Until there are options to limit pages, return minimal info to avoid
|
;; Until there are options to limit pages, return minimal info to avoid
|
||||||
;; exceeding max payload size
|
;; exceeding max payload size
|
||||||
(select-keys [:block/uuid :block/title :block/created-at :block/updated-at])
|
(select-keys [:db/id :db/ident :block/uuid :block/title :block/created-at :block/updated-at])
|
||||||
(update :block/uuid str))
|
(update :block/uuid str))
|
||||||
{:block/title (:block/title e)
|
(minimal-list-item e)))))))
|
||||||
:block/uuid (str (:block/uuid e))}))))))
|
|
||||||
|
|
||||||
;; upsert-nodes tool
|
;; upsert-nodes tool
|
||||||
;; =================
|
;; =================
|
||||||
|
|||||||
@@ -9,6 +9,29 @@ Tech Stack: ClojureScript, babashka/cli, lambdaisland.glogi, Node.js fs/path.
|
|||||||
|
|
||||||
Related: Builds on docs/agent-guide/004-logseq-cli-verb-subcommands.md and docs/agent-guide/003-db-worker-node-cli-orchestration.md.
|
Related: Builds on docs/agent-guide/004-logseq-cli-verb-subcommands.md and docs/agent-guide/003-db-worker-node-cli-orchestration.md.
|
||||||
|
|
||||||
|
## Human Output Specification
|
||||||
|
|
||||||
|
Target: plain text, no ANSI colors. Each command has a stable layout and ordering.
|
||||||
|
|
||||||
|
| Command | OK output (human) | Empty output | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| graph list | Table with header `GRAPH` and rows of graph names, followed by `Count: N` | Header + `Count: 0` | Data from `{:graphs [...]}` |
|
||||||
|
| graph create | `Graph created: <graph>` | n/a | Use graph name from action/options |
|
||||||
|
| graph switch | `Graph switched: <graph>` | n/a | Use graph name from action/options |
|
||||||
|
| graph remove | `Graph removed: <graph>` | n/a | Use graph name from action/options |
|
||||||
|
| graph validate | `Graph validated: <graph>` | n/a | Use graph name from action/options |
|
||||||
|
| graph info | Lines: `Graph: <graph>`, `Created at: <ts>`, `Schema version: <v>` | n/a | Use `:logseq.kv/*` data; show `-` if missing |
|
||||||
|
| server list | Table with header `REPO STATUS HOST PORT PID`, rows for servers, followed by `Count: N` | Header + `Count: 0` | Data from `{:servers [...]}` |
|
||||||
|
| server status/start/stop/restart | `Server <status>: <repo>` + details line `Host: <host> Port: <port>` when available | n/a | Use `:status` keyword where present |
|
||||||
|
| list page/tag/property | Table with header (fields vary by command) and rows, followed by `Count: N` | Header + `Count: 0` | Defaults: page/tag/property `ID TITLE UPDATED-AT CREATED-AT` (ID uses `:db/id`); if `:db/ident` present, include `IDENT` column |
|
||||||
|
| add block | `Added blocks: <count> (repo: <repo>)` | n/a | Count = number of blocks submitted |
|
||||||
|
| add page | `Added page: <page> (repo: <repo>)` | n/a | |
|
||||||
|
| remove block | `Removed block: <block-id> (repo: <repo>)` | n/a | Prefer UUID if available |
|
||||||
|
| remove page | `Removed page: <page> (repo: <repo>)` | n/a | |
|
||||||
|
| search | Table with header `TYPE TITLE/CONTENT UUID UPDATED-AT CREATED-AT`, rows in stable order, followed by `Count: N` | Header + `Count: 0` | For block rows use content snippet; for tag/property rows omit timestamps |
|
||||||
|
| show (text) | Raw tree text (no table), trimmed | n/a | For `--format json|edn`, keep existing structured output |
|
||||||
|
| errors | `Error (<code>): <message>` + optional `Hint: <hint>` line | n/a | Ensure error codes are stable and consistent |
|
||||||
|
|
||||||
## Problem statement
|
## Problem statement
|
||||||
|
|
||||||
The current logseq-cli human output is mostly raw pr-str output, which is hard to read and inconsistent across commands.
|
The current logseq-cli human output is mostly raw pr-str output, which is hard to read and inconsistent across commands.
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ Subcommands:
|
|||||||
|
|
||||||
Output formats:
|
Output formats:
|
||||||
- Global `--output <human|json|edn>` (also accepted per subcommand)
|
- Global `--output <human|json|edn>` (also accepted per subcommand)
|
||||||
|
- Human output is plain text. List/search commands render tables with a final `Count: N` line. For list subcommands, the ID column uses `:db/id` (not UUID). If `:db/ident` exists, an `IDENT` column is included. Errors include error codes and may include a `Hint:` line. Use `--output json|edn` for structured output.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
(ns frontend.worker.db-worker-node
|
(ns frontend.worker.db-worker-node
|
||||||
"Node.js daemon entrypoint for db-worker."
|
"Node.js daemon entrypoint for db-worker."
|
||||||
(:require ["http" :as http]
|
(:require ["fs" :as fs]
|
||||||
|
["http" :as http]
|
||||||
|
["path" :as node-path]
|
||||||
[clojure.string :as string]
|
[clojure.string :as string]
|
||||||
[frontend.worker.db-core :as db-core]
|
[frontend.worker.db-core :as db-core]
|
||||||
[frontend.worker.db-worker-node-lock :as db-lock]
|
[frontend.worker.db-worker-node-lock :as db-lock]
|
||||||
@@ -8,13 +10,13 @@
|
|||||||
[frontend.worker.state :as worker-state]
|
[frontend.worker.state :as worker-state]
|
||||||
[goog.object :as gobj]
|
[goog.object :as gobj]
|
||||||
[lambdaisland.glogi :as log]
|
[lambdaisland.glogi :as log]
|
||||||
[lambdaisland.glogi.console :as glogi-console]
|
|
||||||
[logseq.db :as ldb]
|
[logseq.db :as ldb]
|
||||||
[promesa.core :as p]))
|
[promesa.core :as p]))
|
||||||
|
|
||||||
(defonce ^:private *ready? (atom false))
|
(defonce ^:private *ready? (atom false))
|
||||||
(defonce ^:private *sse-clients (atom #{}))
|
(defonce ^:private *sse-clients (atom #{}))
|
||||||
(defonce ^:private *lock-info (atom nil))
|
(defonce ^:private *lock-info (atom nil))
|
||||||
|
(defonce ^:private *file-handler (atom nil))
|
||||||
|
|
||||||
(defn- send-json!
|
(defn- send-json!
|
||||||
[^js res status payload]
|
[^js res status payload]
|
||||||
@@ -222,15 +224,82 @@
|
|||||||
(println " --repo <name> (required)")
|
(println " --repo <name> (required)")
|
||||||
(println " --rtc-ws-url <url> (optional)")
|
(println " --rtc-ws-url <url> (optional)")
|
||||||
(println " --log-level <level> (default info)")
|
(println " --log-level <level> (default info)")
|
||||||
|
(println " logs: <data-dir>/<graph-dir>/db-worker-node-YYYYMMDD.log (retains 7)")
|
||||||
(println " --auth-token <token> (optional)"))
|
(println " --auth-token <token> (optional)"))
|
||||||
|
|
||||||
|
(defn- pad2
|
||||||
|
[value]
|
||||||
|
(if (< value 10)
|
||||||
|
(str "0" value)
|
||||||
|
(str value)))
|
||||||
|
|
||||||
|
(defn- yyyymmdd
|
||||||
|
[^js date]
|
||||||
|
(str (.getFullYear date)
|
||||||
|
(pad2 (inc (.getMonth date)))
|
||||||
|
(pad2 (.getDate date))))
|
||||||
|
|
||||||
|
(defn- log-path
|
||||||
|
[data-dir repo]
|
||||||
|
(let [data-dir (db-lock/resolve-data-dir data-dir)
|
||||||
|
repo-dir (db-lock/repo-dir data-dir repo)
|
||||||
|
date-str (yyyymmdd (js/Date.))]
|
||||||
|
(node-path/join repo-dir (str "db-worker-node-" date-str ".log"))))
|
||||||
|
|
||||||
|
(defn- log-files
|
||||||
|
[repo-dir]
|
||||||
|
(->> (when (fs/existsSync repo-dir)
|
||||||
|
(fs/readdirSync repo-dir))
|
||||||
|
(filter (fn [^js name]
|
||||||
|
(re-matches #"db-worker-node-\d{8}\.log" name)))
|
||||||
|
(sort)))
|
||||||
|
|
||||||
|
(defn- enforce-log-retention!
|
||||||
|
[repo-dir]
|
||||||
|
(let [files (log-files repo-dir)
|
||||||
|
excess (max 0 (- (count files) 7))]
|
||||||
|
(doseq [name (take excess files)]
|
||||||
|
(fs/unlinkSync (node-path/join repo-dir name)))))
|
||||||
|
|
||||||
|
(defn- format-log-line
|
||||||
|
[{:keys [time level message logger-name exception]}]
|
||||||
|
(let [ts (.toISOString (js/Date. time))
|
||||||
|
base (str ts
|
||||||
|
" ["
|
||||||
|
(name level)
|
||||||
|
"] ["
|
||||||
|
logger-name
|
||||||
|
"] "
|
||||||
|
(pr-str message))]
|
||||||
|
(str base (when exception (str " " (pr-str exception))) "\n")))
|
||||||
|
|
||||||
|
(defn- install-file-logger!
|
||||||
|
[{:keys [data-dir repo log-level]}]
|
||||||
|
(let [data-dir (db-lock/resolve-data-dir data-dir)
|
||||||
|
repo-dir (db-lock/repo-dir data-dir repo)
|
||||||
|
file-path (log-path data-dir repo)]
|
||||||
|
(fs/mkdirSync repo-dir #js {:recursive true})
|
||||||
|
(fs/writeFileSync file-path "" #js {:flag "a"})
|
||||||
|
(enforce-log-retention! repo-dir)
|
||||||
|
(when-let [handler @*file-handler]
|
||||||
|
(log/remove-handler handler))
|
||||||
|
(let [handler (fn [record]
|
||||||
|
(fs/appendFileSync file-path (format-log-line record)))]
|
||||||
|
(reset! *file-handler handler)
|
||||||
|
(log/add-handler handler))
|
||||||
|
(log/set-levels {:glogi/root log-level})
|
||||||
|
file-path))
|
||||||
|
|
||||||
(defn start-daemon!
|
(defn start-daemon!
|
||||||
[{:keys [data-dir repo rtc-ws-url auth-token]}]
|
[{:keys [data-dir repo rtc-ws-url auth-token log-level]}]
|
||||||
(let [host "127.0.0.1"
|
(let [host "127.0.0.1"
|
||||||
port 0]
|
port 0]
|
||||||
(if-not (seq repo)
|
(if-not (seq repo)
|
||||||
(p/rejected (ex-info "repo is required" {:code :missing-repo}))
|
(p/rejected (ex-info "repo is required" {:code :missing-repo}))
|
||||||
(do
|
(do
|
||||||
|
(install-file-logger! {:data-dir data-dir
|
||||||
|
:repo repo
|
||||||
|
:log-level (keyword (or log-level "info"))})
|
||||||
(reset! *ready? false)
|
(reset! *ready? false)
|
||||||
(set-main-thread-stub!)
|
(set-main-thread-stub!)
|
||||||
(-> (p/let [platform (platform-node/node-platform {:data-dir data-dir
|
(-> (p/let [platform (platform-node/node-platform {:data-dir data-dir
|
||||||
@@ -288,22 +357,20 @@
|
|||||||
|
|
||||||
(defn main
|
(defn main
|
||||||
[]
|
[]
|
||||||
(let [{:keys [data-dir repo rtc-ws-url log-level auth-token help?]}
|
(let [{:keys [data-dir repo rtc-ws-url auth-token help?] :as opts}
|
||||||
(parse-args (.-argv js/process))
|
(parse-args (.-argv js/process))]
|
||||||
log-level (keyword (or log-level "info"))]
|
|
||||||
(when help?
|
(when help?
|
||||||
(show-help!)
|
(show-help!)
|
||||||
(.exit js/process 0))
|
(.exit js/process 0))
|
||||||
(when-not (seq repo)
|
(when-not (seq repo)
|
||||||
(show-help!)
|
(show-help!)
|
||||||
(.exit js/process 1))
|
(.exit js/process 1))
|
||||||
(glogi-console/install!)
|
|
||||||
(log/set-levels {:glogi/root log-level})
|
|
||||||
(p/let [{:keys [stop!] :as daemon}
|
(p/let [{:keys [stop!] :as daemon}
|
||||||
(start-daemon! {:data-dir data-dir
|
(start-daemon! {:data-dir data-dir
|
||||||
:repo repo
|
:repo repo
|
||||||
:rtc-ws-url rtc-ws-url
|
:rtc-ws-url rtc-ws-url
|
||||||
:auth-token auth-token})]
|
:auth-token auth-token
|
||||||
|
:log-level (:log-level opts)})]
|
||||||
(log/info :db-worker-node-ready {:host (:host daemon) :port (:port daemon)})
|
(log/info :db-worker-node-ready {:host (:host daemon) :port (:port daemon)})
|
||||||
(let [shutdown (fn []
|
(let [shutdown (fn []
|
||||||
(-> (stop!)
|
(-> (stop!)
|
||||||
|
|||||||
@@ -778,17 +778,20 @@
|
|||||||
(case command
|
(case command
|
||||||
:graph-list
|
:graph-list
|
||||||
{:ok? true
|
{:ok? true
|
||||||
:action {:type :graph-list}}
|
:action {:type :graph-list
|
||||||
|
:command :graph-list}}
|
||||||
|
|
||||||
:graph-create
|
:graph-create
|
||||||
(if-not (seq graph)
|
(if-not (seq graph)
|
||||||
(missing-graph-error)
|
(missing-graph-error)
|
||||||
{:ok? true
|
{:ok? true
|
||||||
:action {:type :invoke
|
:action {:type :invoke
|
||||||
|
:command :graph-create
|
||||||
:method "thread-api/create-or-open-db"
|
:method "thread-api/create-or-open-db"
|
||||||
:direct-pass? false
|
:direct-pass? false
|
||||||
:args [repo {}]
|
:args [repo {}]
|
||||||
:repo repo
|
:repo repo
|
||||||
|
:graph (repo->graph repo)
|
||||||
:allow-missing-graph true
|
:allow-missing-graph true
|
||||||
:persist-repo (repo->graph repo)}})
|
:persist-repo (repo->graph repo)}})
|
||||||
|
|
||||||
@@ -797,6 +800,7 @@
|
|||||||
(missing-graph-error)
|
(missing-graph-error)
|
||||||
{:ok? true
|
{:ok? true
|
||||||
:action {:type :graph-switch
|
:action {:type :graph-switch
|
||||||
|
:command :graph-switch
|
||||||
:repo repo
|
:repo repo
|
||||||
:graph (repo->graph repo)}})
|
:graph (repo->graph repo)}})
|
||||||
|
|
||||||
@@ -805,26 +809,31 @@
|
|||||||
(missing-graph-error)
|
(missing-graph-error)
|
||||||
{:ok? true
|
{:ok? true
|
||||||
:action {:type :invoke
|
:action {:type :invoke
|
||||||
|
:command :graph-remove
|
||||||
:method "thread-api/unsafe-unlink-db"
|
:method "thread-api/unsafe-unlink-db"
|
||||||
:direct-pass? false
|
:direct-pass? false
|
||||||
:args [repo]
|
:args [repo]
|
||||||
:repo repo}})
|
:repo repo
|
||||||
|
:graph (repo->graph repo)}})
|
||||||
|
|
||||||
:graph-validate
|
:graph-validate
|
||||||
(if-not (seq repo)
|
(if-not (seq repo)
|
||||||
(missing-graph-error)
|
(missing-graph-error)
|
||||||
{:ok? true
|
{:ok? true
|
||||||
:action {:type :invoke
|
:action {:type :invoke
|
||||||
|
:command :graph-validate
|
||||||
:method "thread-api/validate-db"
|
:method "thread-api/validate-db"
|
||||||
:direct-pass? false
|
:direct-pass? false
|
||||||
:args [repo]
|
:args [repo]
|
||||||
:repo repo}})
|
:repo repo
|
||||||
|
:graph (repo->graph repo)}})
|
||||||
|
|
||||||
:graph-info
|
:graph-info
|
||||||
(if-not (seq repo)
|
(if-not (seq repo)
|
||||||
(missing-graph-error)
|
(missing-graph-error)
|
||||||
{:ok? true
|
{:ok? true
|
||||||
:action {:type :graph-info
|
:action {:type :graph-info
|
||||||
|
:command :graph-info
|
||||||
:repo repo
|
:repo repo
|
||||||
:graph (repo->graph repo)}})))
|
:graph (repo->graph repo)}})))
|
||||||
|
|
||||||
@@ -1369,29 +1378,32 @@
|
|||||||
|
|
||||||
(defn execute
|
(defn execute
|
||||||
[action config]
|
[action config]
|
||||||
(-> (p/let [check (ensure-existing-graph action config)]
|
(-> (p/let [check (ensure-existing-graph action config)
|
||||||
(if-not (:ok? check)
|
result (if-not (:ok? check)
|
||||||
{:status :error
|
{:status :error
|
||||||
:error (:error check)}
|
:error (:error check)}
|
||||||
(case (:type action)
|
(case (:type action)
|
||||||
:graph-list (execute-graph-list action config)
|
:graph-list (execute-graph-list action config)
|
||||||
:invoke (execute-invoke action config)
|
:invoke (execute-invoke action config)
|
||||||
:graph-switch (execute-graph-switch action config)
|
:graph-switch (execute-graph-switch action config)
|
||||||
:graph-info (execute-graph-info action config)
|
:graph-info (execute-graph-info action config)
|
||||||
:list-page (execute-list-page action config)
|
:list-page (execute-list-page action config)
|
||||||
:list-tag (execute-list-tag action config)
|
:list-tag (execute-list-tag action config)
|
||||||
:list-property (execute-list-property action config)
|
:list-property (execute-list-property action config)
|
||||||
:add-block (execute-add-block action config)
|
:add-block (execute-add-block action config)
|
||||||
:add-page (execute-add-page action config)
|
:add-page (execute-add-page action config)
|
||||||
:remove-block (execute-remove action config)
|
:remove-block (execute-remove action config)
|
||||||
:remove-page (execute-remove action config)
|
:remove-page (execute-remove action config)
|
||||||
:search (execute-search action config)
|
:search (execute-search action config)
|
||||||
:show (execute-show action config)
|
:show (execute-show action config)
|
||||||
:server-list (execute-server-list action config)
|
:server-list (execute-server-list action config)
|
||||||
:server-status (execute-server-status action config)
|
:server-status (execute-server-status action config)
|
||||||
:server-start (execute-server-start action config)
|
:server-start (execute-server-start action config)
|
||||||
:server-stop (execute-server-stop action config)
|
:server-stop (execute-server-stop action config)
|
||||||
:server-restart (execute-server-restart action config)
|
:server-restart (execute-server-restart action config)
|
||||||
{:status :error
|
{:status :error
|
||||||
:error {:code :unknown-action
|
:error {:code :unknown-action
|
||||||
:message "unknown action"}})))))
|
:message "unknown action"}}))]
|
||||||
|
(assoc result
|
||||||
|
:command (or (:command action) (:type action))
|
||||||
|
:context (select-keys action [:repo :graph :page :block :blocks])))))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
(ns logseq.cli.format
|
(ns logseq.cli.format
|
||||||
"Formatting helpers for CLI output."
|
"Formatting helpers for CLI output."
|
||||||
(:require [clojure.walk :as walk]))
|
(:require [clojure.string :as string]
|
||||||
|
[clojure.walk :as walk]))
|
||||||
|
|
||||||
(defn- normalize-json
|
(defn- normalize-json
|
||||||
[value]
|
[value]
|
||||||
@@ -22,18 +23,239 @@
|
|||||||
(set! (.-error obj) (clj->js (normalize-json (update error :code name)))))
|
(set! (.-error obj) (clj->js (normalize-json (update error :code name)))))
|
||||||
(js/JSON.stringify obj)))
|
(js/JSON.stringify obj)))
|
||||||
|
|
||||||
|
(defn- pad-right
|
||||||
|
[value width]
|
||||||
|
(let [text (str value)
|
||||||
|
missing (- width (count text))]
|
||||||
|
(if (pos? missing)
|
||||||
|
(str text (apply str (repeat missing " ")))
|
||||||
|
text)))
|
||||||
|
|
||||||
|
(defn- normalize-cell
|
||||||
|
[value]
|
||||||
|
(cond
|
||||||
|
(nil? value) "-"
|
||||||
|
(keyword? value) (str value)
|
||||||
|
:else (str value)))
|
||||||
|
|
||||||
|
(defn- render-table
|
||||||
|
[headers rows]
|
||||||
|
(let [normalized-rows (mapv (fn [row]
|
||||||
|
(mapv normalize-cell row))
|
||||||
|
rows)
|
||||||
|
trim-right (fn [value]
|
||||||
|
(string/replace value #"\s+$" ""))
|
||||||
|
widths (mapv (fn [idx header]
|
||||||
|
(apply max (count header)
|
||||||
|
(map #(count (nth % idx)) normalized-rows)))
|
||||||
|
(range (count headers))
|
||||||
|
headers)
|
||||||
|
render-row (fn [row]
|
||||||
|
(->> (map pad-right row widths)
|
||||||
|
(string/join " ")
|
||||||
|
(trim-right)))
|
||||||
|
lines (cons (render-row headers)
|
||||||
|
(map render-row normalized-rows))]
|
||||||
|
(string/join "\n" lines)))
|
||||||
|
|
||||||
|
(defn- format-counted-table
|
||||||
|
[headers rows]
|
||||||
|
(str (render-table headers rows)
|
||||||
|
"\n"
|
||||||
|
"Count: "
|
||||||
|
(count rows)))
|
||||||
|
|
||||||
|
(defn- error-hint
|
||||||
|
[{:keys [code]}]
|
||||||
|
(case code
|
||||||
|
:missing-graph "Use --graph <name>"
|
||||||
|
:missing-repo "Use --repo <name>"
|
||||||
|
:missing-content "Use --content or pass content as args"
|
||||||
|
:missing-search-text "Provide search text or --text"
|
||||||
|
nil))
|
||||||
|
|
||||||
|
(defn- format-error
|
||||||
|
[error]
|
||||||
|
(let [{:keys [code message]} error
|
||||||
|
hint (error-hint error)]
|
||||||
|
(cond-> (str "Error (" (name (or code :error)) "): " message)
|
||||||
|
hint (str "\nHint: " hint))))
|
||||||
|
|
||||||
|
(defn- maybe-ident-header
|
||||||
|
[items]
|
||||||
|
(when (some :db/ident items)
|
||||||
|
["IDENT"]))
|
||||||
|
|
||||||
|
(defn- parse-ts
|
||||||
|
[value]
|
||||||
|
(cond
|
||||||
|
(number? value) value
|
||||||
|
(string? value) (let [ms (js/Date.parse value)]
|
||||||
|
(when-not (js/isNaN ms) ms))
|
||||||
|
:else nil))
|
||||||
|
|
||||||
|
(defn- human-ago
|
||||||
|
[value now-ms]
|
||||||
|
(if-let [ts (parse-ts value)]
|
||||||
|
(let [diff-ms (max 0 (- now-ms ts))
|
||||||
|
secs (js/Math.floor (/ diff-ms 1000))
|
||||||
|
mins (js/Math.floor (/ secs 60))
|
||||||
|
hours (js/Math.floor (/ mins 60))
|
||||||
|
days (js/Math.floor (/ hours 24))
|
||||||
|
months (js/Math.floor (/ days 30))
|
||||||
|
years (js/Math.floor (/ days 365))]
|
||||||
|
(cond
|
||||||
|
(< secs 60) (str secs "s ago")
|
||||||
|
(< mins 60) (str mins "m ago")
|
||||||
|
(< hours 24) (str hours "h ago")
|
||||||
|
(< days 30) (str days "d ago")
|
||||||
|
(< months 12) (str months "mo ago")
|
||||||
|
:else (str years "y ago")))
|
||||||
|
"-"))
|
||||||
|
|
||||||
|
(defn- format-list-row
|
||||||
|
[item include-ident? now-ms]
|
||||||
|
(let [base [(or (:db/id item) (:id item))
|
||||||
|
(or (:title item) (:block/title item) (:name item))]
|
||||||
|
with-ident (cond-> base
|
||||||
|
include-ident? (conj (:db/ident item)))
|
||||||
|
updated (human-ago (or (:updated-at item) (:block/updated-at item)) now-ms)
|
||||||
|
created (human-ago (or (:created-at item) (:block/created-at item)) now-ms)]
|
||||||
|
(conj with-ident updated created)))
|
||||||
|
|
||||||
|
(defn- format-list-page
|
||||||
|
[items now-ms]
|
||||||
|
(let [items (or items [])
|
||||||
|
include-ident? (boolean (some :db/ident items))
|
||||||
|
headers (into ["ID" "TITLE"]
|
||||||
|
(concat (or (maybe-ident-header items) [])
|
||||||
|
["UPDATED-AT" "CREATED-AT"]))]
|
||||||
|
(format-counted-table
|
||||||
|
headers
|
||||||
|
(mapv #(format-list-row % include-ident? now-ms) items))))
|
||||||
|
|
||||||
|
(defn- format-list-tag-or-property
|
||||||
|
[items now-ms]
|
||||||
|
(let [items (or items [])
|
||||||
|
include-ident? (boolean (some :db/ident items))
|
||||||
|
headers (into ["ID" "TITLE"]
|
||||||
|
(concat (or (maybe-ident-header items) [])
|
||||||
|
["UPDATED-AT" "CREATED-AT"]))]
|
||||||
|
(format-counted-table
|
||||||
|
headers
|
||||||
|
(mapv #(format-list-row % include-ident? now-ms) items))))
|
||||||
|
|
||||||
|
(defn- format-graph-list
|
||||||
|
[graphs]
|
||||||
|
(format-counted-table
|
||||||
|
["GRAPH"]
|
||||||
|
(mapv (fn [graph] [graph]) (or graphs []))))
|
||||||
|
|
||||||
|
(defn- format-server-list
|
||||||
|
[servers]
|
||||||
|
(format-counted-table
|
||||||
|
["REPO" "STATUS" "HOST" "PORT" "PID"]
|
||||||
|
(mapv (fn [server]
|
||||||
|
[(:repo server)
|
||||||
|
(:status server)
|
||||||
|
(:host server)
|
||||||
|
(:port server)
|
||||||
|
(:pid server)])
|
||||||
|
(or servers []))))
|
||||||
|
|
||||||
|
(defn- format-search-results
|
||||||
|
[results]
|
||||||
|
(format-counted-table
|
||||||
|
["TYPE" "TITLE/CONTENT" "UUID" "UPDATED-AT" "CREATED-AT"]
|
||||||
|
(mapv (fn [item]
|
||||||
|
[(:type item)
|
||||||
|
(or (:title item) (:content item))
|
||||||
|
(:uuid item)
|
||||||
|
(:updated-at item)
|
||||||
|
(:created-at item)])
|
||||||
|
(or results []))))
|
||||||
|
|
||||||
|
(defn- format-graph-info
|
||||||
|
[{:keys [graph logseq.kv/graph-created-at logseq.kv/schema-version]}]
|
||||||
|
(string/join "\n"
|
||||||
|
[(str "Graph: " (or graph "-"))
|
||||||
|
(str "Created at: " (or graph-created-at "-"))
|
||||||
|
(str "Schema version: " (or schema-version "-"))]))
|
||||||
|
|
||||||
|
(defn- format-server-status
|
||||||
|
[{:keys [repo status host port]}]
|
||||||
|
(string/join "\n"
|
||||||
|
(cond-> [(str "Server " (name (or status :unknown)) ": " repo)]
|
||||||
|
(and host port) (conj (str "Host: " host " Port: " port)))))
|
||||||
|
|
||||||
|
(defn- format-server-action
|
||||||
|
[command {:keys [repo status host port]}]
|
||||||
|
(let [status (or status
|
||||||
|
(case command
|
||||||
|
:server-start :started
|
||||||
|
:server-stop :stopped
|
||||||
|
:server-restart :restarted
|
||||||
|
:unknown))]
|
||||||
|
(string/join "\n"
|
||||||
|
(cond-> [(str "Server " (name status) ": " repo)]
|
||||||
|
(and host port) (conj (str "Host: " host " Port: " port))))))
|
||||||
|
|
||||||
|
(defn- format-add-block
|
||||||
|
[{:keys [repo blocks]}]
|
||||||
|
(str "Added blocks: " (count blocks) " (repo: " repo ")"))
|
||||||
|
|
||||||
|
(defn- format-add-page
|
||||||
|
[{:keys [repo page]}]
|
||||||
|
(str "Added page: " page " (repo: " repo ")"))
|
||||||
|
|
||||||
|
(defn- format-remove-page
|
||||||
|
[{:keys [repo page]}]
|
||||||
|
(str "Removed page: " page " (repo: " repo ")"))
|
||||||
|
|
||||||
|
(defn- format-remove-block
|
||||||
|
[{:keys [repo block]}]
|
||||||
|
(str "Removed block: " block " (repo: " repo ")"))
|
||||||
|
|
||||||
|
(defn- format-graph-action
|
||||||
|
[command {:keys [graph]}]
|
||||||
|
(let [verb (case command
|
||||||
|
:graph-create "created"
|
||||||
|
:graph-switch "switched"
|
||||||
|
:graph-remove "removed"
|
||||||
|
:graph-validate "validated"
|
||||||
|
"updated")]
|
||||||
|
(str "Graph " verb ": " graph)))
|
||||||
|
|
||||||
(defn- ->human
|
(defn- ->human
|
||||||
[{:keys [status data error]}]
|
[{:keys [status data error command context]} {:keys [now-ms]}]
|
||||||
(case status
|
(let [now-ms (or now-ms (js/Date.now))]
|
||||||
:ok
|
(case status
|
||||||
(if (and (map? data) (contains? data :message))
|
:ok
|
||||||
(:message data)
|
(case command
|
||||||
(pr-str data))
|
:graph-list (format-graph-list (:graphs data))
|
||||||
|
:graph-info (format-graph-info data)
|
||||||
|
(:graph-create :graph-switch :graph-remove :graph-validate)
|
||||||
|
(format-graph-action command context)
|
||||||
|
:server-list (format-server-list (:servers data))
|
||||||
|
:server-status (format-server-status data)
|
||||||
|
(:server-start :server-stop :server-restart)
|
||||||
|
(format-server-action command data)
|
||||||
|
:list-page (format-list-page (:items data) now-ms)
|
||||||
|
(:list-tag :list-property) (format-list-tag-or-property (:items data) now-ms)
|
||||||
|
:add-block (format-add-block context)
|
||||||
|
:add-page (format-add-page context)
|
||||||
|
:remove-page (format-remove-page context)
|
||||||
|
:remove-block (format-remove-block context)
|
||||||
|
:search (format-search-results (:results data))
|
||||||
|
:show (or (:message data) (pr-str data))
|
||||||
|
(if (and (map? data) (contains? data :message))
|
||||||
|
(:message data)
|
||||||
|
(pr-str data)))
|
||||||
|
|
||||||
:error
|
:error
|
||||||
(str "error: " (:message error))
|
(format-error error)
|
||||||
|
|
||||||
(pr-str {:status status :data data :error error})))
|
(pr-str {:status status :data data :error error}))))
|
||||||
|
|
||||||
(defn- ->edn
|
(defn- ->edn
|
||||||
[{:keys [status data error]}]
|
[{:keys [status data error]}]
|
||||||
@@ -42,7 +264,7 @@
|
|||||||
(= status :error) (assoc :error error))))
|
(= status :error) (assoc :error error))))
|
||||||
|
|
||||||
(defn format-result
|
(defn format-result
|
||||||
[result {:keys [output-format]}]
|
[result {:keys [output-format now-ms] :as opts}]
|
||||||
(let [format (cond
|
(let [format (cond
|
||||||
(= output-format :edn) :edn
|
(= output-format :edn) :edn
|
||||||
(= output-format :json) :json
|
(= output-format :json) :json
|
||||||
@@ -50,4 +272,4 @@
|
|||||||
(case format
|
(case format
|
||||||
:json (->json result)
|
:json (->json result)
|
||||||
:edn (->edn result)
|
:edn (->edn result)
|
||||||
(->human result))))
|
(->human result opts))))
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
(not (:ok? parsed))
|
(not (:ok? parsed))
|
||||||
(p/resolved {:exit-code 1
|
(p/resolved {:exit-code 1
|
||||||
:output (format/format-result {:status :error
|
:output (format/format-result {:status :error
|
||||||
:error (:error parsed)}
|
:error (:error parsed)
|
||||||
|
:command (:command parsed)}
|
||||||
{})})
|
{})})
|
||||||
|
|
||||||
:else
|
:else
|
||||||
@@ -38,7 +39,10 @@
|
|||||||
(if-not (:ok? action-result)
|
(if-not (:ok? action-result)
|
||||||
(p/resolved {:exit-code 1
|
(p/resolved {:exit-code 1
|
||||||
:output (format/format-result {:status :error
|
:output (format/format-result {:status :error
|
||||||
:error (:error action-result)}
|
:error (:error action-result)
|
||||||
|
:command (:command parsed)
|
||||||
|
:context (select-keys (:options parsed)
|
||||||
|
[:repo :graph :page :block])}
|
||||||
cfg)})
|
cfg)})
|
||||||
(-> (commands/execute (:action action-result) cfg)
|
(-> (commands/execute (:action action-result) cfg)
|
||||||
(p/then (fn [result]
|
(p/then (fn [result]
|
||||||
|
|||||||
@@ -6,17 +6,10 @@
|
|||||||
["os" :as os]
|
["os" :as os]
|
||||||
["path" :as node-path]
|
["path" :as node-path]
|
||||||
[clojure.string :as string]
|
[clojure.string :as string]
|
||||||
[frontend.worker.db-worker-node :as db-worker-node]
|
|
||||||
[frontend.worker-common.util :as worker-util]
|
[frontend.worker-common.util :as worker-util]
|
||||||
[lambdaisland.glogi :as log]
|
[lambdaisland.glogi :as log]
|
||||||
[promesa.core :as p]))
|
[promesa.core :as p]))
|
||||||
|
|
||||||
(defonce ^:private *inproc-servers (atom {}))
|
|
||||||
|
|
||||||
(defn- inproc-enabled?
|
|
||||||
[]
|
|
||||||
(boolean (.-DEBUG js/goog)))
|
|
||||||
|
|
||||||
(defn- expand-home
|
(defn- expand-home
|
||||||
[path]
|
[path]
|
||||||
(if (string/starts-with? path "~")
|
(if (string/starts-with? path "~")
|
||||||
@@ -172,20 +165,13 @@
|
|||||||
|
|
||||||
(defn- spawn-server!
|
(defn- spawn-server!
|
||||||
[{:keys [repo data-dir]}]
|
[{:keys [repo data-dir]}]
|
||||||
(let [script (node-path/join (js/process.cwd) "static" "db-worker-node.js")
|
(let [script (node-path/join js/__dirname "db-worker-node.js")
|
||||||
args #js [script "--repo" repo "--data-dir" data-dir]
|
args #js [script "--repo" repo "--data-dir" data-dir]
|
||||||
child (.spawn child-process "node" args #js {:detached true
|
child (.spawn child-process "node" args #js {:detached true
|
||||||
:stdio "ignore"})]
|
:stdio "ignore"})]
|
||||||
(.unref child)
|
(.unref child)
|
||||||
child))
|
child))
|
||||||
|
|
||||||
(defn- start-inproc-server!
|
|
||||||
[{:keys [repo data-dir]}]
|
|
||||||
(p/let [daemon (db-worker-node/start-daemon! {:data-dir data-dir
|
|
||||||
:repo repo})]
|
|
||||||
(swap! *inproc-servers assoc repo daemon)
|
|
||||||
daemon))
|
|
||||||
|
|
||||||
(defn- ensure-server-started!
|
(defn- ensure-server-started!
|
||||||
[config repo]
|
[config repo]
|
||||||
(let [data-dir (resolve-data-dir config)
|
(let [data-dir (resolve-data-dir config)
|
||||||
@@ -193,9 +179,7 @@
|
|||||||
(p/let [existing (read-lock path)
|
(p/let [existing (read-lock path)
|
||||||
_ (cleanup-stale-lock! path existing)
|
_ (cleanup-stale-lock! path existing)
|
||||||
_ (when (not (fs/existsSync path))
|
_ (when (not (fs/existsSync path))
|
||||||
(if (inproc-enabled?)
|
(spawn-server! {:repo repo :data-dir data-dir})
|
||||||
(start-inproc-server! {:repo repo :data-dir data-dir})
|
|
||||||
(spawn-server! {:repo repo :data-dir data-dir}))
|
|
||||||
(wait-for-lock path))
|
(wait-for-lock path))
|
||||||
lock (read-lock path)]
|
lock (read-lock path)]
|
||||||
(when-not lock
|
(when-not lock
|
||||||
@@ -232,7 +216,6 @@
|
|||||||
(p/resolved (not (fs/existsSync path))))
|
(p/resolved (not (fs/existsSync path))))
|
||||||
{:timeout-ms 5000
|
{:timeout-ms 5000
|
||||||
:interval-ms 200})
|
:interval-ms 200})
|
||||||
(swap! *inproc-servers dissoc repo)
|
|
||||||
{:ok? true
|
{:ok? true
|
||||||
:data {:repo repo}})
|
:data {:repo repo}})
|
||||||
(p/catch (fn [_]
|
(p/catch (fn [_]
|
||||||
@@ -248,10 +231,8 @@
|
|||||||
{:ok? false
|
{:ok? false
|
||||||
:error {:code :server-stop-timeout
|
:error {:code :server-stop-timeout
|
||||||
:message "timed out stopping server"}}
|
:message "timed out stopping server"}}
|
||||||
(do
|
{:ok? true
|
||||||
(swap! *inproc-servers dissoc repo)
|
:data {:repo repo}})))))))
|
||||||
{:ok? true
|
|
||||||
:data {:repo repo}}))))))))
|
|
||||||
|
|
||||||
(defn start-server!
|
(defn start-server!
|
||||||
[config repo]
|
[config repo]
|
||||||
|
|||||||
@@ -79,6 +79,67 @@
|
|||||||
repo-dir (node-path/join data-dir (str "." pool-name))]
|
repo-dir (node-path/join data-dir (str "." pool-name))]
|
||||||
(node-path/join repo-dir "db-worker.lock")))
|
(node-path/join repo-dir "db-worker.lock")))
|
||||||
|
|
||||||
|
(defn- pad2
|
||||||
|
[value]
|
||||||
|
(if (< value 10)
|
||||||
|
(str "0" value)
|
||||||
|
(str value)))
|
||||||
|
|
||||||
|
(defn- yyyymmdd
|
||||||
|
[^js date]
|
||||||
|
(str (.getFullYear date)
|
||||||
|
(pad2 (inc (.getMonth date)))
|
||||||
|
(pad2 (.getDate date))))
|
||||||
|
|
||||||
|
(defn- log-path
|
||||||
|
[data-dir repo]
|
||||||
|
(let [pool-name (worker-util/get-pool-name repo)
|
||||||
|
repo-dir (node-path/join data-dir (str "." pool-name))
|
||||||
|
date-str (yyyymmdd (js/Date.))]
|
||||||
|
(node-path/join repo-dir (str "db-worker-node-" date-str ".log"))))
|
||||||
|
|
||||||
|
(deftest db-worker-node-creates-log-file
|
||||||
|
(async done
|
||||||
|
(let [daemon (atom nil)
|
||||||
|
data-dir (node-helper/create-tmp-dir "db-worker-log")
|
||||||
|
repo (str "logseq_db_log_" (subs (str (random-uuid)) 0 8))
|
||||||
|
log-file (log-path data-dir repo)]
|
||||||
|
(-> (p/let [{:keys [stop!]}
|
||||||
|
(db-worker-node/start-daemon! {:data-dir data-dir
|
||||||
|
:repo repo})
|
||||||
|
_ (reset! daemon {:stop! stop!})
|
||||||
|
_ (p/delay 50)]
|
||||||
|
(is (fs/existsSync log-file)))
|
||||||
|
(p/catch (fn [e]
|
||||||
|
(is false (str "unexpected error: " e))))
|
||||||
|
(p/finally (fn []
|
||||||
|
(if-let [stop! (:stop! @daemon)]
|
||||||
|
(-> (stop!) (p/finally (fn [] (done))))
|
||||||
|
(done))))))))
|
||||||
|
|
||||||
|
(deftest db-worker-node-log-file-has-entries
|
||||||
|
(async done
|
||||||
|
(let [daemon (atom nil)
|
||||||
|
data-dir (node-helper/create-tmp-dir "db-worker-log-entries")
|
||||||
|
repo (str "logseq_db_log_entries_" (subs (str (random-uuid)) 0 8))
|
||||||
|
log-file (log-path data-dir repo)]
|
||||||
|
(-> (p/let [{:keys [host port stop!]}
|
||||||
|
(db-worker-node/start-daemon! {:data-dir data-dir
|
||||||
|
:repo repo})
|
||||||
|
_ (reset! daemon {:stop! stop!})
|
||||||
|
_ (invoke host port "thread-api/create-or-open-db" [repo {}])
|
||||||
|
_ (p/delay 50)
|
||||||
|
contents (when (fs/existsSync log-file)
|
||||||
|
(.toString (fs/readFileSync log-file) "utf8"))]
|
||||||
|
(is (fs/existsSync log-file))
|
||||||
|
(is (pos? (count contents))))
|
||||||
|
(p/catch (fn [e]
|
||||||
|
(is false (str "unexpected error: " e))))
|
||||||
|
(p/finally (fn []
|
||||||
|
(if-let [stop! (:stop! @daemon)]
|
||||||
|
(-> (stop!) (p/finally (fn [] (done))))
|
||||||
|
(done))))))))
|
||||||
|
|
||||||
(deftest db-worker-node-parse-args-ignores-host-and-port
|
(deftest db-worker-node-parse-args-ignores-host-and-port
|
||||||
(let [parse-args #'db-worker-node/parse-args
|
(let [parse-args #'db-worker-node/parse-args
|
||||||
result (parse-args #js ["node" "db-worker-node.js"
|
result (parse-args #js ["node" "db-worker-node.js"
|
||||||
|
|||||||
@@ -39,4 +39,133 @@
|
|||||||
(testing "human error (default)"
|
(testing "human error (default)"
|
||||||
(let [result (format/format-result {:status :error :error {:code :boom :message "nope"}}
|
(let [result (format/format-result {:status :error :error {:code :boom :message "nope"}}
|
||||||
{:output-format nil})]
|
{:output-format nil})]
|
||||||
(is (= "error: nope" result)))))
|
(is (= "Error (boom): nope" result)))))
|
||||||
|
|
||||||
|
(deftest test-human-output-list-page
|
||||||
|
(testing "list page renders a table with count"
|
||||||
|
(let [result (format/format-result {:status :ok
|
||||||
|
:command :list-page
|
||||||
|
:data {:items [{:db/id 1
|
||||||
|
:title "Alpha"
|
||||||
|
:updated-at 90000
|
||||||
|
:created-at 40000}]}}
|
||||||
|
{:output-format nil
|
||||||
|
:now-ms 100000})]
|
||||||
|
(is (= (str "ID TITLE UPDATED-AT CREATED-AT\n"
|
||||||
|
"1 Alpha 10s ago 1m ago\n"
|
||||||
|
"Count: 1")
|
||||||
|
result)))))
|
||||||
|
|
||||||
|
(deftest test-human-output-list-tag-property
|
||||||
|
(testing "list tag uses ID column from :db/id"
|
||||||
|
(let [result (format/format-result {:status :ok
|
||||||
|
:command :list-tag
|
||||||
|
:data {:items [{:block/title "Tag"
|
||||||
|
:db/id 42
|
||||||
|
:block/created-at 40000
|
||||||
|
:block/updated-at 90000
|
||||||
|
:db/ident :logseq.class/Tag}]}}
|
||||||
|
{:output-format nil
|
||||||
|
:now-ms 100000})]
|
||||||
|
(is (= (str "ID TITLE IDENT UPDATED-AT CREATED-AT\n"
|
||||||
|
"42 Tag :logseq.class/Tag 10s ago 1m ago\n"
|
||||||
|
"Count: 1")
|
||||||
|
result))))
|
||||||
|
|
||||||
|
(testing "list property uses ID column from :db/id"
|
||||||
|
(let [result (format/format-result {:status :ok
|
||||||
|
:command :list-property
|
||||||
|
:data {:items [{:block/title "Prop"
|
||||||
|
:db/id 99
|
||||||
|
:block/created-at 40000
|
||||||
|
:block/updated-at 90000}]}}
|
||||||
|
{:output-format nil
|
||||||
|
:now-ms 100000})]
|
||||||
|
(is (= (str "ID TITLE UPDATED-AT CREATED-AT\n"
|
||||||
|
"99 Prop 10s ago 1m ago\n"
|
||||||
|
"Count: 1")
|
||||||
|
result)))))
|
||||||
|
|
||||||
|
(deftest test-human-output-add-remove
|
||||||
|
(testing "add block renders a succinct success line"
|
||||||
|
(let [result (format/format-result {:status :ok
|
||||||
|
:command :add-block
|
||||||
|
:context {:repo "demo-repo"
|
||||||
|
:blocks ["a" "b"]}
|
||||||
|
:data {:result {:ok true}}}
|
||||||
|
{:output-format nil})]
|
||||||
|
(is (= "Added blocks: 2 (repo: demo-repo)" result))))
|
||||||
|
|
||||||
|
(testing "remove page renders a succinct success line"
|
||||||
|
(let [result (format/format-result {:status :ok
|
||||||
|
:command :remove-page
|
||||||
|
:context {:repo "demo-repo"
|
||||||
|
:page "Home"}
|
||||||
|
:data {:result {:ok true}}}
|
||||||
|
{:output-format nil})]
|
||||||
|
(is (= "Removed page: Home (repo: demo-repo)" result)))))
|
||||||
|
|
||||||
|
(deftest test-human-output-graph-info
|
||||||
|
(testing "graph info includes key metadata lines"
|
||||||
|
(let [result (format/format-result {:status :ok
|
||||||
|
:command :graph-info
|
||||||
|
:data {:graph "demo-graph"
|
||||||
|
:logseq.kv/graph-created-at 123
|
||||||
|
:logseq.kv/schema-version 2}}
|
||||||
|
{:output-format nil})]
|
||||||
|
(is (= (str "Graph: demo-graph\n"
|
||||||
|
"Created at: 123\n"
|
||||||
|
"Schema version: 2")
|
||||||
|
result)))))
|
||||||
|
|
||||||
|
(deftest test-human-output-server-status
|
||||||
|
(testing "server status includes repo, status, host, port"
|
||||||
|
(let [result (format/format-result {:status :ok
|
||||||
|
:command :server-status
|
||||||
|
:data {:repo "demo-repo"
|
||||||
|
:status :ready
|
||||||
|
:host "127.0.0.1"
|
||||||
|
:port 1234}}
|
||||||
|
{:output-format nil})]
|
||||||
|
(is (= (str "Server ready: demo-repo\n"
|
||||||
|
"Host: 127.0.0.1 Port: 1234")
|
||||||
|
result)))))
|
||||||
|
|
||||||
|
(deftest test-human-output-search-and-show
|
||||||
|
(testing "search renders a table with count"
|
||||||
|
(let [result (format/format-result {:status :ok
|
||||||
|
:command :search
|
||||||
|
:data {:results [{:type "page"
|
||||||
|
:title "Alpha"
|
||||||
|
:uuid "u1"
|
||||||
|
:updated-at 3
|
||||||
|
:created-at 1}
|
||||||
|
{:type "block"
|
||||||
|
:content "Note"
|
||||||
|
:uuid "u2"
|
||||||
|
:updated-at 4
|
||||||
|
:created-at 2}]}}
|
||||||
|
{:output-format nil})]
|
||||||
|
(is (= (str "TYPE TITLE/CONTENT UUID UPDATED-AT CREATED-AT\n"
|
||||||
|
"page Alpha u1 3 1\n"
|
||||||
|
"block Note u2 4 2\n"
|
||||||
|
"Count: 2")
|
||||||
|
result))))
|
||||||
|
|
||||||
|
(testing "show renders text payloads directly"
|
||||||
|
(let [result (format/format-result {:status :ok
|
||||||
|
:command :show
|
||||||
|
:data {:message "Line 1\nLine 2"}}
|
||||||
|
{:output-format nil})]
|
||||||
|
(is (= "Line 1\nLine 2" result)))))
|
||||||
|
|
||||||
|
(deftest test-human-output-error-formatting
|
||||||
|
(testing "errors include code and hint when available"
|
||||||
|
(let [result (format/format-result {:status :error
|
||||||
|
:command :graph-create
|
||||||
|
:error {:code :missing-graph
|
||||||
|
:message "graph name is required"}}
|
||||||
|
{:output-format nil})]
|
||||||
|
(is (= (str "Error (missing-graph): graph name is required\n"
|
||||||
|
"Hint: Use --graph <name>")
|
||||||
|
result)))))
|
||||||
|
|||||||
@@ -128,3 +128,48 @@
|
|||||||
(p/catch (fn [e]
|
(p/catch (fn [e]
|
||||||
(is false (str "unexpected error: " e))
|
(is false (str "unexpected error: " e))
|
||||||
(done)))))))
|
(done)))))))
|
||||||
|
|
||||||
|
(deftest test-cli-list-outputs-include-id
|
||||||
|
(async done
|
||||||
|
(let [data-dir (node-helper/create-tmp-dir "db-worker")]
|
||||||
|
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
|
||||||
|
_ (fs/writeFileSync cfg-path "{:output-format :json}")
|
||||||
|
_ (run-cli ["graph" "create" "--repo" "list-id-graph"] data-dir cfg-path)
|
||||||
|
_ (run-cli ["add" "page" "--page" "TestPage"] data-dir cfg-path)
|
||||||
|
list-page-result (run-cli ["list" "page"] data-dir cfg-path)
|
||||||
|
list-page-payload (parse-json-output list-page-result)
|
||||||
|
list-tag-result (run-cli ["list" "tag"] data-dir cfg-path)
|
||||||
|
list-tag-payload (parse-json-output list-tag-result)
|
||||||
|
list-property-result (run-cli ["list" "property"] data-dir cfg-path)
|
||||||
|
list-property-payload (parse-json-output list-property-result)
|
||||||
|
stop-result (run-cli ["server" "stop" "--repo" "list-id-graph"] data-dir cfg-path)
|
||||||
|
stop-payload (parse-json-output stop-result)]
|
||||||
|
(is (= "ok" (:status list-page-payload)))
|
||||||
|
(is (every? #(contains? % :id) (get-in list-page-payload [:data :items])))
|
||||||
|
(is (= "ok" (:status list-tag-payload)))
|
||||||
|
(is (every? #(contains? % :id) (get-in list-tag-payload [:data :items])))
|
||||||
|
(is (= "ok" (:status list-property-payload)))
|
||||||
|
(is (every? #(contains? % :id) (get-in list-property-payload [:data :items])))
|
||||||
|
(is (= "ok" (:status stop-payload)))
|
||||||
|
(done))
|
||||||
|
(p/catch (fn [e]
|
||||||
|
(is false (str "unexpected error: " e))
|
||||||
|
(done)))))))
|
||||||
|
|
||||||
|
(deftest test-cli-list-page-human-output
|
||||||
|
(async done
|
||||||
|
(let [data-dir (node-helper/create-tmp-dir "db-worker")]
|
||||||
|
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
|
||||||
|
_ (fs/writeFileSync cfg-path "{:output-format :json}")
|
||||||
|
_ (run-cli ["graph" "create" "--repo" "human-list-graph"] data-dir cfg-path)
|
||||||
|
_ (run-cli ["add" "page" "--page" "TestPage"] data-dir cfg-path)
|
||||||
|
list-page-result (run-cli ["list" "page" "--output" "human"] data-dir cfg-path)
|
||||||
|
output (:output list-page-result)]
|
||||||
|
(is (= 0 (:exit-code list-page-result)))
|
||||||
|
(is (string/includes? output "TITLE"))
|
||||||
|
(is (string/includes? output "TestPage"))
|
||||||
|
(is (string/includes? output "Count:"))
|
||||||
|
(done))
|
||||||
|
(p/catch (fn [e]
|
||||||
|
(is false (str "unexpected error: " e))
|
||||||
|
(done)))))))
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
(deftest spawn-server-omits-host-and-port-flags
|
(deftest spawn-server-omits-host-and-port-flags
|
||||||
(let [spawn-server! #'cli-server/spawn-server!
|
(let [spawn-server! #'cli-server/spawn-server!
|
||||||
captured (atom nil)
|
captured (atom nil)
|
||||||
original-spawn (.-spawn child-process)]
|
original-spawn (.-spawn child-process)
|
||||||
|
original-cwd (.cwd js/process)]
|
||||||
(set! (.-spawn child-process)
|
(set! (.-spawn child-process)
|
||||||
(fn [cmd args opts]
|
(fn [cmd args opts]
|
||||||
(reset! captured {:cmd cmd
|
(reset! captured {:cmd cmd
|
||||||
@@ -19,15 +20,20 @@
|
|||||||
:opts (js->clj opts :keywordize-keys true)})
|
:opts (js->clj opts :keywordize-keys true)})
|
||||||
(js-obj "unref" (fn [] nil))))
|
(js-obj "unref" (fn [] nil))))
|
||||||
(try
|
(try
|
||||||
|
(.chdir js/process "/")
|
||||||
(spawn-server! {:repo "logseq_db_spawn_test"
|
(spawn-server! {:repo "logseq_db_spawn_test"
|
||||||
:data-dir "/tmp/logseq-db-worker"})
|
:data-dir "/tmp/logseq-db-worker"})
|
||||||
(is (= "node" (:cmd @captured)))
|
(is (= "node" (:cmd @captured)))
|
||||||
|
(is (= (node-path/join js/__dirname "db-worker-node.js")
|
||||||
|
(first (:args @captured))))
|
||||||
(is (some #{"--repo"} (:args @captured)))
|
(is (some #{"--repo"} (:args @captured)))
|
||||||
(is (some #{"--data-dir"} (:args @captured)))
|
(is (some #{"--data-dir"} (:args @captured)))
|
||||||
(is (not-any? #{"--host" "--port"} (:args @captured)))
|
(is (not-any? #{"--host" "--port"} (:args @captured)))
|
||||||
(finally
|
(finally
|
||||||
|
(.chdir js/process original-cwd)
|
||||||
(set! (.-spawn child-process) original-spawn)))))
|
(set! (.-spawn child-process) original-spawn)))))
|
||||||
|
|
||||||
|
|
||||||
(deftest ensure-server-repairs-stale-lock
|
(deftest ensure-server-repairs-stale-lock
|
||||||
(async done
|
(async done
|
||||||
(let [data-dir (node-helper/create-tmp-dir "cli-server")
|
(let [data-dir (node-helper/create-tmp-dir "cli-server")
|
||||||
|
|||||||
Reference in New Issue
Block a user