fix: multiple issues with --fields option for list* commands

- 4 visible fields couldn't be used with --fields
- Only print selected fields/columns. Previous behavior didn't make
  sense as unspecified fields were still listed and took up horizontal space
- Allow --fields to work when --expand isn't used. Previous behavior
  wasn't explained to user and needlessly limiting
This commit is contained in:
Gabriel Horner
2026-03-13 16:17:20 -04:00
parent bad750c173
commit eec5b4a060
5 changed files with 94 additions and 71 deletions

View File

@@ -79,7 +79,7 @@ List page options:
| --include-hidden | Include hidden pages | Requires a flag to bypass entity-util/hidden? filtering. |
| --updated-after ISO8601 | Filter by updated-at | Compare to :block/updated-at. |
| --created-after ISO8601 | Filter by created-at | Compare to :block/created-at. |
| --fields FIELD,FIELD | Select output fields | Applies when --expand is true. |
| --fields FIELD,FIELD | Select output fields | |
List tag options:
@@ -88,7 +88,7 @@ List tag options:
| --include-built-in | Include built-in classes | Built-in tags are currently included by default, clarify behavior. |
| --with-properties | Include class properties | Uses :logseq.property.class/properties when expanded. |
| --with-extends | Include class extends | Uses :logseq.property.class/extends when expanded. |
| --fields FIELD,FIELD | Select output fields | Applies when --expand is true. |
| --fields FIELD,FIELD | Select output fields | |
List property options:
@@ -97,7 +97,7 @@ List property options:
| --include-built-in | Include built-in properties | Built-in properties are currently included by default, clarify behavior. |
| --with-classes | Include property classes | Uses :logseq.property/classes when expanded. |
| --with-type | Include property type | Uses :logseq.property/type when expanded. |
| --fields FIELD,FIELD | Select output fields | Applies when --expand is true. |
| --fields FIELD,FIELD | Select output fields | |
List block is removed to avoid overlap with search.

View File

@@ -94,23 +94,31 @@
:options options}}))
(def ^:private list-page-field-map
{"title" :block/title
{"id" :db/id
"ident" :db/ident
"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
{"id" :db/id
"ident" :db/ident
"title" :block/title
"uuid" :block/uuid
"created-at" :block/created-at
"updated-at" :block/updated-at
"properties" :logseq.property.class/properties
"extends" :logseq.property.class/extends
"description" :logseq.property/description})
(def ^:private list-property-field-map
{"name" :block/title
{"id" :db/id
"ident" :db/ident
"title" :block/title
"uuid" :block/uuid
"created-at" :block/created-at
"updated-at" :block/updated-at
"classes" :logseq.property/classes
"type" :logseq.property/type
"description" :logseq.property/description})
@@ -178,9 +186,7 @@
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)]
final (apply-fields limited fields list-page-field-map)]
{:status :ok
:data {:items final}})))
@@ -195,9 +201,7 @@
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)]
final (apply-fields limited fields list-tag-field-map)]
{:status :ok
:data {:items final}})))
@@ -212,8 +216,6 @@
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)]
final (apply-fields limited fields list-property-field-map)]
{:status :ok
:data {:items final}})))

View File

@@ -122,11 +122,6 @@
candidates* (str candidates*)
hint (str "\nHint: " hint)))))
(defn- maybe-ident-header
[items]
(when (some :db/ident items)
["IDENT"]))
(defn- parse-ts
[value]
(cond
@@ -154,37 +149,36 @@
: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- items-have-key?
[items & ks]
(some (fn [item] (some #(contains? item %) ks)) items))
(def ^:private list-columns
[["ID" (fn [item _] (or (:db/id item) (:id item))) [:db/id :id]]
["TITLE" (fn [item _] (or (:title item) (:block/title item) (:name item))) [:title :block/title :name]]
["IDENT" (fn [item _] (:db/ident item)) [:db/ident]]
["UPDATED-AT" (fn [item now-ms] (human-ago (or (:updated-at item) (:block/updated-at item)) now-ms)) [:updated-at :block/updated-at]]
["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at]]])
(defn- format-list-dynamic
[items now-ms columns]
(let [items (or items [])
active (filterv (fn [[_ _ ks always?]]
(or always? (apply items-have-key? items ks)))
columns)
headers (mapv first active)
rows (mapv (fn [item]
(mapv (fn [[_ extractor _]] (extractor item now-ms)) active))
items)]
(format-counted-table headers rows)))
(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))))
(format-list-dynamic items now-ms list-columns))
(defn- format-list-tag
[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))))
(format-list-dynamic items now-ms list-columns))
(defn- normalize-property-type
[value]
@@ -193,27 +187,17 @@
(nil? value) "-"
:else (str value)))
(defn- format-list-property-row
[item include-ident? now-ms]
(let [base [(or (:db/id item) (:id item))
(or (:title item) (:block/title item) (:name item))
(normalize-property-type (:logseq.property/type 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)))
(def ^:private list-property-columns
[["ID" (fn [item _] (or (:db/id item) (:id item))) [:db/id :id]]
["TITLE" (fn [item _] (or (:title item) (:block/title item) (:name item))) [:title :block/title :name]]
["TYPE" (fn [item _] (normalize-property-type (:logseq.property/type item))) [:logseq.property/type]]
["IDENT" (fn [item _] (:db/ident item)) [:db/ident]]
["UPDATED-AT" (fn [item now-ms] (human-ago (or (:updated-at item) (:block/updated-at item)) now-ms)) [:updated-at :block/updated-at]]
["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at]]])
(defn- format-list-property
[items now-ms]
(let [items (or items [])
include-ident? (boolean (some :db/ident items))
headers (into ["ID" "TITLE" "TYPE"]
(concat (or (maybe-ident-header items) [])
["UPDATED-AT" "CREATED-AT"]))]
(format-counted-table
headers
(mapv #(format-list-property-row % include-ident? now-ms) items))))
(format-list-dynamic items now-ms list-property-columns))
(defn- format-graph-list
[graphs current-graph]

View File

@@ -133,15 +133,21 @@
(testing "list property renders missing type as -"
(let [result (format/format-result {:status :ok
:command :list-property
:data {:items [{:block/title "Untyped"
:data {:items [{:block/title "Prop"
:db/id 99
:logseq.property/type :node
:block/created-at 40000
:block/updated-at 90000}
{:block/title "Untyped"
:db/id 100
:block/created-at 40000
:block/updated-at 90000}]}}
{:output-format nil
:now-ms 100000})]
(is (= (str "ID TITLE TYPE UPDATED-AT CREATED-AT\n"
"99 Prop node 10s ago 1m ago\n"
"100 Untyped - 10s ago 1m ago\n"
"Count: 1")
"Count: 2")
result)))))
(deftest test-human-output-add-upsert-remove

View File

@@ -200,7 +200,7 @@
:updated-at 1735686000000}
overrides)))
(deftest test-cli-login-integration
(deftest ^:long test-cli-login-integration
(async done
(let [data-dir (node-helper/create-tmp-dir "cli-login-data")
cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
@@ -237,7 +237,7 @@
(p/finally (fn []
(done))))))))
(deftest test-cli-logout-integration
(deftest ^:long test-cli-logout-integration
(async done
(let [data-dir (node-helper/create-tmp-dir "cli-logout-data")
cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
@@ -270,7 +270,7 @@
(p/finally (fn []
(done))))))))
(deftest test-cli-sync-remote-graphs-refreshes-auth-file-and-injects-runtime-token
(deftest ^:long test-cli-sync-remote-graphs-refreshes-auth-file-and-injects-runtime-token
(async done
(let [data-dir (node-helper/create-tmp-dir "cli-sync-auth")
cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
@@ -316,7 +316,7 @@
(p/finally (fn []
(done))))))))
(deftest test-cli-sync-download-and-start-readiness-with-mocked-sync
(deftest ^:long test-cli-sync-download-and-start-readiness-with-mocked-sync
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-sync-cli")
download-repo "sync-download-graph"
@@ -390,7 +390,7 @@
(is false (str "unexpected error: " e))
(done)))))))
(deftest test-cli-sync-upload-with-mocked-worker-bootstrap
(deftest ^:long test-cli-sync-upload-with-mocked-worker-bootstrap
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-sync-upload-cli")
upload-repo "sync-upload-graph"
@@ -432,7 +432,7 @@
(is false (str "unexpected error: " e))))
(p/finally done)))))
(deftest test-cli-sync-upload-followed-by-graph-info-shows-graph-uuid-test
(deftest ^:long test-cli-sync-upload-followed-by-graph-info-shows-graph-uuid-test
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-sync-upload-info-cli")
upload-repo "sync-upload-graph-info"
@@ -2209,6 +2209,37 @@
(is false (str "unexpected error: " e))
(done)))))))
(deftest ^:long test-cli-list-page-fields-filter
(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" "--graph" "fields-graph"] data-dir cfg-path)
_ (run-cli ["upsert" "page" "--page" "FieldTest"] data-dir cfg-path)
all-result (run-cli ["list" "page"] data-dir cfg-path)
all-payload (parse-json-output all-result)
filtered-result (run-cli ["list" "page" "--fields" "title"] data-dir cfg-path)
filtered-payload (parse-json-output filtered-result)
human-filtered-result (run-cli ["list" "page" "--fields" "title" "--output" "human"] data-dir cfg-path)
first-all (first (get-in all-payload [:data :items]))
first-filtered (first (get-in filtered-payload [:data :items]))
_ (stop-repo! data-dir cfg-path "fields-graph")]
(is (= 0 (:exit-code all-result)))
(is (= 0 (:exit-code filtered-result)))
(is (contains? first-all :title))
(is (contains? first-all :updated-at))
(is (contains? first-filtered :title))
(is (not (contains? first-filtered :updated-at))
"--fields title should exclude other fields")
(is (= 0 (:exit-code human-filtered-result)))
(is (string/includes? (:output human-filtered-result) "TITLE"))
(is (not (string/includes? (:output human-filtered-result) "UPDATED-AT"))
"--fields title human output should not show UPDATED-AT column")
(done))
(p/catch (fn [e]
(is false (str "unexpected error: " e))
(done)))))))
(deftest ^:long test-cli-show-page-block-by-id-and-uuid
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker")]