exporter_test.cljs 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. (ns ^:node-only logseq.graph-parser.exporter-test
  2. (:require ["fs" :as fs]
  3. ["path" :as node-path]
  4. [cljs.test :refer [testing is]]
  5. [clojure.set :as set]
  6. [clojure.string :as string]
  7. [datascript.core :as d]
  8. [logseq.common.config :as common-config]
  9. [logseq.common.graph :as common-graph]
  10. [logseq.common.util.date-time :as date-time-util]
  11. [logseq.db :as ldb]
  12. [logseq.db.common.entity-plus :as entity-plus]
  13. [logseq.db.frontend.content :as db-content]
  14. [logseq.db.frontend.malli-schema :as db-malli-schema]
  15. [logseq.db.frontend.rules :as rules]
  16. [logseq.db.frontend.validate :as db-validate]
  17. [logseq.db.test.helper :as db-test]
  18. [logseq.graph-parser.block :as gp-block]
  19. [logseq.graph-parser.exporter :as gp-exporter]
  20. [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper]
  21. [logseq.graph-parser.test.helper :as test-helper :include-macros true :refer [deftest-async]]
  22. [logseq.outliner.db-pipeline :as db-pipeline]
  23. [promesa.core :as p]
  24. [logseq.db.frontend.asset :as db-asset]))
  25. ;; Helpers
  26. ;; =======
  27. ;; some have been copied from db-import script
  28. (defn- extract-rules
  29. [rules]
  30. (rules/extract-rules rules/db-query-dsl-rules
  31. rules
  32. {:deps rules/rules-dependencies}))
  33. (defn- find-block-by-property [db property]
  34. (d/q '[:find [?b ...]
  35. :in $ ?prop %
  36. :where (has-property ?b ?prop)]
  37. db property (extract-rules [:has-property])))
  38. (defn- find-block-by-property-value [db property property-value]
  39. (->> (d/q '[:find [?b ...]
  40. :in $ ?prop ?prop-value %
  41. :where (property ?b ?prop ?prop-value)]
  42. db property property-value (extract-rules [:property]))
  43. first
  44. (d/entity db)))
  45. (defn- build-graph-files
  46. "Given a file graph directory, return all files including assets and adds relative paths
  47. on ::rpath since paths are absolute by default and exporter needs relative paths for
  48. some operations"
  49. [dir*]
  50. (let [dir (node-path/resolve dir*)]
  51. (->> (common-graph/get-files dir)
  52. (concat (when (fs/existsSync (node-path/join dir* "assets"))
  53. (common-graph/readdir (node-path/join dir* "assets"))))
  54. (mapv #(hash-map :path %
  55. ::rpath (node-path/relative dir* %))))))
  56. (defn- <read-file
  57. [file]
  58. (p/let [s (fs/readFileSync (:path file))]
  59. (str s)))
  60. (defn- notify-user [m]
  61. (println (:msg m))
  62. (when (:ex-data m)
  63. (println "Ex-data:" (pr-str (merge (dissoc (:ex-data m) :error)
  64. (when-let [err (get-in m [:ex-data :error])]
  65. {:original-error (ex-data (.-cause err))}))))
  66. (println "Stacktrace:")
  67. (if-let [stack (some-> (get-in m [:ex-data :error]) ex-data :sci.impl/callstack deref)]
  68. (println (string/join
  69. "\n"
  70. (map
  71. #(str (:file %)
  72. (when (:line %) (str ":" (:line %)))
  73. (when (:sci.impl/f-meta %)
  74. (str " calls #'" (get-in % [:sci.impl/f-meta :ns]) "/" (get-in % [:sci.impl/f-meta :name]))))
  75. (reverse stack))))
  76. (println (some-> (get-in m [:ex-data :error]) .-stack))))
  77. (when (= :error (:level m))
  78. (js/process.exit 1)))
  79. (def default-export-options
  80. {;; common options
  81. :rpath-key ::rpath
  82. :notify-user notify-user
  83. :<read-file <read-file
  84. ;; :set-ui-state prn
  85. ;; config file options
  86. ;; TODO: Add actual default
  87. :default-config {}})
  88. ;; Copied from db-import
  89. (defn- <read-asset-file [file assets]
  90. (p/let [buffer (fs/readFileSync (:path file))
  91. checksum (db-asset/<get-file-array-buffer-checksum buffer)]
  92. (swap! assets assoc
  93. (node-path/basename (:path file))
  94. {:size (.-length buffer)
  95. :checksum checksum
  96. :type (db-asset/asset-path->type (:path file))
  97. :path (:path file)})))
  98. ;; Copied from db-import script and tweaked for an in-memory import
  99. (defn- import-file-graph-to-db
  100. "Import a file graph dir just like UI does. However, unlike the UI the
  101. exporter receives file maps containing keys :path and ::rpath since :path
  102. are full paths"
  103. [file-graph-dir conn {:keys [assets] :or {assets (atom [])} :as options}]
  104. (let [*files (build-graph-files file-graph-dir)
  105. config-file (first (filter #(string/ends-with? (:path %) "logseq/config.edn") *files))
  106. _ (assert config-file "No 'logseq/config.edn' found for file graph dir")
  107. options' (merge default-export-options
  108. {:user-options (merge {:convert-all-tags? false} (dissoc options :assets :verbose))
  109. ;; asset file options
  110. :<read-asset <read-asset-file
  111. :<copy-asset #(swap! assets conj %)}
  112. (select-keys options [:verbose]))]
  113. (gp-exporter/export-file-graph conn conn config-file *files options')))
  114. (defn- import-files-to-db
  115. "Import specific doc files for dev purposes"
  116. [files conn options]
  117. (reset! gp-block/*export-to-db-graph? true)
  118. (-> (p/let [doc-options (gp-exporter/build-doc-options (merge {:macros {} :file/name-format :triple-lowbar}
  119. (:user-config options))
  120. (merge default-export-options
  121. {:user-options (merge {:convert-all-tags? false}
  122. (dissoc options :user-config :verbose))}
  123. (select-keys options [:verbose])))
  124. files' (mapv #(hash-map :path %) files)
  125. _ (gp-exporter/export-doc-files conn files' <read-file doc-options)]
  126. {:import-state (:import-state doc-options)})
  127. (p/finally (fn [_]
  128. (reset! gp-block/*export-to-db-graph? false)))))
  129. ;; Tests
  130. ;; =====
  131. (deftest-async ^:integration export-docs-graph-with-convert-all-tags
  132. (p/let [file-graph-dir "test/resources/docs-0.10.12"
  133. start-time (cljs.core/system-time)
  134. _ (docs-graph-helper/clone-docs-repo-if-not-exists file-graph-dir "v0.10.12")
  135. conn (db-test/create-conn)
  136. _ (db-pipeline/add-listener conn)
  137. {:keys [import-state]}
  138. (import-file-graph-to-db file-graph-dir conn {:convert-all-tags? true})
  139. end-time (cljs.core/system-time)]
  140. ;; Add multiplicative factor for CI as it runs about twice as slow
  141. (let [max-time (-> 25 (* (if js/process.env.CI 2 1)))]
  142. (is (< (-> end-time (- start-time) (/ 1000)) max-time)
  143. (str "Importing large graph takes less than " max-time "s")))
  144. (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
  145. "Created graph has no validation errors")
  146. (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties")
  147. (is (= []
  148. (->> (d/q '[:find (pull ?b [:block/title {:block/tags [:db/ident]}])
  149. :where [?b :block/tags :logseq.class/Tag]]
  150. @conn)
  151. (map first)
  152. (remove #(= [{:db/ident :logseq.class/Tag}] (:block/tags %)))))
  153. "All classes only have :logseq.class/Tag as their tag (and don't have Page)")))
  154. (deftest-async ^:focus export-basic-graph-with-convert-all-tags
  155. ;; This graph will contain basic examples of different features to import
  156. (p/let [file-graph-dir "test/resources/exporter-test-graph"
  157. conn (db-test/create-conn)
  158. ;; Calculate refs and path-refs like frontend
  159. _ (db-pipeline/add-listener conn)
  160. assets (atom [])
  161. {:keys [import-state]} (import-file-graph-to-db file-graph-dir conn {:assets assets :convert-all-tags? true})]
  162. (testing "whole graph"
  163. (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
  164. "Created graph has no validation errors")
  165. ;; Counts
  166. ;; Includes journals as property values e.g. :logseq.property/deadline
  167. (is (= 25 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
  168. (is (= 1 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Asset]] @conn))))
  169. (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn))))
  170. (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Query]] @conn))))
  171. (is (= 2 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Card]] @conn))))
  172. ;; Properties and tags aren't included in this count as they aren't a Page
  173. (is (= 10
  174. (->> (d/q '[:find [?b ...]
  175. :where
  176. [?b :block/title]
  177. [_ :block/page ?b]
  178. (not [?b :logseq.property/built-in?])] @conn)
  179. (map #(d/entity @conn %))
  180. (filter ldb/internal-page?)
  181. #_(map #(select-keys % [:block/title :block/tags]))
  182. count))
  183. "Correct number of pages with block content")
  184. (is (= 13 (->> @conn
  185. (d/q '[:find [?ident ...]
  186. :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
  187. count))
  188. "Correct number of user classes")
  189. (is (= 4 (count (d/datoms @conn :avet :block/tags :logseq.class/Whiteboard))))
  190. (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties")
  191. (is (= 1 (count @(:ignored-files import-state))) "Ignore .edn for now")
  192. (is (= 1 (count @assets))))
  193. (testing "logseq files"
  194. (is (= ".foo {}\n"
  195. (ffirst (d/q '[:find ?content :where [?b :file/path "logseq/custom.css"] [?b :file/content ?content]] @conn))))
  196. (is (= "logseq.api.show_msg('hello good sir!');\n"
  197. (ffirst (d/q '[:find ?content :where [?b :file/path "logseq/custom.js"] [?b :file/content ?content]] @conn)))))
  198. (testing "favorites"
  199. (is (= #{"Interstellar" "some page"}
  200. (->>
  201. (ldb/get-page-blocks @conn
  202. (:db/id (ldb/get-page @conn common-config/favorites-page-name))
  203. {:pull-keys '[* {:block/link [:block/title]}]})
  204. (map #(get-in % [:block/link :block/title]))
  205. set))))
  206. (testing "user properties"
  207. (is (= 19
  208. (->> @conn
  209. (d/q '[:find [(pull ?b [:db/ident]) ...]
  210. :where [?b :block/tags :logseq.class/Property]])
  211. (remove #(db-malli-schema/internal-ident? (:db/ident %)))
  212. count))
  213. "Correct number of user properties")
  214. (is (= #{{:db/ident :user.property/prop-bool :logseq.property/type :checkbox}
  215. {:db/ident :user.property/prop-string :logseq.property/type :default}
  216. {:db/ident :user.property/prop-num :logseq.property/type :number}
  217. {:db/ident :user.property/sameas :logseq.property/type :url}
  218. {:db/ident :user.property/rangeincludes :logseq.property/type :node}
  219. {:db/ident :user.property/startedat :logseq.property/type :date}}
  220. (->> @conn
  221. (d/q '[:find [(pull ?b [:db/ident :logseq.property/type]) ...]
  222. :where [?b :block/tags :logseq.class/Property]])
  223. (filter #(contains? #{:prop-bool :prop-string :prop-num :rangeincludes :sameas :startedat}
  224. (keyword (name (:db/ident %)))))
  225. set))
  226. "Main property types have correct inferred :type")
  227. (is (= :default
  228. (:logseq.property/type (d/entity @conn :user.property/description)))
  229. "Property value consisting of text and refs is inferred as :default")
  230. (is (= :url
  231. (:logseq.property/type (d/entity @conn :user.property/url)))
  232. "Property value with a macro correctly inferred as :url")
  233. (is (= {:user.property/prop-bool true
  234. :user.property/prop-num 5
  235. :user.property/prop-string "woot"}
  236. (db-test/readable-properties (db-test/find-block-by-content @conn "b1")))
  237. "Basic block has correct properties")
  238. (is (= #{"prop-num" "prop-string" "prop-bool"}
  239. (->> (db-test/find-block-by-content @conn "b1")
  240. :block/refs
  241. (map :block/title)
  242. set))
  243. "Block with properties has correct refs")
  244. (is (= {:user.property/prop-num2 10
  245. :block/tags [:logseq.class/Page]}
  246. (db-test/readable-properties (db-test/find-page-by-title @conn "new page")))
  247. "New page has correct properties")
  248. (is (= {:user.property/prop-bool true
  249. :user.property/prop-num 5
  250. :user.property/prop-string "yeehaw"
  251. :block/tags [:logseq.class/Page :user.class/SomeNamespace]}
  252. (db-test/readable-properties (db-test/find-page-by-title @conn "some page")))
  253. "Existing page has correct properties")
  254. (is (= {:user.property/rating 5.5}
  255. (db-test/readable-properties (db-test/find-block-by-content @conn ":rating float")))
  256. "Block with float property imports as a float")
  257. (is (= []
  258. (->> (d/q '[:find (pull ?b [:block/title {:block/tags [:db/ident]}])
  259. :where [?b :block/tags :logseq.class/Property]]
  260. @conn)
  261. (map first)
  262. (remove #(= [{:db/ident :logseq.class/Property}] (:block/tags %)))))
  263. "All properties only have :logseq.class/Property as their tag (and don't have Page)"))
  264. (testing "built-in properties"
  265. (is (= [(:db/id (db-test/find-block-by-content @conn "original block"))]
  266. (mapv :db/id (:block/refs (db-test/find-block-by-content @conn #"ref to"))))
  267. "block with a block-ref has correct :block/refs")
  268. (let [b (db-test/find-block-by-content @conn #"MEETING TITLE")]
  269. (is (= {}
  270. (and b (db-test/readable-properties b)))
  271. ":template properties are ignored to not invalidate its property types"))
  272. (is (= 20221126
  273. (-> (db-test/readable-properties (db-test/find-block-by-content @conn "only deadline"))
  274. :logseq.property/deadline
  275. date-time-util/ms->journal-day))
  276. "deadline block has correct journal as property value")
  277. (is (= 20221125
  278. (-> (db-test/readable-properties (db-test/find-block-by-content @conn "only scheduled"))
  279. :logseq.property/scheduled
  280. date-time-util/ms->journal-day))
  281. "scheduled block converted to correct deadline")
  282. (is (= 1 (count (d/q '[:find [(pull ?b [*]) ...]
  283. :in $ ?content
  284. :where [?b :block/title ?content]]
  285. @conn "Apr 1st, 2024")))
  286. "Only one journal page exists when deadline is on same day as journal")
  287. (is (= {:logseq.property/priority :logseq.property/priority.high}
  288. (db-test/readable-properties (db-test/find-block-by-content @conn "high priority")))
  289. "priority block has correct property")
  290. (is (= {:logseq.property/status :logseq.property/status.doing
  291. :logseq.property/priority :logseq.property/priority.medium
  292. :block/tags [:logseq.class/Task]}
  293. (db-test/readable-properties (db-test/find-block-by-content @conn "status test")))
  294. "status block has correct task properties and class")
  295. (is (= #{:logseq.property/status :block/tags}
  296. (set (keys (db-test/readable-properties (db-test/find-block-by-content @conn "old todo block")))))
  297. "old task properties like 'todo' are ignored")
  298. (is (= {:logseq.property/order-list-type "number"}
  299. (db-test/readable-properties (db-test/find-block-by-content @conn "list one")))
  300. "numered block has correct property")
  301. (is (= #{"gpt"}
  302. (:block/alias (db-test/readable-properties (db-test/find-page-by-title @conn "chat-gpt"))))
  303. "alias set correctly")
  304. (is (= ["y"]
  305. (->> (d/q '[:find [?b ...] :where [?b :block/title "y"] [?b :logseq.property/parent]]
  306. @conn)
  307. first
  308. (d/entity @conn)
  309. :block/alias
  310. (map :block/title)))
  311. "alias set correctly on namespaced page")
  312. (is (= {:logseq.property.linked-references/includes #{"Oct 9th, 2024"}
  313. :logseq.property.linked-references/excludes #{"ref2"}}
  314. (select-keys (db-test/readable-properties (db-test/find-page-by-title @conn "chat-gpt"))
  315. [:logseq.property.linked-references/excludes :logseq.property.linked-references/includes]))
  316. "linked ref filters set correctly"))
  317. (testing "built-in classes and their properties"
  318. ;; Queries
  319. (is (= {:logseq.property.table/sorting [{:id :user.property/prop-num, :asc? false}]
  320. :logseq.property.view/type :logseq.property.view/type.table
  321. :logseq.property.table/ordered-columns [:block/title :user.property/prop-string :user.property/prop-num]
  322. :logseq.property/query "(property :prop-string)"
  323. :block/tags [:logseq.class/Query]}
  324. (db-test/readable-properties (find-block-by-property-value @conn :logseq.property/query "(property :prop-string)")))
  325. "simple query block has correct query properties")
  326. (is (= "For example, here's a query with title text:"
  327. (:block/title (db-test/find-block-by-content @conn #"query with title text")))
  328. "Text around a simple query block is set as a query's title")
  329. (is (= {:logseq.property.view/type :logseq.property.view/type.list
  330. :logseq.property/query "{:query (task todo doing)}"
  331. :block/tags [:logseq.class/Query]
  332. :logseq.property.table/ordered-columns [:block/title]}
  333. (db-test/readable-properties (db-test/find-block-by-content @conn #"tasks with")))
  334. "Advanced query has correct query properties")
  335. (is (= "tasks with todo and doing"
  336. (:block/title (db-test/find-block-by-content @conn #"tasks with")))
  337. "Advanced query has custom title migrated")
  338. ;; Cards
  339. (is (= {:block/tags [:logseq.class/Card]}
  340. (db-test/readable-properties (db-test/find-block-by-content @conn "card 1")))
  341. "None of the card properties are imported since they are deprecated")
  342. ;; Assets
  343. (is (= {:block/tags [:logseq.class/Asset]
  344. :logseq.property.asset/type "png"
  345. :logseq.property.asset/checksum "3d5e620cac62159d8196c118574bfea7a16e86fa86efd1c3fa15a00a0a08792d"
  346. :logseq.property.asset/size 753471}
  347. (db-test/readable-properties (db-test/find-block-by-content @conn "greg-popovich-thumbs-up_1704749687791_0")))))
  348. (testing "tags convert to classes"
  349. (is (= :user.class/Quotes___life
  350. (:db/ident (db-test/find-page-by-title @conn "life")))
  351. "Namespaced tag's ident has hierarchy to make it unique")
  352. (is (= [:logseq.class/Tag]
  353. (map :db/ident (:block/tags (db-test/find-page-by-title @conn "life"))))
  354. "When a class is used and referenced on the same page, there should only be one instance of it")
  355. (is (= [:user.class/Quotes___life]
  356. (mapv :db/ident (:block/tags (db-test/find-block-by-content @conn #"with namespace tag"))))
  357. "Block tagged with namespace tag is only associated with leaf child tag")
  358. (is (= []
  359. (->> (d/q '[:find (pull ?b [:block/title {:block/tags [:db/ident]}])
  360. :where [?b :block/tags :logseq.class/Tag]]
  361. @conn)
  362. (map first)
  363. (remove #(= [{:db/ident :logseq.class/Tag}] (:block/tags %)))))
  364. "All classes only have :logseq.class/Tag as their tag (and don't have Page)"))
  365. (testing "namespaces"
  366. (let [expand-children (fn expand-children [ent parent]
  367. (if-let [children (:logseq.property/_parent ent)]
  368. (cons {:parent (:block/title parent) :child (:block/title ent)}
  369. (mapcat #(expand-children % ent) children))
  370. [{:parent (:block/title parent) :child (:block/title ent)}]))]
  371. (is (= [{:parent "n1" :child "x"}
  372. {:parent "x" :child "z"}
  373. {:parent "x" :child "y"}]
  374. (rest (expand-children (db-test/find-page-by-title @conn "n1") nil)))
  375. "First namespace tests duplicate parent page name")
  376. (is (= [{:parent "n2" :child "x"}
  377. {:parent "x" :child "z"}
  378. {:parent "n2" :child "alias"}]
  379. (rest (expand-children (db-test/find-page-by-title @conn "n2") nil)))
  380. "First namespace tests duplicate child page name and built-in page name")))
  381. (testing "journal timestamps"
  382. (is (= (date-time-util/journal-day->ms 20240207)
  383. (:block/created-at (db-test/find-page-by-title @conn "Feb 7th, 2024")))
  384. "journal pages are created on their journal day")
  385. (is (= (date-time-util/journal-day->ms 20240207)
  386. (:block/created-at (db-test/find-block-by-content @conn #"Inception")))
  387. "journal blocks are created on their page's journal day"))
  388. (testing "db attributes"
  389. (is (= true
  390. (:block/collapsed? (db-test/find-block-by-content @conn "collapsed block")))
  391. "Collapsed blocks are imported"))
  392. (testing "property :type changes"
  393. (is (= :node
  394. (:logseq.property/type (d/entity @conn :user.property/finishedat)))
  395. ":date property to :node value changes to :node")
  396. (is (= :node
  397. (:logseq.property/type (d/entity @conn :user.property/participants)))
  398. ":node property to :date value remains :node")
  399. (is (= :default
  400. (:logseq.property/type (d/entity @conn :user.property/description)))
  401. ":default property to :node (or any non :default value) remains :default")
  402. (is (= "[[Jakob]]"
  403. (:user.property/description (db-test/readable-properties (db-test/find-block-by-content @conn #":default to :node"))))
  404. ":default to :node property saves :default property value default with full text")
  405. (testing "with changes to upstream/existing property value"
  406. (is (= :default
  407. (:logseq.property/type (d/entity @conn :user.property/duration)))
  408. ":number property to :default value changes to :default")
  409. (is (= "20"
  410. (:user.property/duration (db-test/readable-properties (db-test/find-block-by-content @conn "existing :number to :default"))))
  411. "existing :number property value correctly saved as :default")
  412. (is (= {:logseq.property/type :default :db/cardinality :db.cardinality/many}
  413. (select-keys (d/entity @conn :user.property/people) [:logseq.property/type :db/cardinality]))
  414. ":node property to :default value changes to :default and keeps existing cardinality")
  415. (is (= #{"[[Jakob]] [[Gabriel]]"}
  416. (:user.property/people (db-test/readable-properties (db-test/find-block-by-content @conn ":node people"))))
  417. "existing :node property value correctly saved as :default with full text")
  418. (is (= #{"[[Gabriel]] [[Jakob]]"}
  419. (:user.property/people (db-test/readable-properties (db-test/find-block-by-content @conn #"pending block for :node"))))
  420. "pending :node property value correctly saved as :default with full text")
  421. (is (some? (db-test/find-page-by-title @conn "Jakob"))
  422. "Previous :node property value still exists")
  423. (is (= 3 (count (find-block-by-property @conn :user.property/people)))
  424. "Converted property has correct number of property values")))
  425. (testing "imported concepts can have names of new-built concepts"
  426. (is (= #{:logseq.property/description :user.property/description}
  427. (set (d/q '[:find [?ident ...] :where [?b :db/ident ?ident] [?b :block/name "description"]] @conn)))
  428. "user description property is separate from built-in one")
  429. (is (= #{"Page" "Tag"}
  430. (set (d/q '[:find [?t-title ...] :where
  431. [?b :block/tags ?t]
  432. [?b :block/name "task"]
  433. [?t :block/title ?t-title]] @conn)))
  434. "user page is separate from built-in class"))
  435. (testing "multiline blocks"
  436. (is (= "|markdown| table|\n|some|thing|" (:block/title (db-test/find-block-by-content @conn #"markdown.*table"))))
  437. (is (= "multiline block\na 2nd\nand a 3rd" (:block/title (db-test/find-block-by-content @conn #"multiline block"))))
  438. (is (= "logbook block" (:block/title (db-test/find-block-by-content @conn #"logbook block")))))
  439. (testing ":block/refs and :block/path-refs"
  440. (let [page (db-test/find-page-by-title @conn "chat-gpt")]
  441. (is (set/subset?
  442. #{"type" "LargeLanguageModel"}
  443. (->> page :block/refs (map #(:block/title (d/entity @conn (:db/id %)))) set))
  444. "Page has correct property and property value :block/refs")
  445. (is (set/subset?
  446. #{"type" "LargeLanguageModel"}
  447. (->> page :block/path-refs (map #(:block/title (d/entity @conn (:db/id %)))) set))
  448. "Page has correct property and property value :block/path-refs"))
  449. (let [block (db-test/find-block-by-content @conn "old todo block")]
  450. (is (set/subset?
  451. #{:logseq.property/status :logseq.class/Task}
  452. (->> block
  453. :block/refs
  454. (map #(:db/ident (d/entity @conn (:db/id %))))
  455. set))
  456. "Block has correct task tag and property :block/refs")
  457. (is (set/subset?
  458. #{:logseq.property/status :logseq.class/Task}
  459. (->> block
  460. :block/path-refs
  461. (map #(:db/ident (d/entity @conn (:db/id %))))
  462. set))
  463. "Block has correct task tag and property :block/path-refs")))
  464. (testing "whiteboards"
  465. (let [block-with-props (db-test/find-block-by-content @conn #"block with props")]
  466. (is (= {:user.property/prop-num 10}
  467. (db-test/readable-properties block-with-props)))
  468. (is (= "block with props" (:block/title block-with-props)))))))
  469. (deftest-async export-basic-graph-with-convert-all-tags-option-disabled
  470. (p/let [file-graph-dir "test/resources/exporter-test-graph"
  471. conn (db-test/create-conn)
  472. {:keys [import-state]}
  473. (import-file-graph-to-db file-graph-dir conn {:convert-all-tags? false})]
  474. (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
  475. "Created graph has no validation errors")
  476. (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties")
  477. (is (= 0 (->> @conn
  478. (d/q '[:find [?ident ...]
  479. :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
  480. count))
  481. "Correct number of user classes")
  482. (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn))))
  483. (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Query]] @conn))))
  484. (is (= 2 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Card]] @conn))))
  485. (testing "replacing refs in :block/title when :remove-inline-tags? set"
  486. (is (= 2
  487. (->> (entity-plus/lookup-kv-then-entity
  488. (db-test/find-block-by-content @conn #"replace with same start string")
  489. :block/raw-title)
  490. (re-seq db-content/id-ref-pattern)
  491. distinct
  492. count))
  493. "A block with ref names that start with same string has 2 distinct refs")
  494. (is (= 1
  495. (->> (entity-plus/lookup-kv-then-entity
  496. (db-test/find-block-by-content @conn #"replace case insensitive")
  497. :block/raw-title)
  498. (re-seq db-content/id-ref-pattern)
  499. distinct
  500. count))
  501. "A block with different case of same ref names has 1 distinct ref"))
  502. (testing "tags convert to page, refs and page-tags"
  503. (let [block (db-test/find-block-by-content @conn #"Inception")
  504. tag-page (db-test/find-page-by-title @conn "Movie")
  505. tagged-page (db-test/find-page-by-title @conn "Interstellar")]
  506. (is (string/starts-with? (str (:block/title block)) "Inception [[")
  507. "tagged block tag converts tag to page ref")
  508. (is (= [(:db/id tag-page)] (map :db/id (:block/refs block)))
  509. "tagged block has correct refs")
  510. (is (and tag-page (not (ldb/class? tag-page)))
  511. "tag page is not a class")
  512. (is (= #{"Movie"}
  513. (:logseq.property/page-tags (db-test/readable-properties tagged-page)))
  514. "tagged page has existing page imported as a tag to page-tags")
  515. (is (= #{"LargeLanguageModel" "fun" "ai"}
  516. (:logseq.property/page-tags (db-test/readable-properties (db-test/find-page-by-title @conn "chat-gpt"))))
  517. "tagged page has new page and other pages marked with '#' and '[[]]` imported as tags to page-tags")))))
  518. (deftest-async export-files-with-tag-classes-option
  519. (p/let [file-graph-dir "test/resources/exporter-test-graph"
  520. files (mapv #(node-path/join file-graph-dir %) ["journals/2024_02_07.md" "pages/Interstellar.md"])
  521. conn (db-test/create-conn)
  522. _ (import-files-to-db files conn {:tag-classes ["movie"]})]
  523. (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
  524. "Created graph has no validation errors")
  525. (let [block (db-test/find-block-by-content @conn #"Inception")
  526. tag-page (db-test/find-page-by-title @conn "Movie")
  527. another-tag-page (db-test/find-page-by-title @conn "p0")]
  528. (is (= (:block/title block) "Inception")
  529. "tagged block with configured tag strips tag from content")
  530. (is (= [:user.class/Movie]
  531. (:block/tags (db-test/readable-properties block)))
  532. "tagged block has configured tag imported as a class")
  533. (is (= [:logseq.class/Tag] (mapv :db/ident (:block/tags tag-page)))
  534. "configured tag page in :tag-classes is a class")
  535. (is (and another-tag-page (not (ldb/class? another-tag-page)))
  536. "unconfigured tag page is not a class")
  537. (is (= {:block/tags [:logseq.class/Page :user.class/Movie]}
  538. (db-test/readable-properties (db-test/find-page-by-title @conn "Interstellar")))
  539. "tagged page has configured tag imported as a class"))))
  540. (deftest-async export-files-with-property-classes-option
  541. (p/let [file-graph-dir "test/resources/exporter-test-graph"
  542. files (mapv #(node-path/join file-graph-dir %)
  543. ["journals/2024_02_23.md" "pages/url.md" "pages/Whiteboard___Tool.md"
  544. "pages/Whiteboard___Arrow_head_toggle.md"])
  545. conn (db-test/create-conn)
  546. _ (import-files-to-db files conn {:property-classes ["type"]})
  547. _ (@#'gp-exporter/export-class-properties conn conn)]
  548. (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
  549. "Created graph has no validation errors")
  550. (is (= #{:user.class/Property :user.class/Movie :user.class/Class :user.class/Tool}
  551. (->> @conn
  552. (d/q '[:find [?ident ...]
  553. :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
  554. set))
  555. "All classes are correctly defined by :type")
  556. (is (= #{:user.property/url :user.property/sameas :user.property/rangeincludes}
  557. (->> (d/entity @conn :user.class/Property)
  558. :logseq.property.class/properties
  559. (map :db/ident)
  560. set))
  561. "Properties are correctly inferred for a class")
  562. (let [block (db-test/find-block-by-content @conn #"The Creator")
  563. tag-page (db-test/find-page-by-title @conn "Movie")]
  564. (is (= (:block/title block) "The Creator")
  565. "tagged block with configured tag strips tag from content")
  566. (is (= [:user.class/Movie]
  567. (:block/tags (db-test/readable-properties block)))
  568. "tagged block has configured tag imported as a class")
  569. (is (= (:user.property/testtagclass block) (:block/tags block))
  570. "tagged block can have another property that references the same class it is tagged with,
  571. without creating a duplicate class")
  572. (is (= [:logseq.class/Tag] (map :db/ident (:block/tags tag-page)))
  573. "configured tag page derived from :property-classes is a class")
  574. (is (nil? (db-test/find-page-by-title @conn "type"))
  575. "No page exists for configured property")
  576. (is (= #{:user.class/Property :logseq.class/Property}
  577. (set (:block/tags (db-test/readable-properties (db-test/find-page-by-title @conn "url")))))
  578. "tagged page has correct tags including one from option"))))
  579. (deftest-async export-files-with-remove-inline-tags
  580. (p/let [file-graph-dir "test/resources/exporter-test-graph"
  581. files (mapv #(node-path/join file-graph-dir %) ["journals/2024_02_07.md"])
  582. conn (db-test/create-conn)
  583. _ (import-files-to-db files conn {:remove-inline-tags? false :convert-all-tags? true})]
  584. (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
  585. "Created graph has no validation errors")
  586. (is (string/starts-with? (:block/title (db-test/find-block-by-content @conn #"Inception"))
  587. "Inception #Movie")
  588. "block with tag preserves inline tag")))
  589. (deftest-async export-files-with-ignored-properties
  590. (p/let [file-graph-dir "test/resources/exporter-test-graph"
  591. files (mapv #(node-path/join file-graph-dir %) ["ignored/icon-page.md"])
  592. conn (db-test/create-conn)
  593. {:keys [import-state]} (import-files-to-db files conn {})]
  594. (is (= 2
  595. (count (filter #(= :icon (:property %)) @(:ignored-properties import-state))))
  596. "icon properties are visibly ignored in order to not fail import")))
  597. (deftest-async export-files-with-property-parent-classes-option
  598. (p/let [file-graph-dir "test/resources/exporter-test-graph"
  599. files (mapv #(node-path/join file-graph-dir %) ["journals/2024_11_26.md"
  600. "pages/CreativeWork.md" "pages/Movie.md" "pages/type.md"
  601. "pages/Whiteboard___Tool.md" "pages/Whiteboard___Arrow_head_toggle.md"
  602. "pages/Property.md" "pages/url.md"])
  603. conn (db-test/create-conn)
  604. _ (import-files-to-db files conn {:property-parent-classes ["parent"]
  605. ;; Also add this option to trigger some edge cases with namespace pages
  606. :property-classes ["type"]})]
  607. (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
  608. "Created graph has no validation errors")
  609. (is (= #{:user.class/Movie :user.class/CreativeWork :user.class/Thing :user.class/Feature
  610. :user.class/Class :user.class/Tool :user.class/Whiteboard___Tool :user.class/Property}
  611. (->> @conn
  612. (d/q '[:find [?ident ...]
  613. :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
  614. set))
  615. "All classes are correctly defined by :type")
  616. (is (= "CreativeWork" (get-in (d/entity @conn :user.class/Movie) [:logseq.property/parent :block/title]))
  617. "Existing page correctly set as class parent")
  618. (is (= "Thing" (get-in (d/entity @conn :user.class/CreativeWork) [:logseq.property/parent :block/title]))
  619. "New page correctly set as class parent")))
  620. (deftest-async export-files-with-property-pages-disabled
  621. (p/let [file-graph-dir "test/resources/exporter-test-graph"
  622. ;; any page with properties
  623. files (mapv #(node-path/join file-graph-dir %) ["journals/2024_01_17.md"])
  624. conn (db-test/create-conn)
  625. _ (import-files-to-db files conn {:user-config {:property-pages/enabled? false
  626. :property-pages/excludelist #{:prop-string}}})]
  627. (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
  628. "Created graph has no validation errors")))
  629. (deftest-async export-config-file-sets-title-format
  630. (p/let [conn (db-test/create-conn)
  631. read-file #(p/do! "{:journal/page-title-format \"yyyy-MM-dd\"}")
  632. _ (gp-exporter/export-config-file conn "logseq/config.edn" read-file {})]
  633. (is (= "yyyy-MM-dd"
  634. (:logseq.property.journal/title-format (d/entity @conn :logseq.class/Journal)))
  635. "title format set correctly by config")))