mirror of
https://github.com/logseq/logseq.git
synced 2026-05-14 16:02:31 +00:00
043-logseq-cli-tag-property-management.md
This commit is contained in:
182
docs/agent-guide/043-logseq-cli-tag-property-management.md
Normal file
182
docs/agent-guide/043-logseq-cli-tag-property-management.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Logseq CLI Tag and Property Management Implementation Plan
|
||||
|
||||
Goal: Add first class CLI support for `upsert tag`, `upsert property`, `remove tag`, and `remove property`, and restructure existing remove behavior into `remove block` and `remove page`.
|
||||
|
||||
Architecture: Replace current `add tag` command entry with `upsert tag` so tag creation and idempotent update semantics are unified under one verb.
|
||||
Architecture: Expand `remove` into typed subcommands (`block`, `page`, `tag`, `property`) to make deletion intent explicit and prevent mixed selector ambiguity.
|
||||
Architecture: Reuse `:thread-api/apply-outliner-ops` in db-worker-node so no new HTTP route or transport protocol is introduced.
|
||||
|
||||
Tech Stack: ClojureScript, babashka.cli, Promesa, Datascript, db-worker-node, outliner ops.
|
||||
|
||||
Related: Builds on docs/agent-guide/042-logseq-cli-add-tag-command.md and docs/agent-guide/029-logseq-cli-show-properties.md.
|
||||
|
||||
## Problem statement
|
||||
|
||||
Current CLI supports tag creation through `add tag` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs`, but does not expose an `upsert` verb for tags and properties.
|
||||
|
||||
Current `update` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/update.cljs` updates tag and property values on blocks, but does not upsert or remove tag/property entities.
|
||||
|
||||
Current `remove` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/remove.cljs` mixes block and page deletion under one command, and has no explicit tag/property deletion path.
|
||||
|
||||
db-worker-node already supports required mutation primitives through `:thread-api/apply-outliner-ops` in `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs` and outliner ops in `/Users/rcmerci/gh-repos/logseq/deps/outliner/src/logseq/outliner/op.cljs`.
|
||||
|
||||
The implementation gap is CLI command surface design, parser wiring, validation, and output contract coverage.
|
||||
|
||||
## Testing Plan
|
||||
|
||||
I will follow `@test-driven-development` and write failing parser, action, executor, formatter, and integration tests before adding behavior.
|
||||
|
||||
I will add parser tests for the new `upsert` verb and the new typed `remove` subcommands.
|
||||
|
||||
I will add action builder tests that verify repo propagation, normalized names, schema coercion, and typed action payloads for each new command.
|
||||
|
||||
I will add executor tests that stub transport and assert exact outliner ops emitted for each command path.
|
||||
|
||||
I will add formatter tests for human and json output so command responses are stable for scripts.
|
||||
|
||||
I will add integration tests against db-worker-node that verify graph state changes through `list`, `show`, and entity queries.
|
||||
|
||||
I will use `@clojure-debug` only when failures are caused by test setup rather than behavior.
|
||||
|
||||
NOTE: I will write *all* tests before I add any implementation behavior.
|
||||
|
||||
## Command contract
|
||||
|
||||
The final command surface is top level verbs without a `manage` group.
|
||||
|
||||
| Command | Required options | Optional options | Behavior |
|
||||
| --- | --- | --- | --- |
|
||||
| `logseq upsert tag` | `--name` | none in v1 | Creates tag when missing, returns existing tag when already present, and errors if same title exists as non-tag page. |
|
||||
| `logseq upsert property` | `--name` | `--type`, `--cardinality`, `--hide`, `--public` | Creates property when missing, updates schema for existing property, and validates type or cardinality compatibility. |
|
||||
| `logseq remove tag` | one of `--name`, `--id` | none | Deletes a tag entity after validating target type and removability, and fails with a candidate list when `--name` matches multiple tags. |
|
||||
| `logseq remove property` | one of `--name`, `--id` | none | Deletes a property entity after validating target type and removability, and fails with a candidate list when `--name` matches multiple properties. |
|
||||
| `logseq remove block` | one of `--id`, `--uuid` | none | Deletes one or more blocks with existing block remove behavior. |
|
||||
| `logseq remove page` | `--name` | none | Deletes a page with existing page remove behavior. |
|
||||
|
||||
`add tag` will be removed from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` and replaced by `upsert tag`.
|
||||
|
||||
Bare `remove` without a subcommand will be rejected with a clear parse error.
|
||||
|
||||
No compatibility aliases or deprecation shims will be kept for removed commands.
|
||||
|
||||
## Integration overview
|
||||
|
||||
```text
|
||||
logseq upsert property --name "owner" --type node --cardinality many
|
||||
-> /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs
|
||||
-> /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs
|
||||
-> /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/transport.cljs (/v1/invoke)
|
||||
-> /Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs (:thread-api/apply-outliner-ops)
|
||||
-> /Users/rcmerci/gh-repos/logseq/deps/outliner/src/logseq/outliner/op.cljs (:upsert-property)
|
||||
```
|
||||
|
||||
```text
|
||||
logseq remove tag --name "Quote"
|
||||
-> /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/remove.cljs
|
||||
-> remove tag resolver validates :logseq.class/Tag
|
||||
-> :thread-api/apply-outliner-ops with [:delete-page [tag-uuid]]
|
||||
-> outliner page delete flow applies class cleanup and persistence
|
||||
```
|
||||
|
||||
## Detailed implementation plan
|
||||
|
||||
1. Add failing parse help tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` that expect top level `upsert` and `remove` subcommands (`block`, `page`, `tag`, `property`).
|
||||
2. Add failing parse tests for `['upsert' 'tag' '--name' 'Quote']` and `['upsert' 'property' '--name' 'owner' '--type' 'node' '--cardinality' 'many']` in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
3. Add failing parse tests for `['remove' 'tag' '--name' 'Quote']` and `['remove' 'property' '--id' '123']` in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
4. Add failing parse tests for `['remove' 'block' '--id' '1']` and `['remove' 'page' '--name' 'Home']` to preserve old delete behavior in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
5. Add failing parse validation tests that reject bare `remove` and old `add tag` command usage in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
6. Add failing parse validation tests for invalid property type and cardinality values in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
7. Add failing build action tests for `upsert tag` name normalization and `#` prefix stripping in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
8. Add failing build action tests for `upsert property` schema coercion to `:logseq.property/type` and `:db/cardinality` in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
9. Add failing executor tests that `upsert tag` emits `[:create-page [name {:class? true}]]` only when the tag is missing in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
10. Add failing executor tests that `upsert tag` is idempotent for existing tag entities and rejects non-tag title conflicts in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
11. Add failing executor tests that `upsert property` emits `[:upsert-property [property-id schema opts]]` and passes `{:property-name name}` when creating a new property in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
12. Add failing executor tests that `remove tag` and `remove property` resolve entities by `--name` or `--id` and emit `[:delete-page [uuid]]` in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
13. Add failing executor tests that `remove tag --name` and `remove property --name` fail on multiple matches, return all matched candidates in output, and require rerun with `--id` in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
14. Add failing executor tests that built in or hidden tag or property targets are rejected with explicit error codes in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
15. Add failing formatter tests for `upsert tag`, `upsert property`, `remove tag`, and `remove property` outputs in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs`.
|
||||
16. Add failing formatter tests that `remove block` and `remove page` outputs remain backward compatible in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs`.
|
||||
17. Add failing integration tests for `upsert tag` create and idempotent behavior in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs`.
|
||||
18. Add failing integration tests for `upsert property` create and schema update behavior in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs`.
|
||||
19. Add failing integration tests for `remove tag` and `remove property` ensuring entities disappear from `list tag` and `list property` in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs`.
|
||||
20. Add failing integration tests for `remove tag --name` and `remove property --name` ambiguous matches to assert candidate list output and `--id` guidance in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs`.
|
||||
21. Add failing integration tests for `remove block` and `remove page` command migration behavior in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs`.
|
||||
22. Run focused tests and confirm all new tests fail for behavior reasons before implementation.
|
||||
23. Create `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs` with command specs, entries, validation helpers, action builders, and executors for tag and property upsert.
|
||||
24. Refactor shared tag resolver helpers from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` into `upsert.cljs` or a shared helper namespace to avoid duplication.
|
||||
25. Remove `add tag` command entry and related build or execute dispatch from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs`.
|
||||
26. Extend `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/remove.cljs` to register `remove block`, `remove page`, `remove tag`, and `remove property` entries.
|
||||
27. Keep existing block and page delete execution logic and map it behind `remove block` and `remove page` subcommands.
|
||||
28. Implement new remove tag and remove property resolver and execution paths in `remove.cljs` using `:delete-page` outliner op after entity type validation and ambiguity failure behavior for `--name`.
|
||||
29. Update `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` table, parse validation, action builder, context propagation, and execute dispatch for new upsert and remove contracts.
|
||||
30. Update `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` to format results for `upsert tag`, `upsert property`, `remove tag`, and `remove property`, including ambiguous candidate lists.
|
||||
31. Run focused unit and integration tests, then run `bb dev:lint-and-test`, and keep only behavior preserving refactors.
|
||||
32. Update CLI help text snapshots and any docs references to remove `add tag` and bare `remove` usage.
|
||||
|
||||
## Edge cases to cover
|
||||
|
||||
Tag names with leading `#` should normalize consistently in `upsert tag`.
|
||||
|
||||
Tag and property names that collide by title or case must fail resolution for `--name`, list all candidate matches, and require explicit `--id`.
|
||||
|
||||
Remove selectors must reject ambiguous name matches with a clear error, include all candidate ids and names in output, and require `--id`.
|
||||
|
||||
Built in tags and built in properties must not be removable.
|
||||
|
||||
Property type and cardinality updates must reject invalid transitions already enforced by outliner validation.
|
||||
|
||||
`remove block` multi id behavior must stay unchanged from current implementation.
|
||||
|
||||
`remove page` must preserve current not found and built in page deletion behavior.
|
||||
|
||||
Commands must reject blank names after trim.
|
||||
|
||||
## Verification commands
|
||||
|
||||
| Command | Expected result |
|
||||
| --- | --- |
|
||||
| `bb dev:test -v logseq.cli.commands-test/test-parse-args-help` | Help output includes `upsert` and typed `remove` subcommands. |
|
||||
| `bb dev:test -v logseq.cli.commands-test/test-verb-subcommand-parse-upsert-remove` | Parse and validation tests for new command contracts pass. |
|
||||
| `bb dev:test -v logseq.cli.commands-test/test-build-action-upsert-remove` | Action payload tests pass for all new paths. |
|
||||
| `bb dev:test -v logseq.cli.commands-test/test-execute-upsert-remove-tag-property` | Outliner op emission tests pass for upsert and entity removal flows. |
|
||||
| `bb dev:test -v logseq.cli.format-test/test-human-output-upsert-remove` | Human output formatting for new commands is stable. |
|
||||
| `bb dev:test -v logseq.cli.integration-test/test-cli-upsert-and-remove-tag-property` | End to end behavior through db-worker-node passes for all four new commands. |
|
||||
| `bb dev:test -v logseq.cli.integration-test/test-cli-remove-block-page-subcommands` | Block and page deletion still work through new remove subcommands. |
|
||||
| `bb dev:lint-and-test` | Full lint and unit suite pass. |
|
||||
|
||||
## Migration and compatibility
|
||||
|
||||
No db-worker-node route migration is required because this feature reuses `/v1/invoke` and existing thread-api methods.
|
||||
|
||||
No schema migration is required because tag and property entities are created and deleted through existing outliner operations.
|
||||
|
||||
This is a CLI breaking change because `add tag` is removed and bare `remove` is replaced by typed `remove block` and `remove page` commands.
|
||||
|
||||
This breaking change will be applied directly with no compatibility alias period.
|
||||
|
||||
## Testing Details
|
||||
|
||||
Tests validate parser, action, execution, formatter, and integration behavior for `upsert tag`, `upsert property`, `remove tag`, and `remove property`.
|
||||
|
||||
Tests also validate migration behavior for `remove block` and `remove page` so previous block and page deletion semantics are preserved.
|
||||
|
||||
Tests focus on command contracts and graph outcomes instead of helper internals.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- Add new upsert command module at `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs`.
|
||||
- Register upsert entries and dispatch hooks in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs`.
|
||||
- Remove `add tag` command entry and related dispatch from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs`.
|
||||
- Refactor remove command entries in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/remove.cljs` to `remove block`, `remove page`, `remove tag`, and `remove property`.
|
||||
- Reuse `:thread-api/apply-outliner-ops` with `:create-page`, `:upsert-property`, and `:delete-page` for all entity mutations.
|
||||
- Add formatter branches in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` for new commands.
|
||||
- Add parser and executor unit tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`.
|
||||
- Add formatter tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs`.
|
||||
- Add end to end coverage in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs`.
|
||||
- Use `@clojure-debug` only when failures indicate fixture or async wiring issues.
|
||||
|
||||
## Question
|
||||
|
||||
No open questions.
|
||||
|
||||
---
|
||||
@@ -34,13 +34,9 @@
|
||||
: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 ^:private add-tag-spec
|
||||
{:name {:desc "Tag name"}})
|
||||
|
||||
(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)
|
||||
(core/command-entry ["add" "tag"] :add-tag "Create tag" add-tag-spec)])
|
||||
(core/command-entry ["add" "page"] :add-page "Create page" add-page-spec)])
|
||||
|
||||
(defn- today-page-title
|
||||
[config repo]
|
||||
@@ -1011,43 +1007,6 @@
|
||||
:error {:code :missing-page-name
|
||||
:message "page name is required"}}))))
|
||||
|
||||
(defn- normalize-tag-name-option
|
||||
[value]
|
||||
(let [normalized (normalize-tag-value value)]
|
||||
(when (string? normalized)
|
||||
(let [name (string/trim normalized)]
|
||||
(when (seq name)
|
||||
name)))))
|
||||
|
||||
(defn build-add-tag-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for add"}}
|
||||
(let [name (normalize-tag-name-option (:name options))]
|
||||
(if (seq name)
|
||||
{:ok? true
|
||||
:action {:type :add-tag
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:name name}}
|
||||
{:ok? false
|
||||
:error {:code :missing-tag-name
|
||||
:message "tag name is required"}}))))
|
||||
|
||||
(defn- pull-page-by-name
|
||||
[config repo page-name]
|
||||
(pull-entity config repo
|
||||
[:db/id :block/name :block/title :block/uuid
|
||||
{:block/tags [:db/id :db/ident :block/name :block/title]}]
|
||||
[:block/name (common-util/page-name-sanity-lc page-name)]))
|
||||
|
||||
(defn- tag-entity?
|
||||
[entity]
|
||||
(some #(= :logseq.class/Tag (:db/ident %))
|
||||
(:block/tags entity)))
|
||||
|
||||
(defn execute-add-block
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
@@ -1134,33 +1093,3 @@
|
||||
created-ids (resolve-created-page-ids cfg (:repo action) (:page action) create-result)]
|
||||
{:status :ok
|
||||
:data {:result created-ids}})))
|
||||
|
||||
(defn execute-add-tag
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
existing (pull-page-by-name cfg (:repo action) (:name action))
|
||||
existing-id (:db/id existing)
|
||||
_ (when (and existing-id (not (tag-entity? existing)))
|
||||
(throw (ex-info "tag already exists as a page and is not a tag"
|
||||
{:code :tag-name-conflict
|
||||
:name (:name action)})))
|
||||
_ (when-not existing-id
|
||||
(transport/invoke cfg :thread-api/apply-outliner-ops false
|
||||
[(:repo action)
|
||||
[[:create-page [(:name action) {:class? true}]]]
|
||||
{}]))
|
||||
page (or (when existing-id existing)
|
||||
(pull-page-by-name cfg (:repo action) (:name action)))
|
||||
page-id (:db/id page)
|
||||
_ (when-not page-id
|
||||
(throw (ex-info "tag not found after create"
|
||||
{:code :tag-not-found
|
||||
:name (:name action)})))
|
||||
_ (when-not (tag-entity? page)
|
||||
(throw (ex-info "created entity is not tagged as :logseq.class/Tag"
|
||||
{:code :tag-create-not-tag
|
||||
:name (:name action)
|
||||
:id page-id})))
|
||||
created-ids (normalize-created-ids [page-id])]
|
||||
{:status :ok
|
||||
:data {:result created-ids}})))
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
(defn top-level-summary
|
||||
[table]
|
||||
(let [groups [{:title "Graph Inspect and Edit"
|
||||
:commands #{"list" "add" "remove" "update" "query" "show"}}
|
||||
:commands #{"list" "add" "upsert" "remove" "update" "query" "show"}}
|
||||
{:title "Graph Management"
|
||||
:commands #{"graph" "server" "doctor"}}]
|
||||
render-group (fn [{:keys [title commands]}]
|
||||
|
||||
@@ -8,27 +8,66 @@
|
||||
[logseq.common.util :as common-util]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private remove-spec
|
||||
(def ^:private remove-block-spec
|
||||
{:id {:desc "Block db/id or EDN vector of ids"}
|
||||
:uuid {:desc "Block UUID"}
|
||||
:page {:desc "Page name"}})
|
||||
:uuid {:desc "Block UUID"}})
|
||||
|
||||
(def ^:private remove-page-spec
|
||||
{:name {:desc "Page name"}})
|
||||
|
||||
(def ^:private remove-entity-spec
|
||||
{:id {:desc "Entity db/id"
|
||||
:coerce :long}
|
||||
:name {:desc "Entity name"}})
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["remove"] :remove "Remove blocks or pages" remove-spec)])
|
||||
[(core/command-entry ["remove" "block"] :remove-block "Remove blocks" remove-block-spec)
|
||||
(core/command-entry ["remove" "page"] :remove-page "Remove page" remove-page-spec)
|
||||
(core/command-entry ["remove" "tag"] :remove-tag "Remove tag" remove-entity-spec)
|
||||
(core/command-entry ["remove" "property"] :remove-property "Remove property" remove-entity-spec)])
|
||||
|
||||
(defn invalid-options?
|
||||
[opts]
|
||||
(let [id-result (id-command/parse-id-option (:id opts))]
|
||||
(cond
|
||||
(and (some? (:id opts)) (not (:ok? id-result)))
|
||||
(:message id-result)
|
||||
[command opts]
|
||||
(case command
|
||||
:remove-block
|
||||
(let [id-result (id-command/parse-id-option (:id opts))
|
||||
selectors (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])]
|
||||
(cond
|
||||
(and (some? (:id opts)) (not (:ok? id-result)))
|
||||
(:message id-result)
|
||||
|
||||
:else
|
||||
nil)))
|
||||
(> (count selectors) 1)
|
||||
"only one of --id or --uuid is allowed"
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
(:remove-tag :remove-property)
|
||||
(let [name (some-> (:name opts) string/trim)
|
||||
selectors (filter some? [(:id opts) name])]
|
||||
(cond
|
||||
(> (count selectors) 1)
|
||||
"only one of --id or --name is allowed"
|
||||
|
||||
(and (contains? opts :name) (string/blank? (or (:name opts) "")))
|
||||
"name must be non-empty"
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
nil))
|
||||
|
||||
(def ^:private block-id-selector
|
||||
[:db/id :block/uuid])
|
||||
|
||||
(def ^:private page-id-selector
|
||||
[:db/id :block/uuid :block/name :block/title])
|
||||
|
||||
(def ^:private entity-selector
|
||||
[:db/id :db/ident :block/uuid :block/name :block/title
|
||||
:logseq.property/type :logseq.property/public? :logseq.property/built-in?
|
||||
{:block/tags [:db/id :db/ident :block/title :block/name]}])
|
||||
|
||||
(defn- fetch-block-by-id
|
||||
[config repo id]
|
||||
(transport/invoke config :thread-api/pull false
|
||||
@@ -48,6 +87,11 @@
|
||||
(transport/invoke config :thread-api/apply-outliner-ops false
|
||||
[repo [[:delete-blocks [ids {}]]] {}]))
|
||||
|
||||
(defn- delete-page-by-uuid
|
||||
[config repo page-uuid]
|
||||
(transport/invoke config :thread-api/apply-outliner-ops false
|
||||
[repo [[:delete-page [page-uuid]]] {}]))
|
||||
|
||||
(defn- remove-block-id
|
||||
[config repo id]
|
||||
(p/let [entity (fetch-block-by-id config repo id)]
|
||||
@@ -74,8 +118,8 @@
|
||||
:missing-ids missing-ids
|
||||
:result result}))
|
||||
|
||||
(defn- perform-remove
|
||||
[config {:keys [repo ids multi-id? uuid page]}]
|
||||
(defn- perform-remove-block
|
||||
[config {:keys [repo ids multi-id? uuid]}]
|
||||
(cond
|
||||
(and (seq ids) multi-id?)
|
||||
(remove-block-ids-best-effort config repo ids)
|
||||
@@ -91,59 +135,288 @@
|
||||
(delete-block-ids config repo [id])
|
||||
(throw (ex-info "block not found" {:code :block-not-found})))))
|
||||
|
||||
(seq page)
|
||||
(p/let [entity (transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id :block/uuid] [:block/name page]])]
|
||||
(if-let [page-uuid (:block/uuid entity)]
|
||||
(transport/invoke config :thread-api/apply-outliner-ops false
|
||||
[repo [[:delete-page [page-uuid]]] {}])
|
||||
(throw (ex-info "page not found" {:code :page-not-found}))))
|
||||
:else
|
||||
(p/rejected (ex-info "block is required" {:code :missing-target}))))
|
||||
|
||||
(defn- resolve-page-by-name
|
||||
[config repo name]
|
||||
(transport/invoke config :thread-api/pull false
|
||||
[repo page-id-selector [:block/name (common-util/page-name-sanity-lc name)]]))
|
||||
|
||||
(defn- item-id
|
||||
[item]
|
||||
(or (:db/id item) (:id item)))
|
||||
|
||||
(defn- item-name
|
||||
[item]
|
||||
(or (:block/title item) (:title item) (:name item) (:block/name item)))
|
||||
|
||||
(defn- normalize-name
|
||||
[value]
|
||||
(common-util/page-name-sanity-lc (or value "")))
|
||||
|
||||
(defn- tag-entity?
|
||||
[entity]
|
||||
(some #(= :logseq.class/Tag (:db/ident %))
|
||||
(:block/tags entity)))
|
||||
|
||||
(defn- property-entity?
|
||||
[entity]
|
||||
(some? (:logseq.property/type entity)))
|
||||
|
||||
(defn- list-matches-by-name
|
||||
[config repo method name]
|
||||
(let [normalized (normalize-name name)]
|
||||
(p/let [items (transport/invoke config method false [repo {:include-built-in true :expand true}])
|
||||
matches (->> (or items [])
|
||||
(filter (fn [item]
|
||||
(= normalized (normalize-name (item-name item)))))
|
||||
vec)]
|
||||
matches)))
|
||||
|
||||
(defn- ambiguous-error
|
||||
[code label name matches]
|
||||
(let [candidates (->> matches
|
||||
(map (fn [item]
|
||||
{:id (item-id item)
|
||||
:name (item-name item)}))
|
||||
(filter :id)
|
||||
vec)]
|
||||
{:code code
|
||||
:message (str "multiple " label "s match name: " name "; rerun with --id")
|
||||
:candidates candidates}))
|
||||
|
||||
(defn- resolve-target
|
||||
[config repo {:keys [id name]} {:keys [list-method not-found-code ambiguous-code label]}]
|
||||
(cond
|
||||
(some? id)
|
||||
(p/resolved {:ok? true
|
||||
:lookup id
|
||||
:id id})
|
||||
|
||||
(seq name)
|
||||
(p/let [matches (list-matches-by-name config repo list-method name)]
|
||||
(cond
|
||||
(empty? matches)
|
||||
{:ok? false
|
||||
:error {:code not-found-code
|
||||
:message (str label " not found")}}
|
||||
|
||||
(> (count matches) 1)
|
||||
{:ok? false
|
||||
:error (ambiguous-error ambiguous-code label name matches)}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:lookup [:block/name (normalize-name (or (item-name (first matches)) name))]
|
||||
:id (item-id (first matches))
|
||||
:name (item-name (first matches))}))
|
||||
|
||||
:else
|
||||
(p/rejected (ex-info "block or page required" {:code :missing-target}))))
|
||||
(p/resolved {:ok? false
|
||||
:error {:code :missing-target
|
||||
:message (str label " name or id is required")}})))
|
||||
|
||||
(defn- validate-tag-target
|
||||
[entity]
|
||||
(cond
|
||||
(nil? (:db/id entity))
|
||||
{:ok? false
|
||||
:error {:code :tag-not-found
|
||||
:message "tag not found"}}
|
||||
|
||||
(not (tag-entity? entity))
|
||||
{:ok? false
|
||||
:error {:code :invalid-tag-target
|
||||
:message "target is not a tag"}}
|
||||
|
||||
(true? (:logseq.property/built-in? entity))
|
||||
{:ok? false
|
||||
:error {:code :tag-built-in
|
||||
:message "built-in tag cannot be removed"}}
|
||||
|
||||
(false? (:logseq.property/public? entity))
|
||||
{:ok? false
|
||||
:error {:code :tag-hidden
|
||||
:message "hidden tag cannot be removed"}}
|
||||
|
||||
(nil? (:block/uuid entity))
|
||||
{:ok? false
|
||||
:error {:code :tag-not-found
|
||||
:message "tag uuid not found"}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:entity entity}))
|
||||
|
||||
(defn- validate-property-target
|
||||
[entity]
|
||||
(cond
|
||||
(nil? (:db/id entity))
|
||||
{:ok? false
|
||||
:error {:code :property-not-found
|
||||
:message "property not found"}}
|
||||
|
||||
(not (property-entity? entity))
|
||||
{:ok? false
|
||||
:error {:code :invalid-property-target
|
||||
:message "target is not a property"}}
|
||||
|
||||
(true? (:logseq.property/built-in? entity))
|
||||
{:ok? false
|
||||
:error {:code :property-built-in
|
||||
:message "built-in property cannot be removed"}}
|
||||
|
||||
(false? (:logseq.property/public? entity))
|
||||
{:ok? false
|
||||
:error {:code :property-hidden
|
||||
:message "hidden property cannot be removed"}}
|
||||
|
||||
(nil? (:block/uuid entity))
|
||||
{:ok? false
|
||||
:error {:code :property-not-found
|
||||
:message "property uuid not found"}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:entity entity}))
|
||||
|
||||
(defn- perform-remove-entity
|
||||
[config action {:keys [list-method not-found-code ambiguous-code label validate-fn]}]
|
||||
(p/let [resolved (resolve-target config (:repo action) action
|
||||
{:list-method list-method
|
||||
:not-found-code not-found-code
|
||||
:ambiguous-code ambiguous-code
|
||||
:label label})]
|
||||
(if-not (:ok? resolved)
|
||||
{:status :error
|
||||
:error (:error resolved)}
|
||||
(p/let [entity (transport/invoke config :thread-api/pull false
|
||||
[(:repo action) entity-selector (:lookup resolved)])
|
||||
validation (validate-fn entity)]
|
||||
(if-not (:ok? validation)
|
||||
{:status :error
|
||||
:error (:error validation)}
|
||||
(p/let [result (delete-page-by-uuid config (:repo action) (:block/uuid entity))]
|
||||
{:status :ok
|
||||
:data {:result result
|
||||
:id (:db/id entity)
|
||||
:name (or (:name resolved) (:block/title entity) (:block/name entity))}}))))))
|
||||
|
||||
(defn build-action
|
||||
[options repo]
|
||||
[command options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for remove"}}
|
||||
(let [id-result (id-command/parse-id-option (:id options))
|
||||
ids (:value id-result)
|
||||
multi-id? (:multi? id-result)
|
||||
uuid (some-> (:uuid options) string/trim)
|
||||
page (some-> (:page options) string/trim)
|
||||
selectors (filter some? [(:id options) uuid page])]
|
||||
(cond
|
||||
(empty? selectors)
|
||||
{:ok? false
|
||||
:error {:code :missing-target
|
||||
:message "block or page is required"}}
|
||||
(case command
|
||||
:remove-block
|
||||
(let [id-result (id-command/parse-id-option (:id options))
|
||||
ids (:value id-result)
|
||||
multi-id? (:multi? id-result)
|
||||
uuid (some-> (:uuid options) string/trim)
|
||||
selectors (filter some? [(:id options) uuid])]
|
||||
(cond
|
||||
(empty? selectors)
|
||||
{:ok? false
|
||||
:error {:code :missing-target
|
||||
:message "block is required"}}
|
||||
|
||||
(> (count selectors) 1)
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message "only one of --id, --uuid, or --page is allowed"}}
|
||||
(> (count selectors) 1)
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message "only one of --id or --uuid is allowed"}}
|
||||
|
||||
(and (some? (:id options)) (not (:ok? id-result)))
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message (:message id-result)}}
|
||||
(and (some? (:id options)) (not (:ok? id-result)))
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message (:message id-result)}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:action {:type :remove
|
||||
:repo repo
|
||||
:id (when (and (seq ids) (not multi-id?)) (first ids))
|
||||
:ids ids
|
||||
:multi-id? multi-id?
|
||||
:uuid uuid
|
||||
:page page}}))))
|
||||
:else
|
||||
{:ok? true
|
||||
:action {:type :remove-block
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:id (when (and (seq ids) (not multi-id?)) (first ids))
|
||||
:ids ids
|
||||
:multi-id? multi-id?
|
||||
:uuid uuid}}))
|
||||
|
||||
(defn execute-remove
|
||||
:remove-page
|
||||
(let [name (some-> (:name options) string/trim)]
|
||||
(if (seq name)
|
||||
{:ok? true
|
||||
:action {:type :remove-page
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:name name}}
|
||||
{:ok? false
|
||||
:error {:code :missing-page-name
|
||||
:message "page name is required"}}))
|
||||
|
||||
(:remove-tag :remove-property)
|
||||
(let [name (some-> (:name options) string/trim)
|
||||
id (:id options)
|
||||
selectors (filter some? [id name])]
|
||||
(cond
|
||||
(empty? selectors)
|
||||
{:ok? false
|
||||
:error {:code :missing-target
|
||||
:message "name or id is required"}}
|
||||
|
||||
(> (count selectors) 1)
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message "only one of --id or --name is allowed"}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:action {:type command
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:id id
|
||||
:name name}}))
|
||||
|
||||
{:ok? false
|
||||
:error {:code :unknown-command
|
||||
:message (str "unknown remove command: " command)}})))
|
||||
|
||||
(defn execute-remove-block
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
result (perform-remove cfg action)]
|
||||
result (perform-remove-block cfg action)]
|
||||
{:status :ok
|
||||
:data (cond-> {:result result}
|
||||
(map? result) (merge (dissoc result :result)))})))
|
||||
|
||||
(defn execute-remove-page
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
entity (resolve-page-by-name cfg (:repo action) (:name action))]
|
||||
(if-let [page-uuid (:block/uuid entity)]
|
||||
(p/let [result (delete-page-by-uuid cfg (:repo action) page-uuid)]
|
||||
{:status :ok
|
||||
:data {:result result}})
|
||||
{:status :error
|
||||
:error {:code :page-not-found
|
||||
:message "page not found"}}))))
|
||||
|
||||
(defn execute-remove-tag
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))]
|
||||
(perform-remove-entity cfg action
|
||||
{:list-method :thread-api/api-list-tags
|
||||
:not-found-code :tag-not-found
|
||||
:ambiguous-code :ambiguous-tag-name
|
||||
:label "tag"
|
||||
:validate-fn validate-tag-target}))))
|
||||
|
||||
(defn execute-remove-property
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))]
|
||||
(perform-remove-entity cfg action
|
||||
{:list-method :thread-api/api-list-properties
|
||||
:not-found-code :property-not-found
|
||||
:ambiguous-code :ambiguous-property-name
|
||||
:label "property"
|
||||
:validate-fn validate-property-target}))))
|
||||
|
||||
227
src/main/logseq/cli/command/upsert.cljs
Normal file
227
src/main/logseq/cli/command/upsert.cljs
Normal file
@@ -0,0 +1,227 @@
|
||||
(ns logseq.cli.command.upsert
|
||||
"Upsert-related CLI commands."
|
||||
(:require [clojure.string :as string]
|
||||
[logseq.cli.command.core :as core]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.transport :as transport]
|
||||
[logseq.common.util :as common-util]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private upsert-tag-spec
|
||||
{:name {:desc "Tag name"}})
|
||||
|
||||
(def ^:private upsert-property-spec
|
||||
{:name {:desc "Property name"}
|
||||
:type {:desc "Property type (default, number, date, datetime, checkbox, url, node, json, string)"}
|
||||
:cardinality {:desc "Property cardinality (one, many)"}
|
||||
:hide {:desc "Hide property"
|
||||
:coerce :boolean}
|
||||
:public {:desc "Set property public visibility"
|
||||
:coerce :boolean}})
|
||||
|
||||
(def entries
|
||||
[(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
|
||||
#{"default" "number" "date" "datetime" "checkbox" "url" "node" "json" "string"})
|
||||
|
||||
(def ^:private property-cardinalities
|
||||
#{"one" "many"})
|
||||
|
||||
(defn- normalize-tag-name
|
||||
[value]
|
||||
(let [text (some-> value string/trim (string/replace #"^#+" ""))]
|
||||
(when (seq text)
|
||||
text)))
|
||||
|
||||
(defn- normalize-property-name
|
||||
[value]
|
||||
(let [text (some-> value string/trim)]
|
||||
(when (seq text)
|
||||
text)))
|
||||
|
||||
(defn- normalize-property-type
|
||||
[value]
|
||||
(some-> value string/trim string/lower-case))
|
||||
|
||||
(defn- normalize-property-cardinality
|
||||
[value]
|
||||
(let [v (some-> value string/trim string/lower-case)]
|
||||
(case v
|
||||
"db.cardinality/one" "one"
|
||||
"db.cardinality/many" "many"
|
||||
v)))
|
||||
|
||||
(defn invalid-options?
|
||||
[command opts]
|
||||
(case command
|
||||
:upsert-property
|
||||
(let [type' (normalize-property-type (:type opts))
|
||||
cardinality' (normalize-property-cardinality (:cardinality opts))]
|
||||
(cond
|
||||
(and (seq (:type opts)) (not (contains? property-types type')))
|
||||
(str "invalid type: " (:type opts))
|
||||
|
||||
(and (seq (:cardinality opts)) (not (contains? property-cardinalities cardinality')))
|
||||
(str "invalid cardinality: " (:cardinality opts))
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
nil))
|
||||
|
||||
(defn build-tag-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for upsert"}}
|
||||
(let [name (normalize-tag-name (:name options))]
|
||||
(if (seq name)
|
||||
{:ok? true
|
||||
:action {:type :upsert-tag
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:name name}}
|
||||
{:ok? false
|
||||
:error {:code :missing-tag-name
|
||||
:message "tag name is required"}}))))
|
||||
|
||||
(defn- cardinality->db
|
||||
[value]
|
||||
(when-let [v (normalize-property-cardinality value)]
|
||||
(case v
|
||||
"many" :db.cardinality/many
|
||||
"one" :db.cardinality/one
|
||||
nil)))
|
||||
|
||||
(defn- property-schema
|
||||
[options]
|
||||
(cond-> {}
|
||||
(seq (:type options))
|
||||
(assoc :logseq.property/type (keyword (normalize-property-type (:type options))))
|
||||
|
||||
(seq (:cardinality options))
|
||||
(assoc :db/cardinality (cardinality->db (:cardinality options)))
|
||||
|
||||
(contains? options :hide)
|
||||
(assoc :logseq.property/hide? (boolean (:hide options)))
|
||||
|
||||
(contains? options :public)
|
||||
(assoc :logseq.property/public? (boolean (:public options)))))
|
||||
|
||||
(defn build-property-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for upsert"}}
|
||||
(let [name (normalize-property-name (:name options))
|
||||
invalid-message (invalid-options? :upsert-property options)]
|
||||
(cond
|
||||
(not (seq name))
|
||||
{:ok? false
|
||||
:error {:code :missing-property-name
|
||||
:message "property name is required"}}
|
||||
|
||||
(seq invalid-message)
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message invalid-message}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:action {:type :upsert-property
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:name name
|
||||
:schema (property-schema options)}}))))
|
||||
|
||||
(defn- pull-page-by-name
|
||||
[config repo page-name selector]
|
||||
(transport/invoke config :thread-api/pull false
|
||||
[repo selector [:block/name (common-util/page-name-sanity-lc page-name)]]))
|
||||
|
||||
(defn- tag-entity?
|
||||
[entity]
|
||||
(some #(= :logseq.class/Tag (:db/ident %))
|
||||
(:block/tags entity)))
|
||||
|
||||
(defn execute-upsert-tag
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
existing (pull-page-by-name cfg (:repo action) (:name action)
|
||||
[:db/id :block/name :block/title
|
||||
{:block/tags [:db/ident]}])
|
||||
existing-id (:db/id existing)]
|
||||
(cond
|
||||
(and existing-id (not (tag-entity? existing)))
|
||||
{:status :error
|
||||
:error {:code :tag-name-conflict
|
||||
:message "tag already exists as a page and is not a tag"}}
|
||||
|
||||
:else
|
||||
(p/let [_ (when-not existing-id
|
||||
(transport/invoke cfg :thread-api/apply-outliner-ops false
|
||||
[(:repo action)
|
||||
[[:create-page [(:name action) {:class? true}]]]
|
||||
{}]))
|
||||
page (or (when existing-id existing)
|
||||
(pull-page-by-name cfg (:repo action) (:name action)
|
||||
[:db/id :block/name :block/title
|
||||
{:block/tags [:db/ident]}]))
|
||||
page-id (:db/id page)]
|
||||
(cond
|
||||
(not page-id)
|
||||
{:status :error
|
||||
:error {:code :tag-not-found
|
||||
:message "tag not found after upsert"}}
|
||||
|
||||
(not (tag-entity? page))
|
||||
{:status :error
|
||||
:error {:code :tag-create-not-tag
|
||||
:message "created entity is not tagged as :logseq.class/Tag"}}
|
||||
|
||||
:else
|
||||
{:status :ok
|
||||
:data {:result [page-id]}}))))))
|
||||
|
||||
(def ^:private property-selector
|
||||
[:db/id :db/ident :block/name :block/title :logseq.property/type])
|
||||
|
||||
(defn- property-entity?
|
||||
[entity]
|
||||
(some? (:logseq.property/type entity)))
|
||||
|
||||
(defn execute-upsert-property
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
existing (pull-page-by-name cfg (:repo action) (:name action) property-selector)
|
||||
existing-id (:db/id existing)]
|
||||
(cond
|
||||
(and existing-id (not (property-entity? existing)))
|
||||
{:status :error
|
||||
:error {:code :property-name-conflict
|
||||
:message "property already exists as a page and is not a property"}}
|
||||
|
||||
:else
|
||||
(p/let [property-ident (when (property-entity? existing)
|
||||
(:db/ident existing))
|
||||
property-opts (cond-> {}
|
||||
(nil? property-ident)
|
||||
(assoc :property-name (:name action)))
|
||||
_ (transport/invoke cfg :thread-api/apply-outliner-ops false
|
||||
[(:repo action)
|
||||
[[:upsert-property [property-ident
|
||||
(:schema action)
|
||||
property-opts]]]
|
||||
{}])
|
||||
property (pull-page-by-name cfg (:repo action) (:name action) property-selector)
|
||||
property-id (:db/id property)]
|
||||
(if property-id
|
||||
{:status :ok
|
||||
:data {:result [property-id]}}
|
||||
{:status :error
|
||||
:error {:code :property-not-found
|
||||
:message "property not found after upsert"}}))))))
|
||||
@@ -11,6 +11,7 @@
|
||||
[logseq.cli.command.remove :as remove-command]
|
||||
[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]))
|
||||
@@ -69,6 +70,13 @@
|
||||
:message "tag name is required"}
|
||||
:summary summary})
|
||||
|
||||
(defn- missing-property-name-result
|
||||
[summary]
|
||||
{:ok? false
|
||||
:error {:code :missing-property-name
|
||||
:message "property name is required"}
|
||||
:summary summary})
|
||||
|
||||
(defn- missing-type-result
|
||||
[summary]
|
||||
{:ok? false
|
||||
@@ -106,6 +114,7 @@
|
||||
server-command/entries
|
||||
list-command/entries
|
||||
add-command/entries
|
||||
upsert-command/entries
|
||||
remove-command/entries
|
||||
update-command/entries
|
||||
query-command/entries
|
||||
@@ -153,9 +162,6 @@
|
||||
(seq (:blocks-file opts))
|
||||
has-args?)
|
||||
show-targets (filter some? [(:id opts) (:uuid opts) (:page opts)])
|
||||
remove-targets (filter some? [(:id opts)
|
||||
(some-> (:uuid opts) string/trim)
|
||||
(some-> (:page opts) string/trim)])
|
||||
update-sources (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])]
|
||||
(cond
|
||||
(:help opts)
|
||||
@@ -174,17 +180,24 @@
|
||||
(and (= command :add-page) (not (seq (:page opts))))
|
||||
(missing-page-name-result summary)
|
||||
|
||||
(and (= command :add-tag) (not (seq (some-> (:name opts) string/trim))))
|
||||
(and (= command :upsert-tag) (not (seq (some-> (:name opts) string/trim))))
|
||||
(missing-tag-name-result summary)
|
||||
|
||||
(and (= command :remove) (seq args))
|
||||
(command-core/invalid-options-result summary "remove does not accept subcommands")
|
||||
(and (= command :upsert-property) (not (seq (some-> (:name opts) string/trim))))
|
||||
(missing-property-name-result summary)
|
||||
|
||||
(and (= command :remove) (empty? remove-targets))
|
||||
(and (= command :upsert-property) (upsert-command/invalid-options? command opts))
|
||||
(command-core/invalid-options-result summary (upsert-command/invalid-options? command opts))
|
||||
|
||||
(and (= command :remove-block) (empty? (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])))
|
||||
(missing-target-result summary)
|
||||
|
||||
(and (= command :remove) (> (count remove-targets) 1))
|
||||
(command-core/invalid-options-result summary "only one of --id, --uuid, or --page is allowed")
|
||||
(and (= command :remove-page) (not (seq (some-> (:name opts) string/trim))))
|
||||
(missing-page-name-result summary)
|
||||
|
||||
(and (#{:remove-tag :remove-property} command)
|
||||
(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))
|
||||
@@ -207,8 +220,9 @@
|
||||
(list-command/invalid-options? command opts))
|
||||
(command-core/invalid-options-result summary (list-command/invalid-options? command opts))
|
||||
|
||||
(and (= command :remove) (remove-command/invalid-options? opts))
|
||||
(command-core/invalid-options-result summary (remove-command/invalid-options? opts))
|
||||
(and (#{:remove-block :remove-page :remove-tag :remove-property} command)
|
||||
(remove-command/invalid-options? command opts))
|
||||
(command-core/invalid-options-result summary (remove-command/invalid-options? command opts))
|
||||
|
||||
(and (= command :show) (show-command/invalid-options? opts))
|
||||
(command-core/invalid-options-result summary (show-command/invalid-options? opts))
|
||||
@@ -268,7 +282,7 @@
|
||||
:message "missing command"}
|
||||
:summary summary})
|
||||
|
||||
(and (= 1 (count args)) (#{"graph" "server" "list" "add" "query"} (first args)))
|
||||
(and (= 1 (count args)) (#{"graph" "server" "list" "add" "upsert" "remove" "query"} (first args)))
|
||||
(command-core/help-result (command-core/group-summary (first args) table))
|
||||
|
||||
:else
|
||||
@@ -372,14 +386,17 @@
|
||||
:add-page
|
||||
(add-command/build-add-page-action options repo)
|
||||
|
||||
:add-tag
|
||||
(add-command/build-add-tag-action options repo)
|
||||
:upsert-tag
|
||||
(upsert-command/build-tag-action options repo)
|
||||
|
||||
:upsert-property
|
||||
(upsert-command/build-property-action options repo)
|
||||
|
||||
:update-block
|
||||
(update-command/build-action options repo)
|
||||
|
||||
:remove
|
||||
(remove-command/build-action options repo)
|
||||
(:remove-block :remove-page :remove-tag :remove-property)
|
||||
(remove-command/build-action command options repo)
|
||||
|
||||
:query
|
||||
(query-command/build-action options repo config)
|
||||
@@ -423,9 +440,13 @@
|
||||
: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)
|
||||
:add-tag (add-command/execute-add-tag 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 (remove-command/execute-remove 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)
|
||||
:remove-property (remove-command/execute-remove-property action config)
|
||||
:query (query-command/execute-query action config)
|
||||
:query-list (query-command/execute-query-list action config)
|
||||
:show (show-command/execute-show action config)
|
||||
@@ -440,8 +461,8 @@
|
||||
:message "unknown action"}}))]
|
||||
(assoc result
|
||||
:command (or (:command action) (:type action))
|
||||
:context (select-keys action [:repo :graph :page :id :ids :uuid :block :blocks
|
||||
:name
|
||||
:context (select-keys action [:repo :graph :page :name :id :ids :uuid :block :blocks
|
||||
:schema
|
||||
:source :target :update-tags :update-properties
|
||||
:remove-tags :remove-properties
|
||||
:export-type :output :import-type :input])))))
|
||||
|
||||
@@ -93,17 +93,30 @@
|
||||
:missing-tag-name "Use --name <tag-name>"
|
||||
:missing-query "Use --query <edn>"
|
||||
:unknown-query "Use `logseq query list` to see available queries"
|
||||
:ambiguous-tag-name "Retry with --id <tag-id>"
|
||||
:ambiguous-property-name "Retry with --id <property-id>"
|
||||
:data-dir-permission "Check filesystem permissions or set LOGSEQ_CLI_DATA_DIR"
|
||||
:server-owned-by-other "Retry from the process owner that started the server"
|
||||
:server-start-timeout-orphan "Check and stop lingering db-worker-node processes, then retry"
|
||||
nil))
|
||||
|
||||
(defn- format-candidates
|
||||
[candidates]
|
||||
(when (seq candidates)
|
||||
(str "\nCandidates:\n"
|
||||
(string/join "\n"
|
||||
(map (fn [{:keys [id name]}]
|
||||
(str " " id " " (or name "-")))
|
||||
candidates)))))
|
||||
|
||||
(defn- format-error
|
||||
[error]
|
||||
(let [{:keys [code message]} error
|
||||
(let [{:keys [code message candidates]} error
|
||||
hint (error-hint error)
|
||||
message* (style/bold-keywords message ["option" "command" "argument"])]
|
||||
message* (style/bold-keywords message ["option" "command" "argument"])
|
||||
candidates* (format-candidates candidates)]
|
||||
(cond-> (str "Error (" (name (or code :error)) "): " message*)
|
||||
candidates* (str candidates*)
|
||||
hint (str "\nHint: " hint))))
|
||||
|
||||
(defn- maybe-ident-header
|
||||
@@ -248,14 +261,37 @@
|
||||
[_context ids]
|
||||
(str "Added tag:\n" (pr-str (vec (or ids [])))))
|
||||
|
||||
(defn- format-remove
|
||||
[{:keys [repo page uuid id ids]}]
|
||||
(defn- format-upsert-tag
|
||||
[_context ids]
|
||||
(str "Upserted tag:\n" (pr-str (vec (or ids [])))))
|
||||
|
||||
(defn- format-upsert-property
|
||||
[_context ids]
|
||||
(str "Upserted property:\n" (pr-str (vec (or ids [])))))
|
||||
|
||||
(defn- format-remove-block
|
||||
[{:keys [repo uuid id ids]}]
|
||||
(cond
|
||||
(seq page) (str "Removed page: " page " (repo: " repo ")")
|
||||
(seq uuid) (str "Removed block: " uuid " (repo: " repo ")")
|
||||
(seq ids) (str "Removed blocks: " (count ids) " (repo: " repo ")")
|
||||
(some? id) (str "Removed block: " id " (repo: " repo ")")
|
||||
:else (str "Removed item (repo: " repo ")")))
|
||||
:else (str "Removed block (repo: " repo ")")))
|
||||
|
||||
(defn- format-remove-page
|
||||
[{:keys [repo name]}]
|
||||
(str "Removed page: " name " (repo: " repo ")"))
|
||||
|
||||
(defn- format-remove-tag
|
||||
[{:keys [repo name id]}]
|
||||
(if (seq name)
|
||||
(str "Removed tag: " name " (repo: " repo ")")
|
||||
(str "Removed tag: " id " (repo: " repo ")")))
|
||||
|
||||
(defn- format-remove-property
|
||||
[{:keys [repo name id]}]
|
||||
(if (seq name)
|
||||
(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]}]
|
||||
@@ -319,7 +355,12 @@
|
||||
:add-block (format-add-block context (:result data))
|
||||
:add-page (format-add-page context (:result data))
|
||||
:add-tag (format-add-tag context (:result data))
|
||||
:remove (format-remove context)
|
||||
: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)
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
(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"))
|
||||
@@ -73,8 +74,12 @@
|
||||
(is (contains-bold? summary "list property"))
|
||||
(is (contains-bold? summary "add block"))
|
||||
(is (contains-bold? summary "add page"))
|
||||
(is (contains-bold? summary "add tag"))
|
||||
(is (contains-bold? summary "remove"))
|
||||
(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"))
|
||||
@@ -134,17 +139,27 @@
|
||||
(is (string/includes? plain-summary "Global options:"))
|
||||
(is (string/includes? plain-summary "Command options:"))))
|
||||
|
||||
(testing "remove command shows help"
|
||||
(testing "upsert group shows subcommands"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["remove" "--help"]))
|
||||
(commands/parse-args ["upsert"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "Usage: logseq remove"))
|
||||
(is (string/includes? plain-summary "upsert tag"))
|
||||
(is (string/includes? plain-summary "upsert property"))
|
||||
(is (contains-bold? summary "upsert tag"))
|
||||
(is (contains-bold? summary "upsert property"))))
|
||||
|
||||
(testing "remove block command shows help"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["remove" "block" "--help"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "Usage: logseq remove block"))
|
||||
(is (string/includes? plain-summary "Command options:"))
|
||||
(is (contains-bold? summary "--id"))
|
||||
(is (contains-bold? summary "--uuid"))
|
||||
(is (contains-bold? summary "--page"))))
|
||||
(is (contains-bold? summary "--uuid"))))
|
||||
|
||||
(testing "update command shows help"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
@@ -192,7 +207,7 @@
|
||||
(is (seq lines))
|
||||
(is (every? #(not (string/includes? % "[options]")) lines)))))
|
||||
|
||||
(deftest test-parse-args-help-add-group
|
||||
(deftest test-parse-args-help-add-upsert-group
|
||||
(testing "add group shows subcommands"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["add"]))
|
||||
@@ -201,10 +216,19 @@
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "add block"))
|
||||
(is (string/includes? plain-summary "add page"))
|
||||
(is (string/includes? plain-summary "add tag"))
|
||||
(is (contains-bold? summary "add block"))
|
||||
(is (contains-bold? summary "add page"))
|
||||
(is (contains-bold? summary "add tag")))))
|
||||
(is (contains-bold? summary "add page"))))
|
||||
|
||||
(testing "upsert group shows subcommands"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["upsert"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "upsert tag"))
|
||||
(is (string/includes? plain-summary "upsert property"))
|
||||
(is (contains-bold? summary "upsert tag"))
|
||||
(is (contains-bold? summary "upsert property")))))
|
||||
|
||||
(deftest test-parse-args-help-alignment
|
||||
(testing "graph group aligns subcommand columns"
|
||||
@@ -268,13 +292,26 @@
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :unknown-command (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-parse-args-rejects-legacy-remove-subcommands
|
||||
(testing "rejects legacy remove subcommands"
|
||||
(doseq [args [["remove" "block"]
|
||||
["remove" "page"]]]
|
||||
(let [result (commands/parse-args args)]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))))
|
||||
(deftest test-parse-args-remove-help-and-rejects-add-tag
|
||||
(testing "bare remove shows remove subcommand help"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["remove"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "remove block"))
|
||||
(is (string/includes? plain-summary "remove page"))
|
||||
(is (string/includes? plain-summary "remove tag"))
|
||||
(is (string/includes? plain-summary "remove 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"))))
|
||||
|
||||
(testing "rejects removed add tag command"
|
||||
(let [result (commands/parse-args ["add" "tag" "--name" "Quote"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :unknown-command (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-parse-args-rejects-graph-option
|
||||
(testing "rejects legacy --graph option"
|
||||
@@ -826,42 +863,69 @@
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-verb-subcommand-parse-add-remove
|
||||
(testing "remove requires target"
|
||||
(let [result (commands/parse-args ["remove"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-target (get-in result [:error :code])))))
|
||||
|
||||
(testing "remove parses with id"
|
||||
(let [result (commands/parse-args ["remove" "--id" "10"])]
|
||||
(deftest test-verb-subcommand-parse-upsert-remove
|
||||
(testing "remove block parses with id"
|
||||
(let [result (commands/parse-args ["remove" "block" "--id" "10"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :remove (:command result)))
|
||||
(is (= :remove-block (:command result)))
|
||||
(is (= 10 (get-in result [:options :id])))))
|
||||
|
||||
(testing "remove parses with uuid"
|
||||
(let [result (commands/parse-args ["remove" "--uuid" "abc"])]
|
||||
(testing "remove page parses with name"
|
||||
(let [result (commands/parse-args ["remove" "page" "--name" "Home"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :remove (:command result)))
|
||||
(is (= "abc" (get-in result [:options :uuid])))))
|
||||
(is (= :remove-page (:command result)))
|
||||
(is (= "Home" (get-in result [:options :name])))))
|
||||
|
||||
(testing "remove parses with page"
|
||||
(let [result (commands/parse-args ["remove" "--page" "Home"])]
|
||||
(testing "remove tag parses with name"
|
||||
(let [result (commands/parse-args ["remove" "tag" "--name" "Quote"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :remove (:command result)))
|
||||
(is (= "Home" (get-in result [:options :page])))))
|
||||
(is (= :remove-tag (:command result)))
|
||||
(is (= "Quote" (get-in result [:options :name])))))
|
||||
|
||||
(testing "remove rejects multiple selectors"
|
||||
(let [result (commands/parse-args ["remove" "--id" "1" "--page" "Home"])]
|
||||
(testing "remove property parses with id"
|
||||
(let [result (commands/parse-args ["remove" "property" "--id" "123"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :remove-property (:command result)))
|
||||
(is (= 123 (get-in result [:options :id])))))
|
||||
|
||||
(testing "remove block rejects empty id vector"
|
||||
(let [result (commands/parse-args ["remove" "block" "--id" "[]"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "remove rejects empty id vector"
|
||||
(let [result (commands/parse-args ["remove" "--id" "[]"])]
|
||||
(testing "remove block rejects invalid id vector"
|
||||
(let [result (commands/parse-args ["remove" "block" "--id" "[1 \"no\"]"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "remove rejects invalid id vector"
|
||||
(let [result (commands/parse-args ["remove" "--id" "[1 \"no\"]"])]
|
||||
(testing "upsert tag parses with name"
|
||||
(let [result (commands/parse-args ["upsert" "tag" "--name" "Quote"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-tag (:command result)))
|
||||
(is (= "Quote" (get-in result [:options :name])))))
|
||||
|
||||
(testing "upsert property parses with type and cardinality"
|
||||
(let [result (commands/parse-args ["upsert" "property"
|
||||
"--name" "owner"
|
||||
"--type" "node"
|
||||
"--cardinality" "many"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-property (:command result)))
|
||||
(is (= "owner" (get-in result [:options :name])))
|
||||
(is (= "node" (get-in result [:options :type])))
|
||||
(is (= "many" (get-in result [:options :cardinality])))))
|
||||
|
||||
(testing "upsert property rejects invalid type"
|
||||
(let [result (commands/parse-args ["upsert" "property"
|
||||
"--name" "owner"
|
||||
"--type" "wat"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "upsert property rejects invalid cardinality"
|
||||
(let [result (commands/parse-args ["upsert" "property"
|
||||
"--name" "owner"
|
||||
"--cardinality" "triple"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
@@ -973,21 +1037,10 @@
|
||||
(is (= "[\"TagA\"]" (get-in result [:options :tags])))
|
||||
(is (= "{:logseq.property/publishing-public? true}" (get-in result [:options :properties])))))
|
||||
|
||||
(testing "add tag requires name"
|
||||
(let [result (commands/parse-args ["add" "tag"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-tag-name (get-in result [:error :code])))))
|
||||
|
||||
(testing "add tag parses with name"
|
||||
(testing "add tag is no longer supported"
|
||||
(let [result (commands/parse-args ["add" "tag" "--name" "Quote"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :add-tag (:command result)))
|
||||
(is (= "Quote" (get-in result [:options :name])))))
|
||||
|
||||
(testing "add tag rejects blank name"
|
||||
(let [result (commands/parse-args ["add" "tag" "--name" " "])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-tag-name (get-in result [:error :code]))))))
|
||||
(is (= :unknown-command (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-verb-subcommand-parse-update-target-page
|
||||
(testing "update parses with target page"
|
||||
@@ -1113,7 +1166,8 @@
|
||||
(testing "verb subcommands reject unknown flags"
|
||||
(doseq [args [["list" "page" "--wat"]
|
||||
["add" "block" "--wat"]
|
||||
["remove" "--wat"]
|
||||
["remove" "block" "--wat"]
|
||||
["upsert" "tag" "--wat"]
|
||||
["update" "--wat"]
|
||||
["show" "--wat"]]]
|
||||
(let [result (commands/parse-args args)]
|
||||
@@ -1207,7 +1261,7 @@
|
||||
(is (= (cli-server/db-worker-dev-script-path)
|
||||
(get-in result [:action :script-path]))))))
|
||||
|
||||
(deftest test-build-action-inspect-edit
|
||||
(deftest test-build-action-inspect-edit-add-upsert
|
||||
(testing "list page requires repo"
|
||||
(let [parsed {:ok? true :command :list-page :options {}}
|
||||
result (commands/build-action parsed {})]
|
||||
@@ -1232,35 +1286,79 @@
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-page-name (get-in result [:error :code])))))
|
||||
|
||||
(testing "add tag requires name"
|
||||
(let [parsed {:ok? true :command :add-tag :options {}}
|
||||
(testing "upsert tag requires name"
|
||||
(let [parsed {:ok? true :command :upsert-tag :options {}}
|
||||
result (commands/build-action parsed {:repo "demo"})]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-tag-name (get-in result [:error :code])))))
|
||||
|
||||
(testing "add tag builds normalized action"
|
||||
(let [parsed {:ok? true :command :add-tag :options {:name " #Quote "}}
|
||||
(testing "upsert tag builds normalized action"
|
||||
(let [parsed {:ok? true :command :upsert-tag :options {:name " #Quote "}}
|
||||
result (commands/build-action parsed {:repo "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= {:type :add-tag
|
||||
(is (= {:type :upsert-tag
|
||||
:repo "logseq_db_demo"
|
||||
:graph "demo"
|
||||
:name "Quote"}
|
||||
(:action result)))))
|
||||
|
||||
(testing "remove requires target"
|
||||
(let [parsed {:ok? true :command :remove :options {}}
|
||||
(testing "upsert property coerces schema options"
|
||||
(let [parsed {:ok? true
|
||||
:command :upsert-property
|
||||
:options {:name "owner"
|
||||
:type "node"
|
||||
:cardinality "many"
|
||||
:hide true
|
||||
:public false}}
|
||||
result (commands/build-action parsed {:repo "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= {:type :upsert-property
|
||||
:repo "logseq_db_demo"
|
||||
:graph "demo"
|
||||
:name "owner"
|
||||
:schema {:logseq.property/type :node
|
||||
:db/cardinality :db.cardinality/many
|
||||
:logseq.property/hide? true
|
||||
:logseq.property/public? false}}
|
||||
(:action result)))))
|
||||
|
||||
)
|
||||
|
||||
(deftest test-build-action-inspect-edit-remove-show
|
||||
|
||||
(testing "remove block requires target"
|
||||
(let [parsed {:ok? true :command :remove-block :options {}}
|
||||
result (commands/build-action parsed {:repo "demo"})]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-target (get-in result [:error :code])))))
|
||||
|
||||
(testing "remove normalizes id vector in build action"
|
||||
(let [parsed {:ok? true :command :remove :options {:id "[1 2]"}}
|
||||
(testing "remove block normalizes id vector in build action"
|
||||
(let [parsed {:ok? true :command :remove-block :options {:id "[1 2]"}}
|
||||
result (commands/build-action parsed {:repo "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :remove (get-in result [:action :type])))
|
||||
(is (= :remove-block (get-in result [:action :type])))
|
||||
(is (= [1 2] (get-in result [:action :ids])))))
|
||||
|
||||
(testing "remove page requires name"
|
||||
(let [parsed {:ok? true :command :remove-page :options {}}
|
||||
result (commands/build-action parsed {:repo "demo"})]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-page-name (get-in result [:error :code])))))
|
||||
|
||||
(testing "remove tag parses by id"
|
||||
(let [parsed {:ok? true :command :remove-tag :options {:id 42}}
|
||||
result (commands/build-action parsed {:repo "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :remove-tag (get-in result [:action :type])))
|
||||
(is (= 42 (get-in result [:action :id])))))
|
||||
|
||||
(testing "remove property parses by name"
|
||||
(let [parsed {:ok? true :command :remove-property :options {:name "owner"}}
|
||||
result (commands/build-action parsed {:repo "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :remove-property (get-in result [:action :type])))
|
||||
(is (= "owner" (get-in result [:action :name])))))
|
||||
|
||||
(testing "show requires target"
|
||||
(let [parsed {:ok? true :command :show :options {}}
|
||||
result (commands/build-action parsed {:repo "demo"})]
|
||||
@@ -1386,15 +1484,17 @@
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-execute-add-tag-builds-create-page-op
|
||||
(deftest test-execute-upsert-tag-builds-create-page-op
|
||||
(async done
|
||||
(let [ops* (atom nil)
|
||||
created?* (atom false)
|
||||
orig-list-graphs cli-server/list-graphs
|
||||
orig-ensure-server! cli-server/ensure-server!
|
||||
orig-invoke transport/invoke
|
||||
action {:type :add-tag
|
||||
action {:type :upsert-tag
|
||||
:repo "demo"
|
||||
:name "Quote"}]
|
||||
(set! cli-server/list-graphs (fn [_] ["demo"]))
|
||||
(set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"}))
|
||||
(set! transport/invoke (fn [_ method _ args]
|
||||
(case method
|
||||
@@ -1412,7 +1512,7 @@
|
||||
(reset! ops* ops)
|
||||
{:result :ok})
|
||||
(throw (ex-info "unexpected invoke" {:method method :args args})))))
|
||||
(-> (p/let [result (add-command/execute-add-tag action {})]
|
||||
(-> (p/let [result (commands/execute action {})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= [4242] (get-in result [:data :result])))
|
||||
(is (= [[:create-page ["Quote" {:class? true}]]]
|
||||
@@ -1420,17 +1520,20 @@
|
||||
(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! transport/invoke orig-invoke)
|
||||
(done)))))))
|
||||
|
||||
(deftest test-execute-add-tag-rejects-existing-non-tag-page
|
||||
(deftest test-execute-upsert-tag-rejects-existing-non-tag-page
|
||||
(async done
|
||||
(let [action {:type :add-tag
|
||||
(let [action {:type :upsert-tag
|
||||
:repo "demo"
|
||||
:name "Home"}
|
||||
orig-list-graphs cli-server/list-graphs
|
||||
orig-ensure-server! cli-server/ensure-server!
|
||||
orig-invoke transport/invoke]
|
||||
(set! cli-server/list-graphs (fn [_] ["demo"]))
|
||||
(set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"}))
|
||||
(set! transport/invoke (fn [_ method _ args]
|
||||
(case method
|
||||
@@ -1444,24 +1547,27 @@
|
||||
:thread-api/apply-outliner-ops
|
||||
(throw (ex-info "should not create tag" {:args args}))
|
||||
(throw (ex-info "unexpected invoke" {:method method :args args})))))
|
||||
(-> (add-command/execute-add-tag action {})
|
||||
(p/then (fn [_]
|
||||
(is false "expected add tag conflict error")))
|
||||
(-> (p/let [result (commands/execute action {})]
|
||||
(is (= :error (:status result)))
|
||||
(is (= :tag-name-conflict (get-in result [:error :code]))))
|
||||
(p/catch (fn [e]
|
||||
(is (= :tag-name-conflict (:code (ex-data 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! transport/invoke orig-invoke)
|
||||
(done)))))))
|
||||
|
||||
(deftest test-execute-add-tag-idempotent-when-tag-exists
|
||||
(deftest test-execute-upsert-tag-idempotent-when-tag-exists
|
||||
(async done
|
||||
(let [apply-calls* (atom 0)
|
||||
orig-list-graphs cli-server/list-graphs
|
||||
orig-ensure-server! cli-server/ensure-server!
|
||||
orig-invoke transport/invoke
|
||||
action {:type :add-tag
|
||||
action {:type :upsert-tag
|
||||
:repo "demo"
|
||||
:name "Quote"}]
|
||||
(set! cli-server/list-graphs (fn [_] ["demo"]))
|
||||
(set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"}))
|
||||
(set! transport/invoke (fn [_ method _ args]
|
||||
(case method
|
||||
@@ -1476,13 +1582,159 @@
|
||||
(swap! apply-calls* inc)
|
||||
{:result :ok})
|
||||
(throw (ex-info "unexpected invoke" {:method method :args args})))))
|
||||
(-> (p/let [result (add-command/execute-add-tag action {})]
|
||||
(-> (p/let [result (commands/execute action {})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= [4242] (get-in result [:data :result])))
|
||||
(is (= 0 @apply-calls*)))
|
||||
(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! transport/invoke orig-invoke)
|
||||
(done)))))))
|
||||
|
||||
(deftest test-execute-upsert-property-emits-upsert-op
|
||||
(async done
|
||||
(let [ops* (atom nil)
|
||||
created?* (atom false)
|
||||
orig-list-graphs cli-server/list-graphs
|
||||
orig-ensure-server! cli-server/ensure-server!
|
||||
orig-invoke transport/invoke
|
||||
action {:type :upsert-property
|
||||
:repo "demo"
|
||||
:name "owner"
|
||||
:schema {:logseq.property/type :node
|
||||
:db/cardinality :db.cardinality/many}}]
|
||||
(set! cli-server/list-graphs (fn [_] ["demo"]))
|
||||
(set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"}))
|
||||
(set! transport/invoke (fn [_ method _ args]
|
||||
(case method
|
||||
:thread-api/pull (if @created?*
|
||||
{:db/id 654
|
||||
:db/ident :user.property/owner
|
||||
:block/name "owner"
|
||||
:block/title "owner"
|
||||
:logseq.property/type :node}
|
||||
{})
|
||||
:thread-api/apply-outliner-ops (let [[_ ops _] args]
|
||||
(reset! created?* true)
|
||||
(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 (= [[:upsert-property [nil
|
||||
{:logseq.property/type :node
|
||||
:db/cardinality :db.cardinality/many}
|
||||
{:property-name "owner"}]]]
|
||||
@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! transport/invoke orig-invoke)
|
||||
(done)))))))
|
||||
|
||||
(deftest test-execute-remove-tag-property
|
||||
(async done
|
||||
(let [ops* (atom [])
|
||||
orig-list-graphs cli-server/list-graphs
|
||||
orig-ensure-server! cli-server/ensure-server!
|
||||
orig-invoke transport/invoke]
|
||||
(set! cli-server/list-graphs (fn [_] ["demo"]))
|
||||
(set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"}))
|
||||
(set! transport/invoke
|
||||
(fn [_ method _ args]
|
||||
(case method
|
||||
:thread-api/api-list-tags [{:db/id 1 :block/title "Quote"}]
|
||||
:thread-api/api-list-properties [{:db/id 2 :block/title "owner"}]
|
||||
:thread-api/pull (let [[_ selector lookup] args]
|
||||
(cond
|
||||
(= lookup 1)
|
||||
{:db/id 1
|
||||
:block/title "Quote"
|
||||
:block/uuid (uuid "00000000-0000-0000-0000-000000000011")
|
||||
:block/tags [{:db/ident :logseq.class/Tag}]
|
||||
:logseq.property/public? true}
|
||||
|
||||
(= lookup 2)
|
||||
{:db/id 2
|
||||
:db/ident :user.property/owner
|
||||
:block/title "owner"
|
||||
:block/uuid (uuid "00000000-0000-0000-0000-000000000022")
|
||||
:logseq.property/type :node
|
||||
:logseq.property/public? true}
|
||||
|
||||
(= lookup [:block/name "quote"])
|
||||
{:db/id 1
|
||||
:block/title "Quote"
|
||||
:block/uuid (uuid "00000000-0000-0000-0000-000000000011")
|
||||
:block/tags [{:db/ident :logseq.class/Tag}]
|
||||
:logseq.property/public? true}
|
||||
|
||||
(= lookup [:block/name "owner"])
|
||||
{:db/id 2
|
||||
:db/ident :user.property/owner
|
||||
:block/title "owner"
|
||||
:block/uuid (uuid "00000000-0000-0000-0000-000000000022")
|
||||
:logseq.property/type :node
|
||||
:logseq.property/public? true}
|
||||
|
||||
:else
|
||||
(throw (ex-info "unexpected pull lookup"
|
||||
{:lookup lookup :selector selector}))))
|
||||
:thread-api/apply-outliner-ops (let [[_ ops _] args]
|
||||
(swap! ops* conj ops)
|
||||
{:result :ok})
|
||||
(throw (ex-info "unexpected invoke" {:method method :args args})))))
|
||||
(-> (p/let [tag-result (commands/execute {:type :remove-tag
|
||||
:repo "demo"
|
||||
:name "Quote"}
|
||||
{})
|
||||
property-result (commands/execute {:type :remove-property
|
||||
:repo "demo"
|
||||
:id 2}
|
||||
{})]
|
||||
(is (= :ok (:status tag-result)))
|
||||
(is (= :ok (:status property-result)))
|
||||
(is (= [[:delete-page [(uuid "00000000-0000-0000-0000-000000000011")]]]
|
||||
(first @ops*)))
|
||||
(is (= [[:delete-page [(uuid "00000000-0000-0000-0000-000000000022")]]]
|
||||
(second @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! transport/invoke orig-invoke)
|
||||
(done)))))))
|
||||
|
||||
(deftest test-execute-remove-tag-ambiguous-name
|
||||
(async done
|
||||
(let [orig-list-graphs cli-server/list-graphs
|
||||
orig-ensure-server! cli-server/ensure-server!
|
||||
orig-invoke transport/invoke]
|
||||
(set! cli-server/list-graphs (fn [_] ["demo"]))
|
||||
(set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"}))
|
||||
(set! transport/invoke
|
||||
(fn [_ method _ _]
|
||||
(case method
|
||||
:thread-api/api-list-tags [{:db/id 1 :block/title "Quote"}
|
||||
{:db/id 2 :block/title "QUOTE"}]
|
||||
(throw (ex-info "unexpected invoke" {:method method})))))
|
||||
(-> (p/let [result (commands/execute {:type :remove-tag
|
||||
:repo "demo"
|
||||
:name "Quote"}
|
||||
{})]
|
||||
(is (= :error (:status result)))
|
||||
(is (= :ambiguous-tag-name (get-in result [:error :code])))
|
||||
(is (= 2 (count (get-in result [:error :candidates])))))
|
||||
(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! transport/invoke orig-invoke)
|
||||
(done)))))))
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"Count: 1")
|
||||
result)))))
|
||||
|
||||
(deftest test-human-output-add-remove
|
||||
(deftest test-human-output-add-upsert-remove
|
||||
(testing "add block renders ids in two lines"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :add-block
|
||||
@@ -109,24 +109,60 @@
|
||||
{:output-format nil})]
|
||||
(is (= "Added page:\n[123]" result))))
|
||||
|
||||
(testing "add tag renders ids in two lines"
|
||||
(testing "upsert tag renders ids in two lines"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :add-tag
|
||||
:command :upsert-tag
|
||||
:context {:repo "demo-repo"
|
||||
:name "Quote"}
|
||||
:data {:result [321]}}
|
||||
{:output-format nil})]
|
||||
(is (= "Added tag:\n[321]" result))))
|
||||
(is (= "Upserted tag:\n[321]" result))))
|
||||
|
||||
(testing "upsert property renders ids in two lines"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :upsert-property
|
||||
:context {:repo "demo-repo"
|
||||
:name "owner"}
|
||||
:data {:result [654]}}
|
||||
{:output-format nil})]
|
||||
(is (= "Upserted property:\n[654]" result))))
|
||||
|
||||
(testing "remove page renders a succinct success line"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :remove
|
||||
:command :remove-page
|
||||
:context {:repo "demo-repo"
|
||||
:page "Home"}
|
||||
:name "Home"}
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= "Removed page: Home (repo: demo-repo)" result))))
|
||||
|
||||
(testing "remove block with id list renders block count"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :remove-block
|
||||
:context {:repo "demo-repo"
|
||||
:ids [1 2 3]}
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= "Removed blocks: 3 (repo: demo-repo)" result))))
|
||||
|
||||
(testing "remove tag renders a succinct success line"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :remove-tag
|
||||
:context {:repo "demo-repo"
|
||||
:name "Quote"}
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= "Removed tag: Quote (repo: demo-repo)" result))))
|
||||
|
||||
(testing "remove property renders a succinct success line"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :remove-property
|
||||
:context {:repo "demo-repo"
|
||||
:name "owner"}
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= "Removed property: owner (repo: demo-repo)" result))))
|
||||
|
||||
(testing "update block renders a succinct success line"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :update-block
|
||||
@@ -430,7 +466,22 @@
|
||||
{:output-format nil})]
|
||||
(is (= (str "Error (server-start-timeout-orphan): db-worker-node failed to create lock\n"
|
||||
"Hint: Check and stop lingering db-worker-node processes, then retry")
|
||||
result)))))
|
||||
result))))
|
||||
|
||||
(testing "remove tag ambiguity includes candidate list"
|
||||
(let [result (format/format-result {:status :error
|
||||
:command :remove-tag
|
||||
:error {:code :ambiguous-tag-name
|
||||
:message "multiple tags match name: Quote"
|
||||
:candidates [{:id 1 :name "Quote"}
|
||||
{:id 2 :name "QUOTE"}]}}
|
||||
{:output-format nil})]
|
||||
(is (string/includes? result "Error (ambiguous-tag-name):"))
|
||||
(is (string/includes? result "multiple tags match name: Quote"))
|
||||
(is (string/includes? result "1"))
|
||||
(is (string/includes? result "2"))
|
||||
(is (string/includes? result "Quote"))
|
||||
(is (string/includes? result "QUOTE")))))
|
||||
|
||||
(deftest test-human-output-doctor
|
||||
(testing "doctor renders concise check summary"
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
_ (p/delay 100)
|
||||
show-result (run-cli ["--repo" "content-graph" "show" "--page" "TestPage"] data-dir cfg-path)
|
||||
show-payload (parse-json-output show-result)
|
||||
remove-page-result (run-cli ["--repo" "content-graph" "remove" "--page" "TestPage"] data-dir cfg-path)
|
||||
remove-page-result (run-cli ["--repo" "content-graph" "remove" "page" "--name" "TestPage"] data-dir cfg-path)
|
||||
remove-page-payload (parse-json-output remove-page-result)
|
||||
stop-result (run-cli ["server" "stop" "--repo" "content-graph"] data-dir cfg-path)
|
||||
stop-payload (parse-json-output stop-result)]
|
||||
@@ -437,7 +437,7 @@
|
||||
"--update-properties" "{:logseq.property/publishing-public? true}"]
|
||||
data-dir cfg-path)
|
||||
update-payload (parse-json-output update-result)
|
||||
remove-result (run-cli ["--repo" repo "remove" "--id" (str block-id)] data-dir cfg-path)
|
||||
remove-result (run-cli ["--repo" repo "remove" "block" "--id" (str block-id)] data-dir cfg-path)
|
||||
remove-payload (parse-json-output remove-result)
|
||||
query-after-remove (run-query data-dir cfg-path repo
|
||||
"[:find ?e . :in $ ?title :where [?e :block/title ?title]]"
|
||||
@@ -857,19 +857,19 @@
|
||||
(is false (str "unexpected error: " e))
|
||||
(done)))))))
|
||||
|
||||
(deftest ^:long test-cli-add-tag-create-and-use
|
||||
(deftest ^:long test-cli-upsert-tag-create-and-use
|
||||
(async done
|
||||
(let [data-dir (node-helper/create-tmp-dir "db-worker-add-tag-create")]
|
||||
(let [data-dir (node-helper/create-tmp-dir "db-worker-upsert-tag-create")]
|
||||
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
|
||||
repo "add-tag-create-graph"
|
||||
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)
|
||||
add-tag-result (run-cli ["--repo" repo
|
||||
"add" "tag"
|
||||
"--name" "CliQuote"]
|
||||
data-dir cfg-path)
|
||||
add-tag-payload (parse-json-output add-tag-result)
|
||||
upsert-tag-result (run-cli ["--repo" repo
|
||||
"upsert" "tag"
|
||||
"--name" "CliQuote"]
|
||||
data-dir cfg-path)
|
||||
upsert-tag-payload (parse-json-output upsert-tag-result)
|
||||
list-tag-result (run-cli ["--repo" repo "list" "tag"] data-dir cfg-path)
|
||||
list-tag-payload (parse-json-output list-tag-result)
|
||||
tag-names (->> (get-in list-tag-payload [:data :items])
|
||||
@@ -878,17 +878,17 @@
|
||||
add-block-result (run-cli ["--repo" repo
|
||||
"add" "block"
|
||||
"--target-page-name" "Home"
|
||||
"--content" "Tagged by add tag"
|
||||
"--content" "Tagged by upsert tag"
|
||||
"--tags" "[\"CliQuote\"]"]
|
||||
data-dir cfg-path)
|
||||
add-block-payload (parse-json-output add-block-result)
|
||||
_ (p/delay 100)
|
||||
block-tag-names (query-tags data-dir cfg-path repo "Tagged by add tag")
|
||||
block-tag-names (query-tags data-dir cfg-path repo "Tagged by upsert tag")
|
||||
stop-result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path)
|
||||
stop-payload (parse-json-output stop-result)]
|
||||
(is (= 0 (:exit-code add-tag-result))
|
||||
(pr-str (:error add-tag-payload)))
|
||||
(is (= "ok" (:status add-tag-payload)))
|
||||
(is (= 0 (:exit-code upsert-tag-result))
|
||||
(pr-str (:error upsert-tag-payload)))
|
||||
(is (= "ok" (:status upsert-tag-payload)))
|
||||
(is (= "ok" (:status list-tag-payload)))
|
||||
(is (contains? tag-names "CliQuote"))
|
||||
(is (= 0 (:exit-code add-block-result))
|
||||
@@ -901,19 +901,19 @@
|
||||
(is false (str "unexpected error: " e))
|
||||
(done)))))))
|
||||
|
||||
(deftest ^:long test-cli-add-tag-rejects-existing-non-tag-page
|
||||
(deftest ^:long test-cli-upsert-tag-rejects-existing-non-tag-page
|
||||
(async done
|
||||
(let [data-dir (node-helper/create-tmp-dir "db-worker-add-tag-conflict")]
|
||||
(let [data-dir (node-helper/create-tmp-dir "db-worker-upsert-tag-conflict")]
|
||||
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
|
||||
repo "add-tag-conflict-graph"
|
||||
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)
|
||||
add-tag-result (run-cli ["--repo" repo
|
||||
"add" "tag"
|
||||
"--name" "ConflictPage"]
|
||||
data-dir cfg-path)
|
||||
add-tag-payload (parse-json-output add-tag-result)
|
||||
upsert-tag-result (run-cli ["--repo" repo
|
||||
"upsert" "tag"
|
||||
"--name" "ConflictPage"]
|
||||
data-dir cfg-path)
|
||||
upsert-tag-payload (parse-json-output upsert-tag-result)
|
||||
list-tag-result (run-cli ["--repo" repo "list" "tag"] data-dir cfg-path)
|
||||
list-tag-payload (parse-json-output list-tag-result)
|
||||
tag-names (->> (get-in list-tag-payload [:data :items])
|
||||
@@ -921,9 +921,9 @@
|
||||
set)
|
||||
stop-result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path)
|
||||
stop-payload (parse-json-output stop-result)]
|
||||
(is (= 1 (:exit-code add-tag-result)))
|
||||
(is (= "error" (:status add-tag-payload)))
|
||||
(is (string/includes? (get-in add-tag-payload [:error :message])
|
||||
(is (= 0 (:exit-code upsert-tag-result)))
|
||||
(is (= "error" (:status upsert-tag-payload)))
|
||||
(is (string/includes? (get-in upsert-tag-payload [:error :message])
|
||||
"already exists as a page and is not a tag"))
|
||||
(is (not (contains? tag-names "ConflictPage")))
|
||||
(is (= "ok" (:status stop-payload)))
|
||||
@@ -932,29 +932,29 @@
|
||||
(is false (str "unexpected error: " e))
|
||||
(done)))))))
|
||||
|
||||
(deftest ^:long test-cli-add-tag-idempotent-existing-tag
|
||||
(deftest ^:long test-cli-upsert-tag-idempotent-existing-tag
|
||||
(async done
|
||||
(let [data-dir (node-helper/create-tmp-dir "db-worker-add-tag-idempotent")]
|
||||
(let [data-dir (node-helper/create-tmp-dir "db-worker-upsert-tag-idempotent")]
|
||||
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
|
||||
repo "add-tag-idempotent-graph"
|
||||
repo "upsert-tag-idempotent-graph"
|
||||
_ (fs/writeFileSync cfg-path "{:output-format :json}")
|
||||
_ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path)
|
||||
first-add-result (run-cli ["--repo" repo "add" "tag" "--name" "StableTag"]
|
||||
data-dir cfg-path)
|
||||
first-add-payload (parse-json-output first-add-result)
|
||||
second-add-result (run-cli ["--repo" repo "add" "tag" "--name" "StableTag"]
|
||||
data-dir cfg-path)
|
||||
second-add-payload (parse-json-output second-add-result)
|
||||
first-upsert-result (run-cli ["--repo" repo "upsert" "tag" "--name" "StableTag"]
|
||||
data-dir cfg-path)
|
||||
first-upsert-payload (parse-json-output first-upsert-result)
|
||||
second-upsert-result (run-cli ["--repo" repo "upsert" "tag" "--name" "StableTag"]
|
||||
data-dir cfg-path)
|
||||
second-upsert-payload (parse-json-output second-upsert-result)
|
||||
list-tag-result (run-cli ["--repo" repo "list" "tag"] data-dir cfg-path)
|
||||
list-tag-payload (parse-json-output list-tag-result)
|
||||
stable-tags (->> (get-in list-tag-payload [:data :items])
|
||||
(filter #(= "StableTag" (or (:block/title %) (:title %) (:name %)))))
|
||||
stop-result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path)
|
||||
stop-payload (parse-json-output stop-result)]
|
||||
(is (= 0 (:exit-code first-add-result)))
|
||||
(is (= "ok" (:status first-add-payload)))
|
||||
(is (= 0 (:exit-code second-add-result)))
|
||||
(is (= "ok" (:status second-add-payload)))
|
||||
(is (= 0 (:exit-code first-upsert-result)))
|
||||
(is (= "ok" (:status first-upsert-payload)))
|
||||
(is (= 0 (:exit-code second-upsert-result)))
|
||||
(is (= "ok" (:status second-upsert-payload)))
|
||||
(is (= 1 (count stable-tags)))
|
||||
(is (= "ok" (:status stop-payload)))
|
||||
(done))
|
||||
@@ -962,6 +962,73 @@
|
||||
(is false (str "unexpected error: " e))
|
||||
(done)))))))
|
||||
|
||||
(deftest ^:long test-cli-upsert-and-remove-tag-property
|
||||
(async done
|
||||
(let [data-dir (node-helper/create-tmp-dir "db-worker-upsert-remove-tag-property")]
|
||||
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
|
||||
repo "upsert-remove-tag-property-graph"
|
||||
tag-name "CliQuoteTagX"
|
||||
property-name "CliOwnerPropX"
|
||||
property-name-lc (common-util/page-name-sanity-lc property-name)
|
||||
_ (fs/writeFileSync cfg-path "{:output-format :json}")
|
||||
_ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path)
|
||||
upsert-tag-result (run-cli ["--repo" repo "upsert" "tag" "--name" tag-name] data-dir cfg-path)
|
||||
upsert-tag-payload (parse-json-output upsert-tag-result)
|
||||
upsert-property-result (run-cli ["--repo" repo
|
||||
"upsert" "property"
|
||||
"--name" property-name
|
||||
"--type" "node"
|
||||
"--cardinality" "many"]
|
||||
data-dir cfg-path)
|
||||
upsert-property-payload (parse-json-output upsert-property-result)
|
||||
update-property-result (run-cli ["--repo" repo
|
||||
"upsert" "property"
|
||||
"--name" property-name
|
||||
"--type" "node"
|
||||
"--cardinality" "one"]
|
||||
data-dir cfg-path)
|
||||
update-property-payload (parse-json-output update-property-result)
|
||||
property-schema-before-remove (run-query data-dir cfg-path repo
|
||||
"[:find ?type ?cardinality :in $ ?name :where [?p :block/name ?name] [?p :logseq.property/type ?type] [?p :db/cardinality ?cardinality]]"
|
||||
(pr-str [property-name-lc]))
|
||||
remove-tag-result (run-cli ["--repo" repo "remove" "tag" "--name" tag-name] data-dir cfg-path)
|
||||
remove-tag-payload (parse-json-output remove-tag-result)
|
||||
remove-property-result (run-cli ["--repo" repo "remove" "property" "--name" property-name] data-dir cfg-path)
|
||||
remove-property-payload (parse-json-output remove-property-result)
|
||||
list-tag-result (run-cli ["--repo" repo "list" "tag"] data-dir cfg-path)
|
||||
list-tag-payload (parse-json-output list-tag-result)
|
||||
list-property-result (run-cli ["--repo" repo "list" "property"] data-dir cfg-path)
|
||||
list-property-payload (parse-json-output list-property-result)
|
||||
tag-names (->> (get-in list-tag-payload [:data :items])
|
||||
(map #(or (:block/title %) (:title %) (:name %)))
|
||||
set)
|
||||
property-names (->> (get-in list-property-payload [:data :items])
|
||||
(map #(or (:block/title %) (:title %) (:name %)))
|
||||
set)
|
||||
stop-result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path)
|
||||
stop-payload (parse-json-output stop-result)]
|
||||
(is (= 0 (:exit-code upsert-tag-result)))
|
||||
(is (= "ok" (:status upsert-tag-payload)))
|
||||
(is (= 0 (:exit-code upsert-property-result)))
|
||||
(is (= "ok" (:status upsert-property-payload)))
|
||||
(is (= 0 (:exit-code update-property-result)))
|
||||
(is (= "ok" (:status update-property-payload)))
|
||||
(is (= [["node" "one"]]
|
||||
(get-in property-schema-before-remove [:data :result])))
|
||||
(is (= 0 (:exit-code remove-tag-result)))
|
||||
(is (= "ok" (:status remove-tag-payload))
|
||||
(pr-str remove-tag-payload))
|
||||
(is (= 0 (:exit-code remove-property-result)))
|
||||
(is (= "ok" (:status remove-property-payload))
|
||||
(pr-str remove-property-payload))
|
||||
(is (not (contains? tag-names tag-name)))
|
||||
(is (not (contains? property-names property-name)))
|
||||
(is (= "ok" (:status stop-payload)))
|
||||
(done))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))
|
||||
(done)))))))
|
||||
|
||||
(deftest ^:long test-cli-query
|
||||
(async done
|
||||
(let [data-dir (node-helper/create-tmp-dir "db-worker-query")
|
||||
|
||||
Reference in New Issue
Block a user