mirror of
https://github.com/logseq/logseq.git
synced 2026-02-01 22:47:36 +00:00
025-logseq-cli-builtin-status-priority-queries.md
This commit is contained in:
@@ -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 CLI’s `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).
|
||||
|
||||
---
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]))))))
|
||||
|
||||
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user