027-logseq-cli-update-command.md

This commit is contained in:
rcmerci
2026-02-01 23:38:53 +08:00
parent e9b68d5280
commit bd0f4ebe6a
9 changed files with 672 additions and 184 deletions

View File

@@ -0,0 +1,133 @@
# Logseq CLI Update Command (Move Refactor)
## Summary
Introduce a new `update` CLI command that subsumes the current `move` command and adds tag/property updates. The new command keeps all existing move capabilities/options, makes move targets optional (no target means no move), and adds update/remove semantics for tags and properties. The plan is grounded in current `logseq-cli` parsing/execution and db-worker-node outliner ops.
## Goals
- Replace `move` command behavior with `update` while keeping all current move options and semantics.
- Allow `update` to move blocks *and/or* update tags/properties in one command.
- Support new options:
- `--update-tags`, `--update-properties`
- `--remove-tags`, `--remove-properties`
- Reuse tag/property parsing and resolution rules from `add block`:
- Identifiers accept `db/id`, `db/ident`, `block/title`.
- `--update-tags` and `--update-properties` accept the same EDN format as `add block` `--tags`/`--properties`.
- `--remove-tags` and `--remove-properties` accept EDN vectors of tag/property identifiers.
## Non-goals
- Changing db-worker-node APIs or introducing new outliner ops.
- Expanding `update` beyond blocks (no page-level update scope in this iteration).
- Changing move semantics or target resolution.
## Background: Current Move Command
- CLI move implementation lives in `src/main/logseq/cli/command/move.cljs`.
- It requires a source (`--id` or `--uuid`) and a target (`--target-id`, `--target-uuid`, or `--target-page`).
- Execution uses `:thread-api/apply-outliner-ops` with `[:move-blocks ...]`.
- Move output formatting in `src/main/logseq/cli/format.cljs` uses `format-move-block`.
## Proposed Behavior (Update Command)
### Required/Optional Inputs
- **Source is required**: one of `--id` or `--uuid`.
- **Move targets are optional**:
- `--target-page`, `--target-uuid`, `--target-id`, `--pos` are optional.
- If no target is provided, no move occurs and the command only updates tags/properties.
- If a target is provided, move executes with the same semantics as today.
- **Update/remove options are optional**:
- `--update-tags` (EDN vector, same as `add block --tags`)
- `--update-properties` (EDN map, same as `add block --properties`)
- `--remove-tags` (EDN vector of tag identifiers)
- `--remove-properties` (EDN vector of property identifiers)
### Validation Rules
- Only one source selector is allowed: `--id` or `--uuid`.
- Only one target selector is allowed: `--target-id`, `--target-uuid`, or `--target-page`.
- `--pos sibling` is only valid when the target is a block (not a page), same as `move`.
- At least one of the following must be provided: target (move) or update/remove options.
- `--update-tags` and `--update-properties` accept the same EDN grammar and validations as in `add`.
- `--remove-tags` and `--remove-properties` must be non-empty vectors (EDN), with identifiers validated similarly to add.
### Execution Semantics
- Resolve source block to `db/id` using existing move helpers.
- If move target provided, resolve target entity using existing move helpers and compute `pos` opts.
- Tag/property updates use current outliner ops (no new db-worker changes):
- **Add/update tags**: `[:batch-set-property [block-ids :block/tags tag-id {}]]` for each tag.
- **Add/update properties**: `[:batch-set-property [block-ids property-id value {}]]`.
- **Remove tags**: `[:batch-delete-property-value [block-ids :block/tags tag-id]]`.
- **Remove properties**: `[:batch-remove-property [block-ids property-id]]`, with property resolution aligned to `add` (built-in/public property rules).
- Combine operations into a single `apply-outliner-ops` call when possible to keep updates atomic and reduce roundtrips.
## Design and Implementation Plan
### 1) Create `update` command module
- New file: `src/main/logseq/cli/command/update.cljs`.
- Start from `move.cljs` and expand the spec to include update/remove tag/property options.
- Extract shared helpers from `move.cljs` (e.g., `resolve-source`, `resolve-target`, `pos->opts`, `invalid-options?`) into a small shared namespace or move into `update.cljs`.
### 2) Reuse tag/property parsing and resolution from `add`
- Refactor `src/main/logseq/cli/command/add.cljs` to expose reusable helpers for:
- `parse-tags-option`, `parse-properties-option` (for update)
- `resolve-tags`, `resolve-properties` (for update)
- Tag/property identifier normalization functions if needed for remove vectors
- Keep the parsing behavior exactly consistent with `add block`.
- Add new helper in `add` (or a shared namespace) to parse **remove vectors**:
- `parse-tags-vector-option` (vector of tag identifiers)
- `parse-properties-vector-option` (vector of property identifiers)
### 3) Update top-level command registry
- Add `update` to `src/main/logseq/cli/commands.cljs` table and summary groups.
- Remove `move` from the command registry and help output (no alias/compat).
### 4) Update parse/validation logic
- In `finalize-command` (`src/main/logseq/cli/commands.cljs`):
- Add `update`-specific validation for sources, targets, and update/remove options.
- Reuse `update-command/invalid-options?`.
- Allow missing target when update/remove options are present.
- Keep error messaging aligned with existing CLI patterns.
### 5) Implement update action building
- `build-action` returns a combined action with:
- `:type :update-block`
- `:id`/`:uuid` for source, optional target selectors, `:pos` default `first-child` when move is requested
- `:update-tags`, `:update-properties`, `:remove-tags`, `:remove-properties`
- `execute-update`:
- Resolve source; resolve target only when move is requested.
- Build a vector of outliner ops, in order: move (if present), then remove tags/properties, then update/add tags/properties (order can be adjusted if needed for predictable results).
- Use `:thread-api/apply-outliner-ops` once with all ops.
### 6) Update output formatting
- `src/main/logseq/cli/format.cljs`:
- Add `format-update-block` to describe move + updates in a concise line.
- Update dispatcher to handle `:update-block`.
### 7) Tests
- Unit tests in `src/test/logseq/cli/commands_test.cljs`:
- Parsing: `update` accepts `--id/--uuid`, optional target, and new options.
- Validation: missing source, invalid target selector combinations, invalid pos, invalid EDN options.
- Ensure that `update` without target but with update/remove options is accepted.
- Format tests in `src/test/logseq/cli/format_test.cljs` for `:update-block`.
- Integration tests in `src/test/logseq/cli/integration_test.cljs`:
- Move-only update should behave the same as current `move`.
- Update tags/properties on an existing block.
- Remove tags/properties and validate via `show` or query.
## Open Questions
- If only `--pos` is provided without any target selector, return the error: `--pos is only valid when a target is provided`.
## Risks / Edge Cases
- If tag/property parsing rules diverge from `add`, user experience becomes inconsistent. Refactor shared parsing to avoid drift.
- Combining move and property updates in one `apply-outliner-ops` call needs to preserve correct operation order; keep move first unless property updates depend on position.
- Ensure non-page validation for source and block target remains intact when refactoring.
## Implementation Checklist (Concrete File Touches)
- Add: `src/main/logseq/cli/command/update.cljs`.
- Update: `src/main/logseq/cli/commands.cljs` (table, validation, action dispatch).
- Update: `src/main/logseq/cli/command/core.cljs` (top-level summary group list to include update).
- Update: `src/main/logseq/cli/format.cljs` (formatting for update).
- Update: `src/test/logseq/cli/commands_test.cljs`.
- Update: `src/test/logseq/cli/format_test.cljs`.
- Update: `src/test/logseq/cli/integration_test.cljs`.
## Verification
- Run unit tests: `bb dev:test -v logseq.cli.commands-test`.
- Run format tests: `bb dev:test -v logseq.cli.format-test`.
- Run CLI integration tests (move/update subset): `bb dev:test -v logseq.cli.integration-test`.
- Optional full suite: `bb dev:lint-and-test`.

View File

@@ -51,12 +51,12 @@
(let [page-name-lc (common-util/page-name-sanity-lc page-name)]
(p/let [page (transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/name :block/title] [:block/name page-name-lc]])]
(if (:db/id page)
page
(p/let [_ (transport/invoke config :thread-api/apply-outliner-ops false
[repo [[:create-page [page-name {}]]] {}])]
(transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/name :block/title] [:block/name page-name-lc]]))))))
(if (:db/id page)
page
(p/let [_ (transport/invoke config :thread-api/apply-outliner-ops false
[repo [[:create-page [page-name {}]]] {}])]
(transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/name :block/title] [:block/name page-name-lc]]))))))
(def ^:private add-positions
#{"first-child" "last-child" "sibling"})
@@ -224,7 +224,7 @@
:else text))
:else nil))
(defn- parse-tags-option
(defn parse-tags-option
[value]
(if-not (seq value)
{:ok? true :value nil}
@@ -245,6 +245,10 @@
(invalid-options-result "tags must be strings, keywords, uuids, or ids")
{:ok? true :value tags}))))))
(defn parse-tags-vector-option
[value]
(parse-tags-option value))
(defn- normalize-property-key
[value]
(cond
@@ -272,7 +276,7 @@
(when (seq text)
(get built-in-properties-by-title (common-util/page-name-sanity-lc text))))))
(defn- normalize-property-key-input
(defn normalize-property-key-input
[value]
(cond
(keyword? value) {:type :ident :value value}
@@ -426,7 +430,7 @@
[property]
(true? (get-in property [:schema :public?])))
(defn- parse-properties-option
(defn parse-properties-option
[value]
(if-not (seq value)
{:ok? true :value nil}
@@ -469,6 +473,44 @@
(invalid-options-result (str "invalid value for " key-ident ": " message))
(recur (rest prop-entries) (assoc acc key-ident normalized-value))))))))))))))))
(defn parse-properties-vector-option
[value]
(if-not (seq value)
{:ok? true :value nil}
(let [parsed (parse-edn-option value)]
(cond
(nil? parsed)
(invalid-options-result "properties must be valid EDN vector")
(not (vector? parsed))
(invalid-options-result "properties must be a vector")
(empty? parsed)
(invalid-options-result "properties must be a non-empty vector")
:else
(loop [prop-entries (seq parsed)
acc []]
(if (empty? prop-entries)
{:ok? true :value acc}
(let [entry (first prop-entries)
key-result (normalize-property-key-input entry)]
(if-not key-result
(invalid-options-result (str "invalid property key: " entry))
(let [{:keys [type value]} key-result]
(if (= type :id)
(recur (rest prop-entries) (conj acc value))
(let [property (get db-property/built-in-properties value)]
(cond
(nil? property)
(invalid-options-result (str "unknown built-in property: " value))
(not (property-public? property))
(invalid-options-result (str "property is not public: " value))
:else
(recur (rest prop-entries) (conj acc value))))))))))))))
(defn invalid-options?
[opts]
(let [pos (some-> (:pos opts) string/trim string/lower-case)
@@ -528,7 +570,7 @@
:else
entity))))
(defn- resolve-tags
(defn resolve-tags
[config repo tags]
(if (seq tags)
(p/let [entities (p/all (map #(resolve-tag-entity config repo %) tags))]
@@ -613,7 +655,7 @@
:date (resolve-date-page-id config repo value)
(p/resolved value))))
(defn- resolve-properties
(defn resolve-properties
[config repo properties]
(if-not (seq properties)
(p/resolved nil)
@@ -682,6 +724,61 @@
properties))]
(into {} resolved-entries))))
(defn resolve-property-identifiers
[config repo properties]
(if-not (seq properties)
(p/resolved nil)
(p/let [resolved-entries (p/all
(map (fn [k]
(cond
(keyword? k)
(let [property (get db-property/built-in-properties k)]
(when-not property
(throw (ex-info "unknown built-in property"
{:code :unknown-property :property k})))
(when-not (property-public? property)
(throw (ex-info "property is not public"
{:code :property-not-public :property k})))
(p/resolved k))
(number? k)
(p/let [entity (pull-entity config repo [:db/ident] k)
ident (:db/ident entity)
property (get db-property/built-in-properties ident)]
(cond
(nil? ident)
(throw (ex-info "property not found"
{:code :property-not-found :property k}))
(nil? property)
(throw (ex-info "unknown built-in property"
{:code :unknown-property :property ident}))
(not (property-public? property))
(throw (ex-info "property is not public"
{:code :property-not-public :property ident}))
:else
ident))
(string? k)
(let [ident (or (property-title->ident k)
(normalize-property-key k))
property (get db-property/built-in-properties ident)]
(when-not property
(throw (ex-info "unknown built-in property"
{:code :unknown-property :property k})))
(when-not (property-public? property)
(throw (ex-info "property is not public"
{:code :property-not-public :property ident})))
(p/resolved ident))
:else
(p/rejected (ex-info "invalid property key"
{:code :invalid-property :property k}))))
properties))]
(vec resolved-entries))))
(defn- resolve-add-target
[config {:keys [repo target-id target-uuid target-page-name]}]
(cond

View File

@@ -91,7 +91,7 @@
(defn top-level-summary
[table]
(let [groups [{:title "Graph Inspect and Edit"
:commands #{"list" "add" "remove" "move" "query" "show"}}
:commands #{"list" "add" "remove" "update" "query" "show"}}
{:title "Graph Management"
:commands #{"graph" "server"}}]
render-group (fn [{:keys [title commands]}]

View File

@@ -1,13 +1,14 @@
(ns logseq.cli.command.move
"Move-related CLI commands."
(ns logseq.cli.command.update
"Update-related CLI commands."
(:require [clojure.string :as string]
[logseq.cli.command.add :as add-command]
[logseq.cli.command.core :as core]
[logseq.cli.server :as cli-server]
[logseq.cli.transport :as transport]
[logseq.common.util :as common-util]
[promesa.core :as p]))
(def ^:private move-spec
(def ^:private update-spec
{:id {:desc "Source block db/id"
:coerce :long}
:uuid {:desc "Source block UUID"}
@@ -15,12 +16,16 @@
:coerce :long}
:target-uuid {:desc "Target block UUID"}
:target-page {:desc "Target page name"}
:pos {:desc "Position (first-child, last-child, sibling). Default: first-child"}})
:pos {:desc "Position (first-child, last-child, sibling). Default: first-child"}
:update-tags {:desc "Tags to add/update (EDN vector)"}
:update-properties {:desc "Properties to add/update (EDN map)"}
:remove-tags {:desc "Tags to remove (EDN vector)"}
:remove-properties {:desc "Properties to remove (EDN vector)"}})
(def entries
[(core/command-entry ["move"] :move-block "Move block" move-spec)])
[(core/command-entry ["update"] :update-block "Update block" update-spec)])
(def ^:private move-positions
(def ^:private update-positions
#{"first-child" "last-child" "sibling"})
(defn invalid-options?
@@ -29,9 +34,14 @@
source-selectors (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])
target-selectors (filter some? [(:target-id opts)
(:target-uuid opts)
(some-> (:target-page opts) string/trim)])]
(some-> (:target-page opts) string/trim)])
has-update-tags? (seq (some-> (:update-tags opts) string/trim))
has-update-properties? (seq (some-> (:update-properties opts) string/trim))
has-remove-tags? (seq (some-> (:remove-tags opts) string/trim))
has-remove-properties? (seq (some-> (:remove-properties opts) string/trim))
has-updates? (or has-update-tags? has-update-properties? has-remove-tags? has-remove-properties?)]
(cond
(and (seq pos) (not (contains? move-positions pos)))
(and (seq pos) (not (contains? update-positions pos)))
(str "invalid pos: " (:pos opts))
(> (count source-selectors) 1)
@@ -43,6 +53,12 @@
(and (= pos "sibling") (seq (some-> (:target-page opts) string/trim)))
"--pos sibling is only valid for block targets"
(and (seq pos) (empty? target-selectors))
"--pos is only valid when a target is provided"
(and (empty? target-selectors) (not has-updates?))
"target or update/remove options are required"
:else
nil)))
@@ -104,12 +120,13 @@
(throw (ex-info "target block not found" {:code :target-not-found})))))
(seq target-page)
(p/let [entity (transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/name :block/title]
[:block/name target-page]])]
(let [page-name (common-util/page-name-sanity-lc target-page)]
(p/let [entity (transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/name :block/title]
[:block/name page-name]])]
(if (:db/id entity)
entity
(throw (ex-info "page not found" {:code :page-not-found}))))
(throw (ex-info "page not found" {:code :page-not-found})))))
:else
(p/rejected (ex-info "target is required" {:code :missing-target}))))
@@ -127,13 +144,23 @@
(if-not (seq repo)
{:ok? false
:error {:code :missing-repo
:message "repo is required for move"}}
:message "repo is required for update"}}
(let [id (:id options)
uuid (some-> (:uuid options) string/trim)
target-id (:target-id options)
target-uuid (some-> (:target-uuid options) string/trim)
page-name (some-> (:target-page options) string/trim)
pos (some-> (:pos options) string/trim string/lower-case)
update-tags-result (add-command/parse-tags-option (:update-tags options))
update-properties-result (add-command/parse-properties-option (:update-properties options))
remove-tags-result (add-command/parse-tags-vector-option (:remove-tags options))
remove-properties-result (add-command/parse-properties-vector-option (:remove-properties options))
update-tags (:value update-tags-result)
update-properties (:value update-properties-result)
remove-tags (:value remove-tags-result)
remove-properties (:value remove-properties-result)
has-target? (or (some? target-id) (seq target-uuid) (seq page-name))
has-updates? (or (seq update-tags) (seq update-properties) (seq remove-tags) (seq remove-properties))
source-label (cond
(seq uuid) uuid
(some? id) (str id)
@@ -149,14 +176,26 @@
:error {:code :missing-source
:message "source block is required"}}
(not (or (some? target-id) (seq target-uuid) (seq page-name)))
(and (not has-target?) (not has-updates?))
{:ok? false
:error {:code :missing-target
:message "target is required"}}
:error {:code :invalid-options
:message "target or update/remove options are required"}}
(not (:ok? update-tags-result))
update-tags-result
(not (:ok? update-properties-result))
update-properties-result
(not (:ok? remove-tags-result))
remove-tags-result
(not (:ok? remove-properties-result))
remove-properties-result
:else
{:ok? true
:action {:type :move-block
:action {:type :update-block
:repo repo
:graph (core/repo->graph repo)
:id id
@@ -164,18 +203,56 @@
:target-id target-id
:target-uuid target-uuid
:target-page page-name
:pos (or pos "first-child")
:pos (when has-target? (or pos "first-child"))
:update-tags update-tags
:update-properties update-properties
:remove-tags remove-tags
:remove-properties remove-properties
:source source-label
:target target-label}}))))
(defn execute-move
(defn execute-update
[action config]
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
source (resolve-source cfg (:repo action) action)
target (resolve-target cfg (:repo action) action)
opts (pos->opts (:pos action))
ops [[:move-blocks [[(:db/id source)] (:db/id target) opts]]]
result (transport/invoke cfg :thread-api/apply-outliner-ops false
[(:repo action) ops {}])]
target (when (or (:target-id action)
(:target-uuid action)
(seq (:target-page action)))
(resolve-target cfg (:repo action) action))
opts (when target (pos->opts (:pos action)))
update-tags (add-command/resolve-tags cfg (:repo action) (:update-tags action))
remove-tags (add-command/resolve-tags cfg (:repo action) (:remove-tags action))
update-properties (add-command/resolve-properties cfg (:repo action) (:update-properties action))
remove-properties (add-command/resolve-property-identifiers cfg (:repo action)
(:remove-properties action))
block-id (:db/id source)
block-ids [block-id]
update-tag-ids (when (seq update-tags)
(->> update-tags (map :db/id) (remove nil?) vec))
remove-tag-ids (when (seq remove-tags)
(->> remove-tags (map :db/id) (remove nil?) vec))
ops (cond-> []
target (conj [:move-blocks [[(:db/id source)] (:db/id target) opts]]))
ops (cond-> ops
(seq remove-tag-ids)
(into (map (fn [tag-id]
[:batch-delete-property-value [block-ids :block/tags tag-id]])
remove-tag-ids))
(seq remove-properties)
(into (map (fn [property-id]
[:batch-remove-property [block-ids property-id]])
remove-properties))
(seq update-tag-ids)
(into (map (fn [tag-id]
[:batch-set-property [block-ids :block/tags tag-id {}]])
update-tag-ids))
(seq update-properties)
(into (map (fn [[k v]]
[:batch-set-property [block-ids k v {}]])
update-properties)))
result (if (seq ops)
(transport/invoke cfg :thread-api/apply-outliner-ops false
[(:repo action) ops {}])
(p/resolved nil))]
{:status :ok
:data {:result result}})))

View File

@@ -6,11 +6,11 @@
[logseq.cli.command.core :as command-core]
[logseq.cli.command.graph :as graph-command]
[logseq.cli.command.list :as list-command]
[logseq.cli.command.move :as move-command]
[logseq.cli.command.query :as query-command]
[logseq.cli.command.remove :as remove-command]
[logseq.cli.command.server :as server-command]
[logseq.cli.command.show :as show-command]
[logseq.cli.command.update :as update-command]
[logseq.cli.server :as cli-server]
[promesa.core :as p]))
@@ -98,8 +98,8 @@
server-command/entries
list-command/entries
add-command/entries
move-command/entries
remove-command/entries
update-command/entries
query-command/entries
show-command/entries)))
@@ -147,10 +147,7 @@
remove-targets (filter some? [(:id opts)
(some-> (:uuid opts) string/trim)
(some-> (:page opts) string/trim)])
move-sources (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])
move-targets (filter some? [(:target-id opts)
(some-> (:target-uuid opts) string/trim)
(some-> (:target-page opts) string/trim)])]
update-sources (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])]
(cond
(:help opts)
(command-core/help-result cmd-summary)
@@ -177,15 +174,12 @@
(and (= command :remove) (> (count remove-targets) 1))
(command-core/invalid-options-result summary "only one of --id, --uuid, or --page is allowed")
(and (= command :move-block) (move-command/invalid-options? opts))
(command-core/invalid-options-result summary (move-command/invalid-options? opts))
(and (= command :update-block) (update-command/invalid-options? opts))
(command-core/invalid-options-result summary (update-command/invalid-options? opts))
(and (= command :move-block) (empty? move-sources))
(and (= command :update-block) (empty? update-sources))
(missing-source-result summary)
(and (= command :move-block) (empty? move-targets))
(missing-target-result summary)
(and (= command :show) (empty? show-targets))
(missing-target-result summary)
@@ -366,8 +360,8 @@
:add-page
(add-command/build-add-page-action options repo)
:move-block
(move-command/build-action options repo)
:update-block
(update-command/build-action options repo)
:remove
(remove-command/build-action options repo)
@@ -411,7 +405,7 @@
:list-property (list-command/execute-list-property action config)
:add-block (add-command/execute-add-block action config)
:add-page (add-command/execute-add-page action config)
:move-block (move-command/execute-move action config)
:update-block (update-command/execute-update action config)
:remove (remove-command/execute-remove action config)
:query (query-command/execute-query action config)
:query-list (query-command/execute-query-list action config)
@@ -426,4 +420,6 @@
:message "unknown action"}}))]
(assoc result
:command (or (:command action) (:type action))
:context (select-keys action [:repo :graph :page :id :ids :uuid :block :blocks :source :target])))))
:context (select-keys action [:repo :graph :page :id :ids :uuid :block :blocks
:source :target :update-tags :update-properties
:remove-tags :remove-properties])))))

View File

@@ -246,17 +246,25 @@
(some? id) (str "Removed block: " id " (repo: " repo ")")
:else (str "Removed item (repo: " repo ")")))
(defn- format-move-block
[{:keys [repo source target]}]
(str "Moved block: " source " -> " target " (repo: " repo ")"))
(defn- format-update-block
[{:keys [repo source target update-tags update-properties remove-tags remove-properties]}]
(let [change-parts (cond-> []
(seq update-tags) (conj (str "tags:+" (count update-tags)))
(seq update-properties) (conj (str "properties:+" (count update-properties)))
(seq remove-tags) (conj (str "remove-tags:+" (count remove-tags)))
(seq remove-properties) (conj (str "remove-properties:+" (count remove-properties))))
changes (when (seq change-parts)
(str ", " (string/join ", " change-parts)))
move-fragment (when (seq target)
(str " -> " target))]
(str "Updated block: " source (or move-fragment "") " (repo: " repo (or changes "") ")")))
(defn- format-graph-export
[{:keys [export-type output]}]
(str "Exported " export-type " to " output))
(defn- format-graph-import
[{:keys [import-type input] :as xxx}]
(prn :xxx xxx)
[{:keys [import-type input]}]
(str "Imported " import-type " from " input))
(defn- format-graph-action
@@ -288,7 +296,7 @@
:add-block (format-add-block context)
:add-page (format-add-page context)
:remove (format-remove context)
:move-block (format-move-block context)
:update-block (format-update-block context)
:graph-export (format-graph-export context)
:graph-import (format-graph-import context)
:query (format-query-results (:result data))

View File

@@ -61,7 +61,7 @@
(is (string/includes? plain-summary "list"))
(is (string/includes? plain-summary "add"))
(is (string/includes? plain-summary "remove"))
(is (string/includes? plain-summary "move"))
(is (string/includes? plain-summary "update"))
(is (string/includes? plain-summary "query"))
(is (string/includes? plain-summary "show"))
(is (string/includes? plain-summary "graph"))
@@ -72,7 +72,7 @@
(is (contains-bold? summary "add block"))
(is (contains-bold? summary "add page"))
(is (contains-bold? summary "remove"))
(is (contains-bold? summary "move"))
(is (contains-bold? summary "update"))
(is (contains-bold? summary "query"))
(is (contains-bold? summary "query list"))
(is (contains-bold? summary "show"))
@@ -153,18 +153,22 @@
(is (contains-bold? summary "--uuid"))
(is (contains-bold? summary "--page"))))
(testing "move command shows help"
(testing "update command shows help"
(let [result (binding [style/*color-enabled?* true]
(commands/parse-args ["move" "--help"]))
(commands/parse-args ["update" "--help"]))
summary (:summary result)
plain-summary (strip-ansi summary)]
(is (true? (:help? result)))
(is (string/includes? plain-summary "Usage: logseq move"))
(is (string/includes? plain-summary "Usage: logseq update"))
(is (string/includes? plain-summary "Command options:"))
(is (contains-bold? summary "--id"))
(is (contains-bold? summary "--uuid"))
(is (contains-bold? summary "--target-id"))
(is (contains-bold? summary "--target-uuid"))))
(is (contains-bold? summary "--target-uuid"))
(is (contains-bold? summary "--update-tags"))
(is (contains-bold? summary "--update-properties"))
(is (contains-bold? summary "--remove-tags"))
(is (contains-bold? summary "--remove-properties"))))
(testing "server group shows subcommands"
(let [result (binding [style/*color-enabled?* true]
@@ -743,23 +747,39 @@
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "move requires source selector"
(let [result (commands/parse-args ["move" "--target-id" "10"])]
(testing "update requires source selector"
(let [result (commands/parse-args ["update" "--target-id" "10"])]
(is (false? (:ok? result)))
(is (= :missing-source (get-in result [:error :code])))))
(testing "move requires target selector"
(let [result (commands/parse-args ["move" "--id" "1"])]
(testing "update requires target or update/remove options"
(let [result (commands/parse-args ["update" "--id" "1"])]
(is (false? (:ok? result)))
(is (= :missing-target (get-in result [:error :code])))))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "move parses with source and target"
(let [result (commands/parse-args ["move" "--uuid" "abc" "--target-uuid" "def" "--pos" "last-child"])]
(testing "update parses with source and target"
(let [result (commands/parse-args ["update" "--uuid" "abc" "--target-uuid" "def" "--pos" "last-child"])]
(is (true? (:ok? result)))
(is (= :move-block (:command result)))
(is (= :update-block (:command result)))
(is (= "abc" (get-in result [:options :uuid])))
(is (= "def" (get-in result [:options :target-uuid])))
(is (= "last-child" (get-in result [:options :pos]))))))
(is (= "last-child" (get-in result [:options :pos])))))
(testing "update parses with tags and properties"
(let [result (commands/parse-args ["update" "--id" "1"
"--update-tags" "[\"TagA\"]"
"--update-properties" "{:logseq.property/publishing-public? true}"])]
(is (true? (:ok? result)))
(is (= :update-block (:command result)))
(is (= "[\"TagA\"]" (get-in result [:options :update-tags])))
(is (= "{:logseq.property/publishing-public? true}" (get-in result [:options :update-properties])))))
(testing "update allows update without move target"
(let [result (commands/parse-args ["update" "--uuid" "abc"
"--update-tags" "[\"TagA\"]"])]
(is (true? (:ok? result)))
(is (= :update-block (:command result)))
(is (= "abc" (get-in result [:options :uuid]))))))
(deftest test-verb-subcommand-parse-add
(testing "add block requires content source"
@@ -835,11 +855,11 @@
(is (= "[\"TagA\"]" (get-in result [:options :tags])))
(is (= "{:logseq.property/publishing-public? true}" (get-in result [:options :properties]))))))
(deftest test-verb-subcommand-parse-move-target-page
(testing "move parses with target page"
(let [result (commands/parse-args ["move" "--id" "1" "--target-page" "Home"])]
(deftest test-verb-subcommand-parse-update-target-page
(testing "update parses with target page"
(let [result (commands/parse-args ["update" "--id" "1" "--target-page" "Home"])]
(is (true? (:ok? result)))
(is (= :move-block (:command result)))
(is (= :update-block (:command result)))
(is (= 1 (get-in result [:options :id])))
(is (= "Home" (get-in result [:options :target-page]))))))
@@ -953,7 +973,7 @@
(doseq [args [["list" "page" "--wat"]
["add" "block" "--wat"]
["remove" "--wat"]
["move" "--wat"]
["update" "--wat"]
["show" "--wat"]]]
(let [result (commands/parse-args args)]
(is (false? (:ok? result)))
@@ -1126,45 +1146,119 @@
(let [normalize-property-key-input #'add-command/normalize-property-key-input]
(is (= {:type :id :value 42} (normalize-property-key-input 42)))))
(deftest test-build-action-move
(testing "move requires source selector"
(let [parsed {:ok? true :command :move-block :options {:target-id 2}}
(deftest test-build-action-update
(testing "update requires source selector"
(let [parsed {:ok? true :command :update-block :options {:target-id 2}}
result (commands/build-action parsed {:repo "demo"})]
(is (false? (:ok? result)))
(is (= :missing-source (get-in result [:error :code])))))
(testing "move requires target selector"
(let [parsed {:ok? true :command :move-block :options {:id 1}}
(testing "update requires target or update/remove options"
(let [parsed {:ok? true :command :update-block :options {:id 1}}
result (commands/build-action parsed {:repo "demo"})]
(is (false? (:ok? result)))
(is (= :missing-target (get-in result [:error :code]))))))
(deftest test-move-parse-validation
(testing "move rejects multiple source selectors"
(let [result (commands/parse-args ["move" "--id" "1" "--uuid" "abc" "--target-id" "2"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "move rejects multiple target selectors"
(let [result (commands/parse-args ["move" "--id" "1" "--target-id" "2" "--target-uuid" "def"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "update accepts update tags without target"
(let [parsed {:ok? true
:command :update-block
:options {:id 1 :update-tags "[\"TagA\"]"}}
result (commands/build-action parsed {:repo "demo"})]
(is (true? (:ok? result)))
(is (= :update-block (get-in result [:action :type])))
(is (= ["TagA"] (get-in result [:action :update-tags])))))
(testing "move rejects invalid position"
(let [result (commands/parse-args ["move" "--id" "1" "--target-id" "2" "--pos" "middle"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "move rejects sibling pos for page target"
(let [result (commands/parse-args ["move" "--id" "1" "--target-page" "Home" "--pos" "sibling"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "move rejects legacy target-page-name option"
(let [result (commands/parse-args ["move" "--id" "1" "--target-page-name" "Home"])]
(testing "update rejects invalid update tags"
(let [parsed {:ok? true
:command :update-block
:options {:id 1 :update-tags "{:tag \"no\"}"}}
result (commands/build-action parsed {:repo "demo"})]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code]))))))
(deftest test-update-parse-validation
(testing "update rejects multiple source selectors"
(let [result (commands/parse-args ["update" "--id" "1" "--uuid" "abc" "--target-id" "2"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "update rejects multiple target selectors"
(let [result (commands/parse-args ["update" "--id" "1" "--target-id" "2" "--target-uuid" "def"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "update rejects invalid position"
(let [result (commands/parse-args ["update" "--id" "1" "--target-id" "2" "--pos" "middle"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "update rejects sibling pos for page target"
(let [result (commands/parse-args ["update" "--id" "1" "--target-page" "Home" "--pos" "sibling"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "update rejects legacy target-page-name option"
(let [result (commands/parse-args ["update" "--id" "1" "--target-page-name" "Home"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "update rejects pos without target"
(let [result (commands/parse-args ["update" "--id" "1" "--pos" "last-child" "--update-tags" "[\"TagA\"]"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code]))))))
(deftest test-execute-update-builds-batch-ops
(async done
(let [ops* (atom nil)
calls* (atom [])
action {:type :update-block
:repo "demo"
:id 1
:target-id 2
:pos "last-child"
:update-tags [:tag/new]
:remove-tags [:tag/old]
:update-properties {:logseq.property/deadline "2026-01-25T12:00:00Z"}
:remove-properties [:logseq.property/publishing-public?]}]
(with-redefs [cli-server/ensure-server! (fn [_ _] {:base-url "http://example"})
add-command/resolve-tags (fn [_ _ tags]
(p/resolved (cond
(= tags [:tag/new]) [{:db/id 101}]
(= tags [:tag/old]) [{:db/id 202}]
:else nil)))
add-command/resolve-properties (fn [_ _ properties] (p/resolved properties))
add-command/resolve-property-identifiers (fn [_ _ properties] (p/resolved properties))
transport/invoke (fn [_ method _ args]
(swap! calls* conj {:method method :args args})
(case method
:thread-api/pull (let [[_ _ lookup] args]
(cond
(= lookup 1)
{:db/id 1
:block/name nil
:block/uuid (uuid "00000000-0000-0000-0000-000000000001")}
(= lookup 2)
{:db/id 2
:block/name nil
:block/uuid (uuid "00000000-0000-0000-0000-000000000002")}
:else {}))
:thread-api/apply-outliner-ops (let [[_ ops _] args]
(reset! ops* ops)
{:result :ok})
(throw (ex-info "unexpected invoke" {:method method :calls @calls*}))))]
(-> (p/let [result (commands/execute action {})]
(is (= :ok (:status result)))
(is (= [[:move-blocks [[1] 2 {:sibling? false :bottom? true}]]
[:batch-delete-property-value [[1] :block/tags 202]]
[:batch-remove-property [[1] :logseq.property/publishing-public?]]
[:batch-set-property [[1] :block/tags 101 {}]]
[:batch-set-property [[1] :logseq.property/deadline "2026-01-25T12:00:00Z" {}]]]
@ops*))
(done))
(p/catch (fn [e]
(is false (str "unexpected error: " e " calls: " @calls*))
(done))))))))
(deftest test-execute-requires-existing-graph
(async done
(with-redefs [cli-server/list-graphs (fn [_] [])

View File

@@ -108,15 +108,29 @@
{:output-format nil})]
(is (= "Removed page: Home (repo: demo-repo)" result))))
(testing "move block renders a succinct success line"
(testing "update block renders a succinct success line"
(let [result (format/format-result {:status :ok
:command :move-block
:command :update-block
:context {:repo "demo-repo"
:source "source-uuid"
:target "target-uuid"}
:target "target-uuid"
:update-tags ["TagA"]
:update-properties {:logseq.property/publishing-public? true}
:remove-tags ["TagB"]
:remove-properties [:logseq.property/deadline]}
:data {:result {:ok true}}}
{:output-format nil})]
(is (= "Moved block: source-uuid -> target-uuid (repo: demo-repo)" result)))))
(is (= "Updated block: source-uuid -> target-uuid (repo: demo-repo, tags:+1, properties:+1, remove-tags:+1, remove-properties:+1)" result))))
(testing "update without move target renders a succinct success line"
(let [result (format/format-result {:status :ok
:command :update-block
:context {:repo "demo-repo"
:source "source-uuid"
:update-tags ["TagA"]}
:data {:result {:ok true}}}
{:output-format nil})]
(is (= "Updated block: source-uuid (repo: demo-repo, tags:+1)" result)))))
(deftest test-human-output-graph-import-export
(testing "graph export renders a succinct success line"

View File

@@ -16,11 +16,19 @@
(defn- run-cli
[args data-dir cfg-path]
(let [args-with-output (if (some #{"--output"} args)
args
(concat args ["--output" "json"]))
(let [args (vec args)
output-idx (.indexOf args "--output")
[args output-args] (if (and (>= output-idx 0)
(< (inc output-idx) (count args)))
[(vec (concat (subvec args 0 output-idx)
(subvec args (+ output-idx 2))))
["--output" (nth args (inc output-idx))]]
[args []])
output-args (if (seq output-args)
output-args
["--output" "json"])
global-opts ["--data-dir" data-dir "--config" cfg-path]
final-args (vec (concat global-opts args-with-output))]
final-args (vec (concat global-opts output-args args))]
(-> (cli-main/run! final-args {:exit? false})
(p/then (fn [result]
(let [res (if (map? result)
@@ -30,7 +38,12 @@
(defn- parse-json-output
[result]
(js->clj (js/JSON.parse (:output result)) :keywordize-keys true))
(try
(js->clj (js/JSON.parse (:output result)) :keywordize-keys true)
(catch :default e
(throw (ex-info "json parse failed"
{:output (:output result)}
e)))))
(defn- parse-json-output-safe
[result label]
@@ -76,6 +89,10 @@
[node]
(or (:block/children node) (:children node)))
(defn- node-id
[node]
(or (:db/id node) (:id node)))
(defn- item-id
[item]
(or (:db/id item) (:id item)))
@@ -112,35 +129,32 @@
(defn- query-tags
[data-dir cfg-path repo title]
(p/let [payload (run-query data-dir cfg-path repo
"[:find ?tag :in $ ?title :where [?b :block/title ?title] [?b :block/tags ?t] [?t :block/title ?tag]]"
(pr-str [title]))]
(->> (get-in payload [:data :result])
(map first)
set)))
(let [name (common-util/page-name-sanity-lc title)]
(p/let [payload (run-query data-dir cfg-path repo
"[:find ?tag :in $ ?title ?name :where (or [?b :block/title ?title] [?b :block/content ?title] [?b :block/name ?name]) [?b :block/tags ?t] (or [?t :block/title ?tag] [?t :block/name ?tag])]"
(pr-str [title name]))]
(->> (get-in payload [:data :result])
(map first)
set))))
(defn- query-property
[data-dir cfg-path repo title property]
(p/let [payload (run-query data-dir cfg-path repo
(str "[:find ?value :in $ ?title :where [?e :block/title ?title] [?e "
property
" ?value]]")
(pr-str [title]))]
(first (first (get-in payload [:data :result])))))
(let [name (common-util/page-name-sanity-lc title)]
(p/let [payload (run-query data-dir cfg-path repo
(str "[:find ?value :in $ ?title ?name :where (or [?e :block/title ?title] [?e :block/content ?title] [?e :block/name ?name]) [?e "
property
" ?value]]")
(pr-str [title name]))]
(first (first (get-in payload [:data :result]))))))
(defn- query-block-id
(defn- query-block-uuid-by-title
[data-dir cfg-path repo title]
(p/let [payload (run-query data-dir cfg-path repo
"[:find ?id . :in $ ?title :where [?b :block/title ?title] [?b :db/id ?id]]"
(pr-str [title]))]
(get-in payload [:data :result])))
(defn- query-block-uuid-by-id
[data-dir cfg-path repo id]
(p/let [payload (run-query data-dir cfg-path repo
"[:find ?uuid . :in $ ?id :where [?b :db/id ?id] [?b :block/uuid ?uuid]]"
(pr-str [id]))]
(get-in payload [:data :result])))
(let [name (common-util/page-name-sanity-lc title)]
(p/let [_ (p/delay 300)
payload (run-query data-dir cfg-path repo
"[:find ?uuid . :in $ ?title ?name :where (or [?b :block/title ?title] [?b :block/content ?title] [?b :block/name ?name]) [?b :block/uuid ?uuid]]"
(pr-str [title name]))]
(get-in payload [:data :result]))))
(defn- list-items
[data-dir cfg-path repo list-type]
@@ -246,9 +260,9 @@
add-block-result (run-cli ["--repo" "content-graph" "add" "block" "--target-page-name" "TestPage" "--content" "Test block"] data-dir cfg-path)
add-block-payload (parse-json-output add-block-result)
_ (p/delay 100)
show-result (run-cli ["--repo" "content-graph" "show" "--page-name" "TestPage" "--format" "json"] data-dir cfg-path)
show-result (run-cli ["--repo" "content-graph" "show" "--page" "TestPage" ] data-dir cfg-path)
show-payload (parse-json-output show-result)
remove-page-result (run-cli ["--repo" "content-graph" "remove" "page" "--page" "TestPage"] data-dir cfg-path)
remove-page-result (run-cli ["--repo" "content-graph" "remove" "--page" "TestPage"] data-dir cfg-path)
remove-page-payload (parse-json-output remove-page-result)
stop-result (run-cli ["server" "stop" "--repo" "content-graph"] data-dir cfg-path)
stop-payload (parse-json-output stop-result)]
@@ -262,7 +276,8 @@
(is (= "ok" (:status list-property-payload)))
(is (vector? (get-in list-property-payload [:data :items])))
(is (= "ok" (:status show-payload)))
(is (contains? (get-in show-payload [:data :root]) :db/id))
(is (some? (or (get-in show-payload [:data :root :db/id])
(get-in show-payload [:data :root :id]))))
(is (not (contains? (get-in show-payload [:data :root]) :block/uuid)))
(is (= "ok" (:status remove-page-payload)))
(is (= "ok" (:status stop-payload)))
@@ -509,7 +524,49 @@
(pr-str (:error add-block-id-payload)))
(is (= "ok" (:status add-block-id-payload)))
(is (true? page-id-value))
(is (number? block-deadline-id))
(is (number? block-deadline-id))
(is (= "ok" (:status stop-payload)))
(done))
(p/catch (fn [e]
(is false (str "unexpected error: " e))
(done)))))))
(deftest test-cli-update-tags-and-properties
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-update-tags")]
(-> (p/let [{:keys [cfg-path repo]} (setup-tags-graph data-dir)
tag-a-name "Quote"
tag-b-name "Math"
add-block-result (run-cli ["--repo" repo
"add" "block"
"--target-page-name" "Home"
"--content" "Update block"
"--tags" "[:logseq.class/Quote-block]"
"--properties" "{:logseq.property/publishing-public? true}"]
data-dir cfg-path)
add-block-payload (parse-json-output add-block-result)
_ (p/delay 100)
show-home (run-cli ["--repo" repo "show" "--page" "Home"] data-dir cfg-path)
show-home-payload (parse-json-output show-home)
block-node (find-block-by-title (get-in show-home-payload [:data :root]) "Update block")
block-id (node-id block-node)
update-result (run-cli ["--repo" repo
"update"
"--id" (str block-id)
"--update-tags" "[:logseq.class/Math-block]"
"--remove-tags" "[:logseq.class/Quote-block]"
"--update-properties" "{:logseq.property/deadline \"2026-01-25T12:00:00Z\"}"
"--remove-properties" "[:logseq.property/publishing-public?]"]
data-dir cfg-path)
update-payload (parse-json-output update-result)
_ (p/delay 300)
stop-payload (stop-repo! data-dir cfg-path repo)]
(is (= 0 (:exit-code add-block-result)))
(is (= "ok" (:status add-block-payload)))
(is (string? tag-a-name))
(is (string? tag-b-name))
(is (some? block-id))
(is (= "ok" (:status update-payload)))
(is (= "ok" (:status stop-payload)))
(done))
(p/catch (fn [e]
@@ -719,8 +776,8 @@
page-id (or (:db/id page-item) (:id page-item))
show-result (run-cli ["--repo" "recent-updated-graph"
"show"
"--page-name" "RecentPage"
"--format" "json"]
"--page" "RecentPage"
]
data-dir cfg-path)
show-payload (parse-json-output show-result)
show-root (get-in show-payload [:data :root])
@@ -804,16 +861,20 @@
_ (run-cli ["graph" "create" "--repo" "nested-refs"] data-dir cfg-path)
_ (run-cli ["--repo" "nested-refs" "add" "page" "--page" "NestedPage"] data-dir cfg-path)
_ (run-cli ["--repo" "nested-refs" "add" "block" "--target-page-name" "NestedPage" "--content" "Inner"] data-dir cfg-path)
inner-id (query-block-id data-dir cfg-path "nested-refs" "Inner")
inner-uuid (query-block-uuid-by-id data-dir cfg-path "nested-refs" inner-id)
show-nested (run-cli ["--repo" "nested-refs" "show" "--page" "NestedPage"] data-dir cfg-path)
show-nested-payload (parse-json-output show-nested)
_inner-node (find-block-by-title (get-in show-nested-payload [:data :root]) "Inner")
inner-uuid (query-block-uuid-by-title data-dir cfg-path "nested-refs" "Inner")
middle-content (str "See [[" inner-uuid "]]")
_ (run-cli ["--repo" "nested-refs" "add" "block" "--target-page-name" "NestedPage"
"--content" middle-content] data-dir cfg-path)
middle-id (query-block-id data-dir cfg-path "nested-refs" middle-content)
middle-uuid (query-block-uuid-by-id data-dir cfg-path "nested-refs" middle-id)
show-middle (run-cli ["--repo" "nested-refs" "show" "--page" "NestedPage"] data-dir cfg-path)
show-middle-payload (parse-json-output show-middle)
_middle-node (find-block-by-title (get-in show-middle-payload [:data :root]) middle-content)
middle-uuid (query-block-uuid-by-title data-dir cfg-path "nested-refs" middle-content)
_ (run-cli ["--repo" "nested-refs" "add" "block" "--target-page-name" "NestedPage"
"--content" (str "Outer [[" middle-uuid "]]")] data-dir cfg-path)
show-outer (run-cli ["--repo" "nested-refs" "show" "--page-name" "NestedPage" "--format" "json"] data-dir cfg-path)
show-outer (run-cli ["--repo" "nested-refs" "show" "--page" "NestedPage" ] data-dir cfg-path)
show-outer-payload (parse-json-output show-outer)
outer-node (find-block-by-title (get-in show-outer-payload [:data :root]) "Outer [[See [[Inner]]]]")
stop-result (run-cli ["server" "stop" "--repo" "nested-refs"] data-dir cfg-path)
@@ -836,21 +897,23 @@
_ (run-cli ["graph" "create" "--repo" "linked-refs-graph"] data-dir cfg-path)
_ (run-cli ["--repo" "linked-refs-graph" "add" "page" "--page" "TargetPage"] data-dir cfg-path)
_ (run-cli ["--repo" "linked-refs-graph" "add" "page" "--page" "SourcePage"] data-dir cfg-path)
target-id (query-block-id data-dir cfg-path "linked-refs-graph" "TargetPage")
target-uuid (query-block-uuid-by-id data-dir cfg-path "linked-refs-graph" target-id)
target-show (run-cli ["--repo" "linked-refs-graph" "show" "--page" "TargetPage"] data-dir cfg-path)
_target-show-payload (parse-json-output target-show)
target-uuid (query-block-uuid-by-title data-dir cfg-path "linked-refs-graph" "TargetPage")
target-title "TargetPage"
ref-content (str "See [[" target-uuid "]]")
ref-title (str "See [[" target-title "]]")
_ (run-cli ["--repo" "linked-refs-graph" "add" "block" "--target-page-name" "SourcePage" "--content" ref-content] data-dir cfg-path)
source-show (run-cli ["--repo" "linked-refs-graph" "show" "--page-name" "SourcePage" "--format" "json"] data-dir cfg-path)
_ (p/delay 100)
source-show (run-cli ["--repo" "linked-refs-graph" "show" "--page" "SourcePage" ] data-dir cfg-path)
source-payload (parse-json-output source-show)
ref-node (find-block-by-title (get-in source-payload [:data :root]) ref-title)
ref-id (:db/id ref-node)
target-show (run-cli ["--repo" "linked-refs-graph" "show" "--page-name" "TargetPage" "--format" "json"] data-dir cfg-path)
ref-id (or (:db/id ref-node) (:id ref-node))
target-show (run-cli ["--repo" "linked-refs-graph" "show" "--page" "TargetPage" ] data-dir cfg-path)
target-payload (parse-json-output target-show)
linked-refs (get-in target-payload [:data :linked-references])
linked-blocks (:blocks linked-refs)
linked-ids (set (map :db/id linked-blocks))
linked-ids (set (map #(or (:db/id %) (:id %)) linked-blocks))
linked-page-titles (set (keep (fn [block]
(or (get-in block [:block/page :block/title])
(get-in block [:block/page :block/name])
@@ -870,7 +933,7 @@
(is false (str "unexpected error: " e))
(done)))))))
(deftest test-cli-move-block
(deftest test-cli-update-block-move
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-move")]
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
@@ -879,19 +942,22 @@
_ (run-cli ["--repo" "move-graph" "add" "page" "--page" "SourcePage"] data-dir cfg-path)
_ (run-cli ["--repo" "move-graph" "add" "page" "--page" "TargetPage"] data-dir cfg-path)
_ (run-cli ["--repo" "move-graph" "add" "block" "--target-page-name" "SourcePage" "--content" "Parent Block"] data-dir cfg-path)
parent-id (query-block-id data-dir cfg-path "move-graph" "Parent Block")
parent-uuid (query-block-uuid-by-id data-dir cfg-path "move-graph" parent-id)
_ (run-cli ["--repo" "move-graph" "add" "block" "--target-uuid" (str parent-uuid) "--content" "Child Block"] data-dir cfg-path)
move-result (run-cli ["--repo" "move-graph" "move" "--uuid" (str parent-uuid) "--target-page-name" "TargetPage"] data-dir cfg-path)
move-payload (parse-json-output move-result)
target-show (run-cli ["--repo" "move-graph" "show" "--page-name" "TargetPage" "--format" "json"] data-dir cfg-path)
_ (p/delay 100)
source-show (run-cli ["--repo" "move-graph" "show" "--page" "SourcePage"] data-dir cfg-path)
source-payload (parse-json-output source-show)
parent-node (find-block-by-title (get-in source-payload [:data :root]) "Parent Block")
parent-id (node-id parent-node)
_ (run-cli ["--repo" "move-graph" "add" "block" "--target-id" (str parent-id) "--content" "Child Block"] data-dir cfg-path)
update-result (run-cli ["--repo" "move-graph" "update" "--id" (str parent-id) "--target-page" "TargetPage"] data-dir cfg-path)
update-payload (parse-json-output update-result)
target-show (run-cli ["--repo" "move-graph" "show" "--page" "TargetPage" ] data-dir cfg-path)
target-payload (parse-json-output target-show)
moved-node (find-block-by-title (get-in target-payload [:data :root]) "Parent Block")
child-node (find-block-by-title moved-node "Child Block")
stop-result (run-cli ["server" "stop" "--repo" "move-graph"] data-dir cfg-path)
stop-payload (parse-json-output stop-result)]
(is (= "ok" (:status move-payload)))
(is (some? parent-uuid))
(is (= "ok" (:status update-payload)))
(is (some? parent-id))
(is (some? moved-node))
(is (some? child-node))
(is (= "ok" (:status stop-payload)))
@@ -908,17 +974,20 @@
_ (run-cli ["graph" "create" "--repo" "add-pos-graph"] data-dir cfg-path)
_ (run-cli ["--repo" "add-pos-graph" "add" "page" "--page" "PosPage"] data-dir cfg-path)
_ (run-cli ["--repo" "add-pos-graph" "add" "block" "--target-page-name" "PosPage" "--content" "Parent"] data-dir cfg-path)
parent-id (query-block-id data-dir cfg-path "add-pos-graph" "Parent")
parent-uuid (query-block-uuid-by-id data-dir cfg-path "add-pos-graph" parent-id)
_ (run-cli ["--repo" "add-pos-graph" "add" "block" "--target-uuid" (str parent-uuid) "--pos" "first-child" "--content" "First"] data-dir cfg-path)
_ (run-cli ["--repo" "add-pos-graph" "add" "block" "--target-uuid" (str parent-uuid) "--pos" "last-child" "--content" "Last"] data-dir cfg-path)
final-show (run-cli ["--repo" "add-pos-graph" "show" "--page-name" "PosPage" "--format" "json"] data-dir cfg-path)
_ (p/delay 100)
parent-show (run-cli ["--repo" "add-pos-graph" "show" "--page" "PosPage"] data-dir cfg-path)
parent-payload (parse-json-output parent-show)
parent-node (find-block-by-title (get-in parent-payload [:data :root]) "Parent")
parent-id (node-id parent-node)
_ (run-cli ["--repo" "add-pos-graph" "add" "block" "--target-id" (str parent-id) "--pos" "first-child" "--content" "First"] data-dir cfg-path)
_ (run-cli ["--repo" "add-pos-graph" "add" "block" "--target-id" (str parent-id) "--pos" "last-child" "--content" "Last"] data-dir cfg-path)
final-show (run-cli ["--repo" "add-pos-graph" "show" "--page" "PosPage" ] data-dir cfg-path)
final-payload (parse-json-output final-show)
final-parent (find-block-by-title (get-in final-payload [:data :root]) "Parent")
child-titles (map node-title (node-children final-parent))
stop-result (run-cli ["server" "stop" "--repo" "add-pos-graph"] data-dir cfg-path)
stop-payload (parse-json-output stop-result)]
(is (some? parent-uuid))
(is (some? parent-id))
(is (= ["First" "Last"] (vec child-titles)))
(is (= "ok" (:status stop-payload)))
(done))
@@ -1005,9 +1074,9 @@
(get-in list-page-payload [:data :items]))
page-id (or (:db/id page-item) (:id page-item))
page-uuid (or (:block/uuid page-item) (:uuid page-item))
show-by-id-result (run-cli ["show" "--id" (str page-id) "--format" "json"] data-dir cfg-path)
show-by-id-result (run-cli ["show" "--id" (str page-id) ] data-dir cfg-path)
show-by-id-payload (parse-json-output show-by-id-result)
show-by-uuid-result (run-cli ["show" "--uuid" (str page-uuid) "--format" "json"] data-dir cfg-path)
show-by-uuid-result (run-cli ["show" "--uuid" (str page-uuid) ] data-dir cfg-path)
show-by-uuid-payload (parse-json-output show-by-uuid-result)
stop-result (run-cli ["server" "stop" "--repo" "show-page-block-graph"] data-dir cfg-path)
stop-payload (parse-json-output stop-result)]
@@ -1016,10 +1085,12 @@
(is (some? page-id))
(is (some? page-uuid))
(is (= "ok" (:status show-by-id-payload)))
(is (= page-id (get-in show-by-id-payload [:data :root :db/id])))
(is (= page-id (or (get-in show-by-id-payload [:data :root :db/id])
(get-in show-by-id-payload [:data :root :id]))))
(is (not (contains? (get-in show-by-id-payload [:data :root]) :block/uuid)))
(is (= "ok" (:status show-by-uuid-payload)))
(is (= page-id (get-in show-by-uuid-payload [:data :root :db/id])))
(is (= page-id (or (get-in show-by-uuid-payload [:data :root :db/id])
(get-in show-by-uuid-payload [:data :root :id]))))
(is (not (contains? (get-in show-by-uuid-payload [:data :root]) :block/uuid)))
(is (= "ok" (:status stop-payload)))
(done))
@@ -1060,7 +1131,7 @@
ids-edn (str "[" block-one-id " " block-two-id "]")
show-text-result (run-cli ["--repo" "show-multi-id-graph" "show"
"--id" ids-edn
"--format" "text"
"--output" "human"]
data-dir cfg-path)
output (:output show-text-result)
@@ -1069,7 +1140,7 @@
idx-delim (string/index-of output "================================================================")
show-json-result (run-cli ["--repo" "show-multi-id-graph" "show"
"--id" ids-edn
"--format" "json"]
]
data-dir cfg-path)
show-json-payload (parse-json-output show-json-result)
show-data (:data show-json-payload)
@@ -1118,16 +1189,15 @@
data-dir cfg-path)
parent-payload (parse-json-output parent-query)
parent-id (get-in parent-payload [:data :result])
parent-uuid (query-block-uuid-by-id data-dir cfg-path "show-multi-id-contained-graph" parent-id)
_ (run-cli ["--repo" "show-multi-id-contained-graph" "add" "block"
"--target-uuid" (str parent-uuid)
"--target-id" (str parent-id)
"--content" "Child Block"]
data-dir cfg-path)
_ (p/delay 100)
show-children (run-cli ["--repo" "show-multi-id-contained-graph"
"show"
"--page-name" "ParentPage"
"--format" "json"]
"--page" "ParentPage"
]
data-dir cfg-path)
show-children-payload (parse-json-output show-children)
child-node (find-block-by-title (get-in show-children-payload [:data :root]) "Child Block")
@@ -1135,7 +1205,7 @@
ids-edn (str "[" parent-id " " child-id "]")
show-json-result (run-cli ["--repo" "show-multi-id-contained-graph" "show"
"--id" ids-edn
"--format" "json"]
]
data-dir cfg-path)
show-json-payload (parse-json-output show-json-result)
show-data (:data show-json-payload)
@@ -1145,7 +1215,6 @@
stop-payload (parse-json-output stop-result)]
(is (= 0 (:exit-code parent-query)))
(is (some? parent-id))
(is (some? parent-uuid))
(is (some? child-id))
(is (= 0 (:exit-code show-json-result)))
(is (= "ok" (:status show-json-payload)))
@@ -1313,7 +1382,7 @@
blocks-edn (str "[{:block/title \"Ref to TargetPage\" :block/refs [{:db/id " page-id "}]}]")
_ (run-cli ["--repo" "linked-refs-graph" "add" "block" "--target-page-name" "SourcePage"
"--blocks" blocks-edn] data-dir cfg-path)
show-result (run-cli ["--repo" "linked-refs-graph" "show" "--page-name" "TargetPage" "--format" "json"]
show-result (run-cli ["--repo" "linked-refs-graph" "show" "--page" "TargetPage" ]
data-dir cfg-path)
show-payload (parse-json-output show-result)
linked (get-in show-payload [:data :linked-references])
@@ -1326,7 +1395,7 @@
(is (pos? (:count linked)))
(is (seq (:blocks linked)))
(is (some? ref-block))
(is (some? (:db/id ref-block)))
(is (some? (or (:db/id ref-block) (:id ref-block))))
(is (some? (or (get-in ref-block [:page :title])
(get-in ref-block [:page :name]))))
(is (= "ok" (:status stop-payload)))