Просмотр исходного кода

Merge branch 'master' into feat/mcp-server

Gabriel Horner 2 месяцев назад
Родитель
Сommit
20ed5bccfb
100 измененных файлов с 3062 добавлено и 721 удалено
  1. 9 5
      AGENTS.md
  2. 8 1
      bb.edn
  3. 243 26
      clj-e2e/test/logseq/e2e/plugins_basic_test.clj
  4. 2 1
      deps.edn
  5. 1 1
      deps/cli/package.json
  6. 1 1
      deps/cli/src/logseq/cli/common/mcp/tools.cljs
  7. 3 3
      deps/cli/yarn.lock
  8. 1 1
      deps/common/package.json
  9. 0 12
      deps/common/src/logseq/common/util.cljs
  10. 3 3
      deps/common/yarn.lock
  11. 2 0
      deps/db/.carve/ignore
  12. 2 1
      deps/db/deps.edn
  13. 1 1
      deps/db/package.json
  14. 4 4
      deps/db/script/create_graph.cljs
  15. 83 50
      deps/db/src/logseq/db.cljs
  16. 8 1
      deps/db/src/logseq/db/common/entity_util.cljs
  17. 1 5
      deps/db/src/logseq/db/common/view.cljs
  18. 15 3
      deps/db/src/logseq/db/frontend/class.cljs
  19. 23 25
      deps/db/src/logseq/db/frontend/db_ident.cljc
  20. 6 2
      deps/db/src/logseq/db/frontend/malli_schema.cljs
  21. 2 1
      deps/db/src/logseq/db/frontend/property.cljs
  22. 21 2
      deps/db/src/logseq/db/frontend/property/build.cljs
  23. 49 32
      deps/db/src/logseq/db/frontend/property/type.cljs
  24. 3 3
      deps/db/src/logseq/db/frontend/validate.cljs
  25. 17 4
      deps/db/src/logseq/db/sqlite/build.cljs
  26. 3 3
      deps/db/yarn.lock
  27. 1 1
      deps/graph-parser/package.json
  28. 2 2
      deps/graph-parser/script/db_import.cljs
  29. 3 3
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  30. 3 3
      deps/graph-parser/yarn.lock
  31. 1 5
      deps/outliner/.carve/ignore
  32. 2 1
      deps/outliner/deps.edn
  33. 1 1
      deps/outliner/package.json
  34. 2 1
      deps/outliner/script/transact.cljs
  35. 4 4
      deps/outliner/src/logseq/outliner/cli.cljs
  36. 11 14
      deps/outliner/src/logseq/outliner/core.cljs
  37. 1 1
      deps/outliner/src/logseq/outliner/db_pipeline.cljs
  38. 14 13
      deps/outliner/src/logseq/outliner/page.cljs
  39. 2 2
      deps/outliner/src/logseq/outliner/pipeline.cljs
  40. 139 63
      deps/outliner/src/logseq/outliner/property.cljs
  41. 3 3
      deps/outliner/test/logseq/outliner/property_test.cljs
  42. 3 3
      deps/outliner/yarn.lock
  43. 1 1
      deps/publishing/package.json
  44. 3 3
      deps/publishing/yarn.lock
  45. 1 0
      libs/.npmignore
  46. 13 0
      libs/README.md
  47. 38 0
      libs/cljs-sdk/.gitignore
  48. 21 0
      libs/cljs-sdk/LICENSE
  49. 31 0
      libs/cljs-sdk/bb.edn
  50. 46 0
      libs/cljs-sdk/build.clj
  51. 20 0
      libs/cljs-sdk/deps.edn
  52. 27 0
      libs/cljs-sdk/package.json
  53. 348 0
      libs/cljs-sdk/src/com/logseq/app.cljs
  54. 37 0
      libs/cljs-sdk/src/com/logseq/assets.cljs
  55. 149 0
      libs/cljs-sdk/src/com/logseq/core.cljs
  56. 34 0
      libs/cljs-sdk/src/com/logseq/db.cljs
  57. 432 0
      libs/cljs-sdk/src/com/logseq/editor.cljs
  58. 23 0
      libs/cljs-sdk/src/com/logseq/git.cljs
  59. 49 0
      libs/cljs-sdk/src/com/logseq/ui.cljs
  60. 31 0
      libs/cljs-sdk/src/com/logseq/util.cljs
  61. 121 0
      libs/cljs-sdk/yarn.lock
  62. 3 1
      libs/package.json
  63. 184 0
      libs/scripts/extract-sdk-schema.js
  64. 3 1
      libs/src/LSPlugin.core.ts
  65. 5 0
      libs/src/LSPlugin.ts
  66. 1 0
      libs/src/LSPlugin.user.ts
  67. 147 0
      libs/yarn.lock
  68. 1 1
      package.json
  69. 0 0
      resources/js/lsplugin.core.js
  70. 2 2
      resources/package.json
  71. 1 1
      scripts/package.json
  72. 234 0
      scripts/src/logseq/libs/sdk_generator.clj
  73. 3 3
      scripts/yarn.lock
  74. 4 0
      src/main/frontend/common.css
  75. 1 1
      src/main/frontend/components/container.css
  76. 1 1
      src/main/frontend/components/editor.cljs
  77. 1 2
      src/main/frontend/components/left_sidebar.cljs
  78. 1 0
      src/main/frontend/components/property.cljs
  79. 0 3
      src/main/frontend/components/property.css
  80. 26 36
      src/main/frontend/components/property/value.cljs
  81. 4 0
      src/main/frontend/components/property/value.css
  82. 6 6
      src/main/frontend/components/reference.cljs
  83. 70 62
      src/main/frontend/components/views.cljs
  84. 1 4
      src/main/frontend/db/async.cljs
  85. 1 37
      src/main/frontend/db/model.cljs
  86. 3 3
      src/main/frontend/handler/db_based/import.cljs
  87. 22 14
      src/main/frontend/handler/editor.cljs
  88. 4 2
      src/main/frontend/handler/events/ui.cljs
  89. 1 1
      src/main/frontend/handler/whiteboard.cljs
  90. 1 2
      src/main/frontend/modules/outliner/pipeline.cljs
  91. 8 2
      src/main/frontend/ui.cljs
  92. 1 2
      src/main/frontend/undo_redo.cljs
  93. 32 31
      src/main/frontend/worker/commands.cljs
  94. 2 1
      src/main/frontend/worker/crypt.cljs
  95. 29 1
      src/main/frontend/worker/db/validate.cljs
  96. 28 32
      src/main/frontend/worker/db_listener.cljs
  97. 32 5
      src/main/frontend/worker/db_worker.cljs
  98. 3 3
      src/main/frontend/worker/embedding.cljs
  99. 63 142
      src/main/frontend/worker/pipeline.cljs
  100. 5 4
      src/main/frontend/worker/rtc/asset.cljs

+ 9 - 5
AGENTS.md

@@ -2,16 +2,20 @@
 - Use clojure-mcp `clojure_inspect_project` to get project structure.
 - Use clojure-mcp `clojure_inspect_project` to get project structure.
 - `src/`: Core source code
 - `src/`: Core source code
   - `src/main/`: The core logic of the application
   - `src/main/`: The core logic of the application
-	- `src/main/mobile/`: Mobile app code
-	- `src/main/frontend/inference_worker/`: Code running in a webworker for text-embedding and vector-search
-	- `src/main/frontend/worker/`: Code running in an another webworker
-		- `src/main/frontend/worker/rtc/`: RTC(Real Time Collaboration) related code
-	- `src/main/frontend/components/`: UI components
+    - `src/main/mobile/`: Mobile app code
+    - `src/main/frontend/inference_worker/`: Code running in a webworker for text-embedding and vector-search
+    - `src/main/frontend/worker/`: Code running in an another webworker
+        - `src/main/frontend/worker/rtc/`: RTC(Real Time Collaboration) related code
+    - `src/main/frontend/components/`: UI components
   - `src/electron/`: Code specifically for the Electron desktop application.
   - `src/electron/`: Code specifically for the Electron desktop application.
   - `src/test/`: unit-tests
   - `src/test/`: unit-tests
 - `deps/`: Internal dependencies/modules
 - `deps/`: Internal dependencies/modules
 - `clj-e2e/`: End to end test code
 - `clj-e2e/`: End to end test code
 
 
+## Common used cljs keywords
+- All commonly used ClojureScript keywords are defined using `logseq.common.defkeywords/defkeyword`.
+- Search for `defkeywords` to find all the definitions.
+
 ## Testing Commands
 ## Testing Commands
 - Run linters and unit-tests: `bb dev:lint-and-test`
 - Run linters and unit-tests: `bb dev:lint-and-test`
 - Run single focused unit-test:
 - Run single focused unit-test:

+ 8 - 1
bb.edn

@@ -10,7 +10,9 @@
   logseq/graph-parser
   logseq/graph-parser
   {:local/root "deps/graph-parser"}
   {:local/root "deps/graph-parser"}
   org.clj-commons/digest
   org.clj-commons/digest
-  {:mvn/version "1.4.100"}}
+  {:mvn/version "1.4.100"}
+  cheshire/cheshire
+  {:mvn/version "5.12.0"}}
  :pods
  :pods
  {clj-kondo/clj-kondo {:version "2024.09.27"}
  {clj-kondo/clj-kondo {:version "2024.09.27"}
   org.babashka/fswatcher {:version "0.0.3"}
   org.babashka/fswatcher {:version "0.0.3"}
@@ -19,6 +21,11 @@
  {dev:desktop-watch
  {dev:desktop-watch
   logseq.tasks.dev.desktop/watch
   logseq.tasks.dev.desktop/watch
 
 
+  libs:generate-cljs-sdk
+  {:doc "Generate CLJS wrappers based on the JS SDK declarations"
+   :requires ([logseq.libs.sdk-generator :as sdk-gen])
+   :task (sdk-gen/run! (sdk-gen/parse-args *command-line-args*))}
+
   dev:open-dev-electron-app
   dev:open-dev-electron-app
   logseq.tasks.dev.desktop/open-dev-electron-app
   logseq.tasks.dev.desktop/open-dev-electron-app
 
 

+ 243 - 26
clj-e2e/test/logseq/e2e/plugins_basic_test.clj

@@ -1,5 +1,6 @@
 (ns logseq.e2e.plugins-basic-test
 (ns logseq.e2e.plugins-basic-test
   (:require
   (:require
+   [clojure.set :as set]
    [clojure.string :as string]
    [clojure.string :as string]
    [clojure.test :refer [deftest testing is use-fixtures]]
    [clojure.test :refer [deftest testing is use-fixtures]]
    [jsonista.core :as json]
    [jsonista.core :as json]
@@ -14,6 +15,10 @@
 (use-fixtures :once fixtures/open-page)
 (use-fixtures :once fixtures/open-page)
 (use-fixtures :each fixtures/new-logseq-page)
 (use-fixtures :each fixtures/new-logseq-page)
 
 
+(defn ->plugin-ident
+  [property-name]
+  (str ":plugin.property._test_plugin/" property-name))
+
 (defn- to-snake-case
 (defn- to-snake-case
   "Converts a string to snake_case. Handles camelCase, PascalCase, spaces, hyphens, and existing underscores.
   "Converts a string to snake_case. Handles camelCase, PascalCase, spaces, hyphens, and existing underscores.
    Examples:
    Examples:
@@ -31,9 +36,14 @@
       ;; Remove redundant underscores and trim
       ;; Remove redundant underscores and trim
         (clojure.string/replace #"_+" "_")
         (clojure.string/replace #"_+" "_")
         (clojure.string/trim)
         (clojure.string/trim)
-      ;; Convert to lowercase
+        ;; Convert to lowercase
         (clojure.string/lower-case))))
         (clojure.string/lower-case))))
 
 
+(defonce ^:private *property-idx (atom 0))
+(defn- new-property
+  []
+  (str "p" (swap! *property-idx inc)))
+
 (defn- ls-api-call!
 (defn- ls-api-call!
   [tag & args]
   [tag & args]
   (let [tag (name tag)
   (let [tag (name tag)
@@ -43,10 +53,10 @@
         ns1 (string/lower-case (if (and ns? (not inbuilt?))
         ns1 (string/lower-case (if (and ns? (not inbuilt?))
                                  (str "sdk." (first ns')) "api"))
                                  (str "sdk." (first ns')) "api"))
         name1 (if ns? (to-snake-case (last ns')) tag)
         name1 (if ns? (to-snake-case (last ns')) tag)
-        estr (format "s => { const args = JSON.parse(s);const o=logseq.%1$s; return o['%2$s']?.apply(null, args || []); }" ns1 name1)]
-    (let [args (json/write-value-as-string (vec args))]
-      ;(prn "Debug: eval-js #" estr args)
-      (w/eval-js estr args))))
+        estr (format "s => { const args = JSON.parse(s);const o=logseq.%1$s; return o['%2$s']?.apply(null, args || []); }" ns1 name1)
+        args (json/write-value-as-string (vec args))]
+    ;; (prn "Debug: eval-js #" estr args)
+    (w/eval-js estr args)))
 
 
 (defn- assert-api-ls-block!
 (defn- assert-api-ls-block!
   ([ret] (assert-api-ls-block! ret 1))
   ([ret] (assert-api-ls-block! ret 1))
@@ -56,22 +66,23 @@
      (assert/assert-have-count (str "#ls-block-" uuid') count)
      (assert/assert-have-count (str "#ls-block-" uuid') count)
      uuid')))
      uuid')))
 
 
-(deftest apis-related-test
-  (testing "block related apis"
+(deftest editor-apis-test
+  (testing "editor related apis"
     (page/new-page "test-block-apis")
     (page/new-page "test-block-apis")
     (ls-api-call! :ui.showMsg "hello world" "info")
     (ls-api-call! :ui.showMsg "hello world" "info")
     (let [ret (ls-api-call! :editor.appendBlockInPage "test-block-apis" "append-block-in-page-0")
     (let [ret (ls-api-call! :editor.appendBlockInPage "test-block-apis" "append-block-in-page-0")
           ret1 (ls-api-call! :editor.appendBlockInPage "append-block-in-current-page-0")
           ret1 (ls-api-call! :editor.appendBlockInPage "append-block-in-current-page-0")
-          _ (assert-api-ls-block! ret1)
           uuid' (assert-api-ls-block! ret)]
           uuid' (assert-api-ls-block! ret)]
+      (assert-api-ls-block! ret1)
       (-> (ls-api-call! :editor.insertBlock uuid' "insert-0")
       (-> (ls-api-call! :editor.insertBlock uuid' "insert-0")
           (assert-api-ls-block!))
           (assert-api-ls-block!))
       (ls-api-call! :editor.updateBlock uuid' "append-but-updated-0")
       (ls-api-call! :editor.updateBlock uuid' "append-but-updated-0")
       (k/esc)
       (k/esc)
       (w/wait-for ".block-title-wrap:text('append-but-updated-0')")
       (w/wait-for ".block-title-wrap:text('append-but-updated-0')")
       (ls-api-call! :editor.removeBlock uuid')
       (ls-api-call! :editor.removeBlock uuid')
-      (assert-api-ls-block! uuid' 0)))
+      (assert-api-ls-block! uuid' 0))))
 
 
+(deftest block-properties-test
   (testing "block properties related apis"
   (testing "block properties related apis"
     (page/new-page "test-block-properties-apis")
     (page/new-page "test-block-properties-apis")
     (let [ret (ls-api-call! :editor.appendBlockInPage "test-block-properties-apis" "block-in-page-0" {:properties {:p1 1}})
     (let [ret (ls-api-call! :editor.appendBlockInPage "test-block-properties-apis" "block-in-page-0" {:properties {:p1 1}})
@@ -81,8 +92,8 @@
           props2 (ls-api-call! :editor.getPageProperties "test-block-properties-apis")]
           props2 (ls-api-call! :editor.getPageProperties "test-block-properties-apis")]
       (w/wait-for ".property-k:text('p1')")
       (w/wait-for ".property-k:text('p1')")
       (is (= 1 (get prop1 "value")))
       (is (= 1 (get prop1 "value")))
-      (is (= (get prop1 "ident") ":plugin.property._api/p1"))
-      (is (= 1 (get props1 ":plugin.property._api/p1")))
+      (is (= (get prop1 "ident") ":plugin.property._test_plugin/p1"))
+      (is (= 1 (get props1 ":plugin.property._test_plugin/p1")))
       (is (= ["Page"] (get props2 ":block/tags")))
       (is (= ["Page"] (get props2 ":block/tags")))
       (ls-api-call! :editor.upsertBlockProperty uuid' "p2" "p2")
       (ls-api-call! :editor.upsertBlockProperty uuid' "p2" "p2")
       (ls-api-call! :editor.upsertBlockProperty uuid' "p3" true)
       (ls-api-call! :editor.upsertBlockProperty uuid' "p3" true)
@@ -102,20 +113,226 @@
       (ls-api-call! :editor.upsertBlockProperty uuid' "p2" "p2-updated")
       (ls-api-call! :editor.upsertBlockProperty uuid' "p2" "p2-updated")
       (w/wait-for ".block-title-wrap:text('p2-updated')")
       (w/wait-for ".block-title-wrap:text('p2-updated')")
       (let [props (ls-api-call! :editor.getBlockProperties uuid')]
       (let [props (ls-api-call! :editor.getBlockProperties uuid')]
-        (is (= (get props ":plugin.property._api/p3") false))
-        (is (= (get props ":plugin.property._api/p2") "p2-updated")))))
+        (is (= (get props ":plugin.property._test_plugin/p3") false))
+        (is (= (get props ":plugin.property._test_plugin/p2") "p2-updated"))))))
+
+(deftest property-upsert-test
+  (testing "property with default settings"
+    (let [p (new-property)]
+      (ls-api-call! :editor.upsertProperty p)
+      (let [property (ls-api-call! :editor.getProperty p)]
+        (is (= "default" (get property "type")))
+        (is (= ":db.cardinality/one" (get property "cardinality"))))))
+  (testing "property with specified cardinality && type"
+    (let [p (new-property)]
+      (ls-api-call! :editor.upsertProperty p {:type "number"
+                                              :cardinality "one"})
+      (let [property (ls-api-call! :editor.getProperty p)]
+        (is (= "number" (get property "type")))
+        (is (= ":db.cardinality/one" (get property "cardinality")))))
+    (let [p (new-property)]
+      (ls-api-call! :editor.upsertProperty p {:type "number"
+                                              :cardinality "many"})
+      (let [property (ls-api-call! :editor.getProperty p)]
+        (is (= "number" (get property "type")))
+        (is (= ":db.cardinality/many" (get property "cardinality"))))
+      (ls-api-call! :editor.upsertProperty p {:type "default"})
+      (let [property (ls-api-call! :editor.getProperty p)]
+        (is (= "default" (get property "type"))))))
+  ;; TODO: How to test against eval-js errors on playwright?
+  #_(testing ":checkbox property doesn't allow :many cardinality"
+      (let [p (new-property)]
+        (ls-api-call! :editor.upsertProperty p {:type "checkbox"
+                                                :cardinality "many"}))))
 
 
+(deftest property-related-test
   (testing "properties management related apis"
   (testing "properties management related apis"
-    (let [_ (ls-api-call! :editor.upsertProperty "o1")
-          _ (ls-api-call! :editor.upsertProperty "o2" {:type "number"})
-          _ (ls-api-call! :editor.upsertProperty "user.property/o3" {:type "node"})
-          prop1 (ls-api-call! :editor.getProperty "o1")
-          prop2 (ls-api-call! :editor.getProperty "o2")
-          prop3 (ls-api-call! :editor.getProperty "user.property/o3")]
-      (is (= (get prop1 "ident") ":plugin.property._api/o1"))
-      (is (= (get prop1 "type") "default"))
-      (is (= (get prop2 "type") "number"))
-      (is (= (get prop3 "ident") ":user.property/o3"))
-      (is (= (get prop3 "type") "node"))
-      (ls-api-call! :editor.removeProperty "o2")
-      (is (nil? (w/find-one-by-text ".property-k" "o2"))))))
+    (dorun
+     (map-indexed
+      (fn [idx property-type]
+        (let [property-name (str "p" idx)
+              _ (ls-api-call! :editor.upsertProperty property-name {:type property-type})
+              property (ls-api-call! :editor.getProperty property-name)]
+          (is (= (get property "ident") (str ":plugin.property._test_plugin/" property-name)))
+          (is (= (get property "type") property-type))
+          (ls-api-call! :editor.removeProperty property-name)
+          (is (nil? (ls-api-call! :editor.getProperty property-name)))))
+      ["default" "number" "date" "datetime" "checkbox" "url" "node" "json" "string"]))))
+
+(deftest insert-block-with-properties
+  (testing "insert block with properties"
+    (let [page "insert-block-properties-test"
+          _ (page/new-page page)
+          ;; :checkbox, :number, :url, :json can be inferred and default to :default, but not for :page
+          b1 (ls-api-call! :editor.insertBlock page "b1" {:properties {"x1" true
+                                                                       "x2" "https://logseq.com"
+                                                                       "x3" 1
+                                                                       "x4" [1]
+                                                                       "x5" {:foo "bar"}
+                                                                       "x6" "Page x"
+                                                                       "x7" ["Page y" "Page z"]
+                                                                       "x8" "some content"}
+                                                          :schema {"x6" {:type "page"}
+                                                                   "x7" {:type "page"}}})]
+      (is (true? (get b1 (->plugin-ident "x1"))))
+      (is (= "https://logseq.com" (-> (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x2")))
+                                      (get "title"))))
+      (is (= 1 (-> (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x3")))
+                   (get ":logseq.property/value"))))
+      (is (= 1 (-> (ls-api-call! :editor.getBlock (first (get b1 (->plugin-ident "x4"))))
+                   (get ":logseq.property/value"))))
+      (is (= "{\"foo\":\"bar\"}" (get b1 (->plugin-ident "x5"))))
+      (let [page-x (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x6")))]
+        (is (= "page x" (get page-x "name"))))
+      (is (= ["page y" "page z"] (map #(-> (ls-api-call! :editor.getBlock %)
+                                           (get "name")) (get b1 (->plugin-ident "x7")))))
+      (let [x8-block-value (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x8")))]
+        (is (= "some content" (get x8-block-value "title")))
+        (is (some? (get x8-block-value "page")))))))
+
+(deftest update-block-with-properties
+  (testing "update block with properties"
+    (let [page "update-block-properties-test"
+          _ (page/new-page page)
+          block (ls-api-call! :editor.insertBlock page "b1")
+          _ (ls-api-call! :editor.updateBlock (get block "uuid")
+                          "b1-new-content"
+                          {:properties {"y1" true
+                                        "y2" "https://logseq.com"
+                                        "y3" 1
+                                        "y4" [1]
+                                        "y5" {:foo "bar"}
+                                        "y6" "Page x"
+                                        "y7" ["Page y" "Page z"]
+                                        "y8" "some content"}
+                           :schema {"y6" {:type "page"}
+                                    "y7" {:type "page"}}})
+          b1 (ls-api-call! :editor.getBlock (get block "uuid"))]
+      (is (true? (get b1 (->plugin-ident "y1"))))
+      (is (= "https://logseq.com" (-> (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y2") "id"]))
+                                      (get "title"))))
+      (is (= 1 (-> (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y3") "id"]))
+                   (get ":logseq.property/value"))))
+      (is (= 1 (-> (ls-api-call! :editor.getBlock (get (first (get b1 (->plugin-ident "y4"))) "id"))
+                   (get ":logseq.property/value"))))
+      (is (= "{\"foo\":\"bar\"}" (get b1 (->plugin-ident "y5"))))
+      (let [page-x (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y6") "id"]))]
+        (is (= "page x" (get page-x "name"))))
+      (is (= ["page y" "page z"] (map #(-> (ls-api-call! :editor.getBlock %)
+                                           (get "name"))
+                                      (map #(get % "id") (get b1 (->plugin-ident "y7"))))))
+      (let [y8-block-value (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y8") "id"]))]
+        (is (= "some content" (get y8-block-value "title")))
+        (is (some? (get y8-block-value "page")))))))
+
+(deftest insert-batch-blocks-test
+  (testing "insert batch blocks"
+    (let [page "insert batch blocks"
+          _ (page/new-page page)
+          page-uuid (get (ls-api-call! :editor.getBlock page) "uuid")
+          result (ls-api-call! :editor.insertBatchBlock page-uuid
+                               [{:content "b1"
+                                 :children [{:content "b1.1"
+                                             :children [{:content "b1.1.1"}
+                                                        {:content "b1.1.2"}]}
+                                            {:content "b1.2"}]}
+                                {:content "b2"}])
+          contents (util/get-page-blocks-contents)]
+      (is (= contents ["b1" "b1.1" "b1.1.1" "b1.1.2" "b1.2" "b2"]))
+      (is (= (map #(get % "title") result) ["b1" "b1.1" "b1.1.1" "b1.1.2" "b1.2" "b2"]))))
+  (testing "insert batch blocks with properties"
+    (let [page "insert batch blocks with properties"
+          _ (page/new-page page)
+          page-uuid (get (ls-api-call! :editor.getBlock page) "uuid")
+          result (ls-api-call! :editor.insertBatchBlock page-uuid
+                               [{:content "b1"
+                                 :children [{:content "b1.1"
+                                             :children [{:content "b1.1.1"
+                                                         :properties {"z3" "Page 1"
+                                                                      "z4" ["Page 2" "Page 3"]}}
+                                                        {:content "b1.1.2"}]}
+                                            {:content "b1.2"}]
+                                 :properties {"z1" "test"
+                                              "z2" true}}
+                                {:content "b2"}]
+                               {:schema {"z3" "page"
+                                         "z4" "page"}})
+          contents (util/get-page-blocks-contents)]
+      (is (= contents
+             ["b1" "test" "b1.1" "b1.1.1" "Page 1" "Page 2" "Page 3" "b1.1.2" "b1.2" "b2"]))
+      (is (true? (get (first result) (->plugin-ident "z2")))))))
+
+(deftest create-page-test
+  (testing "create page"
+    (let [result (ls-api-call! :editor.createPage "Test page 1")]
+      (is (= "Test page 1" (get result "title")))
+      (is
+       (=
+        ":logseq.class/Page"
+        (-> (ls-api-call! :editor.getBlock (first (get result "tags")))
+            (get "ident"))))))
+
+  (testing "create page with properties"
+    (let [result (ls-api-call! :editor.createPage "Test page 2"
+                               {:px1 "test"
+                                :px2 1
+                                :px3 "Page 1"
+                                :px4 ["Page 2" "Page 3"]}
+                               {:schema {:px3 {:type "page"}
+                                         :px4 {:type "page"}}})
+          page (ls-api-call! :editor.getBlock "Test page 2")]
+      (is (= "Test page 2" (get result "title")))
+      (is
+       (=
+        ":logseq.class/Page"
+        (-> (ls-api-call! :editor.getBlock (first (get result "tags")))
+            (get "ident"))))
+      ;; verify properties
+      (is (= "test" (-> (ls-api-call! :editor.getBlock (get-in page [(->plugin-ident "px1") "id"]))
+                        (get "title"))))
+      (is (= 1 (-> (ls-api-call! :editor.getBlock (get-in page [(->plugin-ident "px2") "id"]))
+                   (get ":logseq.property/value"))))
+      (let [page-1 (ls-api-call! :editor.getBlock (get-in page [(->plugin-ident "px3") "id"]))]
+        (is (= "page 1" (get page-1 "name"))))
+      (is (= ["page 2" "page 3"] (map #(-> (ls-api-call! :editor.getBlock %)
+                                           (get "name"))
+                                      (map #(get % "id") (get page (->plugin-ident "px4"))))))))
+
+  (testing "create tag page"
+    (let [result (ls-api-call! :editor.createPage "Tag new"
+                               {}
+                               {:class true})]
+      (is
+       (=
+        ":logseq.class/Tag"
+        (-> (ls-api-call! :editor.getBlock (first (get result "tags")))
+            (get "ident")))))))
+
+(deftest get-all-tags-test
+  (testing "get_all_tags"
+    (let [result (ls-api-call! :editor.get_all_tags)
+          built-in-tags #{":logseq.class/Template"
+                          ":logseq.class/Query"
+                          ":logseq.class/Math-block"
+                          ":logseq.class/Pdf-annotation"
+                          ":logseq.class/Task"
+                          ":logseq.class/Code-block"
+                          ":logseq.class/Card"
+                          ":logseq.class/Quote-block"
+                          ":logseq.class/Cards"}]
+      (is (set/subset? built-in-tags (set (map #(get % "ident") result)))))))
+
+(deftest get-all-properties-test
+  (testing "get_all_properties"
+    (let [result (ls-api-call! :editor.get_all_properties)]
+      (is (>= (count result) 94)))))
+
+(deftest get-tag-objects-test
+  (testing "get_tag_objects"
+    (let [page "tag objects test"
+          _ (page/new-page page)
+          _ (ls-api-call! :editor.insertBlock page "task 1"
+                          {:properties {"logseq.property/status" "Doing"}})
+          result (ls-api-call! :editor.get_tag_objects "logseq.class/Task")]
+      (is (= (count result) 1))
+      (is (= "task 1" (get (first result) "title"))))))

+ 2 - 1
deps.edn

@@ -5,7 +5,8 @@
                                          :sha     "5d672bf84ed944414b9f61eeb83808ead7be9127"}
                                          :sha     "5d672bf84ed944414b9f61eeb83808ead7be9127"}
 
 
   datascript/datascript                 {:git/url "https://github.com/logseq/datascript" ;; fork
   datascript/datascript                 {:git/url "https://github.com/logseq/datascript" ;; fork
-                                         :sha     "45f6721bf2038c24eb9fe3afb422322ab3f473b5"}
+                                         :sha     "3971e2d43bd93d89f42191dc7b4b092989e0cc61"}
+  ;; datascript/datascript                 {:local/root "../../datascript"}
 
 
   datascript-transit/datascript-transit {:mvn/version "0.3.0"}
   datascript-transit/datascript-transit {:mvn/version "0.3.0"}
   borkdude/rewrite-edn                  {:mvn/version "0.4.9"}
   borkdude/rewrite-edn                  {:mvn/version "0.4.9"}

+ 1 - 1
deps/cli/package.json

@@ -10,7 +10,7 @@
   },
   },
   "license": "MIT",
   "license": "MIT",
   "dependencies": {
   "dependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28",
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v29",
     "@modelcontextprotocol/sdk": "^1.17.5",
     "@modelcontextprotocol/sdk": "^1.17.5",
     "better-sqlite3": "~11.10.0",
     "better-sqlite3": "~11.10.0",
     "fastify": "5.3.2",
     "fastify": "5.3.2",

+ 1 - 1
deps/cli/src/logseq/cli/common/mcp/tools.cljs

@@ -79,7 +79,7 @@
   [m]
   [m]
   (->> (remove (fn [[k _v]]
   (->> (remove (fn [[k _v]]
                  (or (= "block.temp" (namespace k))
                  (or (= "block.temp" (namespace k))
-                     (contains? #{:logseq.property.embedding/hnsw-label-updated-at} k))) m)
+                     (contains? #{:logseq.property.embedding/hnsw-label-updated-at :block/tx-id} k))) m)
        (into {})))
        (into {})))
 
 
 (defn get-page-data
 (defn get-page-data

+ 3 - 3
deps/cli/yarn.lock

@@ -43,9 +43,9 @@
     "@fastify/forwarded" "^3.0.0"
     "@fastify/forwarded" "^3.0.0"
     ipaddr.js "^2.1.0"
     ipaddr.js "^2.1.0"
 
 
-"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v28":
-  version "1.2.173-feat-db-v28"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/c0410ff81a8b0d510705581cccd39788d862dc91"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v29":
+  version "1.2.173-feat-db-v29"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/6c4f8eec72a0a5b0c7b96c32cc73f86b045305c5"
   dependencies:
   dependencies:
     import-meta-resolve "^4.1.0"
     import-meta-resolve "^4.1.0"
 
 

+ 1 - 1
deps/common/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "version": "1.0.0",
   "private": true,
   "private": true,
   "devDependencies": {
   "devDependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v29"
   },
   },
   "scripts": {
   "scripts": {
     "test": "yarn nbb-logseq -cp test -m nextjournal.test-runner"
     "test": "yarn nbb-logseq -cp test -m nextjournal.test-runner"

+ 0 - 12
deps/common/src/logseq/common/util.cljs

@@ -6,7 +6,6 @@
             [cljs.reader :as reader]
             [cljs.reader :as reader]
             [clojure.edn :as edn]
             [clojure.edn :as edn]
             [clojure.string :as string]
             [clojure.string :as string]
-            [clojure.walk :as walk]
             [goog.string :as gstring]
             [goog.string :as gstring]
             [logseq.common.log :as log]))
             [logseq.common.log :as log]))
 
 
@@ -24,17 +23,6 @@
   [s]
   [s]
   (.normalize s "NFC"))
   (.normalize s "NFC"))
 
 
-(defn remove-nils
-  "remove pairs of key-value that has nil value from a (possibly nested) map or
-  coll of maps."
-  [nm]
-  (walk/postwalk
-   (fn [el]
-     (if (map? el)
-       (into {} (remove (comp nil? second)) el)
-       el))
-   nm))
-
 (defn remove-nils-non-nested
 (defn remove-nils-non-nested
   "remove pairs of key-value that has nil value from a map (nested not supported)."
   "remove pairs of key-value that has nil value from a map (nested not supported)."
   [nm]
   [nm]

+ 3 - 3
deps/common/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 # yarn lockfile v1
 
 
 
 
-"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v28":
-  version "1.2.173-feat-db-v28"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/c0410ff81a8b0d510705581cccd39788d862dc91"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v29":
+  version "1.2.173-feat-db-v29"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/6c4f8eec72a0a5b0c7b96c32cc73f86b045305c5"
   dependencies:
   dependencies:
     import-meta-resolve "^4.1.0"
     import-meta-resolve "^4.1.0"
 
 

+ 2 - 0
deps/db/.carve/ignore

@@ -46,5 +46,7 @@ logseq.db.sqlite.gc/gc-kvs-table!
 logseq.db.sqlite.gc/gc-kvs-table-node-version!
 logseq.db.sqlite.gc/gc-kvs-table-node-version!
 ;; API
 ;; API
 logseq.db.sqlite.gc/ensure-no-garbage
 logseq.db.sqlite.gc/ensure-no-garbage
+;; API
+logseq.db.common.entity-util/entity->map
 ;; documenting keywords
 ;; documenting keywords
 logseq.db.frontend.kv-entity/kv-entities
 logseq.db.frontend.kv-entity/kv-entities

+ 2 - 1
deps/db/deps.edn

@@ -1,7 +1,8 @@
 {:deps
 {:deps
  ;; These nbb-logseq deps are kept in sync with https://github.com/logseq/nbb-logseq/blob/main/bb.edn
  ;; These nbb-logseq deps are kept in sync with https://github.com/logseq/nbb-logseq/blob/main/bb.edn
  {datascript/datascript {:git/url "https://github.com/logseq/datascript" ;; fork
  {datascript/datascript {:git/url "https://github.com/logseq/datascript" ;; fork
-                         :sha     "45f6721bf2038c24eb9fe3afb422322ab3f473b5"}
+                         :sha     "3971e2d43bd93d89f42191dc7b4b092989e0cc61"}
+  ;; datascript/datascript                 {:local/root "../../../../datascript"}
   datascript-transit/datascript-transit {:mvn/version "0.3.0"
   datascript-transit/datascript-transit {:mvn/version "0.3.0"
                                          :exclusions [datascript/datascript]}
                                          :exclusions [datascript/datascript]}
   cljs-bean/cljs-bean         {:mvn/version "1.5.0"}
   cljs-bean/cljs-bean         {:mvn/version "1.5.0"}

+ 1 - 1
deps/db/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "version": "1.0.0",
   "private": true,
   "private": true,
   "devDependencies": {
   "devDependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v29"
   },
   },
   "dependencies": {
   "dependencies": {
     "better-sqlite3": "11.10.0"
     "better-sqlite3": "11.10.0"

+ 4 - 4
deps/db/script/create_graph.cljs

@@ -5,7 +5,7 @@
             ["path" :as node-path]
             ["path" :as node-path]
             [babashka.cli :as cli]
             [babashka.cli :as cli]
             [clojure.edn :as edn]
             [clojure.edn :as edn]
-            [datascript.core :as d]
+            [logseq.db :as ldb]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
             [logseq.db.sqlite.export :as sqlite-export]
             [logseq.db.sqlite.export :as sqlite-export]
             [logseq.outliner.cli :as outliner-cli]
             [logseq.outliner.cli :as outliner-cli]
@@ -51,9 +51,9 @@
              (count (filter :block/title init-tx)) "blocks ...")
              (count (filter :block/title init-tx)) "blocks ...")
     ;; (fs/writeFileSync "txs.edn" (with-out-str (cljs.pprint/pprint _txs)))
     ;; (fs/writeFileSync "txs.edn" (with-out-str (cljs.pprint/pprint _txs)))
     ;; (cljs.pprint/pprint _txs)
     ;; (cljs.pprint/pprint _txs)
-    (d/transact! conn init-tx)
-    (when (seq block-props-tx) (d/transact! conn block-props-tx))
-    (when (seq misc-tx) (d/transact! conn misc-tx))
+    (ldb/transact! conn init-tx)
+    (when (seq block-props-tx) (ldb/transact! conn block-props-tx))
+    (when (seq misc-tx) (ldb/transact! conn misc-tx))
     (println (if graph-exists? "Updated graph" "Created graph") (str db-name "!"))
     (println (if graph-exists? "Updated graph" "Created graph") (str db-name "!"))
     (when (:validate options)
     (when (:validate options)
       (validate-db/validate-db @conn db-name {:group-errors true :closed-maps true :humanize true}))))
       (validate-db/validate-db @conn db-name {:group-errors true :closed-maps true :humanize true}))))

+ 83 - 50
deps/db/src/logseq/db.cljs

@@ -5,6 +5,7 @@
   (:require [clojure.set :as set]
   (:require [clojure.set :as set]
             [clojure.string :as string]
             [clojure.string :as string]
             [clojure.walk :as walk]
             [clojure.walk :as walk]
+            [datascript.conn :as dc]
             [datascript.core :as d]
             [datascript.core :as d]
             [datascript.impl.entity :as de]
             [datascript.impl.entity :as de]
             [logseq.common.config :as common-config]
             [logseq.common.config :as common-config]
@@ -20,6 +21,7 @@
             [logseq.db.frontend.entity-util :as entity-util]
             [logseq.db.frontend.entity-util :as entity-util]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.frontend.schema :as db-schema]
+            [logseq.db.frontend.validate :as db-validate]
             [logseq.db.sqlite.util :as sqlite-util])
             [logseq.db.sqlite.util :as sqlite-util])
   (:refer-clojure :exclude [object?]))
   (:refer-clojure :exclude [object?]))
 
 
@@ -32,9 +34,18 @@
 (def build-favorite-tx db-db/build-favorite-tx)
 (def build-favorite-tx db-db/build-favorite-tx)
 
 
 (defonce *transact-fn (atom nil))
 (defonce *transact-fn (atom nil))
+(defonce *transact-invalid-callback (atom nil))
+(defonce *transact-pipeline-fn (atom nil))
+
 (defn register-transact-fn!
 (defn register-transact-fn!
   [f]
   [f]
   (when f (reset! *transact-fn f)))
   (when f (reset! *transact-fn f)))
+(defn register-transact-invalid-callback-fn!
+  [f]
+  (when f (reset! *transact-invalid-callback f)))
+(defn register-transact-pipeline-fn!
+  [f]
+  (when f (reset! *transact-pipeline-fn f)))
 
 
 (defn- remove-temp-block-data
 (defn- remove-temp-block-data
   [tx-data]
   [tx-data]
@@ -59,35 +70,87 @@
               data))
               data))
           tx-data)))
           tx-data)))
 
 
-(defn assert-no-entities
+(defn entity->db-id
   [tx-data]
   [tx-data]
   (walk/prewalk
   (walk/prewalk
    (fn [f]
    (fn [f]
      (if (de/entity? f)
      (if (de/entity? f)
-       (throw (ex-info "ldb/transact! doesn't support Entity"
-                       {:entity f
-                        :tx-data tx-data}))
+       (if-let [id (:db/id f)]
+         id
+         (throw (ex-info "ldb/transact! doesn't support Entity"
+                         {:entity f
+                          :tx-data tx-data})))
        f))
        f))
    tx-data))
    tx-data))
 
 
+(comment
+  (defn- skip-db-validate?
+    [datoms]
+    (every?
+     (fn [d]
+       (contains? #{:logseq.property/created-by-ref :block/refs :block/tx-id}
+                  (:a d)))
+     datoms)))
+
+(defn- throw-if-page-has-block-parent!
+  [db tx-data]
+  (when (some (fn [d] (and (:added d)
+                           (= :block/parent (:a d))
+                           (entity-util/page? (d/entity db (:e d)))
+                           (not (entity-util/page? (d/entity db (:v d)))))) tx-data)
+    (throw (ex-info "Page can't have block as parent"
+                    {:tx-data tx-data}))))
+
+(defn- transact-sync
+  [repo-or-conn tx-data tx-meta]
+  (try
+    (let [conn repo-or-conn
+          db @conn
+          db-based? (entity-plus/db-based-graph? db)]
+      (if (and db-based?
+               (not (:reset-conn! tx-meta))
+               (not (:initial-db? tx-meta))
+               (not (:skip-validate-db? tx-meta false))
+               (not (:logseq.graph-parser.exporter/new-graph? tx-meta)))
+        (let [tx-report* (d/with db tx-data tx-meta)
+              pipeline-f @*transact-pipeline-fn
+              tx-report (if-let [f pipeline-f] (f tx-report*) tx-report*)
+              _ (throw-if-page-has-block-parent! (:db-after tx-report) (:tx-data tx-report))
+              validate-result (db-validate/validate-tx-report tx-report nil)]
+          (if validate-result
+            (when (and tx-report (seq (:tx-data tx-report)))
+              ;; perf enhancement: avoid repeated call on `d/with`
+              (reset! conn (:db-after tx-report))
+              (dc/store-after-transact! conn tx-report)
+              (dc/run-callbacks conn tx-report))
+            (do
+              ;; notify ui
+              (when-let [f @*transact-invalid-callback]
+                (f tx-report))
+              (throw (ex-info "DB write failed with invalid data" {:tx-data tx-data}))))
+          tx-report)
+        (d/transact! conn tx-data tx-meta)))
+    (catch :default e
+      (prn :debug :transact-failed :tx-meta tx-meta :tx-data tx-data)
+      (throw e))))
+
 (defn transact!
 (defn transact!
   "`repo-or-conn`: repo for UI thread and conn for worker/node"
   "`repo-or-conn`: repo for UI thread and conn for worker/node"
   ([repo-or-conn tx-data]
   ([repo-or-conn tx-data]
    (transact! repo-or-conn tx-data nil))
    (transact! repo-or-conn tx-data nil))
   ([repo-or-conn tx-data tx-meta]
   ([repo-or-conn tx-data tx-meta]
-   (when (or (exists? js/process)
-             (and (exists? js/goog) js/goog.DEBUG))
-     (assert-no-entities tx-data))
-   (let [tx-data (map (fn [m]
-                        (if (map? m)
-                          (cond->
-                           (dissoc m :block/children :block/meta :block/top? :block/bottom? :block/anchor
-                                   :block/level :block/container :db/other-tx
-                                   :block/unordered)
-                            (not @*transact-fn)
-                            (dissoc :block.temp/load-status))
-                          m)) tx-data)
-         tx-data (->> (remove-temp-block-data tx-data)
+   (let [tx-data (->> tx-data
+                      entity->db-id
+                      (map (fn [m]
+                             (if (map? m)
+                               (cond->
+                                (dissoc m :block/children :block/meta :block/top? :block/bottom? :block/anchor
+                                        :block/level :block/container :db/other-tx
+                                        :block/unordered)
+                                 (not @*transact-fn)
+                                 (dissoc :block.temp/load-status))
+                               m)))
+                      (remove-temp-block-data)
                       (common-util/fast-remove-nils)
                       (common-util/fast-remove-nils)
                       (remove empty?))
                       (remove empty?))
          delete-blocks-tx (when-not (string? repo-or-conn)
          delete-blocks-tx (when-not (string? repo-or-conn)
@@ -96,7 +159,7 @@
 
 
      ;; Ensure worker can handle the request sequentially (one by one)
      ;; Ensure worker can handle the request sequentially (one by one)
      ;; Because UI assumes that the in-memory db has all the data except the last one transaction
      ;; Because UI assumes that the in-memory db has all the data except the last one transaction
-     (when (or (seq tx-data) (:db-persist? tx-meta))
+     (when (seq tx-data)
 
 
        ;; (prn :debug :transact :sync? (= d/transact! (or @*transact-fn d/transact!)) :tx-meta tx-meta)
        ;; (prn :debug :transact :sync? (= d/transact! (or @*transact-fn d/transact!)) :tx-meta tx-meta)
        ;; (cljs.pprint/pprint tx-data)
        ;; (cljs.pprint/pprint tx-data)
@@ -104,12 +167,7 @@
 
 
        (if-let [transact-fn @*transact-fn]
        (if-let [transact-fn @*transact-fn]
          (transact-fn repo-or-conn tx-data tx-meta)
          (transact-fn repo-or-conn tx-data tx-meta)
-         (try
-           (d/transact! repo-or-conn tx-data tx-meta)
-           (catch :default e
-             (js/console.trace)
-             (prn :debug :transact-failed :tx-meta tx-meta :tx-data tx-data)
-             (throw e))))))))
+         (transact-sync repo-or-conn tx-data tx-meta))))))
 
 
 (def page? common-entity-util/page?)
 (def page? common-entity-util/page?)
 (def internal-page? entity-util/internal-page?)
 (def internal-page? entity-util/internal-page?)
@@ -167,7 +225,7 @@
         closed-property (:block/closed-value-property block)]
         closed-property (:block/closed-value-property block)]
     (sort-by-order (cond
     (sort-by-order (cond
                      closed-property
                      closed-property
-                     (:property/closed-values closed-property)
+                     (:block/_closed-value-property closed-property)
 
 
                      from-property
                      from-property
                      (filter (fn [e]
                      (filter (fn [e]
@@ -467,31 +525,6 @@
    (set)
    (set)
    (set/union #{page-id})))
    (set/union #{page-id})))
 
 
-(defn get-block-refs
-  [db id]
-  (let [entity (d/entity db id)
-        db-based? (db-based-graph? db)
-        alias (->> (get-block-alias db id)
-                   (cons id)
-                   distinct)
-        ref-ids (->> (mapcat (fn [id]
-                               (cond->> (:block/_refs (d/entity db id))
-                                 db-based?
-                                 (remove (fn [ref]
-                                           ;; remove refs that have the block as either tag or property
-                                           (or (and
-                                                (class? entity)
-                                                (d/datom db :eavt (:db/id ref) :block/tags (:db/id entity)))
-                                               (and
-                                                (property? entity)
-                                                (d/datom db :eavt (:db/id ref) (:db/ident entity))))))
-                                 true
-                                 (map :db/id)))
-                             alias)
-                     distinct)]
-    (when (seq ref-ids)
-      (d/pull-many db '[*] ref-ids))))
-
 (def get-block-refs-count common-initial-data/get-block-refs-count)
 (def get-block-refs-count common-initial-data/get-block-refs-count)
 
 
 (defn hidden-or-internal-tag?
 (defn hidden-or-internal-tag?

+ 8 - 1
deps/db/src/logseq/db/common/entity_util.cljs

@@ -1,6 +1,7 @@
 (ns logseq.db.common.entity-util
 (ns logseq.db.common.entity-util
   "Lower level entity util fns for DB and file graphs"
   "Lower level entity util fns for DB and file graphs"
-  (:require [logseq.db.file-based.entity-util :as file-entity-util]
+  (:require [datascript.impl.entity :as de]
+            [logseq.db.file-based.entity-util :as file-entity-util]
             [logseq.db.frontend.entity-util :as entity-util]))
             [logseq.db.frontend.entity-util :as entity-util]))
 
 
 (defn whiteboard?
 (defn whiteboard?
@@ -17,3 +18,9 @@
   [entity]
   [entity]
   (or (entity-util/page? entity)
   (or (entity-util/page? entity)
       (file-entity-util/page? entity)))
       (file-entity-util/page? entity)))
+
+(defn entity->map
+  "Convert a db Entity to a map"
+  [e]
+  (assert (de/entity? e))
+  (assoc (into {} e) :db/id (:db/id e)))

+ 1 - 5
deps/db/src/logseq/db/common/view.cljs

@@ -318,11 +318,7 @@
       (get-entities-for-all-pages db sorting property-ident {:db-based? db-based?})
       (get-entities-for-all-pages db sorting property-ident {:db-based? db-based?})
 
 
       :class-objects
       :class-objects
-      (let [class-id view-for-id
-            class-children (db-class/get-structured-children db class-id)
-            class-ids (distinct (conj class-children class-id))
-            datoms (mapcat (fn [id] (d/datoms db :avet :block/tags id)) class-ids)]
-        (keep (fn [d] (non-hidden-e (:e d))) datoms))
+      (db-class/get-class-objects db view-for-id)
 
 
       :property-objects
       :property-objects
       (->>
       (->>

+ 15 - 3
deps/db/src/logseq/db/frontend/class.cljs

@@ -7,6 +7,7 @@
             [flatland.ordered.map :refer [ordered-map]]
             [flatland.ordered.map :refer [ordered-map]]
             [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.db.frontend.db-ident :as db-ident]
             [logseq.db.frontend.db-ident :as db-ident]
+            [logseq.db.frontend.entity-util :as entity-util]
             [logseq.db.frontend.rules :as rules]
             [logseq.db.frontend.rules :as rules]
             [logseq.db.sqlite.util :as sqlite-util]))
             [logseq.db.sqlite.util :as sqlite-util]))
 
 
@@ -139,11 +140,11 @@
   [class]
   [class]
   (assert (de/entity? class) "get-class-extends `class` should be an entity")
   (assert (de/entity? class) "get-class-extends `class` should be an entity")
   (loop [extends (:logseq.property.class/extends class)
   (loop [extends (:logseq.property.class/extends class)
-         result #{}]
+         result []]
     (if (seq extends)
     (if (seq extends)
-      (recur (set (mapcat :logseq.property.class/extends extends))
+      (recur (mapcat :logseq.property.class/extends extends)
              (into result extends))
              (into result extends))
-      result)))
+      (reverse (distinct result)))))
 
 
 (defn create-user-class-ident-from-name
 (defn create-user-class-ident-from-name
   "Creates a class :db/ident for a default user namespace.
   "Creates a class :db/ident for a default user namespace.
@@ -173,3 +174,14 @@
   "Determines if namespace string is a user class"
   "Determines if namespace string is a user class"
   [s]
   [s]
   (string/includes? s ".class"))
   (string/includes? s ".class"))
+
+(defn get-class-objects
+  "Get class objects including children classes'"
+  [db class-id]
+  (let [class-children (get-structured-children db class-id)
+        class-ids (distinct (conj class-children class-id))
+        datoms (mapcat (fn [id] (d/datoms db :avet :block/tags id)) class-ids)
+        non-hidden-e (fn [id] (let [e (d/entity db id)]
+                                (when-not (entity-util/hidden? e)
+                                  e)))]
+    (keep (fn [d] (non-hidden-e (:e d))) datoms)))

+ 23 - 25
deps/db/src/logseq/db/frontend/db_ident.cljc

@@ -56,6 +56,13 @@
                      (str id)))
                      (str id)))
          id)))))
          id)))))
 
 
+(defn normalize-ident-name-part
+  [name-string]
+  (->> (string/replace-first name-string #"^(\d)" "NUM-$1")
+       ;; '-' must go last in char class
+       (filter #(re-find #"[0-9a-zA-Z*+!_'?<>=-]{1}" %))
+       (apply str)))
+
 (defn create-db-ident-from-name
 (defn create-db-ident-from-name
   "Creates a :db/ident for a class or property by sanitizing the given name.
   "Creates a :db/ident for a class or property by sanitizing the given name.
   The created ident must obey clojure's rules for keywords i.e.
   The created ident must obey clojure's rules for keywords i.e.
@@ -63,32 +70,23 @@
 
 
    NOTE: Only use this when creating a db-ident for a new class/property. Using
    NOTE: Only use this when creating a db-ident for a new class/property. Using
    this in read-only contexts like querying can result in db-ident conflicts"
    this in read-only contexts like querying can result in db-ident conflicts"
-  ([user-namespace name-string]
-   (create-db-ident-from-name user-namespace name-string true))
-  ([user-namespace name-string random-suffix?]
-   {:pre [(or (keyword? user-namespace) (string? user-namespace)) (string? name-string) (boolean? random-suffix?)]}
-   (assert (not (re-find #"^(logseq|block)(\.|$)" (name user-namespace)))
-           "New ident is not allowed to use an internal namespace")
-   (if #?(:org.babashka/nbb true
-          :cljs             (or (false? random-suffix?)
-                                (and (exists? js/process)
-                                     (or js/process.env.REPEATABLE_IDENTS js/process.env.DB_GRAPH)))
-          :default          false)
+  [user-namespace name-string]
+  {:pre [(or (keyword? user-namespace) (string? user-namespace)) (string? name-string)]}
+  (assert (not (re-find #"^(logseq|block)(\.|$)" (name user-namespace)))
+          "New ident is not allowed to use an internal namespace")
+  (if #?(:org.babashka/nbb true
+         :cljs             (and (exists? js/process)
+                                (or js/process.env.REPEATABLE_IDENTS js/process.env.DB_GRAPH))
+         :default          false)
      ;; Used for contexts where we want repeatable idents e.g. tests and CLIs
      ;; Used for contexts where we want repeatable idents e.g. tests and CLIs
-     (keyword user-namespace
-              (->> (string/replace-first name-string #"^(\d)" "NUM-$1")
-         ;; '-' must go last in char class
-                   (filter #(re-find #"[0-9a-zA-Z*+!_'?<>=-]{1}" %))
-                   (apply str)))
-     (keyword user-namespace
-              (str
-               (->> (string/replace-first name-string #"^(\d)" "NUM-$1")
-           ;; '-' must go last in char class
-                    (filter #(re-find #"[0-9a-zA-Z*+!_'?<>=-]{1}" %))
-                    (apply str))
-               "-"
-               (rand-nth non-int-char-range)
-               (nano-id 7))))))
+    (keyword user-namespace (normalize-ident-name-part name-string))
+    (let [suffix (str "-"
+                      (rand-nth non-int-char-range)
+                      (nano-id 7))]
+      (keyword user-namespace
+               (str
+                (normalize-ident-name-part name-string)
+                suffix)))))
 
 
 (defn replace-db-ident-random-suffix
 (defn replace-db-ident-random-suffix
   [db-ident-kw new-suffix]
   [db-ident-kw new-suffix]

+ 6 - 2
deps/db/src/logseq/db/frontend/malli_schema.cljs

@@ -102,7 +102,11 @@
                                (seq (:property/closed-values property)))
                                (seq (:property/closed-values property)))
                         (fn closed-value-valid? [val]
                         (fn closed-value-valid? [val]
                           (and (validate-fn' val)
                           (and (validate-fn' val)
-                               (contains? (set (map :db/id (:property/closed-values property))) val)))
+                               (let [ids (set (map :db/id (:property/closed-values property)))
+                                     result (contains? ids val)]
+                                 (when-not result
+                                   (js/console.error (str "Error: not a closed value, id: " val ", existing choices: " ids ", property: " (:db/ident property))))
+                                 result)))
                         validate-fn')]
                         validate-fn')]
     (if (db-property/many? property)
     (if (db-property/many? property)
       (or (every? validate-fn'' property-val)
       (or (every? validate-fn'' property-val)
@@ -346,7 +350,7 @@
    (concat
    (concat
     [:map
     [:map
      [:db/ident plugin-property-ident]
      [:db/ident plugin-property-ident]
-     [:logseq.property/type (apply vector :enum (conj db-property-type/user-built-in-property-types :string))]]
+     [:logseq.property/type (apply vector :enum (concat db-property-type/user-built-in-property-types [:json :string :page]))]]
     property-common-schema-attrs
     property-common-schema-attrs
     property-attrs
     property-attrs
     page-attrs
     page-attrs

+ 2 - 1
deps/db/src/logseq/db/frontend/property.cljs

@@ -683,7 +683,8 @@
   [db property-id]
   [db property-id]
   (when db
   (when db
     (when-let [property (d/entity db property-id)]
     (when-let [property (d/entity db property-id)]
-      (:property/closed-values property))))
+      (some->> (:block/_closed-value-property property)
+               (sort-by :block/order)))))
 
 
 (defn closed-value-content
 (defn closed-value-content
   "Gets content/value of a given closed value ent/map. Works for all closed value types"
   "Gets content/value of a given closed value ent/map. Works for all closed value types"

+ 21 - 2
deps/db/src/logseq/db/frontend/property/build.cljs

@@ -111,7 +111,10 @@
                   (assert (:db/ident property-map) "Key in map must have a :db/ident")
                   (assert (:db/ident property-map) "Key in map must have a :db/ident")
                   (when pure? (assert (some? gen-uuid-value-prefix) block))
                   (when pure? (assert (some? gen-uuid-value-prefix) block))
                   [(or (:original-property-id property-map) (:db/ident property-map))
                   [(or (:original-property-id property-map) (:db/ident property-map))
-                   (if (set? v)
+                   (cond
+                     (and (set? v) (every? uuid? v))
+                     (set (map #(vector :block/uuid %) v))
+                     (set? v)
                      (set (map #(build-property-value-block
                      (set (map #(build-property-value-block
                                  block' property-map %
                                  block' property-map %
                                  (cond-> {}
                                  (cond-> {}
@@ -121,6 +124,9 @@
                                    (assoc :block-uuid
                                    (assoc :block-uuid
                                           (common-uuid/gen-uuid :builtin-block-uuid (str gen-uuid-value-prefix "-" %)))))
                                           (common-uuid/gen-uuid :builtin-block-uuid (str gen-uuid-value-prefix "-" %)))))
                                v))
                                v))
+                     (uuid? v)
+                     [:block/uuid v]
+                     :else
                      (build-property-value-block block' property-map v
                      (build-property-value-block block' property-map v
                                                  (cond-> {}
                                                  (cond-> {}
                                                    property-value-properties
                                                    property-value-properties
@@ -130,12 +136,25 @@
                                                           (common-uuid/gen-uuid :builtin-block-uuid (str gen-uuid-value-prefix "-" v))))))])))
                                                           (common-uuid/gen-uuid :builtin-block-uuid (str gen-uuid-value-prefix "-" v))))))])))
          (into {}))))
          (into {}))))
 
 
+(defn- lookup-id?
+  [v]
+  (and (vector? v)
+       (= 2 (count v))
+       (= :block/uuid (first v))
+       (uuid? (second v))))
+
 (defn build-properties-with-ref-values
 (defn build-properties-with-ref-values
   "Given a properties map with property values to be transacted e.g. from
   "Given a properties map with property values to be transacted e.g. from
   build-property-values-tx-m, build a properties map to be transacted with the block"
   build-property-values-tx-m, build a properties map to be transacted with the block"
   [prop-vals-tx-m]
   [prop-vals-tx-m]
   (update-vals prop-vals-tx-m
   (update-vals prop-vals-tx-m
                (fn [v]
                (fn [v]
-                 (if (set? v)
+                 (cond
+                   (and (set? v) (every? lookup-id? v))
+                   v
+                   (set? v)
                    (set (map #(vector :block/uuid (:block/uuid %)) v))
                    (set (map #(vector :block/uuid (:block/uuid %)) v))
+                   (lookup-id? v)
+                   v
+                   :else
                    (vector :block/uuid (:block/uuid v))))))
                    (vector :block/uuid (:block/uuid v))))))

+ 49 - 32
deps/db/src/logseq/db/frontend/property/type.cljs

@@ -14,7 +14,7 @@
 
 
 (def internal-built-in-property-types
 (def internal-built-in-property-types
   "Valid property types only for use by internal built-in-properties"
   "Valid property types only for use by internal built-in-properties"
-  #{:keyword :map :coll :any :entity :class :page :property :string :raw-number})
+  #{:keyword :map :coll :any :entity :class :page :property :string :json :raw-number})
 
 
 (def user-built-in-property-types
 (def user-built-in-property-types
   "Valid property types for users in order they appear in the UI"
   "Valid property types for users in order they appear in the UI"
@@ -134,7 +134,8 @@
   (if new-closed-value?
   (if new-closed-value?
     (string? s)
     (string? s)
     (when-let [ent (d/entity db s)]
     (when-let [ent (d/entity db s)]
-      (string? (:block/title ent)))))
+      (and (string? (:block/title ent))
+           (some? (:block/page ent))))))
 
 
 (defn- node-entity?
 (defn- node-entity?
   [db val]
   [db val]
@@ -147,33 +148,17 @@
     (and (some? (:block/title ent))
     (and (some? (:block/title ent))
          (entity-util/journal? ent))))
          (entity-util/journal? ent))))
 
 
-(def built-in-validation-schemas
-  "Map of types to malli validation schemas that validate a property value for that type"
-  {:default  [:fn
-              {:error/message "should be a text block"}
-              text-entity?]
-   :number   [:fn
-              {:error/message "should be a number"}
-              number-entity?]
-   :date     [:fn
-              {:error/message "should be a journal date"}
-              date?]
-   :datetime [:fn
-              {:error/message "should be a datetime"}
-              number?]
-   :checkbox boolean?
-   :url      [:fn
-              {:error/message "should be a URL"}
-              url-entity?]
-   :node   [:fn
-            {:error/message "should be a page/block with tags"}
-            node-entity?]
-
-   ;; Internal usage
-   ;; ==============
-
-   :string   string?
-   :raw-number number?
+;; Internal usage
+(def internal-validation-schemas
+  {:string   [:fn
+              {:error/message "should be a string"}
+              string?]
+   :json     [:fn
+              {:error/message "should be JSON string"}
+              string?]
+   :raw-number [:fn
+                {:error/message "should be a raw number"}
+                number?]
    :entity   [:fn
    :entity   [:fn
               {:error/message "should be an Entity"}
               {:error/message "should be an Entity"}
               entity?]
               entity?]
@@ -186,12 +171,44 @@
    :page     [:fn
    :page     [:fn
               {:error/message "should be a Page"}
               {:error/message "should be a Page"}
               page-entity?]
               page-entity?]
-   :keyword  keyword?
-   :map      map?
+   :keyword  [:fn
+              {:error/message "should be a Clojure keyword"}
+              keyword?]
+   :map      [:fn
+              {:error/message "should be a Clojure map"}
+              map?]
    ;; coll elements are ordered as it's saved as a vec
    ;; coll elements are ordered as it's saved as a vec
-   :coll     coll?
+   :coll     [:fn
+              {:error/message "should be a collection"}
+              coll?]
    :any      some?})
    :any      some?})
 
 
+(def built-in-validation-schemas
+  "Map of types to malli validation schemas that validate a property value for that type"
+  (into
+   {:default  [:fn
+               {:error/message "should be a text block"}
+               text-entity?]
+    :number   [:fn
+               {:error/message "should be a number"}
+               number-entity?]
+    :date     [:fn
+               {:error/message "should be a journal date"}
+               date?]
+    :datetime [:fn
+               {:error/message "should be a datetime"}
+               number?]
+    :checkbox [:fn
+               {:error/message "should be a boolean"}
+               boolean?]
+    :url      [:fn
+               {:error/message "should be a URL"}
+               url-entity?]
+    :node   [:fn
+             {:error/message "should be a node with a title"}
+             node-entity?]}
+   internal-validation-schemas))
+
 (assert (= (set (keys built-in-validation-schemas))
 (assert (= (set (keys built-in-validation-schemas))
            (into internal-built-in-property-types
            (into internal-built-in-property-types
                  user-built-in-property-types))
                  user-built-in-property-types))

+ 3 - 3
deps/db/src/logseq/db/frontend/validate.cljs

@@ -21,10 +21,10 @@
   [closed-schema?]
   [closed-schema?]
   (if closed-schema? closed-db-schema-explainer db-schema-explainer))
   (if closed-schema? closed-db-schema-explainer db-schema-explainer))
 
 
-(defn validate-tx-report!
+(defn validate-tx-report
   "Validates the datascript tx-report for entities that have changed. Returns
   "Validates the datascript tx-report for entities that have changed. Returns
   boolean indicating if db is valid"
   boolean indicating if db is valid"
-  [{:keys [db-after tx-data tx-meta]} validate-options]
+  [{:keys [db-after tx-data _tx-meta]} validate-options]
   (let [changed-ids (->> tx-data (keep :e) distinct)
   (let [changed-ids (->> tx-data (keep :e) distinct)
         tx-datoms (mapcat #(d/datoms db-after :eavt %) changed-ids)
         tx-datoms (mapcat #(d/datoms db-after :eavt %) changed-ids)
         ent-maps* (map (fn [[db-id m]]
         ent-maps* (map (fn [[db-id m]]
@@ -38,7 +38,7 @@
                               ;; remove :db/id as it adds needless declarations to schema
                               ;; remove :db/id as it adds needless declarations to schema
                               #(validator [(dissoc % :db/id)])
                               #(validator [(dissoc % :db/id)])
                               ent-maps)]
                               ent-maps)]
-        (prn "changed eids:" changed-ids :tx-meta tx-meta)
+        ;; (prn "changed eids:" changed-ids :tx-meta tx-meta)
         (if (seq invalid-ent-maps)
         (if (seq invalid-ent-maps)
           (let [explainer (get-schema-explainer (:closed-schema? validate-options))]
           (let [explainer (get-schema-explainer (:closed-schema? validate-options))]
             (prn "Invalid datascript entities detected amongst changed entity ids:" changed-ids)
             (prn "Invalid datascript entities detected amongst changed entity ids:" changed-ids)

+ 17 - 4
deps/db/src/logseq/db/sqlite/build.cljs

@@ -140,9 +140,20 @@
                     (cond-> property-map
                     (cond-> property-map
                       (and (:build/property-value v) (seq pvalue-attrs))
                       (and (:build/property-value v) (seq pvalue-attrs))
                       (assoc :property-value-properties pvalue-attrs)))
                       (assoc :property-value-properties pvalue-attrs)))
-                  (if (:build/property-value v)
-                    (or (:logseq.property/value v) (:block/title v))
-                    v)])))
+                  (let [property (when (keyword? k) (get properties-config k))
+                        closed-value-id (when property (some (fn [item]
+                                                               (when (= (:value item) v)
+                                                                 (:uuid item)))
+                                                             (get property :build/closed-values)))]
+                    (cond
+                      closed-value-id
+                      closed-value-id
+
+                      (:build/property-value v)
+                      (or (:logseq.property/value v) (:block/title v))
+
+                      :else
+                      v))])))
        (db-property-build/build-property-values-tx-m new-block)))
        (db-property-build/build-property-values-tx-m new-block)))
 
 
 (defn- extract-basic-content-refs
 (defn- extract-basic-content-refs
@@ -502,7 +513,9 @@
         [init-tx block-props-tx]
         [init-tx block-props-tx]
         (reduce (fn [[init-tx* block-props-tx*] m]
         (reduce (fn [[init-tx* block-props-tx*] m]
                   (let [props (select-keys m property-idents)]
                   (let [props (select-keys m property-idents)]
-                    [(conj init-tx* (apply dissoc m property-idents))
+                    [(if (map? m)
+                       (conj init-tx* (apply dissoc m property-idents))
+                       init-tx*)
                      (if (seq props)
                      (if (seq props)
                        (conj block-props-tx*
                        (conj block-props-tx*
                              (merge {:block/uuid (or (:block/uuid m)
                              (merge {:block/uuid (or (:block/uuid m)

+ 3 - 3
deps/db/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 # yarn lockfile v1
 
 
 
 
-"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v28":
-  version "1.2.173-feat-db-v28"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/c0410ff81a8b0d510705581cccd39788d862dc91"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v29":
+  version "1.2.173-feat-db-v29"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/6c4f8eec72a0a5b0c7b96c32cc73f86b045305c5"
   dependencies:
   dependencies:
     import-meta-resolve "^4.1.0"
     import-meta-resolve "^4.1.0"
 
 

+ 1 - 1
deps/graph-parser/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "version": "1.0.0",
   "private": true,
   "private": true,
   "devDependencies": {
   "devDependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28",
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v29",
     "better-sqlite3": "11.10.0"
     "better-sqlite3": "11.10.0"
   },
   },
   "dependencies": {
   "dependencies": {

+ 2 - 2
deps/graph-parser/script/db_import.cljs

@@ -22,6 +22,7 @@
             [promesa.core :as p]))
             [promesa.core :as p]))
 
 
 (def tx-queue (atom cljs.core/PersistentQueue.EMPTY))
 (def tx-queue (atom cljs.core/PersistentQueue.EMPTY))
+;; This is a lower-level dev hook to inspect txs and shouldn't hook into ldb/transact!
 (def original-transact! d/transact!)
 (def original-transact! d/transact!)
 (defn dev-transact! [conn tx-data tx-meta]
 (defn dev-transact! [conn tx-data tx-meta]
   (swap! tx-queue (fn [queue]
   (swap! tx-queue (fn [queue]
@@ -94,8 +95,7 @@
       (println (some-> (get-in m [:ex-data :error]) .-stack)))
       (println (some-> (get-in m [:ex-data :error]) .-stack)))
     (when debug
     (when debug
       (when-let [matching-tx (seq (filter #(and (get-in m [:ex-data :path])
       (when-let [matching-tx (seq (filter #(and (get-in m [:ex-data :path])
-                                                (or (= (get-in % [:tx-meta ::gp-exporter/path]) (get-in m [:ex-data :path]))
-                                                    (= (get-in % [:tx-meta ::outliner-pipeline/original-tx-meta ::gp-exporter/path]) (get-in m [:ex-data :path]))))
+                                                (= (get-in % [:tx-meta ::gp-exporter/path]) (get-in m [:ex-data :path])))
                                           @tx-queue))]
                                           @tx-queue))]
         (println (str "\n" (count matching-tx)) "Tx Maps for failing path:")
         (println (str "\n" (count matching-tx)) "Tx Maps for failing path:")
         (pprint/pprint matching-tx))))
         (pprint/pprint matching-tx))))

+ 3 - 3
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -1791,7 +1791,7 @@
         (split-pages-and-properties-tx pages-tx old-properties existing-pages (:import-state options) @(:upstream-properties tx-options))
         (split-pages-and-properties-tx pages-tx old-properties existing-pages (:import-state options) @(:upstream-properties tx-options))
         ;; _ (when (seq property-pages-tx) (cljs.pprint/pprint {:property-pages-tx property-pages-tx}))
         ;; _ (when (seq property-pages-tx) (cljs.pprint/pprint {:property-pages-tx property-pages-tx}))
         ;; Necessary to transact new property entities first so that block+page properties can be transacted next
         ;; Necessary to transact new property entities first so that block+page properties can be transacted next
-        main-props-tx-report (d/transact! conn property-pages-tx {::new-graph? true ::path file})
+        main-props-tx-report (ldb/transact! conn property-pages-tx {::new-graph? true ::path file})
         _ (save-from-tx property-pages-tx options)
         _ (save-from-tx property-pages-tx options)
 
 
         classes-tx @(:classes-tx tx-options)
         classes-tx @(:classes-tx tx-options)
@@ -1817,13 +1817,13 @@
         ;;                        [:whiteboard-pages :pages-index :page-properties-tx :property-page-properties-tx :pages-tx' :classes-tx :blocks-index :blocks-tx]
         ;;                        [:whiteboard-pages :pages-index :page-properties-tx :property-page-properties-tx :pages-tx' :classes-tx :blocks-index :blocks-tx]
         ;;                        [whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx' classes-tx blocks-index blocks-tx]))
         ;;                        [whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx' classes-tx blocks-index blocks-tx]))
         ;; _ (when (not (seq whiteboard-pages)) (cljs.pprint/pprint {#_:property-pages-tx #_property-pages-tx :pages-tx pages-tx :tx tx'}))
         ;; _ (when (not (seq whiteboard-pages)) (cljs.pprint/pprint {#_:property-pages-tx #_property-pages-tx :pages-tx pages-tx :tx tx'}))
-        main-tx-report (d/transact! conn tx' {::new-graph? true ::path file})
+        main-tx-report (ldb/transact! conn tx' {::new-graph? true ::path file})
         _ (save-from-tx tx' options)
         _ (save-from-tx tx' options)
 
 
         upstream-properties-tx
         upstream-properties-tx
         (build-upstream-properties-tx @conn @(:upstream-properties tx-options) (:import-state options) log-fn)
         (build-upstream-properties-tx @conn @(:upstream-properties tx-options) (:import-state options) log-fn)
         ;; _ (when (seq upstream-properties-tx) (cljs.pprint/pprint {:upstream-properties-tx upstream-properties-tx}))
         ;; _ (when (seq upstream-properties-tx) (cljs.pprint/pprint {:upstream-properties-tx upstream-properties-tx}))
-        upstream-tx-report (when (seq upstream-properties-tx) (d/transact! conn upstream-properties-tx {::new-graph? true ::path file}))
+        upstream-tx-report (when (seq upstream-properties-tx) (ldb/transact! conn upstream-properties-tx {::new-graph? true ::path file}))
         _ (save-from-tx upstream-properties-tx options)]
         _ (save-from-tx upstream-properties-tx options)]
 
 
     ;; Return all tx-reports that occurred in this fn as UI needs to know what changed
     ;; Return all tx-reports that occurred in this fn as UI needs to know what changed

+ 3 - 3
deps/graph-parser/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 # yarn lockfile v1
 
 
 
 
-"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v28":
-  version "1.2.173-feat-db-v28"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/c0410ff81a8b0d510705581cccd39788d862dc91"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v29":
+  version "1.2.173-feat-db-v29"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/6c4f8eec72a0a5b0c7b96c32cc73f86b045305c5"
   dependencies:
   dependencies:
     import-meta-resolve "^4.1.0"
     import-meta-resolve "^4.1.0"
 
 

+ 1 - 5
deps/outliner/.carve/ignore

@@ -1,7 +1,3 @@
-;; private
-logseq.outliner.core/*transaction-opts*
-;; API fn
-logseq.outliner.datascript/transact!
 ;; API fn
 ;; API fn
 logseq.outliner.op/apply-ops!
 logseq.outliner.op/apply-ops!
 ;; API fn
 ;; API fn
@@ -9,4 +5,4 @@ logseq.outliner.op/register-op-handlers!
 ;; API fn
 ;; API fn
 logseq.outliner.page/delete!
 logseq.outliner.page/delete!
 ;; API fn
 ;; API fn
-logseq.outliner.page/create!
+logseq.outliner.page/create!

+ 2 - 1
deps/outliner/deps.edn

@@ -1,7 +1,8 @@
 {:deps
 {:deps
  ;; These nbb-logseq deps are kept in sync with https://github.com/logseq/nbb-logseq/blob/main/bb.edn
  ;; These nbb-logseq deps are kept in sync with https://github.com/logseq/nbb-logseq/blob/main/bb.edn
  {datascript/datascript {:git/url "https://github.com/logseq/datascript" ;; fork
  {datascript/datascript {:git/url "https://github.com/logseq/datascript" ;; fork
-                         :sha     "45f6721bf2038c24eb9fe3afb422322ab3f473b5"}
+                         :sha     "3971e2d43bd93d89f42191dc7b4b092989e0cc61"}
+  ;; datascript/datascript {:local/root "../../../../datascript"}
   com.cognitect/transit-cljs {:mvn/version "0.8.280"}
   com.cognitect/transit-cljs {:mvn/version "0.8.280"}
 
 
   ;; Any other deps should be added here and to nbb.edn
   ;; Any other deps should be added here and to nbb.edn

+ 1 - 1
deps/outliner/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "version": "1.0.0",
   "private": true,
   "private": true,
   "devDependencies": {
   "devDependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v29"
   },
   },
   "dependencies": {
   "dependencies": {
     "better-sqlite3": "11.10.0",
     "better-sqlite3": "11.10.0",

+ 2 - 1
deps/outliner/script/transact.cljs

@@ -2,6 +2,7 @@
   "This script generically runs transactions against the queried blocks"
   "This script generically runs transactions against the queried blocks"
   (:require [clojure.edn :as edn]
   (:require [clojure.edn :as edn]
             [datascript.core :as d]
             [datascript.core :as d]
+            [logseq.db :as ldb]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
             [logseq.db.frontend.rules :as rules]
             [logseq.db.frontend.rules :as rules]
             [logseq.outliner.db-pipeline :as db-pipeline]
             [logseq.outliner.db-pipeline :as db-pipeline]
@@ -30,7 +31,7 @@
           (prn (map #(select-keys (d/entity @conn %) [:block/name :block/title]) blocks-to-update)))
           (prn (map #(select-keys (d/entity @conn %) [:block/name :block/title]) blocks-to-update)))
       (do
       (do
         (db-pipeline/add-listener conn)
         (db-pipeline/add-listener conn)
-        (d/transact! conn update-tx)
+        (ldb/transact! conn update-tx)
         (println "Updated" (count update-tx) "block(s) for graph" (str db-name "!"))))))
         (println "Updated" (count update-tx) "block(s) for graph" (str db-name "!"))))))
 
 
 (when (= nbb/*file* (nbb/invoked-file))
 (when (= nbb/*file* (nbb/invoked-file))

+ 4 - 4
deps/outliner/src/logseq/outliner/cli.cljs

@@ -5,8 +5,8 @@
             ["path" :as node-path]
             ["path" :as node-path]
             [borkdude.rewrite-edn :as rewrite]
             [borkdude.rewrite-edn :as rewrite]
             [clojure.string :as string]
             [clojure.string :as string]
-            [datascript.core :as d]
             [logseq.common.config :as common-config]
             [logseq.common.config :as common-config]
+            [logseq.db :as ldb]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
             [logseq.db.sqlite.build :as sqlite-build]
             [logseq.db.sqlite.build :as sqlite-build]
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
@@ -48,9 +48,9 @@
           additional-config
           additional-config
           (pretty-print-merge additional-config))
           (pretty-print-merge additional-config))
         git-sha (get-git-sha)]
         git-sha (get-git-sha)]
-    (d/transact! conn (sqlite-create-graph/build-db-initial-data config-content
-                                                                 (merge {:import-type import-type}
-                                                                        (when git-sha {:graph-git-sha git-sha}))))))
+    (ldb/transact! conn (sqlite-create-graph/build-db-initial-data config-content
+                                                                   (merge {:import-type import-type}
+                                                                          (when git-sha {:graph-git-sha git-sha}))))))
 
 
 (defn init-conn
 (defn init-conn
   "Create sqlite DB, initialize datascript connection and sync listener and then
   "Create sqlite DB, initialize datascript connection and sync listener and then

+ 11 - 14
deps/outliner/src/logseq/outliner/core.cljs

@@ -288,8 +288,6 @@
                   (dissoc :block/children :block/meta :block/unordered
                   (dissoc :block/children :block/meta :block/unordered
                           :block.temp/ast-title :block.temp/ast-body :block/level :block.temp/load-status
                           :block.temp/ast-title :block.temp/ast-body :block/level :block.temp/load-status
                           :block.temp/has-children?)
                           :block.temp/has-children?)
-                  common-util/remove-nils
-
                   (fix-tag-ids db {:db-graph? db-based?}))
                   (fix-tag-ids db {:db-graph? db-based?}))
                (not collapse-or-expand?)
                (not collapse-or-expand?)
                block-with-updated-at)
                block-with-updated-at)
@@ -1103,12 +1101,13 @@
 ;;; ### write-operations have side-effects (do transactions) ;;;;;;;;;;;;;;;;
 ;;; ### write-operations have side-effects (do transactions) ;;;;;;;;;;;;;;;;
 
 
 (defn- op-transact!
 (defn- op-transact!
-  [f & args]
+  [outliner-op f & args]
   {:pre [(fn? f)]}
   {:pre [(fn? f)]}
   (try
   (try
     (let [result (apply f args)]
     (let [result (apply f args)]
       (when result
       (when result
-        (let [tx-meta (assoc (:tx-meta result) :skip-store? true)]
+        (let [tx-meta (assoc (:tx-meta result)
+                             :outliner-op outliner-op)]
           (ldb/transact! (second args) (:tx-data result) tx-meta)))
           (ldb/transact! (second args) (:tx-data result) tx-meta)))
       result)
       result)
     (catch :default e
     (catch :default e
@@ -1119,31 +1118,29 @@
           (save-block repo @conn date-formatter block opts))]
           (save-block repo @conn date-formatter block opts))]
   (defn save-block!
   (defn save-block!
     [repo conn date-formatter block & {:as opts}]
     [repo conn date-formatter block & {:as opts}]
-    (op-transact! f repo conn date-formatter block opts)))
+    (op-transact! :save-block f repo conn date-formatter block opts)))
 
 
 (let [f (fn [repo conn blocks target-block opts]
 (let [f (fn [repo conn blocks target-block opts]
           (insert-blocks repo @conn blocks target-block opts))]
           (insert-blocks repo @conn blocks target-block opts))]
   (defn insert-blocks!
   (defn insert-blocks!
     [repo conn blocks target-block opts]
     [repo conn blocks target-block opts]
-    (op-transact! f repo conn blocks target-block (assoc opts :outliner-op :insert-blocks))))
+    (op-transact! :insert-blocks f repo conn blocks target-block (assoc opts :outliner-op :insert-blocks))))
 
 
-(let [f (fn [_repo conn blocks opts]
-          (let [{:keys [tx-data]} (delete-blocks @conn blocks)]
-            {:tx-data tx-data
-             :tx-meta (select-keys opts [:outliner-op])}))]
+(let [f (fn [_repo conn blocks _opts]
+          (delete-blocks @conn blocks))]
   (defn delete-blocks!
   (defn delete-blocks!
     [repo conn _date-formatter blocks opts]
     [repo conn _date-formatter blocks opts]
-    (op-transact! f repo conn blocks opts)))
+    (op-transact! :delete-blocks f repo conn blocks opts)))
 
 
 (defn move-blocks!
 (defn move-blocks!
   [repo conn blocks target-block opts]
   [repo conn blocks target-block opts]
-  (op-transact! move-blocks repo conn blocks target-block
+  (op-transact! :move-blocks move-blocks repo conn blocks target-block
                 (assoc opts :outliner-op :move-blocks)))
                 (assoc opts :outliner-op :move-blocks)))
 
 
 (defn move-blocks-up-down!
 (defn move-blocks-up-down!
   [repo conn blocks up?]
   [repo conn blocks up?]
-  (op-transact! move-blocks-up-down repo conn blocks up?))
+  (op-transact! :move-blocks-up-down move-blocks-up-down repo conn blocks up?))
 
 
 (defn indent-outdent-blocks!
 (defn indent-outdent-blocks!
   [repo conn blocks indent? & {:as opts}]
   [repo conn blocks indent? & {:as opts}]
-  (op-transact! indent-outdent-blocks repo conn blocks indent? opts))
+  (op-transact! :indent-outdent-blocks indent-outdent-blocks repo conn blocks indent? opts))

+ 1 - 1
deps/outliner/src/logseq/outliner/db_pipeline.cljs

@@ -12,7 +12,7 @@
   "Modified copy of frontend.worker.pipeline/invoke-hooks that handles new DB graphs but
   "Modified copy of frontend.worker.pipeline/invoke-hooks that handles new DB graphs but
    doesn't handle updating DB graphs well yet e.g. doesn't handle :block/tx-id"
    doesn't handle updating DB graphs well yet e.g. doesn't handle :block/tx-id"
   [conn tx-report]
   [conn tx-report]
-  (when (not (get-in tx-report [:tx-meta :pipeline-replace?]))
+  (when-not (:pipeline-replace? (:tx-meta tx-report))
     ;; TODO: Handle block edits with separate :block/refs rebuild as deleting property values is buggy
     ;; TODO: Handle block edits with separate :block/refs rebuild as deleting property values is buggy
     (outliner-pipeline/transact-new-db-graph-refs conn tx-report)))
     (outliner-pipeline/transact-new-db-graph-refs conn tx-report)))
 
 

+ 14 - 13
deps/outliner/src/logseq/outliner/page.cljs

@@ -26,15 +26,16 @@
         id-ref->page #(db-content/content-id-ref->page % [page-entity])]
         id-ref->page #(db-content/content-id-ref->page % [page-entity])]
     (when (seq refs)
     (when (seq refs)
       (let [tx-data (mapcat (fn [{:block/keys [raw-title] :as ref}]
       (let [tx-data (mapcat (fn [{:block/keys [raw-title] :as ref}]
-                                ;; block content
-                              (let [content' (id-ref->page raw-title)
-                                    content-tx (when (not= raw-title content')
-                                                 {:db/id (:db/id ref)
-                                                  :block/title content'})
-                                    tx content-tx]
-                                (concat
-                                 [[:db/retract (:db/id ref) :block/refs (:db/id page-entity)]]
-                                 (when tx [tx])))) refs)]
+                              ;; block content
+                              (when raw-title
+                                (let [content' (id-ref->page raw-title)
+                                      content-tx (when (not= raw-title content')
+                                                   {:db/id (:db/id ref)
+                                                    :block/title content'})
+                                      tx content-tx]
+                                  (concat
+                                   [[:db/retract (:db/id ref) :block/refs (:db/id page-entity)]]
+                                   (when tx [tx]))))) refs)]
         tx-data))))
         tx-data))))
 
 
 (defn delete!
 (defn delete!
@@ -228,7 +229,7 @@
        [page])
        [page])
      (remove nil?))))
      (remove nil?))))
 
 
-(defn- ^:large-vars/cleanup-todo create
+(defn ^:large-vars/cleanup-todo ^:api create
   "Pure function without side effects"
   "Pure function without side effects"
   [db title*
   [db title*
    {uuid' :uuid
    {uuid' :uuid
@@ -305,7 +306,7 @@
 
 
 (defn create!
 (defn create!
   [conn title opts]
   [conn title opts]
-  (let [{:keys [tx-meta tx-data title page-uuid]} (create @conn title opts)]
+  (let [{:keys [tx-meta tx-data title' page-uuid]} (create @conn title opts)]
     (when (seq tx-data)
     (when (seq tx-data)
-      (d/transact! conn tx-data tx-meta)
-      [title page-uuid])))
+      (ldb/transact! conn tx-data tx-meta)
+      [title' page-uuid])))

+ 2 - 2
deps/outliner/src/logseq/outliner/pipeline.cljs

@@ -146,6 +146,6 @@
   [conn tx-report]
   [conn tx-report]
   (let [{:keys [blocks]} (ds-report/get-blocks-and-pages tx-report)
   (let [{:keys [blocks]} (ds-report/get-blocks-and-pages tx-report)
         refs-tx-report (when-let [refs-tx (and (seq blocks) (rebuild-block-refs-tx tx-report blocks))]
         refs-tx-report (when-let [refs-tx (and (seq blocks) (rebuild-block-refs-tx tx-report blocks))]
-                         (ldb/transact! conn refs-tx {:pipeline-replace? true
-                                                      ::original-tx-meta (:tx-meta tx-report)}))]
+                         (ldb/transact! conn refs-tx (-> (:tx-meta tx-report)
+                                                         (assoc :pipeline-replace? true))))]
     refs-tx-report))
     refs-tx-report))

+ 139 - 63
deps/outliner/src/logseq/outliner/property.cljs

@@ -18,7 +18,9 @@
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.outliner.core :as outliner-core]
             [logseq.outliner.core :as outliner-core]
+            [logseq.outliner.page :as outliner-page]
             [logseq.outliner.validate :as outliner-validate]
             [logseq.outliner.validate :as outliner-validate]
+            [malli.core :as m]
             [malli.error :as me]
             [malli.error :as me]
             [malli.util :as mu]))
             [malli.util :as mu]))
 
 
@@ -28,6 +30,14 @@
     (throw (ex-info "Read-only property value shouldn't be edited"
     (throw (ex-info "Read-only property value shouldn't be edited"
                     {:property property-ident}))))
                     {:property property-ident}))))
 
 
+(defn- db-ident->eid
+  [db db-ident]
+  (assert (qualified-keyword? db-ident))
+  (let [id (:db/id (d/entity db db-ident))]
+    (when-not id
+      (throw (ex-info "Wrong property db/ident" {:db-ident db-ident})))
+    id))
+
 (defonce ^:private built-in-class-property->properties
 (defonce ^:private built-in-class-property->properties
   (->>
   (->>
    (mapcat
    (mapcat
@@ -194,7 +204,7 @@
                (or (not= (:logseq.property/type schema) (:logseq.property/type property))
                (or (not= (:logseq.property/type schema) (:logseq.property/type property))
                    (and (:db/cardinality schema) (not= (:db/cardinality schema) (keyword (name (:db/cardinality property)))))
                    (and (:db/cardinality schema) (not= (:db/cardinality schema) (keyword (name (:db/cardinality property)))))
                    (and (= :default (:logseq.property/type schema)) (not= :db.type/ref (:db/valueType property)))
                    (and (= :default (:logseq.property/type schema)) (not= :db.type/ref (:db/valueType property)))
-                   (seq (:property/closed-values property))))
+                   (seq (entity-plus/lookup-kv-then-entity property :property/closed-values))))
           (concat (update-datascript-schema property schema)))
           (concat (update-datascript-schema property schema)))
         tx-data (concat property-tx-data
         tx-data (concat property-tx-data
                         (when (seq properties)
                         (when (seq properties)
@@ -227,27 +237,48 @@
         schema (get-property-value-schema db property-type property)]
         schema (get-property-value-schema db property-type property)]
     (validate-property-value-aux schema value {:many? many?})))
     (validate-property-value-aux schema value {:many? many?})))
 
 
+(defn- validate!
+  "Validates `data` against `schema`.
+   Throws an ex-info with readable message if validation fails."
+  [property schema value]
+  (when-not (and
+             (= :db.type/ref (:db/valueType property))
+             (= value :logseq.property/empty-placeholder))
+    (when-not (m/validate schema value)
+      (let [errors (-> (m/explain schema value)
+                       (me/humanize))
+            error-msg (str "\"" (:block/title property) "\"" " " (if (coll? errors) (first errors) errors))]
+        (throw
+         (ex-info "Schema validation failed"
+                  {:type :notification
+                   :payload {:message error-msg
+                             :type :warning}
+                   :property (:db/ident property)
+                   :value value
+                   :errors errors}))))))
+
+(defn- throw-error-if-invalid-property-value
+  [db property value]
+  (let [property-type (:logseq.property/type property)
+        many? (= :db.cardinality/many (:db/cardinality property))
+        schema (get-property-value-schema db property-type property)
+        value' (if (and many? (not (sequential? value)))
+                 #{value}
+                 value)]
+    (validate! property schema value')))
+
 (defn- ->eid
 (defn- ->eid
   [id]
   [id]
   (if (uuid? id) [:block/uuid id] id))
   (if (uuid? id) [:block/uuid id] id))
 
 
 (defn- raw-set-block-property!
 (defn- raw-set-block-property!
   "Adds the raw property pair (value not modified) to the given block if the property value is valid"
   "Adds the raw property pair (value not modified) to the given block if the property value is valid"
-  [conn block property property-type new-value]
+  [conn block property new-value]
   (throw-error-if-read-only-property (:db/ident property))
   (throw-error-if-read-only-property (:db/ident property))
-  (let [k-name (:block/title property)
-        property-id (:db/ident property)
-        schema (get-property-value-schema @conn property-type property)]
-    (if-let [msg (and
-                  (not= new-value :logseq.property/empty-placeholder)
-                  (validate-property-value-aux schema new-value {:many? (db-property/many? property)}))]
-      (let [msg' (str "\"" k-name "\"" " " (if (coll? msg) (first msg) msg))]
-        (throw (ex-info "Schema validation failed"
-                        {:type :notification
-                         :payload {:message msg'
-                                   :type :warning}})))
-      (let [tx-data (build-property-value-tx-data conn block property-id new-value)]
-        (ldb/transact! conn tx-data {:outliner-op :save-block})))))
+  (throw-error-if-invalid-property-value @conn property new-value)
+  (let [property-id (:db/ident property)
+        tx-data (build-property-value-tx-data conn block property-id new-value)]
+    (ldb/transact! conn tx-data {:outliner-op :save-block})))
 
 
 (defn create-property-text-block!
 (defn create-property-text-block!
   "Creates a property value block for the given property and value. Adds it to
   "Creates a property value block for the given property and value. Adds it to
@@ -258,6 +289,11 @@
         _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
         _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
         value' (convert-property-input-string (:logseq.property/type block)
         value' (convert-property-input-string (:logseq.property/type block)
                                               property value)
                                               property value)
+        _ (when (and (not= (:logseq.property/type property) :number)
+                     (not (string? value')))
+            (throw (ex-info "value should be a string" {:block-id block-id
+                                                        :property-id property-id
+                                                        :value value'})))
         new-value-block (cond-> (db-property-build/build-property-value-block (or block property) property value')
         new-value-block (cond-> (db-property-build/build-property-value-block (or block property) property value')
                           new-block-id
                           new-block-id
                           (assoc :block/uuid new-block-id))]
                           (assoc :block/uuid new-block-id))]
@@ -265,7 +301,7 @@
     (let [property-id (:db/ident property)]
     (let [property-id (:db/ident property)]
       (when (and property-id block)
       (when (and property-id block)
         (when-let [block-id (:db/id (d/entity @conn [:block/uuid (:block/uuid new-value-block)]))]
         (when-let [block-id (:db/id (d/entity @conn [:block/uuid (:block/uuid new-value-block)]))]
-          (raw-set-block-property! conn block property (:logseq.property/type property) block-id)))
+          (raw-set-block-property! conn block property block-id)))
       (:block/uuid new-value-block))))
       (:block/uuid new-value-block))))
 
 
 (defn- get-property-value-eid
 (defn- get-property-value-eid
@@ -285,11 +321,14 @@
   "Find or create a property value. Only to be used with properties that have ref types"
   "Find or create a property value. Only to be used with properties that have ref types"
   [conn property-id v]
   [conn property-id v]
   (let [property (d/entity @conn property-id)
   (let [property (d/entity @conn property-id)
-        closed-values? (seq (:property/closed-values property))
+        closed-values? (seq (entity-plus/lookup-kv-then-entity property :property/closed-values))
         default-or-url? (contains? #{:default :url} (:logseq.property/type property))]
         default-or-url? (contains? #{:default :url} (:logseq.property/type property))]
     (cond
     (cond
       closed-values?
       closed-values?
-      (get-property-value-eid @conn property-id v)
+      (some (fn [item]
+              (when (or (= (:block/title item) v)
+                        (= (:logseq.property/value item) v))
+                (:db/id item))) (:block/_closed-value-property property))
 
 
       (and default-or-url?
       (and default-or-url?
            ;; FIXME: remove this when :logseq.property/order-list-type updated to closed values
            ;; FIXME: remove this when :logseq.property/order-list-type updated to closed values
@@ -308,6 +347,9 @@
   [conn property-id v property-type]
   [conn property-id v property-type]
   (let [number-property? (= property-type :number)]
   (let [number-property? (= property-type :number)]
     (cond
     (cond
+      (and (qualified-keyword? v) (not= :keyword property-type))
+      (db-ident->eid @conn v)
+
       (and (integer? v)
       (and (integer? v)
            (or (not number-property?)
            (or (not number-property?)
                ;; Allows :number property to use number as a ref (for closed value) or value
                ;; Allows :number property to use number as a ref (for closed value) or value
@@ -317,13 +359,19 @@
       v
       v
 
 
       (= property-type :page)
       (= property-type :page)
-      (if (or (string/blank? v) (not (string? v)))
-        (throw (ex-info "Value should be non-empty string" {:property-id property-id
-                                                            :property-type property-type
-                                                            :v v}))
+      (let [error-data {:property-id property-id
+                        :property-type property-type
+                        :v v}]
+        (if (or (string/blank? v) (not (string? v)))
+          (throw (ex-info "Value should be non-empty string" error-data))
+          (let [page (ldb/get-page @conn v)]
+            (if (entity-util/page? page)
+              (:db/id page)
+              (let [[_ page-uuid] (outliner-page/create! conn v error-data)]
+                (if-not page-uuid
+                  (throw (ex-info "Failed to create page" {}))
+                  (:db/id (d/entity @conn [:block/uuid page-uuid]))))))))
 
 
-        ;; TODO: create page
-        nil)
       :else
       :else
       ;; only value-ref-property types should call this
       ;; only value-ref-property types should call this
       (when-let [v' (if (and number-property? (string? v))
       (when-let [v' (if (and number-property? (string? v))
@@ -391,25 +439,32 @@
                 @conn
                 @conn
                 (if (number? v) (d/entity @conn v) v)
                 (if (number? v) (d/entity @conn v) v)
                 (map #(d/entity @conn %) block-eids)))
                 (map #(d/entity @conn %) block-eids)))
-           _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
+           _ (when (nil? property)
+               (throw (ex-info (str "Property " property-id " doesn't exist yet") {:property-id property-id})))
            property-type (get property :logseq.property/type :default)
            property-type (get property :logseq.property/type :default)
-           _ (assert (some? v) "Can't set a nil property value must be not nil")
+           entity-id? (and (:entity-id? options) (number? v))
            ref? (contains? db-property-type/all-ref-property-types property-type)
            ref? (contains? db-property-type/all-ref-property-types property-type)
            default-url-not-closed? (and (contains? #{:default :url} property-type)
            default-url-not-closed? (and (contains? #{:default :url} property-type)
-                                        (not (seq (:property/closed-values property))))
-           entity-id? (and (:entity-id? options) (number? v))
+                                        (not (seq (entity-plus/lookup-kv-then-entity property :property/closed-values))))
            v' (if (and ref? (not entity-id?))
            v' (if (and ref? (not entity-id?))
                 (convert-ref-property-value conn property-id v property-type)
                 (convert-ref-property-value conn property-id v property-type)
                 v)
                 v)
+           _ (when (nil? v')
+               (throw (ex-info "Property value must be not nil" {:v v})))
            txs (doall
            txs (doall
                 (mapcat
                 (mapcat
                  (fn [eid]
                  (fn [eid]
                    (if-let [block (d/entity @conn eid)]
                    (if-let [block (d/entity @conn eid)]
-                     (let [v' (if default-url-not-closed?
-                                (let [v (if (number? v) (:block/title (d/entity @conn v)) v)]
-                                  (convert-ref-property-value conn property-id v property-type))
+                     (let [v' (if (and default-url-not-closed?
+                                       (not (and (keyword? v) entity-id?)))
+                                (do
+                                  (when (number? v')
+                                    (throw-error-if-invalid-property-value @conn property v'))
+                                  (let [v (if (number? v') (:block/title (d/entity @conn v')) v')]
+                                    (convert-ref-property-value conn property-id v property-type)))
                                 v')]
                                 v')]
                        (throw-error-if-self-value block v' ref?)
                        (throw-error-if-self-value block v' ref?)
+                       (throw-error-if-invalid-property-value @conn property v')
                        (build-property-value-tx-data conn block property-id v'))
                        (build-property-value-tx-data conn block property-id v'))
                      (js/console.error "Skipping setting a block's property because the block id could not be found:" eid)))
                      (js/console.error "Skipping setting a block's property because the block id could not be found:" eid)))
                  block-eids))]
                  block-eids))]
@@ -462,37 +517,49 @@
   attributes as properties"
   attributes as properties"
   [conn block-eid property-id v]
   [conn block-eid property-id v]
   (throw-error-if-read-only-property property-id)
   (throw-error-if-read-only-property property-id)
-  (if (nil? v)
-    (remove-block-property! conn block-eid property-id)
-    (let [block-eid (->eid block-eid)
-          _ (assert (qualified-keyword? property-id) "property-id should be a keyword")
-          block (d/entity @conn block-eid)
-          db-attribute? (some? (db-schema/schema property-id))]
-      (when (= property-id :block/tags)
-        (outliner-validate/validate-tags-property @conn [block-eid] v))
-      (when (= property-id :logseq.property.class/extends)
-        (outliner-validate/validate-extends-property @conn v [block]))
-      (cond
-        db-attribute?
-        (when-not (and (= property-id :block/alias) (= v (:db/id block))) ; alias can't be itself
-          (let [tx-data (cond->
-                         [{:db/id (:db/id block) property-id v}]
-                          (= property-id :logseq.property.class/extends)
-                          (conj [:db/retract (:db/id block) :logseq.property.class/extends :logseq.class/Root]))]
-            (ldb/transact! conn tx-data
-                           {:outliner-op :save-block})))
-        :else
-        (let [property (d/entity @conn property-id)
-              _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
-              property-type (get property :logseq.property/type :default)
-              ref? (db-property-type/all-ref-property-types property-type)
-              new-value (if ref?
-                          (convert-ref-property-value conn property-id v property-type)
-                          v)
-              existing-value (get block property-id)]
-          (throw-error-if-self-value block new-value ref?)
-          (when-not (= existing-value new-value)
-            (raw-set-block-property! conn block property property-type new-value)))))))
+  (let [db @conn
+        block-eid (->eid block-eid)
+        _ (assert (qualified-keyword? property-id) "property-id should be a keyword")
+        block (d/entity @conn block-eid)
+        db-attribute? (some? (db-schema/schema property-id))
+        property (d/entity @conn property-id)
+        property-type (get property :logseq.property/type :default)
+        ref? (db-property-type/all-ref-property-types property-type)
+        v' (if ref?
+             (convert-ref-property-value conn property-id v property-type)
+             v)]
+    (when-not (and block property)
+      (throw (ex-info "Set block property failed: block or property doesn't exist"
+                      {:block-eid block-eid
+                       :property-id property-id
+                       :block block
+                       :property property})))
+    (if (nil? v')
+      (remove-block-property! conn block-eid property-id)
+      (do
+        (when (= property-id :block/tags)
+          (outliner-validate/validate-tags-property @conn [block-eid] v'))
+        (when (= property-id :logseq.property.class/extends)
+          (outliner-validate/validate-extends-property @conn v' [block]))
+        (cond
+          db-attribute?
+          (do
+            (throw-error-if-invalid-property-value db property v')
+            (when-not (and (= property-id :block/alias) (= v' (:db/id block))) ; alias can't be itself
+              (let [tx-data (cond->
+                             [{:db/id (:db/id block) property-id v'}]
+                              (= property-id :logseq.property.class/extends)
+                              (conj [:db/retract (:db/id block) :logseq.property.class/extends :logseq.class/Root]))]
+                (ldb/transact! conn tx-data
+                               {:outliner-op :save-block}))))
+          :else
+          (let [_ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
+                ref? (db-property-type/all-ref-property-types property-type)
+                existing-value (get block property-id)]
+            (throw-error-if-self-value block v' ref?)
+
+            (when-not (= existing-value v')
+              (raw-set-block-property! conn block property v'))))))))
 
 
 (defn upsert-property!
 (defn upsert-property!
   "Updates property if property-id is given. Otherwise creates a property
   "Updates property if property-id is given. Otherwise creates a property
@@ -508,6 +575,10 @@
                                              :payload {:message "Property failed to create. Please try a different property name."
                                              :payload {:message "Property failed to create. Please try a different property name."
                                                        :type :error}})))))]
                                                        :type :error}})))))]
     (assert (qualified-keyword? db-ident))
     (assert (qualified-keyword? db-ident))
+    (when (and (contains? #{:checkbox} (:logseq.property/type  schema))
+               (= :db.cardinality/many (:db/cardinality schema)))
+      (throw (ex-info ":checkbox property doesn't allow multiple values" {:property-id property-id
+                                                                          :schema schema})))
     (if-let [property (and (qualified-keyword? property-id) (d/entity db db-ident))]
     (if-let [property (and (qualified-keyword? property-id) (d/entity db db-ident))]
       (update-property conn db-ident property schema opts)
       (update-property conn db-ident property schema opts)
       (let [k-name (or (and property-name (name property-name))
       (let [k-name (or (and property-name (name property-name))
@@ -651,7 +722,7 @@
                         {:block/title resolved-value})))
                         {:block/title resolved-value})))
                      icon
                      icon
                      (assoc :logseq.property/icon icon))]
                      (assoc :logseq.property/icon icon))]
-                  (let [max-order (:block/order (last (:property/closed-values property)))
+                  (let [max-order (:block/order (last (entity-plus/lookup-kv-then-entity property :property/closed-values)))
                         new-block (-> (db-property-build/build-closed-value-block block-id nil resolved-value
                         new-block (-> (db-property-build/build-closed-value-block block-id nil resolved-value
                                                                                   property {:icon icon})
                                                                                   property {:icon icon})
                                       (assoc :block/order (db-order/gen-key max-order nil)))]
                                       (assoc :block/order (db-order/gen-key max-order nil)))]
@@ -738,6 +809,11 @@
 (defn delete-closed-value!
 (defn delete-closed-value!
   "Returns true when deleted or if not deleted displays warning and returns false"
   "Returns true when deleted or if not deleted displays warning and returns false"
   [conn property-id value-block-id]
   [conn property-id value-block-id]
+  (when (or (nil? property-id)
+            (nil? value-block-id))
+    (throw (ex-info "empty property-id or value-block-id when delete-closed-value!"
+                    {:property-id property-id
+                     :value-block-id value-block-id})))
   (when-let [value-block (d/entity @conn value-block-id)]
   (when-let [value-block (d/entity @conn value-block-id)]
     (if (ldb/built-in? value-block)
     (if (ldb/built-in? value-block)
       (throw (ex-info "The choice can't be deleted"
       (throw (ex-info "The choice can't be deleted"

+ 3 - 3
deps/outliner/test/logseq/outliner/property_test.cljs

@@ -301,8 +301,8 @@
                [{:page {:block/title "page1"}
                [{:page {:block/title "page1"}
                  :blocks [{:block/title "b1" :user.property/default [:block/uuid used-closed-value-uuid]}]}]})
                  :blocks [{:block/title "b1" :user.property/default [:block/uuid used-closed-value-uuid]}]}]})
         _ (assert (:user.property/default (db-test/find-block-by-content @conn "b1")))
         _ (assert (:user.property/default (db-test/find-block-by-content @conn "b1")))
-        property-uuid (:block/uuid (d/entity @conn :user.property-default))
-        _ (outliner-property/delete-closed-value! conn property-uuid [:block/uuid closed-value-uuid])]
+        property-id (:db/id (d/entity @conn :user.property/default))
+        _ (outliner-property/delete-closed-value! conn property-id [:block/uuid closed-value-uuid])]
     (is (nil? (d/entity @conn [:block/uuid closed-value-uuid])))))
     (is (nil? (d/entity @conn [:block/uuid closed-value-uuid])))))
 
 
 (deftest class-add-property!
 (deftest class-add-property!
@@ -367,4 +367,4 @@
                                               (:db/id (d/entity @conn :user.class/C1)))
                                               (:db/id (d/entity @conn :user.class/C1)))
     (is (= [:logseq.class/Root]
     (is (= [:logseq.class/Root]
            (:logseq.property.class/extends (db-test/readable-properties (d/entity @conn :user.class/C3))))
            (:logseq.property.class/extends (db-test/readable-properties (d/entity @conn :user.class/C3))))
-        "Extends property is restored back to Root")))
+        "Extends property is restored back to Root")))

+ 3 - 3
deps/outliner/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 # yarn lockfile v1
 
 
 
 
-"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v28":
-  version "1.2.173-feat-db-v28"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/c0410ff81a8b0d510705581cccd39788d862dc91"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v29":
+  version "1.2.173-feat-db-v29"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/6c4f8eec72a0a5b0c7b96c32cc73f86b045305c5"
   dependencies:
   dependencies:
     import-meta-resolve "^4.1.0"
     import-meta-resolve "^4.1.0"
 
 

+ 1 - 1
deps/publishing/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "version": "1.0.0",
   "private": true,
   "private": true,
   "devDependencies": {
   "devDependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28",
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v29",
     "mldoc": "^1.5.9"
     "mldoc": "^1.5.9"
   },
   },
   "dependencies": {
   "dependencies": {

+ 3 - 3
deps/publishing/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 # yarn lockfile v1
 
 
 
 
-"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v28":
-  version "1.2.173-feat-db-v28"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/c0410ff81a8b0d510705581cccd39788d862dc91"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v29":
+  version "1.2.173-feat-db-v29"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/6c4f8eec72a0a5b0c7b96c32cc73f86b045305c5"
   dependencies:
   dependencies:
     import-meta-resolve "^4.1.0"
     import-meta-resolve "^4.1.0"
 
 

+ 1 - 0
libs/.npmignore

@@ -2,3 +2,4 @@ src/
 webpack.*
 webpack.*
 .DS_Store
 .DS_Store
 docs/
 docs/
+cljs-sdk/

+ 13 - 0
libs/README.md

@@ -30,3 +30,16 @@ import "@logseq/libs"
 #### Feedback
 #### Feedback
 If you have any feedback or encounter any issues, feel free to join Logseq's discord group.
 If you have any feedback or encounter any issues, feel free to join Logseq's discord group.
 https://discord.gg/KpN4eHY
 https://discord.gg/KpN4eHY
+
+#### Generate CLJS SDK wrappers
+
+To regenerate the ClojureScript facade from the JS SDK declarations (keeping the same argument shapes as the JS APIs while auto-converting to/from CLJS data):
+
+```bash
+yarn run generate:schema              # emits dist/logseq-sdk-schema.json
+bb libs:generate-cljs-sdk            # emits logseq/core.cljs and per-proxy files under target/generated-cljs
+```
+
+Non-proxy methods (those defined on `ILSPluginUser`, e.g. `ready`, `provide-ui`) land in `logseq.core`. Each proxy (`IAppProxy`, `IEditorProxy`, ...) is emitted to its own namespace such as `logseq.app` or `logseq.editor`, preserving the original JS argument ordering while automatically bean-converting CLJS data.
+
+Pass `--out-dir` to change the output location or `--ns-prefix` to pick a different namespace root.

+ 38 - 0
libs/cljs-sdk/.gitignore

@@ -0,0 +1,38 @@
+# Clojure/Leiningen
+pom.xml
+pom.xml.asc
+*.jar
+*.class
+/lib/
+/classes/
+/target/
+/checkouts/
+.lein-deps-sum
+.lein-repl-history
+.lein-plugins/
+.lein-failures
+.nrepl-port
+
+# ClojureScript/Build
+.cpcache/
+.shadow-cljs/
+dist/
+out/
+
+# Node.js
+node_modules/
+package-lock.json
+coverage/
+.nyc_output/
+
+# IDE
+.idea/
+.vscode/
+.calva/
+.calva/output-window
+.lsp/
+.clj-kondo/
+
+# OS
+.DS_Store
+*.log

+ 21 - 0
libs/cljs-sdk/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Logseq
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 31 - 0
libs/cljs-sdk/bb.edn

@@ -0,0 +1,31 @@
+{:paths ["src" "test"]
+ :deps {org.babashka/cli {:mvn/version "0.7.53"}
+        slipset/deps-deploy {:mvn/version "0.2.1"}}
+ :tasks
+ {:requires ([babashka.cli :as cli])
+  :init (do
+          (defn run [cmd]
+            (let [result (shell cmd)]
+              (when-not (zero? (:exit result))
+                (throw (ex-info "Command failed" result))))))
+
+  clean {:doc "Clean compiled files"
+         :task (do
+                 (run "rm -rf dist")
+                 (run "rm -rf .shadow-cljs")
+                 (run "rm -rf node_modules")
+                 (run "rm -rf out")
+                 (run "rm -rf .nyc_output")
+                 (run "rm -rf coverage"))}
+
+  install {:doc "Install dependencies"
+           :task (run "yarn install")}
+
+  build-jar {:doc "Build jar file"
+             :task (shell "clojure -T:build jar")}
+
+  deploy {:doc "Deploy jar to Clojars"
+          :task (shell "clojure -T:build deploy")}
+
+  release {:doc "Build release version and deploy to Clojars"
+           :depends [clean install build-jar deploy]}}}

+ 46 - 0
libs/cljs-sdk/build.clj

@@ -0,0 +1,46 @@
+(ns build
+  (:require [clojure.data.json :as json]
+            [clojure.tools.build.api :as b]
+            [deps-deploy.deps-deploy :as dd]))
+
+(def lib 'com.logseq/libs)
+(def version
+  (-> (slurp "package.json")
+      (json/read-str :key-fn keyword)
+      :version))
+(def class-dir "target/classes")
+(def basis (delay (b/create-basis {:project "deps.edn"})))
+(def jar-file (format "target/%s-%s.jar" (name lib) version))
+
+(def pom-template
+  [[:description "ClojureScript wrapper for @logseq/libs"]
+   [:url "https://github.com/logseq/logseq"]
+   [:licenses
+    [:license
+     [:name "MIT License"]
+     [:url "https://opensource.org/licenses/MIT"]]]])
+
+(def options
+  {:class-dir class-dir
+   :lib lib
+   :version version
+   :basis @basis
+   :jar-file jar-file
+   :src-dirs ["src"]
+   :pom-data pom-template})
+
+(defn clean [_]
+  (b/delete {:path "target"}))
+
+(defn jar [_]
+  (clean nil)
+  (b/write-pom options)
+  (b/copy-dir {:src-dirs (:paths @basis)
+               :target-dir class-dir})
+  (b/jar options))
+
+(defn deploy [_]
+  (jar nil)
+  (dd/deploy {:installer :remote
+              :artifact jar-file
+              :pom-file (b/pom-path {:lib lib :class-dir class-dir})}))

+ 20 - 0
libs/cljs-sdk/deps.edn

@@ -0,0 +1,20 @@
+{:paths ["src" "test"]
+ :deps {org.clojure/clojurescript {:mvn/version "1.11.60"}
+        cljs-bean/cljs-bean       {:mvn/version "1.5.0"}}
+
+ :npm-deps {"@logseq/libs" "0.2.3"}
+
+ :aliases
+ {:dev
+  {:extra-paths ["example"]}
+
+  :test
+  {:extra-paths ["test"]
+   :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}}
+   :ns-default build}
+
+  :build
+  {:deps {io.github.clojure/tools.build {:mvn/version "0.9.6"}
+          slipset/deps-deploy {:mvn/version "0.2.1"}
+          org.clojure/data.json {:mvn/version "2.4.0"}}
+   :ns-default build}}}

+ 27 - 0
libs/cljs-sdk/package.json

@@ -0,0 +1,27 @@
+{
+  "name": "@logseq/cljs-libs",
+  "version": "0.0.10",
+  "description": "Logseq plugin API wrapper",
+  "dependencies": {
+    "@logseq/libs": "^0.2.3"
+  },
+  "files": [
+    "dist"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/logseq/logseq.git"
+  },
+  "keywords": [
+    "logseq",
+    "clojurescript",
+    "plugin",
+    "@logseq/libs"
+  ],
+  "author": "logseq",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/logseq/logseq/issues"
+  },
+  "homepage": "https://github.com/logseq/logseq#readme"
+}

+ 348 - 0
libs/cljs-sdk/src/com/logseq/app.cljs

@@ -0,0 +1,348 @@
+;; Auto-generated via `bb libs:generate-cljs-sdk`
+(ns com.logseq.app
+  (:require [com.logseq.core :as core]))
+
+(def api-proxy (aget js/logseq "App"))
+
+(defn- get-info-impl
+  [key]
+  (let [method (aget api-proxy "getInfo")
+        args [key]]
+    (core/call-method api-proxy method args)))
+
+(defn get-info
+  ([]
+   (get-info-impl nil))
+  ([key]
+   (get-info-impl key)))
+
+(defn get-user-info
+  []
+  (let [method (aget api-proxy "getUserInfo")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-user-configs
+  []
+  (let [method (aget api-proxy "getUserConfigs")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn register-search-service
+  [s]
+  (let [method (aget api-proxy "registerSearchService")
+        args [s]]
+    (core/call-method api-proxy method args)))
+
+(defn register-command
+  [type opts action]
+  (let [method (aget api-proxy "registerCommand")
+        args [type opts action]]
+    (core/call-method api-proxy method args)))
+
+(defn register-command-palette
+  [opts action]
+  (let [method (aget api-proxy "registerCommandPalette")
+        args [opts action]]
+    (core/call-method api-proxy method args)))
+
+(defn- register-command-shortcut-impl
+  [keybinding action opts]
+  (let [method (aget api-proxy "registerCommandShortcut")
+        args [keybinding action opts]]
+    (core/call-method api-proxy method args)))
+
+(defn register-command-shortcut
+  "Supported key names"
+  ([keybinding action]
+   (register-command-shortcut-impl keybinding action nil))
+  ([keybinding action opts]
+   (register-command-shortcut-impl keybinding action opts)))
+
+(defn invoke-external-command
+  "Supported all registered palette commands"
+  [type & args]
+  (let [method (aget api-proxy "invokeExternalCommand")
+        rest-args (vec args)
+        args (into [type] rest-args)]
+    (core/call-method api-proxy method args)))
+
+(defn invoke-external-plugin
+  "Call external plugin command provided by models or registered commands"
+  [type & args]
+  (let [method (aget api-proxy "invokeExternalPlugin")
+        rest-args (vec args)
+        args (into [type] rest-args)]
+    (core/call-method api-proxy method args)))
+
+(defn get-external-plugin
+  [pid]
+  (let [method (aget api-proxy "getExternalPlugin")
+        args [pid]]
+    (core/call-method api-proxy method args)))
+
+(defn get-state-from-store
+  "Get state from app store\nvalid state is here\nhttps://github.com/logseq/logseq/blob/master/src/main/frontend/state.cljs#L27"
+  [path]
+  (let [method (aget api-proxy "getStateFromStore")
+        args [path]]
+    (core/call-method api-proxy method args)))
+
+(defn set-state-from-store
+  [path value]
+  (let [method (aget api-proxy "setStateFromStore")
+        args [path value]]
+    (core/call-method api-proxy method args)))
+
+(defn relaunch
+  []
+  (let [method (aget api-proxy "relaunch")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn quit
+  []
+  (let [method (aget api-proxy "quit")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn open-external-link
+  [url]
+  (let [method (aget api-proxy "openExternalLink")
+        args [url]]
+    (core/call-method api-proxy method args)))
+
+(defn exec-git-command
+  [args]
+  (let [method (aget api-proxy "execGitCommand")
+        args [args]]
+    (core/call-method api-proxy method args)))
+
+(defn get-current-graph
+  []
+  (let [method (aget api-proxy "getCurrentGraph")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn check-current-is-db-graph
+  []
+  (let [method (aget api-proxy "checkCurrentIsDbGraph")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-current-graph-configs
+  [& keys]
+  (let [method (aget api-proxy "getCurrentGraphConfigs")
+        rest-keys (vec keys)
+        args (into [] rest-keys)]
+    (core/call-method api-proxy method args)))
+
+(defn set-current-graph-configs
+  [configs]
+  (let [method (aget api-proxy "setCurrentGraphConfigs")
+        args [configs]]
+    (core/call-method api-proxy method args)))
+
+(defn get-current-graph-favorites
+  []
+  (let [method (aget api-proxy "getCurrentGraphFavorites")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-current-graph-recent
+  []
+  (let [method (aget api-proxy "getCurrentGraphRecent")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-current-graph-templates
+  []
+  (let [method (aget api-proxy "getCurrentGraphTemplates")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn- push-state-impl
+  [k params query]
+  (let [method (aget api-proxy "pushState")
+        args [k params query]]
+    (core/call-method api-proxy method args)))
+
+(defn push-state
+  ([k]
+   (push-state-impl k nil nil))
+  ([k params]
+   (push-state-impl k params nil))
+  ([k params query]
+   (push-state-impl k params query)))
+
+(defn- replace-state-impl
+  [k params query]
+  (let [method (aget api-proxy "replaceState")
+        args [k params query]]
+    (core/call-method api-proxy method args)))
+
+(defn replace-state
+  ([k]
+   (replace-state-impl k nil nil))
+  ([k params]
+   (replace-state-impl k params nil))
+  ([k params query]
+   (replace-state-impl k params query)))
+
+(defn get-template
+  [name]
+  (let [method (aget api-proxy "getTemplate")
+        args [name]]
+    (core/call-method api-proxy method args)))
+
+(defn exist-template
+  [name]
+  (let [method (aget api-proxy "existTemplate")
+        args [name]]
+    (core/call-method api-proxy method args)))
+
+(defn- create-template-impl
+  [target name opts]
+  (let [method (aget api-proxy "createTemplate")
+        args [target name opts]]
+    (core/call-method api-proxy method args)))
+
+(defn create-template
+  ([target name]
+   (create-template-impl target name nil))
+  ([target name opts]
+   (create-template-impl target name opts)))
+
+(defn remove-template
+  [name]
+  (let [method (aget api-proxy "removeTemplate")
+        args [name]]
+    (core/call-method api-proxy method args)))
+
+(defn insert-template
+  [target name]
+  (let [method (aget api-proxy "insertTemplate")
+        args [target name]]
+    (core/call-method api-proxy method args)))
+
+(defn set-zoom-factor
+  [factor]
+  (let [method (aget api-proxy "setZoomFactor")
+        args [factor]]
+    (core/call-method api-proxy method args)))
+
+(defn set-full-screen
+  [flag]
+  (let [method (aget api-proxy "setFullScreen")
+        args [flag]]
+    (core/call-method api-proxy method args)))
+
+(defn set-left-sidebar-visible
+  [flag]
+  (let [method (aget api-proxy "setLeftSidebarVisible")
+        args [flag]]
+    (core/call-method api-proxy method args)))
+
+(defn set-right-sidebar-visible
+  [flag]
+  (let [method (aget api-proxy "setRightSidebarVisible")
+        args [flag]]
+    (core/call-method api-proxy method args)))
+
+(defn- clear-right-sidebar-blocks-impl
+  [opts]
+  (let [method (aget api-proxy "clearRightSidebarBlocks")
+        args [opts]]
+    (core/call-method api-proxy method args)))
+
+(defn clear-right-sidebar-blocks
+  ([]
+   (clear-right-sidebar-blocks-impl nil))
+  ([opts]
+   (clear-right-sidebar-blocks-impl opts)))
+
+(defn register-ui-item
+  [type opts]
+  (let [method (aget api-proxy "registerUIItem")
+        args [type opts]]
+    (core/call-method api-proxy method args)))
+
+(defn register-page-menu-item
+  [tag action]
+  (let [method (aget api-proxy "registerPageMenuItem")
+        args [tag action]]
+    (core/call-method api-proxy method args)))
+
+(defn on-current-graph-changed
+  [callback]
+  (let [method (aget api-proxy "onCurrentGraphChanged")
+        args [callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-graph-after-indexed
+  [callback]
+  (let [method (aget api-proxy "onGraphAfterIndexed")
+        args [callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-theme-mode-changed
+  [callback]
+  (let [method (aget api-proxy "onThemeModeChanged")
+        args [callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-theme-changed
+  [callback]
+  (let [method (aget api-proxy "onThemeChanged")
+        args [callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-today-journal-created
+  [callback]
+  (let [method (aget api-proxy "onTodayJournalCreated")
+        args [callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-before-command-invoked
+  [condition callback]
+  (let [method (aget api-proxy "onBeforeCommandInvoked")
+        args [condition callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-after-command-invoked
+  [condition callback]
+  (let [method (aget api-proxy "onAfterCommandInvoked")
+        args [condition callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-block-renderer-slotted
+  "provide ui slot to specific block with UUID"
+  [condition callback]
+  (let [method (aget api-proxy "onBlockRendererSlotted")
+        args [condition callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-macro-renderer-slotted
+  "provide ui slot to block `renderer` macro for `{{renderer arg1, arg2}}`"
+  [callback]
+  (let [method (aget api-proxy "onMacroRendererSlotted")
+        args [callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-page-head-actions-slotted
+  [callback]
+  (let [method (aget api-proxy "onPageHeadActionsSlotted")
+        args [callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-route-changed
+  [callback]
+  (let [method (aget api-proxy "onRouteChanged")
+        args [callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-sidebar-visible-changed
+  [callback]
+  (let [method (aget api-proxy "onSidebarVisibleChanged")
+        args [callback]]
+    (core/call-method api-proxy method args)))

+ 37 - 0
libs/cljs-sdk/src/com/logseq/assets.cljs

@@ -0,0 +1,37 @@
+;; Auto-generated via `bb libs:generate-cljs-sdk`
+(ns com.logseq.assets
+  (:require [com.logseq.core :as core]))
+
+(def api-proxy (aget js/logseq "Assets"))
+
+(defn- list-files-of-current-graph-impl
+  [exts]
+  (let [method (aget api-proxy "listFilesOfCurrentGraph")
+        args [exts]]
+    (core/call-method api-proxy method args)))
+
+(defn list-files-of-current-graph
+  ([]
+   (list-files-of-current-graph-impl nil))
+  ([exts]
+   (list-files-of-current-graph-impl exts)))
+
+(defn make-sandbox-storage
+  []
+  (let [method (aget api-proxy "makeSandboxStorage")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn make-url
+  "make assets scheme url based on current graph"
+  [path]
+  (let [method (aget api-proxy "makeUrl")
+        args [path]]
+    (core/call-method api-proxy method args)))
+
+(defn built-in-open
+  "try to open asset type file in Logseq app"
+  [path]
+  (let [method (aget api-proxy "builtInOpen")
+        args [path]]
+    (core/call-method api-proxy method args)))

+ 149 - 0
libs/cljs-sdk/src/com/logseq/core.cljs

@@ -0,0 +1,149 @@
+;; Auto-generated via `bb libs:generate-cljs-sdk`
+(ns com.logseq.core
+  (:require ["@logseq/libs"]
+            [cljs-bean.core :as bean]
+            [com.logseq.util :as util]))
+
+(defn- normalize-result [result]
+  (if (instance? js/Promise result)
+    (.then result (fn [value] (normalize-result value)))
+    (util/->clj-tagged result)))
+
+(defn call-method [owner method args]
+  (when-not method
+    (throw (js/Error. "Missing method on logseq namespace")))
+  (normalize-result (.apply method owner (bean/->js args))))
+
+(def api-proxy js/logseq)
+
+(defn- ready-impl
+  [model callback]
+  (let [method (aget api-proxy "ready")
+        args [model callback]]
+    (-> (call-method api-proxy method args)
+        (.then (fn []
+                 (js/logseq._execCallableAPIAsync
+                  "setSDKMetadata"
+                  #js {:runtime "cljs"}))))))
+
+(defn ready
+  ([]
+   (ready-impl nil nil))
+  ([model]
+   (ready-impl model nil))
+  ([model callback]
+   (ready-impl model callback)))
+
+(defn ensure-connected
+  []
+  (let [method (aget api-proxy "ensureConnected")
+        args []]
+    (call-method api-proxy method args)))
+
+(defn beforeunload
+  [callback]
+  (let [method (aget api-proxy "beforeunload")
+        args [callback]]
+    (call-method api-proxy method args)))
+
+(defn provide-model
+  [model]
+  (let [method (aget api-proxy "provideModel")
+        args [model]]
+    (call-method api-proxy method args)))
+
+(defn provide-theme
+  [theme]
+  (let [method (aget api-proxy "provideTheme")
+        args [theme]]
+    (call-method api-proxy method args)))
+
+(defn provide-style
+  [style]
+  (let [method (aget api-proxy "provideStyle")
+        args [style]]
+    (call-method api-proxy method args)))
+
+(defn provide-ui
+  [ui]
+  (let [method (aget api-proxy "provideUI")
+        args [ui]]
+    (call-method api-proxy method args)))
+
+(defn use-settings-schema
+  [schema]
+  (let [method (aget api-proxy "useSettingsSchema")
+        args [schema]]
+    (call-method api-proxy method args)))
+
+(defn update-settings
+  [attrs]
+  (let [method (aget api-proxy "updateSettings")
+        args [attrs]]
+    (call-method api-proxy method args)))
+
+(defn on-settings-changed
+  [cb]
+  (let [method (aget api-proxy "onSettingsChanged")
+        args [cb]]
+    (call-method api-proxy method args)))
+
+(defn show-settings-ui
+  []
+  (let [method (aget api-proxy "showSettingsUI")
+        args []]
+    (call-method api-proxy method args)))
+
+(defn hide-settings-ui
+  []
+  (let [method (aget api-proxy "hideSettingsUI")
+        args []]
+    (call-method api-proxy method args)))
+
+(defn set-main-ui-attrs
+  [attrs]
+  (let [method (aget api-proxy "setMainUIAttrs")
+        args [attrs]]
+    (call-method api-proxy method args)))
+
+(defn set-main-ui-inline-style
+  [style]
+  (let [method (aget api-proxy "setMainUIInlineStyle")
+        args [style]]
+    (call-method api-proxy method args)))
+
+(defn- hide-main-ui-impl
+  [opts]
+  (let [method (aget api-proxy "hideMainUI")
+        args [opts]]
+    (call-method api-proxy method args)))
+
+(defn hide-main-ui
+  ([]
+   (hide-main-ui-impl nil))
+  ([opts]
+   (hide-main-ui-impl opts)))
+
+(defn- show-main-ui-impl
+  [opts]
+  (let [method (aget api-proxy "showMainUI")
+        args [opts]]
+    (call-method api-proxy method args)))
+
+(defn show-main-ui
+  ([]
+   (show-main-ui-impl nil))
+  ([opts]
+   (show-main-ui-impl opts)))
+
+(defn toggle-main-ui
+  []
+  (let [method (aget api-proxy "toggleMainUI")
+        args []]
+    (call-method api-proxy method args)))
+
+(defn resolve-resource-full-url
+  [file-path]
+  (let [method (aget api-proxy "resolveResourceFullUrl")
+        args [file-path]]
+    (call-method api-proxy method args)))

+ 34 - 0
libs/cljs-sdk/src/com/logseq/db.cljs

@@ -0,0 +1,34 @@
+;; Auto-generated via `bb libs:generate-cljs-sdk`
+(ns com.logseq.db
+  (:require [com.logseq.core :as core]))
+
+(def api-proxy (aget js/logseq "DB"))
+
+(defn q
+  "Run a DSL query"
+  [dsl]
+  (let [method (aget api-proxy "q")
+        args [dsl]]
+    (core/call-method api-proxy method args)))
+
+(defn datascript-query
+  "Run a datascript query"
+  [query & inputs]
+  (let [method (aget api-proxy "datascriptQuery")
+        rest-inputs (vec inputs)
+        args (into [query] rest-inputs)]
+    (core/call-method api-proxy method args)))
+
+(defn on-changed
+  "Hook all transaction data of DB"
+  [callback]
+  (let [method (aget api-proxy "onChanged")
+        args [callback]]
+    (core/call-method api-proxy method args)))
+
+(defn on-block-changed
+  "Subscribe a specific block changed event"
+  [uuid callback]
+  (let [method (aget api-proxy "onBlockChanged")
+        args [uuid callback]]
+    (core/call-method api-proxy method args)))

+ 432 - 0
libs/cljs-sdk/src/com/logseq/editor.cljs

@@ -0,0 +1,432 @@
+;; Auto-generated via `bb libs:generate-cljs-sdk`
+(ns com.logseq.editor
+  (:require [com.logseq.core :as core]))
+
+(def api-proxy (aget js/logseq "Editor"))
+
+(defn register-slash-command
+  "register a custom command which will be added to the Logseq slash command list"
+  [tag action]
+  (let [method (aget api-proxy "registerSlashCommand")
+        args [tag action]]
+    (core/call-method api-proxy method args)))
+
+(defn register-block-context-menu-item
+  "register a custom command in the block context menu (triggered by right-clicking the block dot)"
+  [label action]
+  (let [method (aget api-proxy "registerBlockContextMenuItem")
+        args [label action]]
+    (core/call-method api-proxy method args)))
+
+(defn- register-highlight-context-menu-item-impl
+  [label action opts]
+  (let [method (aget api-proxy "registerHighlightContextMenuItem")
+        args [label action opts]]
+    (core/call-method api-proxy method args)))
+
+(defn register-highlight-context-menu-item
+  "Current it's only available for pdf viewer"
+  ([label action]
+   (register-highlight-context-menu-item-impl label action nil))
+  ([label action opts]
+   (register-highlight-context-menu-item-impl label action opts)))
+
+(defn check-editing
+  []
+  (let [method (aget api-proxy "checkEditing")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn insert-at-editing-cursor
+  [content]
+  (let [method (aget api-proxy "insertAtEditingCursor")
+        args [content]]
+    (core/call-method api-proxy method args)))
+
+(defn restore-editing-cursor
+  []
+  (let [method (aget api-proxy "restoreEditingCursor")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn- exit-editing-mode-impl
+  [select-block]
+  (let [method (aget api-proxy "exitEditingMode")
+        args [select-block]]
+    (core/call-method api-proxy method args)))
+
+(defn exit-editing-mode
+  ([]
+   (exit-editing-mode-impl nil))
+  ([select-block]
+   (exit-editing-mode-impl select-block)))
+
+(defn get-editing-cursor-position
+  []
+  (let [method (aget api-proxy "getEditingCursorPosition")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-editing-block-content
+  []
+  (let [method (aget api-proxy "getEditingBlockContent")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-current-page
+  []
+  (let [method (aget api-proxy "getCurrentPage")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-current-block
+  []
+  (let [method (aget api-proxy "getCurrentBlock")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-selected-blocks
+  []
+  (let [method (aget api-proxy "getSelectedBlocks")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn clear-selected-blocks
+  []
+  (let [method (aget api-proxy "clearSelectedBlocks")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-current-page-blocks-tree
+  "get all blocks of the current page as a tree structure"
+  []
+  (let [method (aget api-proxy "getCurrentPageBlocksTree")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-page-blocks-tree
+  "get all blocks for the specified page"
+  [src-page]
+  (let [method (aget api-proxy "getPageBlocksTree")
+        args [src-page]]
+    (core/call-method api-proxy method args)))
+
+(defn get-page-linked-references
+  "get all page/block linked references"
+  [src-page]
+  (let [method (aget api-proxy "getPageLinkedReferences")
+        args [src-page]]
+    (core/call-method api-proxy method args)))
+
+(defn get-pages-from-namespace
+  "get flatten pages from top namespace"
+  [namespace]
+  (let [method (aget api-proxy "getPagesFromNamespace")
+        args [namespace]]
+    (core/call-method api-proxy method args)))
+
+(defn get-pages-tree-from-namespace
+  "construct pages tree from namespace pages"
+  [namespace]
+  (let [method (aget api-proxy "getPagesTreeFromNamespace")
+        args [namespace]]
+    (core/call-method api-proxy method args)))
+
+(defn new-block-uuid
+  "Create a unique UUID string which can then be assigned to a block."
+  []
+  (let [method (aget api-proxy "newBlockUUID")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn is-page-block
+  [block]
+  (let [method (aget api-proxy "isPageBlock")
+        args [block]]
+    (core/call-method api-proxy method args)))
+
+(defn- insert-block-impl
+  [src-block content opts]
+  (let [method (aget api-proxy "insertBlock")
+        args [src-block content opts]]
+    (core/call-method api-proxy method args)))
+
+(defn insert-block
+  ([src-block content]
+   (insert-block-impl src-block content nil))
+  ([src-block content opts]
+   (insert-block-impl src-block content opts)))
+
+(defn- insert-batch-block-impl
+  [src-block batch opts]
+  (let [method (aget api-proxy "insertBatchBlock")
+        args [src-block batch opts]]
+    (core/call-method api-proxy method args)))
+
+(defn insert-batch-block
+  ([src-block batch]
+   (insert-batch-block-impl src-block batch nil))
+  ([src-block batch opts]
+   (insert-batch-block-impl src-block batch opts)))
+
+(defn- update-block-impl
+  [src-block content opts]
+  (let [method (aget api-proxy "updateBlock")
+        args [src-block content opts]]
+    (core/call-method api-proxy method args)))
+
+(defn update-block
+  ([src-block content]
+   (update-block-impl src-block content nil))
+  ([src-block content opts]
+   (update-block-impl src-block content opts)))
+
+(defn remove-block
+  [src-block]
+  (let [method (aget api-proxy "removeBlock")
+        args [src-block]]
+    (core/call-method api-proxy method args)))
+
+(defn- get-block-impl
+  [src-block opts]
+  (let [method (aget api-proxy "getBlock")
+        args [src-block opts]]
+    (core/call-method api-proxy method args)))
+
+(defn get-block
+  ([src-block]
+   (get-block-impl src-block nil))
+  ([src-block opts]
+   (get-block-impl src-block opts)))
+
+(defn set-block-collapsed
+  [uuid opts]
+  (let [method (aget api-proxy "setBlockCollapsed")
+        args [uuid opts]]
+    (core/call-method api-proxy method args)))
+
+(defn- get-page-impl
+  [src-page opts]
+  (let [method (aget api-proxy "getPage")
+        args [src-page opts]]
+    (core/call-method api-proxy method args)))
+
+(defn get-page
+  ([src-page]
+   (get-page-impl src-page nil))
+  ([src-page opts]
+   (get-page-impl src-page opts)))
+
+(defn- create-page-impl
+  [page-name properties opts]
+  (let [method (aget api-proxy "createPage")
+        args [page-name properties opts]]
+    (core/call-method api-proxy method args)))
+
+(defn create-page
+  ([page-name]
+   (create-page-impl page-name nil nil))
+  ([page-name properties]
+   (create-page-impl page-name properties nil))
+  ([page-name properties opts]
+   (create-page-impl page-name properties opts)))
+
+(defn create-journal-page
+  [date]
+  (let [method (aget api-proxy "createJournalPage")
+        args [date]]
+    (core/call-method api-proxy method args)))
+
+(defn delete-page
+  [page-name]
+  (let [method (aget api-proxy "deletePage")
+        args [page-name]]
+    (core/call-method api-proxy method args)))
+
+(defn rename-page
+  [old-name new-name]
+  (let [method (aget api-proxy "renamePage")
+        args [old-name new-name]]
+    (core/call-method api-proxy method args)))
+
+(defn- get-all-pages-impl
+  [repo]
+  (let [method (aget api-proxy "getAllPages")
+        args [repo]]
+    (core/call-method api-proxy method args)))
+
+(defn get-all-pages
+  ([]
+   (get-all-pages-impl nil))
+  ([repo]
+   (get-all-pages-impl repo)))
+
+(defn get-all-tags
+  []
+  (let [method (aget api-proxy "getAllTags")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-all-properties
+  []
+  (let [method (aget api-proxy "getAllProperties")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-tag-objects
+  [page-identity]
+  (let [method (aget api-proxy "getTagObjects")
+        args [page-identity]]
+    (core/call-method api-proxy method args)))
+
+(defn- prepend-block-in-page-impl
+  [page content opts]
+  (let [method (aget api-proxy "prependBlockInPage")
+        args [page content opts]]
+    (core/call-method api-proxy method args)))
+
+(defn prepend-block-in-page
+  ([page content]
+   (prepend-block-in-page-impl page content nil))
+  ([page content opts]
+   (prepend-block-in-page-impl page content opts)))
+
+(defn- append-block-in-page-impl
+  [page content opts]
+  (let [method (aget api-proxy "appendBlockInPage")
+        args [page content opts]]
+    (core/call-method api-proxy method args)))
+
+(defn append-block-in-page
+  ([page content]
+   (append-block-in-page-impl page content nil))
+  ([page content opts]
+   (append-block-in-page-impl page content opts)))
+
+(defn get-previous-sibling-block
+  [src-block]
+  (let [method (aget api-proxy "getPreviousSiblingBlock")
+        args [src-block]]
+    (core/call-method api-proxy method args)))
+
+(defn get-next-sibling-block
+  [src-block]
+  (let [method (aget api-proxy "getNextSiblingBlock")
+        args [src-block]]
+    (core/call-method api-proxy method args)))
+
+(defn- move-block-impl
+  [src-block target-block opts]
+  (let [method (aget api-proxy "moveBlock")
+        args [src-block target-block opts]]
+    (core/call-method api-proxy method args)))
+
+(defn move-block
+  ([src-block target-block]
+   (move-block-impl src-block target-block nil))
+  ([src-block target-block opts]
+   (move-block-impl src-block target-block opts)))
+
+(defn- edit-block-impl
+  [src-block opts]
+  (let [method (aget api-proxy "editBlock")
+        args [src-block opts]]
+    (core/call-method api-proxy method args)))
+
+(defn edit-block
+  ([src-block]
+   (edit-block-impl src-block nil))
+  ([src-block opts]
+   (edit-block-impl src-block opts)))
+
+(defn select-block
+  [src-block]
+  (let [method (aget api-proxy "selectBlock")
+        args [src-block]]
+    (core/call-method api-proxy method args)))
+
+(defn save-focused-code-editor-content
+  []
+  (let [method (aget api-proxy "saveFocusedCodeEditorContent")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn get-property
+  [key]
+  (let [method (aget api-proxy "getProperty")
+        args [key]]
+    (core/call-method api-proxy method args)))
+
+(defn- upsert-property-impl
+  [key schema opts]
+  (let [method (aget api-proxy "upsertProperty")
+        args [key schema opts]]
+    (core/call-method api-proxy method args)))
+
+(defn upsert-property
+  ([key]
+   (upsert-property-impl key nil nil))
+  ([key schema]
+   (upsert-property-impl key schema nil))
+  ([key schema opts]
+   (upsert-property-impl key schema opts)))
+
+(defn remove-property
+  [key]
+  (let [method (aget api-proxy "removeProperty")
+        args [key]]
+    (core/call-method api-proxy method args)))
+
+(defn upsert-block-property
+  [block key value]
+  (let [method (aget api-proxy "upsertBlockProperty")
+        args [block key value]]
+    (core/call-method api-proxy method args)))
+
+(defn remove-block-property
+  [block key]
+  (let [method (aget api-proxy "removeBlockProperty")
+        args [block key]]
+    (core/call-method api-proxy method args)))
+
+(defn get-block-property
+  [block key]
+  (let [method (aget api-proxy "getBlockProperty")
+        args [block key]]
+    (core/call-method api-proxy method args)))
+
+(defn get-block-properties
+  [block]
+  (let [method (aget api-proxy "getBlockProperties")
+        args [block]]
+    (core/call-method api-proxy method args)))
+
+(defn get-page-properties
+  [page]
+  (let [method (aget api-proxy "getPageProperties")
+        args [page]]
+    (core/call-method api-proxy method args)))
+
+(defn- scroll-to-block-in-page-impl
+  [page-name block-id opts]
+  (let [method (aget api-proxy "scrollToBlockInPage")
+        args [page-name block-id opts]]
+    (core/call-method api-proxy method args)))
+
+(defn scroll-to-block-in-page
+  ([page-name block-id]
+   (scroll-to-block-in-page-impl page-name block-id nil))
+  ([page-name block-id opts]
+   (scroll-to-block-in-page-impl page-name block-id opts)))
+
+(defn open-in-right-sidebar
+  [id]
+  (let [method (aget api-proxy "openInRightSidebar")
+        args [id]]
+    (core/call-method api-proxy method args)))
+
+(defn on-input-selection-end
+  [callback]
+  (let [method (aget api-proxy "onInputSelectionEnd")
+        args [callback]]
+    (core/call-method api-proxy method args)))

+ 23 - 0
libs/cljs-sdk/src/com/logseq/git.cljs

@@ -0,0 +1,23 @@
+;; Auto-generated via `bb libs:generate-cljs-sdk`
+(ns com.logseq.git
+  (:require [com.logseq.core :as core]))
+
+(def api-proxy (aget js/logseq "Git"))
+
+(defn exec-command
+  [args]
+  (let [method (aget api-proxy "execCommand")
+        args [args]]
+    (core/call-method api-proxy method args)))
+
+(defn load-ignore-file
+  []
+  (let [method (aget api-proxy "loadIgnoreFile")
+        args []]
+    (core/call-method api-proxy method args)))
+
+(defn save-ignore-file
+  [content]
+  (let [method (aget api-proxy "saveIgnoreFile")
+        args [content]]
+    (core/call-method api-proxy method args)))

+ 49 - 0
libs/cljs-sdk/src/com/logseq/ui.cljs

@@ -0,0 +1,49 @@
+;; Auto-generated via `bb libs:generate-cljs-sdk`
+(ns com.logseq.ui
+  (:require [com.logseq.core :as core]))
+
+(def api-proxy (aget js/logseq "UI"))
+
+(defn- show-msg-impl
+  [content status opts]
+  (let [method (aget api-proxy "showMsg")
+        args [content status opts]]
+    (core/call-method api-proxy method args)))
+
+(defn show-msg
+  ([content]
+   (show-msg-impl content nil nil))
+  ([content status]
+   (show-msg-impl content status nil))
+  ([content status opts]
+   (show-msg-impl content status opts)))
+
+(defn close-msg
+  [key]
+  (let [method (aget api-proxy "closeMsg")
+        args [key]]
+    (core/call-method api-proxy method args)))
+
+(defn query-element-rect
+  [selector]
+  (let [method (aget api-proxy "queryElementRect")
+        args [selector]]
+    (core/call-method api-proxy method args)))
+
+(defn query-element-by-id
+  [id]
+  (let [method (aget api-proxy "queryElementById")
+        args [id]]
+    (core/call-method api-proxy method args)))
+
+(defn check-slot-valid
+  [slot]
+  (let [method (aget api-proxy "checkSlotValid")
+        args [slot]]
+    (core/call-method api-proxy method args)))
+
+(defn resolve-theme-css-props-vals
+  [props]
+  (let [method (aget api-proxy "resolveThemeCssPropsVals")
+        args [props]]
+    (core/call-method api-proxy method args)))

+ 31 - 0
libs/cljs-sdk/src/com/logseq/util.cljs

@@ -0,0 +1,31 @@
+(ns com.logseq.util
+  (:require [cljs-bean.core :as bean]
+            [clojure.string :as string]
+            [clojure.walk :as walk]))
+
+(def ^:private kw-tag "___kw___")
+
+(defn- decode-kw [v]
+  (if (and (string? v) (string/starts-with? v kw-tag))
+    (let [s (subs v (count kw-tag))
+          i (.indexOf s "/")]
+      (if (neg? i)
+        (keyword s)                                ; :name
+        (keyword (subs s 0 i) (subs s (inc i)))))  ; :ns/name
+    v))
+
+(defn ->clj-tagged [js]
+  (some->> js
+           bean/->clj
+           (walk/postwalk (fn [f]
+                            (cond
+                              (keyword? f)
+                              (decode-kw (if-let [ns (namespace f)]
+                                           (str ns "/" (name f))
+                                           (name f)))
+
+                              (string? f)
+                              (decode-kw f)
+
+                              :else
+                              f)))))

+ 121 - 0
libs/cljs-sdk/yarn.lock

@@ -0,0 +1,121 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@logseq/libs@^0.2.3":
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/@logseq/libs/-/libs-0.2.3.tgz#33c4b7ad1db02a2335269cd38eac7e5f5196f675"
+  integrity sha512-aMtZFsNvbFgVhiaA9K9DANPhIv+ZKaC5qW61si2UMsfwgykr4/hyT9Q7aoqf7ir3ZIwYDDhfEJqpthZCsUwbeA==
+  dependencies:
+    csstype "3.1.0"
+    debug "4.3.4"
+    deepmerge "4.3.1"
+    dompurify "2.5.4"
+    eventemitter3 "4.0.7"
+    fast-deep-equal "3.1.3"
+    lodash-es "4.17.21"
+    path "0.12.7"
+    snake-case "3.0.4"
+
[email protected]:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
+  integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
+
[email protected]:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
[email protected]:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
+  integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
+
[email protected]:
+  version "2.5.4"
+  resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.4.tgz#347e91070963b22db31c7c8d0ce9a0a2c3c08746"
+  integrity sha512-l5NNozANzaLPPe0XaAwvg3uZcHtDBnziX/HjsY1UcDj1MxTK8Dd0Kv096jyPK5HRzs/XM5IMj20dW8Fk+HnbUA==
+
+dot-case@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
+  integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
+  dependencies:
+    no-case "^3.0.4"
+    tslib "^2.0.3"
+
[email protected]:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
[email protected]:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
[email protected]:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+  integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
+
[email protected]:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
+lower-case@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
+  integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
+  dependencies:
+    tslib "^2.0.3"
+
[email protected]:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+no-case@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
+  integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
+  dependencies:
+    lower-case "^2.0.2"
+    tslib "^2.0.3"
+
[email protected]:
+  version "0.12.7"
+  resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
+  integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==
+  dependencies:
+    process "^0.11.1"
+    util "^0.10.3"
+
+process@^0.11.1:
+  version "0.11.10"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+  integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
[email protected]:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
+  integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==
+  dependencies:
+    dot-case "^3.0.4"
+    tslib "^2.0.3"
+
+tslib@^2.0.3:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+  integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
+util@^0.10.3:
+  version "0.10.4"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
+  integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
+  dependencies:
+    inherits "2.0.3"

+ 3 - 1
libs/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@logseq/libs",
   "name": "@logseq/libs",
-  "version": "0.2.1",
+  "version": "0.2.3",
   "description": "Logseq SDK libraries",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",
   "typings": "index.d.ts",
@@ -10,6 +10,7 @@
     "dev:user": "npm run build:user -- --mode development --watch",
     "dev:user": "npm run build:user -- --mode development --watch",
     "build:core": "webpack --config webpack.config.core.js --mode production",
     "build:core": "webpack --config webpack.config.core.js --mode production",
     "dev:core": "npm run build:core -- --mode development --watch",
     "dev:core": "npm run build:core -- --mode development --watch",
+    "generate:schema": "node scripts/extract-sdk-schema.js",
     "build": "tsc && rm dist/*.js && npm run build:user",
     "build": "tsc && rm dist/*.js && npm run build:user",
     "lint": "prettier --check \"src/**/*.{ts, js}\"",
     "lint": "prettier --check \"src/**/*.{ts, js}\"",
     "fix": "prettier --write \"src/**/*.{ts, js}\"",
     "fix": "prettier --write \"src/**/*.{ts, js}\"",
@@ -27,6 +28,7 @@
     "snake-case": "3.0.4"
     "snake-case": "3.0.4"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "ts-morph": "^22.0.0",
     "@babel/core": "^7.20.2",
     "@babel/core": "^7.20.2",
     "@babel/preset-env": "^7.20.2",
     "@babel/preset-env": "^7.20.2",
     "@types/debug": "^4.1.5",
     "@types/debug": "^4.1.5",

+ 184 - 0
libs/scripts/extract-sdk-schema.js

@@ -0,0 +1,184 @@
+#!/usr/bin/env node
+/**
+ * Extracts metadata about the Logseq JS SDK from the generated *.d.ts files.
+ *
+ * This script uses ts-morph so we can rely on the TypeScript compiler's view of
+ * the declarations. We intentionally read the emitted declaration files in
+ * dist/ so that consumers do not need to depend on the source layout.
+ *
+ * The resulting schema is written to dist/logseq-sdk-schema.json and contains
+ * a simplified representation that downstream tooling (Babashka) can consume.
+ */
+
+const fs = require('node:fs');
+const path = require('node:path');
+const { Project, Node } = require('ts-morph');
+
+const ROOT = path.resolve(__dirname, '..');
+const DIST_DIR = path.join(ROOT, 'dist');
+const OUTPUT_FILE = path.join(DIST_DIR, 'logseq-sdk-schema.json');
+const DECL_FILES = [
+  'LSPlugin.d.ts',
+  'LSPlugin.user.d.ts',
+];
+
+/**
+ * Interfaces whose methods will be turned into CLJS wrappers at runtime.
+ * These correspond to `logseq.<Namespace>` targets in the JS SDK.
+ */
+const TARGET_INTERFACES = [
+  'IAppProxy',
+  'IEditorProxy',
+  'IDBProxy',
+  'IUIProxy',
+  'IUtilsProxy',
+  'IGitProxy',
+  'IAssetsProxy',
+];
+
+/**
+ * Simple heuristics to determine whether a parameter should be converted via
+ * cljs-bean when crossing the JS <-> CLJS boundary.
+ */
+const BEAN_TO_JS_REGEX =
+  /(Record<|Array<|Partial<|UIOptions|UIContainerAttrs|StyleString|StyleOptions|object|any|unknown|IHookEvent|BlockEntity|PageEntity|Promise<\s*Record)/i;
+
+const project = new Project({
+  compilerOptions: { allowJs: true },
+});
+
+DECL_FILES.forEach((file) => {
+  const full = path.join(DIST_DIR, file);
+  if (fs.existsSync(full)) {
+    project.addSourceFileAtPath(full);
+  }
+});
+
+const schema = {
+  generatedAt: new Date().toISOString(),
+  interfaces: {},
+  classes: {},
+};
+
+const serializeDoc = (symbol) => {
+  if (!symbol) return undefined;
+  const decl = symbol.getDeclarations()[0];
+  if (!decl) return undefined;
+
+  const docs = decl
+    .getJsDocs()
+    .map((doc) => doc.getComment())
+    .filter(Boolean);
+  return docs.length ? docs.join('\n\n') : undefined;
+};
+
+const serializeParameter = (signature, symbol, memberNode) => {
+  const name = symbol.getName();
+  const declaration = symbol.getDeclarations()[0];
+
+  let typeText;
+  let optional = symbol.isOptional?.() ?? false;
+  let rest = symbol.isRestParameter?.() ?? false;
+
+  if (declaration && Node.isParameterDeclaration(declaration)) {
+    typeText = declaration.getType().getText();
+    optional = declaration.hasQuestionToken?.() ?? false;
+    rest = declaration.isRestParameter?.() ?? false;
+  } else {
+    const location =
+      signature.getDeclaration?.() ??
+      memberNode ??
+      declaration ??
+      symbol.getDeclarations()[0];
+    typeText = symbol.getTypeAtLocation(location).getText();
+  }
+
+  const convertToJs = BEAN_TO_JS_REGEX.test(typeText);
+
+  return {
+    name,
+    type: typeText,
+    optional,
+    rest,
+    beanToJs: convertToJs,
+  };
+};
+
+const serializeSignature = (sig, memberNode) => {
+  const params = sig.getParameters().map((paramSymbol) =>
+    serializeParameter(sig, paramSymbol, memberNode)
+  );
+  const returnType = sig.getReturnType().getText();
+  return {
+    parameters: params,
+    returnType,
+  };
+};
+
+const serializeCallable = (symbol, member) => {
+  if (!symbol) return null;
+  const type = symbol.getTypeAtLocation(member);
+  const callSignatures = type.getCallSignatures();
+  if (!callSignatures.length) {
+    return null;
+  }
+
+  return {
+    name: symbol.getName(),
+    documentation: serializeDoc(symbol),
+    signatures: callSignatures.map((sig) => serializeSignature(sig, member)),
+  };
+};
+
+const sourceFiles = project.getSourceFiles();
+sourceFiles.forEach((source) => {
+  source.getInterfaces().forEach((iface) => {
+    const name = iface.getName();
+    if (!TARGET_INTERFACES.includes(name)) {
+      return;
+    }
+
+    const interfaceSymbol = iface.getType().getSymbol();
+    const doc = serializeDoc(interfaceSymbol);
+    const methods = iface
+      .getMembers()
+      .map((member) => serializeCallable(member.getSymbol(), member))
+      .filter(Boolean);
+
+    schema.interfaces[name] = {
+      documentation: doc,
+      methods,
+    };
+  });
+
+  source.getClasses().forEach((cls) => {
+    const name = cls.getName();
+    if (name !== 'LSPluginUser') {
+      return;
+    }
+
+    const classSymbol = cls.getType().getSymbol();
+    const doc = serializeDoc(classSymbol);
+    const methods = cls
+      .getInstanceMethods()
+      .filter((method) => method.getName() !== 'constructor')
+      .map((method) => serializeCallable(method.getSymbol(), method))
+      .filter(Boolean);
+    const getters = cls.getGetAccessors().map((accessor) => ({
+      name: accessor.getName(),
+      documentation: serializeDoc(accessor.getSymbol()),
+      returnType: accessor.getReturnType().getText(),
+    }));
+
+    schema.classes[name] = {
+      documentation: doc,
+      methods,
+      getters,
+    };
+  });
+});
+
+fs.mkdirSync(DIST_DIR, { recursive: true });
+fs.writeFileSync(OUTPUT_FILE, JSON.stringify(schema, null, 2));
+
+console.log(`Wrote ${OUTPUT_FILE}`);

+ 3 - 1
libs/src/LSPlugin.core.ts

@@ -154,6 +154,7 @@ interface PluginLocalOptions {
   url: string // Plugin package absolute fs location
   url: string // Plugin package absolute fs location
   name: string
   name: string
   version: string
   version: string
+  runtime: string
   mode: 'shadow' | 'iframe'
   mode: 'shadow' | 'iframe'
   webPkg?: any // web plugin package.json data
   webPkg?: any // web plugin package.json data
   settingsSchema?: SettingSchemaDesc[]
   settingsSchema?: SettingSchemaDesc[]
@@ -166,6 +167,7 @@ interface PluginLocalOptions {
 
 
 interface PluginLocalSDKMetadata {
 interface PluginLocalSDKMetadata {
   version: string
   version: string
+  runtime: string
 
 
   [key: string]: any
   [key: string]: any
 }
 }
@@ -669,7 +671,7 @@ class PluginLocal extends EventEmitter<
           ? `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>`
           ? `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>`
           : `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`
           : `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`
       }
       }
-    
+
   </head>
   </head>
   <body>
   <body>
   <div id="app"></div>
   <div id="app"></div>

+ 5 - 0
libs/src/LSPlugin.ts

@@ -703,6 +703,8 @@ export interface IEditorProxy extends Record<string, any> {
     opts?: Partial<{
     opts?: Partial<{
       before: boolean
       before: boolean
       sibling: boolean
       sibling: boolean
+      start: boolean
+      end: boolean
       isPageBlock: boolean
       isPageBlock: boolean
       focus: boolean
       focus: boolean
       customUUID: string
       customUUID: string
@@ -774,6 +776,9 @@ export interface IEditorProxy extends Record<string, any> {
   renamePage: (oldName: string, newName: string) => Promise<void>
   renamePage: (oldName: string, newName: string) => Promise<void>
 
 
   getAllPages: (repo?: string) => Promise<PageEntity[] | null>
   getAllPages: (repo?: string) => Promise<PageEntity[] | null>
+  getAllTags: () => Promise<PageEntity[] | null>
+  getAllProperties: () => Promise<PageEntity[] | null>
+  getTagObjects: (PageIdentity) => Promise<BlockEntity[] | null>
 
 
   prependBlockInPage: (
   prependBlockInPage: (
     page: PageIdentity,
     page: PageIdentity,

+ 1 - 0
libs/src/LSPlugin.user.ts

@@ -575,6 +575,7 @@ export class LSPluginUser
       try {
       try {
         await this._execCallableAPIAsync('setSDKMetadata', {
         await this._execCallableAPIAsync('setSDKMetadata', {
           version: this._version,
           version: this._version,
+          runtime: 'js',
         })
         })
       } catch (e) {
       } catch (e) {
         console.warn(e)
         console.warn(e)

+ 147 - 0
libs/yarn.lock

@@ -1066,11 +1066,42 @@
     "@jridgewell/resolve-uri" "^3.0.3"
     "@jridgewell/resolve-uri" "^3.0.3"
     "@jridgewell/sourcemap-codec" "^1.4.10"
     "@jridgewell/sourcemap-codec" "^1.4.10"
 
 
+"@nodelib/[email protected]":
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.5"
+    run-parallel "^1.1.9"
+
+"@nodelib/[email protected]", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.5"
+    fastq "^1.6.0"
+
 "@polka/url@^1.0.0-next.17":
 "@polka/url@^1.0.0-next.17":
   version "1.0.0-next.17"
   version "1.0.0-next.17"
   resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.17.tgz#25fdbdfd282c2f86ddf3fcefbd98be99cd2627e2"
   resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.17.tgz#25fdbdfd282c2f86ddf3fcefbd98be99cd2627e2"
   integrity sha512-0p1rCgM3LLbAdwBnc7gqgnvjHg9KpbhcSphergHShlkWz8EdPawoMJ3/VbezI0mGC5eKCDzMaPgF9Yca6cKvrg==
   integrity sha512-0p1rCgM3LLbAdwBnc7gqgnvjHg9KpbhcSphergHShlkWz8EdPawoMJ3/VbezI0mGC5eKCDzMaPgF9Yca6cKvrg==
 
 
+"@ts-morph/common@~0.23.0":
+  version "0.23.0"
+  resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.23.0.tgz#bd4ddbd3f484f29476c8bd985491592ae5fc147e"
+  integrity sha512-m7Lllj9n/S6sOkCkRftpM7L24uvmfXQFedlW/4hENcuJH1HHm9u5EgxZb9uVjQSCGrbBWBkOGgcTxNg36r6ywA==
+  dependencies:
+    fast-glob "^3.3.2"
+    minimatch "^9.0.3"
+    mkdirp "^3.0.1"
+    path-browserify "^1.0.1"
+
 "@types/debug@^4.1.5":
 "@types/debug@^4.1.5":
   version "4.1.7"
   version "4.1.7"
   resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
   resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@@ -1434,6 +1465,13 @@ braces@^3.0.1:
   dependencies:
   dependencies:
     fill-range "^7.0.1"
     fill-range "^7.0.1"
 
 
+braces@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+  integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+  dependencies:
+    fill-range "^7.1.1"
+
 browserslist@^4.14.5:
 browserslist@^4.14.5:
   version "4.16.8"
   version "4.16.8"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0"
@@ -1501,6 +1539,11 @@ clone-deep@^4.0.1:
     kind-of "^6.0.2"
     kind-of "^6.0.2"
     shallow-clone "^3.0.0"
     shallow-clone "^3.0.0"
 
 
+code-block-writer@^13.0.1:
+  version "13.0.3"
+  resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-13.0.3.tgz#90f8a84763a5012da7af61319dd638655ae90b5b"
+  integrity sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==
+
 color-convert@^1.9.0:
 color-convert@^1.9.0:
   version "1.9.3"
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -1709,6 +1752,17 @@ [email protected], fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
 
+fast-glob@^3.3.2:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
+  integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.2"
+    merge2 "^1.3.0"
+    micromatch "^4.0.8"
+
 fast-json-stable-stringify@^2.0.0:
 fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@@ -1719,6 +1773,13 @@ fastest-levenshtein@^1.0.12:
   resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
   resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
   integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
   integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
 
 
+fastq@^1.6.0:
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
+  integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
+  dependencies:
+    reusify "^1.0.4"
+
 fill-range@^7.0.1:
 fill-range@^7.0.1:
   version "7.0.1"
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
   resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -1726,6 +1787,13 @@ fill-range@^7.0.1:
   dependencies:
   dependencies:
     to-regex-range "^5.0.1"
     to-regex-range "^5.0.1"
 
 
+fill-range@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+  integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+  dependencies:
+    to-regex-range "^5.0.1"
+
 find-cache-dir@^3.3.2:
 find-cache-dir@^3.3.2:
   version "3.3.2"
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
   resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
@@ -1763,6 +1831,13 @@ get-stream@^6.0.0:
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
   integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
   integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
 
 
+glob-parent@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
 glob-to-regexp@^0.4.1:
 glob-to-regexp@^0.4.1:
   version "0.4.1"
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
@@ -1868,6 +1943,18 @@ is-core-module@^2.9.0:
   dependencies:
   dependencies:
     has "^1.0.3"
     has "^1.0.3"
 
 
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
 is-number@^7.0.0:
 is-number@^7.0.0:
   version "7.0.0"
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
@@ -2028,6 +2115,11 @@ merge-stream@^2.0.0:
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
   integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
   integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
 
 
+merge2@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
 micromatch@^4.0.0:
 micromatch@^4.0.0:
   version "4.0.4"
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
@@ -2036,6 +2128,14 @@ micromatch@^4.0.0:
     braces "^3.0.1"
     braces "^3.0.1"
     picomatch "^2.2.3"
     picomatch "^2.2.3"
 
 
+micromatch@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
+  integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
+  dependencies:
+    braces "^3.0.3"
+    picomatch "^2.3.1"
+
 [email protected]:
 [email protected]:
   version "1.49.0"
   version "1.49.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
@@ -2065,6 +2165,18 @@ minimatch@^5.0.1, minimatch@^5.1.0:
   dependencies:
   dependencies:
     brace-expansion "^2.0.1"
     brace-expansion "^2.0.1"
 
 
+minimatch@^9.0.3:
+  version "9.0.5"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
+  integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
+  dependencies:
+    brace-expansion "^2.0.1"
+
+mkdirp@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
+  integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
+
 [email protected]:
 [email protected]:
   version "2.1.2"
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -2150,6 +2262,11 @@ p-try@^2.0.0:
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
 
+path-browserify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
+  integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
+
 path-exists@^4.0.0:
 path-exists@^4.0.0:
   version "4.0.0"
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -2183,6 +2300,11 @@ picomatch@^2.2.3:
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
   integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
   integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
 
 
+picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
 pkg-dir@^4.1.0, pkg-dir@^4.2.0:
 pkg-dir@^4.1.0, pkg-dir@^4.2.0:
   version "4.2.0"
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
   resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@@ -2210,6 +2332,11 @@ punycode@^2.1.0:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
 
+queue-microtask@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
 randombytes@^2.1.0:
 randombytes@^2.1.0:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
   resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -2314,6 +2441,18 @@ resolve@^1.9.0:
     is-core-module "^2.2.0"
     is-core-module "^2.2.0"
     path-parse "^1.0.6"
     path-parse "^1.0.6"
 
 
+reusify@^1.0.4:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
+  integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
+
+run-parallel@^1.1.9:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+  integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+  dependencies:
+    queue-microtask "^1.2.2"
+
 safe-buffer@^5.1.0:
 safe-buffer@^5.1.0:
   version "5.2.1"
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@@ -2535,6 +2674,14 @@ [email protected]:
     micromatch "^4.0.0"
     micromatch "^4.0.0"
     semver "^7.3.4"
     semver "^7.3.4"
 
 
+ts-morph@^22.0.0:
+  version "22.0.0"
+  resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-22.0.0.tgz#5532c592fb6dddae08846f12c9ab0fc590b1d42e"
+  integrity sha512-M9MqFGZREyeb5fTl6gNHKZLqBQA0TjA1lea+CR48R8EBTDuWrNqW6ccC5QvjNR4s6wDumD3LTCjOFSp9iwlzaw==
+  dependencies:
+    "@ts-morph/common" "~0.23.0"
+    code-block-writer "^13.0.1"
+
 tslib@^2.0.3:
 tslib@^2.0.3:
   version "2.3.1"
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"

+ 1 - 1
package.json

@@ -4,7 +4,7 @@
     "private": true,
     "private": true,
     "main": "static/electron.js",
     "main": "static/electron.js",
     "engines": {
     "engines": {
-        "node": ">=20.19.1"
+        "node": ">=22.20.0"
     },
     },
     "devDependencies": {
     "devDependencies": {
         "@axe-core/playwright": "=4.4.4",
         "@axe-core/playwright": "=4.4.4",

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
resources/js/lsplugin.core.js


+ 2 - 2
resources/package.json

@@ -15,7 +15,7 @@
     "electron:make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
     "electron:make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
     "electron:make-win-arm64": "electron-forge make --platform=win32 --arch=arm64",
     "electron:make-win-arm64": "electron-forge make --platform=win32 --arch=arm64",
     "electron:publish:github": "electron-forge publish",
     "electron:publish:github": "electron-forge publish",
-    "rebuild:all": "electron-rebuild -v 37.2.6 -f",
+    "rebuild:all": "electron-rebuild -v 38.4.0 -f",
     "postinstall": "install-app-deps"
     "postinstall": "install-app-deps"
   },
   },
   "config": {
   "config": {
@@ -57,7 +57,7 @@
     "@electron-forge/maker-wix": "^7.8.3",
     "@electron-forge/maker-wix": "^7.8.3",
     "@electron-forge/maker-zip": "^7.8.3",
     "@electron-forge/maker-zip": "^7.8.3",
     "@electron/rebuild": "4.0.1",
     "@electron/rebuild": "4.0.1",
-    "electron": "37.2.6",
+    "electron": "38.4.0",
     "electron-builder": "26.0.12",
     "electron-builder": "26.0.12",
     "electron-devtools-installer": "4.0.0",
     "electron-devtools-installer": "4.0.0",
     "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git"
     "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git"

+ 1 - 1
scripts/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "version": "1.0.0",
   "private": true,
   "private": true,
   "devDependencies": {
   "devDependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v29"
   },
   },
   "dependencies": {
   "dependencies": {
     "better-sqlite3": "11.10.0",
     "better-sqlite3": "11.10.0",

+ 234 - 0
scripts/src/logseq/libs/sdk_generator.clj

@@ -0,0 +1,234 @@
+(ns logseq.libs.sdk-generator
+  (:require [babashka.fs :as fs]
+            [cheshire.core :as json]
+            [clojure.string :as string]))
+
+(def default-schema "libs/dist/logseq-sdk-schema.json")
+(def default-output-dir "libs/cljs-sdk/src")
+(def default-ns-prefix "com.logseq")
+(def core-namespace "core")
+
+(defn parse-args
+  [args]
+  (loop [opts {}
+         tokens args]
+    (if (empty? tokens)
+      opts
+      (let [[flag value & more] tokens]
+        (case flag
+          "--schema" (recur (assoc opts :schema value) more)
+          "--out-dir" (recur (assoc opts :out-dir value) more)
+          "--out" (recur (assoc opts :out-dir value) more)
+          "--ns-prefix" (recur (assoc opts :ns-prefix value) more)
+          (throw (ex-info (str "Unknown flag: " flag) {:flag flag})))))))
+
+(defn camel->kebab [s]
+  (-> s
+      (string/replace #"([a-z0-9])([A-Z])" "$1-$2")
+      (string/replace #"([A-Z]+)([A-Z][a-z])" "$1-$2")
+      (string/lower-case)
+      (string/replace #"[^a-z0-9]+" "-")
+      (string/replace #"(^-|-$)" "")))
+
+(defn interface->target [iface-name]
+  (-> iface-name
+      (string/replace #"^I" "")
+      (string/replace #"Proxy$" "")))
+
+(defn interface->namespace [ns-prefix iface-name]
+  (str ns-prefix "." (camel->kebab (interface->target iface-name))))
+
+(defn getter->interface-name [return-type]
+  (some->> (re-find #"\.(I[A-Za-z0-9]+)" return-type)
+           second))
+
+(defn iface-key->string [k]
+  (cond
+    (string? k) k
+    (keyword? k) (name k)
+    :else (str k)))
+
+(defn format-docstring [doc]
+  (when (and doc (not (string/blank? doc)))
+    (str "  " (pr-str doc) "\n")))
+
+(defn param->info
+  [{:keys [name optional rest rest?]}]
+  (let [sym (camel->kebab name)]
+    {:name name
+     :sym sym
+     :optional (boolean optional)
+     :rest (boolean (or rest rest?))}))
+
+(defn emit-rest-binding [{:keys [sym]}]
+  (let [rest-var (str "rest-" sym)
+        line (str "        " rest-var " (vec " sym ")\n")]
+    {:binding line
+     :var rest-var}))
+
+(defn format-param-vector [params]
+  (str "[" (string/join " " params) "]"))
+
+(defn emit-method-body
+  [method-name params {:keys [call]}]
+  (let [rest-param (some #(when (:rest %) %) params)
+        fixed-params (->> (if rest-param (vec (remove :rest params)) params)
+                          (map :sym))
+        {:keys [binding var]} (when rest-param (emit-rest-binding rest-param))
+        rest-lines (if binding [binding] [])
+        args-expr (if rest-param
+                    (str "(into [" (string/join " " fixed-params) "] " var ")")
+                    (str "[" (string/join " " fixed-params) "]"))]
+    (str (format "  (let [method (aget api-proxy \"%s\")\n"  method-name)
+         (apply str rest-lines)
+         "        args " args-expr "]\n"
+         "    (" call " api-proxy method args)))\n")))
+
+(defn emit-optional-def
+  [fn-name doc-str params impl-name helpers method-name]
+  (let [required (take-while (complement :optional) params)
+        total (count params)
+        param-syms (map :sym params)
+        arities (range (count required) (inc total))
+        header (str "\n(defn " fn-name "\n"
+                    (or doc-str ""))]
+    (str "\n(defn- " impl-name "\n"
+         "  " (format-param-vector param-syms) "\n"
+         (emit-method-body method-name params helpers)
+         header
+         (apply str
+                (map-indexed
+                 (fn [idx arity]
+                   (let [provided (take arity param-syms)
+                         missing (- total arity)
+                         call-args (concat provided (repeat missing "nil"))
+                         param-vector (format-param-vector provided)
+                         call-arg-str (string/join " " call-args)
+                         call-arg-str (if (string/blank? call-arg-str) "" (str " " call-arg-str))]
+                     (str "  (" param-vector "\n"
+                          "   (" impl-name call-arg-str "))"
+                          (when (not= (inc idx) (count arities))
+                            "\n"))))
+                 arities))
+         ")\n")))
+
+(defn emit-method
+  [{:keys [name documentation signatures]}
+   helpers]
+  (let [{:keys [parameters]} (apply max-key #(count (:parameters %)) signatures)
+        params (map param->info parameters)
+        fn-name (camel->kebab name)
+        doc-str (format-docstring documentation)
+        rest-param (some #(when (:rest %) %) params)
+        optional-params (filter :optional params)
+        impl-name (str fn-name "-impl")
+        method-body (emit-method-body name params helpers)]
+    (when-not (string/starts-with? name "_") ; system methods
+      (cond
+        rest-param
+        (let [fixed-syms (map :sym (vec (remove :rest params)))
+              param-vector (format-param-vector (concat fixed-syms ["&" (:sym rest-param)]))]
+          (str "\n(defn " fn-name "\n"
+               (or doc-str "")
+               "  " param-vector "\n"
+               method-body))
+
+        (seq optional-params)
+        (emit-optional-def fn-name doc-str params impl-name helpers name)
+
+        :else
+        (let [param-vector (format-param-vector (map :sym params))]
+          (str "\n(defn " fn-name "\n"
+               (or doc-str "")
+               "  " param-vector "\n"
+               method-body))))))
+
+(defn emit-core-namespace
+  [ns-prefix {:keys [methods]}]
+  (let [ns (str ns-prefix "." core-namespace)
+        header (str ";; Auto-generated via `bb libs:generate-cljs-sdk`\n"
+                    "(ns " ns "\n"
+                    "  (:require [\"@logseq/libs\"]
+            [cljs-bean.core :as bean]
+            [com.logseq.util :as util]))\n\n"
+                    "(defn- normalize-result [result]\n"
+                    "  (if (instance? js/Promise result)\n"
+                    "    (.then result (fn [value] (normalize-result value)))\n"
+                    "    (util/->clj-tagged result)))\n\n"
+                    "(defn call-method [owner method args]
+  (when-not method
+    (throw (js/Error. \"Missing method on logseq namespace\")))
+  (normalize-result (.apply method owner (bean/->js args))))\n")
+        helpers {:call "call-method"}
+        owner "\n(def api-proxy js/logseq)\n"
+        methods-str (->> methods
+                         (map #(emit-method % helpers))
+                         (apply str))]
+    [ns (str header owner methods-str)]))
+
+(defn emit-proxy-namespace
+  [ns-prefix iface-name iface]
+  (let [ns (interface->namespace ns-prefix iface-name)
+        target (interface->target iface-name)
+        owner-expr (format "(aget js/logseq \"%s\")" target)
+        header (str ";; Auto-generated via `bb libs:generate-cljs-sdk`\n"
+                    "(ns " ns "\n"
+                    "  (:require [com.logseq.core :as core]))\n")
+        helpers {:call "core/call-method"}
+        owner (format "\n(def api-proxy %s)\n" owner-expr)
+        methods-str (->> (:methods iface)
+                         (map #(emit-method % helpers))
+                         (apply str))]
+    [ns (str header owner methods-str)]))
+
+(defn namespace->file
+  [out-dir ns]
+  (let [parts (string/split ns #"\.")
+        dir-parts (butlast parts)
+        file-name (str (last parts) ".cljs")]
+    (apply fs/path out-dir (concat dir-parts [file-name]))))
+
+(defn ensure-schema! [schema-path]
+  (when-not (fs/exists? schema-path)
+    (throw (ex-info (str "Schema not found, run `yarn --cwd libs generate:schema` first: " schema-path)
+                    {:schema schema-path}))))
+
+(defn write-namespaces!
+  [out-dir namespaces]
+  (doseq [[ns content] namespaces]
+    (when ns
+      (let [file (namespace->file out-dir ns)]
+        (fs/create-dirs (fs/parent file))
+        (spit (str file) content)
+        (println "Generated" (str file))))))
+
+(defn run!
+  ([] (run! {}))
+  ([opts]
+   (let [schema-path (fs/absolutize (or (:schema opts) default-schema))
+         out-dir (fs/absolutize (or (:out-dir opts) default-output-dir))
+         ns-prefix (or (:ns-prefix opts) default-ns-prefix)]
+     (ensure-schema! schema-path)
+     (let [schema (json/parse-string (slurp (str schema-path)) true)
+           interfaces (:interfaces schema)
+           ls-user (get-in schema [:classes :LSPluginUser])
+           _ (when-not ls-user
+               (throw (ex-info "Missing LSPluginUser metadata in schema" {:schema schema-path})))
+           getters (:getters ls-user)
+           proxy-names (->> getters
+                            (keep #(some-> (getter->interface-name (:returnType %)) keyword))
+                            (remove #{:IUtilsProxy})
+                            distinct)
+           proxies (for [iface-key proxy-names
+                         :let [iface (get interfaces iface-key)]
+                         :when iface]
+                     (emit-proxy-namespace ns-prefix (iface-key->string iface-key) iface))
+           core (emit-core-namespace ns-prefix ls-user)
+           namespaces (cons core proxies)]
+       (fs/create-dirs out-dir)
+       (write-namespaces! out-dir namespaces)
+       out-dir))))
+
+(defn -main [& args]
+  (let [opts (parse-args args)]
+    (run! opts)))

+ 3 - 3
scripts/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 # yarn lockfile v1
 
 
 
 
-"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v28":
-  version "1.2.173-feat-db-v28"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/c0410ff81a8b0d510705581cccd39788d862dc91"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v29":
+  version "1.2.173-feat-db-v29"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/6c4f8eec72a0a5b0c7b96c32cc73f86b045305c5"
   dependencies:
   dependencies:
     import-meta-resolve "^4.1.0"
     import-meta-resolve "^4.1.0"
 
 

+ 4 - 0
src/main/frontend/common.css

@@ -20,6 +20,10 @@ html:not(.is-native-android) {
   serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol !important;
   serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol !important;
 }
 }
 
 
+:root {
+  text-autospace: normal;
+}
+
 /* region Reset top elements */
 /* region Reset top elements */
 html {
 html {
   overflow: hidden;
   overflow: hidden;

+ 1 - 1
src/main/frontend/components/container.css

@@ -232,7 +232,7 @@
             @apply px-2 flex items-center justify-between relative h-[32px] w-full rounded-md;
             @apply px-2 flex items-center justify-between relative h-[32px] w-full rounded-md;
 
 
             .page-title {
             .page-title {
-              @apply whitespace-nowrap hidden text-ellipsis flex-grow overflow-hidden pr-2;
+              @apply whitespace-nowrap text-ellipsis flex-grow overflow-hidden pr-2;
             }
             }
 
 
             .page-icon {
             .page-icon {

+ 1 - 1
src/main/frontend/components/editor.cljs

@@ -341,7 +341,7 @@
                      (let [page-entity (db/entity [:block/uuid page])
                      (let [page-entity (db/entity [:block/uuid page])
                            repo (state/sub :git/current-repo)
                            repo (state/sub :git/current-repo)
                            format (get page-entity :block/format :markdown)
                            format (get page-entity :block/format :markdown)
-                           block (db-model/query-block-by-uuid uuid)
+                           block (db-model/get-block-by-uuid uuid)
                            content (:block/title block)]
                            content (:block/title block)]
                        (when-not (string/blank? content)
                        (when-not (string/blank? content)
                          [:.py-2 (search/block-search-result-item repo uuid format content q :block)])))
                          [:.py-2 (search/block-search-result-item repo uuid format content q :block)])))

+ 1 - 2
src/main/frontend/components/left_sidebar.cljs

@@ -119,8 +119,7 @@
            (assoc :title (block-handler/block-unique-title page))))
            (assoc :title (block-handler/block-unique-title page))))
        [:span.page-icon {:key "page-icon"} icon]
        [:span.page-icon {:key "page-icon"} icon]
        [:span.page-title {:key "title"
        [:span.page-title {:key "title"
-                          :class (when untitled? "opacity-50")
-                          :style {:display "ruby"}}
+                          :class (when untitled? "opacity-50")}
         (cond
         (cond
           (not (db/page? page))
           (not (db/page? page))
           (block/inline-text :markdown (string/replace (apply str (take 64 (:block/title page))) "\n" " "))
           (block/inline-text :markdown (string/replace (apply str (take 64 (:block/title page))) "\n" " "))

+ 1 - 0
src/main/frontend/components/property.cljs

@@ -370,6 +370,7 @@
         property-key (rum/react *property-key)
         property-key (rum/react *property-key)
         batch? (pv/batch-operation?)
         batch? (pv/batch-operation?)
         hide-property-key? (or (contains? #{:date :datetime} (:logseq.property/type property))
         hide-property-key? (or (contains? #{:date :datetime} (:logseq.property/type property))
+                               (= (:db/ident property) :logseq.property/icon)
                                (pv/select-type? block property)
                                (pv/select-type? block property)
                                (and
                                (and
                                 batch?
                                 batch?

+ 0 - 3
src/main/frontend/components/property.css

@@ -363,9 +363,6 @@ a.control-link {
 .ls-property-dropdown {
 .ls-property-dropdown {
   @apply w-[260px] p-1.5;
   @apply w-[260px] p-1.5;
 
 
-  .ui__dropdown-menu-item, .ui__dropdown-menu-sub-trigger {
-  }
-
   [role=separator] {
   [role=separator] {
     @apply my-2;
     @apply my-2;
   }
   }

+ 26 - 36
src/main/frontend/components/property/value.cljs

@@ -27,7 +27,6 @@
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
             [frontend.util.cursor :as cursor]
-            [goog.dom :as gdom]
             [goog.functions :refer [debounce]]
             [goog.functions :refer [debounce]]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
             [logseq.common.util.macro :as macro-util]
             [logseq.common.util.macro :as macro-util]
@@ -54,9 +53,9 @@
                :else
                :else
                "Empty")]
                "Empty")]
     (if (= text "Empty")
     (if (= text "Empty")
-      (shui/button (merge {:class "empty-btn !text-base" :variant :text} opts)
+      (shui/button (merge {:class "empty-btn" :variant :text} opts)
                    text)
                    text)
-      (shui/button (merge {:class "empty-btn !text-base" :variant :text} opts)
+      (shui/button (merge {:class "empty-btn" :variant :text} opts)
                    text))))
                    text))))
 
 
 (rum/defc property-empty-text-value
 (rum/defc property-empty-text-value
@@ -95,6 +94,11 @@
 
 
 (rum/defc icon-row
 (rum/defc icon-row
   [block editing?]
   [block editing?]
+  (hooks/use-effect!
+   (fn []
+     (fn []
+       (when editing?
+         (editor-handler/restore-last-saved-cursor!)))))
   (let [icon-value (:logseq.property/icon block)
   (let [icon-value (:logseq.property/icon block)
         clear-overlay! (fn []
         clear-overlay! (fn []
                          (shui/popup-hide-all!))
                          (shui/popup-hide-all!))
@@ -108,36 +112,18 @@
                         (when icon (select-keys icon [:type :id :color]))))
                         (when icon (select-keys icon [:type :id :color]))))
                      (clear-overlay!)
                      (clear-overlay!)
                      (when editing?
                      (when editing?
-                       (editor-handler/restore-last-saved-cursor!)))]
-
-    (hooks/use-effect!
-     (fn []
-       (when editing?
-         (clear-overlay!)
-         (let [^js container (or (some-> js/document.activeElement (.closest ".page"))
-                                 (gdom/getElement "main-content-container"))
-               icon (get block :logseq.property/icon)]
-           (util/schedule
-            (fn []
-              (when-let [^js target (some-> (.querySelector container (str "#ls-block-" (str (:block/uuid block))))
-                                            (.querySelector ".block-main-container"))]
-                (state/set-editor-action! :property-icon-picker)
-                (shui/popup-show! target
-                                  #(icon-component/icon-search
-                                    {:on-chosen on-chosen!
-                                     :icon-value icon
-                                     :del-btn? (some? icon)})
-                                  {:id :ls-icon-picker
-                                   :on-after-hide #(state/set-editor-action! nil)
-                                   :content-props {:onEscapeKeyDown #(when editing? (editor-handler/restore-last-saved-cursor!))}
-                                   :align :start})))))))
-     [editing?])
-
-    [:div.col-span-3.flex.flex-row.items-center.gap-2
-     (icon-component/icon-picker icon-value
-                                 {:disabled? config/publishing?
-                                  :del-btn? (some? icon-value)
-                                  :on-chosen on-chosen!})]))
+                       (editor-handler/restore-last-saved-cursor!)))
+        icon (get block :logseq.property/icon)]
+    (if editing?
+      (icon-component/icon-search
+       {:on-chosen on-chosen!
+        :icon-value icon
+        :del-btn? (some? icon)})
+      [:div.col-span-3.flex.flex-row.items-center.gap-2
+       (icon-component/icon-picker icon-value
+                                   {:disabled? config/publishing?
+                                    :del-btn? (some? icon-value)
+                                    :on-chosen on-chosen!})])))
 
 
 (defn select-type?
 (defn select-type?
   [block property]
   [block property]
@@ -630,9 +616,13 @@
                          (not (and (ldb/class? block) (= (:db/ident property) :logseq.property.class/extends)))
                          (not (and (ldb/class? block) (= (:db/ident property) :logseq.property.class/extends)))
                          (not= (:db/ident property) :logseq.property.view/type))
                          (not= (:db/ident property) :logseq.property.view/type))
                   (concat sorted-items
                   (concat sorted-items
-                          [{:value clear-value
-                            :label clear-value-label
-                            :clear? true}])
+                          (when-not (or (= (:logseq.property/default-value property)
+                                           (get block (:db/ident property)))
+                                        (= (:logseq.property/scalar-default-value property)
+                                           (get block (:db/ident property))))
+                            [{:value clear-value
+                              :label clear-value-label
+                              :clear? true}]))
                   sorted-items)
                   sorted-items)
                 (remove #(= :logseq.property/empty-placeholder (:value %))))
                 (remove #(= :logseq.property/empty-placeholder (:value %))))
         k :on-chosen
         k :on-chosen

+ 4 - 0
src/main/frontend/components/property/value.css

@@ -45,3 +45,7 @@
   @apply border rounded pl-2;
   @apply border rounded pl-2;
   min-width: 3em;
   min-width: 3em;
 }
 }
+
+.ls-properties-area .empty-btn {
+  @apply !text-base;
+}

+ 6 - 6
src/main/frontend/components/reference.cljs

@@ -5,6 +5,7 @@
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
             [frontend.db-mixins :as db-mixins]
+            [frontend.db.async :as db-async]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [logseq.db.common.reference :as db-reference]
             [logseq.db.common.reference :as db-reference]
@@ -56,20 +57,19 @@
 (rum/defc references
 (rum/defc references
   [entity config]
   [entity config]
   (when-let [id (:db/id entity)]
   (when-let [id (:db/id entity)]
-    (let [[has-references? set-has-references!] (hooks/use-state nil)]
+    (let [[refs-total-count set-refs-total-count!] (hooks/use-state nil)]
       (hooks/use-effect!
       (hooks/use-effect!
        #(c.m/run-task*
        #(c.m/run-task*
          (m/sp
          (m/sp
-           (let [result (c.m/<? (state/<invoke-db-worker :thread-api/block-refs-check
-                                                         (state/get-current-repo) id {}))]
-             (set-has-references! result))))
+           (let [result (c.m/<? (db-async/<get-block-refs-count (state/get-current-repo) id))]
+             (set-refs-total-count! result))))
        [])
        [])
-      (when has-references?
+      (when (> refs-total-count 0)
         (ui/catch-error
         (ui/catch-error
          (ui/component-error (if (config/db-based-graph? (state/get-current-repo))
          (ui/component-error (if (config/db-based-graph? (state/get-current-repo))
                                "Linked References: Unexpected error."
                                "Linked References: Unexpected error."
                                "Linked References: Unexpected error. Please re-index your graph first."))
                                "Linked References: Unexpected error. Please re-index your graph first."))
-         (references-cp entity config))))))
+         (references-cp entity (assoc config :refs-total-count refs-total-count)))))))
 
 
 (rum/defc unlinked-references
 (rum/defc unlinked-references
   [entity config]
   [entity config]

+ 70 - 62
src/main/frontend/components/views.cljs

@@ -1861,67 +1861,73 @@
       (db/entity [:block/uuid (:block/uuid result)]))))
       (db/entity [:block/uuid (:block/uuid result)]))))
 
 
 (rum/defc views-tab < rum/reactive db-mixins/query
 (rum/defc views-tab < rum/reactive db-mixins/query
-  [view-parent current-view {:keys [views data items-count set-view-entity! set-data! set-views! view-feature-type show-items-count? references? opacity]}]
-  [:div.views
-   (for [view* views]
-     (let [view (db/sub-block (:db/id view*))
-           current-view? (= (:db/id current-view) (:db/id view))]
-       (shui/button
-        {:variant :text
-         :size :sm
-         :class (str "text-sm px-0 py-0 h-6 " (when-not current-view? "text-muted-foreground"))
-         :on-click (fn [e]
-                     (if (and current-view? (not= (:db/id view) (:db/id view-parent)))
-                       (shui/popup-show!
-                        (.-target e)
-                        (fn []
-                          [:<>
-                           (shui/dropdown-menu-sub
-                            (shui/dropdown-menu-sub-trigger
-                             "Rename")
-                            (shui/dropdown-menu-sub-content
-                             (when-let [block-container-cp (state/get-component :block/container)]
-                               (block-container-cp {} view))))
-                           (shui/dropdown-menu-item
-                            {:key "Delete"
-                             :on-click (fn []
-                                         (p/do!
-                                          (editor-handler/delete-block-aux! view)
-                                          (let [views' (remove (fn [v] (= (:db/id v) (:db/id view))) views)]
-                                            (set-views! views')
-                                            (set-view-entity! (first views'))
-                                            (shui/popup-hide!))))}
-                            "Delete")])
-                        {:as-dropdown? true
-                         :dropdown-menu? true
-                         :align "start"
-                         :content-props {:onClick shui/popup-hide!}})
-                       (do
-                         (set-view-entity! view)
-                         (set-data! nil))))}
-        (when-not references?
-          (let [display-type (or (:db/ident (get view :logseq.property.view/type))
-                                 :logseq.property.view/type.table)]
-            (when-let [icon (:logseq.property/icon (db/entity display-type))]
-              (icon-component/icon icon {:color? true
-                                         :size 15}))))
-        (let [title (:block/title view)]
-          (if (= title "")
-            "New view"
-            title))
-        (when (and current-view? show-items-count? (> items-count 0) (seq data))
-          [:span.text-muted-foreground.text-xs
-           items-count]))))
+  [view-parent current-view {:keys [views data items-count set-view-entity! set-data! set-views! view-feature-type show-items-count? config references? opacity]}]
+  (let [refs-total-count (:refs-total-count config)]
+    [:div.views
+     (for [view* views]
+       (let [view (db/sub-block (:db/id view*))
+             current-view? (= (:db/id current-view) (:db/id view))]
+         (shui/button
+          {:variant :text
+           :size :sm
+           :class (str "text-sm px-0 py-0 h-6 " (when-not current-view? "text-muted-foreground"))
+           :on-click (fn [e]
+                       (if (and current-view? (not= (:db/id view) (:db/id view-parent)))
+                         (shui/popup-show!
+                          (.-target e)
+                          (fn []
+                            [:<>
+                             (shui/dropdown-menu-sub
+                              (shui/dropdown-menu-sub-trigger
+                               "Rename")
+                              (shui/dropdown-menu-sub-content
+                               (when-let [block-container-cp (state/get-component :block/container)]
+                                 (block-container-cp {} view))))
+                             (shui/dropdown-menu-item
+                              {:key "Delete"
+                               :on-click (fn []
+                                           (p/do!
+                                            (editor-handler/delete-block-aux! view)
+                                            (let [views' (remove (fn [v] (= (:db/id v) (:db/id view))) views)]
+                                              (set-views! views')
+                                              (set-view-entity! (first views'))
+                                              (shui/popup-hide!))))}
+                              "Delete")])
+                          {:as-dropdown? true
+                           :dropdown-menu? true
+                           :align "start"
+                           :content-props {:onClick shui/popup-hide!}})
+                         (do
+                           (set-view-entity! view)
+                           (set-data! nil))))}
+          (when-not references?
+            (let [display-type (or (:db/ident (get view :logseq.property.view/type))
+                                   :logseq.property.view/type.table)]
+              (when-let [icon (:logseq.property/icon (db/entity display-type))]
+                (icon-component/icon icon {:color? true
+                                           :size 15}))))
+          (let [title (:block/title view)]
+            (if (= title "")
+              "New view"
+              title))
+          (when (and current-view? show-items-count? (> items-count 0) (seq data))
+            [:span.text-muted-foreground.text-xs
+             items-count
+             (when (and refs-total-count
+                        (> refs-total-count items-count))
+               [:span
+                [:span "/"]
+                [:span {:title "Total refs count"} refs-total-count]])]))))
 
 
-   (shui/button
-    {:variant :text
-     :size :sm
-     :title "Add new view"
-     :class (str "!px-1 -ml-1 text-muted-foreground hover:text-foreground transition-opacity ease-in duration-300 " opacity)
-     :on-click (fn []
-                 (p/let [view (create-view! view-parent view-feature-type {:auto-triggered? false})]
-                   (set-views! (concat views [view]))))}
-    (ui/icon "plus" {:size 15}))])
+     (shui/button
+      {:variant :text
+       :size :sm
+       :title "Add new view"
+       :class (str "!px-1 -ml-1 text-muted-foreground hover:text-foreground transition-opacity ease-in duration-300 " opacity)
+       :on-click (fn []
+                   (p/let [view (create-view! view-parent view-feature-type {:auto-triggered? false})]
+                     (set-views! (concat views [view]))))}
+      (ui/icon "plus" {:size 15}))]))
 
 
 (rum/defc view-head < rum/static
 (rum/defc view-head < rum/static
   [view-parent view-entity table columns input sorting
   [view-parent view-entity table columns input sorting
@@ -1935,9 +1941,11 @@
                   (and references? (not hover?)) "opacity-0"
                   (and references? (not hover?)) "opacity-0"
                   hover? "opacity-100"
                   hover? "opacity-100"
                   :else "opacity-75")]
                   :else "opacity-75")]
-    [:div.flex.flex-1.flex-nowrap.items-center.justify-between.gap-1.overflow-hidden
+    [:div.ls-view-head.flex.flex-1.flex-nowrap.items-center.justify-between.gap-1.overflow-hidden
      {:on-mouse-over #(set-hover? true)
      {:on-mouse-over #(set-hover? true)
-      :on-mouse-out #(set-hover? false)}
+      :on-mouse-out #(when-not (or (ui/popup-exists?)
+                                   (ui/dropdown-exists?))
+                       (set-hover? false))}
      [:div.flex.flex-row.items-center.gap-2
      [:div.flex.flex-row.items-center.gap-2
       (if db-based?
       (if db-based?
         (if (= view-feature-type :query-result)
         (if (= view-feature-type :query-result)

+ 1 - 4
src/main/frontend/db/async.cljs

@@ -174,10 +174,7 @@
 (defn <get-block-refs
 (defn <get-block-refs
   [graph eid]
   [graph eid]
   (assert (integer? eid))
   (assert (integer? eid))
-  (p/let [result (state/<invoke-db-worker :thread-api/get-block-refs graph eid)
-          conn (db/get-db graph false)
-          _ (d/transact! conn result)]
-    result))
+  (state/<invoke-db-worker :thread-api/get-block-refs graph eid))
 
 
 (defn <get-block-refs-count
 (defn <get-block-refs-count
   [graph eid]
   [graph eid]

+ 1 - 37
src/main/frontend/db/model.cljs

@@ -1,14 +1,12 @@
 (ns frontend.db.model
 (ns frontend.db.model
   "Core db functions."
   "Core db functions."
-  (:require [clojure.set :as set]
-            [clojure.string :as string]
+  (:require [clojure.string :as string]
             [clojure.walk :as walk]
             [clojure.walk :as walk]
             [datascript.core :as d]
             [datascript.core :as d]
             [frontend.common.graph-view :as graph-view]
             [frontend.common.graph-view :as graph-view]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db.conn :as conn]
             [frontend.db.conn :as conn]
-            [frontend.db.file-based.model :as file-model]
             [frontend.db.react :as react]
             [frontend.db.react :as react]
             [frontend.db.utils :as db-utils]
             [frontend.db.utils :as db-utils]
             [frontend.state :as state]
             [frontend.state :as state]
@@ -370,32 +368,6 @@ independent of format as format specific heading characters are stripped"
   (when-let [db (conn/get-db repo)]
   (when-let [db (conn/get-db repo)]
     (graph-view/get-pages-that-mentioned-page db page-id include-journals?)))
     (graph-view/get-pages-that-mentioned-page db page-id include-journals?)))
 
 
-(defn get-page-referenced-blocks-full
-  ([page-id]
-   (get-page-referenced-blocks-full (state/get-current-repo) page-id))
-  ([repo page-id]
-   (when (and repo page-id)
-     (when-let [db (conn/get-db repo)]
-       (let [pages (page-alias-set repo page-id)
-             aliases (set/difference pages #{page-id})]
-         (->>
-          (d/q
-           '[:find [(pull ?block ?block-attrs) ...]
-             :in $ [?ref-page ...] ?block-attrs
-             :where
-             [?r :block/name ?ref-page]
-             [?block :block/refs ?r]]
-           db
-           pages
-           (butlast file-model/file-graph-block-attrs))
-          (remove (fn [block] (= page-id (:db/id (:block/page block)))))
-          db-utils/group-by-page
-          (map (fn [[k blocks]]
-                 (let [k (if (contains? aliases (:db/id k))
-                           (assoc k :block/alias? true)
-                           k)]
-                   [k blocks])))))))))
-
 (defn get-referenced-blocks
 (defn get-referenced-blocks
   ([eid]
   ([eid]
    (get-referenced-blocks (state/get-current-repo) eid))
    (get-referenced-blocks (state/get-current-repo) eid))
@@ -416,14 +388,6 @@ independent of format as format specific heading characters are stripped"
                          (some? (get block (:db/ident entity))))))
                          (some? (get block (:db/ident entity))))))
               (util/distinct-by :db/id)))))))
               (util/distinct-by :db/id)))))))
 
 
-(defn get-block-referenced-blocks
-  [block-id]
-  (when-let [repo (state/get-current-repo)]
-    (when (conn/get-db repo)
-      (->> (get-referenced-blocks repo block-id)
-           (sort-by-order-recursive)
-           db-utils/group-by-page))))
-
 (defn journal-page?
 (defn journal-page?
   "sanitized page-name only"
   "sanitized page-name only"
   [page-name]
   [page-name]

+ 3 - 3
src/main/frontend/handler/db_based/import.cljs

@@ -7,6 +7,8 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.modules.outliner.op :as outliner-op]
+            [frontend.modules.outliner.ui :as ui-outliner-tx]
             [frontend.persist-db :as persist-db]
             [frontend.persist-db :as persist-db]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util :as util]
@@ -14,9 +16,7 @@
             [logseq.db.sqlite.export :as sqlite-export]
             [logseq.db.sqlite.export :as sqlite-export]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.shui.ui :as shui]
             [logseq.shui.ui :as shui]
-            [promesa.core :as p]
-            [frontend.modules.outliner.ui :as ui-outliner-tx]
-            [frontend.modules.outliner.op :as outliner-op]))
+            [promesa.core :as p]))
 
 
 (defn import-from-sqlite-db!
 (defn import-from-sqlite-db!
   [buffer bare-graph-name finished-ok-handler]
   [buffer bare-graph-name finished-ok-handler]

+ 22 - 14
src/main/frontend/handler/editor.cljs

@@ -556,7 +556,9 @@
                  (state/set-state! :editor/async-unsaved-chars nil))))))
                  (state/set-state! :editor/async-unsaved-chars nil))))))
 
 
 (defn api-insert-new-block!
 (defn api-insert-new-block!
-  [content {:keys [page block-uuid sibling? before? properties
+  [content {:keys [page block-uuid
+                   sibling? before? start? end?
+                   properties
                    custom-uuid replace-empty-target? edit-block? ordered-list? other-attrs]
                    custom-uuid replace-empty-target? edit-block? ordered-list? other-attrs]
             :or {sibling? false
             :or {sibling? false
                  before? false
                  before? false
@@ -598,6 +600,7 @@
                             (wrap-parse-block)
                             (wrap-parse-block)
                             (assoc :block/uuid (or custom-uuid (db/new-block-id))))
                             (assoc :block/uuid (or custom-uuid (db/new-block-id))))
               new-block (merge new-block other-attrs)
               new-block (merge new-block other-attrs)
+              block' (db/entity (:db/id block))
               [target-block sibling?] (cond
               [target-block sibling?] (cond
                                         before?
                                         before?
                                         (let [left-or-parent (or (ldb/get-left-sibling block)
                                         (let [left-or-parent (or (ldb/get-left-sibling block)
@@ -607,13 +610,21 @@
                                           [left-or-parent sibling?])
                                           [left-or-parent sibling?])
 
 
                                         sibling?
                                         sibling?
-                                        [(db/entity (:db/id block)) sibling?]
+                                        [block' sibling?]
+
+                                        start?
+                                        [block' false]
+
+                                        end?
+                                        (if last-block
+                                          [block' false]
+                                          [last-block true])
 
 
                                         last-block
                                         last-block
                                         [last-block true]
                                         [last-block true]
 
 
                                         block
                                         block
-                                        [(db/entity (:db/id block)) sibling?]
+                                        [block' sibling?]
 
 
                                         ;; FIXME: assert
                                         ;; FIXME: assert
                                         :else
                                         :else
@@ -2064,12 +2075,11 @@
                                               (when-not keep-uuid? [:id])
                                               (when-not keep-uuid? [:id])
                                               [:custom_id :custom-id]
                                               [:custom_id :custom-id]
                                               exclude-properties))
                                               exclude-properties))
-                    :block/format format)
-             (not db-based?)
-             (assoc :block/properties-text-values (apply dissoc (:block/properties-text-values block)
+                    :block/properties-text-values (apply dissoc (:block/properties-text-values block)
                                                          (concat
                                                          (concat
                                                           (when-not keep-uuid? [:id])
                                                           (when-not keep-uuid? [:id])
-                                                          exclude-properties)))))))
+                                                          exclude-properties))
+                    :block/format format)))))
 
 
 (defn- edit-last-block-after-inserted!
 (defn- edit-last-block-after-inserted!
   [result]
   [result]
@@ -2179,17 +2189,15 @@
    A block element: {:content :properties :children [block-1, block-2, ...]}"
    A block element: {:content :properties :children [block-1, block-2, ...]}"
   [tree-vec format {:keys [target-block keep-uuid?] :as opts}]
   [tree-vec format {:keys [target-block keep-uuid?] :as opts}]
   (let [repo (state/get-current-repo)
   (let [repo (state/get-current-repo)
-        page-id (:db/id (:block/page target-block))
+        page-id (or (:db/id (:block/page target-block))
+                    (when (ldb/page? target-block)
+                      (:db/id target-block)))
         page-name (some-> page-id (db/entity) :block/name)
         page-name (some-> page-id (db/entity) :block/name)
         blocks (block-tree->blocks repo tree-vec format keep-uuid? page-name)
         blocks (block-tree->blocks repo tree-vec format keep-uuid? page-name)
-        blocks (gp-block/with-parent-and-order page-id blocks)
-        block-refs (->> (mapcat :block/refs blocks)
-                        (set)
-                        (filter (fn [ref] (and (vector? ref) (= :block/uuid (first ref))))))]
+        blocks (gp-block/with-parent-and-order page-id blocks)]
+
     (ui-outliner-tx/transact!
     (ui-outliner-tx/transact!
      {:outliner-op :paste-blocks}
      {:outliner-op :paste-blocks}
-     (when (seq block-refs)
-       (db/transact! (map (fn [[_ id]] {:block/uuid id}) block-refs)))
      (paste-blocks blocks (merge opts {:ops-only? true})))))
      (paste-blocks blocks (merge opts {:ops-only? true})))))
 
 
 (defn insert-block-tree-after-target
 (defn insert-block-tree-after-target

+ 4 - 2
src/main/frontend/handler/events/ui.cljs

@@ -182,7 +182,7 @@
                   (shui/dialog-close!)
                   (shui/dialog-close!)
                   (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))
                   (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))
 
 
-(defn- editor-new-property [block target {:keys [selected-blocks] :as opts}]
+(defn- editor-new-property [block target {:keys [selected-blocks popup-id] :as opts}]
   (let [editing-block (state/get-edit-block)
   (let [editing-block (state/get-edit-block)
         pos (state/get-edit-pos)
         pos (state/get-edit-pos)
         edit-block-or-selected (cond
         edit-block-or-selected (cond
@@ -234,7 +234,9 @@
         (if target'
         (if target'
           (shui/popup-show! target'
           (shui/popup-show! target'
                             #(property-dialog/dialog blocks opts')
                             #(property-dialog/dialog blocks opts')
-                            {:align "start"})
+                            (cond-> {:align "start"}
+                              popup-id
+                              (assoc :id popup-id)))
           (shui/dialog-open! #(property-dialog/dialog blocks opts')
           (shui/dialog-open! #(property-dialog/dialog blocks opts')
                              {:id :property-dialog
                              {:id :property-dialog
                               :align "start"}))))))
                               :align "start"}))))))

+ 1 - 1
src/main/frontend/handler/whiteboard.cljs

@@ -149,7 +149,7 @@
        :deleted-shapes deleted-shapes
        :deleted-shapes deleted-shapes
        :new-shapes created-shapes
        :new-shapes created-shapes
        :metadata {:whiteboard/transact? true
        :metadata {:whiteboard/transact? true
-                  :pipeline-replace? replace?}})))
+                  :whiteboard/replace? replace?}})))
 
 
 (defonce *last-shapes-nonce (atom {}))
 (defonce *last-shapes-nonce (atom {}))
 
 

+ 1 - 2
src/main/frontend/modules/outliner/pipeline.cljs

@@ -28,8 +28,7 @@
   [{:keys [repo tx-meta tx-data deleted-block-uuids deleted-assets affected-keys blocks]}]
   [{:keys [repo tx-meta tx-data deleted-block-uuids deleted-assets affected-keys blocks]}]
   ;; (prn :debug
   ;; (prn :debug
   ;;      :tx-meta tx-meta
   ;;      :tx-meta tx-meta
-  ;;      ;; :tx-data tx-data
-  ;;      )
+  ;;      :tx-data tx-data)
   (let [{:keys [from-disk? new-graph? initial-pages? end?]} tx-meta
   (let [{:keys [from-disk? new-graph? initial-pages? end?]} tx-meta
         tx-report {:tx-meta tx-meta
         tx-report {:tx-meta tx-meta
                    :tx-data tx-data}]
                    :tx-data tx-data}]

+ 8 - 2
src/main/frontend/ui.cljs

@@ -54,7 +54,13 @@
 
 
 (defonce icon-size (if (mobile-util/native-platform?) 24 20))
 (defonce icon-size (if (mobile-util/native-platform?) 24 20))
 
 
-(defn shui-popups? [] (some-> (shui-popup/get-popups) (count) (> 0)))
+(defn popup-exists? []
+  (boolean (seq (shui-popup/get-popups))))
+
+(defn dropdown-exists?
+  []
+  (some? (js/document.querySelector "[data-radix-popper-content-wrapper]")))
+
 (defn last-shui-preview-popup?
 (defn last-shui-preview-popup?
   []
   []
   (= "ls-preview-popup"
   (= "ls-preview-popup"
@@ -63,7 +69,7 @@
   []
   []
   (if (util/mobile?)
   (if (util/mobile?)
     (shui/popup-hide!)
     (shui/popup-hide!)
-    (while (and (shui-popups?)
+    (while (and (popup-exists?)
                 (not (last-shui-preview-popup?)))
                 (not (last-shui-preview-popup?)))
       (shui/popup-hide!))))
       (shui/popup-hide!))))
 
 

+ 1 - 2
src/main/frontend/undo_redo.cljs

@@ -269,8 +269,7 @@
           (when (seq tx-data)
           (when (seq tx-data)
             (let [reversed-tx-data (get-reversed-datoms conn undo? data tx-meta)
             (let [reversed-tx-data (get-reversed-datoms conn undo? data tx-meta)
                   tx-meta' (-> tx-meta
                   tx-meta' (-> tx-meta
-                               (dissoc :pipeline-replace?
-                                       :batch-tx/batch-tx-mode?)
+                               (dissoc :batch-tx/batch-tx-mode?)
                                (assoc
                                (assoc
                                 :gen-undo-ops? false
                                 :gen-undo-ops? false
                                 :undo? undo?
                                 :undo? undo?

+ 32 - 31
src/main/frontend/worker/commands.cljs

@@ -149,15 +149,16 @@
         (tc/to-long next-time)))))
         (tc/to-long next-time)))))
 
 
 (defn- compute-reschedule-property-tx
 (defn- compute-reschedule-property-tx
-  [conn db entity property-ident]
-  (let [frequency (or (db-property/property-value-content (:logseq.property.repeat/recur-frequency entity))
-                      (let [property (d/entity db :logseq.property.repeat/recur-frequency)
-                            default-value-block (db-property-build/build-property-value-block property property 1)
-                            default-value-tx-data [default-value-block
-                                                   {:db/id (:db/id property)
-                                                    :logseq.property/default-value [:block/uuid (:block/uuid default-value-block)]}]]
-                        (d/transact! conn default-value-tx-data)
-                        1))
+  [db entity property-ident]
+  (let [[frequency default-value-tx-data]
+        (or [(db-property/property-value-content (:logseq.property.repeat/recur-frequency entity))
+             nil]
+            (let [property (d/entity db :logseq.property.repeat/recur-frequency)
+                  default-value-block (db-property-build/build-property-value-block property property 1)
+                  default-value-tx-data [default-value-block
+                                         {:db/id (:db/id property)
+                                          :logseq.property/default-value [:block/uuid (:block/uuid default-value-block)]}]]
+              [1 default-value-tx-data]))
         unit (:logseq.property.repeat/recur-unit entity)
         unit (:logseq.property.repeat/recur-unit entity)
         property (d/entity db property-ident)
         property (d/entity db property-ident)
         date? (= :date (:logseq.property/type property))
         date? (= :date (:logseq.property/type property))
@@ -175,11 +176,12 @@
                                               (outliner-page/create db title {})))
                                               (outliner-page/create db title {})))
               value (if date? [:block/uuid page-uuid] next-time-long)]
               value (if date? [:block/uuid page-uuid] next-time-long)]
           (concat
           (concat
+           default-value-tx-data
            tx-data
            tx-data
            (when value
            (when value
              [[:db/add (:db/id entity) property-ident value]])))))))
              [[:db/add (:db/id entity) property-ident value]])))))))
 
 
-(defmethod handle-command :reschedule [_ conn db entity _datoms]
+(defmethod handle-command :reschedule [_ db entity _datoms]
   (let [property-ident (or (:db/ident (:logseq.property.repeat/temporal-property entity))
   (let [property-ident (or (:db/ident (:logseq.property.repeat/temporal-property entity))
                            :logseq.property/scheduled)
                            :logseq.property/scheduled)
         other-property-idents (cond
         other-property-idents (cond
@@ -193,14 +195,14 @@
 
 
                                 :else
                                 :else
                                 (filter (fn [p] (get entity p)) [:logseq.property/deadline :logseq.property/scheduled]))]
                                 (filter (fn [p] (get entity p)) [:logseq.property/deadline :logseq.property/scheduled]))]
-    (mapcat #(compute-reschedule-property-tx conn db entity %) (distinct (cons property-ident other-property-idents)))))
+    (mapcat #(compute-reschedule-property-tx db entity %) (distinct (cons property-ident other-property-idents)))))
 
 
-(defmethod handle-command :set-property [_ _db _conn entity _datoms property value]
+(defmethod handle-command :set-property [_ _db entity _datoms property value]
   (let [property' (get-property entity property)
   (let [property' (get-property entity property)
         value' (get-value entity property value)]
         value' (get-value entity property value)]
     [[:db/add (:db/id entity) property' value']]))
     [[:db/add (:db/id entity) property' value']]))
 
 
-(defmethod handle-command :record-property-history [_ _conn db entity datoms]
+(defmethod handle-command :record-property-history [_ db entity datoms]
   (let [changes (keep (fn [d]
   (let [changes (keep (fn [d]
                         (let [property (d/entity db (:a d))]
                         (let [property (d/entity db (:a d))]
                           (when (and (true? (get property :logseq.property/enable-history?))
                           (when (and (true? (get property :logseq.property/enable-history?))
@@ -218,7 +220,7 @@
            :logseq.property.history/property (:db/id property)})))
            :logseq.property.history/property (:db/id property)})))
      changes)))
      changes)))
 
 
-(defmethod handle-command :default [command _conn _db entity datoms]
+(defmethod handle-command :default [command _db entity datoms]
   (throw (ex-info "Unhandled command"
   (throw (ex-info "Unhandled command"
                   {:command command
                   {:command command
                    :entity entity
                    :entity entity
@@ -226,23 +228,22 @@
 
 
 (defn execute-command
 (defn execute-command
   "Build tx-data"
   "Build tx-data"
-  [conn db entity datoms [_command {:keys [actions]}]]
+  [db entity datoms [_command {:keys [actions]}]]
   (mapcat (fn [action]
   (mapcat (fn [action]
-            (apply handle-command (first action) conn db entity datoms (rest action))) actions))
+            (apply handle-command (first action) db entity datoms (rest action))) actions))
 
 
 (defn run-commands
 (defn run-commands
-  [conn {:keys [tx-data db-after]}]
-  (let [db db-after]
-    (mapcat (fn [[e datoms]]
-              (let [entity (d/entity db e)
-                    commands (filter (fn [[_command {:keys [entity-conditions tx-conditions]}]]
-                                       (and
-                                        (if (seq entity-conditions)
-                                          (every? #(satisfy-condition? db entity % nil) entity-conditions)
-                                          true)
-                                        (every? #(satisfy-condition? db entity % datoms) tx-conditions))) @*commands)]
-                (mapcat
-                 (fn [command]
-                   (execute-command conn db entity datoms command))
-                 commands)))
-            (group-by :e tx-data))))
+  [{:keys [tx-data db-after]}]
+  (mapcat (fn [[e datoms]]
+            (let [entity (d/entity db-after e)
+                  commands (filter (fn [[_command {:keys [entity-conditions tx-conditions]}]]
+                                     (and
+                                      (if (seq entity-conditions)
+                                        (every? #(satisfy-condition? db-after entity % nil) entity-conditions)
+                                        true)
+                                      (every? #(satisfy-condition? db-after entity % datoms) tx-conditions))) @*commands)]
+              (mapcat
+               (fn [command]
+                 (execute-command db-after entity datoms command))
+               commands)))
+          (group-by :e tx-data)))

+ 2 - 1
src/main/frontend/worker/crypt.cljs

@@ -3,6 +3,7 @@
   (:require [datascript.core :as d]
   (:require [datascript.core :as d]
             [frontend.common.thread-api :refer [def-thread-api]]
             [frontend.common.thread-api :refer [def-thread-api]]
             [frontend.worker.state :as worker-state]
             [frontend.worker.state :as worker-state]
+            [logseq.db :as ldb]
             [promesa.core :as p]))
             [promesa.core :as p]))
 
 
 (defonce ^:private encoder (new js/TextEncoder "utf-8"))
 (defonce ^:private encoder (new js/TextEncoder "utf-8"))
@@ -115,7 +116,7 @@
     (assert (some? conn) repo)
     (assert (some? conn) repo)
     (let [aes-key-datom (first (d/datoms @conn :avet :aes-key-jwk))]
     (let [aes-key-datom (first (d/datoms @conn :avet :aes-key-jwk))]
       (assert (nil? aes-key-datom) aes-key-datom)
       (assert (nil? aes-key-datom) aes-key-datom)
-      (d/transact! conn [[:db/add "e1" :aes-key-jwk aes-key-jwk]]))))
+      (ldb/transact! conn [[:db/add "e1" :aes-key-jwk aes-key-jwk]]))))
 
 
 (defn get-graph-keys-jwk
 (defn get-graph-keys-jwk
   [repo]
   [repo]

+ 29 - 1
src/main/frontend/worker/db/validate.cljs

@@ -1,6 +1,7 @@
 (ns frontend.worker.db.validate
 (ns frontend.worker.db.validate
   "Validate db"
   "Validate db"
-  (:require [datascript.core :as d]
+  (:require [clojure.string :as string]
+            [datascript.core :as d]
             [frontend.worker.db.migrate :as db-migrate]
             [frontend.worker.db.migrate :as db-migrate]
             [frontend.worker.shared-service :as shared-service]
             [frontend.worker.shared-service :as shared-service]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
@@ -19,6 +20,8 @@
                      (fn [{:keys [entity dispatch-key]}]
                      (fn [{:keys [entity dispatch-key]}]
                        (let [entity (d/entity db (:db/id entity))]
                        (let [entity (d/entity db (:db/id entity))]
                          (cond
                          (cond
+                           (and (:block/tx-id entity) (nil? (:block/title entity)))
+                           [[:db/retractEntity (:db/id entity)]]
                            (= :block/path-refs (:db/ident entity))
                            (= :block/path-refs (:db/ident entity))
                            (concat [[:db/retractEntity (:db/id entity)]]
                            (concat [[:db/retractEntity (:db/id entity)]]
                                    (try
                                    (try
@@ -96,11 +99,36 @@
     (when (seq tx-data)
     (when (seq tx-data)
       (d/transact! conn tx-data {:fix-db? true}))))
       (d/transact! conn tx-data {:fix-db? true}))))
 
 
+(defn- fix-num-prefix-db-idents!
+  "Fix invalid db/ident keywords for both classes and properties"
+  [conn]
+  (let [db @conn
+        tx-data (->> (d/datoms db :avet :db/ident)
+                     (filter (fn [d] (re-find #"^(\d)" (name (:v d)))))
+                     (mapcat (fn [d]
+                               (let [new-db-ident (keyword (namespace (:v d)) (string/replace-first (name (:v d)) #"^(\d)" "NUM-$1"))
+                                     property (ldb/property? (d/entity db (:v d)))]
+                                 (concat
+                                  [[:db/add (:e d) :db/ident new-db-ident]]
+                                  (when property
+                                    (->> (d/datoms db :avet (:v d))
+                                         (mapcat (fn [d]
+                                                   [[:db/retract (:e d) (:a d) (:v d)]
+                                                    [:db/add (:e d) new-db-ident (:v d)]])))))))))]
+    (when (seq tx-data)
+      (ldb/transact! conn tx-data))))
+
 (defn validate-db
 (defn validate-db
   [conn]
   [conn]
+  (fix-num-prefix-db-idents! conn)
+
   (let [db @conn
   (let [db @conn
         {:keys [errors datom-count entities]} (db-validate/validate-db! db)
         {:keys [errors datom-count entities]} (db-validate/validate-db! db)
         invalid-entity-ids (distinct (map (fn [e] (:db/id (:entity e))) errors))]
         invalid-entity-ids (distinct (map (fn [e] (:db/id (:entity e))) errors))]
+
+    (doseq [id invalid-entity-ids]
+      (prn :debug :id id :entity (into {} (d/entity db id))))
+
     (if errors
     (if errors
       (do
       (do
         (fix-invalid-blocks! conn errors)
         (fix-invalid-blocks! conn errors)

+ 28 - 32
src/main/frontend/worker/db_listener.cljs

@@ -35,7 +35,7 @@
 
 
         (when-not from-disk?
         (when-not from-disk?
           (p/do!
           (p/do!
-         ;; Sync SQLite search
+           ;; Sync SQLite search
            (let [{:keys [blocks-to-remove-set blocks-to-add]} (search/sync-search-indice repo tx-report')]
            (let [{:keys [blocks-to-remove-set blocks-to-add]} (search/sync-search-indice repo tx-report')]
              (when (seq blocks-to-remove-set)
              (when (seq blocks-to-remove-set)
                ((@thread-api/*thread-apis :thread-api/search-delete-blocks) repo blocks-to-remove-set))
                ((@thread-api/*thread-apis :thread-api/search-delete-blocks) repo blocks-to-remove-set))
@@ -65,9 +65,7 @@
                                       (map (fn [id] [:db/add id :logseq.property.embedding/hnsw-label-updated-at 0])))
                                       (map (fn [id] [:db/add id :logseq.property.embedding/hnsw-label-updated-at 0])))
           tx-data (concat remove-old-hnsw-tx-data mark-embedding-tx-data)]
           tx-data (concat remove-old-hnsw-tx-data mark-embedding-tx-data)]
       (when (seq tx-data)
       (when (seq tx-data)
-        (d/transact! conn tx-data
-                     {:skip-refresh? true
-                      :pipeline-replace? true})))))
+        (ldb/transact! conn tx-data {})))))
 
 
 (defn listen-db-changes!
 (defn listen-db-changes!
   [repo conn & {:keys [handler-keys]}]
   [repo conn & {:keys [handler-keys]}]
@@ -90,39 +88,37 @@
                    (remove-old-embeddings-and-reset-new-updates! conn tx-data tx-meta)
                    (remove-old-embeddings-and-reset-new-updates! conn tx-data tx-meta)
 
 
                    (let [tx-meta (merge (batch-tx/get-batch-opts) tx-meta)
                    (let [tx-meta (merge (batch-tx/get-batch-opts) tx-meta)
-                         pipeline-replace? (:pipeline-replace? tx-meta)
                          in-batch-tx-mode? (:batch-tx/batch-tx-mode? tx-meta)]
                          in-batch-tx-mode? (:batch-tx/batch-tx-mode? tx-meta)]
-                     (when-not pipeline-replace?
-                       (when in-batch-tx-mode?
-                         (batch-tx/set-batch-opts (dissoc tx-meta :pipeline-replace?)))
-                       (cond
-                         (and in-batch-tx-mode?
-                              (not (:batch-tx/exit? tx-meta)))
+                     (when in-batch-tx-mode?
+                       (batch-tx/set-batch-opts tx-meta))
+                     (cond
+                       (and in-batch-tx-mode?
+                            (not (:batch-tx/exit? tx-meta)))
                          ;; still in batch mode
                          ;; still in batch mode
-                         (vswap! *batch-all-txs into tx-data)
+                       (vswap! *batch-all-txs into tx-data)
 
 
-                         in-batch-tx-mode?
+                       in-batch-tx-mode?
                          ;; exit batch mode
                          ;; exit batch mode
-                         (when-let [tx-data (not-empty (get-batch-txs))]
-                           (vreset! *batch-all-txs [])
-                           (let [db-before (batch-tx/get-batch-db-before)
-                                 tx-meta (dissoc tx-meta :batch-tx/batch-tx-mode? :batch-tx/exit?)
-                                 tx-report (assoc tx-report
-                                                  :tx-data tx-data
-                                                  :db-before db-before
-                                                  :tx-meta tx-meta)
-                                 tx-report' (if sync-db-to-main-thread?
-                                              (sync-db-to-main-thread repo conn tx-report)
-                                              tx-report)
-                                 opt (assoc (additional-args (:tx-data tx-report')) :repo repo)]
-                             (doseq [[k handler-fn] handlers]
-                               (handler-fn k opt tx-report'))))
-
-                         (seq tx-data)
-                         ;; raw transact
-                         (let [tx-report' (if sync-db-to-main-thread?
+                       (when-let [tx-data (not-empty (get-batch-txs))]
+                         (vreset! *batch-all-txs [])
+                         (let [db-before (batch-tx/get-batch-db-before)
+                               tx-meta (dissoc tx-meta :batch-tx/batch-tx-mode? :batch-tx/exit?)
+                               tx-report (assoc tx-report
+                                                :tx-data tx-data
+                                                :db-before db-before
+                                                :tx-meta tx-meta)
+                               tx-report' (if sync-db-to-main-thread?
                                             (sync-db-to-main-thread repo conn tx-report)
                                             (sync-db-to-main-thread repo conn tx-report)
                                             tx-report)
                                             tx-report)
                                opt (assoc (additional-args (:tx-data tx-report')) :repo repo)]
                                opt (assoc (additional-args (:tx-data tx-report')) :repo repo)]
                            (doseq [[k handler-fn] handlers]
                            (doseq [[k handler-fn] handlers]
-                             (handler-fn k opt tx-report')))))))))))
+                             (handler-fn k opt tx-report'))))
+
+                       (seq tx-data)
+                         ;; raw transact
+                       (let [tx-report' (if sync-db-to-main-thread?
+                                          (sync-db-to-main-thread repo conn tx-report)
+                                          tx-report)
+                             opt (assoc (additional-args (:tx-data tx-report')) :repo repo)]
+                         (doseq [[k handler-fn] handlers]
+                           (handler-fn k opt tx-report'))))))))))

+ 32 - 5
src/main/frontend/worker/db_worker.cljs

@@ -25,6 +25,7 @@
             [frontend.worker.file.reset :as file-reset]
             [frontend.worker.file.reset :as file-reset]
             [frontend.worker.handler.page :as worker-page]
             [frontend.worker.handler.page :as worker-page]
             [frontend.worker.handler.page.file-based.rename :as file-worker-page-rename]
             [frontend.worker.handler.page.file-based.rename :as file-worker-page-rename]
+            [frontend.worker.pipeline :as worker-pipeline]
             [frontend.worker.rtc.asset-db-listener]
             [frontend.worker.rtc.asset-db-listener]
             [frontend.worker.rtc.client-op :as client-op]
             [frontend.worker.rtc.client-op :as client-op]
             [frontend.worker.rtc.core :as rtc.core]
             [frontend.worker.rtc.core :as rtc.core]
@@ -41,10 +42,13 @@
             [logseq.common.util :as common-util]
             [logseq.common.util :as common-util]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
             [logseq.db.common.entity-plus :as entity-plus]
             [logseq.db.common.entity-plus :as entity-plus]
+            [logseq.db.common.entity-util :as common-entity-util]
             [logseq.db.common.initial-data :as common-initial-data]
             [logseq.db.common.initial-data :as common-initial-data]
             [logseq.db.common.order :as db-order]
             [logseq.db.common.order :as db-order]
+            [logseq.db.common.reference :as db-reference]
             [logseq.db.common.sqlite :as common-sqlite]
             [logseq.db.common.sqlite :as common-sqlite]
             [logseq.db.common.view :as db-view]
             [logseq.db.common.view :as db-view]
+            [logseq.db.frontend.class :as db-class]
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.sqlite.export :as sqlite-export]
             [logseq.db.sqlite.export :as sqlite-export]
@@ -253,8 +257,8 @@
       (doseq [db (if @*publishing? [sqlite-db] [sqlite-db client-ops-db])]
       (doseq [db (if @*publishing? [sqlite-db] [sqlite-db client-ops-db])]
         (sqlite-gc/gc-kvs-table! db {:full-gc? full-gc?})
         (sqlite-gc/gc-kvs-table! db {:full-gc? full-gc?})
         (.exec db "VACUUM"))
         (.exec db "VACUUM"))
-      (d/transact! datascript-conn [{:db/ident :logseq.kv/graph-last-gc-at
-                                     :kv/value (common-util/time-ms)}]))))
+      (ldb/transact! datascript-conn [{:db/ident :logseq.kv/graph-last-gc-at
+                                       :kv/value (common-util/time-ms)}]))))
 
 
 (defn- create-or-open-db!
 (defn- create-or-open-db!
   [repo {:keys [config datoms] :as opts}]
   [repo {:keys [config datoms] :as opts}]
@@ -272,6 +276,9 @@
       (common-sqlite/create-kvs-table! db)
       (common-sqlite/create-kvs-table! db)
       (when-not @*publishing? (common-sqlite/create-kvs-table! client-ops-db))
       (when-not @*publishing? (common-sqlite/create-kvs-table! client-ops-db))
       (search/create-tables-and-triggers! search-db)
       (search/create-tables-and-triggers! search-db)
+      (ldb/register-transact-pipeline-fn!
+       (fn [tx-report]
+         (worker-pipeline/transact-pipeline repo tx-report)))
       (let [schema (ldb/get-schema repo)
       (let [schema (ldb/get-schema repo)
             conn (common-sqlite/get-storage-conn storage schema)
             conn (common-sqlite/get-storage-conn storage schema)
             _ (db-fix/check-and-fix-schema! repo conn)
             _ (db-fix/check-and-fix-schema! repo conn)
@@ -293,7 +300,7 @@
           (let [config (or config "")
           (let [config (or config "")
                 initial-data (sqlite-create-graph/build-db-initial-data
                 initial-data (sqlite-create-graph/build-db-initial-data
                               config (select-keys opts [:import-type :graph-git-sha]))]
                               config (select-keys opts [:import-type :graph-git-sha]))]
-            (d/transact! conn initial-data {:initial-db? true})))
+            (ldb/transact! conn initial-data {:initial-db? true})))
 
 
         (gc-sqlite-dbs! db client-ops-db conn {})
         (gc-sqlite-dbs! db client-ops-db conn {})
 
 
@@ -462,7 +469,9 @@
 (def-thread-api :thread-api/get-block-refs
 (def-thread-api :thread-api/get-block-refs
   [repo id]
   [repo id]
   (when-let [conn (worker-state/get-datascript-conn repo)]
   (when-let [conn (worker-state/get-datascript-conn repo)]
-    (ldb/get-block-refs @conn id)))
+    (->> (db-reference/get-linked-references @conn id)
+         :ref-blocks
+         (map (fn [b] (assoc (into {} b) :db/id (:db/id b)))))))
 
 
 (def-thread-api :thread-api/get-block-refs-count
 (def-thread-api :thread-api/get-block-refs-count
   [repo id]
   [repo id]
@@ -628,7 +637,9 @@
               {:keys [type payload]} (when (map? data) data)]
               {:keys [type payload]} (when (map? data) data)]
           (case type
           (case type
             :notification
             :notification
-            (shared-service/broadcast-to-clients! :notification [(:message payload) (:type payload)])
+            (do
+              (log/error ::apply-outliner-ops-failed e)
+              (shared-service/broadcast-to-clients! :notification [(:message payload) (:type payload)]))
             (throw e)))))))
             (throw e)))))))
 
 
 (def-thread-api :thread-api/file-writes-finished?
 (def-thread-api :thread-api/file-writes-finished?
@@ -698,6 +709,12 @@
   (let [db @(worker-state/get-datascript-conn repo)]
   (let [db @(worker-state/get-datascript-conn repo)]
     (db-view/get-view-data db view-id option)))
     (db-view/get-view-data db view-id option)))
 
 
+(def-thread-api :thread-api/get-class-objects
+  [repo class-id]
+  (let [db @(worker-state/get-datascript-conn repo)]
+    (->> (db-class/get-class-objects db class-id)
+         (map common-entity-util/entity->map))))
+
 (def-thread-api :thread-api/get-property-values
 (def-thread-api :thread-api/get-property-values
   [repo {:keys [property-ident] :as option}]
   [repo {:keys [property-ident] :as option}]
   (let [conn (worker-state/get-datascript-conn repo)]
   (let [conn (worker-state/get-datascript-conn repo)]
@@ -896,9 +913,19 @@
           (reset! *service [graph service])
           (reset! *service [graph service])
           service)))))
           service)))))
 
 
+(defn- notify-invalid-data
+  [{:keys [tx-meta]}]
+  ;; don't notify on production when undo/redo failed
+  (when-not (and (or (:undo? tx-meta) (:redo? tx-meta))
+                 (not worker-util/dev?))
+    (shared-service/broadcast-to-clients! :notification
+                                          [["Invalid DB!"] :error])))
+
 (defn init
 (defn init
   "web worker entry"
   "web worker entry"
   []
   []
+  (ldb/register-transact-invalid-callback-fn! notify-invalid-data)
+
   (let [proxy-object (->>
   (let [proxy-object (->>
                       fns
                       fns
                       (map
                       (map

+ 3 - 3
src/main/frontend/worker/embedding.cljs

@@ -179,7 +179,7 @@
                         (into-array (map :db/id stale-block-chunk))
                         (into-array (map :db/id stale-block-chunk))
                         false))
                         false))
                     tx-data (labels-update-tx-data @conn e+updated-at-coll)]
                     tx-data (labels-update-tx-data @conn e+updated-at-coll)]
-                (d/transact! conn tx-data {:skip-refresh? true})
+                (ldb/transact! conn tx-data {:skip-refresh? true})
                 (m/? (task--update-index-info!* repo infer-worker true))
                 (m/? (task--update-index-info!* repo infer-worker true))
                 (c.m/<? (.write-index! infer-worker repo))))
                 (c.m/<? (.write-index! infer-worker repo))))
             (m/? (task--update-index-info!* repo infer-worker false))))))))
             (m/? (task--update-index-info!* repo infer-worker false))))))))
@@ -207,7 +207,7 @@
                                         (d/datoms @conn :avet :block/title)
                                         (d/datoms @conn :avet :block/title)
                                         (map (fn [d]
                                         (map (fn [d]
                                                [:db/add (:e d) :logseq.property.embedding/hnsw-label-updated-at 0])))]
                                                [:db/add (:e d) :logseq.property.embedding/hnsw-label-updated-at 0])))]
-            (d/transact! conn mark-embedding-tx-data {:skip-refresh? true})))
+            (ldb/transact! conn mark-embedding-tx-data {:skip-refresh? true})))
 
 
         (embedding-stale-blocks! repo reset-embedding?)))))
         (embedding-stale-blocks! repo reset-embedding?)))))
 
 
@@ -236,7 +236,7 @@
     (when-let [^js infer-worker @worker-state/*infer-worker]
     (when-let [^js infer-worker @worker-state/*infer-worker]
       (let [conn (worker-state/get-datascript-conn repo)]
       (let [conn (worker-state/get-datascript-conn repo)]
         (when (c.m/<? (.load-model infer-worker model-name))
         (when (c.m/<? (.load-model infer-worker model-name))
-          (d/transact! conn [(ldb/kv :logseq.kv/graph-text-embedding-model-name model-name)])
+          (ldb/transact! conn [(ldb/kv :logseq.kv/graph-text-embedding-model-name model-name)])
           (log/info :loaded-model model-name))))))
           (log/info :loaded-model model-name))))))
 
 
 (defn task--search
 (defn task--search

+ 63 - 142
src/main/frontend/worker/pipeline.cljs

@@ -6,19 +6,16 @@
             [frontend.worker.commands :as commands]
             [frontend.worker.commands :as commands]
             [frontend.worker.file :as file]
             [frontend.worker.file :as file]
             [frontend.worker.react :as worker-react]
             [frontend.worker.react :as worker-react]
-            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
             [frontend.worker.state :as worker-state]
-            [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.common.util :as common-util]
             [logseq.common.util :as common-util]
             [logseq.common.util.page-ref :as page-ref]
             [logseq.common.util.page-ref :as page-ref]
             [logseq.common.uuid :as common-uuid]
             [logseq.common.uuid :as common-uuid]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
+            [logseq.db.common.entity-plus :as entity-plus]
             [logseq.db.common.order :as db-order]
             [logseq.db.common.order :as db-order]
             [logseq.db.common.sqlite :as common-sqlite]
             [logseq.db.common.sqlite :as common-sqlite]
             [logseq.db.frontend.class :as db-class]
             [logseq.db.frontend.class :as db-class]
-            [logseq.db.frontend.validate :as db-validate]
             [logseq.db.sqlite.export :as sqlite-export]
             [logseq.db.sqlite.export :as sqlite-export]
-            [logseq.db.sqlite.util :as sqlite-util]
             [logseq.graph-parser.exporter :as gp-exporter]
             [logseq.graph-parser.exporter :as gp-exporter]
             [logseq.outliner.core :as outliner-core]
             [logseq.outliner.core :as outliner-core]
             [logseq.outliner.datascript-report :as ds-report]
             [logseq.outliner.datascript-report :as ds-report]
@@ -97,44 +94,6 @@
                                          (:tx-data result)))))))]
                                          (:tx-data result)))))))]
     tx-data))
     tx-data))
 
 
-(defkeywords
-  ::skip-validate-db? {:doc "tx-meta option, default = false"}
-  ::skip-store-conn {:doc "tx-meta option, skip `d/store` on conn. default = false"})
-
-(defn validate-db!
-  "Validate db is slow, we probably don't want to enable it for production."
-  [repo conn tx-report tx-meta context]
-  (when (and (not (::skip-validate-db? tx-meta false))
-             (or (:dev? context) (:undo? tx-meta) (:redo? tx-meta))
-             (not (:importing? context)) (sqlite-util/db-based-graph? repo))
-    (let [valid? (if (get-in tx-report [:tx-meta :reset-conn!])
-                   true
-                   (db-validate/validate-tx-report! tx-report (:validate-db-options context)))]
-      (when-not valid?
-        (when (and (or (get-in context [:validate-db-options :fail-invalid?]) worker-util/dev?)
-                   ;; don't notify on production when undo/redo failed
-                   (not (and (not (:dev? context)) (or (:undo? tx-meta) (:redo? tx-meta)))))
-          (shared-service/broadcast-to-clients! :notification
-                                                [["Invalid DB!"] :error]))
-        (throw (ex-info "Invalid data" {:graph repo})))))
-
-  ;; Ensure :block/order is unique for any block that has :block/parent
-  (when false;; (:dev? context)
-    (let [order-datoms (filter (fn [d] (= :block/order (:a d)))
-                               (:tx-data tx-report))]
-      (doseq [datom order-datoms]
-        (let [entity (d/entity @conn (:e datom))
-              parent (:block/parent entity)]
-          (when parent
-            (let [children (:block/_parent parent)
-                  order-different? (= (count (distinct (map :block/order children))) (count children))]
-              (when-not order-different?
-                (throw (ex-info (str ":block/order is not unique for children blocks, parent id: " (:db/id parent))
-                                {:children (->> (map (fn [b] (select-keys b [:db/id :block/title :block/order])) children)
-                                                (sort-by :block/order))
-                                 :tx-meta tx-meta
-                                 :tx-data (:tx-data tx-report)}))))))))))
-
 (defn- fix-page-tags
 (defn- fix-page-tags
   "Add missing attributes and remove #Page when inserting or updating block/title with inline tags"
   "Add missing attributes and remove #Page when inserting or updating block/title with inline tags"
   [{:keys [db-after tx-data tx-meta]}]
   [{:keys [db-after tx-data tx-meta]}]
@@ -158,7 +117,7 @@
                    [:db/add eid :logseq.property.class/extends :logseq.class/Root]
                    [:db/add eid :logseq.property.class/extends :logseq.class/Root]
                    [:db/retract eid :block/tags :logseq.class/Page]])))
                    [:db/retract eid :block/tags :logseq.class/Page]])))
 
 
-            ;; remove #Page from tags/journals/whitebaords, etc.
+            ;; remove #Page from tags/journals/whiteboards, etc.
             (and (= :block/tags (:a datom))
             (and (= :block/tags (:a datom))
                  (:added datom)
                  (:added datom)
                  (= (:db/id page-tag) (:v datom)))
                  (= (:db/id page-tag) (:v datom)))
@@ -210,9 +169,9 @@
        (apply concat)))))
        (apply concat)))))
 
 
 (defn- toggle-page-and-block
 (defn- toggle-page-and-block
-  [conn {:keys [db-before db-after tx-data tx-meta]}]
+  [db {:keys [db-before db-after tx-data tx-meta]}]
   (when-not (rtc-tx-or-download-graph? tx-meta)
   (when-not (rtc-tx-or-download-graph? tx-meta)
-    (let [page-tag (d/entity @conn :logseq.class/Page)
+    (let [page-tag (d/entity db :logseq.class/Page)
           library-page (ldb/get-library-page db-after)]
           library-page (ldb/get-library-page db-after)]
       (mapcat
       (mapcat
        (fn [datom]
        (fn [datom]
@@ -361,15 +320,16 @@
           (nil? created-by-ent) (cons created-by-block))))))
           (nil? created-by-ent) (cons created-by-block))))))
 
 
 (defn- compute-extra-tx-data
 (defn- compute-extra-tx-data
-  [repo conn tx-report]
+  [repo tx-report]
   (let [{:keys [db-before db-after tx-data tx-meta]} tx-report
   (let [{:keys [db-before db-after tx-data tx-meta]} tx-report
+        db db-after
         fix-page-tags-tx-data (fix-page-tags tx-report)
         fix-page-tags-tx-data (fix-page-tags tx-report)
         fix-inline-page-tx-data (fix-inline-built-in-page-classes tx-report)
         fix-inline-page-tx-data (fix-inline-built-in-page-classes tx-report)
         toggle-page-and-block-tx-data (when (empty? fix-inline-page-tx-data)
         toggle-page-and-block-tx-data (when (empty? fix-inline-page-tx-data)
-                                        (toggle-page-and-block conn tx-report))
+                                        (toggle-page-and-block db tx-report))
         display-blocks-tx-data (add-missing-properties-to-typed-display-blocks db-after tx-data tx-meta)
         display-blocks-tx-data (add-missing-properties-to-typed-display-blocks db-after tx-data tx-meta)
         commands-tx (when-not (or (:undo? tx-meta) (:redo? tx-meta) (rtc-tx-or-download-graph? tx-meta))
         commands-tx (when-not (or (:undo? tx-meta) (:redo? tx-meta) (rtc-tx-or-download-graph? tx-meta))
-                      (commands/run-commands conn tx-report))
+                      (commands/run-commands tx-report))
         insert-templates-tx (when-not (rtc-tx-or-download-graph? tx-meta)
         insert-templates-tx (when-not (rtc-tx-or-download-graph? tx-meta)
                               (insert-tag-templates repo tx-report))
                               (insert-tag-templates repo tx-report))
         created-by-tx (add-created-by-ref-hook db-before db-after tx-data tx-meta)]
         created-by-tx (add-created-by-ref-hook db-before db-after tx-data tx-meta)]
@@ -381,108 +341,71 @@
             fix-page-tags-tx-data
             fix-page-tags-tx-data
             fix-inline-page-tx-data)))
             fix-inline-page-tx-data)))
 
 
-(defn- reverse-tx!
-  [conn tx-data]
-  (let [reversed-tx-data (map (fn [[e a v _tx add?]]
-                                (let [op (if add? :db/retract :db/add)]
-                                  [op e a v])) tx-data)]
-    (d/transact! conn reversed-tx-data {:revert-tx-data? true
-                                        :gen-undo-ops? false})))
-
-(defn- undo-tx-data-if-disallowed!
-  [conn {:keys [tx-data tx-meta]}]
-  (when-not (:rtc-download-graph? tx-meta)
-    (let [db @conn
-          page-has-block-parent? (some (fn [d] (and (:added d)
-                                                    (= :block/parent (:a d))
-                                                    (ldb/page? (d/entity db (:e d)))
-                                                    (not (ldb/page? (d/entity db (:v d)))))) tx-data)]
-      ;; TODO: add other cases that need to be undo
-      (when page-has-block-parent?
-        (reverse-tx! conn tx-data)
-        (throw (ex-info "Page can't have block as parent"
-                        {:type :notification
-                         :payload {:message "Page can't have block as parent"
-                                   :type :warning}
-                         :tx-data tx-data}))))))
+(defn transact-pipeline
+  "Compute extra tx-data and block/refs, should ensure it's a pure function and
+  doesn't call `d/transact!` or `ldb/transact!`."
+  [repo {:keys [db-after tx-meta] :as tx-report}]
+  (let [db-based? (entity-plus/db-based-graph? db-after)
+        extra-tx-data (when db-based?
+                        (compute-extra-tx-data repo tx-report))
+        tx-report* (if (seq extra-tx-data)
+                     (let [result (d/with db-after extra-tx-data)]
+                       (assoc tx-report
+                              :tx-data (concat (:tx-data tx-report) (:tx-data result))
+                              :db-after (:db-after result)))
+                     tx-report)
+        {:keys [pages blocks]} (ds-report/get-blocks-and-pages tx-report*)
+        deleted-blocks (outliner-pipeline/filter-deleted-blocks (:tx-data tx-report*))
+        deleted-block-ids (set (map :db/id deleted-blocks))
+        blocks' (remove (fn [b] (deleted-block-ids (:db/id b))) blocks)
+        block-refs (when (seq blocks')
+                     (rebuild-block-refs repo tx-report* blocks'))
+        tx-id-data (let [db-after (:db-after tx-report*)
+                         updated-blocks (remove (fn [b] (contains? deleted-block-ids (:db/id b)))
+                                                (concat pages blocks))
+                         tx-id (get-in tx-report* [:tempids :db/current-tx])]
+                     (keep (fn [b]
+                             (when-let [db-id (:db/id b)]
+                               (when (:block/uuid (d/entity db-after db-id))
+                                 {:db/id db-id
+                                  :block/tx-id tx-id}))) updated-blocks))
+        block-refs-tx-id-data (concat block-refs tx-id-data)
+        replace-tx-report (when (seq block-refs-tx-id-data)
+                            (d/with (:db-after tx-report*) block-refs-tx-id-data))
+        tx-report' (or replace-tx-report tx-report*)
+        full-tx-data (concat (:tx-data tx-report*)
+                             (:tx-data replace-tx-report))]
+    (assoc tx-report'
+           :tx-data full-tx-data
+           :tx-meta tx-meta
+           :db-before (:db-before tx-report)
+           :db-after (or (:db-after tx-report')
+                         (:db-after tx-report)))))
 
 
 (defn- invoke-hooks-default
 (defn- invoke-hooks-default
   [repo conn {:keys [tx-meta] :as tx-report} context]
   [repo conn {:keys [tx-meta] :as tx-report} context]
-  ;; Notice: don't catch `undo-tx-data-if-disallowed!` since we want it failed immediately
-  (undo-tx-data-if-disallowed! conn tx-report)
   (try
   (try
-    (let [extra-tx-data (when (sqlite-util/db-based-graph? repo)
-                          (compute-extra-tx-data repo conn tx-report))
-          tx-report* (if (seq extra-tx-data)
-                       (let [result (ldb/transact! conn extra-tx-data {:pipeline-replace? true
-                                                                       :outliner-op :pre-hook-invoke
-                                                                       :skip-store? true})]
-                         (assoc tx-report
-                                :tx-data (concat (:tx-data tx-report) (:tx-data result))
-                                :db-after (:db-after result)))
-                       tx-report)
-          {:keys [pages blocks]} (ds-report/get-blocks-and-pages tx-report*)
+    (let [{:keys [pages blocks]} (ds-report/get-blocks-and-pages tx-report)
+          deleted-blocks (outliner-pipeline/filter-deleted-blocks (:tx-data tx-report))
           _ (when (common-sqlite/local-file-based-graph? repo)
           _ (when (common-sqlite/local-file-based-graph? repo)
               (let [page-ids (distinct (map :db/id pages))]
               (let [page-ids (distinct (map :db/id pages))]
                 (doseq [page-id page-ids]
                 (doseq [page-id page-ids]
                   (when (d/entity @conn page-id)
                   (when (d/entity @conn page-id)
                     (file/sync-to-file repo page-id tx-meta)))))
                     (file/sync-to-file repo page-id tx-meta)))))
-          deleted-blocks (outliner-pipeline/filter-deleted-blocks (:tx-data tx-report*))
-          deleted-block-ids (set (map :db/id deleted-blocks))
           deleted-block-uuids (set (map :block/uuid deleted-blocks))
           deleted-block-uuids (set (map :block/uuid deleted-blocks))
+          deleted-block-ids (set (map :db/id deleted-blocks))
           _ (when (seq deleted-block-uuids)
           _ (when (seq deleted-block-uuids)
               (swap! worker-state/*deleted-block-uuid->db-id merge
               (swap! worker-state/*deleted-block-uuid->db-id merge
                      (zipmap (map :block/uuid deleted-blocks)
                      (zipmap (map :block/uuid deleted-blocks)
                              (map :db/id deleted-blocks))))
                              (map :db/id deleted-blocks))))
           deleted-assets (keep (fn [id]
           deleted-assets (keep (fn [id]
-                                 (let [e (d/entity (:db-before tx-report*) id)]
+                                 (let [e (d/entity (:db-before tx-report) id)]
                                    (when (ldb/asset? e)
                                    (when (ldb/asset? e)
                                      {:block/uuid (:block/uuid e)
                                      {:block/uuid (:block/uuid e)
                                       :ext (:logseq.property.asset/type e)}))) deleted-block-ids)
                                       :ext (:logseq.property.asset/type e)}))) deleted-block-ids)
-          blocks' (remove (fn [b] (deleted-block-ids (:db/id b))) blocks)
-          block-refs (when (seq blocks')
-                       (rebuild-block-refs repo tx-report* blocks'))
-          refs-tx-report (when (seq block-refs)
-                           (ldb/transact! conn block-refs {:pipeline-replace? true
-                                                           :skip-store? true}))
-          replace-tx (let [db-after (or (:db-after refs-tx-report) (:db-after tx-report*))]
-                       (concat
-                        ;; update block/tx-id
-                        (let [updated-blocks (remove (fn [b] (contains? deleted-block-ids (:db/id b)))
-                                                     (concat pages blocks))
-                              tx-id (get-in (or refs-tx-report tx-report*) [:tempids :db/current-tx])]
-                          (keep (fn [b]
-                                  (when-let [db-id (:db/id b)]
-                                    (when (:block/uuid (d/entity db-after db-id))
-                                      {:db/id db-id
-                                       :block/tx-id tx-id}))) updated-blocks))))
-          tx-report' (ldb/transact! conn replace-tx {:pipeline-replace? true
-                                                     ;; Ensure db persisted
-                                                     :db-persist? true})
-          _ (when-not (:revert-tx-data? tx-meta)
-              (try
-                (validate-db! repo conn tx-report* tx-meta context)
-                (catch :default e
-                  (when-not (rtc-tx-or-download-graph? tx-meta)
-                    (prn :debug :revert-invalid-tx
-                         :tx-meta
-                         tx-meta
-                         :tx-data
-                         (:tx-data tx-report*))
-                    (reverse-tx! conn (:tx-data tx-report*)))
-                  (throw e))))
-          full-tx-data (concat (:tx-data tx-report*)
-                               (:tx-data refs-tx-report)
-                               (:tx-data tx-report'))
-          final-tx-report (assoc tx-report'
-                                 :tx-data full-tx-data
-                                 :tx-meta tx-meta
-                                 :db-before (:db-before tx-report)
-                                 :db-after (or (:db-after tx-report')
-                                               (:db-after tx-report)))
           affected-query-keys (when-not (or (:importing? context) (:rtc-download-graph? tx-meta))
           affected-query-keys (when-not (or (:importing? context) (:rtc-download-graph? tx-meta))
-                                (worker-react/get-affected-queries-keys final-tx-report))]
-      {:tx-report final-tx-report
+                                (worker-react/get-affected-queries-keys tx-report))]
+      {:tx-report tx-report
        :affected-keys affected-query-keys
        :affected-keys affected-query-keys
        :deleted-block-uuids deleted-block-uuids
        :deleted-block-uuids deleted-block-uuids
        :deleted-assets deleted-assets
        :deleted-assets deleted-assets
@@ -494,16 +417,14 @@
 
 
 (defn invoke-hooks
 (defn invoke-hooks
   [repo conn {:keys [tx-meta] :as tx-report} context]
   [repo conn {:keys [tx-meta] :as tx-report} context]
-  (when-not (or (:pipeline-replace? tx-meta)
-                (:revert-tx-data? tx-meta))
-    (let [{:keys [from-disk? new-graph?]} tx-meta]
-      (cond
-        (or from-disk? new-graph?)
-        {:tx-report tx-report}
+  (let [{:keys [from-disk? new-graph?]} tx-meta]
+    (cond
+      (or from-disk? new-graph?)
+      {:tx-report tx-report}
 
 
-        (or (::gp-exporter/new-graph? tx-meta)
-            (and (::sqlite-export/imported-data? tx-meta) (:import-db? tx-meta)))
-        (invoke-hooks-for-imported-graph conn tx-report)
+      (or (::gp-exporter/new-graph? tx-meta)
+          (and (::sqlite-export/imported-data? tx-meta) (:import-db? tx-meta)))
+      (invoke-hooks-for-imported-graph conn tx-report)
 
 
-        :else
-        (invoke-hooks-default repo conn tx-report context)))))
+      :else
+      (invoke-hooks-default repo conn tx-report context))))

+ 5 - 4
src/main/frontend/worker/rtc/asset.cljs

@@ -14,6 +14,7 @@
             [frontend.worker.rtc.ws-util :as ws-util]
             [frontend.worker.rtc.ws-util :as ws-util]
             [frontend.worker.state :as worker-state]
             [frontend.worker.state :as worker-state]
             [logseq.common.path :as path]
             [logseq.common.path :as path]
+            [logseq.db :as ldb]
             [malli.core :as ma]
             [malli.core :as ma]
             [missionary.core :as m]))
             [missionary.core :as m]))
 
 
@@ -144,11 +145,11 @@
                (throw (ex-info "upload asset failed" r)))
                (throw (ex-info "upload asset failed" r)))
              ;; asset might be deleted by the user before uploaded successfully
              ;; asset might be deleted by the user before uploaded successfully
              (when (d/entity @conn [:block/uuid asset-uuid])
              (when (d/entity @conn [:block/uuid asset-uuid])
-               (d/transact! conn
-                            [{:block/uuid asset-uuid
-                              :logseq.property.asset/remote-metadata {:checksum checksum :type asset-type}}]
+               (ldb/transact! conn
+                              [{:block/uuid asset-uuid
+                                :logseq.property.asset/remote-metadata {:checksum checksum :type asset-type}}]
                             ;; Don't generate rtc ops again, (block-ops & asset-ops)
                             ;; Don't generate rtc ops again, (block-ops & asset-ops)
-                            {:persist-op? false}))
+                              {:persist-op? false}))
              (client-op/remove-asset-op repo asset-uuid))))
              (client-op/remove-asset-op repo asset-uuid))))
        (c.m/concurrent-exec-flow 3 (m/seed asset-uuid->url))
        (c.m/concurrent-exec-flow 3 (m/seed asset-uuid->url))
        (m/reduce (constantly nil))))
        (m/reduce (constantly nil))))

Некоторые файлы не были показаны из-за большого количества измененных файлов