From 67645700cccccfc07e67c02853c80dec06508b31 Mon Sep 17 00:00:00 2001 From: rcmerci Date: Mon, 2 Mar 2026 18:59:48 +0800 Subject: [PATCH] 044-logseq-cli-upsert-block-page.md --- .../044-logseq-cli-upsert-block-page.md | 191 ++++++ docs/cli/logseq-cli.md | 23 +- src/main/logseq/cli/command/add.cljs | 547 +++++++++--------- src/main/logseq/cli/command/core.cljs | 2 +- src/main/logseq/cli/command/update.cljs | 34 +- src/main/logseq/cli/command/upsert.cljs | 283 ++++++++- src/main/logseq/cli/commands.cljs | 45 +- src/main/logseq/cli/format.cljs | 44 +- src/test/logseq/cli/commands_test.cljs | 414 ++++++++++--- src/test/logseq/cli/format_test.cljs | 24 +- src/test/logseq/cli/integration_test.cljs | 360 ++++++++---- 11 files changed, 1385 insertions(+), 582 deletions(-) create mode 100644 docs/agent-guide/044-logseq-cli-upsert-block-page.md diff --git a/docs/agent-guide/044-logseq-cli-upsert-block-page.md b/docs/agent-guide/044-logseq-cli-upsert-block-page.md new file mode 100644 index 0000000000..43bf316684 --- /dev/null +++ b/docs/agent-guide/044-logseq-cli-upsert-block-page.md @@ -0,0 +1,191 @@ +# Logseq CLI Upsert Block and Upsert Page Implementation Plan + +Goal: Consolidate block and page write commands by replacing `add block`, `add page`, and `update` with `upsert block` and `upsert page` while preserving current db-worker-node write behavior. + +Architecture: Keep db-worker-node RPC and outliner operation contracts unchanged, and implement command consolidation in CLI parsing, action building, execution, and formatting layers. +Architecture: Reuse existing helper logic from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/update.cljs` first, then fold shared behavior into `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs`. +Architecture: Route all mutations through existing `:thread-api/apply-outliner-ops` and `:thread-api/pull` calls so `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_worker_node.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs` require no new thread APIs. + +Tech Stack: ClojureScript, babashka.cli, Promesa, Logseq CLI transport, db-worker-node, and outliner ops. + +Related: Relates to `docs/agent-guide/027-logseq-cli-update-command.md` and builds on `docs/agent-guide/041-logseq-cli-add-block-json-identifiers.md`. +Document naming follows @planning-documents with sequence `044`. + +## Problem statement + +The current CLI splits block mutations across `add block` and `update`, while page writes are exposed as `add page`. +This creates an inconsistent user model and duplicates validation and formatting paths in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`. +The current block update property options only support built-in properties, which prevents consistent upsert behavior for custom properties. +The current `add page` flow applies tags after create, but page property behavior for existing pages is not fully upsert-like because `create-page` may no-op when the page exists. +The db-worker-node layer already exposes stable generic APIs, so this feature should be implemented as a CLI surface refactor without protocol changes. + +## Testing Plan + +I will use @test-driven-development for all implementation batches. +I will write all RED tests for parser, action builder, formatter, and integration flows before changing implementation behavior. +I will add parser and builder tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` for `upsert block` and `upsert page` command forms and for hard-removal behavior of `add` and `update`. +I will add formatter tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs` for human output of `:upsert-block` and `:upsert-page`. +I will add integration tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs` that verify block creation, block update, block move, page creation, and page update through only `upsert` commands. +I will add one integration test that verifies `upsert page` updates properties on an existing page, which closes the current `add page` gap. +I will verify RED failures come from missing behavior and not from broken test setup. +I will run focused GREEN tests after minimal implementation, then refactor, then rerun focused tests and full lint-test. + +NOTE: I will write *all* tests before I add any implementation behavior. + +## Current implementation baseline + +| Area | Current implementation | Change target | +| --- | --- | --- | +| Command entries | `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` defines `["add" "block"]` and `["add" "page"]`, and `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/update.cljs` defines `["update"]`. | `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs` defines `["upsert" "block"]` and `["upsert" "page"]` together with existing upsert subcommands. | +| Validation and dispatch | `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` validates and dispatches `:add-block`, `:add-page`, and `:update-block` separately. | Replace with `:upsert-block` and `:upsert-page` parse and dispatch paths. | +| Help and group summaries | `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs` top-level summary and group handling still expose `add` and `update`. | Expose only `upsert` for these write cases. | +| Formatter | `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` has `format-add-block`, `format-add-page`, and `format-update-block`. | Replace with upsert-focused formatter routes while preserving existing output contract style. | +| Worker APIs | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_worker_node.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs` already provide `:thread-api/apply-outliner-ops`. | No new worker endpoint or transport shape. | +| Page create behavior | `/Users/rcmerci/gh-repos/logseq/deps/outliner/src/logseq/outliner/page.cljs` `create!` can return existing page with no transaction. | `upsert page` applies properties and tags explicitly on existing page to ensure real upsert behavior. | + +## Interface contract proposal + +`upsert block` supports two modes with deterministic priority. +If `--id` or `--uuid` is provided, `upsert block` always runs update mode. +If neither `--id` nor `--uuid` is provided, `upsert block` runs create mode. +For `upsert block` update mode, add, update, and remove property options must support all existing properties, not only built-in properties. +`upsert page` requires `--page` and always resolves an existing or newly created page, then applies add, update, and remove semantics for tags and properties. +For `upsert page`, all add, update, and remove tag or property operations require the target tag and property to already exist, otherwise the command returns an error. + +| Command | Input signal | Behavior | Existing code path to reuse | +| --- | --- | --- | --- | +| `upsert block` create mode | `--id` and `--uuid` are absent and a content source is present. | Insert blocks under target with existing add semantics and support add, update, and remove tag or property options in post-insert ops. | `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` add helpers plus update option helpers. | +| `upsert block` update mode | `--id` or `--uuid` is present. | Move and or add, update, and remove tags or properties with existing update semantics, and support both built-in and custom properties in property options. | `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/update.cljs` update helpers. | +| `upsert page` | `--page` is present. | Create page if missing, then apply add, update, and remove tags and properties for both new and existing pages, with strict existing-tag and existing-property validation. | `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` plus explicit remove op wiring. | + +## Architecture sketch + +```text +CLI args + -> parse-args in commands.cljs + -> build-action returns :upsert-block or :upsert-page + -> execute routes to upsert command executor + -> transport/invoke :thread-api/apply-outliner-ops and :thread-api/pull + -> db-worker-node /v1/invoke passthrough + -> db_core thread-api handlers and outliner-op/apply-ops! +``` + +## Plan + +1. Add RED parser tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` for `upsert block` create mode with `--content` and for update mode with `--id` plus `--update-tags`. +2. Add RED parser tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` that confirm `--id` or `--uuid` forces update mode even when create inputs are also present. +3. Add RED parser tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` for `upsert page --page ` with tags and properties. +4. Add RED parser tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` to define expected unknown-command behavior for legacy `add block`, `add page`, and `update`. +5. Add RED builder tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` for `:upsert-block` action shape in create mode. +6. Add RED builder tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` for `:upsert-block` action shape in update mode, including custom property option inputs. +7. Add RED builder tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` for `:upsert-page` action shape including resolved options. +8. Add RED execute tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` to verify `:upsert-block` delegates to insert-style ops for create mode. +9. Add RED execute tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` to verify `:upsert-block` delegates to move and property ops for update mode across built-in and custom properties. +10. Add RED execute tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` to verify `:upsert-page` applies add, update, and remove property or tag ops on an already existing page. +11. Add RED execute tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` to verify `:upsert-page` returns errors when any referenced tag or property does not exist. +12. Add RED formatter tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs` for human output text of `:upsert-block` and `:upsert-page`. +13. Add RED integration tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs` for `upsert block` create mode id outputs. +14. Add RED integration tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs` for `upsert block` update mode move behavior and custom property updates. +15. Add RED integration tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs` for `upsert page` create and update-existing behaviors. +16. Add RED integration tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs` for `upsert page` erroring when referenced tags or properties do not exist. +17. Run focused RED commands and confirm failures are expectation failures rather than transport or fixture errors. +18. Extend `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs` spec to include block and page options while keeping existing tag and property specs. +19. Implement `build-block-action` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs` to classify create mode versus update mode with `--id` or `--uuid` priority, and normalize property options for all property identifiers. +20. Implement `build-page-action` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs` for `upsert page`. +21. Extract or reuse add helper functions from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` for reading blocks, parsing tags, parsing properties, and resolving ids. +22. Extract or reuse update helper functions from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/update.cljs` for source and target resolution and move option mapping. +23. Implement `execute-upsert-block` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs` that branches by mode and calls reused logic without behavior drift, including custom property update support. +24. Implement `execute-upsert-page` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs` so add, update, and remove property or tag ops are applied after resolving the page entity in both create and existing-page paths. +25. Enforce strict `upsert page` validation and execution behavior where missing tags or properties fail fast instead of creating missing entities. +26. Update `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` table entries and finalize-command validation for `:upsert-block` and `:upsert-page`. +27. Update `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` build and execute case dispatch to remove `:add-block`, `:add-page`, and `:update-block` routing. +28. Update `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs` top-level summary and group-help triggers to reflect the new command family. +29. Update `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` with `format-upsert-block` and `format-upsert-page` and command dispatch keys. +30. Keep `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_worker_node.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs` unchanged unless test evidence proves a missing worker behavior. +31. Update CLI documentation in `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md` to replace add and update examples with upsert equivalents. +32. Run focused GREEN tests and confirm parser, builder, formatter, and integration cases pass. +33. Refactor duplicated helper wiring between add, update, and upsert modules while preserving test behavior. +34. Rerun focused test set after refactor to confirm no regressions. +35. Run `bb dev:lint-and-test` as final regression verification. + +## Edge cases + +`upsert block` with `--id` or `--uuid` plus create inputs must deterministically run update mode because source selectors have priority. +`upsert block` update mode must keep current `--pos` validation where `sibling` is invalid for page targets and `--pos` requires a target. +`upsert block` update mode property options must support all existing properties, including non built-in properties. +`upsert block` create mode must preserve current default target fallback to today journal when no target selector is provided. +`upsert block` create mode with `--blocks` or `--blocks-file` must keep the existing restriction that tags and properties cannot be combined if that restriction is still required by current insert behavior. +`upsert block` and `upsert page` must support remove options for tags and properties in addition to add and update options. +`upsert page` must return stable `data.result` id vectors for JSON, EDN, and human output just like current add command id outputs. +`upsert page` on an existing page must apply property updates and removals explicitly so upsert semantics are true for both create and existing states. +`upsert page` must error when any tag or property referenced by add, update, or remove options does not already exist. +Legacy command behavior must be hard removal with standard `unknown-command` errors for `add block`, `add page`, and `update`. +Help output must not regress command grouping or ANSI formatting alignment in `commands_test`. + +## Verification commands and expected output + +Run parser and builder tests during RED. + +```bash +bb dev:test -v logseq.cli.commands-test +``` + +Expected RED behavior is failing assertions for missing `upsert block` and `upsert page` paths before implementation. + +Run formatter tests during RED. + +```bash +bb dev:test -v logseq.cli.format-test +``` + +Expected RED behavior is failing assertions for unknown command formatter branches for upsert block and upsert page. + +Run focused integration tests after implementation. + +```bash +bb dev:test -v logseq.cli.integration-test/test-cli-upsert-block-create-json-output-returns-ids +bb dev:test -v logseq.cli.integration-test/test-cli-upsert-block-update-move +bb dev:test -v logseq.cli.integration-test/test-cli-upsert-page-create-and-update-existing +``` + +Expected GREEN behavior is zero failures and zero errors for these tests. + +Run full verification. + +```bash +bb dev:lint-and-test +``` + +Expected GREEN behavior is full suite pass with exit code `0`. + +## Testing Details + +Behavior tests will assert command-level outcomes through real CLI execution and Datascript queries instead of only checking mock invocation counts. +Unit-level command tests will assert parse, validation, and action-shape behavior at module boundaries. +Integration tests will verify persisted graph state changes for both create and update paths of upsert block and upsert page. + +## Implementation Details + +- Keep db-worker-node API contracts unchanged and implement all command-surface changes in CLI modules only. +- Add `upsert block` and `upsert page` entries to `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs`. +- Reuse add and update helper functions to minimize behavior drift and reduce migration risk. +- Ensure `upsert block` and `upsert page` support add, update, and remove options for tags and properties. +- Ensure `upsert block` property update options accept all existing properties, including custom properties, not only built-in properties. +- Ensure `upsert page` applies tags and properties after resolving page entity so existing pages are updated too. +- Ensure `upsert page` fails fast when referenced tags or properties do not exist, and never auto-creates them through upsert-page mutation options. +- Remove old command routes in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` for `add block`, `add page`, and `update`, returning standard `unknown-command`. +- Update formatter dispatch in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` for new command ids. +- Update command summaries in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs` to keep help output accurate. +- Update `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md` examples and command reference text. +- Keep implementation batches aligned to @test-driven-development RED, GREEN, and refactor phases. + +## Question + +No open questions. +Decided: remove `add block`, `add page`, and `update` immediately. +Decided: in `upsert block`, `--id` or `--uuid` means update mode and absence of both means create mode. +Decided: support add, update, and remove semantics for tags and properties. +Decided: in `upsert block` update mode, property mutation options support all existing properties, including custom properties. +Decided: for `upsert page`, add, update, and remove tag or property options require existing tags and properties, otherwise return error. + +--- diff --git a/docs/cli/logseq-cli.md b/docs/cli/logseq-cli.md index 41cfe0ba11..17dd89fa96 100644 --- a/docs/cli/logseq-cli.md +++ b/docs/cli/logseq-cli.md @@ -94,10 +94,11 @@ Inspect and edit commands: - `list page [--expand] [--limit ] [--offset ] [--sort ] [--order asc|desc]` - list pages - `list tag [--expand] [--limit ] [--offset ] [--sort ] [--order asc|desc]` - list tags - `list property [--expand] [--limit ] [--offset ] [--sort ] [--order asc|desc]` - list properties -- `add block --content [--target-page-name |--target-id |--target-uuid ] [--pos first-child|last-child|sibling]` - add blocks; defaults to today’s journal page if no target is given -- `add block --blocks [--target-page-name |--target-id |--target-uuid ] [--pos first-child|last-child|sibling]` - insert blocks via EDN vector -- `add block --blocks-file [--target-page-name |--target-id |--target-uuid ] [--pos first-child|last-child|sibling]` - insert blocks from an EDN file -- `add page --page ` - create a page +- `upsert block --content [--target-page |--target-id |--target-uuid ] [--pos first-child|last-child|sibling]` - create blocks; defaults to today’s journal page if no target is given +- `upsert block --blocks [--target-page |--target-id |--target-uuid ] [--pos first-child|last-child|sibling]` - insert blocks via EDN vector +- `upsert block --blocks-file [--target-page |--target-id |--target-uuid ] [--pos first-child|last-child|sibling]` - insert blocks from an EDN file +- `upsert block --id |--uuid [--target-id |--target-uuid |--target-page ] [--pos first-child|last-child|sibling] [--update-tags ] [--update-properties ] [--remove-tags ] [--remove-properties ]` - update and/or move a block +- `upsert page --page [--tags ] [--properties ] [--update-tags ] [--update-properties ] [--remove-tags ] [--remove-properties ]` - create or update a page - `move --id |--uuid --target-id |--target-uuid |--target-page [--pos first-child|last-child|sibling]` - move a block and its children (defaults to first-child) - `remove --id |--uuid |--page ` - remove blocks (by db/id or UUID) or pages - `search [--type page|block|tag|property|all] [--tag ] [--case-sensitive] [--sort updated-at|created-at] [--order asc|desc]` - search across pages, blocks, tags, and properties (query is positional) @@ -115,8 +116,10 @@ Subcommands: list page [options] List pages list tag [options] List tags list property [options] List properties - add block [options] Add blocks - add page [options] Create page + upsert block [options] Upsert block + upsert page [options] Upsert page + upsert tag [options] Upsert tag + upsert property [options] Upsert property move [options] Move block remove [options] Remove block or page search [options] Search graph @@ -138,15 +141,15 @@ Output formats: - Global `--output ` applies to all commands - For `graph export`, `--output` refers to the destination file path. Output formatting is controlled via `:output-format` in config or `LOGSEQ_CLI_OUTPUT`. - Human output is plain text. List/search commands render tables with a final `Count: N` line. For list and search subcommands, the ID column uses `:db/id` (not UUID). If `:db/ident` exists, an `IDENT` column is included. Search table columns are `ID` and `TITLE`. Block titles can include multiple lines; multi-line rows align additional lines under the `TITLE` column. Times such as list `UPDATED-AT`/`CREATED-AT` and `graph info` `Created at` are shown in human-friendly relative form. Errors include error codes and may include a `Hint:` line. Use `--output json|edn` for structured output. -- `add page` and `add block` return created entity ids in `data.result` for JSON/EDN output, and include ids in human output. +- `upsert page` and `upsert block` return entity ids in `data.result` for JSON/EDN output, and include ids in human output. - Human example: ```text - Added page: + Upserted page: [123] ``` - Human example: ```text - Added blocks: + Upserted blocks: [201 202] ``` - JSON example: `{"status":"ok","data":{"result":[123]}}` @@ -180,7 +183,7 @@ Examples: node ./dist/logseq.js graph create --repo demo node ./dist/logseq.js graph export --type edn --output /tmp/demo.edn --repo demo node ./dist/logseq.js graph import --type edn --input /tmp/demo.edn --repo demo-import -node ./dist/logseq.js add block --target-page-name TestPage --content "hello world" +node ./dist/logseq.js upsert block --target-page TestPage --content "hello world" node ./dist/logseq.js move --uuid --target-page TargetPage node ./dist/logseq.js search "hello" node ./dist/logseq.js show --page TestPage --output json diff --git a/src/main/logseq/cli/command/add.cljs b/src/main/logseq/cli/command/add.cljs index 90c858923b..1bc62a057b 100644 --- a/src/main/logseq/cli/command/add.cljs +++ b/src/main/logseq/cli/command/add.cljs @@ -16,28 +16,6 @@ [logseq.db.frontend.property.type :as db-property-type] [promesa.core :as p])) -(def ^:private content-add-spec - {:content {:desc "Block content for add"} - :blocks {:desc "EDN vector of blocks for add"} - :blocks-file {:desc "EDN file of blocks for add"} - :tags {:desc "EDN vector of tags. Identifiers can be id, :db/ident, or :block/title."} - :properties {:desc "EDN map of built-in properties. Identifiers can be id, :db/ident, or :block/title."} - :target-id {:desc "Target block db/id" - :coerce :long} - :target-uuid {:desc "Target block UUID"} - :target-page-name {:desc "Target page name"} - :pos {:desc "Position (first-child, last-child, sibling). Default: last-child"} - :status {:desc "Task status (todo, doing, done, etc.)"}}) - -(def ^:private add-page-spec - {:page {:desc "Page name"} - :tags {:desc "EDN vector of tags. Identifiers can be id, :db/ident, or :block/title."} - :properties {:desc "EDN map of built-in properties. Identifiers can be id, :db/ident, or :block/title."}}) - -(def entries - [(core/command-entry ["add" "block"] :add-block "Add blocks" content-add-spec) - (core/command-entry ["add" "page"] :add-page "Create page" add-page-spec)]) - (defn- today-page-title [config repo] (p/let [journal (transport/invoke config :thread-api/pull false @@ -194,24 +172,6 @@ ordered-uuids))] (created-ids-in-order ordered-uuids entities :block))))) -(defn- resolve-created-page-ids - [config repo page create-result] - (let [page-uuid (some-> create-result second normalized-uuid)] - (if page-uuid - (p/let [page-entity (transport/invoke config :thread-api/pull false - [repo [:db/id :block/uuid] [:block/uuid page-uuid]])] - (created-ids-in-order [page-uuid] [page-entity] :page)) - (p/let [page-entity (transport/invoke config :thread-api/pull false - [repo [:db/id :block/uuid] - [:block/name (common-util/page-name-sanity-lc page)]]) - page-id (:db/id page-entity)] - (if (some? page-id) - [page-id] - (throw (ex-info "unable to resolve created page id" - {:code :add-id-resolution-failed - :entity-kind :page - :page page}))))))) - (defn- extract-page-refs [title] (when (string? title) @@ -531,85 +491,95 @@ (true? (get-in property [:schema :public?]))) (defn parse-properties-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 map") + ([value] + (parse-properties-option value {:allow-non-built-in? false})) + ([value {:keys [allow-non-built-in?] + :or {allow-non-built-in? false}}] + (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 map") - (not (map? parsed)) - (invalid-options-result "properties must be a map") + (not (map? parsed)) + (invalid-options-result "properties must be a map") - (empty? parsed) - (invalid-options-result "properties must be a non-empty map") + (empty? parsed) + (invalid-options-result "properties must be a non-empty map") - :else - (loop [prop-entries (seq parsed) - acc {}] - (if (empty? prop-entries) - {:ok? true :value acc} - (let [[k v] (first prop-entries) - key-result (normalize-property-key-input k)] - (if-not key-result - (invalid-options-result (str "invalid property key: " k)) - (let [{:keys [type value]} key-result - key-ident value] - (if (= type :id) - (recur (rest prop-entries) (assoc acc key-ident v)) - (let [property (get db-property/built-in-properties key-ident)] - (cond - (nil? property) - (invalid-options-result (str "unknown built-in property: " key-ident)) + :else + (loop [prop-entries (seq parsed) + acc {}] + (if (empty? prop-entries) + {:ok? true :value acc} + (let [[k v] (first prop-entries) + key-result (normalize-property-key-input k)] + (if-not key-result + (invalid-options-result (str "invalid property key: " k)) + (let [{:keys [type value]} key-result + key-ident value] + (if (= type :id) + (recur (rest prop-entries) (assoc acc key-ident v)) + (let [property (get db-property/built-in-properties key-ident)] + (cond + (nil? property) + (if allow-non-built-in? + (recur (rest prop-entries) (assoc acc key-ident v)) + (invalid-options-result (str "unknown built-in property: " key-ident))) - (not (property-public? property)) - (invalid-options-result (str "property is not public: " key-ident)) + (not (property-public? property)) + (invalid-options-result (str "property is not public: " key-ident)) - :else - (let [{:keys [ok? value message]} (normalize-property-values property v) - normalized-value value] - (if-not ok? - (invalid-options-result (str "invalid value for " key-ident ": " message)) - (recur (rest prop-entries) (assoc acc key-ident normalized-value)))))))))))))))) + :else + (let [{:keys [ok? value message]} (normalize-property-values property v) + normalized-value value] + (if-not ok? + (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") + ([value] + (parse-properties-vector-option value {:allow-non-built-in? false})) + ([value {:keys [allow-non-built-in?] + :or {allow-non-built-in? false}}] + (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") + (not (vector? parsed)) + (invalid-options-result "properties must be a vector") - (empty? parsed) - (invalid-options-result "properties must be a non-empty 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)) + :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) + (if allow-non-built-in? + (recur (rest prop-entries) (conj acc value)) + (invalid-options-result (str "unknown built-in property: " value))) - (not (property-public? property)) - (invalid-options-result (str "property is not public: " value)) + (not (property-public? property)) + (invalid-options-result (str "property is not public: " value)) - :else - (recur (rest prop-entries) (conj acc value)))))))))))))) + :else + (recur (rest prop-entries) (conj acc value))))))))))))))) (defn invalid-options? [opts] @@ -755,129 +725,219 @@ :date (resolve-date-page-id config repo value) (p/resolved value)))) +(def ^:private property-entity-selector + [:db/id :db/ident :block/name :block/title + :logseq.property/type :db/cardinality :logseq.property/public?]) + +(defn- property-entity? + [entity] + (some? (:logseq.property/type entity))) + +(defn- property-entity-public? + [entity] + (not (false? (:logseq.property/public? entity)))) + +(defn- property-entity->property + [entity] + {:schema {:type (or (:logseq.property/type entity) :default) + :cardinality (if (= :db.cardinality/many (:db/cardinality entity)) + :many + :one) + :public? (property-entity-public? entity)}}) + +(defn- lookup-property-entity + [config repo property-key] + (let [lookup-by-title (fn [title] + (pull-entity config repo property-entity-selector + [:block/name (common-util/page-name-sanity-lc title)]))] + (cond + (number? property-key) + (pull-entity config repo property-entity-selector property-key) + + (keyword? property-key) + (p/let [entity (pull-entity config repo property-entity-selector [:db/ident property-key])] + (if (or (:db/id entity) (qualified-keyword? property-key)) + entity + (lookup-by-title (name property-key)))) + + (string? property-key) + (let [text (string/trim property-key) + ident (normalize-property-key text)] + (if-not (seq text) + (p/resolved nil) + (p/let [entity (lookup-by-title text)] + (if (:db/id entity) + entity + (if ident + (pull-entity config repo property-entity-selector [:db/ident ident]) + (p/resolved nil)))))) + + :else + (p/resolved nil)))) + +(defn- resolve-property-entry-allow-non-built-in + [config repo property-key] + (p/let [entity (lookup-property-entity config repo property-key) + ident (:db/ident entity)] + (cond + (nil? (:db/id entity)) + (throw (ex-info "property not found" + {:code :property-not-found + :property property-key})) + + (not (property-entity? entity)) + (throw (ex-info "target is not a property" + {:code :invalid-property-target + :property property-key})) + + (nil? ident) + (throw (ex-info "property not found" + {:code :property-not-found + :property property-key})) + + (not (property-entity-public? entity)) + (throw (ex-info "property is not public" + {:code :property-not-public + :property ident})) + + :else + {:ident ident + :property (property-entity->property entity)}))) + (defn resolve-properties - [config repo properties] - (if-not (seq properties) - (p/resolved nil) - (p/let [resolved-entries (p/all - (map (fn [[k v]] - (p/let [{:keys [ident property]} - (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 {:ident k :property property})) + ([config repo properties] + (resolve-properties config repo properties {:allow-non-built-in? false})) + ([config repo properties {:keys [allow-non-built-in?] + :or {allow-non-built-in? false}}] + (if-not (seq properties) + (p/resolved nil) + (p/let [resolved-entries (p/all + (map (fn [[k v]] + (p/let [{:keys [ident property]} + (if allow-non-built-in? + (resolve-property-entry-allow-non-built-in config repo 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 {:ident k :property property})) - (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})) + (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})) + (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})) + (not (property-public? property)) + (throw (ex-info "property is not public" + {:code :property-not-public :property ident})) - :else - {:ident ident :property property})) + :else + {:ident ident :property property})) - (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 ident :property property})) + (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 ident :property property})) - :else - (p/rejected (ex-info "invalid property key" - {:code :invalid-property :property k}))) - {:keys [ok? value message]} (normalize-property-values property v)] - (when-not ok? - (throw (ex-info "invalid property value" - {:code :invalid-property-value - :property ident - :message message}))) - (let [many? (= :many (get-in property [:schema :cardinality])) - values (if many? - (if (and (coll? value) (not (string? value))) value [value]) - [value])] - (p/let [resolved (p/all (map #(resolve-property-value config repo property %) values)) - final-value (if many? (vec resolved) (first resolved))] - [ident final-value])))) - properties))] - (into {} resolved-entries)))) + :else + (p/rejected (ex-info "invalid property key" + {:code :invalid-property :property k})))) + {:keys [ok? value message]} (normalize-property-values property v)] + (when-not ok? + (throw (ex-info "invalid property value" + {:code :invalid-property-value + :property ident + :message message}))) + (let [many? (= :many (get-in property [:schema :cardinality])) + values (if many? + (if (and (coll? value) (not (string? value))) value [value]) + [value])] + (p/let [resolved (p/all (map #(resolve-property-value config repo property %) values)) + final-value (if many? (vec resolved) (first resolved))] + [ident final-value])))) + 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)) + ([config repo properties] + (resolve-property-identifiers config repo properties {:allow-non-built-in? false})) + ([config repo properties {:keys [allow-non-built-in?] + :or {allow-non-built-in? false}}] + (if-not (seq properties) + (p/resolved nil) + (p/let [resolved-entries (p/all + (map (fn [k] + (if allow-non-built-in? + (p/let [{:keys [ident]} (resolve-property-entry-allow-non-built-in config repo k)] + ident) + (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})) + (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})) + (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})) + (not (property-public? property)) + (throw (ex-info "property is not public" + {:code :property-not-public :property ident})) - :else - 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)) + (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)))) + :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]}] @@ -978,35 +1038,6 @@ :properties properties :blocks blocks}})))))))) -(defn build-add-page-action - [options repo] - (if-not (seq repo) - {:ok? false - :error {:code :missing-repo - :message "repo is required for add"}} - (let [page (some-> (:page options) string/trim)] - (if (seq page) - (let [tags-result (parse-tags-option (:tags options)) - properties-result (parse-properties-option (:properties options))] - (cond - (not (:ok? tags-result)) - tags-result - - (not (:ok? properties-result)) - properties-result - - :else - {:ok? true - :action {:type :add-page - :repo repo - :graph (core/repo->graph repo) - :page page - :tags (:value tags-result) - :properties (:value properties-result)}})) - {:ok? false - :error {:code :missing-page-name - :message "page name is required"}})))) - (defn execute-add-block [action config] (-> (p/let [cfg (cli-server/ensure-server! config (:repo action)) @@ -1065,31 +1096,3 @@ created-ids (resolve-created-block-ids cfg (:repo action) blocks-for-insert insert-result)] {:status :ok :data {:result created-ids}}))) - -(defn execute-add-page - [action config] - (-> (p/let [cfg (cli-server/ensure-server! config (:repo action)) - tags (resolve-tags cfg (:repo action) (:tags action)) - tag-ids (when (seq tags) - (->> tags (map :db/id) (remove nil?) vec)) - properties (resolve-properties cfg (:repo action) (:properties action)) - options (cond-> {} - (seq properties) (assoc :properties properties)) - ops [[:create-page [(:page action) options]]] - create-result (transport/invoke cfg :thread-api/apply-outliner-ops false [(:repo action) ops {}]) - _ (when (seq tag-ids) - (p/let [page-name (common-util/page-name-sanity-lc (:page action)) - page (pull-entity cfg (:repo action) [:db/id :block/uuid] [:block/name page-name]) - page-uuid (:block/uuid page)] - (when-not page-uuid - (throw (ex-info "page not found" {:code :page-not-found :page (:page action)}))) - (p/all - (map (fn [tag-id] - (transport/invoke cfg :thread-api/apply-outliner-ops false - [(:repo action) - [[:batch-set-property [[page-uuid] :block/tags tag-id {}]]] - {}])) - tag-ids)))) - created-ids (resolve-created-page-ids cfg (:repo action) (:page action) create-result)] - {:status :ok - :data {:result created-ids}}))) diff --git a/src/main/logseq/cli/command/core.cljs b/src/main/logseq/cli/command/core.cljs index cb9ac273d3..1a99ba6ddf 100644 --- a/src/main/logseq/cli/command/core.cljs +++ b/src/main/logseq/cli/command/core.cljs @@ -93,7 +93,7 @@ (defn top-level-summary [table] (let [groups [{:title "Graph Inspect and Edit" - :commands #{"list" "add" "upsert" "remove" "update" "query" "show"}} + :commands #{"list" "upsert" "remove" "query" "show"}} {:title "Graph Management" :commands #{"graph" "server" "doctor"}}] render-group (fn [{:keys [title commands]}] diff --git a/src/main/logseq/cli/command/update.cljs b/src/main/logseq/cli/command/update.cljs index 4f6fa24d0c..8e3f5e8fa4 100644 --- a/src/main/logseq/cli/command/update.cljs +++ b/src/main/logseq/cli/command/update.cljs @@ -8,23 +8,6 @@ [logseq.common.util :as common-util] [promesa.core :as p])) -(def ^:private update-spec - {:id {:desc "Source block db/id" - :coerce :long} - :uuid {:desc "Source block UUID"} - :target-id {:desc "Target block db/id" - :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"} - :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 ["update"] :update-block "Update block" update-spec)]) - (def ^:private update-positions #{"first-child" "last-child" "sibling"}) @@ -152,9 +135,13 @@ 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)) + update-properties-result (add-command/parse-properties-option + (:update-properties options) + {:allow-non-built-in? true}) remove-tags-result (add-command/parse-tags-vector-option (:remove-tags options)) - remove-properties-result (add-command/parse-properties-vector-option (:remove-properties options)) + remove-properties-result (add-command/parse-properties-vector-option + (:remove-properties options) + {:allow-non-built-in? true}) update-tags (:value update-tags-result) update-properties (:value update-properties-result) remove-tags (:value remove-tags-result) @@ -222,9 +209,12 @@ 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)) + update-properties (add-command/resolve-properties + cfg (:repo action) (:update-properties action) + {:allow-non-built-in? true}) + remove-properties (add-command/resolve-property-identifiers + cfg (:repo action) (:remove-properties action) + {:allow-non-built-in? true}) block-id (:db/id source) block-ids [block-id] update-tag-ids (when (seq update-tags) diff --git a/src/main/logseq/cli/command/upsert.cljs b/src/main/logseq/cli/command/upsert.cljs index 94b91d5a6b..211a56dcf3 100644 --- a/src/main/logseq/cli/command/upsert.cljs +++ b/src/main/logseq/cli/command/upsert.cljs @@ -1,12 +1,43 @@ (ns logseq.cli.command.upsert "Upsert-related CLI commands." (:require [clojure.string :as string] + [logseq.cli.command.add :as add-command] [logseq.cli.command.core :as core] + [logseq.cli.command.update :as update-command] [logseq.cli.server :as cli-server] [logseq.cli.transport :as transport] [logseq.common.util :as common-util] [promesa.core :as p])) +(def ^:private upsert-block-spec + {:id {:desc "Source block db/id (forces update mode)" + :coerce :long} + :uuid {:desc "Source block UUID (forces update mode)"} + :target-id {:desc "Target block db/id" + :coerce :long} + :target-uuid {:desc "Target block UUID"} + :target-page {:desc "Target page name"} + :pos {:desc "Position (first-child, last-child, sibling). Default: create=last-child, update=first-child"} + :content {:desc "Block content for create mode"} + :blocks {:desc "EDN vector of blocks for create mode"} + :blocks-file {:desc "EDN file of blocks for create mode"} + :status {:desc "Task status (todo, doing, done, etc.)"} + :tags {:desc "Tags to add in create mode (EDN vector). Identifiers can be id, :db/ident, or :block/title."} + :properties {:desc "Properties to add in create mode (EDN map). Identifiers can be id, :db/ident, or :block/title."} + :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 ^:private upsert-page-spec + {:page {:desc "Page name"} + :tags {:desc "Tags to add (EDN vector). Identifiers can be id, :db/ident, or :block/title."} + :properties {:desc "Properties to add (EDN map). Identifiers can be id, :db/ident, or :block/title."} + :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 ^:private upsert-tag-spec {:name {:desc "Tag name"}}) @@ -20,7 +51,9 @@ :coerce :boolean}}) (def entries - [(core/command-entry ["upsert" "tag"] :upsert-tag "Upsert tag" upsert-tag-spec) + [(core/command-entry ["upsert" "block"] :upsert-block "Upsert block" upsert-block-spec) + (core/command-entry ["upsert" "page"] :upsert-page "Upsert page" upsert-page-spec) + (core/command-entry ["upsert" "tag"] :upsert-tag "Upsert tag" upsert-tag-spec) (core/command-entry ["upsert" "property"] :upsert-property "Upsert property" upsert-property-spec)]) (def ^:private property-types @@ -56,6 +89,16 @@ (defn invalid-options? [command opts] (case command + :upsert-block + (let [opts (cond-> opts + (seq (:target-page opts)) + (assoc :target-page-name (:target-page opts))) + update-mode? (or (some? (:id opts)) + (seq (some-> (:uuid opts) string/trim)))] + (if update-mode? + (update-command/invalid-options? opts) + (add-command/invalid-options? opts))) + :upsert-property (let [type' (normalize-property-type (:type opts)) cardinality' (normalize-property-cardinality (:cardinality opts))] @@ -71,6 +114,115 @@ nil)) +(defn update-mode? + [opts] + (or (some? (:id opts)) + (seq (some-> (:uuid opts) string/trim)))) + +(defn build-block-action + [options args repo] + (let [update-mode* (update-mode? options)] + (if update-mode* + (let [options (cond-> options + (seq (:target-page options)) + (assoc :target-page (:target-page options)))] + (-> (update-command/build-action options repo) + (update :action + (fn [action] + (when action + (assoc action :type :upsert-block :mode :update)))))) + (let [options (cond-> options + (seq (:target-page options)) + (assoc :target-page-name (:target-page options)) + true + (dissoc :target-page)) + create-result (add-command/build-add-block-action options args repo) + update-tags-result (add-command/parse-tags-option (:update-tags options)) + update-properties-result (add-command/parse-properties-option + (:update-properties options) + {:allow-non-built-in? true}) + remove-tags-result (add-command/parse-tags-vector-option (:remove-tags options)) + remove-properties-result (add-command/parse-properties-vector-option + (:remove-properties options) + {:allow-non-built-in? true})] + (cond + (not (:ok? create-result)) + create-result + + (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 + (-> create-result + (update :action + (fn [action] + (-> action + (assoc :type :upsert-block + :mode :create + :update-tags (:value update-tags-result) + :update-properties (:value update-properties-result) + :remove-tags (:value remove-tags-result) + :remove-properties (:value remove-properties-result))))))))))) + +(defn build-page-action + [options repo] + (if-not (seq repo) + {:ok? false + :error {:code :missing-repo + :message "repo is required for upsert"}} + (let [page (some-> (:page options) string/trim) + tags-result (add-command/parse-tags-option (:tags options)) + properties-result (add-command/parse-properties-option (:properties options)) + 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))] + (cond + (not (seq page)) + {:ok? false + :error {:code :missing-page-name + :message "page name is required"}} + + (not (:ok? tags-result)) + tags-result + + (not (:ok? properties-result)) + properties-result + + (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 :upsert-page + :repo repo + :graph (core/repo->graph repo) + :page page + :tags (:value tags-result) + :properties (:value properties-result) + :update-tags (:value update-tags-result) + :update-properties (:value update-properties-result) + :remove-tags (:value remove-tags-result) + :remove-properties (:value remove-properties-result)}})))) + (defn build-tag-action [options repo] (if-not (seq repo) @@ -143,6 +295,135 @@ (transport/invoke config :thread-api/pull false [repo selector [:block/name (common-util/page-name-sanity-lc page-name)]])) +(defn- ensure-property-identifiers-exist! + [config repo property-idents] + (if (seq property-idents) + (p/all + (map (fn [property-ident] + (p/let [entity (transport/invoke config :thread-api/pull false + [repo [:db/id] [:db/ident property-ident]])] + (when-not (:db/id entity) + (throw (ex-info "property not found" + {:code :property-not-found + :property property-ident}))))) + (distinct property-idents))) + (p/resolved nil))) + +(defn- ensure-page-entity! + [config repo page-name] + (p/let [existing (pull-page-by-name config repo page-name [:db/id :block/uuid])] + (if (:db/id existing) + existing + (p/let [_ (transport/invoke config :thread-api/apply-outliner-ops false + [repo [[:create-page [page-name {}]]] {}]) + created (pull-page-by-name config repo page-name [:db/id :block/uuid])] + (if (:db/id created) + created + (throw (ex-info "page not found after upsert" + {:code :page-not-found + :page page-name}))))))) + +(defn- append-tag-and-property-ops + [ops block-ids {:keys [update-tag-ids remove-tag-ids update-properties remove-properties]}] + (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)))) + +(defn- execute-extra-upsert-block-ops! + [action config block-ids] + (if (seq block-ids) + (p/let [cfg (cli-server/ensure-server! config (:repo 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) + {:allow-non-built-in? true}) + remove-properties (add-command/resolve-property-identifiers + cfg (:repo action) (:remove-properties action) + {:allow-non-built-in? true}) + update-property-idents (keys (or update-properties {})) + _ (ensure-property-identifiers-exist! cfg (:repo action) update-property-idents) + _ (ensure-property-identifiers-exist! cfg (:repo action) remove-properties) + ops (append-tag-and-property-ops [] + block-ids + {:update-tag-ids (->> update-tags (map :db/id) (remove nil?) distinct vec) + :remove-tag-ids (->> remove-tags (map :db/id) (remove nil?) distinct vec) + :update-properties update-properties + :remove-properties remove-properties})] + (when (seq ops) + (transport/invoke cfg :thread-api/apply-outliner-ops false + [(:repo action) ops {}]))) + (p/resolved nil))) + +(defn execute-upsert-block + [action config] + (-> (if (= :update (:mode action)) + (update-command/execute-update (assoc action :type :update-block) config) + (p/let [result (add-command/execute-add-block (assoc action :type :add-block) config) + created-ids (vec (or (get-in result [:data :result]) [])) + _ (execute-extra-upsert-block-ops! action config created-ids)] + {:status :ok + :data {:result created-ids}})) + (p/catch (fn [e] + {:status :error + :error {:code (or (get-in (ex-data e) [:code]) :exception) + :message (or (ex-message e) (str e))}})))) + +(defn execute-upsert-page + [action config] + (-> (p/let [cfg (cli-server/ensure-server! config (:repo action)) + page (ensure-page-entity! cfg (:repo action) (:page action)) + page-id (:db/id page) + block-ids [page-id] + add-tags (add-command/resolve-tags cfg (:repo action) (:tags 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)) + add-properties (add-command/resolve-properties cfg (:repo action) (:properties 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)) + merged-properties (merge (or add-properties {}) (or update-properties {})) + _ (ensure-property-identifiers-exist! cfg (:repo action) (keys merged-properties)) + _ (ensure-property-identifiers-exist! cfg (:repo action) remove-properties) + update-tag-ids (->> (concat (or add-tags []) (or update-tags [])) + (map :db/id) + (remove nil?) + distinct + vec) + remove-tag-ids (->> remove-tags (map :db/id) (remove nil?) distinct vec) + ops (append-tag-and-property-ops [] + block-ids + {:update-tag-ids update-tag-ids + :remove-tag-ids remove-tag-ids + :update-properties merged-properties + :remove-properties remove-properties}) + _ (when (seq ops) + (transport/invoke cfg :thread-api/apply-outliner-ops false + [(:repo action) ops {}]))] + {:status :ok + :data {:result [page-id]}}) + (p/catch (fn [e] + {:status :error + :error {:code (or (get-in (ex-data e) [:code]) :exception) + :message (or (ex-message e) (str e))}})))) + (defn- tag-entity? [entity] (some #(= :logseq.class/Tag (:db/ident %)) diff --git a/src/main/logseq/cli/commands.cljs b/src/main/logseq/cli/commands.cljs index 2a3bde01a1..d7f40c2615 100644 --- a/src/main/logseq/cli/commands.cljs +++ b/src/main/logseq/cli/commands.cljs @@ -2,7 +2,6 @@ "Command parsing and action building for the Logseq CLI." (:require [babashka.cli :as cli] [clojure.string :as string] - [logseq.cli.command.add :as add-command] [logseq.cli.command.core :as command-core] [logseq.cli.command.doctor :as doctor-command] [logseq.cli.command.graph :as graph-command] @@ -12,7 +11,6 @@ [logseq.cli.command.server :as server-command] [logseq.cli.command.show :as show-command] [logseq.cli.command.upsert :as upsert-command] - [logseq.cli.command.update :as update-command] [logseq.cli.server :as cli-server] [promesa.core :as p])) @@ -49,13 +47,6 @@ :message "block or page is required"} :summary summary}) -(defn- missing-source-result - [summary] - {:ok? false - :error {:code :missing-source - :message "source block is required"} - :summary summary}) - (defn- missing-page-name-result [summary] {:ok? false @@ -113,10 +104,8 @@ (vec (concat graph-command/entries server-command/entries list-command/entries - add-command/entries upsert-command/entries remove-command/entries - update-command/entries query-command/entries show-command/entries doctor-command/entries))) @@ -162,7 +151,7 @@ (seq (:blocks-file opts)) has-args?) show-targets (filter some? [(:id opts) (:uuid opts) (:page opts)]) - update-sources (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])] + upsert-update-mode? (upsert-command/update-mode? opts)] (cond (:help opts) (command-core/help-result cmd-summary) @@ -171,13 +160,13 @@ (not (seq graph))) (missing-graph-result summary) - (and (= command :add-block) (not has-content?)) + (and (= command :upsert-block) (not upsert-update-mode?) (not has-content?)) (missing-content-result summary) - (and (= command :add-block) (add-command/invalid-options? opts)) - (command-core/invalid-options-result summary (add-command/invalid-options? opts)) + (and (= command :upsert-block) (upsert-command/invalid-options? command opts)) + (command-core/invalid-options-result summary (upsert-command/invalid-options? command opts)) - (and (= command :add-page) (not (seq (:page opts)))) + (and (= command :upsert-page) (not (seq (:page opts)))) (missing-page-name-result summary) (and (= command :upsert-tag) (not (seq (some-> (:name opts) string/trim)))) @@ -199,12 +188,6 @@ (empty? (filter some? [(:id opts) (some-> (:name opts) string/trim)]))) (missing-target-result summary) - (and (= command :update-block) (update-command/invalid-options? opts)) - (command-core/invalid-options-result summary (update-command/invalid-options? opts)) - - (and (= command :update-block) (empty? update-sources)) - (missing-source-result summary) - (and (= command :show) (empty? show-targets)) (missing-target-result summary) @@ -282,7 +265,7 @@ :message "missing command"} :summary summary}) - (and (= 1 (count args)) (#{"graph" "server" "list" "add" "upsert" "remove" "query"} (first args))) + (and (= 1 (count args)) (#{"graph" "server" "list" "upsert" "remove" "query"} (first args))) (command-core/help-result (command-core/group-summary (first args) table)) :else @@ -380,11 +363,11 @@ (:list-page :list-tag :list-property) (list-command/build-action command options repo) - :add-block - (add-command/build-add-block-action options args repo) + :upsert-block + (upsert-command/build-block-action options args repo) - :add-page - (add-command/build-add-page-action options repo) + :upsert-page + (upsert-command/build-page-action options repo) :upsert-tag (upsert-command/build-tag-action options repo) @@ -392,9 +375,6 @@ :upsert-property (upsert-command/build-property-action options repo) - :update-block - (update-command/build-action options repo) - (:remove-block :remove-page :remove-tag :remove-property) (remove-command/build-action command options repo) @@ -438,11 +418,10 @@ :list-page (list-command/execute-list-page action config) :list-tag (list-command/execute-list-tag action config) :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) + :upsert-block (upsert-command/execute-upsert-block action config) + :upsert-page (upsert-command/execute-upsert-page action config) :upsert-tag (upsert-command/execute-upsert-tag action config) :upsert-property (upsert-command/execute-upsert-property action config) - :update-block (update-command/execute-update action config) :remove-block (remove-command/execute-remove-block action config) :remove-page (remove-command/execute-remove-page action config) :remove-tag (remove-command/execute-remove-tag action config) diff --git a/src/main/logseq/cli/format.cljs b/src/main/logseq/cli/format.cljs index 6052212040..e6d1efc675 100644 --- a/src/main/logseq/cli/format.cljs +++ b/src/main/logseq/cli/format.cljs @@ -249,17 +249,24 @@ (cond-> [(str "Server " (name status) ": " repo)] (and host port) (conj (str "Host: " host " Port: " port)))))) -(defn- format-add-block - [_context ids] - (str "Added blocks:\n" (pr-str (vec (or ids []))))) +(defn- format-upsert-block + [{:keys [repo source target update-tags update-properties remove-tags remove-properties]} result] + (if (vector? result) + (str "Upserted blocks:\n" (pr-str (vec (or result [])))) + (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 "Upserted block: " source (or move-fragment "") " (repo: " repo (or changes "") ")")))) -(defn- format-add-page +(defn- format-upsert-page [_context ids] - (str "Added page:\n" (pr-str (vec (or ids []))))) - -(defn- format-add-tag - [_context ids] - (str "Added tag:\n" (pr-str (vec (or ids []))))) + (str "Upserted page:\n" (pr-str (vec (or ids []))))) (defn- format-upsert-tag [_context ids] @@ -293,19 +300,6 @@ (str "Removed property: " name " (repo: " repo ")") (str "Removed property: " id " (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)) @@ -352,16 +346,14 @@ (format-server-action command data) :list-page (format-list-page (:items data) now-ms) (:list-tag :list-property) (format-list-tag-or-property (:items data) now-ms) - :add-block (format-add-block context (:result data)) - :add-page (format-add-page context (:result data)) - :add-tag (format-add-tag context (:result data)) + :upsert-block (format-upsert-block context (:result data)) + :upsert-page (format-upsert-page context (:result data)) :upsert-tag (format-upsert-tag context (:result data)) :upsert-property (format-upsert-property context (:result data)) :remove-block (format-remove-block context) :remove-page (format-remove-page context) :remove-tag (format-remove-tag context) :remove-property (format-remove-property 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 bbbafd346f..969eb7afbf 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -59,10 +59,8 @@ (is (string/includes? plain-summary "Graph Inspect and Edit")) (is (string/includes? plain-summary "Graph Management")) (is (string/includes? plain-summary "list")) - (is (string/includes? plain-summary "add")) (is (string/includes? plain-summary "upsert")) (is (string/includes? plain-summary "remove")) - (is (string/includes? plain-summary "update")) (is (string/includes? plain-summary "query")) (is (string/includes? plain-summary "show")) (is (string/includes? plain-summary "doctor")) @@ -72,15 +70,14 @@ (is (contains-bold? summary "list page")) (is (contains-bold? summary "list tag")) (is (contains-bold? summary "list property")) - (is (contains-bold? summary "add block")) - (is (contains-bold? summary "add page")) + (is (contains-bold? summary "upsert block")) + (is (contains-bold? summary "upsert page")) (is (contains-bold? summary "upsert tag")) (is (contains-bold? summary "upsert property")) (is (contains-bold? summary "remove block")) (is (contains-bold? summary "remove page")) (is (contains-bold? summary "remove tag")) (is (contains-bold? summary "remove property")) - (is (contains-bold? summary "update")) (is (contains-bold? summary "query")) (is (contains-bold? summary "query list")) (is (contains-bold? summary "show")) @@ -161,16 +158,17 @@ (is (contains-bold? summary "--id")) (is (contains-bold? summary "--uuid")))) - (testing "update command shows help" + (testing "upsert block command shows help" (let [result (binding [style/*color-enabled?* true] - (commands/parse-args ["update" "--help"])) + (commands/parse-args ["upsert" "block" "--help"])) summary (:summary result) plain-summary (strip-ansi summary)] (is (true? (:help? result))) - (is (string/includes? plain-summary "Usage: logseq update")) + (is (string/includes? plain-summary "Usage: logseq upsert block")) (is (string/includes? plain-summary "Command options:")) (is (contains-bold? summary "--id")) (is (contains-bold? summary "--uuid")) + (is (contains-bold? summary "--content")) (is (contains-bold? summary "--target-id")) (is (contains-bold? summary "--target-uuid")) (is (contains-bold? summary "--update-tags")) @@ -207,17 +205,11 @@ (is (seq lines)) (is (every? #(not (string/includes? % "[options]")) lines))))) -(deftest test-parse-args-help-add-upsert-group - (testing "add group shows subcommands" - (let [result (binding [style/*color-enabled?* true] - (commands/parse-args ["add"])) - summary (:summary result) - plain-summary (strip-ansi summary)] - (is (true? (:help? result))) - (is (string/includes? plain-summary "add block")) - (is (string/includes? plain-summary "add page")) - (is (contains-bold? summary "add block")) - (is (contains-bold? summary "add page")))) +(deftest test-parse-args-help-upsert-group + (testing "add group is removed" + (let [result (commands/parse-args ["add"])] + (is (false? (:ok? result))) + (is (= :unknown-command (get-in result [:error :code]))))) (testing "upsert group shows subcommands" (let [result (binding [style/*color-enabled?* true] @@ -225,8 +217,12 @@ summary (:summary result) plain-summary (strip-ansi summary)] (is (true? (:help? result))) + (is (string/includes? plain-summary "upsert block")) + (is (string/includes? plain-summary "upsert page")) (is (string/includes? plain-summary "upsert tag")) (is (string/includes? plain-summary "upsert property")) + (is (contains-bold? summary "upsert block")) + (is (contains-bold? summary "upsert page")) (is (contains-bold? summary "upsert tag")) (is (contains-bold? summary "upsert property"))))) @@ -282,6 +278,14 @@ (is (false? (:ok? result))) (is (= :unknown-command (get-in result [:error :code])))))) + (testing "rejects removed write commands" + (doseq [args [["add" "block" "--content" "x"] + ["add" "page" "--page" "Home"] + ["update" "--id" "1" "--update-tags" "[\"TagA\"]"]]] + (let [result (commands/parse-args args)] + (is (false? (:ok? result))) + (is (= :unknown-command (get-in result [:error :code])))))) + (testing "errors on missing command" (let [result (commands/parse-args [])] (is (false? (:ok? result))) @@ -634,11 +638,11 @@ (deftest test-help-tags-properties-identifiers (testing "add help mentions tag and property identifiers" (let [summary (:summary (binding [style/*color-enabled?* true] - (commands/parse-args ["add" "block" "--help"])))] + (commands/parse-args ["upsert" "block" "--help"])))] (is (string/includes? (strip-ansi summary) "Identifiers can be id, :db/ident, or :block/title."))) (let [summary (:summary (binding [style/*color-enabled?* true] - (commands/parse-args ["add" "page" "--help"])))] + (commands/parse-args ["upsert" "page" "--help"])))] (is (string/includes? (strip-ansi summary) "Identifiers can be id, :db/ident, or :block/title."))))) @@ -927,126 +931,149 @@ "--name" "owner" "--cardinality" "triple"])] (is (false? (:ok? result))) - (is (= :invalid-options (get-in result [:error :code]))))) + (is (= :invalid-options (get-in result [:error :code])))))) - (testing "update requires source selector" - (let [result (commands/parse-args ["update" "--target-id" "10"])] +(deftest test-verb-subcommand-parse-upsert-block-mode + + (testing "upsert block create mode requires content when source selectors are absent" + (let [result (commands/parse-args ["upsert" "block" "--target-id" "10"])] (is (false? (:ok? result))) - (is (= :missing-source (get-in result [:error :code]))))) + (is (= :missing-content (get-in result [:error :code]))))) - (testing "update requires target or update/remove options" - (let [result (commands/parse-args ["update" "--id" "1"])] + (testing "upsert block update mode requires target or update/remove options" + (let [result (commands/parse-args ["upsert" "block" "--id" "1"])] (is (false? (:ok? result))) (is (= :invalid-options (get-in result [:error :code]))))) - (testing "update parses with source and target" - (let [result (commands/parse-args ["update" "--uuid" "abc" "--target-uuid" "def" "--pos" "last-child"])] + (testing "upsert block parses with source and target" + (let [result (commands/parse-args ["upsert" "block" "--uuid" "abc" "--target-uuid" "def" "--pos" "last-child"])] (is (true? (:ok? result))) - (is (= :update-block (:command result))) + (is (= :upsert-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]))))) - (testing "update parses with tags and properties" - (let [result (commands/parse-args ["update" "--id" "1" + (testing "upsert block parses with update tags and properties" + (let [result (commands/parse-args ["upsert" "block" "--id" "1" "--update-tags" "[\"TagA\"]" "--update-properties" "{:logseq.property/publishing-public? true}"])] (is (true? (:ok? result))) - (is (= :update-block (:command result))) + (is (= :upsert-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" + (testing "upsert block allows updates without move target" + (let [result (commands/parse-args ["upsert" "block" "--uuid" "abc" "--update-tags" "[\"TagA\"]"])] (is (true? (:ok? result))) - (is (= :update-block (:command result))) - (is (= "abc" (get-in result [:options :uuid])))))) + (is (= :upsert-block (:command result))) + (is (= "abc" (get-in result [:options :uuid]))))) + + (testing "upsert block forces update mode when id and content are both provided" + (let [result (commands/parse-args ["upsert" "block" + "--id" "1" + "--content" "hello" + "--update-tags" "[\"TagA\"]"])] + (is (true? (:ok? result))) + (is (= :upsert-block (:command result))) + (is (= 1 (get-in result [:options :id]))) + (is (= "hello" (get-in result [:options :content]))) + (is (= "[\"TagA\"]" (get-in result [:options :update-tags])))))) (deftest test-verb-subcommand-parse-add - (testing "add block requires content source" - (let [result (commands/parse-args ["add" "block"])] + (testing "upsert block create mode requires content source" + (let [result (commands/parse-args ["upsert" "block"])] (is (false? (:ok? result))) (is (= :missing-content (get-in result [:error :code]))))) - (testing "add block parses with content" - (let [result (commands/parse-args ["add" "block" "--content" "hello"])] + (testing "upsert block create mode parses with content" + (let [result (commands/parse-args ["upsert" "block" "--content" "hello"])] (is (true? (:ok? result))) - (is (= :add-block (:command result))) + (is (= :upsert-block (:command result))) (is (= "hello" (get-in result [:options :content]))))) - (testing "add block parses with target selectors and pos" - (let [result (commands/parse-args ["add" "block" + (testing "upsert block create mode parses with target selectors and pos" + (let [result (commands/parse-args ["upsert" "block" "--content" "hello" "--target-uuid" "abc" "--pos" "first-child"])] (is (true? (:ok? result))) - (is (= :add-block (:command result))) + (is (= :upsert-block (:command result))) (is (= "abc" (get-in result [:options :target-uuid]))) (is (= "first-child" (get-in result [:options :pos]))))) - (testing "add block parses with tags and properties" - (let [result (commands/parse-args ["add" "block" + (testing "upsert block create mode parses with tags and properties" + (let [result (commands/parse-args ["upsert" "block" "--content" "hello" "--tags" "[\"TagA\" \"TagB\"]" "--properties" "{:logseq.property/publishing-public? true}"])] (is (true? (:ok? result))) - (is (= :add-block (:command result))) + (is (= :upsert-block (:command result))) (is (= "[\"TagA\" \"TagB\"]" (get-in result [:options :tags]))) (is (= "{:logseq.property/publishing-public? true}" (get-in result [:options :properties]))))) - (testing "add block rejects invalid pos" - (let [result (commands/parse-args ["add" "block" + (testing "upsert block rejects invalid pos" + (let [result (commands/parse-args ["upsert" "block" "--content" "hello" "--pos" "middle"])] (is (false? (:ok? result))) (is (= :invalid-options (get-in result [:error :code]))))) - (testing "add block rejects tags with blocks payload" - (let [result (commands/parse-args ["add" "block" + (testing "upsert block rejects tags with blocks payload" + (let [result (commands/parse-args ["upsert" "block" "--blocks" "[]" "--tags" "[\"TagA\"]"])] (is (false? (:ok? result))) (is (= :invalid-options (get-in result [:error :code]))))) - (testing "add block rejects properties with blocks-file payload" - (let [result (commands/parse-args ["add" "block" + (testing "upsert block rejects properties with blocks-file payload" + (let [result (commands/parse-args ["upsert" "block" "--blocks-file" "/tmp/blocks.edn" "--properties" "{:logseq.property/publishing-public? true}"])] (is (false? (:ok? result))) (is (= :invalid-options (get-in result [:error :code]))))) - (testing "add page requires page name" - (let [result (commands/parse-args ["add" "page"])] + (testing "upsert page requires page name" + (let [result (commands/parse-args ["upsert" "page"])] (is (false? (:ok? result))) (is (= :missing-page-name (get-in result [:error :code]))))) - (testing "add page parses with name" - (let [result (commands/parse-args ["add" "page" "--page" "Home"])] + (testing "upsert page parses with name" + (let [result (commands/parse-args ["upsert" "page" "--page" "Home"])] (is (true? (:ok? result))) - (is (= :add-page (:command result))) + (is (= :upsert-page (:command result))) (is (= "Home" (get-in result [:options :page]))))) - (testing "add page parses with tags and properties" - (let [result (commands/parse-args ["add" "page" + (testing "upsert page parses with tags and properties" + (let [result (commands/parse-args ["upsert" "page" "--page" "Home" "--tags" "[\"TagA\"]" "--properties" "{:logseq.property/publishing-public? true}"])] (is (true? (:ok? result))) - (is (= :add-page (:command result))) + (is (= :upsert-page (:command result))) (is (= "[\"TagA\"]" (get-in result [:options :tags]))) (is (= "{:logseq.property/publishing-public? true}" (get-in result [:options :properties]))))) - (testing "add tag is no longer supported" + (testing "upsert page parses update and remove options" + (let [result (commands/parse-args ["upsert" "page" + "--page" "Home" + "--update-tags" "[\"TagB\"]" + "--remove-properties" "[:logseq.property/deadline]"])] + (is (true? (:ok? result))) + (is (= :upsert-page (:command result))) + (is (= "[\"TagB\"]" (get-in result [:options :update-tags]))) + (is (= "[:logseq.property/deadline]" (get-in result [:options :remove-properties]))))) + + (testing "legacy add tag is no longer supported" (let [result (commands/parse-args ["add" "tag" "--name" "Quote"])] (is (false? (:ok? result))) (is (= :unknown-command (get-in result [:error :code])))))) (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"])] + (testing "upsert block update mode parses with target page" + (let [result (commands/parse-args ["upsert" "block" "--id" "1" "--target-page" "Home"])] (is (true? (:ok? result))) - (is (= :update-block (:command result))) + (is (= :upsert-block (:command result))) (is (= 1 (get-in result [:options :id]))) (is (= "Home" (get-in result [:options :target-page])))))) @@ -1165,10 +1192,10 @@ (deftest test-verb-subcommand-parse-flags (testing "verb subcommands reject unknown flags" (doseq [args [["list" "page" "--wat"] - ["add" "block" "--wat"] + ["upsert" "block" "--wat"] + ["upsert" "page" "--wat"] ["remove" "block" "--wat"] ["upsert" "tag" "--wat"] - ["update" "--wat"] ["show" "--wat"]]] (let [result (commands/parse-args args)] (is (false? (:ok? result))) @@ -1269,19 +1296,19 @@ (is (= :missing-repo (get-in result [:error :code]))))) (testing "add block requires content" - (let [parsed {:ok? true :command :add-block :options {}} + (let [parsed {:ok? true :command :upsert-block :options {}} result (commands/build-action parsed {:repo "demo"})] (is (false? (:ok? result))) (is (= :missing-content (get-in result [:error :code]))))) (testing "add block builds insert-blocks op" - (let [parsed {:ok? true :command :add-block :options {:content "hello"}} + (let [parsed {:ok? true :command :upsert-block :options {:content "hello"}} result (commands/build-action parsed {:repo "demo"})] (is (true? (:ok? result))) - (is (= :add-block (get-in result [:action :type]))))) + (is (= :upsert-block (get-in result [:action :type]))))) (testing "add page requires name" - (let [parsed {:ok? true :command :add-page :options {}} + (let [parsed {:ok? true :command :upsert-page :options {}} result (commands/build-action parsed {:repo "demo"})] (is (false? (:ok? result))) (is (= :missing-page-name (get-in result [:error :code]))))) @@ -1374,7 +1401,7 @@ (deftest test-build-action-add-validates-properties (testing "add block rejects unknown property" - (let [parsed (commands/parse-args ["add" "block" + (let [parsed (commands/parse-args ["upsert" "block" "--content" "hello" "--properties" "{:not/a 1}"]) result (commands/build-action parsed {:repo "demo"})] @@ -1382,7 +1409,7 @@ (is (= :invalid-options (get-in result [:error :code]))))) (testing "add block accepts property title key" - (let [parsed (commands/parse-args ["add" "block" + (let [parsed (commands/parse-args ["upsert" "block" "--content" "hello" "--properties" "{\"Publishing Public?\" true}"]) result (commands/build-action parsed {:repo "demo"})] @@ -1391,7 +1418,7 @@ (-> result :action :properties keys first))))) (testing "add block rejects non-public built-in property" - (let [parsed (commands/parse-args ["add" "block" + (let [parsed (commands/parse-args ["upsert" "block" "--content" "hello" "--properties" "{:logseq.property/heading 1}"]) result (commands/build-action parsed {:repo "demo"})] @@ -1399,7 +1426,7 @@ (is (= :invalid-options (get-in result [:error :code]))))) (testing "add block rejects invalid checkbox value" - (let [parsed (commands/parse-args ["add" "block" + (let [parsed (commands/parse-args ["upsert" "block" "--content" "hello" "--properties" "{:logseq.property/publishing-public? \"nope\"}"]) result (commands/build-action parsed {:repo "demo"})] @@ -1408,7 +1435,7 @@ (deftest test-build-action-add-accepts-tag-ids (testing "add block accepts numeric tag ids" - (let [parsed (commands/parse-args ["add" "block" + (let [parsed (commands/parse-args ["upsert" "block" "--content" "hello" "--tags" "[42]"]) result (commands/build-action parsed {:repo "demo"})] @@ -1424,63 +1451,87 @@ (is (= {:type :id :value 42} (normalize-property-key-input 42))))) (deftest test-build-action-update - (testing "update requires source selector" - (let [parsed {:ok? true :command :update-block :options {:target-id 2}} + (testing "upsert block create mode requires content when source selector is absent" + (let [parsed {:ok? true :command :upsert-block :options {:target-id 2}} result (commands/build-action parsed {:repo "demo"})] (is (false? (:ok? result))) - (is (= :missing-source (get-in result [:error :code]))))) + (is (= :missing-content (get-in result [:error :code]))))) - (testing "update requires target or update/remove options" - (let [parsed {:ok? true :command :update-block :options {:id 1}} + (testing "upsert block update mode requires target or update/remove options" + (let [parsed {:ok? true :command :upsert-block :options {:id 1}} result (commands/build-action parsed {:repo "demo"})] (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 + :command :upsert-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 (= :upsert-block (get-in result [:action :type]))) (is (= ["TagA"] (get-in result [:action :update-tags]))))) (testing "update rejects invalid update tags" (let [parsed {:ok? true - :command :update-block + :command :upsert-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])))))) + (is (= :invalid-options (get-in result [:error :code]))))) + + (testing "upsert block forces update mode when id and content are both provided" + (let [parsed {:ok? true + :command :upsert-block + :options {:id 1 :content "hello" :update-tags "[\"TagA\"]"}} + result (commands/build-action parsed {:repo "demo"})] + (is (true? (:ok? result))) + (is (= :upsert-block (get-in result [:action :type]))) + (is (= 1 (get-in result [:action :id]))) + (is (= ["TagA"] (get-in result [:action :update-tags]))))) + + (testing "update accepts custom property identifiers" + (let [parsed {:ok? true + :command :upsert-block + :options {:id 1 + :update-properties "{:user.property/owner \"alice\"}" + :remove-properties "[:user.property/owner]"}} + result (commands/build-action parsed {:repo "demo"})] + (is (true? (:ok? result))) + (is (= :upsert-block (get-in result [:action :type]))) + (is (= {:user.property/owner "alice"} + (get-in result [:action :update-properties]))) + (is (= [:user.property/owner] + (get-in result [:action :remove-properties])))))) (deftest test-update-parse-validation (testing "update rejects multiple source selectors" - (let [result (commands/parse-args ["update" "--id" "1" "--uuid" "abc" "--target-id" "2"])] + (let [result (commands/parse-args ["upsert" "block" "--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"])] + (let [result (commands/parse-args ["upsert" "block" "--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"])] + (let [result (commands/parse-args ["upsert" "block" "--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"])] + (let [result (commands/parse-args ["upsert" "block" "--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"])] + (let [result (commands/parse-args ["upsert" "block" "--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\"]"])] + (let [result (commands/parse-args ["upsert" "block" "--id" "1" "--pos" "last-child" "--update-tags" "[\"TagA\"]"])] (is (false? (:ok? result))) (is (= :invalid-options (get-in result [:error :code])))))) @@ -1637,6 +1688,178 @@ (set! transport/invoke orig-invoke) (done))))))) +(deftest test-execute-upsert-block-create-applies-extra-tag-property-ops + (async done + (let [ops* (atom nil) + orig-list-graphs cli-server/list-graphs + orig-ensure-server! cli-server/ensure-server! + orig-execute-add-block add-command/execute-add-block + orig-resolve-tags add-command/resolve-tags + orig-resolve-properties add-command/resolve-properties + orig-resolve-property-identifiers add-command/resolve-property-identifiers + orig-invoke transport/invoke + action {:type :upsert-block + :mode :create + :repo "demo" + :update-tags [:tag/new] + :remove-tags [:tag/old] + :update-properties {:logseq.property/deadline "2026-01-25T12:00:00Z"} + :remove-properties [:logseq.property/publishing-public?]}] + (set! cli-server/list-graphs (fn [_] ["demo"])) + (set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"})) + (set! add-command/execute-add-block (fn [_ _] + (p/resolved {:status :ok + :data {:result [11 12]}}))) + (set! add-command/resolve-tags (fn [_ _ tags] + (p/resolved (cond + (= tags [:tag/new]) [{:db/id 101}] + (= tags [:tag/old]) [{:db/id 202}] + :else nil)))) + (set! add-command/resolve-properties (fn [_ _ properties & _] (p/resolved properties))) + (set! add-command/resolve-property-identifiers (fn [_ _ properties & _] (p/resolved properties))) + (set! transport/invoke (fn [_ method _ args] + (case method + :thread-api/pull (let [[_ _ lookup] args] + (if (and (vector? lookup) + (= :db/ident (first lookup))) + {:db/id 99} + {})) + :thread-api/apply-outliner-ops (let [[_ ops _] args] + (reset! ops* ops) + {:result :ok}) + (throw (ex-info "unexpected invoke" {:method method :args args}))))) + (-> (p/let [result (commands/execute action {})] + (is (= :ok (:status result))) + (is (= [11 12] (get-in result [:data :result]))) + (is (= [[:batch-delete-property-value [[11 12] :block/tags 202]] + [:batch-remove-property [[11 12] :logseq.property/publishing-public?]] + [:batch-set-property [[11 12] :block/tags 101 {}]] + [:batch-set-property [[11 12] :logseq.property/deadline "2026-01-25T12:00:00Z" {}]]] + @ops*))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally (fn [] + (set! cli-server/list-graphs orig-list-graphs) + (set! cli-server/ensure-server! orig-ensure-server!) + (set! add-command/execute-add-block orig-execute-add-block) + (set! add-command/resolve-tags orig-resolve-tags) + (set! add-command/resolve-properties orig-resolve-properties) + (set! add-command/resolve-property-identifiers orig-resolve-property-identifiers) + (set! transport/invoke orig-invoke) + (done))))))) + +(deftest test-execute-upsert-page-applies-ops-on-existing-page + (async done + (let [ops* (atom nil) + orig-list-graphs cli-server/list-graphs + orig-ensure-server! cli-server/ensure-server! + orig-resolve-tags add-command/resolve-tags + orig-resolve-properties add-command/resolve-properties + orig-resolve-property-identifiers add-command/resolve-property-identifiers + orig-invoke transport/invoke + action {:type :upsert-page + :repo "demo" + :page "Home" + :tags [:tag/new] + :update-tags [:tag/next] + :remove-tags [:tag/old] + :properties {:logseq.property/deadline "2026-01-25T12:00:00Z"} + :update-properties {:logseq.property/publishing-public? true} + :remove-properties [:logseq.property/deadline]}] + (set! cli-server/list-graphs (fn [_] ["demo"])) + (set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"})) + (set! add-command/resolve-tags (fn [_ _ tags] + (p/resolved (cond + (= tags [:tag/new]) [{:db/id 101}] + (= tags [:tag/next]) [{:db/id 303}] + (= tags [:tag/old]) [{:db/id 202}] + :else nil)))) + (set! add-command/resolve-properties (fn [_ _ properties & _] (p/resolved properties))) + (set! add-command/resolve-property-identifiers (fn [_ _ properties & _] (p/resolved properties))) + (set! transport/invoke (fn [_ method _ args] + (case method + :thread-api/pull (let [[_ _ lookup] args] + (cond + (= lookup [:block/name "home"]) + {:db/id 50 + :block/uuid (uuid "00000000-0000-0000-0000-000000000050")} + + (and (vector? lookup) (= :db/ident (first lookup))) + {:db/id 888} + + :else {})) + :thread-api/apply-outliner-ops (let [[_ ops _] args] + (reset! ops* ops) + {:result :ok}) + (throw (ex-info "unexpected invoke" {:method method :args args}))))) + (-> (p/let [result (commands/execute action {}) + ops @ops*] + (is (= :ok (:status result))) + (is (= [50] (get-in result [:data :result]))) + (is (= 6 (count ops))) + (is (some #(= [:batch-delete-property-value [[50] :block/tags 202]] %) ops)) + (is (some #(= [:batch-remove-property [[50] :logseq.property/deadline]] %) ops)) + (is (some #(= [:batch-set-property [[50] :block/tags 101 {}]] %) ops)) + (is (some #(= [:batch-set-property [[50] :block/tags 303 {}]] %) ops)) + (is (some #(= [:batch-set-property [[50] :logseq.property/deadline "2026-01-25T12:00:00Z" {}]] %) ops)) + (is (some #(= [:batch-set-property [[50] :logseq.property/publishing-public? true {}]] %) ops))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally (fn [] + (set! cli-server/list-graphs orig-list-graphs) + (set! cli-server/ensure-server! orig-ensure-server!) + (set! add-command/resolve-tags orig-resolve-tags) + (set! add-command/resolve-properties orig-resolve-properties) + (set! add-command/resolve-property-identifiers orig-resolve-property-identifiers) + (set! transport/invoke orig-invoke) + (done))))))) + +(deftest test-execute-upsert-page-errors-when-property-does-not-exist + (async done + (let [orig-list-graphs cli-server/list-graphs + orig-ensure-server! cli-server/ensure-server! + orig-resolve-tags add-command/resolve-tags + orig-resolve-properties add-command/resolve-properties + orig-resolve-property-identifiers add-command/resolve-property-identifiers + orig-invoke transport/invoke + action {:type :upsert-page + :repo "demo" + :page "Home" + :update-properties {:logseq.property/deadline "2026-01-25T12:00:00Z"}}] + (set! cli-server/list-graphs (fn [_] ["demo"])) + (set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"})) + (set! add-command/resolve-tags (fn [_ _ _] (p/resolved nil))) + (set! add-command/resolve-properties (fn [_ _ properties & _] (p/resolved properties))) + (set! add-command/resolve-property-identifiers (fn [_ _ properties & _] (p/resolved properties))) + (set! transport/invoke (fn [_ method _ args] + (case method + :thread-api/pull (let [[_ _ lookup] args] + (cond + (= lookup [:block/name "home"]) + {:db/id 50} + + (and (vector? lookup) (= :db/ident (first lookup))) + {} + + :else {})) + :thread-api/apply-outliner-ops + (throw (ex-info "should not apply ops when property lookup fails" + {:args args})) + (throw (ex-info "unexpected invoke" {:method method :args args}))))) + (-> (p/let [result (commands/execute action {})] + (is (= :error (:status result))) + (is (= :property-not-found (get-in result [:error :code])))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally (fn [] + (set! cli-server/list-graphs orig-list-graphs) + (set! cli-server/ensure-server! orig-ensure-server!) + (set! add-command/resolve-tags orig-resolve-tags) + (set! add-command/resolve-properties orig-resolve-properties) + (set! add-command/resolve-property-identifiers orig-resolve-property-identifiers) + (set! transport/invoke orig-invoke) + (done))))))) + (deftest test-execute-remove-tag-property (async done (let [ops* (atom []) @@ -1749,7 +1972,8 @@ orig-resolve-properties add-command/resolve-properties orig-resolve-property-identifiers add-command/resolve-property-identifiers orig-invoke transport/invoke - action {:type :update-block + action {:type :upsert-block + :mode :update :repo "demo" :id 1 :target-id 2 @@ -1765,8 +1989,8 @@ (= tags [:tag/new]) [{:db/id 101}] (= tags [:tag/old]) [{:db/id 202}] :else nil)))) - (set! add-command/resolve-properties (fn [_ _ properties] (p/resolved properties))) - (set! add-command/resolve-property-identifiers (fn [_ _ properties] (p/resolved properties))) + (set! add-command/resolve-properties (fn [_ _ properties & _] (p/resolved properties))) + (set! add-command/resolve-property-identifiers (fn [_ _ properties & _] (p/resolved properties))) (set! transport/invoke (fn [_ method _ args] (swap! calls* conj {:method method :args args}) (case method diff --git a/src/test/logseq/cli/format_test.cljs b/src/test/logseq/cli/format_test.cljs index ff794149f7..91c8528894 100644 --- a/src/test/logseq/cli/format_test.cljs +++ b/src/test/logseq/cli/format_test.cljs @@ -91,23 +91,23 @@ result))))) (deftest test-human-output-add-upsert-remove - (testing "add block renders ids in two lines" + (testing "upsert block renders ids in two lines" (let [result (format/format-result {:status :ok - :command :add-block + :command :upsert-block :context {:repo "demo-repo" :blocks ["a" "b"]} :data {:result [201 202]}} {:output-format nil})] - (is (= "Added blocks:\n[201 202]" result)))) + (is (= "Upserted blocks:\n[201 202]" result)))) - (testing "add page renders ids in two lines" + (testing "upsert page renders ids in two lines" (let [result (format/format-result {:status :ok - :command :add-page + :command :upsert-page :context {:repo "demo-repo" :page "Home"} :data {:result [123]}} {:output-format nil})] - (is (= "Added page:\n[123]" result)))) + (is (= "Upserted page:\n[123]" result)))) (testing "upsert tag renders ids in two lines" (let [result (format/format-result {:status :ok @@ -163,9 +163,9 @@ {:output-format nil})] (is (= "Removed property: owner (repo: demo-repo)" result)))) - (testing "update block renders a succinct success line" + (testing "upsert block update mode renders a succinct success line" (let [result (format/format-result {:status :ok - :command :update-block + :command :upsert-block :context {:repo "demo-repo" :source "source-uuid" :target "target-uuid" @@ -175,17 +175,17 @@ :remove-properties [:logseq.property/deadline]} :data {:result {:ok true}}} {:output-format nil})] - (is (= "Updated block: source-uuid -> target-uuid (repo: demo-repo, tags:+1, properties:+1, remove-tags:+1, remove-properties:+1)" result)))) + (is (= "Upserted 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" + (testing "upsert block update without move target renders a succinct success line" (let [result (format/format-result {:status :ok - :command :update-block + :command :upsert-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))))) + (is (= "Upserted 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 dec868d709..2ea2ac4f47 100644 --- a/src/test/logseq/cli/integration_test.cljs +++ b/src/test/logseq/cli/integration_test.cljs @@ -129,7 +129,7 @@ (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "tags-graph"] data-dir cfg-path) - _ (run-cli ["--repo" "tags-graph" "add" "page" "--page" "Home"] data-dir cfg-path)] + _ (run-cli ["--repo" "tags-graph" "upsert" "page" "--page" "Home"] data-dir cfg-path)] {:cfg-path cfg-path :repo "tags-graph"})) (defn- stop-repo! @@ -269,7 +269,7 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "content-graph"] data-dir cfg-path) - add-page-result (run-cli ["--repo" "content-graph" "add" "page" "--page" "TestPage"] data-dir cfg-path) + add-page-result (run-cli ["--repo" "content-graph" "upsert" "page" "--page" "TestPage"] data-dir cfg-path) add-page-payload (parse-json-output add-page-result) list-page-result (run-cli ["--repo" "content-graph" "list" "page"] data-dir cfg-path) list-page-payload (parse-json-output list-page-result) @@ -277,7 +277,7 @@ list-tag-payload (parse-json-output list-tag-result) list-property-result (run-cli ["--repo" "content-graph" "list" "property"] data-dir cfg-path) list-property-payload (parse-json-output list-property-result) - add-block-result (run-cli ["--repo" "content-graph" "add" "block" "--target-page-name" "TestPage" "--content" "Test block"] data-dir cfg-path) + add-block-result (run-cli ["--repo" "content-graph" "upsert" "block" "--target-page" "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" "TestPage"] data-dir cfg-path) @@ -288,7 +288,8 @@ stop-payload (parse-json-output stop-result)] (is (= 0 (:exit-code add-page-result))) (is (= "ok" (:status add-page-payload))) - (is (= "ok" (:status add-block-payload))) + (is (= "ok" (:status add-block-payload)) + (pr-str (:error add-block-payload))) (is (= "ok" (:status list-page-payload))) (is (vector? (get-in list-page-payload [:data :items]))) (is (= "ok" (:status list-tag-payload))) @@ -306,14 +307,14 @@ (is false (str "unexpected error: " e)) (done))))))) -(deftest ^:long test-cli-add-page-json-output-returns-id +(deftest ^:long test-cli-upsert-page-json-output-returns-id (async done (let [data-dir (node-helper/create-tmp-dir "db-worker-add-page-json-id") repo "add-page-json-id-graph"] (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) - add-page-result (run-cli ["--repo" repo "add" "page" "--page" "Home"] data-dir cfg-path) + add-page-result (run-cli ["--repo" repo "upsert" "page" "--page" "Home"] data-dir cfg-path) add-page-payload (parse-json-output add-page-result) page-ids (get-in add-page-payload [:data :result]) page-id (first page-ids) @@ -335,7 +336,7 @@ (is false (str "unexpected error: " e)) (done))))))) -(deftest ^:long test-cli-add-block-json-output-returns-ids +(deftest ^:long test-cli-upsert-block-create-json-output-returns-ids (async done (let [data-dir (node-helper/create-tmp-dir "db-worker-add-block-json-ids") repo "add-block-json-ids-graph"] @@ -345,10 +346,10 @@ {:block/title "Sibling"}]) _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) - _ (run-cli ["--repo" repo "add" "page" "--page" "Home"] data-dir cfg-path) + _ (run-cli ["--repo" repo "upsert" "page" "--page" "Home"] data-dir cfg-path) add-block-result (run-cli ["--repo" repo - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--blocks" blocks-edn] data-dir cfg-path) add-block-payload (parse-json-output add-block-result) @@ -361,8 +362,10 @@ set) stop-result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path) stop-payload (parse-json-output stop-result)] - (is (= 0 (:exit-code add-block-result))) - (is (= "ok" (:status add-block-payload))) + (is (= 0 (:exit-code add-block-result)) + (pr-str (:error add-block-payload))) + (is (= "ok" (:status add-block-payload)) + (pr-str (:error add-block-payload))) (is (vector? block-ids)) (is (= 3 (count block-ids))) (is (= 3 (count (distinct block-ids)))) @@ -383,15 +386,15 @@ _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) add-page-result (run-cli ["--repo" repo "--output" "edn" - "add" "page" + "upsert" "page" "--page" "Home"] data-dir cfg-path) add-page-payload (parse-edn-output add-page-result) page-ids (get-in add-page-payload [:data :result]) add-block-result (run-cli ["--repo" repo "--output" "edn" - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" "EDN block"] data-dir cfg-path) add-block-payload (parse-edn-output add-block-result) @@ -421,18 +424,18 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) - add-page-result (run-cli ["--repo" repo "add" "page" "--page" "ChainPage"] data-dir cfg-path) + add-page-result (run-cli ["--repo" repo "upsert" "page" "--page" "ChainPage"] data-dir cfg-path) add-page-payload (parse-json-output add-page-result) page-id (first-result-id add-page-payload) add-block-result (run-cli ["--repo" repo - "add" "block" + "upsert" "block" "--target-id" (str page-id) "--content" "Chain block"] data-dir cfg-path) add-block-payload (parse-json-output add-block-result) block-id (first-result-id add-block-payload) update-result (run-cli ["--repo" repo - "update" + "upsert" "block" "--id" (str block-id) "--update-properties" "{:logseq.property/publishing-public? true}"] data-dir cfg-path) @@ -462,16 +465,96 @@ (is false (str "unexpected error: " e)) (done))))))) +(deftest ^:long test-cli-upsert-page-create-and-update-existing + (async done + (let [data-dir (node-helper/create-tmp-dir "db-worker-upsert-page-existing") + repo "upsert-page-existing-graph"] + (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") + _ (fs/writeFileSync cfg-path "{:output-format :json}") + _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) + create-result (run-cli ["--repo" repo "upsert" "page" "--page" "Home"] data-dir cfg-path) + create-payload (parse-json-output create-result) + page-id (first-result-id create-payload) + update-result (run-cli ["--repo" repo + "upsert" "page" + "--page" "Home" + "--update-properties" "{:logseq.property/publishing-public? true}"] + data-dir cfg-path) + update-payload (parse-json-output update-result) + update-id (first-result-id update-payload) + property-after-update (query-property data-dir cfg-path repo "Home" + ":logseq.property/publishing-public?") + remove-result (run-cli ["--repo" repo + "upsert" "page" + "--page" "Home" + "--remove-properties" "[:logseq.property/publishing-public?]"] + data-dir cfg-path) + remove-payload (parse-json-output remove-result) + remove-id (first-result-id remove-payload) + property-after-remove (query-property data-dir cfg-path repo "Home" + ":logseq.property/publishing-public?") + stop-result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path) + stop-payload (parse-json-output stop-result)] + (is (= 0 (:exit-code create-result))) + (is (= "ok" (:status create-payload))) + (is (number? page-id)) + (is (= 0 (:exit-code update-result))) + (is (= "ok" (:status update-payload))) + (is (= page-id update-id)) + (is (= true property-after-update)) + (is (= 0 (:exit-code remove-result))) + (is (= "ok" (:status remove-payload))) + (is (= page-id remove-id)) + (is (nil? property-after-remove)) + (is (= "ok" (:status stop-payload))) + (done)) + (p/catch (fn [e] + (is false (str "unexpected error: " e)) + (done))))))) + +(deftest ^:long test-cli-upsert-page-errors-on-missing-tags-properties + (async done + (let [data-dir (node-helper/create-tmp-dir "db-worker-upsert-page-missing") + repo "upsert-page-missing-graph"] + (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") + _ (fs/writeFileSync cfg-path "{:output-format :json}") + _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) + missing-tag-result (run-cli ["--repo" repo + "upsert" "page" + "--page" "Home" + "--update-tags" "[\"MissingTag\"]"] + data-dir cfg-path) + missing-tag-payload (parse-json-output missing-tag-result) + missing-property-result (run-cli ["--repo" repo + "upsert" "page" + "--page" "Home" + "--update-properties" "{:not/a 1}"] + data-dir cfg-path) + missing-property-payload (parse-json-output missing-property-result) + stop-result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path) + stop-payload (parse-json-output stop-result)] + (is (= 1 (:exit-code missing-tag-result))) + (is (= "error" (:status missing-tag-payload))) + (is (= :tag-not-found (keyword (get-in missing-tag-payload [:error :code])))) + (is (= 1 (:exit-code missing-property-result))) + (is (= "error" (:status missing-property-payload))) + (is (= :invalid-options (keyword (get-in missing-property-payload [:error :code])))) + (is (= "ok" (:status stop-payload))) + (done)) + (p/catch (fn [e] + (is false (str "unexpected error: " e)) + (done))))))) + (deftest ^:long test-cli-add-block-rewrites-page-ref (async done (let [data-dir (node-helper/create-tmp-dir "db-worker-ref-rewrite")] (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "ref-rewrite-graph"] data-dir cfg-path) - _ (run-cli ["--repo" "ref-rewrite-graph" "add" "page" "--page" "Home"] data-dir cfg-path) + _ (run-cli ["--repo" "ref-rewrite-graph" "upsert" "page" "--page" "Home"] data-dir cfg-path) add-block-result (run-cli ["--repo" "ref-rewrite-graph" - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" "See [[New Page]]"] data-dir cfg-path) add-block-payload (parse-json-output-safe add-block-result "add-block") @@ -513,10 +596,10 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "uuid-ref-graph"] data-dir cfg-path) - _ (run-cli ["--repo" "uuid-ref-graph" "add" "page" "--page" "Home"] data-dir cfg-path) + _ (run-cli ["--repo" "uuid-ref-graph" "upsert" "page" "--page" "Home"] data-dir cfg-path) _ (run-cli ["--repo" "uuid-ref-graph" - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" "Target block"] data-dir cfg-path) _ (p/delay 100) @@ -525,8 +608,8 @@ (pr-str ["Target block"])) target-uuid (first (first (get-in target-query-payload [:data :result]))) add-block-result (run-cli ["--repo" "uuid-ref-graph" - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" (str "See [[" target-uuid "]]")] data-dir cfg-path) add-block-payload (parse-json-output add-block-result) @@ -564,11 +647,11 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "missing-uuid-ref-graph"] data-dir cfg-path) - _ (run-cli ["--repo" "missing-uuid-ref-graph" "add" "page" "--page" "Home"] data-dir cfg-path) + _ (run-cli ["--repo" "missing-uuid-ref-graph" "upsert" "page" "--page" "Home"] data-dir cfg-path) missing-uuid (str (random-uuid)) add-block-result (run-cli ["--repo" "missing-uuid-ref-graph" - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" (str "See [[" missing-uuid "]]")] data-dir cfg-path) add-block-payload (parse-json-output add-block-result) @@ -588,23 +671,23 @@ (let [data-dir (node-helper/create-tmp-dir "db-worker-tags")] (-> (p/let [{:keys [cfg-path repo]} (setup-tags-graph data-dir) add-page-result (run-cli ["--repo" "tags-graph" - "add" "page" + "upsert" "page" "--page" "TaggedPage" "--tags" "[\"Quote\"]" "--properties" "{:logseq.property/publishing-public? true}"] data-dir cfg-path) add-page-payload (parse-json-output add-page-result) add-block-result (run-cli ["--repo" "tags-graph" - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" "Tagged block" "--tags" "[\"Quote\"]" "--properties" "{:logseq.property/deadline \"2026-01-25T12:00:00Z\"}"] data-dir cfg-path) add-block-payload (parse-json-output add-block-result) add-block-ident-result (run-cli ["--repo" "tags-graph" - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" "Tagged block ident" "--tags" "[:logseq.class/Quote-block]"] data-dir cfg-path) @@ -612,14 +695,14 @@ deadline-prop-title (get-in db-property/built-in-properties [:logseq.property/deadline :title]) publishing-prop-title (get-in db-property/built-in-properties [:logseq.property/publishing-public? :title]) add-page-title-result (run-cli ["--repo" "tags-graph" - "add" "page" + "upsert" "page" "--page" "TaggedPageTitle" "--properties" (str "{\"" publishing-prop-title "\" true}")] data-dir cfg-path) add-page-title-payload (parse-json-output add-page-title-result) add-block-title-result (run-cli ["--repo" "tags-graph" - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" "Tagged block title" "--properties" (str "{\"" deadline-prop-title "\" \"2026-01-25T12:00:00Z\"}")] data-dir cfg-path) @@ -670,15 +753,15 @@ deadline-id (find-item-id (get-in list-property-payload [:data :items]) deadline-title) publishing-id (find-item-id (get-in list-property-payload [:data :items]) publishing-title) add-page-id-result (run-cli ["--repo" repo - "add" "page" + "upsert" "page" "--page" "TaggedPageId" "--tags" (pr-str [quote-tag-id]) "--properties" (pr-str {publishing-id true})] data-dir cfg-path) add-page-id-payload (parse-json-output add-page-id-result) add-block-id-result (run-cli ["--repo" repo - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" "Tagged block id" "--tags" (pr-str [quote-tag-id]) "--properties" (pr-str {deadline-id "2026-01-25T12:00:00Z"})] @@ -793,8 +876,8 @@ tag-a-name "Quote" tag-b-name "Math" add-block-result (run-cli ["--repo" repo - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" "Update block" "--tags" "[:logseq.class/Quote-block]" "--properties" "{:logseq.property/publishing-public? true}"] @@ -806,7 +889,7 @@ 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" + "upsert" "block" "--id" (str block-id) "--update-tags" "[:logseq.class/Math-block]" "--remove-tags" "[:logseq.class/Quote-block]" @@ -828,6 +911,63 @@ (is false (str "unexpected error: " e)) (done))))))) +(deftest ^:long test-cli-upsert-block-update-custom-property + (async done + (let [data-dir (node-helper/create-tmp-dir "db-worker-upsert-block-custom-property")] + (-> (p/let [{:keys [cfg-path repo]} (setup-tags-graph data-dir) + upsert-property-result (run-cli ["--repo" repo + "upsert" "property" + "--name" "owner" + "--type" "default"] + data-dir cfg-path) + upsert-property-payload (parse-json-output upsert-property-result) + add-block-result (run-cli ["--repo" repo + "upsert" "block" + "--target-page" "Home" + "--content" "Block with custom property"] + 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]) "Block with custom property") + block-id (node-id block-node) + update-result (run-cli ["--repo" repo + "upsert" "block" + "--id" (str block-id) + "--update-properties" "{:user.property/owner \"alice\"}"] + data-dir cfg-path) + update-payload (parse-json-output update-result) + _ (p/delay 100) + property-after-update (query-property data-dir cfg-path repo "Block with custom property" + ":user.property/owner") + remove-result (run-cli ["--repo" repo + "upsert" "block" + "--id" (str block-id) + "--remove-properties" "[:user.property/owner]"] + data-dir cfg-path) + remove-payload (parse-json-output remove-result) + _ (p/delay 100) + property-after-remove (query-property data-dir cfg-path repo "Block with custom property" + ":user.property/owner") + stop-payload (stop-repo! data-dir cfg-path repo)] + (is (= 0 (:exit-code upsert-property-result))) + (is (= "ok" (:status upsert-property-payload))) + (is (= 0 (:exit-code add-block-result))) + (is (= "ok" (:status add-block-payload))) + (is (some? block-id)) + (is (= 0 (:exit-code update-result))) + (is (= "ok" (:status update-payload))) + (is (some? property-after-update)) + (is (= 0 (:exit-code remove-result))) + (is (= "ok" (:status remove-payload))) + (is (nil? property-after-remove)) + (is (= "ok" (:status stop-payload))) + (done)) + (p/catch (fn [e] + (is false (str "unexpected error: " e)) + (done))))))) + (deftest ^:long test-cli-add-tags-rejects-missing-tag (async done (let [data-dir (node-helper/create-tmp-dir "db-worker-tags-missing")] @@ -835,8 +975,8 @@ _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "tags-missing-graph"] data-dir cfg-path) add-block-result (run-cli ["--repo" "tags-missing-graph" - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" "Block with missing tag" "--tags" "[\"MissingTag\"]"] data-dir cfg-path) @@ -864,7 +1004,7 @@ repo "upsert-tag-create-graph" _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) - _ (run-cli ["--repo" repo "add" "page" "--page" "Home"] data-dir cfg-path) + _ (run-cli ["--repo" repo "upsert" "page" "--page" "Home"] data-dir cfg-path) upsert-tag-result (run-cli ["--repo" repo "upsert" "tag" "--name" "CliQuote"] @@ -876,8 +1016,8 @@ (map #(or (:block/title %) (:title %) (:name %))) set) add-block-result (run-cli ["--repo" repo - "add" "block" - "--target-page-name" "Home" + "upsert" "block" + "--target-page" "Home" "--content" "Tagged by upsert tag" "--tags" "[\"CliQuote\"]"] data-dir cfg-path) @@ -908,7 +1048,7 @@ repo "upsert-tag-conflict-graph" _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) - _ (run-cli ["--repo" repo "add" "page" "--page" "ConflictPage"] data-dir cfg-path) + _ (run-cli ["--repo" repo "upsert" "page" "--page" "ConflictPage"] data-dir cfg-path) upsert-tag-result (run-cli ["--repo" repo "upsert" "tag" "--name" "ConflictPage"] @@ -1037,13 +1177,13 @@ _ (fs/writeFileSync cfg-path "{:output-format :json}") create-result (run-cli ["graph" "create" "--repo" "query-graph"] data-dir cfg-path) create-payload (parse-json-output create-result) - _ (run-cli ["--repo" "query-graph" "add" "page" "--page" "QueryPage"] data-dir cfg-path) - _ (run-cli ["--repo" "query-graph" "add" "block" - "--target-page-name" "QueryPage" + _ (run-cli ["--repo" "query-graph" "upsert" "page" "--page" "QueryPage"] data-dir cfg-path) + _ (run-cli ["--repo" "query-graph" "upsert" "block" + "--target-page" "QueryPage" "--content" "Query block"] data-dir cfg-path) - _ (run-cli ["--repo" "query-graph" "add" "block" - "--target-page-name" "QueryPage" + _ (run-cli ["--repo" "query-graph" "upsert" "block" + "--target-page" "QueryPage" "--content" "Query block"] data-dir cfg-path) _ (p/delay 100) @@ -1074,22 +1214,22 @@ _ (fs/writeFileSync cfg-path "{:output-format :json}") create-result (run-cli ["graph" "create" "--repo" "task-query-graph"] data-dir cfg-path) create-payload (parse-json-output create-result) - _ (run-cli ["--repo" "task-query-graph" "add" "page" "--page" "Tasks"] data-dir cfg-path) + _ (run-cli ["--repo" "task-query-graph" "upsert" "page" "--page" "Tasks"] data-dir cfg-path) _ (run-cli ["--repo" "task-query-graph" - "add" "block" - "--target-page-name" "Tasks" + "upsert" "block" + "--target-page" "Tasks" "--content" "Task one" "--status" "doing"] data-dir cfg-path) _ (run-cli ["--repo" "task-query-graph" - "add" "block" - "--target-page-name" "Tasks" + "upsert" "block" + "--target-page" "Tasks" "--content" "Task two" "--status" "doing"] data-dir cfg-path) _ (run-cli ["--repo" "task-query-graph" - "add" "block" - "--target-page-name" "Tasks" + "upsert" "block" + "--target-page" "Tasks" "--content" "Task three" "--status" "todo"] data-dir cfg-path) @@ -1194,9 +1334,9 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "recent-updated-graph"] data-dir cfg-path) - _ (run-cli ["--repo" "recent-updated-graph" "add" "page" "--page" "RecentPage"] data-dir cfg-path) - _ (run-cli ["--repo" "recent-updated-graph" "add" "block" - "--target-page-name" "RecentPage" + _ (run-cli ["--repo" "recent-updated-graph" "upsert" "page" "--page" "RecentPage"] data-dir cfg-path) + _ (run-cli ["--repo" "recent-updated-graph" "upsert" "block" + "--target-page" "RecentPage" "--content" "Recent block"] data-dir cfg-path) _ (p/delay 100) @@ -1291,20 +1431,20 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--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) + _ (run-cli ["--repo" "nested-refs" "upsert" "page" "--page" "NestedPage"] data-dir cfg-path) + _ (run-cli ["--repo" "nested-refs" "upsert" "block" "--target-page" "NestedPage" "--content" "Inner"] data-dir cfg-path) 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" + _ (run-cli ["--repo" "nested-refs" "upsert" "block" "--target-page" "NestedPage" "--content" middle-content] data-dir cfg-path) 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" + _ (run-cli ["--repo" "nested-refs" "upsert" "block" "--target-page" "NestedPage" "--content" (str "Outer [[" middle-uuid "]]")] 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) @@ -1327,15 +1467,15 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--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) + _ (run-cli ["--repo" "linked-refs-graph" "upsert" "page" "--page" "TargetPage"] data-dir cfg-path) + _ (run-cli ["--repo" "linked-refs-graph" "upsert" "page" "--page" "SourcePage"] data-dir cfg-path) 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) + _ (run-cli ["--repo" "linked-refs-graph" "upsert" "block" "--target-page" "SourcePage" "--content" ref-content] 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) @@ -1365,22 +1505,22 @@ (is false (str "unexpected error: " e)) (done))))))) -(deftest ^:long test-cli-update-block-move +(deftest ^:long test-cli-upsert-block-update-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") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "move-graph"] data-dir cfg-path) - _ (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) + _ (run-cli ["--repo" "move-graph" "upsert" "page" "--page" "SourcePage"] data-dir cfg-path) + _ (run-cli ["--repo" "move-graph" "upsert" "page" "--page" "TargetPage"] data-dir cfg-path) + _ (run-cli ["--repo" "move-graph" "upsert" "block" "--target-page" "SourcePage" "--content" "Parent Block"] 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) + _ (run-cli ["--repo" "move-graph" "upsert" "block" "--target-id" (str parent-id) "--content" "Child Block"] data-dir cfg-path) + update-result (run-cli ["--repo" "move-graph" "upsert" "block" "--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) @@ -1404,15 +1544,15 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--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) + _ (run-cli ["--repo" "add-pos-graph" "upsert" "page" "--page" "PosPage"] data-dir cfg-path) + _ (run-cli ["--repo" "add-pos-graph" "upsert" "block" "--target-page" "PosPage" "--content" "Parent"] 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) + _ (run-cli ["--repo" "add-pos-graph" "upsert" "block" "--target-id" (str parent-id) "--pos" "first-child" "--content" "First"] data-dir cfg-path) + _ (run-cli ["--repo" "add-pos-graph" "upsert" "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") @@ -1452,7 +1592,7 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "list-id-graph"] data-dir cfg-path) - _ (run-cli ["add" "page" "--page" "TestPage"] data-dir cfg-path) + _ (run-cli ["upsert" "page" "--page" "TestPage"] data-dir cfg-path) list-page-result (run-cli ["list" "page"] data-dir cfg-path) list-page-payload (parse-json-output list-page-result) list-tag-result (run-cli ["list" "tag"] data-dir cfg-path) @@ -1479,7 +1619,7 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "human-list-graph"] data-dir cfg-path) - _ (run-cli ["add" "page" "--page" "TestPage"] data-dir cfg-path) + _ (run-cli ["upsert" "page" "--page" "TestPage"] data-dir cfg-path) list-page-result (run-cli ["list" "page" "--output" "human"] data-dir cfg-path) output (:output list-page-result)] (is (= 0 (:exit-code list-page-result))) @@ -1497,7 +1637,7 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "show-page-block-graph"] data-dir cfg-path) - _ (run-cli ["add" "page" "--page" "TestPage"] data-dir cfg-path) + _ (run-cli ["upsert" "page" "--page" "TestPage"] data-dir cfg-path) list-page-result (run-cli ["list" "page" "--expand"] data-dir cfg-path) list-page-payload (parse-json-output list-page-result) page-item (some (fn [item] @@ -1536,14 +1676,14 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "show-multi-id-graph"] data-dir cfg-path) - _ (run-cli ["--repo" "show-multi-id-graph" "add" "page" "--page" "MultiPage"] + _ (run-cli ["--repo" "show-multi-id-graph" "upsert" "page" "--page" "MultiPage"] data-dir cfg-path) - _ (run-cli ["--repo" "show-multi-id-graph" "add" "block" - "--target-page-name" "MultiPage" + _ (run-cli ["--repo" "show-multi-id-graph" "upsert" "block" + "--target-page" "MultiPage" "--content" "Multi show one"] data-dir cfg-path) - _ (run-cli ["--repo" "show-multi-id-graph" "add" "block" - "--target-page-name" "MultiPage" + _ (run-cli ["--repo" "show-multi-id-graph" "upsert" "block" + "--target-page" "MultiPage" "--content" "Multi show two"] data-dir cfg-path) _ (p/delay 100) @@ -1608,10 +1748,10 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "show-multi-id-contained-graph"] data-dir cfg-path) - _ (run-cli ["--repo" "show-multi-id-contained-graph" "add" "page" "--page" "ParentPage"] + _ (run-cli ["--repo" "show-multi-id-contained-graph" "upsert" "page" "--page" "ParentPage"] data-dir cfg-path) - _ (run-cli ["--repo" "show-multi-id-contained-graph" "add" "block" - "--target-page-name" "ParentPage" + _ (run-cli ["--repo" "show-multi-id-contained-graph" "upsert" "block" + "--target-page" "ParentPage" "--content" "Parent Block"] data-dir cfg-path) parent-query (run-cli ["--repo" "show-multi-id-contained-graph" "query" @@ -1620,7 +1760,7 @@ data-dir cfg-path) parent-payload (parse-json-output parent-query) parent-id (get-in parent-payload [:data :result]) - _ (run-cli ["--repo" "show-multi-id-contained-graph" "add" "block" + _ (run-cli ["--repo" "show-multi-id-contained-graph" "upsert" "block" "--target-id" (str parent-id) "--content" "Child Block"] data-dir cfg-path) @@ -1662,14 +1802,14 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "query-pipe-graph"] data-dir cfg-path) - _ (run-cli ["--repo" "query-pipe-graph" "add" "page" "--page" "PipePage"] + _ (run-cli ["--repo" "query-pipe-graph" "upsert" "page" "--page" "PipePage"] data-dir cfg-path) - _ (run-cli ["--repo" "query-pipe-graph" "add" "block" - "--target-page-name" "PipePage" + _ (run-cli ["--repo" "query-pipe-graph" "upsert" "block" + "--target-page" "PipePage" "--content" "Pipe One"] data-dir cfg-path) - _ (run-cli ["--repo" "query-pipe-graph" "add" "block" - "--target-page-name" "PipePage" + _ (run-cli ["--repo" "query-pipe-graph" "upsert" "block" + "--target-page" "PipePage" "--content" "Pipe Two"] data-dir cfg-path) _ (p/delay 100) @@ -1736,14 +1876,14 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--repo" "query-stdin-graph"] data-dir cfg-path) - _ (run-cli ["--repo" "query-stdin-graph" "add" "page" "--page" "PipePage"] + _ (run-cli ["--repo" "query-stdin-graph" "upsert" "page" "--page" "PipePage"] data-dir cfg-path) - _ (run-cli ["--repo" "query-stdin-graph" "add" "block" - "--target-page-name" "PipePage" + _ (run-cli ["--repo" "query-stdin-graph" "upsert" "block" + "--target-page" "PipePage" "--content" "Pipe One"] data-dir cfg-path) - _ (run-cli ["--repo" "query-stdin-graph" "add" "block" - "--target-page-name" "PipePage" + _ (run-cli ["--repo" "query-stdin-graph" "upsert" "block" + "--target-page" "PipePage" "--content" "Pipe Two"] data-dir cfg-path) _ (p/delay 100) @@ -1798,8 +1938,8 @@ (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{:output-format :json}") _ (run-cli ["graph" "create" "--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) + _ (run-cli ["--repo" "linked-refs-graph" "upsert" "page" "--page" "TargetPage"] data-dir cfg-path) + _ (run-cli ["--repo" "linked-refs-graph" "upsert" "page" "--page" "SourcePage"] data-dir cfg-path) list-page-result (run-cli ["--repo" "linked-refs-graph" "list" "page" "--expand"] data-dir cfg-path) list-page-payload (parse-json-output list-page-result) @@ -1809,7 +1949,7 @@ (get-in list-page-payload [:data :items])) page-id (or (:db/id page-item) (:id page-item)) 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" + _ (run-cli ["--repo" "linked-refs-graph" "upsert" "block" "--target-page" "SourcePage" "--blocks" blocks-edn] data-dir cfg-path) show-result (run-cli ["--repo" "linked-refs-graph" "show" "--page" "TargetPage"] data-dir cfg-path) @@ -1842,8 +1982,8 @@ import-graph "import-edn-graph" export-path (node-path/join (node-helper/create-tmp-dir "exports") "graph.edn") _ (run-cli ["graph" "create" "--repo" export-graph] data-dir cfg-path) - _ (run-cli ["--repo" export-graph "add" "page" "--page" "ExportPage"] data-dir cfg-path) - _ (run-cli ["--repo" export-graph "add" "block" "--target-page-name" "ExportPage" "--content" "Export content"] data-dir cfg-path) + _ (run-cli ["--repo" export-graph "upsert" "page" "--page" "ExportPage"] data-dir cfg-path) + _ (run-cli ["--repo" export-graph "upsert" "block" "--target-page" "ExportPage" "--content" "Export content"] data-dir cfg-path) export-result (run-cli ["--repo" export-graph "graph" "export" "--type" "edn" @@ -1881,8 +2021,8 @@ import-graph "import-sqlite-graph" export-path (node-path/join (node-helper/create-tmp-dir "exports") "graph.sqlite") _ (run-cli ["graph" "create" "--repo" export-graph] data-dir cfg-path) - _ (run-cli ["--repo" export-graph "add" "page" "--page" "SQLiteExportPage"] data-dir cfg-path) - _ (run-cli ["--repo" export-graph "add" "block" "--target-page-name" "SQLiteExportPage" "--content" "SQLite export content"] data-dir cfg-path) + _ (run-cli ["--repo" export-graph "upsert" "page" "--page" "SQLiteExportPage"] data-dir cfg-path) + _ (run-cli ["--repo" export-graph "upsert" "block" "--target-page" "SQLiteExportPage" "--content" "SQLite export content"] data-dir cfg-path) export-result (run-cli ["--repo" export-graph "graph" "export" "--type" "sqlite"