025-logseq-cli-builtin-status-priority-queries.md

This commit is contained in:
rcmerci
2026-01-31 20:33:58 +08:00
parent 0476328012
commit 923e44199f
5 changed files with 212 additions and 10 deletions

View File

@@ -0,0 +1,94 @@
# Logseq CLI Built-in Status/Priority Queries Plan
Goal: Add built-in `query` names `list-status` and `list-priority` that return the available Status/Priority options from a graph.
Architecture: Keep the existing logseq-cli → db-worker-node transport and thread-api usage; implement `list-status`/`list-priority` via `:thread-api/q` rather than adding a dedicated thread-api.
Tech Stack: ClojureScript, logseq-cli, db-worker-node, Datascript, logseq.db.frontend.property.
Related: `src/main/logseq/cli/command/query.cljs`, `src/main/frontend/worker/db_core.cljs`, `deps/db/src/logseq/db/frontend/property.cljs`.
## Problem Statement
Logseq CLIs `query` built-ins are limited to Datascript queries. Users cannot easily discover valid Status or Priority options for a graph (e.g., TODO/DOING/DONE, Urgent/High/Low) without knowing the property internals or running custom queries. We need simple built-ins that list the closed values configured for Status and Priority.
## Non-Goals
- Changing property schemas or defaults.
- Replacing the existing Datascript query flow.
- Adding new UI commands outside `query` or changing CLI output formats.
## Current Behavior (Key Points)
- Built-in queries are defined in `built-in-query-specs` in `src/main/logseq/cli/command/query.cljs` and executed via `:thread-api/q`.
- Status and Priority options are stored as closed values on `:logseq.property/status` and `:logseq.property/priority` (see `logseq.db.frontend.property/get-closed-property-values`).
- There is no CLI helper to list those closed values.
## Proposed Changes
1) **Use `:thread-api/q` for closed values**
- Implement `list-status`/`list-priority` by issuing a Datascript query via `:thread-api/q`.
- Return a vector of maps with `:db/ident` and `:db/id`.
- Use `:find` with an ellipsis form to return a vector, e.g. `:find [(pull ?e [:db/ident :db/id]) ...]` (instead of `:find (pull ?e [:db/ident :db/id])`).
2) **Add built-in query names `list-status` and `list-priority`**
- Add entries to `built-in-query-specs` that specify a non-Datascript execution path (e.g., `:method`/`:handler` metadata).
- Wire `build-action` to create a dedicated action type when these names are used.
- Update `execute-query` to call the new thread API and return results as `{:result ["TODO" ...]}` so existing output formatting works unchanged.
3) **Expose built-ins in `query list`**
- Ensure `query list` includes the new entries (with `doc` and `inputs: []`).
- Keep existing “custom overrides built-in” semantics.
## Implementation Plan
1) **db-worker-node API**
- No new thread-api should be added; use `:thread-api/q` only.
2) **CLI built-in wiring**
- Extend `built-in-query-specs` in `src/main/logseq/cli/command/query.cljs`:
- `list-status``:property-ident :logseq.property/status`
- `list-priority``:property-ident :logseq.property/priority`
- Include `:doc` and `:inputs []`.
- Update `normalize-query-entry` / `find-query` handling to preserve the extra metadata.
- Update `build-action`:
- When the built-in includes a `:property-ident` (or `:method`), create an action like
`{:type :query-closed-values :repo ... :property-ident ...}`.
- Update `execute-query`:
- Branch on the new action type to call `transport/invoke` with `:thread-api/q` and return `{:result values}` (vector of maps with `:db/ident` and `:db/id`).
3) **Output and docs**
- No change to formatters; `format-query-results` already prints vectors.
- Update CLI docs if needed (e.g., `docs/cli/logseq-cli.md`) to mention the two built-ins.
## Testing Plan
- **Unit tests** (`src/test/logseq/cli/command/query_test.cljs`):
- `list-queries` includes `list-status` and `list-priority` with empty inputs.
- `build-action` for `--name list-status` returns `:query-closed-values` action with property ident.
- Custom query overrides built-in name still works.
- **db-worker-node tests**: no new thread-api, so no additional db-worker-node test needed.
- **Integration** (`src/test/logseq/cli/integration_test.cljs`):
- Start a graph, run `logseq query --name list-status` / `list-priority`, assert status `ok` and a non-empty vector.
- If stable defaults are known, assert a known value is present; otherwise, seed closed values in the test graph.
## Edge Cases
- Property has no closed values → return an empty vector (not an error).
- Closed values may be stored in either `:block/title` or `:logseq.property/value`; if `:db/ident` is absent, the map should still include the key with a nil value.
- Ensure ordering follows `:block/order` from `get-closed-property-values`.
## Files to Touch
- `src/main/frontend/worker/db_core.cljs` (no new thread-api)
- `src/main/logseq/cli/command/query.cljs` (built-in specs, build-action branching, execute-query)
- `src/test/logseq/cli/command/query_test.cljs` (unit coverage)
- `src/test/frontend/worker/db_worker_node_test.cljs` or a db-core test (not required for new thread-api)
- `src/test/logseq/cli/integration_test.cljs` (CLI end-to-end)
- `docs/cli/logseq-cli.md` (optional docs update)
## Open Questions
- Use `:thread-api/q` to return structured values: `{:db/ident .., :db/id ..}`.
- Expose `list-status`/`list-priority` via `query --name` only (no dedicated subcommands).
---

View File

@@ -78,6 +78,8 @@ Inspect and edit commands:
- `remove --id <id>|--uuid <uuid>|--page <name>` - remove blocks (by db/id or UUID) or pages
- `search <query> [--type page|block|tag|property|all] [--tag <name>] [--case-sensitive] [--sort updated-at|created-at] [--order asc|desc]` - search across pages, blocks, tags, and properties (query is positional)
- `query --query <edn> [--inputs <edn-vector>]` - run a Datascript query against the graph
- `query --name <query-name> [--inputs <edn-vector>]` - run a named query (built-in or from `cli.edn`)
- `query list` - list available named queries
- `show --page <name> [--level <n>]` - show page tree
- `show --uuid <uuid> [--level <n>]` - show block tree
- `show --id <id> [--level <n>]` - show block tree by db/id
@@ -113,6 +115,7 @@ Output formats:
- For `graph export`, `--output` refers to the destination file path. Output formatting is controlled via `:output-format` in config or `LOGSEQ_CLI_OUTPUT`.
- Human output is plain text. List/search commands render tables with a final `Count: N` line. For list and search subcommands, the ID column uses `:db/id` (not UUID). If `:db/ident` exists, an `IDENT` column is included. Search table columns are `ID` and `TITLE`. Block titles can include multiple lines; multi-line rows align additional lines under the `TITLE` column. Times such as list `UPDATED-AT`/`CREATED-AT` and `graph info` `Created at` are shown in human-friendly relative form. Errors include error codes and may include a `Hint:` line. Use `--output json|edn` for structured output.
- `query` human output returns a plain string (the query result rendered via `pr-str`), which is convenient for pipelines like `logseq query ... | xargs logseq show --id`.
- Built-in named queries currently include `block-search`, `task-search`, `recent-updated`, `list-status`, and `list-priority`. Use `query list` to see the full set for your config.
- Show and search outputs resolve block reference UUIDs inside text, replacing `[[<uuid>]]` with the referenced block content. Nested references are resolved recursively up to 10 levels to avoid excessive expansion. For example: `[[<uuid1>]]``[[some text [[<uuid2>]]]]` and then `<uuid2>` is also replaced.
- `show` human output prints the `:db/id` as the first column followed by a tree:

View File

@@ -69,7 +69,23 @@
[(missing? $ ?e :logseq.property/built-in?)]
[(* ?recent-days 86400000) ?recent-days-ms]
[(- ?now-ms ?recent-days-ms) ?days-ago]
[(>= ?updated-at ?days-ago)]]}})
[(>= ?updated-at ?days-ago)]]}
"list-status"
{:doc "List closed values for the Status property."
:inputs []
:query '[:find [(pull ?value [:db/id :db/ident :block/order]) ...]
:where
[?property :db/ident :logseq.property/status]
[?value :block/closed-value-property ?property]]}
"list-priority"
{:doc "List closed values for the Priority property."
:inputs []
:query '[:find [(pull ?value [:db/id :db/ident :block/order]) ...]
:where
[?property :db/ident :logseq.property/priority]
[?value :block/closed-value-property ?property]]}})
(defn- parse-edn
[label value]
@@ -264,11 +280,12 @@
(defn execute-query
[action config]
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
args (into [(:query action)] (:inputs action))
results (transport/invoke cfg :thread-api/q false [(:repo action) args])]
{:status :ok
:data {:result results}})))
(case (:type action)
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
args (into [(:query action)] (:inputs action))
results (transport/invoke cfg :thread-api/q false [(:repo action) args])]
{:status :ok
:data {:result results}}))))
(defn execute-query-list
[_action config]

View File

@@ -79,7 +79,7 @@
"logseq_db_demo"
config)]
(is (true? (:ok? result)))
(is (= ["doing" nil nil] (get-in result [:action :inputs])))))
(is (= [:logseq.property/status.doing nil nil] (get-in result [:action :inputs])))))
(testing "missing required inputs returns invalid-options"
(let [config {:custom-queries {"task-search"
@@ -109,7 +109,7 @@
"logseq_db_demo"
config)]
(is (true? (:ok? result)))
(is (= ["doing" "fallback-title" 7] (get-in result [:action :inputs])))))
(is (= [:logseq.property/status.doing "fallback-title" 7] (get-in result [:action :inputs])))))
(testing "built-in task-search uses defaults for optional inputs"
(let [result (query-command/build-action {:name "task-search"
@@ -118,18 +118,53 @@
{})]
(is (true? (:ok? result)))
(let [inputs (get-in result [:action :inputs])]
(is (= ["doing" "" 0] (subvec inputs 0 3)))
(is (= [:logseq.property/status.doing "" 0] (subvec inputs 0 3)))
(is (number? (nth inputs 3)))))))
(deftest test-build-action-closed-value-queries
(testing "list-status builds standard query action"
(let [result (query-command/build-action {:name "list-status"}
"logseq_db_demo"
{})]
(is (true? (:ok? result)))
(is (= :query (get-in result [:action :type])))
(is (= "logseq_db_demo" (get-in result [:action :repo])))
(is (= '[:find [(pull ?value [:db/id :db/ident :block/order]) ...]
:where
[?property :db/ident :logseq.property/status]
[?value :block/closed-value-property ?property]]
(get-in result [:action :query])))))
(testing "list-priority builds standard query action"
(let [result (query-command/build-action {:name "list-priority"}
"logseq_db_demo"
{})]
(is (true? (:ok? result)))
(is (= :query (get-in result [:action :type])))
(is (= :logseq.property/priority (get-in result [:action :query 3 2])))
(is (= '[:find [(pull ?value [:db/id :db/ident :block/order]) ...]
:where
[?property :db/ident :logseq.property/priority]
[?value :block/closed-value-property ?property]]
(get-in result [:action :query]))))))
(deftest test-query-list-merges-built-in-and-custom
(testing "built-in and custom queries are both listed"
(let [queries (query-command/list-queries {:custom-queries {"custom-q" {:query '[:find ?e]}}})
names (set (map :name queries))]
(is (contains? names "block-search"))
(is (contains? names "task-search"))
(is (contains? names "list-status"))
(is (contains? names "list-priority"))
(is (contains? names "custom-q"))))
(testing "custom query overrides built-in name"
(let [queries (query-command/list-queries {:custom-queries {"block-search" {:query '[:find ?e]}}})
block-search (first (filter #(= "block-search" (:name %)) queries))]
(is (= :custom (:source block-search))))))
(is (= :custom (:source block-search)))))
(testing "list-status and list-priority have empty inputs"
(let [queries (query-command/list-queries {})
by-name (into {} (map (fn [entry] [(:name entry) entry]) queries))]
(is (= [] (get-in by-name ["list-status" :inputs])))
(is (= [] (get-in by-name ["list-priority" :inputs]))))))

View File

@@ -621,6 +621,59 @@
(is false (str "unexpected error: " e))
(done)))))))
(deftest test-cli-query-list-status-priority
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-status-query")]
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
_ (fs/writeFileSync cfg-path "{:output-format :json}")
create-result (run-cli ["graph" "create" "--repo" "status-query-graph"] data-dir cfg-path)
create-payload (parse-json-output create-result)
_ (p/delay 100)
list-result (run-cli ["query" "list"] data-dir cfg-path)
list-payload (parse-json-output list-result)
names (set (map :name (get-in list-payload [:data :queries])))
status-result (run-cli ["--repo" "status-query-graph"
"query"
"--name" "list-status"]
data-dir cfg-path)
status-payload (parse-json-output status-result)
status-values (get-in status-payload [:data :result])
priority-result (run-cli ["--repo" "status-query-graph"
"query"
"--name" "list-priority"]
data-dir cfg-path)
priority-payload (parse-json-output priority-result)
priority-values (get-in priority-payload [:data :result])
stop-result (run-cli ["server" "stop" "--repo" "status-query-graph"] data-dir cfg-path)
stop-payload (parse-json-output stop-result)]
(is (= "ok" (:status create-payload)))
(is (= "ok" (:status list-payload)))
(is (contains? names "list-status"))
(is (contains? names "list-priority"))
(is (= 0 (:exit-code status-result)))
(is (= "ok" (:status status-payload)))
(is (vector? status-values))
(when (seq status-values)
(let [row (first status-values)
value (if (vector? row) (first row) row)]
(is (map? value))
(is (contains? value :ident))
(is (contains? value :id))))
(is (= 0 (:exit-code priority-result)))
(is (= "ok" (:status priority-payload)))
(is (vector? priority-values))
(when (seq priority-values)
(let [row (first priority-values)
value (if (vector? row) (first row) row)]
(is (map? value))
(is (contains? value :ident))
(is (contains? value :id))))
(is (= "ok" (:status stop-payload)))
(done))
(p/catch (fn [e]
(is false (str "unexpected error: " e))
(done)))))))
(deftest test-cli-query-recent-updated
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-recent-updated")]