exporter_test.cljs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. (ns ^:node-only logseq.graph-parser.exporter-test
  2. (:require [cljs.test :refer [testing is]]
  3. [logseq.graph-parser.test.helper :as test-helper :include-macros true :refer [deftest-async]]
  4. [datascript.core :as d]
  5. [clojure.string :as string]
  6. ["path" :as node-path]
  7. ["fs" :as fs]
  8. [logseq.common.graph :as common-graph]
  9. [promesa.core :as p]
  10. [logseq.db.frontend.schema :as db-schema]
  11. [logseq.db.frontend.validate :as db-validate]
  12. [logseq.db.sqlite.create-graph :as sqlite-create-graph]
  13. [logseq.graph-parser.exporter :as gp-exporter]
  14. [logseq.db.frontend.malli-schema :as db-malli-schema]
  15. [logseq.db.frontend.property :as db-property]
  16. [logseq.db.frontend.property.type :as db-property-type]))
  17. ;; Helpers
  18. ;; =======
  19. ;; some have been copied from db-import script
  20. (defn- find-block-by-content [db content]
  21. (if (instance? js/RegExp content)
  22. (->> content
  23. (d/q '[:find [(pull ?b [*]) ...]
  24. :in $ ?pattern
  25. :where [?b :block/content ?content] [(re-find ?pattern ?content)]]
  26. db)
  27. first)
  28. (->> content
  29. (d/q '[:find [(pull ?b [*]) ...]
  30. :in $ ?content
  31. :where [?b :block/content ?content]]
  32. db)
  33. first)))
  34. (defn- find-page-by-name [db name]
  35. (->> name
  36. (d/q '[:find [(pull ?b [*]) ...]
  37. :in $ ?name
  38. :where [?b :block/original-name ?name]]
  39. db)
  40. first))
  41. (defn- build-graph-files
  42. "Given a file graph directory, return all files including assets and adds relative paths
  43. on ::rpath since paths are absolute by default and exporter needs relative paths for
  44. some operations"
  45. [dir*]
  46. (let [dir (node-path/resolve dir*)]
  47. (->> (common-graph/get-files dir)
  48. (concat (when (fs/existsSync (node-path/join dir* "assets"))
  49. (common-graph/readdir (node-path/join dir* "assets"))))
  50. (mapv #(hash-map :path %
  51. ::rpath (node-path/relative dir* %))))))
  52. (defn- <read-file
  53. [file]
  54. (p/let [s (fs/readFileSync (:path file))]
  55. (str s)))
  56. (defn- notify-user [m]
  57. (println (:msg m))
  58. (println "Ex-data:" (pr-str (dissoc (:ex-data m) :error)))
  59. (println "Stacktrace:")
  60. (if-let [stack (some-> (get-in m [:ex-data :error]) ex-data :sci.impl/callstack deref)]
  61. (println (string/join
  62. "\n"
  63. (map
  64. #(str (:file %)
  65. (when (:line %) (str ":" (:line %)))
  66. (when (:sci.impl/f-meta %)
  67. (str " calls #'" (get-in % [:sci.impl/f-meta :ns]) "/" (get-in % [:sci.impl/f-meta :name]))))
  68. (reverse stack))))
  69. (println (some-> (get-in m [:ex-data :error]) .-stack))))
  70. (def default-export-options
  71. {;; common options
  72. :rpath-key ::rpath
  73. :notify-user notify-user
  74. :<read-file <read-file
  75. ;; :set-ui-state prn
  76. ;; config file options
  77. ;; TODO: Add actual default
  78. :default-config {}})
  79. ;; Copied from db-import script and tweaked for an in-memory import
  80. (defn- import-file-graph-to-db
  81. "Import a file graph dir just like UI does. However, unlike the UI the
  82. exporter receives file maps containing keys :path and ::rpath since :path
  83. are full paths"
  84. [file-graph-dir conn {:keys [assets] :as options}]
  85. (let [*files (build-graph-files file-graph-dir)
  86. config-file (first (filter #(string/ends-with? (:path %) "logseq/config.edn") *files))
  87. _ (assert config-file "No 'logseq/config.edn' found for file graph dir")
  88. options' (-> (merge options
  89. default-export-options
  90. ;; asset file options
  91. {:<copy-asset #(swap! assets conj %)})
  92. (dissoc :assets))]
  93. (gp-exporter/export-file-graph conn conn config-file *files options')))
  94. (defn- import-files-to-db
  95. "Import specific doc files for dev purposes"
  96. [files conn options]
  97. (let [doc-options (gp-exporter/build-doc-options {:macros {}} (merge options default-export-options))
  98. files' (mapv #(hash-map :path %) files)]
  99. (gp-exporter/export-doc-files conn files' <read-file doc-options)))
  100. (defn- readable-properties
  101. [db query-ent]
  102. (->> (db-property/properties query-ent)
  103. (map (fn [[k v]]
  104. [k
  105. (if-let [built-in-type (get-in db-property/built-in-properties [k :schema :type])]
  106. (if (= :block/tags k)
  107. (mapv #(:db/ident (d/entity db (:db/id %))) v)
  108. (if (db-property-type/ref-property-types built-in-type)
  109. (db-property/ref->property-value-contents db v)
  110. v))
  111. (db-property/ref->property-value-contents db v))]))
  112. (into {})))
  113. ;; Tests
  114. ;; =====
  115. (deftest-async export-basic-graph
  116. ;; This graph will contain basic examples of different features to import
  117. (p/let [file-graph-dir "test/resources/exporter-test-graph"
  118. conn (d/create-conn db-schema/schema-for-db-based-graph)
  119. _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
  120. assets (atom [])
  121. _ (import-file-graph-to-db file-graph-dir conn {:assets assets})]
  122. (is (nil? (:errors (db-validate/validate-db! @conn)))
  123. "Created graph has no validation errors")
  124. (testing "logseq files"
  125. (is (= ".foo {}\n"
  126. (ffirst (d/q '[:find ?content :where [?b :file/path "logseq/custom.css"] [?b :file/content ?content]] @conn))))
  127. (is (= "logseq.api.show_msg('hello good sir!');\n"
  128. (ffirst (d/q '[:find ?content :where [?b :file/path "logseq/custom.js"] [?b :file/content ?content]] @conn)))))
  129. (testing "graph wide counts"
  130. ;; Includes 2 journals as property values for :logseq.task/deadline
  131. (is (= 8 (count (d/q '[:find ?b :where [?b :block/type "journal"]] @conn))))
  132. ;; Count includes Contents and page references
  133. (is (= 6
  134. (count (d/q '[:find (pull ?b [*]) :where [?b :block/original-name ?name] (not [?b :block/type])] @conn))))
  135. (is (= 1 (count @assets))))
  136. (testing "user properties"
  137. (is (= #{{:db/ident :user.property/prop-bool :block/schema {:type :checkbox}}
  138. {:db/ident :user.property/prop-string :block/schema {:type :default}}
  139. {:db/ident :user.property/prop-num :block/schema {:type :number}}
  140. {:db/ident :user.property/prop-num2 :block/schema {:type :number}}}
  141. (->> @conn
  142. (d/q '[:find [(pull ?b [:db/ident :block/schema]) ...]
  143. :where [?b :block/type "property"]])
  144. (remove #(db-malli-schema/internal-ident? (:db/ident %)))
  145. set))
  146. "Properties defined correctly")
  147. (is (= {:user.property/prop-bool true
  148. :user.property/prop-num 5
  149. :user.property/prop-string "woot"}
  150. (update-vals (db-property/properties (find-block-by-content @conn "b1"))
  151. #(db-property/ref->property-value-content @conn %)))
  152. "Basic block has correct properties")
  153. (is (= #{"prop-num" "prop-string" "prop-bool"}
  154. (->> (d/entity @conn (:db/id (find-block-by-content @conn "b1")))
  155. :block/refs
  156. (map :block/original-name)
  157. set))
  158. "Block with properties has correct refs")
  159. (is (= {:user.property/prop-num2 10}
  160. (readable-properties @conn (find-page-by-name @conn "new page")))
  161. "New page has correct properties")
  162. (is (= {:user.property/prop-bool true
  163. :user.property/prop-num 5
  164. :user.property/prop-string "yeehaw"}
  165. (readable-properties @conn (find-page-by-name @conn "some page")))
  166. "Existing page has correct properties"))
  167. (testing "built-in properties"
  168. (is (= {:logseq.task/deadline "Nov 26th, 2022"}
  169. (readable-properties @conn (find-block-by-content @conn "only deadline")))
  170. "deadline block has correct journal as property value")
  171. (is (= {:logseq.task/deadline "Nov 25th, 2022"}
  172. (readable-properties @conn (find-block-by-content @conn "only scheduled")))
  173. "scheduled block converted to correct deadline")
  174. (is (= {:logseq.task/priority "High"}
  175. (readable-properties @conn (find-block-by-content @conn "high priority")))
  176. "priority block has correct property")
  177. (is (= {:logseq.task/status "Doing" :logseq.task/priority "Medium" :block/tags [:logseq.class/task]}
  178. (readable-properties @conn (find-block-by-content @conn "status test")))
  179. "status block has correct task properties and class")
  180. (is (= #{:logseq.task/status :block/tags}
  181. (set (keys (readable-properties @conn (find-block-by-content @conn "old todo block")))))
  182. "old task properties are ignored")
  183. (is (= {:logseq.property/query-sort-by :user.property/prop-num
  184. :logseq.property/query-properties [:block :page :user.property/prop-string :user.property/prop-num]
  185. :logseq.property/query-table true}
  186. (readable-properties @conn (find-block-by-content @conn "{{query (property :prop-string)}}")))
  187. "query block has correct query properties"))
  188. (testing "tags without tag options"
  189. (let [block (find-block-by-content @conn #"Inception")
  190. tag-page (find-page-by-name @conn "Movie")
  191. tagged-page (find-page-by-name @conn "Interstellar")]
  192. (is (string/starts-with? (str (:block/content block)) "Inception [[")
  193. "tagged block tag converts tag to page ref")
  194. (is (= [(:db/id tag-page)] (map :db/id (:block/refs block)))
  195. "tagged block has correct refs")
  196. (is (and tag-page (not (:block/type tag-page)))
  197. "tag page is not a class")
  198. (is (= {:logseq.property/page-tags #{"Movie"}}
  199. (readable-properties @conn tagged-page))
  200. "tagged page has tags imported as page-tags property by default")))))
  201. (deftest-async export-file-with-tag-classes-option
  202. (p/let [file-graph-dir "test/resources/exporter-test-graph"
  203. files (mapv #(node-path/join file-graph-dir %) ["journals/2024_02_07.md" "pages/Interstellar.md"])
  204. conn (d/create-conn db-schema/schema-for-db-based-graph)
  205. _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
  206. _ (import-files-to-db files conn {:tag-classes ["movie"]})]
  207. (let [block (find-block-by-content @conn #"Inception")
  208. tag-page (find-page-by-name @conn "Movie")
  209. another-tag-page (find-page-by-name @conn "p0")]
  210. (is (= (:block/content block) "Inception")
  211. "tagged block with configured tag strips tag from content")
  212. (is (= ["class"] (:block/type tag-page))
  213. "configured tag page in :tag-classes is a class")
  214. (is (and another-tag-page (not (:block/type another-tag-page)))
  215. "unconfigured tag page is not a class")
  216. (is (= {:block/tags [:user.class/Movie]}
  217. (readable-properties @conn (find-page-by-name @conn "Interstellar")))
  218. "tagged page has configured tag imported as a class"))))