diff --git a/docs/agent-guide/027-logseq-cli-update-command.md b/docs/agent-guide/027-logseq-cli-update-command.md new file mode 100644 index 0000000000..1911937726 --- /dev/null +++ b/docs/agent-guide/027-logseq-cli-update-command.md @@ -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`. diff --git a/src/main/logseq/cli/command/add.cljs b/src/main/logseq/cli/command/add.cljs index 63985c5879..6e89244c2f 100644 --- a/src/main/logseq/cli/command/add.cljs +++ b/src/main/logseq/cli/command/add.cljs @@ -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 diff --git a/src/main/logseq/cli/command/core.cljs b/src/main/logseq/cli/command/core.cljs index 6f050506c2..0ec5a91039 100644 --- a/src/main/logseq/cli/command/core.cljs +++ b/src/main/logseq/cli/command/core.cljs @@ -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]}] diff --git a/src/main/logseq/cli/command/move.cljs b/src/main/logseq/cli/command/update.cljs similarity index 51% rename from src/main/logseq/cli/command/move.cljs rename to src/main/logseq/cli/command/update.cljs index b0fcfc43f1..4f6fa24d0c 100644 --- a/src/main/logseq/cli/command/move.cljs +++ b/src/main/logseq/cli/command/update.cljs @@ -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}}))) diff --git a/src/main/logseq/cli/commands.cljs b/src/main/logseq/cli/commands.cljs index ac38c6063e..8191e2cf52 100644 --- a/src/main/logseq/cli/commands.cljs +++ b/src/main/logseq/cli/commands.cljs @@ -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]))))) diff --git a/src/main/logseq/cli/format.cljs b/src/main/logseq/cli/format.cljs index 1e6ac00f7c..c03b804204 100644 --- a/src/main/logseq/cli/format.cljs +++ b/src/main/logseq/cli/format.cljs @@ -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)) diff --git a/src/test/logseq/cli/commands_test.cljs b/src/test/logseq/cli/commands_test.cljs index b9c0e4f709..53a44ac0e3 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -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 [_] []) diff --git a/src/test/logseq/cli/format_test.cljs b/src/test/logseq/cli/format_test.cljs index 819047886e..1f2634b9d7 100644 --- a/src/test/logseq/cli/format_test.cljs +++ b/src/test/logseq/cli/format_test.cljs @@ -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" diff --git a/src/test/logseq/cli/integration_test.cljs b/src/test/logseq/cli/integration_test.cljs index af9adc724b..6131d0e96d 100644 --- a/src/test/logseq/cli/integration_test.cljs +++ b/src/test/logseq/cli/integration_test.cljs @@ -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)))