Jelajahi Sumber

Merge branch 'master' into feat/mcp-server

Gabriel Horner 2 bulan lalu
induk
melakukan
20ed5bccfb
100 mengubah file dengan 3062 tambahan dan 721 penghapusan
  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.
 - `src/`: Core source code
   - `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/test/`: unit-tests
 - `deps/`: Internal dependencies/modules
 - `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
 - Run linters and unit-tests: `bb dev:lint-and-test`
 - Run single focused unit-test:

+ 8 - 1
bb.edn

@@ -10,7 +10,9 @@
   logseq/graph-parser
   {:local/root "deps/graph-parser"}
   org.clj-commons/digest
-  {:mvn/version "1.4.100"}}
+  {:mvn/version "1.4.100"}
+  cheshire/cheshire
+  {:mvn/version "5.12.0"}}
  :pods
  {clj-kondo/clj-kondo {:version "2024.09.27"}
   org.babashka/fswatcher {:version "0.0.3"}
@@ -19,6 +21,11 @@
  {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
   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
   (:require
+   [clojure.set :as set]
    [clojure.string :as string]
    [clojure.test :refer [deftest testing is use-fixtures]]
    [jsonista.core :as json]
@@ -14,6 +15,10 @@
 (use-fixtures :once fixtures/open-page)
 (use-fixtures :each fixtures/new-logseq-page)
 
+(defn ->plugin-ident
+  [property-name]
+  (str ":plugin.property._test_plugin/" property-name))
+
 (defn- to-snake-case
   "Converts a string to snake_case. Handles camelCase, PascalCase, spaces, hyphens, and existing underscores.
    Examples:
@@ -31,9 +36,14 @@
       ;; Remove redundant underscores and trim
         (clojure.string/replace #"_+" "_")
         (clojure.string/trim)
-      ;; Convert to lowercase
+        ;; Convert to lowercase
         (clojure.string/lower-case))))
 
+(defonce ^:private *property-idx (atom 0))
+(defn- new-property
+  []
+  (str "p" (swap! *property-idx inc)))
+
 (defn- ls-api-call!
   [tag & args]
   (let [tag (name tag)
@@ -43,10 +53,10 @@
         ns1 (string/lower-case (if (and ns? (not inbuilt?))
                                  (str "sdk." (first ns')) "api"))
         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!
   ([ret] (assert-api-ls-block! ret 1))
@@ -56,22 +66,23 @@
      (assert/assert-have-count (str "#ls-block-" uuid') count)
      uuid')))
 
-(deftest apis-related-test
-  (testing "block related apis"
+(deftest editor-apis-test
+  (testing "editor related apis"
     (page/new-page "test-block-apis")
     (ls-api-call! :ui.showMsg "hello world" "info")
     (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")
-          _ (assert-api-ls-block! ret1)
           uuid' (assert-api-ls-block! ret)]
+      (assert-api-ls-block! ret1)
       (-> (ls-api-call! :editor.insertBlock uuid' "insert-0")
           (assert-api-ls-block!))
       (ls-api-call! :editor.updateBlock uuid' "append-but-updated-0")
       (k/esc)
       (w/wait-for ".block-title-wrap:text('append-but-updated-0')")
       (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"
     (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}})
@@ -81,8 +92,8 @@
           props2 (ls-api-call! :editor.getPageProperties "test-block-properties-apis")]
       (w/wait-for ".property-k:text('p1')")
       (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")))
       (ls-api-call! :editor.upsertBlockProperty uuid' "p2" "p2")
       (ls-api-call! :editor.upsertBlockProperty uuid' "p3" true)
@@ -102,20 +113,226 @@
       (ls-api-call! :editor.upsertBlockProperty uuid' "p2" "p2-updated")
       (w/wait-for ".block-title-wrap:text('p2-updated')")
       (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"
-    (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"}
 
   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"}
   borkdude/rewrite-edn                  {:mvn/version "0.4.9"}

+ 1 - 1
deps/cli/package.json

@@ -10,7 +10,7 @@
   },
   "license": "MIT",
   "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",
     "better-sqlite3": "~11.10.0",
     "fastify": "5.3.2",

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

@@ -79,7 +79,7 @@
   [m]
   (->> (remove (fn [[k _v]]
                  (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 {})))
 
 (defn get-page-data

+ 3 - 3
deps/cli/yarn.lock

@@ -43,9 +43,9 @@
     "@fastify/forwarded" "^3.0.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:
     import-meta-resolve "^4.1.0"
 

+ 1 - 1
deps/common/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "devDependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v29"
   },
   "scripts": {
     "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]
             [clojure.edn :as edn]
             [clojure.string :as string]
-            [clojure.walk :as walk]
             [goog.string :as gstring]
             [logseq.common.log :as log]))
 
@@ -24,17 +23,6 @@
   [s]
   (.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
   "remove pairs of key-value that has nil value from a map (nested not supported)."
   [nm]

+ 3 - 3
deps/common/yarn.lock

@@ -2,9 +2,9 @@
 # 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:
     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!
 ;; API
 logseq.db.sqlite.gc/ensure-no-garbage
+;; API
+logseq.db.common.entity-util/entity->map
 ;; documenting keywords
 logseq.db.frontend.kv-entity/kv-entities

+ 2 - 1
deps/db/deps.edn

@@ -1,7 +1,8 @@
 {:deps
  ;; 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
-                         :sha     "45f6721bf2038c24eb9fe3afb422322ab3f473b5"}
+                         :sha     "3971e2d43bd93d89f42191dc7b4b092989e0cc61"}
+  ;; datascript/datascript                 {:local/root "../../../../datascript"}
   datascript-transit/datascript-transit {:mvn/version "0.3.0"
                                          :exclusions [datascript/datascript]}
   cljs-bean/cljs-bean         {:mvn/version "1.5.0"}

+ 1 - 1
deps/db/package.json

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

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

@@ -5,7 +5,7 @@
             ["path" :as node-path]
             [babashka.cli :as cli]
             [clojure.edn :as edn]
-            [datascript.core :as d]
+            [logseq.db :as ldb]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
             [logseq.db.sqlite.export :as sqlite-export]
             [logseq.outliner.cli :as outliner-cli]
@@ -51,9 +51,9 @@
              (count (filter :block/title init-tx)) "blocks ...")
     ;; (fs/writeFileSync "txs.edn" (with-out-str (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 "!"))
     (when (:validate options)
       (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]
             [clojure.string :as string]
             [clojure.walk :as walk]
+            [datascript.conn :as dc]
             [datascript.core :as d]
             [datascript.impl.entity :as de]
             [logseq.common.config :as common-config]
@@ -20,6 +21,7 @@
             [logseq.db.frontend.entity-util :as entity-util]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.schema :as db-schema]
+            [logseq.db.frontend.validate :as db-validate]
             [logseq.db.sqlite.util :as sqlite-util])
   (:refer-clojure :exclude [object?]))
 
@@ -32,9 +34,18 @@
 (def build-favorite-tx db-db/build-favorite-tx)
 
 (defonce *transact-fn (atom nil))
+(defonce *transact-invalid-callback (atom nil))
+(defonce *transact-pipeline-fn (atom nil))
+
 (defn register-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
   [tx-data]
@@ -59,35 +70,87 @@
               data))
           tx-data)))
 
-(defn assert-no-entities
+(defn entity->db-id
   [tx-data]
   (walk/prewalk
    (fn [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))
    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!
   "`repo-or-conn`: repo for UI thread and conn for worker/node"
   ([repo-or-conn tx-data]
    (transact! repo-or-conn tx-data nil))
   ([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)
                       (remove empty?))
          delete-blocks-tx (when-not (string? repo-or-conn)
@@ -96,7 +159,7 @@
 
      ;; 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
-     (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)
        ;; (cljs.pprint/pprint tx-data)
@@ -104,12 +167,7 @@
 
        (if-let [transact-fn @*transact-fn]
          (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 internal-page? entity-util/internal-page?)
@@ -167,7 +225,7 @@
         closed-property (:block/closed-value-property block)]
     (sort-by-order (cond
                      closed-property
-                     (:property/closed-values closed-property)
+                     (:block/_closed-value-property closed-property)
 
                      from-property
                      (filter (fn [e]
@@ -467,31 +525,6 @@
    (set)
    (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)
 
 (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
   "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]))
 
 (defn whiteboard?
@@ -17,3 +18,9 @@
   [entity]
   (or (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?})
 
       :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
       (->>

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

@@ -7,6 +7,7 @@
             [flatland.ordered.map :refer [ordered-map]]
             [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.db.frontend.db-ident :as db-ident]
+            [logseq.db.frontend.entity-util :as entity-util]
             [logseq.db.frontend.rules :as rules]
             [logseq.db.sqlite.util :as sqlite-util]))
 
@@ -139,11 +140,11 @@
   [class]
   (assert (de/entity? class) "get-class-extends `class` should be an entity")
   (loop [extends (:logseq.property.class/extends class)
-         result #{}]
+         result []]
     (if (seq extends)
-      (recur (set (mapcat :logseq.property.class/extends extends))
+      (recur (mapcat :logseq.property.class/extends extends)
              (into result extends))
-      result)))
+      (reverse (distinct result)))))
 
 (defn create-user-class-ident-from-name
   "Creates a class :db/ident for a default user namespace.
@@ -173,3 +174,14 @@
   "Determines if namespace string is a user class"
   [s]
   (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)))
          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
   "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.
@@ -63,32 +70,23 @@
 
    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"
-  ([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
-     (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
   [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)))
                         (fn closed-value-valid? [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')]
     (if (db-property/many? property)
       (or (every? validate-fn'' property-val)
@@ -346,7 +350,7 @@
    (concat
     [:map
      [: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-attrs
     page-attrs

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

@@ -683,7 +683,8 @@
   [db property-id]
   (when db
     (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
   "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")
                   (when pure? (assert (some? gen-uuid-value-prefix) block))
                   [(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
                                  block' property-map %
                                  (cond-> {}
@@ -121,6 +124,9 @@
                                    (assoc :block-uuid
                                           (common-uuid/gen-uuid :builtin-block-uuid (str gen-uuid-value-prefix "-" %)))))
                                v))
+                     (uuid? v)
+                     [:block/uuid v]
+                     :else
                      (build-property-value-block block' property-map v
                                                  (cond-> {}
                                                    property-value-properties
@@ -130,12 +136,25 @@
                                                           (common-uuid/gen-uuid :builtin-block-uuid (str gen-uuid-value-prefix "-" v))))))])))
          (into {}))))
 
+(defn- lookup-id?
+  [v]
+  (and (vector? v)
+       (= 2 (count v))
+       (= :block/uuid (first v))
+       (uuid? (second v))))
+
 (defn build-properties-with-ref-values
   "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"
   [prop-vals-tx-m]
   (update-vals prop-vals-tx-m
                (fn [v]
-                 (if (set? v)
+                 (cond
+                   (and (set? v) (every? lookup-id? v))
+                   v
+                   (set? v)
                    (set (map #(vector :block/uuid (:block/uuid %)) v))
+                   (lookup-id? v)
+                   v
+                   :else
                    (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
   "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
   "Valid property types for users in order they appear in the UI"
@@ -134,7 +134,8 @@
   (if new-closed-value?
     (string? s)
     (when-let [ent (d/entity db s)]
-      (string? (:block/title ent)))))
+      (and (string? (:block/title ent))
+           (some? (:block/page ent))))))
 
 (defn- node-entity?
   [db val]
@@ -147,33 +148,17 @@
     (and (some? (:block/title 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
               {:error/message "should be an Entity"}
               entity?]
@@ -186,12 +171,44 @@
    :page     [:fn
               {:error/message "should be a Page"}
               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     coll?
+   :coll     [:fn
+              {:error/message "should be a collection"}
+              coll?]
    :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))
            (into internal-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?]
   (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
   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)
         tx-datoms (mapcat #(d/datoms db-after :eavt %) changed-ids)
         ent-maps* (map (fn [[db-id m]]
@@ -38,7 +38,7 @@
                               ;; remove :db/id as it adds needless declarations to schema
                               #(validator [(dissoc % :db/id)])
                               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)
           (let [explainer (get-schema-explainer (:closed-schema? validate-options))]
             (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
                       (and (:build/property-value v) (seq 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)))
 
 (defn- extract-basic-content-refs
@@ -502,7 +513,9 @@
         [init-tx block-props-tx]
         (reduce (fn [[init-tx* block-props-tx*] m]
                   (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)
                        (conj block-props-tx*
                              (merge {:block/uuid (or (:block/uuid m)

+ 3 - 3
deps/db/yarn.lock

@@ -2,9 +2,9 @@
 # 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:
     import-meta-resolve "^4.1.0"
 

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

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "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"
   },
   "dependencies": {

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

@@ -22,6 +22,7 @@
             [promesa.core :as p]))
 
 (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!)
 (defn dev-transact! [conn tx-data tx-meta]
   (swap! tx-queue (fn [queue]
@@ -94,8 +95,7 @@
       (println (some-> (get-in m [:ex-data :error]) .-stack)))
     (when debug
       (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))]
         (println (str "\n" (count matching-tx)) "Tx Maps for failing path:")
         (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))
         ;; _ (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
-        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)
 
         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]))
         ;; _ (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)
 
         upstream-properties-tx
         (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}))
-        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)]
 
     ;; 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
 
 
-"@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:
     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
 logseq.outliner.op/apply-ops!
 ;; API fn
@@ -9,4 +5,4 @@ logseq.outliner.op/register-op-handlers!
 ;; API fn
 logseq.outliner.page/delete!
 ;; API fn
-logseq.outliner.page/create!
+logseq.outliner.page/create!

+ 2 - 1
deps/outliner/deps.edn

@@ -1,7 +1,8 @@
 {:deps
  ;; 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
-                         :sha     "45f6721bf2038c24eb9fe3afb422322ab3f473b5"}
+                         :sha     "3971e2d43bd93d89f42191dc7b4b092989e0cc61"}
+  ;; datascript/datascript {:local/root "../../../../datascript"}
   com.cognitect/transit-cljs {:mvn/version "0.8.280"}
 
   ;; 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",
   "private": true,
   "devDependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v29"
   },
   "dependencies": {
     "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"
   (:require [clojure.edn :as edn]
             [datascript.core :as d]
+            [logseq.db :as ldb]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
             [logseq.db.frontend.rules :as rules]
             [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)))
       (do
         (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 "!"))))))
 
 (when (= nbb/*file* (nbb/invoked-file))

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

@@ -5,8 +5,8 @@
             ["path" :as node-path]
             [borkdude.rewrite-edn :as rewrite]
             [clojure.string :as string]
-            [datascript.core :as d]
             [logseq.common.config :as common-config]
+            [logseq.db :as ldb]
             [logseq.db.common.sqlite-cli :as sqlite-cli]
             [logseq.db.sqlite.build :as sqlite-build]
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
@@ -48,9 +48,9 @@
           additional-config
           (pretty-print-merge additional-config))
         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
   "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
                           :block.temp/ast-title :block.temp/ast-body :block/level :block.temp/load-status
                           :block.temp/has-children?)
-                  common-util/remove-nils
-
                   (fix-tag-ids db {:db-graph? db-based?}))
                (not collapse-or-expand?)
                block-with-updated-at)
@@ -1103,12 +1101,13 @@
 ;;; ### write-operations have side-effects (do transactions) ;;;;;;;;;;;;;;;;
 
 (defn- op-transact!
-  [f & args]
+  [outliner-op f & args]
   {:pre [(fn? f)]}
   (try
     (let [result (apply f args)]
       (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)))
       result)
     (catch :default e
@@ -1119,31 +1118,29 @@
           (save-block repo @conn date-formatter block opts))]
   (defn save-block!
     [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]
           (insert-blocks repo @conn blocks target-block opts))]
   (defn insert-blocks!
     [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!
     [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!
   [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)))
 
 (defn move-blocks-up-down!
   [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!
   [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
    doesn't handle updating DB graphs well yet e.g. doesn't handle :block/tx-id"
   [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
     (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])]
     (when (seq refs)
       (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))))
 
 (defn delete!
@@ -228,7 +229,7 @@
        [page])
      (remove nil?))))
 
-(defn- ^:large-vars/cleanup-todo create
+(defn ^:large-vars/cleanup-todo ^:api create
   "Pure function without side effects"
   [db title*
    {uuid' :uuid
@@ -305,7 +306,7 @@
 
 (defn create!
   [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)
-      (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]
   (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))]
-                         (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))

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

@@ -18,7 +18,9 @@
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.outliner.core :as outliner-core]
+            [logseq.outliner.page :as outliner-page]
             [logseq.outliner.validate :as outliner-validate]
+            [malli.core :as m]
             [malli.error :as me]
             [malli.util :as mu]))
 
@@ -28,6 +30,14 @@
     (throw (ex-info "Read-only property value shouldn't be edited"
                     {: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
   (->>
    (mapcat
@@ -194,7 +204,7 @@
                (or (not= (:logseq.property/type schema) (:logseq.property/type 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)))
-                   (seq (:property/closed-values property))))
+                   (seq (entity-plus/lookup-kv-then-entity property :property/closed-values))))
           (concat (update-datascript-schema property schema)))
         tx-data (concat property-tx-data
                         (when (seq properties)
@@ -227,27 +237,48 @@
         schema (get-property-value-schema db property-type property)]
     (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
   [id]
   (if (uuid? id) [:block/uuid id] id))
 
 (defn- raw-set-block-property!
   "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))
-  (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!
   "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"))
         value' (convert-property-input-string (:logseq.property/type block)
                                               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-block-id
                           (assoc :block/uuid new-block-id))]
@@ -265,7 +301,7 @@
     (let [property-id (:db/ident property)]
       (when (and property-id 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))))
 
 (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"
   [conn property-id v]
   (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))]
     (cond
       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?
            ;; FIXME: remove this when :logseq.property/order-list-type updated to closed values
@@ -308,6 +347,9 @@
   [conn property-id v property-type]
   (let [number-property? (= property-type :number)]
     (cond
+      (and (qualified-keyword? v) (not= :keyword property-type))
+      (db-ident->eid @conn v)
+
       (and (integer? v)
            (or (not number-property?)
                ;; Allows :number property to use number as a ref (for closed value) or value
@@ -317,13 +359,19 @@
       v
 
       (= 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
       ;; only value-ref-property types should call this
       (when-let [v' (if (and number-property? (string? v))
@@ -391,25 +439,32 @@
                 @conn
                 (if (number? v) (d/entity @conn v) v)
                 (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)
-           _ (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)
            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?))
                 (convert-ref-property-value conn property-id v property-type)
                 v)
+           _ (when (nil? v')
+               (throw (ex-info "Property value must be not nil" {:v v})))
            txs (doall
                 (mapcat
                  (fn [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')]
                        (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'))
                      (js/console.error "Skipping setting a block's property because the block id could not be found:" eid)))
                  block-eids))]
@@ -462,37 +517,49 @@
   attributes as properties"
   [conn block-eid property-id v]
   (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!
   "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."
                                                        :type :error}})))))]
     (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))]
       (update-property conn db-ident property schema opts)
       (let [k-name (or (and property-name (name property-name))
@@ -651,7 +722,7 @@
                         {:block/title resolved-value})))
                      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
                                                                                   property {:icon icon})
                                       (assoc :block/order (db-order/gen-key max-order nil)))]
@@ -738,6 +809,11 @@
 (defn delete-closed-value!
   "Returns true when deleted or if not deleted displays warning and returns false"
   [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)]
     (if (ldb/built-in? value-block)
       (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"}
                  :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")))
-        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])))))
 
 (deftest class-add-property!
@@ -367,4 +367,4 @@
                                               (:db/id (d/entity @conn :user.class/C1)))
     (is (= [:logseq.class/Root]
            (: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
 
 
-"@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:
     import-meta-resolve "^4.1.0"
 

+ 1 - 1
deps/publishing/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "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"
   },
   "dependencies": {

+ 3 - 3
deps/publishing/yarn.lock

@@ -2,9 +2,9 @@
 # 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:
     import-meta-resolve "^4.1.0"
 

+ 1 - 0
libs/.npmignore

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

+ 13 - 0
libs/README.md

@@ -30,3 +30,16 @@ import "@logseq/libs"
 #### Feedback
 If you have any feedback or encounter any issues, feel free to join Logseq's discord group.
 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",
-  "version": "0.2.1",
+  "version": "0.2.3",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",
@@ -10,6 +10,7 @@
     "dev:user": "npm run build:user -- --mode development --watch",
     "build:core": "webpack --config webpack.config.core.js --mode production",
     "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",
     "lint": "prettier --check \"src/**/*.{ts, js}\"",
     "fix": "prettier --write \"src/**/*.{ts, js}\"",
@@ -27,6 +28,7 @@
     "snake-case": "3.0.4"
   },
   "devDependencies": {
+    "ts-morph": "^22.0.0",
     "@babel/core": "^7.20.2",
     "@babel/preset-env": "^7.20.2",
     "@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
   name: string
   version: string
+  runtime: string
   mode: 'shadow' | 'iframe'
   webPkg?: any // web plugin package.json data
   settingsSchema?: SettingSchemaDesc[]
@@ -166,6 +167,7 @@ interface PluginLocalOptions {
 
 interface PluginLocalSDKMetadata {
   version: string
+  runtime: string
 
   [key: string]: any
 }
@@ -669,7 +671,7 @@ class PluginLocal extends EventEmitter<
           ? `<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>`
       }
-    
+
   </head>
   <body>
   <div id="app"></div>

+ 5 - 0
libs/src/LSPlugin.ts

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

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

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

+ 147 - 0
libs/yarn.lock

@@ -1066,11 +1066,42 @@
     "@jridgewell/resolve-uri" "^3.0.3"
     "@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":
   version "1.0.0-next.17"
   resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.17.tgz#25fdbdfd282c2f86ddf3fcefbd98be99cd2627e2"
   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":
   version "4.1.7"
   resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@@ -1434,6 +1465,13 @@ braces@^3.0.1:
   dependencies:
     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:
   version "4.16.8"
   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"
     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:
   version "1.9.3"
   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"
   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:
   version "2.1.0"
   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"
   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:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -1726,6 +1787,13 @@ fill-range@^7.0.1:
   dependencies:
     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:
   version "3.3.2"
   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"
   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:
   version "0.4.1"
   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:
     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:
   version "7.0.0"
   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"
   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:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
@@ -2036,6 +2128,14 @@ micromatch@^4.0.0:
     braces "^3.0.1"
     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]:
   version "1.49.0"
   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:
     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]:
   version "2.1.2"
   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"
   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:
   version "4.0.0"
   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"
   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:
   version "4.2.0"
   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"
   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:
   version "2.1.0"
   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"
     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:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@@ -2535,6 +2674,14 @@ [email protected]:
     micromatch "^4.0.0"
     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:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"

+ 1 - 1
package.json

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

File diff ditekan karena terlalu besar
+ 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-win-arm64": "electron-forge make --platform=win32 --arch=arm64",
     "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"
   },
   "config": {
@@ -57,7 +57,7 @@
     "@electron-forge/maker-wix": "^7.8.3",
     "@electron-forge/maker-zip": "^7.8.3",
     "@electron/rebuild": "4.0.1",
-    "electron": "37.2.6",
+    "electron": "38.4.0",
     "electron-builder": "26.0.12",
     "electron-devtools-installer": "4.0.0",
     "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",
   "private": true,
   "devDependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v28"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v29"
   },
   "dependencies": {
     "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
 
 
-"@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:
     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;
 }
 
+:root {
+  text-autospace: normal;
+}
+
 /* region Reset top elements */
 html {
   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;
 
             .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 {

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

@@ -341,7 +341,7 @@
                      (let [page-entity (db/entity [:block/uuid page])
                            repo (state/sub :git/current-repo)
                            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)]
                        (when-not (string/blank? content)
                          [:.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))))
        [:span.page-icon {:key "page-icon"} icon]
        [:span.page-title {:key "title"
-                          :class (when untitled? "opacity-50")
-                          :style {:display "ruby"}}
+                          :class (when untitled? "opacity-50")}
         (cond
           (not (db/page? page))
           (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)
         batch? (pv/batch-operation?)
         hide-property-key? (or (contains? #{:date :datetime} (:logseq.property/type property))
+                               (= (:db/ident property) :logseq.property/icon)
                                (pv/select-type? block property)
                                (and
                                 batch?

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

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

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

@@ -27,7 +27,6 @@
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
-            [goog.dom :as gdom]
             [goog.functions :refer [debounce]]
             [lambdaisland.glogi :as log]
             [logseq.common.util.macro :as macro-util]
@@ -54,9 +53,9 @@
                :else
                "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)
-      (shui/button (merge {:class "empty-btn !text-base" :variant :text} opts)
+      (shui/button (merge {:class "empty-btn" :variant :text} opts)
                    text))))
 
 (rum/defc property-empty-text-value
@@ -95,6 +94,11 @@
 
 (rum/defc icon-row
   [block editing?]
+  (hooks/use-effect!
+   (fn []
+     (fn []
+       (when editing?
+         (editor-handler/restore-last-saved-cursor!)))))
   (let [icon-value (:logseq.property/icon block)
         clear-overlay! (fn []
                          (shui/popup-hide-all!))
@@ -108,36 +112,18 @@
                         (when icon (select-keys icon [:type :id :color]))))
                      (clear-overlay!)
                      (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?
   [block property]
@@ -630,9 +616,13 @@
                          (not (and (ldb/class? block) (= (:db/ident property) :logseq.property.class/extends)))
                          (not= (:db/ident property) :logseq.property.view/type))
                   (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)
                 (remove #(= :logseq.property/empty-placeholder (:value %))))
         k :on-chosen

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

@@ -45,3 +45,7 @@
   @apply border rounded pl-2;
   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.db :as db]
             [frontend.db-mixins :as db-mixins]
+            [frontend.db.async :as db-async]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [logseq.db.common.reference :as db-reference]
@@ -56,20 +57,19 @@
 (rum/defc references
   [entity config]
   (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!
        #(c.m/run-task*
          (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/component-error (if (config/db-based-graph? (state/get-current-repo))
                                "Linked References: Unexpected error."
                                "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
   [entity config]

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

@@ -1861,67 +1861,73 @@
       (db/entity [:block/uuid (:block/uuid result)]))))
 
 (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
   [view-parent view-entity table columns input sorting
@@ -1935,9 +1941,11 @@
                   (and references? (not hover?)) "opacity-0"
                   hover? "opacity-100"
                   :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-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
       (if db-based?
         (if (= view-feature-type :query-result)

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

@@ -174,10 +174,7 @@
 (defn <get-block-refs
   [graph 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
   [graph eid]

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

@@ -1,14 +1,12 @@
 (ns frontend.db.model
   "Core db functions."
-  (:require [clojure.set :as set]
-            [clojure.string :as string]
+  (:require [clojure.string :as string]
             [clojure.walk :as walk]
             [datascript.core :as d]
             [frontend.common.graph-view :as graph-view]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db.conn :as conn]
-            [frontend.db.file-based.model :as file-model]
             [frontend.db.react :as react]
             [frontend.db.utils :as db-utils]
             [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)]
     (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
   ([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))))))
               (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?
   "sanitized page-name only"
   [page-name]

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

@@ -7,6 +7,8 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.repo :as repo-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.state :as state]
             [frontend.util :as util]
@@ -14,9 +16,7 @@
             [logseq.db.sqlite.export :as sqlite-export]
             [logseq.db.sqlite.util :as sqlite-util]
             [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!
   [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))))))
 
 (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]
             :or {sibling? false
                  before? false
@@ -598,6 +600,7 @@
                             (wrap-parse-block)
                             (assoc :block/uuid (or custom-uuid (db/new-block-id))))
               new-block (merge new-block other-attrs)
+              block' (db/entity (:db/id block))
               [target-block sibling?] (cond
                                         before?
                                         (let [left-or-parent (or (ldb/get-left-sibling block)
@@ -607,13 +610,21 @@
                                           [left-or-parent 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 true]
 
                                         block
-                                        [(db/entity (:db/id block)) sibling?]
+                                        [block' sibling?]
 
                                         ;; FIXME: assert
                                         :else
@@ -2064,12 +2075,11 @@
                                               (when-not keep-uuid? [:id])
                                               [:custom_id :custom-id]
                                               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
                                                           (when-not keep-uuid? [:id])
-                                                          exclude-properties)))))))
+                                                          exclude-properties))
+                    :block/format format)))))
 
 (defn- edit-last-block-after-inserted!
   [result]
@@ -2179,17 +2189,15 @@
    A block element: {:content :properties :children [block-1, block-2, ...]}"
   [tree-vec format {:keys [target-block keep-uuid?] :as opts}]
   (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)
         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!
      {: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})))))
 
 (defn insert-block-tree-after-target

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

@@ -182,7 +182,7 @@
                   (shui/dialog-close!)
                   (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)
         pos (state/get-edit-pos)
         edit-block-or-selected (cond
@@ -234,7 +234,9 @@
         (if target'
           (shui/popup-show! target'
                             #(property-dialog/dialog blocks opts')
-                            {:align "start"})
+                            (cond-> {:align "start"}
+                              popup-id
+                              (assoc :id popup-id)))
           (shui/dialog-open! #(property-dialog/dialog blocks opts')
                              {:id :property-dialog
                               :align "start"}))))))

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

@@ -149,7 +149,7 @@
        :deleted-shapes deleted-shapes
        :new-shapes created-shapes
        :metadata {:whiteboard/transact? true
-                  :pipeline-replace? replace?}})))
+                  :whiteboard/replace? replace?}})))
 
 (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]}]
   ;; (prn :debug
   ;;      :tx-meta tx-meta
-  ;;      ;; :tx-data tx-data
-  ;;      )
+  ;;      :tx-data tx-data)
   (let [{:keys [from-disk? new-graph? initial-pages? end?]} tx-meta
         tx-report {:tx-meta tx-meta
                    :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))
 
-(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?
   []
   (= "ls-preview-popup"
@@ -63,7 +69,7 @@
   []
   (if (util/mobile?)
     (shui/popup-hide!)
-    (while (and (shui-popups?)
+    (while (and (popup-exists?)
                 (not (last-shui-preview-popup?)))
       (shui/popup-hide!))))
 

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

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

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

@@ -149,15 +149,16 @@
         (tc/to-long next-time)))))
 
 (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)
         property (d/entity db property-ident)
         date? (= :date (:logseq.property/type property))
@@ -175,11 +176,12 @@
                                               (outliner-page/create db title {})))
               value (if date? [:block/uuid page-uuid] next-time-long)]
           (concat
+           default-value-tx-data
            tx-data
            (when 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))
                            :logseq.property/scheduled)
         other-property-idents (cond
@@ -193,14 +195,14 @@
 
                                 :else
                                 (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)
         value' (get-value 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 [property (d/entity db (:a d))]
                           (when (and (true? (get property :logseq.property/enable-history?))
@@ -218,7 +220,7 @@
            :logseq.property.history/property (:db/id property)})))
      changes)))
 
-(defmethod handle-command :default [command _conn _db entity datoms]
+(defmethod handle-command :default [command _db entity datoms]
   (throw (ex-info "Unhandled command"
                   {:command command
                    :entity entity
@@ -226,23 +228,22 @@
 
 (defn execute-command
   "Build tx-data"
-  [conn db entity datoms [_command {:keys [actions]}]]
+  [db entity datoms [_command {:keys [actions]}]]
   (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
-  [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]
             [frontend.common.thread-api :refer [def-thread-api]]
             [frontend.worker.state :as worker-state]
+            [logseq.db :as ldb]
             [promesa.core :as p]))
 
 (defonce ^:private encoder (new js/TextEncoder "utf-8"))
@@ -115,7 +116,7 @@
     (assert (some? conn) repo)
     (let [aes-key-datom (first (d/datoms @conn :avet :aes-key-jwk))]
       (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
   [repo]

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

@@ -1,6 +1,7 @@
 (ns frontend.worker.db.validate
   "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.shared-service :as shared-service]
             [logseq.db :as ldb]
@@ -19,6 +20,8 @@
                      (fn [{:keys [entity dispatch-key]}]
                        (let [entity (d/entity db (:db/id entity))]
                          (cond
+                           (and (:block/tx-id entity) (nil? (:block/title entity)))
+                           [[:db/retractEntity (:db/id entity)]]
                            (= :block/path-refs (:db/ident entity))
                            (concat [[:db/retractEntity (:db/id entity)]]
                                    (try
@@ -96,11 +99,36 @@
     (when (seq tx-data)
       (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
   [conn]
+  (fix-num-prefix-db-idents! conn)
+
   (let [db @conn
         {:keys [errors datom-count entities]} (db-validate/validate-db! db)
         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
       (do
         (fix-invalid-blocks! conn errors)

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

@@ -35,7 +35,7 @@
 
         (when-not from-disk?
           (p/do!
-         ;; Sync SQLite search
+           ;; Sync SQLite search
            (let [{:keys [blocks-to-remove-set blocks-to-add]} (search/sync-search-indice repo tx-report')]
              (when (seq 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])))
           tx-data (concat remove-old-hnsw-tx-data mark-embedding-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!
   [repo conn & {:keys [handler-keys]}]
@@ -90,39 +88,37 @@
                    (remove-old-embeddings-and-reset-new-updates! conn tx-data 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)]
-                     (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
-                         (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
-                         (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)
                                             tx-report)
                                opt (assoc (additional-args (:tx-data tx-report')) :repo repo)]
                            (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.handler.page :as worker-page]
             [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.client-op :as client-op]
             [frontend.worker.rtc.core :as rtc.core]
@@ -41,10 +42,13 @@
             [logseq.common.util :as common-util]
             [logseq.db :as ldb]
             [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.order :as db-order]
+            [logseq.db.common.reference :as db-reference]
             [logseq.db.common.sqlite :as common-sqlite]
             [logseq.db.common.view :as db-view]
+            [logseq.db.frontend.class :as db-class]
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.sqlite.export :as sqlite-export]
@@ -253,8 +257,8 @@
       (doseq [db (if @*publishing? [sqlite-db] [sqlite-db client-ops-db])]
         (sqlite-gc/gc-kvs-table! db {:full-gc? full-gc?})
         (.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!
   [repo {:keys [config datoms] :as opts}]
@@ -272,6 +276,9 @@
       (common-sqlite/create-kvs-table! db)
       (when-not @*publishing? (common-sqlite/create-kvs-table! client-ops-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)
             conn (common-sqlite/get-storage-conn storage schema)
             _ (db-fix/check-and-fix-schema! repo conn)
@@ -293,7 +300,7 @@
           (let [config (or config "")
                 initial-data (sqlite-create-graph/build-db-initial-data
                               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 {})
 
@@ -462,7 +469,9 @@
 (def-thread-api :thread-api/get-block-refs
   [repo id]
   (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
   [repo id]
@@ -628,7 +637,9 @@
               {:keys [type payload]} (when (map? data) data)]
           (case type
             :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)))))))
 
 (def-thread-api :thread-api/file-writes-finished?
@@ -698,6 +709,12 @@
   (let [db @(worker-state/get-datascript-conn repo)]
     (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
   [repo {:keys [property-ident] :as option}]
   (let [conn (worker-state/get-datascript-conn repo)]
@@ -896,9 +913,19 @@
           (reset! *service [graph 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
   "web worker entry"
   []
+  (ldb/register-transact-invalid-callback-fn! notify-invalid-data)
+
   (let [proxy-object (->>
                       fns
                       (map

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

@@ -179,7 +179,7 @@
                         (into-array (map :db/id stale-block-chunk))
                         false))
                     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))
                 (c.m/<? (.write-index! infer-worker repo))))
             (m/? (task--update-index-info!* repo infer-worker false))))))))
@@ -207,7 +207,7 @@
                                         (d/datoms @conn :avet :block/title)
                                         (map (fn [d]
                                                [: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?)))))
 
@@ -236,7 +236,7 @@
     (when-let [^js infer-worker @worker-state/*infer-worker]
       (let [conn (worker-state/get-datascript-conn repo)]
         (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))))))
 
 (defn task--search

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

@@ -6,19 +6,16 @@
             [frontend.worker.commands :as commands]
             [frontend.worker.file :as file]
             [frontend.worker.react :as worker-react]
-            [frontend.worker.shared-service :as shared-service]
             [frontend.worker.state :as worker-state]
-            [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.common.util :as common-util]
             [logseq.common.util.page-ref :as page-ref]
             [logseq.common.uuid :as common-uuid]
             [logseq.db :as ldb]
+            [logseq.db.common.entity-plus :as entity-plus]
             [logseq.db.common.order :as db-order]
             [logseq.db.common.sqlite :as common-sqlite]
             [logseq.db.frontend.class :as db-class]
-            [logseq.db.frontend.validate :as db-validate]
             [logseq.db.sqlite.export :as sqlite-export]
-            [logseq.db.sqlite.util :as sqlite-util]
             [logseq.graph-parser.exporter :as gp-exporter]
             [logseq.outliner.core :as outliner-core]
             [logseq.outliner.datascript-report :as ds-report]
@@ -97,44 +94,6 @@
                                          (:tx-data result)))))))]
     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
   "Add missing attributes and remove #Page when inserting or updating block/title with inline tags"
   [{:keys [db-after tx-data tx-meta]}]
@@ -158,7 +117,7 @@
                    [:db/add eid :logseq.property.class/extends :logseq.class/Root]
                    [: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))
                  (:added datom)
                  (= (:db/id page-tag) (:v datom)))
@@ -210,9 +169,9 @@
        (apply concat)))))
 
 (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)
-    (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)]
       (mapcat
        (fn [datom]
@@ -361,15 +320,16 @@
           (nil? created-by-ent) (cons created-by-block))))))
 
 (defn- compute-extra-tx-data
-  [repo conn tx-report]
+  [repo 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-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 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)
         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-tag-templates repo tx-report))
         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-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
   [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
-    (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)
               (let [page-ids (distinct (map :db/id pages))]
                 (doseq [page-id page-ids]
                   (when (d/entity @conn page-id)
                     (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-ids (set (map :db/id deleted-blocks))
           _ (when (seq deleted-block-uuids)
               (swap! worker-state/*deleted-block-uuid->db-id merge
                      (zipmap (map :block/uuid deleted-blocks)
                              (map :db/id deleted-blocks))))
           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)
                                      {:block/uuid (:block/uuid e)
                                       :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))
-                                (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
        :deleted-block-uuids deleted-block-uuids
        :deleted-assets deleted-assets
@@ -494,16 +417,14 @@
 
 (defn invoke-hooks
   [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.state :as worker-state]
             [logseq.common.path :as path]
+            [logseq.db :as ldb]
             [malli.core :as ma]
             [missionary.core :as m]))
 
@@ -144,11 +145,11 @@
                (throw (ex-info "upload asset failed" r)))
              ;; asset might be deleted by the user before uploaded successfully
              (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)
-                            {:persist-op? false}))
+                              {:persist-op? false}))
              (client-op/remove-asset-op repo asset-uuid))))
        (c.m/concurrent-exec-flow 3 (m/seed asset-uuid->url))
        (m/reduce (constantly nil))))

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini