exporter.cljs 99 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829
  1. (ns logseq.graph-parser.exporter
  2. "Exports a file graph to DB graph. Used by the File to DB graph importer and
  3. by nbb-logseq CLIs"
  4. (:require ["path" :as node-path]
  5. [borkdude.rewrite-edn :as rewrite]
  6. [cljs-time.coerce :as tc]
  7. [cljs.pprint]
  8. [clojure.edn :as edn]
  9. [clojure.set :as set]
  10. [clojure.string :as string]
  11. [clojure.walk :as walk]
  12. [datascript.core :as d]
  13. [logseq.common.config :as common-config]
  14. [logseq.common.path :as path]
  15. [logseq.common.util :as common-util]
  16. [logseq.common.util.date-time :as date-time-util]
  17. [logseq.common.util.macro :as macro-util]
  18. [logseq.common.util.namespace :as ns-util]
  19. [logseq.common.util.page-ref :as page-ref]
  20. [logseq.common.uuid :as common-uuid]
  21. [logseq.db :as ldb]
  22. [logseq.db.common.order :as db-order]
  23. [logseq.db.common.property-util :as db-property-util]
  24. [logseq.db.frontend.asset :as db-asset]
  25. [logseq.db.frontend.class :as db-class]
  26. [logseq.db.frontend.content :as db-content]
  27. [logseq.db.frontend.db-ident :as db-ident]
  28. [logseq.db.frontend.malli-schema :as db-malli-schema]
  29. [logseq.db.frontend.property :as db-property]
  30. [logseq.db.frontend.property.build :as db-property-build]
  31. [logseq.db.frontend.property.type :as db-property-type]
  32. [logseq.db.frontend.rules :as rules]
  33. [logseq.db.sqlite.util :as sqlite-util]
  34. [logseq.graph-parser.block :as gp-block]
  35. [logseq.graph-parser.extract :as extract]
  36. [logseq.graph-parser.property :as gp-property]
  37. [promesa.core :as p]))
  38. (defn- add-missing-timestamps
  39. "Add updated-at or created-at timestamps if they doesn't exist"
  40. [block]
  41. (let [updated-at (common-util/time-ms)
  42. block (cond-> block
  43. (nil? (:block/updated-at block))
  44. (assoc :block/updated-at updated-at)
  45. (nil? (:block/created-at block))
  46. (assoc :block/created-at updated-at))]
  47. block))
  48. (defn- build-new-namespace-page [block]
  49. (let [new-title (ns-util/get-last-part (:block/title block))]
  50. (merge block
  51. {;; DB graphs only have child name of namespace
  52. :block/title new-title
  53. :block/name (common-util/page-name-sanity-lc new-title)})))
  54. (defn- get-page-uuid [page-names-to-uuids page-name ex-data']
  55. (or (get @page-names-to-uuids (some-> (if (string/includes? (str page-name) "#")
  56. (string/lower-case (gp-block/sanitize-hashtag-name page-name))
  57. page-name)
  58. string/trimr))
  59. (throw (ex-info (str "No uuid found for page name " (pr-str page-name))
  60. (merge ex-data' {:page-name page-name
  61. :page-names (sort (keys @page-names-to-uuids))})))))
  62. (defn- replace-namespace-with-parent [block page-names-to-uuids parent-k]
  63. (if (:block/namespace block)
  64. (-> (dissoc block :block/namespace)
  65. (assoc parent-k
  66. {:block/uuid (get-page-uuid page-names-to-uuids
  67. (get-in block [:block/namespace :block/name])
  68. {:block block :block/namespace (:block/namespace block)})}))
  69. block))
  70. (defn- build-class-ident-name
  71. [class-name]
  72. (string/replace class-name "/" "___"))
  73. (defn- find-or-create-class
  74. ([db class-name all-idents]
  75. (find-or-create-class db class-name all-idents {}))
  76. ([db class-name all-idents class-block]
  77. (let [ident (keyword class-name)]
  78. (if-let [db-ident (get @all-idents ident)]
  79. {:db/ident db-ident}
  80. (let [m
  81. (if (:block/namespace class-block)
  82. ;; Give namespaced tags a unique ident so they don't conflict with other tags
  83. (-> (db-class/build-new-class db (merge {:block/title (build-class-ident-name class-name)}
  84. (select-keys class-block [:block/tags])))
  85. (merge {:block/title class-name
  86. :block/name (common-util/page-name-sanity-lc class-name)})
  87. (build-new-namespace-page))
  88. (db-class/build-new-class db
  89. (assoc {:block/title class-name
  90. :block/name (common-util/page-name-sanity-lc class-name)}
  91. :block/tags (:block/tags class-block))))]
  92. (swap! all-idents assoc ident (:db/ident m))
  93. (with-meta m {:new-class? true}))))))
  94. (defn- find-or-gen-class-uuid [page-names-to-uuids page-name db-ident & {:keys [temp-new-class?]}]
  95. (or (if temp-new-class?
  96. ;; First lookup by possible parent b/c page-names-to-uuids erroneously has the child name
  97. ;; and full name. To not guess at the parent name we would need to save all properties-from-classes
  98. (or (some #(when (string/ends-with? (key %) (str ns-util/parent-char page-name))
  99. (val %))
  100. @page-names-to-uuids)
  101. (get @page-names-to-uuids page-name))
  102. (get @page-names-to-uuids page-name))
  103. (let [new-uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)]
  104. (swap! page-names-to-uuids assoc page-name new-uuid)
  105. new-uuid)))
  106. (defn- convert-tag? [tag-name {:keys [convert-all-tags? tag-classes]}]
  107. (and (or convert-all-tags?
  108. (contains? tag-classes tag-name)
  109. ;; built-in tags that always convert
  110. (contains? #{"card"} tag-name))
  111. ;; Disallow tags as it breaks :block/tags
  112. (not (contains? #{"tags"} tag-name))))
  113. (defn- find-existing-class
  114. "Finds a class entity by unique name and parents and returns its :block/uuid if found.
  115. db is searched because there is no in-memory index only for created classes by unique name"
  116. [db {full-name :block/name block-ns :block/namespace}]
  117. (if block-ns
  118. (->> (d/q '[:find [?b ...]
  119. :in $ ?name
  120. :where [?b :block/uuid ?uuid] [?b :block/tags :logseq.class/Tag] [?b :block/name ?name]]
  121. db
  122. (ns-util/get-last-part full-name))
  123. (map #(d/entity db %))
  124. (some #(let [parents (->> (ldb/get-class-extends %)
  125. (remove (fn [e] (= :logseq.class/Root (:db/ident e))))
  126. vec)]
  127. (when (= full-name (string/join ns-util/namespace-char (map :block/name (conj parents %))))
  128. (:block/uuid %)))))
  129. (first
  130. (d/q '[:find [?uuid ...]
  131. :in $ ?name
  132. :where [?b :block/uuid ?uuid] [?b :block/tags :logseq.class/Tag] [?b :block/name ?name]]
  133. db
  134. full-name))))
  135. (defn- convert-tag-to-class
  136. "Converts a tag block with class or returns nil if this tag should be removed
  137. because it has been moved"
  138. [db tag-block {:keys [page-names-to-uuids classes-tx]} user-options all-idents]
  139. (if-let [new-class (:block.temp/new-class tag-block)]
  140. (let [class-m (find-or-create-class db new-class all-idents)
  141. class-m' (merge class-m
  142. {:block/uuid
  143. (find-or-gen-class-uuid page-names-to-uuids (common-util/page-name-sanity-lc new-class) (:db/ident class-m) {:temp-new-class? true})})]
  144. (when (:new-class? (meta class-m)) (swap! classes-tx conj class-m'))
  145. (assert (:block/uuid class-m') "Class must have a :block/uuid")
  146. [:block/uuid (:block/uuid class-m')])
  147. (when (convert-tag? (:block/name tag-block) user-options)
  148. (let [existing-tag-uuid (find-existing-class db tag-block)
  149. internal-tag-conflict? (contains? #{"tag" "property" "page" "journal" "asset"} (:block/name tag-block))]
  150. (cond
  151. ;; Don't overwrite internal tags
  152. (and existing-tag-uuid (not internal-tag-conflict?))
  153. [:block/uuid existing-tag-uuid]
  154. :else
  155. ;; Creates or updates page within same tx
  156. (let [class-m (find-or-create-class db (:block/title tag-block) all-idents tag-block)
  157. class-m' (-> (merge tag-block class-m
  158. (if internal-tag-conflict?
  159. {:block/uuid (common-uuid/gen-uuid :db-ident-block-uuid (:db/ident class-m))}
  160. (when-not (:block/uuid tag-block)
  161. (let [id (find-or-gen-class-uuid page-names-to-uuids (:block/name tag-block) (:db/ident class-m))]
  162. {:block/uuid id}))))
  163. ;; override with imported timestamps
  164. (dissoc :block/created-at :block/updated-at)
  165. (merge (add-missing-timestamps
  166. (select-keys tag-block [:block/created-at :block/updated-at])))
  167. (replace-namespace-with-parent page-names-to-uuids :logseq.property.class/extends))]
  168. (when (:new-class? (meta class-m)) (swap! classes-tx conj class-m'))
  169. (assert (:block/uuid class-m') "Class must have a :block/uuid")
  170. [:block/uuid (:block/uuid class-m')]))))))
  171. (defn- logseq-class-ident?
  172. [k]
  173. (and (qualified-keyword? k) (db-class/logseq-class? k)))
  174. (defn- convert-tags-to-classes
  175. "Handles converting tags to classes and any post processing of it e.g.
  176. cleaning :block/tags when a block is tagged with a namespace page"
  177. [tags db per-file-state user-options all-idents]
  178. ;; vec needed is needed so that tags are built in order
  179. (let [tags' (vec (keep #(if (logseq-class-ident? %)
  180. %
  181. (convert-tag-to-class db % per-file-state user-options all-idents))
  182. tags))]
  183. ;; Only associate leaf child tag with block as other tags are only used to define tag parents.
  184. ;; This assumes that extract/extract returns :block/tags with their leaf child first and then its parents
  185. (if-let [child-tag (and (some :block/namespace tags) (first tags'))]
  186. [child-tag]
  187. tags')))
  188. (defn- update-page-tags
  189. [block db user-options per-file-state all-idents]
  190. (if (seq (:block/tags block))
  191. (let [page-tags (->> (:block/tags block)
  192. (remove #(or (:block.temp/new-class %)
  193. (convert-tag? (:block/name %) user-options)
  194. ;; Ignore new class tags from extract e.g. :logseq.class/Journal
  195. (logseq-class-ident? %)))
  196. (map #(vector :block/uuid (get-page-uuid (:page-names-to-uuids per-file-state) (:block/name %) {:block %})))
  197. set)]
  198. (cond-> block
  199. true
  200. (update :block/tags convert-tags-to-classes db per-file-state user-options all-idents)
  201. true
  202. (update :block/tags (fn [tags]
  203. (cond-> (set tags)
  204. ;; ensure pages at least have a Page
  205. true
  206. (conj :logseq.class/Page)
  207. ;; Remove Page if another Page-like class is already present
  208. (seq (set/intersection (disj (set tags) :logseq.class/Page)
  209. db-class/page-classes))
  210. (disj :logseq.class/Page))))
  211. (seq page-tags)
  212. (merge {:logseq.property/page-tags page-tags})))
  213. block))
  214. (defn- add-uuid-to-page-map [m page-names-to-uuids]
  215. (assoc m :block/uuid (get-page-uuid page-names-to-uuids (:block/name m) {:block m})))
  216. (defn- content-without-tags-ignore-case
  217. "Ignore case because tags in content can have any case and still have a valid ref"
  218. [content tags]
  219. (->
  220. (reduce
  221. (fn [content tag]
  222. (-> content
  223. (common-util/replace-ignore-case (str "#" tag) "")
  224. (common-util/replace-ignore-case (str "#" page-ref/left-brackets tag page-ref/right-brackets) "")))
  225. content
  226. (sort > tags))
  227. (string/trim)))
  228. (defn- update-block-tags
  229. [block db {:keys [remove-inline-tags?] :as user-options} per-file-state all-idents]
  230. (let [block'
  231. (if (seq (:block/tags block))
  232. (let [original-tags (remove #(or (:block.temp/new-class %)
  233. ;; Filter out new classes already set on a block e.g. :logseq.class/Query
  234. (logseq-class-ident? %))
  235. (:block/tags block))
  236. convert-tag?' #(convert-tag? (:block/name %) user-options)]
  237. (cond-> block
  238. remove-inline-tags?
  239. (update :block/title
  240. content-without-tags-ignore-case
  241. (->> original-tags
  242. (filter convert-tag?')
  243. (map :block/title)))
  244. true
  245. (update :block/title
  246. db-content/replace-tags-with-id-refs
  247. (->> original-tags
  248. (remove convert-tag?')
  249. (map #(add-uuid-to-page-map % (:page-names-to-uuids per-file-state)))))
  250. true
  251. (update :block/tags convert-tags-to-classes db per-file-state user-options all-idents)))
  252. block)]
  253. block'))
  254. (defn- update-block-marker
  255. "If a block has a marker, convert it to a task object"
  256. [block {:keys [log-fn]}]
  257. (if-let [marker (:block/marker block)]
  258. (let [old-to-new {"TODO" :logseq.property/status.todo
  259. "LATER" :logseq.property/status.todo
  260. "IN-PROGRESS" :logseq.property/status.doing
  261. "NOW" :logseq.property/status.doing
  262. "DOING" :logseq.property/status.doing
  263. "DONE" :logseq.property/status.done
  264. "WAIT" :logseq.property/status.backlog
  265. "WAITING" :logseq.property/status.backlog
  266. "CANCELED" :logseq.property/status.canceled
  267. "CANCELLED" :logseq.property/status.canceled}
  268. status-ident (or (old-to-new marker)
  269. (do
  270. (log-fn :invalid-todo (str (pr-str marker) " is not a valid marker so setting it to TODO"))
  271. :logseq.property/status.todo))]
  272. (-> block
  273. (assoc :logseq.property/status status-ident)
  274. (update :block/title string/replace-first (re-pattern (str marker "\\s*")) "")
  275. (update :block/tags (fnil conj []) :logseq.class/Task)
  276. (dissoc :block/marker)))
  277. block))
  278. (defn- update-block-priority
  279. [block {:keys [log-fn]}]
  280. (if-let [priority (:block/priority block)]
  281. (let [old-to-new {"A" :logseq.property/priority.high
  282. "B" :logseq.property/priority.medium
  283. "C" :logseq.property/priority.low}
  284. priority-value (or (old-to-new priority)
  285. (do
  286. (log-fn :invalid-priority (str (pr-str priority) " is not a valid priority so setting it to low"))
  287. :logseq.property/priority.low))]
  288. (-> block
  289. (assoc :logseq.property/priority priority-value)
  290. (update :block/title string/replace-first (re-pattern (str "\\[#" priority "\\]" "\\s*")) "")
  291. (dissoc :block/priority)))
  292. block))
  293. (defn- update-block-deadline
  294. ":block/title doesn't contain DEADLINE.* text so unable to detect timestamp
  295. or repeater usage and notify user that they aren't supported"
  296. [block page-names-to-uuids {:keys [user-config]}]
  297. (if-let [date-int (or (:block/deadline block) (:block/scheduled block))]
  298. (let [title (date-time-util/int->journal-title date-int (common-config/get-date-formatter user-config))
  299. existing-journal-page (some->> title
  300. common-util/page-name-sanity-lc
  301. (get @page-names-to-uuids)
  302. (hash-map :block/uuid))
  303. deadline-page (->
  304. (or existing-journal-page
  305. ;; FIXME: Register new pages so that two different refs to same new page
  306. ;; don't create different uuids and thus an invalid page
  307. (let [page-m (sqlite-util/build-new-page title)]
  308. (assoc page-m
  309. :block/uuid (common-uuid/gen-uuid :journal-page-uuid date-int)
  310. :block/journal-day date-int)))
  311. (assoc :block/tags #{:logseq.class/Journal}))
  312. time-long (tc/to-long (date-time-util/int->local-date date-int))
  313. datetime-property (if (:block/deadline block) :logseq.property/deadline :logseq.property/scheduled)]
  314. {:block
  315. (-> block
  316. (assoc datetime-property time-long)
  317. (dissoc :block/deadline :block/scheduled :block/repeated?))
  318. :properties-tx (when-not existing-journal-page [deadline-page])})
  319. {:block block :properties-tx []}))
  320. (defn- text-with-refs?
  321. "Detects if a property value has text with refs e.g. `#Logseq is #awesome`
  322. instead of `#Logseq #awesome`. If so the property type is :default instead of :page"
  323. [prop-vals val-text]
  324. (let [replace-regex (re-pattern
  325. ;; Regex removes all characters of a tag or page-ref
  326. ;; so that only ref chars are left
  327. (str "([#[])"
  328. "("
  329. ;; Sorts ref names in descending order so that longer names
  330. ;; come first. Order matters since (foo-bar|foo) correctly replaces
  331. ;; "foo-bar" whereas (foo|foo-bar) does not
  332. (->> prop-vals (sort >) (map common-util/escape-regex-chars) (string/join "|"))
  333. ")"))
  334. remaining-text (string/replace val-text replace-regex "$1")
  335. non-ref-char (some #(if (or (string/blank? %) (#{"[" "]" "," "#"} %))
  336. false
  337. %)
  338. remaining-text)]
  339. (some? non-ref-char)))
  340. (defn- create-property-ident [db all-idents property-name]
  341. (let [db-ident (->> (db-property/create-user-property-ident-from-name (name property-name))
  342. ;; TODO: Detect new ident conflicts within same page
  343. (db-ident/ensure-unique-db-ident db))]
  344. (swap! all-idents assoc property-name db-ident)))
  345. (defn- get-ident [all-idents kw]
  346. (if (and (qualified-keyword? kw) (db-property/logseq-property? kw))
  347. kw
  348. (or (get all-idents kw)
  349. (throw (ex-info (str "No ident found for " (pr-str kw)) {})))))
  350. (defn- get-property-schema [property-schemas kw]
  351. (or (get property-schemas kw)
  352. (throw (ex-info (str "No property schema found for " (pr-str kw)) {}))))
  353. (defn- infer-property-schema-and-get-property-change
  354. "Infers a property's schema from the given _user_ property value and adds new ones to
  355. the property-schemas atom. If a property's :logseq.property/type changes, returns a map of
  356. the schema attribute changed and how it changed e.g. `{:type {:from :default :to :url}}`"
  357. [db prop-val prop prop-val-text refs {:keys [property-schemas all-idents]} macros]
  358. ;; Explicitly fail an unexpected case rather than cause silent downstream failures
  359. (when (and (coll? prop-val) (not (every? string? prop-val)))
  360. (throw (ex-info (str "Import cannot infer schema of unknown property value " (pr-str prop-val))
  361. {:value prop-val :property prop})))
  362. (let [prop-type (cond (and (coll? prop-val)
  363. (seq prop-val)
  364. (set/subset? prop-val
  365. (set (keep #(when (ldb/journal? %)
  366. (:block/title %)) refs))))
  367. :date
  368. (and (coll? prop-val) (seq prop-val) (text-with-refs? prop-val prop-val-text))
  369. :default
  370. (coll? prop-val)
  371. :node
  372. :else
  373. (db-property-type/infer-property-type-from-value
  374. (macro-util/expand-value-if-macro prop-val macros)))
  375. prev-type (get-in @property-schemas [prop :logseq.property/type])]
  376. ;; Create new property
  377. (when-not (get @property-schemas prop)
  378. (create-property-ident db all-idents prop)
  379. (let [schema (cond-> {:logseq.property/type prop-type}
  380. (#{:node :date} prop-type)
  381. ;; Assume :many for now as detecting that detecting property values across files are consistent
  382. ;; isn't possible yet
  383. (assoc :db/cardinality :many))]
  384. (swap! property-schemas assoc prop schema)))
  385. (when (and prev-type (not= prev-type prop-type))
  386. {:type {:from prev-type :to prop-type}})))
  387. (def built-in-property-file-to-db-idents
  388. "Map of built-in property file ids to their db graph idents"
  389. (->> (keys db-property/built-in-properties)
  390. (map (fn [k]
  391. [(db-property-util/get-file-pid k) k]))
  392. (into {})))
  393. (def all-built-in-property-file-ids
  394. "All built-in property file ids as a set of keywords"
  395. (-> built-in-property-file-to-db-idents keys set
  396. ;; built-in-properties that map to new properties
  397. (set/union #{:filters :query-table :query-properties :query-sort-by :query-sort-desc :hl-stamp :file :file-path})))
  398. ;; TODO: Review whether this should be using :block/title instead of file graph ids
  399. (def all-built-in-names
  400. "All built-in properties and classes as a set of keywords"
  401. (set/union all-built-in-property-file-ids
  402. ;; This should list all new pages introduced with db graph
  403. (set (->> db-class/built-in-classes
  404. vals
  405. (map :title)
  406. (concat ["Library"])
  407. (map #(-> % string/lower-case keyword))))))
  408. (def file-built-in-property-names
  409. "File-graph built-in property names that are supported. Expressed as set of keywords"
  410. #{:alias :tags :background-color :heading
  411. :query-table :query-properties :query-sort-by :query-sort-desc
  412. :ls-type :hl-type :hl-color :hl-page :hl-stamp :hl-value :file :file-path
  413. :logseq.order-list-type :logseq.tldraw.page :logseq.tldraw.shape
  414. :icon :public :exclude-from-graph-view :filters})
  415. (assert (set/subset? file-built-in-property-names all-built-in-property-file-ids)
  416. "All file-built-in properties are used in db graph")
  417. (def query-table-special-keys
  418. "Special keywords in previous query table"
  419. {:page :block/title
  420. :block :block/title
  421. :created-at :block/created-at
  422. :updated-at :block/updated-at})
  423. (defn- translate-query-properties [prop-value all-idents options]
  424. (let [property-classes (set (map keyword (:property-classes options)))]
  425. (try
  426. (->> (edn/read-string prop-value)
  427. (keep #(cond (get query-table-special-keys %)
  428. (get query-table-special-keys %)
  429. (property-classes %)
  430. :block/tags
  431. (= :tags %)
  432. ;; This could also be :logseq.property/page-tags
  433. :block/tags
  434. :else
  435. (get-ident @all-idents %)))
  436. distinct
  437. vec)
  438. (catch :default e
  439. (js/console.error "Translating query properties failed with:" e)
  440. []))))
  441. (defn- translate-linked-ref-filters
  442. [prop-value page-names-to-uuids]
  443. (try
  444. (let [filters (edn/read-string prop-value)
  445. filter-by (group-by val filters)
  446. includes (->> (filter-by true)
  447. (map first)
  448. (keep #(or (get @page-names-to-uuids %)
  449. (js/console.error (str "No uuid found for linked reference filter page " (pr-str %)))))
  450. (mapv #(vector :block/uuid %)))
  451. excludes (->> (filter-by false)
  452. (map first)
  453. (keep #(or (get @page-names-to-uuids %)
  454. (js/console.error (str "No uuid found for linked reference filter page " (pr-str %)))))
  455. (mapv #(vector :block/uuid %)))]
  456. (cond-> []
  457. (seq includes)
  458. (conj [:logseq.property.linked-references/includes includes])
  459. (seq excludes)
  460. (conj [:logseq.property.linked-references/excludes excludes])))
  461. (catch :default e
  462. (js/console.error "Translating linked reference filters failed with: " e))))
  463. (defn- update-built-in-property-values
  464. [props page-names-to-uuids {:keys [ignored-properties all-idents]} {:block/keys [title name]} options]
  465. (let [m
  466. (->> props
  467. (mapcat (fn [[prop prop-value]]
  468. (if (#{:icon :file :file-path :hl-stamp} prop)
  469. (do (swap! ignored-properties
  470. conj
  471. {:property prop :value prop-value :location (if name {:page name} {:block title})})
  472. nil)
  473. (case prop
  474. :query-properties
  475. (when-let [cols (not-empty (translate-query-properties prop-value all-idents options))]
  476. [[:logseq.property.table/ordered-columns cols]])
  477. :query-table
  478. [[:logseq.property.view/type
  479. (if prop-value :logseq.property.view/type.table :logseq.property.view/type.list)]]
  480. :query-sort-by
  481. [[:logseq.property.table/sorting
  482. [{:id (or (query-table-special-keys (keyword prop-value))
  483. (get-ident @all-idents (keyword prop-value)))
  484. :asc? true}]]]
  485. ;; ignore to handle below
  486. :query-sort-desc
  487. nil
  488. :filters
  489. (translate-linked-ref-filters prop-value page-names-to-uuids)
  490. :ls-type
  491. [[:logseq.property/ls-type (keyword prop-value)]]
  492. ;; else
  493. [[(built-in-property-file-to-db-idents prop) prop-value]]))))
  494. (into {}))]
  495. (cond-> m
  496. (and (contains? props :query-sort-desc) (:query-sort-by props))
  497. (update :logseq.property.table/sorting
  498. (fn [v]
  499. (assoc-in v [0 :asc?] (not (:query-sort-desc props))))))))
  500. (defn- update-page-or-date-values
  501. "Converts :node or :date names to entity values"
  502. [page-names-to-uuids property-values]
  503. (set (map #(vector :block/uuid
  504. ;; assume for now a ref's :block/name can always be translated by lc helper
  505. (get-page-uuid page-names-to-uuids (common-util/page-name-sanity-lc %) {:original-name %}))
  506. property-values)))
  507. (defn- handle-changed-property
  508. "Handles a property's schema changing across blocks. Handling usually means
  509. converting a property value to a new changed value or nil if the property is
  510. to be ignored. Sometimes handling a property change results in changing a
  511. property's previous usages instead of its current value e.g. when changing to
  512. a :default type. This is done by adding an entry to upstream-properties and
  513. building the additional tx to ensure this happens"
  514. [val prop page-names-to-uuids properties-text-values
  515. {:keys [ignored-properties property-schemas]}
  516. {:keys [property-changes log-fn upstream-properties]}]
  517. (let [type-change (get-in property-changes [prop :type])]
  518. (cond
  519. ;; ignore :to as any property value gets stringified
  520. (= :default (:from type-change))
  521. (or (get properties-text-values prop) (str val))
  522. ;; treat it the same as a :node
  523. (= {:from :node :to :date} type-change)
  524. (update-page-or-date-values page-names-to-uuids val)
  525. ;; Change to :node as dates can be pages but pages can't be dates
  526. (= {:from :date :to :node} type-change)
  527. (do
  528. (swap! property-schemas assoc-in [prop :logseq.property/type] :node)
  529. (update-page-or-date-values page-names-to-uuids val))
  530. ;; Unlike the other property changes, this one changes all the previous values of a property
  531. ;; in order to accommodate the change
  532. (= :default (:to type-change))
  533. (if (get @upstream-properties prop)
  534. ;; Ignore more than one property schema change per file to keep it simple
  535. (do
  536. (log-fn :prop-to-change-ignored {:property prop :val val :change type-change})
  537. (swap! ignored-properties conj {:property prop :value val :schema (get property-changes prop)})
  538. nil)
  539. (do
  540. (swap! upstream-properties assoc prop {:schema {:logseq.property/type :default}
  541. :from-type (:from type-change)})
  542. (swap! property-schemas assoc prop {:logseq.property/type :default})
  543. (get properties-text-values prop)))
  544. :else
  545. (do
  546. (log-fn :prop-change-ignored {:property prop :val val :change type-change})
  547. (swap! ignored-properties conj {:property prop :value val :schema (get property-changes prop)})
  548. nil))))
  549. (defn- update-user-property-values
  550. [props page-names-to-uuids properties-text-values
  551. {:keys [property-schemas] :as import-state}
  552. {:keys [property-changes] :as options}]
  553. (->> props
  554. (keep (fn [[prop val]]
  555. (if (get-in property-changes [prop :type])
  556. (when-let [val' (handle-changed-property val prop page-names-to-uuids properties-text-values import-state options)]
  557. [prop val'])
  558. [prop
  559. (if (set? val)
  560. (if (= :default (:logseq.property/type (get @property-schemas prop)))
  561. (get properties-text-values prop)
  562. (update-page-or-date-values page-names-to-uuids val))
  563. val)])))
  564. (into {})))
  565. (defn- ->property-value-tx-m
  566. "Given a new block and its properties, creates a map of properties which have values of property value tx.
  567. Similar to sqlite.build/->property-value-tx-m"
  568. [new-block properties get-schema-fn all-idents]
  569. (->> properties
  570. (keep (fn [[k v]]
  571. (if-let [built-in-type (get-in db-property/built-in-properties [k :schema :type])]
  572. (when (and (db-property-type/value-ref-property-types built-in-type)
  573. ;; closed values are referenced by their :db/ident so no need to create values
  574. (not (get-in db-property/built-in-properties [k :closed-values])))
  575. (let [property-map {:db/ident k
  576. :logseq.property/type built-in-type}]
  577. [property-map v]))
  578. (when (db-property-type/value-ref-property-types (:logseq.property/type (get-schema-fn k)))
  579. (let [property-map (merge
  580. {:db/ident (get-ident all-idents k)
  581. :original-property-id k}
  582. (get-schema-fn k))]
  583. [property-map v])))))
  584. (db-property-build/build-property-values-tx-m new-block)))
  585. (defn- build-properties-and-values
  586. "For given block properties, builds property values tx and returns a map with
  587. updated properties in :block-properties and any property values tx in :pvalues-tx"
  588. [props _db page-names-to-uuids
  589. {:block/keys [properties-text-values] :as block}
  590. {:keys [import-state user-options] :as options}]
  591. (let [{:keys [all-idents property-schemas]} import-state
  592. get-ident' #(get-ident @all-idents %)
  593. user-properties (apply dissoc props file-built-in-property-names)]
  594. (when (seq user-properties)
  595. (swap! (:block-properties-text-values import-state)
  596. assoc
  597. ;; For pages, valid uuid is in page-names-to-uuids, not in block
  598. (if (:block/name block)
  599. (get-page-uuid page-names-to-uuids ((some-fn ::original-name :block/name) block) {:block block})
  600. (:block/uuid block))
  601. properties-text-values))
  602. ;; TODO: Add import support for :template. Ignore for now as they cause invalid property types
  603. (if (contains? props :template)
  604. {}
  605. (let [props' (-> (update-built-in-property-values
  606. (select-keys props file-built-in-property-names)
  607. page-names-to-uuids
  608. (select-keys import-state [:ignored-properties :all-idents])
  609. (select-keys block [:block/name :block/title])
  610. (select-keys user-options [:property-classes]))
  611. (merge (update-user-property-values user-properties page-names-to-uuids properties-text-values import-state options)))
  612. pvalue-tx-m (->property-value-tx-m block props' #(get-property-schema @property-schemas %) @all-idents)
  613. block-properties (-> (merge props' (db-property-build/build-properties-with-ref-values pvalue-tx-m))
  614. (update-keys get-ident'))]
  615. {:block-properties block-properties
  616. :pvalues-tx (into (mapcat #(if (set? %) % [%]) (vals pvalue-tx-m)))}))))
  617. (def ignored-built-in-properties
  618. "Ignore built-in properties that are already imported or not supported in db graphs"
  619. ;; Already imported via a datascript attribute i.e. have :attribute on property config
  620. [:tags :alias :collapsed
  621. ;; Supported
  622. :id
  623. ;; Not supported as they have been ignored for a long time and cause invalid built-in pages
  624. :now :later :doing :done :canceled :cancelled :in-progress :todo :wait :waiting
  625. ;; deprecated in db graphs
  626. :background-image :macros :logseq.query/nlp-date
  627. :card-last-interval :card-repeats :card-last-reviewed :card-next-schedule
  628. :card-ease-factor :card-last-score
  629. :logseq.color :logseq.table.borders :logseq.table.stripes :logseq.table.max-width
  630. :logseq.table.version :logseq.table.compact :logseq.table.headers :logseq.table.hover])
  631. (defn- pre-update-properties
  632. "Updates page and block properties before their property types are inferred"
  633. [properties class-related-properties]
  634. (let [dissoced-props (concat ignored-built-in-properties
  635. ;; TODO: Deal with these dissoced built-in properties
  636. [:title :created-at :updated-at]
  637. class-related-properties)]
  638. (->> (apply dissoc properties dissoced-props)
  639. (keep (fn [[prop val]]
  640. (if (not (contains? file-built-in-property-names prop))
  641. ;; only update user properties
  642. (if (string? val)
  643. ;; Ignore blank values as they were usually generated by templates
  644. (when-not (string/blank? val)
  645. [prop
  646. ;; handle float strings b/c graph-parser doesn't
  647. (or (parse-double val) val)])
  648. [prop val])
  649. [prop val])))
  650. (into {}))))
  651. (defn- handle-page-and-block-properties
  652. "Returns a map of :block with updated block and :properties-tx with any properties tx.
  653. Handles modifying block properties, updating classes from property-classes
  654. and removing any deprecated property related attributes. Before updating most
  655. block properties, their property schemas are inferred as that can affect how
  656. a property is updated. Only infers property schemas on user properties as
  657. built-in ones must not change"
  658. [{:block/keys [properties] :as block} db page-names-to-uuids refs
  659. {{:keys [property-classes property-parent-classes]} :user-options
  660. :keys [import-state macros]
  661. :as options}]
  662. (-> (if (seq properties)
  663. (let [classes-from-properties (->> (select-keys properties property-classes)
  664. (mapcat (fn [[_k v]] (if (coll? v) v [v])))
  665. distinct)
  666. properties' (pre-update-properties properties (into property-classes property-parent-classes))
  667. properties-to-infer (if (:template properties')
  668. ;; Ignore template properties as they don't consistently have representative property values
  669. {}
  670. (apply dissoc properties' file-built-in-property-names))
  671. property-changes
  672. (->> properties-to-infer
  673. (keep (fn [[prop val]]
  674. (when-let [property-change
  675. (infer-property-schema-and-get-property-change db val prop (get (:block/properties-text-values block) prop) refs import-state macros)]
  676. [prop property-change])))
  677. (into {}))
  678. ;; _ (when (seq property-changes) (prn :prop-changes property-changes))
  679. options' (assoc options :property-changes property-changes)
  680. {:keys [block-properties pvalues-tx]}
  681. (build-properties-and-values properties' db page-names-to-uuids
  682. (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid ::original-name])
  683. options')]
  684. {:block
  685. (cond-> block
  686. true
  687. (merge block-properties)
  688. (seq classes-from-properties)
  689. ;; Add a map of {:block.temp/new-class TAG} to be processed later
  690. (update :block/tags
  691. (fn [tags]
  692. (let [tags' (if (sequential? tags) tags (set tags))]
  693. (into tags' (map #(hash-map :block.temp/new-class %) classes-from-properties))))))
  694. :properties-tx pvalues-tx})
  695. {:block block :properties-tx []})
  696. (update :block dissoc :block/properties :block/properties-text-values :block/properties-order :block/invalid-properties)))
  697. (defn- handle-page-properties
  698. "Adds page properties including special handling for :logseq.property.class/extends or :block/parent"
  699. [{:block/keys [properties] :as block*} db {:keys [page-names-to-uuids classes-tx]} refs
  700. {:keys [user-options log-fn import-state] :as options}]
  701. (let [{:keys [block properties-tx]} (handle-page-and-block-properties block* db page-names-to-uuids refs options)
  702. block'
  703. (if-let [parent-classes-from-properties (->> (select-keys properties (:property-parent-classes user-options))
  704. (mapcat (fn [[_k v]] (if (coll? v) v [v])))
  705. distinct
  706. seq)]
  707. (let [_ (swap! (:classes-from-property-parents import-state) conj (:block/title block*))
  708. class-m (find-or-create-class db ((some-fn ::original-title :block/title) block) (:all-idents import-state) block)
  709. class-m' (-> block
  710. (merge class-m)
  711. (dissoc :block/namespace)
  712. (assoc :logseq.property.class/extends
  713. (let [new-class (first parent-classes-from-properties)
  714. class-m (find-or-create-class db new-class (:all-idents import-state))
  715. class-m' (merge class-m
  716. {:block/uuid (find-or-gen-class-uuid page-names-to-uuids (common-util/page-name-sanity-lc new-class) (:db/ident class-m))})]
  717. (when (> (count parent-classes-from-properties) 1)
  718. (log-fn :skipped-parent-classes "Only one parent class is allowed so skipped ones after the first one" :classes parent-classes-from-properties))
  719. (when (:new-class? (meta class-m)) (swap! classes-tx conj class-m'))
  720. [:block/uuid (:block/uuid class-m')])))]
  721. class-m')
  722. (replace-namespace-with-parent block page-names-to-uuids :block/parent))]
  723. {:block block' :properties-tx properties-tx}))
  724. (defn- pretty-print-dissoc
  725. "Remove list of keys from a given map string while preserving whitespace"
  726. [s dissoc-keys]
  727. (-> (reduce rewrite/dissoc
  728. (rewrite/parse-string s)
  729. dissoc-keys)
  730. str))
  731. (defn- migrate-advanced-query-string [query-str]
  732. (try
  733. (pretty-print-dissoc query-str [:title :group-by-page? :collapsed?])
  734. (catch :default _e
  735. ;; rewrite/parse-string can fail on some queries in Advanced Queries in docs graph
  736. (js/console.error "Failed to parse advanced query string. Falling back to full query string: " (pr-str query-str))
  737. (if-let [query-map (not-empty (common-util/safe-read-map-string query-str))]
  738. (pr-str (dissoc query-map :title :group-by-page? :collapsed?))
  739. query-str))))
  740. (defn- handle-block-properties
  741. "Does everything page properties does and updates a couple of block specific attributes"
  742. [{:block/keys [title] :as block*}
  743. db page-names-to-uuids refs
  744. {{:keys [property-classes]} :user-options :as options}]
  745. (let [{:keys [block properties-tx]} (handle-page-and-block-properties block* db page-names-to-uuids refs options)
  746. advanced-query (some->> (second (re-find #"(?s)#\+BEGIN_QUERY(.*)#\+END_QUERY" title)) string/trim)
  747. additional-props (cond-> {}
  748. ;; Order matters as we ensure a simple query gets priority
  749. (macro-util/query-macro? title)
  750. (assoc :logseq.property/query
  751. (or (some->> (second (re-find #"\{\{query(.*)\}\}" title))
  752. string/trim)
  753. title))
  754. (seq advanced-query)
  755. (assoc :logseq.property/query (migrate-advanced-query-string advanced-query)))
  756. {:keys [block-properties pvalues-tx]}
  757. (when (seq additional-props)
  758. (build-properties-and-values additional-props db page-names-to-uuids
  759. (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid])
  760. options))
  761. pvalues-tx' (if (and pvalues-tx (seq advanced-query))
  762. (concat pvalues-tx [{:block/uuid (second (:logseq.property/query block-properties))
  763. :logseq.property.code/lang "clojure"
  764. :logseq.property.node/display-type :code}])
  765. pvalues-tx)]
  766. {:block
  767. (cond-> block
  768. (seq block-properties)
  769. (merge block-properties)
  770. (macro-util/query-macro? title)
  771. ((fn [b]
  772. (merge (update b :block/tags (fnil conj []) :logseq.class/Query)
  773. ;; Put all non-query content in title. Could just be a blank string
  774. {:block/title (string/trim (string/replace-first title #"\{\{query(.*)\}\}" ""))})))
  775. (seq advanced-query)
  776. ((fn [b]
  777. (let [query-map (common-util/safe-read-map-string advanced-query)]
  778. (cond-> (update b :block/tags (fnil conj []) :logseq.class/Query)
  779. true
  780. (assoc :block/title
  781. (or (when-let [title' (:title query-map)]
  782. (if (string? title') title' (pr-str title')))
  783. ;; Put all non-query content in title for now
  784. (string/trim (string/replace-first title #"(?s)#\+BEGIN_QUERY(.*)#\+END_QUERY" ""))))
  785. (:collapsed? query-map)
  786. (assoc :block/collapsed? true)))))
  787. (and (seq property-classes) (seq (:block/refs block*)))
  788. ;; remove unused, nonexistent property page
  789. (update :block/refs (fn [refs] (remove #(property-classes (keyword (:block/name %))) refs))))
  790. :properties-tx (concat properties-tx (when pvalues-tx' pvalues-tx'))}))
  791. (defn- update-block-refs
  792. "Updates the attributes of a block ref as this is where a new page is defined. Also
  793. updates block content effected by refs"
  794. [block page-names-to-uuids {:keys [whiteboard?]}]
  795. (let [ref-to-ignore? (if whiteboard?
  796. #(and (map? %) (:block/uuid %))
  797. #(and (vector? %) (= :block/uuid (first %))))]
  798. (if (seq (:block/refs block))
  799. (cond-> block
  800. true
  801. (update
  802. :block/refs
  803. (fn [refs]
  804. (mapv (fn [ref]
  805. ;; Only keep :block/uuid as we don't want to re-transact page refs
  806. (if (map? ref)
  807. ;; a new page's uuid can change across blocks so rely on consistent one from pages-tx
  808. (if-let [existing-uuid (some->> (:block/name ref) (get @page-names-to-uuids))]
  809. [:block/uuid existing-uuid]
  810. [:block/uuid (:block/uuid ref)])
  811. ref))
  812. refs)))
  813. (:block/title block)
  814. (assoc :block/title
  815. ;; TODO: Handle refs for whiteboard block which has none
  816. (let [refs (->> (:block/refs block)
  817. (remove #(or (ref-to-ignore? %)
  818. ;; ignore deadline related refs that don't affect content
  819. (and (keyword? %) (db-malli-schema/internal-ident? %))))
  820. (map #(add-uuid-to-page-map % page-names-to-uuids)))]
  821. (db-content/title-ref->id-ref (:block/title block) refs {:replace-tag? false}))))
  822. block)))
  823. (defn- fix-pre-block-references
  824. "Point pre-block children to parents since pre blocks don't exist in db graphs"
  825. [{:block/keys [parent] :as block} pre-blocks page-names-to-uuids]
  826. (cond-> block
  827. (and (vector? parent) (contains? pre-blocks (second parent)))
  828. (assoc :block/parent [:block/uuid (get-page-uuid page-names-to-uuids (second (:block/page block)) {:block block :block/page (:block/page block)})])))
  829. (defn- fix-block-name-lookup-ref
  830. "Some graph-parser attributes return :block/name as a lookup ref. This fixes
  831. those to use uuids since block/name is not unique for db graphs"
  832. [block page-names-to-uuids]
  833. (cond-> block
  834. (= :block/name (first (:block/page block)))
  835. (assoc :block/page [:block/uuid (get-page-uuid page-names-to-uuids (second (:block/page block)) {:block block :block/page (:block/page block)})])
  836. (:block/name (:block/parent block))
  837. (assoc :block/parent {:block/uuid (get-page-uuid page-names-to-uuids (:block/name (:block/parent block)) {:block block :block/parent (:block/parent block)})})))
  838. (defn asset-path->name
  839. "Given an asset's relative or full path, create a unique name for identifying an asset.
  840. Must handle to paths as ../assets/*, assets/* and with subdirectories"
  841. [path]
  842. (re-find #"assets/.*$" path))
  843. (defn- find-all-asset-links
  844. "Walks each ast block in order to its full depth as Link asts can be in different
  845. locations e.g. a Heading vs a Paragraph ast block"
  846. [ast-blocks]
  847. (let [results (atom [])]
  848. (walk/prewalk
  849. (fn [x]
  850. (when (and (vector? x)
  851. (= "Link" (first x))
  852. (common-config/local-asset? (second (:url (second x)))))
  853. (swap! results conj x))
  854. x)
  855. ast-blocks)
  856. @results))
  857. (defn- update-asset-links-in-block-title [block-title asset-name-to-uuids ignored-assets]
  858. (reduce (fn [acc [asset-name asset-uuid]]
  859. (let [new-title (string/replace acc
  860. (re-pattern (str "!?\\[[^\\]]*?\\]\\([^\\)]*?"
  861. asset-name
  862. "\\)(\\{[^}]*\\})?"))
  863. (page-ref/->page-ref asset-uuid))]
  864. (when (string/includes? new-title asset-name)
  865. (swap! ignored-assets conj
  866. {:reason "Some asset links were not updated to block references"
  867. :path asset-name
  868. :location {:block new-title}}))
  869. new-title))
  870. block-title
  871. asset-name-to-uuids))
  872. (defn- handle-assets-in-block
  873. [block* {:keys [assets ignored-assets]}]
  874. (let [block (dissoc block* :block.temp/ast-blocks)
  875. asset-links (find-all-asset-links (:block.temp/ast-blocks block*))]
  876. (if (seq asset-links)
  877. (let [asset-maps
  878. (keep
  879. (fn [asset-link]
  880. (let [asset-name (-> asset-link second :url second asset-path->name)]
  881. (if-let [asset-data (and asset-name (get @assets asset-name))]
  882. (if (:block/uuid asset-data)
  883. {:asset-name-uuid [asset-name (:block/uuid asset-data)]}
  884. (let [new-block (sqlite-util/block-with-timestamps
  885. {:block/uuid (d/squuid)
  886. :block/order (db-order/gen-key)
  887. :block/page :logseq.class/Asset
  888. :block/parent :logseq.class/Asset})
  889. new-asset (merge new-block
  890. {:block/tags [:logseq.class/Asset]
  891. :logseq.property.asset/type (:type asset-data)
  892. :logseq.property.asset/checksum (:checksum asset-data)
  893. :logseq.property.asset/size (:size asset-data)
  894. :block/title (db-asset/asset-name->title (node-path/basename asset-name))}
  895. (when-let [metadata (not-empty (common-util/safe-read-map-string (:metadata (second asset-link))))]
  896. {:logseq.property.asset/resize-metadata metadata}))]
  897. ;; (prn :asset-added! (node-path/basename asset-name) #_(get @assets asset-name))
  898. ;; (cljs.pprint/pprint asset-link)
  899. (swap! assets assoc-in [asset-name :block/uuid] (:block/uuid new-block))
  900. {:asset-name-uuid [asset-name (:block/uuid new-asset)]
  901. :asset new-asset}))
  902. (do
  903. (swap! ignored-assets conj
  904. {:reason "No asset data found for this asset path"
  905. :path (-> asset-link second :url second)
  906. :location {:block (:block/title block)}})
  907. nil))))
  908. asset-links)
  909. asset-blocks (keep :asset asset-maps)
  910. asset-names-to-uuids
  911. (into {} (map :asset-name-uuid asset-maps))]
  912. (cond-> {:block
  913. (update block :block/title update-asset-links-in-block-title asset-names-to-uuids ignored-assets)}
  914. (seq asset-blocks)
  915. (assoc :asset-blocks-tx asset-blocks)))
  916. {:block block})))
  917. (defn- build-block-tx
  918. [db block* pre-blocks {:keys [page-names-to-uuids] :as per-file-state} {:keys [import-state journal-created-ats] :as options}]
  919. ;; (prn ::block-in block*)
  920. (let [;; needs to come before update-block-refs to detect new property schemas
  921. {:keys [block properties-tx]}
  922. (handle-block-properties block* db page-names-to-uuids (:block/refs block*) options)
  923. {block-after-built-in-props :block deadline-properties-tx :properties-tx} (update-block-deadline block page-names-to-uuids options)
  924. {block-after-assets :block :keys [asset-blocks-tx]}
  925. (handle-assets-in-block block-after-built-in-props (select-keys import-state [:assets :ignored-assets]))
  926. ;; :block/page should be [:block/page NAME]
  927. journal-page-created-at (some-> (:block/page block*) second journal-created-ats)
  928. prepared-block (cond-> block-after-assets
  929. journal-page-created-at
  930. (assoc :block/created-at journal-page-created-at))
  931. block' (-> prepared-block
  932. (fix-pre-block-references pre-blocks page-names-to-uuids)
  933. (fix-block-name-lookup-ref page-names-to-uuids)
  934. (update-block-refs page-names-to-uuids options)
  935. (update-block-tags db (:user-options options) per-file-state (:all-idents import-state))
  936. (update-block-marker options)
  937. (update-block-priority options)
  938. add-missing-timestamps
  939. ;; old whiteboards may have :block/left
  940. (dissoc :block/left :block/format)
  941. ;; ((fn [x] (prn :block-out x) x))
  942. )]
  943. ;; Order matters as previous txs are referenced in block
  944. (concat properties-tx deadline-properties-tx asset-blocks-tx [block'])))
  945. (defn- update-page-alias
  946. [m page-names-to-uuids]
  947. (update m :block/alias (fn [aliases]
  948. (map #(vector :block/uuid (get-page-uuid page-names-to-uuids (:block/name %) {:block %}))
  949. aliases))))
  950. (defn- build-new-page-or-class
  951. [m db per-file-state all-idents {:keys [user-options journal-created-ats]}]
  952. (-> (cond-> m
  953. ;; Fix pages missing :block/title. Shouldn't happen
  954. (not (:block/title m))
  955. (assoc :block/title (:block/name m))
  956. (seq (:block/alias m))
  957. (update-page-alias (:page-names-to-uuids per-file-state))
  958. (journal-created-ats (:block/name m))
  959. (assoc :block/created-at (journal-created-ats (:block/name m))))
  960. add-missing-timestamps
  961. (dissoc :block/whiteboard?)
  962. (update-page-tags db user-options per-file-state all-idents)))
  963. (defn- get-page-parents
  964. "Like ldb/get-page-parents but using all-existing-page-uuids"
  965. [node all-existing-page-uuids]
  966. (let [get-parent (fn get-parent [n]
  967. (let [parent (or (:logseq.property.class/extends n) (:block/parent n))]
  968. (when-let [parent-id (:block/uuid parent)]
  969. (or (get all-existing-page-uuids parent-id)
  970. (throw (ex-info (str "No parent page found for " (pr-str (:block/uuid parent)))
  971. {:node n}))))))]
  972. (when-let [parent (get-parent node)]
  973. (loop [current-parent parent
  974. parents' []]
  975. (if (and current-parent (not (contains? parents' current-parent)))
  976. (recur (get-parent current-parent)
  977. (conj parents' current-parent))
  978. (vec (reverse parents')))))))
  979. (defn- get-all-existing-page-uuids
  980. "Returns a map of unique page names mapped to their uuids. The page names
  981. are in a format that is compatible with extract/extract e.g. namespace pages have
  982. their full hierarchy in the name"
  983. [classes-from-property-parents all-existing-page-uuids]
  984. (->> all-existing-page-uuids
  985. (map (fn [[_ p]]
  986. (vector
  987. (if-let [parents (and (or (contains? (:block/tags p) :logseq.class/Tag)
  988. (contains? (:block/tags p) :logseq.class/Page))
  989. ;; These classes have parents now but don't in file graphs (and in extract)
  990. (not (contains? classes-from-property-parents (:block/title p)))
  991. (get-page-parents p all-existing-page-uuids))]
  992. ;; Build a :block/name for namespace pages that matches data from extract/extract
  993. (string/join ns-util/namespace-char (map :block/name (conj (vec parents) p)))
  994. (:block/name p))
  995. (or (:block/uuid p)
  996. (throw (ex-info (str "No uuid for existing page " (pr-str (:block/name p)))
  997. (select-keys p [:block/name :block/tags])))))))
  998. (into {})))
  999. (defn- build-existing-page
  1000. [m db page-uuid {:keys [page-names-to-uuids] :as per-file-state} {:keys [notify-user import-state] :as options}]
  1001. (let [;; These attributes are not allowed to be transacted because they must not change across files
  1002. disallowed-attributes [:block/name :block/uuid :block/format :block/title :block/journal-day
  1003. :block/created-at :block/updated-at]
  1004. allowed-attributes (into [:block/tags :block/alias :block/parent :logseq.property.class/extends :db/ident]
  1005. (keep #(when (db-malli-schema/user-property? (key %)) (key %))
  1006. m))
  1007. block-changes (select-keys m allowed-attributes)]
  1008. (when-let [ignored-attrs (not-empty (apply dissoc m (into disallowed-attributes allowed-attributes)))]
  1009. (notify-user {:msg (str "Import ignored the following attributes on page " (pr-str (:block/title m)) ": "
  1010. ignored-attrs)}))
  1011. (when (seq block-changes)
  1012. (cond-> (merge block-changes {:block/uuid page-uuid})
  1013. (seq (:block/alias m))
  1014. (update-page-alias page-names-to-uuids)
  1015. (:block/tags m)
  1016. (update-page-tags db (:user-options options) per-file-state (:all-idents import-state))))))
  1017. (defn- modify-page-tx
  1018. "Modifies page tx from graph-parser for use with DB graphs. Currently modifies
  1019. namespaces and blocks with built-in page names"
  1020. [page all-existing-page-uuids]
  1021. (let [page'
  1022. (if (contains? all-existing-page-uuids (:block/name page))
  1023. (cond-> page
  1024. (:block/namespace page)
  1025. ;; Fix uuid for existing pages as graph-parser's :block/name is different than
  1026. ;; the DB graph's version e.g. 'b/c/d' vs 'd'
  1027. (assoc :block/uuid
  1028. (or (all-existing-page-uuids (:block/name page))
  1029. (throw (ex-info (str "No uuid found for existing namespace page " (pr-str (:block/name page)))
  1030. (select-keys page [:block/name :block/namespace]))))))
  1031. (cond-> page
  1032. ;; fix extract incorrectly assigning new user pages built-in uuids
  1033. (contains? all-built-in-names (keyword (:block/name page)))
  1034. (assoc :block/uuid (d/squuid))
  1035. ;; only happens for few file built-ins like tags and alias
  1036. (and (contains? all-built-in-names (keyword (:block/name page)))
  1037. (not (:block/tags page)))
  1038. (assoc :block/tags [:logseq.class/Page])))]
  1039. (cond-> page'
  1040. true
  1041. (dissoc :block/format)
  1042. (:block/namespace page)
  1043. ((fn [block']
  1044. (merge (build-new-namespace-page block')
  1045. {;; save original name b/c it's still used for a few name lookups
  1046. ::original-name (:block/name block')
  1047. ::original-title (:block/title block')}))))))
  1048. (defn- build-pages-tx
  1049. "Given all the pages and blocks parsed from a file, return a map containing
  1050. all non-whiteboard pages to be transacted, pages' properties and additional
  1051. data for subsequent steps"
  1052. [conn pages blocks {:keys [import-state user-options]
  1053. :as options}]
  1054. (let [all-pages* (->> (extract/with-ref-pages pages blocks)
  1055. ;; remove unused property pages unless the page has content
  1056. (remove #(and (contains? (into (:property-classes user-options) (:property-parent-classes user-options))
  1057. (keyword (:block/name %)))
  1058. (not (:block/file %))))
  1059. ;; remove file path relative
  1060. (map #(dissoc % :block/file)))
  1061. ;; Build all named ents once per import file to speed up named lookups
  1062. all-existing-page-uuids (get-all-existing-page-uuids @(:classes-from-property-parents import-state)
  1063. @(:all-existing-page-uuids import-state))
  1064. all-pages (map #(modify-page-tx % all-existing-page-uuids) all-pages*)
  1065. all-new-page-uuids (->> all-pages
  1066. (remove #(all-existing-page-uuids (or (::original-name %) (:block/name %))))
  1067. (map (juxt (some-fn ::original-name :block/name) :block/uuid))
  1068. (into {}))
  1069. ;; Stateful because new page uuids can occur via tags
  1070. page-names-to-uuids (atom (merge all-existing-page-uuids all-new-page-uuids))
  1071. per-file-state {:page-names-to-uuids page-names-to-uuids
  1072. :classes-tx (:classes-tx options)}
  1073. all-pages-m (mapv #(handle-page-properties % @conn per-file-state all-pages options)
  1074. all-pages)
  1075. pages-tx (keep (fn [{m :block _properties-tx :properties-tx}]
  1076. (let [page (if-let [page-uuid (if (::original-name m)
  1077. (all-existing-page-uuids (::original-name m))
  1078. (all-existing-page-uuids (:block/name m)))]
  1079. (build-existing-page (dissoc m ::original-name ::original-title) @conn page-uuid per-file-state options)
  1080. (when (or (ldb/class? m)
  1081. ;; Don't build a new page if it overwrites an existing class
  1082. (not (some-> (get @(:all-idents import-state)
  1083. (some-> (or (::original-title m) (:block/title m))
  1084. build-class-ident-name
  1085. keyword))
  1086. db-malli-schema/class?))
  1087. ;; TODO: Enable this when it's valid for all test graphs because
  1088. ;; pages with properties must be built or else properties-tx is invalid
  1089. #_(seq properties-tx))
  1090. (build-new-page-or-class (dissoc m ::original-name ::original-title)
  1091. @conn per-file-state (:all-idents import-state) options)))]
  1092. ;; (when-not ret (println "Skipped page tx for" (pr-str (:block/title m))))
  1093. page))
  1094. all-pages-m)]
  1095. {:pages-tx pages-tx
  1096. :page-properties-tx (mapcat :properties-tx all-pages-m)
  1097. :existing-pages (select-keys all-existing-page-uuids (map :block/name all-pages*))
  1098. :per-file-state per-file-state}))
  1099. (defn- build-upstream-properties-tx-for-default
  1100. "Builds upstream-properties-tx for properties that change to :default type"
  1101. [db prop property-ident from-prop-type block-properties-text-values]
  1102. (let [get-pvalue-content (fn get-pvalue-content [block-uuid prop']
  1103. (or (get-in block-properties-text-values [block-uuid prop'])
  1104. (throw (ex-info (str "No :block/text-properties-values found when changing property values: " (pr-str block-uuid))
  1105. {:property prop'
  1106. :block/uuid block-uuid}))))
  1107. existing-blocks
  1108. (map first
  1109. (d/q '[:find (pull ?b [*])
  1110. :in $ ?p %
  1111. :where (has-property ?b ?p)]
  1112. db
  1113. property-ident
  1114. (rules/extract-rules rules/db-query-dsl-rules)))
  1115. existing-blocks-tx
  1116. (mapcat (fn [m]
  1117. (let [prop-value (get m property-ident)
  1118. ;; Don't delete property values from these types b/c those pages are needed
  1119. ;; for refs and may have content
  1120. retract-tx (if (#{:node :date} from-prop-type)
  1121. [[:db/retract (:db/id m) property-ident]]
  1122. (mapv #(vector :db/retractEntity (:db/id %))
  1123. (if (sequential? prop-value) prop-value [prop-value])))
  1124. prop-value-content (get-pvalue-content (:block/uuid m) prop)
  1125. new-value (db-property-build/build-property-value-block
  1126. m {:db/ident property-ident} prop-value-content)]
  1127. (into retract-tx
  1128. [new-value
  1129. {:block/uuid (:block/uuid m)
  1130. property-ident [:block/uuid (:block/uuid new-value)]}])))
  1131. existing-blocks)]
  1132. existing-blocks-tx))
  1133. (defn- build-upstream-properties-tx
  1134. "Builds tx for upstream properties that have changed and any instances of its
  1135. use in db or in given blocks-tx. Upstream properties can be properties that
  1136. already exist in the DB from another file or from earlier uses of a property
  1137. in the same file"
  1138. [db upstream-properties import-state log-fn]
  1139. (if (seq upstream-properties)
  1140. (let [block-properties-text-values @(:block-properties-text-values import-state)
  1141. all-idents @(:all-idents import-state)
  1142. _ (log-fn :props-upstream-to-change upstream-properties)
  1143. txs
  1144. (mapcat
  1145. (fn [[prop {:keys [schema from-type]}]]
  1146. (let [prop-ident (get-ident all-idents prop)
  1147. upstream-tx
  1148. (when (= :default (:logseq.property/type schema))
  1149. (build-upstream-properties-tx-for-default db prop prop-ident from-type block-properties-text-values))
  1150. property-pages-tx [(merge {:db/ident prop-ident} schema)]]
  1151. ;; If we handle cardinality changes we would need to return these separately
  1152. ;; as property-pages would need to be transacted separately
  1153. (concat property-pages-tx upstream-tx)))
  1154. upstream-properties)]
  1155. txs)
  1156. []))
  1157. (defn new-import-state
  1158. "New import state that is used for import of one graph. State is atom per
  1159. key to make code more readable and encourage local mutations"
  1160. []
  1161. {;; Vec of maps with keys :property, :value, :schema and :location.
  1162. ;; Properties are ignored to keep graph valid and notify users of ignored properties.
  1163. ;; Properties with :schema are ignored due to property schema changes
  1164. :ignored-properties (atom [])
  1165. ;; Vec of maps with keys :path and :reason
  1166. :ignored-files (atom [])
  1167. ;; Vec of maps with keys :path, :reason and :location (optional).
  1168. :ignored-assets (atom [])
  1169. ;; Map of property names (keyword) and their current schemas (map of qualified properties).
  1170. ;; Used for adding schemas to properties and detecting changes across a property's usage
  1171. :property-schemas (atom {})
  1172. ;; Indexes all created pages by uuid. Index is used to fetch all parents of a page
  1173. :all-existing-page-uuids (atom {})
  1174. ;; Map of property or class names (keyword) to db-ident keywords
  1175. :all-idents (atom {})
  1176. ;; Set of children pages turned into classes by :property-parent-classes option
  1177. :classes-from-property-parents (atom #{})
  1178. ;; Map of block uuids to their :block/properties-text-values value.
  1179. ;; Used if a property value changes to :default
  1180. :block-properties-text-values (atom {})
  1181. ;; Track asset data for use across asset and doc import steps
  1182. :assets (atom {})})
  1183. (defn- build-tx-options [{:keys [user-options] :as options}]
  1184. (merge
  1185. (dissoc options :extract-options :user-options)
  1186. {:import-state (or (:import-state options) (new-import-state))
  1187. ;; Track per file changes to make to existing properties
  1188. ;; Map of property names (keyword) and their changes (map)
  1189. :upstream-properties (atom {})
  1190. ;; Track per file class tx so that their tx isn't embedded in individual :block/tags and can be post processed
  1191. :classes-tx (atom [])
  1192. :user-options
  1193. (merge user-options
  1194. {:tag-classes (set (map string/lower-case (:tag-classes user-options)))
  1195. :property-classes (set/difference
  1196. (set (map (comp keyword string/lower-case) (:property-classes user-options)))
  1197. file-built-in-property-names)
  1198. :property-parent-classes (set/difference
  1199. (set (map (comp keyword string/lower-case) (:property-parent-classes user-options)))
  1200. file-built-in-property-names)})}))
  1201. (defn- retract-parent-and-page-tag
  1202. [col]
  1203. (vec
  1204. (mapcat (fn [b]
  1205. (let [eid [:block/uuid (:block/uuid b)]]
  1206. [[:db/retract eid :block/parent]
  1207. [:db/retract eid :block/tags :logseq.class/Page]]))
  1208. col)))
  1209. (defn- split-pages-and-properties-tx
  1210. "Separates new pages from new properties tx in preparation for properties to
  1211. be transacted separately. Also builds property pages tx and converts existing
  1212. pages that are now properties"
  1213. [pages-tx old-properties existing-pages import-state]
  1214. (let [new-properties (set/difference (set (keys @(:property-schemas import-state))) (set old-properties))
  1215. ;; _ (when (seq new-properties) (prn :new-properties new-properties))
  1216. [properties-tx pages-tx'] ((juxt filter remove)
  1217. #(contains? new-properties (keyword (:block/name %))) pages-tx)
  1218. property-pages-tx (map (fn [{block-uuid :block/uuid :block/keys [title]}]
  1219. (let [property-name (keyword (string/lower-case title))
  1220. db-ident (get-ident @(:all-idents import-state) property-name)]
  1221. (sqlite-util/build-new-property db-ident
  1222. (get-property-schema @(:property-schemas import-state) property-name)
  1223. {:title title :block-uuid block-uuid})))
  1224. properties-tx)
  1225. converted-property-pages-tx
  1226. (map (fn [kw-name]
  1227. (let [existing-page-uuid (get existing-pages (name kw-name))
  1228. db-ident (get-ident @(:all-idents import-state) kw-name)
  1229. new-prop (sqlite-util/build-new-property db-ident
  1230. (get-property-schema @(:property-schemas import-state) kw-name)
  1231. {:title (name kw-name)})]
  1232. (assert existing-page-uuid)
  1233. (merge (select-keys new-prop [:block/tags :db/ident :logseq.property/type :db/index :db/cardinality :db/valueType])
  1234. {:block/uuid existing-page-uuid})))
  1235. (set/intersection new-properties (set (map keyword (keys existing-pages)))))
  1236. ;; Could do this only for existing pages but the added complexity isn't worth reducing the tx noise
  1237. retract-page-tag-from-properties-tx (retract-parent-and-page-tag (concat property-pages-tx converted-property-pages-tx))
  1238. ;; Save properties on new property pages separately as they can contain new properties and thus need to be
  1239. ;; transacted separately the property pages
  1240. property-page-properties-tx (keep (fn [b]
  1241. (when-let [page-properties (not-empty (db-property/properties b))]
  1242. (merge page-properties {:block/uuid (:block/uuid b)
  1243. :block/tags (-> (remove #(= :logseq.class/Page %) (:block/tags page-properties))
  1244. (conj :logseq.class/Property))})))
  1245. properties-tx)]
  1246. {:pages-tx pages-tx'
  1247. :property-pages-tx (concat property-pages-tx converted-property-pages-tx retract-page-tag-from-properties-tx)
  1248. :property-page-properties-tx property-page-properties-tx}))
  1249. (defn- update-whiteboard-blocks [blocks format]
  1250. (map (fn [b]
  1251. (if (seq (:block/properties b))
  1252. (-> (dissoc b :block/content)
  1253. (update :block/title #(gp-property/remove-properties format %)))
  1254. (cond-> (dissoc b :block/content)
  1255. (:block/content b)
  1256. (assoc :block/title (:block/content b)))))
  1257. blocks))
  1258. (defn- fix-extracted-block-tags-and-refs
  1259. "A tag or ref can have different :block/uuid's across extracted blocks. This makes
  1260. sense for most in-app uses but not for importing where we want consistent identity.
  1261. This fn fixes that issue. This fn also ensures that tags and pages have the same uuid"
  1262. [blocks]
  1263. (let [name-uuids (atom {})
  1264. fix-block-uuids
  1265. (fn fix-block-uuids [tags-or-refs {:keys [ref? properties]}]
  1266. ;; mapv to determinastically process in order
  1267. (mapv (fn [b]
  1268. (if (and ref? (get properties (keyword (:block/name b))))
  1269. ;; don't change uuid if property since properties and tags have different uuids
  1270. b
  1271. (if-let [existing-uuid (some->> (:block/name b) (get @name-uuids))]
  1272. (if (not= existing-uuid (:block/uuid b))
  1273. ;; fix unequal uuids for same name
  1274. (assoc b :block/uuid existing-uuid)
  1275. b)
  1276. (if (vector? b)
  1277. ;; ignore [:block/uuid] refs
  1278. b
  1279. (do
  1280. (assert (and (:block/name b) (:block/uuid b))
  1281. (str "Extracted block tag/ref must have a name and uuid: " (pr-str b)))
  1282. (swap! name-uuids assoc (:block/name b) (:block/uuid b))
  1283. b)))))
  1284. tags-or-refs))]
  1285. (map (fn [b]
  1286. (cond-> b
  1287. (seq (:block/tags b))
  1288. (update :block/tags fix-block-uuids {})
  1289. (seq (:block/refs b))
  1290. (update :block/refs fix-block-uuids {:ref? true :properties (:block/properties b)})))
  1291. blocks)))
  1292. (defn- extract-pages-and-blocks
  1293. "Main fn which calls graph-parser to convert markdown into data"
  1294. [db file content {:keys [extract-options import-state]}]
  1295. (let [format (common-util/get-format file)
  1296. ;; TODO: Remove once pdf highlights are supported
  1297. ignored-highlight-file? (string/starts-with? (str (path/basename file)) "hls__")
  1298. extract-options' (merge {:block-pattern (common-config/get-block-pattern format)
  1299. :date-formatter "MMM do, yyyy"
  1300. :uri-encoded? false
  1301. ;; Alters behavior in gp-block
  1302. :export-to-db-graph? true
  1303. :filename-format :legacy}
  1304. extract-options
  1305. {:db db})]
  1306. (cond (and (contains? common-config/mldoc-support-formats format) (not ignored-highlight-file?))
  1307. (-> (extract/extract file content extract-options')
  1308. (update :pages (fn [pages]
  1309. (map #(dissoc % :block.temp/original-page-name) pages)))
  1310. (update :blocks fix-extracted-block-tags-and-refs))
  1311. (common-config/whiteboard? file)
  1312. (-> (extract/extract-whiteboard-edn file content extract-options')
  1313. (update :pages (fn [pages]
  1314. (->> pages
  1315. ;; migrate previous attribute for :block/title
  1316. (map #(-> %
  1317. (assoc :block/title (or (:block/original-name %) (:block/title %))
  1318. :block/tags #{:logseq.class/Whiteboard})
  1319. (dissoc :block/type :block/original-name))))))
  1320. (update :blocks update-whiteboard-blocks format))
  1321. :else
  1322. (if ignored-highlight-file?
  1323. (swap! (:ignored-files import-state) conj
  1324. {:path file :reason :pdf-highlight})
  1325. (swap! (:ignored-files import-state) conj
  1326. {:path file :reason :unsupported-file-format})))))
  1327. (defn- build-journal-created-ats
  1328. "Calculate created-at timestamps for journals"
  1329. [pages]
  1330. (->> pages
  1331. (map #(when-let [journal-day (:block/journal-day %)]
  1332. [(:block/name %) (date-time-util/journal-day->ms journal-day)]))
  1333. (into {})))
  1334. (defn- clean-extra-invalid-tags
  1335. "If a page/class tx is an existing property or a new or existing class, ensure that
  1336. it only has one tag by removing :logseq.class/Page from its tx"
  1337. [db pages-tx' classes-tx existing-pages]
  1338. ;; TODO: Improve perf if we tracked all created classes in atom
  1339. (let [existing-classes (->> (d/datoms db :avet :block/tags :logseq.class/Tag)
  1340. (map #(d/entity db (:e %)))
  1341. (map :block/uuid)
  1342. set)
  1343. classes (set/union existing-classes
  1344. (set (map :block/uuid classes-tx)))
  1345. existing-properties (->> (d/datoms db :avet :block/tags :logseq.class/Property)
  1346. (map #(d/entity db (:e %)))
  1347. (map :block/uuid)
  1348. set)
  1349. existing-pages' (set/map-invert existing-pages)
  1350. retract-page-tag-from-existing-pages
  1351. (->> pages-tx'
  1352. ;; Existing pages that have converted to property or class
  1353. (filter #(and (:db/ident %) (get existing-pages' (:block/uuid %))))
  1354. retract-parent-and-page-tag)]
  1355. {:pages-tx
  1356. (mapv (fn [page]
  1357. (if (or (contains? classes (:block/uuid page))
  1358. (contains? existing-properties (:block/uuid page)))
  1359. (-> page
  1360. (update :block/tags (fn [tags] (vec (remove #(= % :logseq.class/Page) tags))))
  1361. (dissoc :block/parent))
  1362. page))
  1363. pages-tx')
  1364. :retract-page-tags-tx
  1365. (into (retract-parent-and-page-tag classes-tx)
  1366. retract-page-tag-from-existing-pages)}))
  1367. (defn- save-from-tx
  1368. "Save importer state from given txs"
  1369. [txs {:keys [import-state]}]
  1370. (when-let [nodes (seq (filter :block/name txs))]
  1371. (swap! (:all-existing-page-uuids import-state) merge (into {} (map (juxt :block/uuid identity) nodes)))))
  1372. (defn add-file-to-db-graph
  1373. "Parse file and save parsed data to the given db graph. Options available:
  1374. * :extract-options - Options map to pass to extract/extract
  1375. * :user-options - User provided options maps that alter how a file is converted to db graph. Current options
  1376. are: :tag-classes (set), :property-classes (set), :property-parent-classes (set), :convert-all-tags? (boolean)
  1377. and :remove-inline-tags? (boolean)
  1378. * :import-state - useful import state to maintain across files e.g. property schemas or ignored properties
  1379. * :macros - map of macros for use with macro expansion
  1380. * :notify-user - Displays warnings to user without failing the import. Fn receives a map with :msg
  1381. * :log-fn - Logs messages for development. Defaults to prn"
  1382. [conn file content {:keys [notify-user log-fn]
  1383. :or {notify-user #(println "[WARNING]" (:msg %))
  1384. log-fn prn}
  1385. :as *options}]
  1386. (let [options (assoc *options :notify-user notify-user :log-fn log-fn)
  1387. {:keys [pages blocks]} (extract-pages-and-blocks @conn file content options)
  1388. tx-options (merge (build-tx-options options)
  1389. {:journal-created-ats (build-journal-created-ats pages)})
  1390. old-properties (keys @(get-in options [:import-state :property-schemas]))
  1391. ;; Build page and block txs
  1392. {:keys [pages-tx page-properties-tx per-file-state existing-pages]} (build-pages-tx conn pages blocks tx-options)
  1393. whiteboard-pages (->> pages-tx
  1394. ;; support old and new whiteboards
  1395. (filter ldb/whiteboard?)
  1396. (map (fn [page-block]
  1397. (-> page-block
  1398. (assoc :logseq.property/ls-type :whiteboard-page)))))
  1399. pre-blocks (->> blocks (keep #(when (:block/pre-block? %) (:block/uuid %))) set)
  1400. blocks-tx (->> blocks
  1401. (remove :block/pre-block?)
  1402. (mapcat #(build-block-tx @conn % pre-blocks per-file-state
  1403. (assoc tx-options :whiteboard? (some? (seq whiteboard-pages)))))
  1404. vec)
  1405. {:keys [property-pages-tx property-page-properties-tx] pages-tx' :pages-tx}
  1406. (split-pages-and-properties-tx pages-tx old-properties existing-pages (:import-state options))
  1407. ;; _ (when (seq property-pages-tx) (cljs.pprint/pprint {:property-pages-tx property-pages-tx}))
  1408. ;; Necessary to transact new property entities first so that block+page properties can be transacted next
  1409. main-props-tx-report (d/transact! conn property-pages-tx {::new-graph? true ::path file})
  1410. _ (save-from-tx property-pages-tx options)
  1411. classes-tx @(:classes-tx tx-options)
  1412. {:keys [retract-page-tags-tx] pages-tx'' :pages-tx} (clean-extra-invalid-tags @conn pages-tx' classes-tx existing-pages)
  1413. classes-tx' (concat classes-tx retract-page-tags-tx)
  1414. ;; Build indices
  1415. pages-index (->> (map #(select-keys % [:block/uuid]) pages-tx'')
  1416. (concat (map #(select-keys % [:block/uuid]) classes-tx))
  1417. distinct)
  1418. block-ids (map (fn [block] {:block/uuid (:block/uuid block)}) blocks-tx)
  1419. block-refs-ids (->> (mapcat :block/refs blocks-tx)
  1420. (filter (fn [ref] (and (vector? ref)
  1421. (= :block/uuid (first ref)))))
  1422. (map (fn [ref] {:block/uuid (second ref)}))
  1423. (seq))
  1424. ;; To prevent "unique constraint" on datascript
  1425. blocks-index (set/union (set block-ids) (set block-refs-ids))
  1426. ;; Order matters. pages-index and blocks-index needs to come before their corresponding tx for
  1427. ;; uuids to be valid. Also upstream-properties-tx comes after blocks-tx to possibly override blocks
  1428. tx (concat whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx'' classes-tx' blocks-index blocks-tx)
  1429. tx' (common-util/fast-remove-nils tx)
  1430. ;; (prn :tx-counts (map #(vector %1 (count %2))
  1431. ;; [:whiteboard-pages :pages-index :page-properties-tx :property-page-properties-tx :pages-tx' :classes-tx :blocks-index :blocks-tx]
  1432. ;; [whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx' classes-tx blocks-index blocks-tx]))
  1433. ;; _ (when (not (seq whiteboard-pages)) (cljs.pprint/pprint {#_:property-pages-tx #_property-pages-tx :pages-tx pages-tx :tx tx'}))
  1434. main-tx-report (d/transact! conn tx' {::new-graph? true ::path file})
  1435. _ (save-from-tx tx' options)
  1436. upstream-properties-tx
  1437. (build-upstream-properties-tx @conn @(:upstream-properties tx-options) (:import-state options) log-fn)
  1438. ;; _ (when (seq upstream-properties-tx) (cljs.pprint/pprint {:upstream-properties-tx upstream-properties-tx}))
  1439. upstream-tx-report (when (seq upstream-properties-tx) (d/transact! conn upstream-properties-tx {::new-graph? true ::path file}))
  1440. _ (save-from-tx upstream-properties-tx options)]
  1441. ;; Return all tx-reports that occurred in this fn as UI needs to know what changed
  1442. [main-props-tx-report main-tx-report upstream-tx-report]))
  1443. ;; Higher level export fns
  1444. ;; =======================
  1445. (defn- export-doc-file
  1446. [{:keys [path idx] :as file} conn <read-file
  1447. {:keys [notify-user set-ui-state export-file]
  1448. :or {set-ui-state (constantly nil)
  1449. export-file (fn export-file [conn m opts]
  1450. (add-file-to-db-graph conn (:file/path m) (:file/content m) opts))}
  1451. :as options}]
  1452. ;; (prn :export-doc-file path idx)
  1453. (-> (p/let [_ (set-ui-state [:graph/importing-state :current-idx] (inc idx))
  1454. _ (set-ui-state [:graph/importing-state :current-page] path)
  1455. content (<read-file file)
  1456. m {:file/path path :file/content content}]
  1457. (export-file conn m (dissoc options :set-ui-state :export-file))
  1458. ;; returning val results in smoother ui updates
  1459. m)
  1460. (p/catch (fn [error]
  1461. (notify-user {:msg (str "Import failed on " (pr-str path) " with error:\n" (.-message error))
  1462. :level :error
  1463. :ex-data {:path path :error error}})))))
  1464. (defn export-doc-files
  1465. "Exports all user created files i.e. under journals/ and pages/.
  1466. Recommended to use build-doc-options and pass that as options"
  1467. [conn *doc-files <read-file {:keys [notify-user set-ui-state]
  1468. :or {set-ui-state (constantly nil) notify-user prn}
  1469. :as options}]
  1470. (set-ui-state [:graph/importing-state :total] (count *doc-files))
  1471. (let [doc-files (mapv #(assoc %1 :idx %2)
  1472. ;; Sort files to ensure reproducible import behavior
  1473. (sort-by :path *doc-files)
  1474. (range 0 (count *doc-files)))]
  1475. (-> (p/loop [_file-map (export-doc-file (get doc-files 0) conn <read-file options)
  1476. i 0]
  1477. (when-not (>= i (dec (count doc-files)))
  1478. (p/recur (export-doc-file (get doc-files (inc i)) conn <read-file options)
  1479. (inc i))))
  1480. (p/catch (fn [e]
  1481. (notify-user {:msg (str "Import has unexpected error:\n" (.-message e))
  1482. :level :error
  1483. :ex-data {:error e}}))))))
  1484. (defn- default-save-file [conn path content]
  1485. (ldb/transact! conn [{:file/path path
  1486. :file/content content
  1487. :file/last-modified-at (js/Date.)}]))
  1488. (defn- export-logseq-files
  1489. "Exports files under logseq/"
  1490. [repo-or-conn logseq-files <read-file {:keys [<save-file notify-user]
  1491. :or {<save-file default-save-file}}]
  1492. (let [custom-css (first (filter #(string/ends-with? (:path %) "logseq/custom.css") logseq-files))
  1493. custom-js (first (filter #(string/ends-with? (:path %) "logseq/custom.js") logseq-files))]
  1494. (-> (p/do!
  1495. (when custom-css
  1496. (-> (<read-file custom-css)
  1497. (p/then #(<save-file repo-or-conn "logseq/custom.css" %))))
  1498. (when custom-js
  1499. (-> (<read-file custom-js)
  1500. (p/then #(<save-file repo-or-conn "logseq/custom.js" %)))))
  1501. (p/catch (fn [error]
  1502. (notify-user {:msg (str "Import unexpectedly failed while reading logseq files:\n" (.-message error))
  1503. :level :error
  1504. :ex-data {:error error}}))))))
  1505. (defn export-config-file
  1506. "Exports logseq/config.edn by saving to database and setting any properties related to config"
  1507. [repo-or-conn config-file <read-file {:keys [<save-file notify-user default-config]
  1508. :or {default-config {}
  1509. <save-file default-save-file}}]
  1510. (-> (<read-file config-file)
  1511. (p/then #(p/do!
  1512. (<save-file repo-or-conn
  1513. "logseq/config.edn"
  1514. ;; Converts a file graph config.edn for use with DB graphs. Unlike common-config/create-config-for-db-graph,
  1515. ;; manually dissoc deprecated keys for config to be valid
  1516. (pretty-print-dissoc % (keys common-config/file-only-config)))
  1517. (let [config (edn/read-string %)]
  1518. (when-let [title-format (or (:journal/page-title-format config) (:date-formatter config))]
  1519. (ldb/transact! repo-or-conn [{:db/ident :logseq.class/Journal
  1520. :logseq.property.journal/title-format title-format}]))
  1521. ;; Return original config as import process depends on original config e.g. :hidden
  1522. config)))
  1523. (p/catch (fn [err]
  1524. (notify-user {:msg "Import may have mistakes due to an invalid config.edn. Recommend re-importing with a valid config.edn"
  1525. :level :error
  1526. :ex-data {:error err}})
  1527. (edn/read-string default-config)))))
  1528. (defn- export-class-properties
  1529. [conn repo-or-conn]
  1530. (let [user-classes (->> (d/q '[:find (pull ?b [:db/id :db/ident])
  1531. :where [?b :block/tags :logseq.class/Tag]] @conn)
  1532. (map first)
  1533. (remove #(db-class/built-in-classes (:db/ident %))))
  1534. class-to-prop-uuids
  1535. (->> (d/q '[:find ?t ?prop #_?class
  1536. :in $ ?user-classes
  1537. :where
  1538. [?b :block/tags ?t]
  1539. [?t :db/ident ?class]
  1540. [(contains? ?user-classes ?class)]
  1541. [?b ?prop _]
  1542. [?prop-e :db/ident ?prop]
  1543. [?prop-e :block/tags :logseq.class/Property]]
  1544. @conn
  1545. (set (map :db/ident user-classes)))
  1546. (remove #(ldb/built-in? (d/entity @conn (second %))))
  1547. (reduce (fn [acc [class-id prop-ident]]
  1548. (update acc class-id (fnil conj #{}) prop-ident))
  1549. {}))
  1550. tx (mapv (fn [[class-id prop-ids]]
  1551. {:db/id class-id
  1552. :logseq.property.class/properties (vec prop-ids)})
  1553. class-to-prop-uuids)]
  1554. (ldb/transact! repo-or-conn tx)))
  1555. (defn- <safe-async-loop
  1556. "Calls async-fn with each element in args-to-loop. Catches an unexpected error in loop and notifies user"
  1557. [async-fn args-to-loop notify-user]
  1558. (-> (p/loop [_ (async-fn (get args-to-loop 0))
  1559. i 0]
  1560. (when-not (>= i (dec (count args-to-loop)))
  1561. (p/recur (async-fn (get args-to-loop (inc i)))
  1562. (inc i))))
  1563. (p/catch (fn [e]
  1564. (notify-user {:msg (str "Import has an unexpected error:\n" (.-message e))
  1565. :level :error
  1566. :ex-data {:error e}})))))
  1567. (defn- read-asset-files
  1568. "Reads files under assets/"
  1569. [*asset-files <read-asset-file {:keys [notify-user set-ui-state assets]
  1570. :or {set-ui-state (constantly nil)}}]
  1571. (assert <read-asset-file "read-asset-file fn required")
  1572. (let [asset-files (mapv #(assoc %1 :idx %2)
  1573. ;; Sort files to ensure reproducible import behavior
  1574. (sort-by :path *asset-files)
  1575. (range 0 (count *asset-files)))
  1576. read-asset (fn read-asset [{:keys [path] :as file}]
  1577. (-> (<read-asset-file file assets)
  1578. (p/catch
  1579. (fn [error]
  1580. (notify-user {:msg (str "Import failed to read " (pr-str path) " with error:\n" (.-message error))
  1581. :level :error
  1582. :ex-data {:path path :error error}})))))]
  1583. (when (seq asset-files)
  1584. (set-ui-state [:graph/importing-state :current-page] "Read asset files")
  1585. (<safe-async-loop read-asset asset-files notify-user))))
  1586. (defn- copy-asset-files
  1587. "Copy files under assets/"
  1588. [asset-maps* <copy-asset-file {:keys [notify-user set-ui-state]
  1589. :or {set-ui-state (constantly nil)}}]
  1590. (assert <copy-asset-file "copy-asset-file fn required")
  1591. (let [asset-maps (mapv #(assoc %1 :idx %2)
  1592. ;; Sort files to ensure reproducible import behavior
  1593. (sort-by :path asset-maps*)
  1594. (range 0 (count asset-maps*)))
  1595. copy-asset (fn copy-asset [{:keys [path] :as asset-m}]
  1596. (p/catch
  1597. (<copy-asset-file asset-m)
  1598. (fn [error]
  1599. (notify-user {:msg (str "Import failed to copy " (pr-str path) " with error:\n" (.-message error))
  1600. :level :error
  1601. :ex-data {:path path :error error}}))))]
  1602. (when (seq asset-maps)
  1603. (set-ui-state [:graph/importing-state :current-page] "Copy asset files")
  1604. (<safe-async-loop copy-asset asset-maps notify-user))))
  1605. (defn- insert-favorites
  1606. "Inserts favorited pages as uuids into a new favorite page"
  1607. [repo-or-conn favorited-ids page-id]
  1608. (let [tx (reduce (fn [acc favorite-id]
  1609. (conj acc
  1610. (sqlite-util/block-with-timestamps
  1611. (merge (ldb/build-favorite-tx favorite-id)
  1612. {:block/uuid (d/squuid)
  1613. :db/id (or (some-> (:db/id (last acc)) dec) -1)
  1614. :block/order (db-order/gen-key nil)
  1615. :block/parent page-id
  1616. :block/page page-id}))))
  1617. []
  1618. favorited-ids)]
  1619. (ldb/transact! repo-or-conn tx)))
  1620. (defn- export-favorites-from-config-edn
  1621. [conn repo config {:keys [log-fn] :or {log-fn prn}}]
  1622. (when-let [favorites (seq (:favorites config))]
  1623. (p/do!
  1624. (if-let [favorited-ids
  1625. (keep (fn [page-name]
  1626. (some-> (ldb/get-page @conn page-name)
  1627. :block/uuid))
  1628. favorites)]
  1629. (let [page-entity (ldb/get-page @conn common-config/favorites-page-name)]
  1630. (insert-favorites repo favorited-ids (:db/id page-entity)))
  1631. (log-fn :no-favorites-found {:favorites favorites})))))
  1632. (defn build-doc-options
  1633. "Builds options for use with export-doc-files and assets"
  1634. [config options]
  1635. (-> {:extract-options {:date-formatter (common-config/get-date-formatter config)
  1636. ;; Remove config keys that break importing
  1637. :user-config (dissoc config :property-pages/excludelist :property-pages/enabled?)
  1638. :filename-format (or (:file/name-format config) :legacy)
  1639. :verbose (:verbose options)}
  1640. :user-config config
  1641. :user-options (merge {:remove-inline-tags? true :convert-all-tags? true} (:user-options options))
  1642. :import-state (new-import-state)
  1643. :macros (or (:macros options) (:macros config))}
  1644. (merge (select-keys options [:set-ui-state :export-file :notify-user]))))
  1645. (defn- move-top-parent-pages-to-library
  1646. [conn repo-or-conn]
  1647. (let [db @conn
  1648. library-page (ldb/get-built-in-page db "Library")
  1649. library-id (:block/uuid library-page)
  1650. top-parent-pages (->> (d/datoms db :avet :block/parent)
  1651. (keep (fn [d]
  1652. (let [child (d/entity db (:e d))
  1653. parent (d/entity db (:v d))]
  1654. (when (and (nil? (:block/parent parent)) (ldb/page? child) (ldb/page? parent))
  1655. parent))))
  1656. (common-util/distinct-by :block/uuid))
  1657. tx-data (map
  1658. (fn [parent]
  1659. {:db/id (:db/id parent)
  1660. :block/parent [:block/uuid library-id]
  1661. :block/order (db-order/gen-key)})
  1662. top-parent-pages)]
  1663. (ldb/transact! repo-or-conn tx-data)))
  1664. (defn export-file-graph
  1665. "Main fn which exports a file graph given its files and imports them
  1666. into a DB graph. Files is expected to be a seq of maps with a :path key.
  1667. The user experiences this as an import so all user-facing messages are
  1668. described as import. options map contains the following keys:
  1669. * :set-ui-state - fn which updates ui to indicate progress of import
  1670. * :notify-user - fn which notifies user of important messages with a map
  1671. containing keys :msg, :level and optionally :ex-data when there is an error
  1672. * :log-fn - fn which logs developer messages
  1673. * :rpath-key - keyword used to get relative path in file map. Default to :path
  1674. * :<read-file - fn which reads a file across multiple steps
  1675. * :default-config - default config if config is unable to be read
  1676. * :user-options - map of user specific options. See add-file-to-db-graph for more
  1677. * :<save-config-file - fn which saves a config file
  1678. * :<save-logseq-file - fn which saves a logseq file
  1679. * :<copy-asset - fn which copies asset file
  1680. * :<read-asset - fn which reads asset file
  1681. Note: See export-doc-files for additional options that are only for it"
  1682. [repo-or-conn conn config-file *files {:keys [<read-file <copy-asset <read-asset rpath-key log-fn]
  1683. :or {rpath-key :path log-fn println}
  1684. :as options}]
  1685. (reset! gp-block/*export-to-db-graph? true)
  1686. (->
  1687. (p/let [config (export-config-file
  1688. repo-or-conn config-file <read-file
  1689. (-> (select-keys options [:notify-user :default-config :<save-config-file])
  1690. (set/rename-keys {:<save-config-file :<save-file})))]
  1691. (let [files (common-config/remove-hidden-files *files config rpath-key)
  1692. logseq-file? #(string/starts-with? (get % rpath-key) "logseq/")
  1693. doc-files (->> files
  1694. (remove logseq-file?)
  1695. (filter #(contains? #{"md" "org" "markdown" "edn"} (path/file-ext (:path %)))))
  1696. asset-files (filter #(string/starts-with? (get % rpath-key) "assets/") files)
  1697. doc-options (build-doc-options config options)]
  1698. (log-fn "Importing" (count doc-files) "files ...")
  1699. ;; These export* fns are all the major export/import steps
  1700. (p/do!
  1701. (export-logseq-files repo-or-conn (filter logseq-file? files) <read-file
  1702. (-> (select-keys options [:notify-user :<save-logseq-file])
  1703. (set/rename-keys {:<save-logseq-file :<save-file})))
  1704. ;; Assets are read first as doc-files need data from them to make Asset blocks.
  1705. ;; Assets are copied after after doc-files as they need block/uuid's from them to name assets
  1706. (read-asset-files asset-files <read-asset (merge (select-keys options [:notify-user :set-ui-state])
  1707. {:assets (get-in doc-options [:import-state :assets])}))
  1708. (export-doc-files conn doc-files <read-file doc-options)
  1709. (copy-asset-files (vals @(get-in doc-options [:import-state :assets]))
  1710. <copy-asset
  1711. (select-keys options [:notify-user :set-ui-state]))
  1712. (export-favorites-from-config-edn conn repo-or-conn config {})
  1713. (export-class-properties conn repo-or-conn)
  1714. (move-top-parent-pages-to-library conn repo-or-conn)
  1715. {:import-state (-> (:import-state doc-options)
  1716. ;; don't leak full asset content (which could be large) out of this ns
  1717. (dissoc :assets))
  1718. :files files})))
  1719. (p/finally (fn [_]
  1720. (reset! gp-block/*export-to-db-graph? false)))
  1721. (p/catch (fn [e]
  1722. (reset! gp-block/*export-to-db-graph? false)
  1723. ((:notify-user options)
  1724. {:msg (str "Import has unexpected error:\n" (.-message e))
  1725. :level :error
  1726. :ex-data {:error e}})))))