044-logseq-cli-upsert-block-page.md

This commit is contained in:
rcmerci
2026-03-02 18:59:48 +08:00
parent 85ebeb976e
commit 67645700cc
11 changed files with 1385 additions and 582 deletions

View File

@@ -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 <name>` 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.
---

View File

@@ -94,10 +94,11 @@ Inspect and edit commands:
- `list page [--expand] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list pages
- `list tag [--expand] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list tags
- `list property [--expand] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list properties
- `add block --content <text> [--target-page-name <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - add blocks; defaults to todays journal page if no target is given
- `add block --blocks <edn> [--target-page-name <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - insert blocks via EDN vector
- `add block --blocks-file <path> [--target-page-name <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - insert blocks from an EDN file
- `add page --page <name>` - create a page
- `upsert block --content <text> [--target-page <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - create blocks; defaults to todays journal page if no target is given
- `upsert block --blocks <edn> [--target-page <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - insert blocks via EDN vector
- `upsert block --blocks-file <path> [--target-page <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - insert blocks from an EDN file
- `upsert block --id <id>|--uuid <uuid> [--target-id <id>|--target-uuid <uuid>|--target-page <name>] [--pos first-child|last-child|sibling] [--update-tags <edn-vector>] [--update-properties <edn-map>] [--remove-tags <edn-vector>] [--remove-properties <edn-vector>]` - update and/or move a block
- `upsert page --page <name> [--tags <edn-vector>] [--properties <edn-map>] [--update-tags <edn-vector>] [--update-properties <edn-map>] [--remove-tags <edn-vector>] [--remove-properties <edn-vector>]` - create or update a page
- `move --id <id>|--uuid <uuid> --target-id <id>|--target-uuid <uuid>|--target-page <name> [--pos first-child|last-child|sibling]` - move a block and its children (defaults to first-child)
- `remove --id <id>|--uuid <uuid>|--page <name>` - remove blocks (by db/id or UUID) or pages
- `search <query> [--type page|block|tag|property|all] [--tag <name>] [--case-sensitive] [--sort updated-at|created-at] [--order asc|desc]` - search across pages, blocks, tags, and properties (query is positional)
@@ -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 <query> [options] Search graph
@@ -138,15 +141,15 @@ Output formats:
- Global `--output <human|json|edn>` 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 <uuid> --target-page TargetPage
node ./dist/logseq.js search "hello"
node ./dist/logseq.js show --page TestPage --output json

View File

@@ -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}})))

View File

@@ -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]}]

View File

@@ -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)

View File

@@ -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 %))

View File

@@ -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)

View File

@@ -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))

View File

@@ -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

View File

@@ -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"

View File

@@ -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"