create_graph.cljs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. (ns logseq.tasks.db-graph.create-graph
  2. "This ns provides fns to create a DB graph using EDN. See `init-conn` for
  3. initializing a DB graph with a datascript connection that syncs to a sqlite DB
  4. at the given directory. See `create-blocks-tx` for the EDN format to create a
  5. graph and current limitations"
  6. (:require [logseq.db.sqlite.db :as sqlite-db]
  7. [logseq.db.sqlite.util :as sqlite-util]
  8. [logseq.db.sqlite.create-graph :as sqlite-create-graph]
  9. [logseq.db.frontend.property.build :as db-property-build]
  10. [logseq.outliner.db-pipeline :as db-pipeline]
  11. [logseq.common.util :as common-util]
  12. [clojure.string :as string]
  13. [clojure.set :as set]
  14. [datascript.core :as d]
  15. ["fs" :as fs]
  16. ["path" :as node-path]
  17. [nbb.classpath :as cp]
  18. [logseq.db.frontend.property :as db-property]
  19. [logseq.db.frontend.order :as db-order]))
  20. (defn- find-on-classpath [rel-path]
  21. (some (fn [dir]
  22. (let [f (node-path/join dir rel-path)]
  23. (when (fs/existsSync f) f)))
  24. (string/split (cp/get-classpath) #":")))
  25. (defn- setup-init-data
  26. "Setup initial data same as frontend.handler.repo/create-db"
  27. [conn additional-config]
  28. (let [config-content
  29. (cond-> (or (some-> (find-on-classpath "templates/config.edn") fs/readFileSync str)
  30. (do (println "Setting graph's config to empty since no templates/config.edn was found.")
  31. "{}"))
  32. additional-config
  33. ;; TODO: Replace with rewrite-clj when it's available
  34. (string/replace-first #"(:file/name-format :triple-lowbar)"
  35. (str "$1 "
  36. (string/replace-first (str additional-config) #"^\{(.*)\}$" "$1"))))]
  37. (d/transact! conn (sqlite-create-graph/build-db-initial-data config-content))))
  38. (defn init-conn
  39. "Create sqlite DB, initialize datascript connection and sync listener and then
  40. transacts initial data"
  41. [dir db-name & {:keys [additional-config]}]
  42. (fs/mkdirSync (node-path/join dir db-name) #js {:recursive true})
  43. ;; Same order as frontend.db.conn/start!
  44. (let [conn (sqlite-db/open-db! dir db-name)]
  45. (db-pipeline/add-listener conn)
  46. (setup-init-data conn additional-config)
  47. conn))
  48. (defn- translate-property-value
  49. "Translates a property value for create-graph edn. A value wrapped in vector
  50. may indicate a reference type e.g. [:page \"some page\"]"
  51. [val {:keys [page-uuids block-uuids]}]
  52. (if (vector? val)
  53. (case (first val)
  54. ;; Converts a page name to block/uuid
  55. :page
  56. (if-let [page-uuid (page-uuids (second val))]
  57. [:block/uuid page-uuid]
  58. (throw (ex-info (str "No uuid for page '" (second val) "'") {:name (second val)})))
  59. :block/uuid
  60. val
  61. ;; TODO: If not used by :default and replace uuid-maps with just page-uuids everywhere
  62. :block
  63. (or (block-uuids (second val))
  64. (throw (ex-info (str "No uuid for block '" (second val) "'") {:name (second val)})))
  65. (throw (ex-info "Invalid property value type. Valid values are :block and :page" {})))
  66. val))
  67. (defn- get-ident [all-idents kw]
  68. (or (get all-idents kw)
  69. (throw (ex-info (str "No ident found for " kw) {}))))
  70. (defn- ->block-properties [properties uuid-maps all-idents]
  71. (->>
  72. (map
  73. (fn [[prop-name val]]
  74. [(get-ident all-idents prop-name)
  75. ;; set indicates a :many value
  76. (if (set? val)
  77. (set (map #(translate-property-value % uuid-maps) val))
  78. (translate-property-value val uuid-maps))])
  79. properties)
  80. (into {})))
  81. (defn- create-uuid-maps
  82. "Creates maps of unique page names, block contents and property names to their uuids"
  83. [pages-and-blocks]
  84. (let [page-uuids (->> pages-and-blocks
  85. (map :page)
  86. (map (juxt #(or (:block/name %) (common-util/page-name-sanity-lc (:block/original-name %)))
  87. :block/uuid))
  88. (into {}))
  89. block-uuids (->> pages-and-blocks
  90. (mapcat :blocks)
  91. (map (juxt :block/content :block/uuid))
  92. (into {}))]
  93. {:page-uuids page-uuids
  94. :block-uuids block-uuids}))
  95. (defn- build-property-refs [properties all-idents]
  96. (mapv
  97. (fn [prop-name]
  98. {:db/ident (get-ident all-idents prop-name)})
  99. (keys properties)))
  100. (def current-db-id (atom 0))
  101. (def new-db-id
  102. "Provides the next temp :db/id to use in a create-graph transact!"
  103. #(swap! current-db-id dec))
  104. (defn- ->block-tx [m uuid-maps all-idents page-id]
  105. (merge (dissoc m :properties)
  106. (sqlite-util/block-with-timestamps
  107. {:db/id (new-db-id)
  108. :block/format :markdown
  109. :block/page {:db/id page-id}
  110. :block/order (db-order/gen-key nil)
  111. :block/parent {:db/id page-id}})
  112. (when (seq (:properties m))
  113. (merge (->block-properties (:properties m) uuid-maps all-idents)
  114. {:block/refs (build-property-refs (:properties m) all-idents)}))))
  115. (defn- build-properties-tx [properties uuid-maps all-idents]
  116. (let [property-db-ids (->> (keys properties)
  117. (map #(vector (name %) (new-db-id)))
  118. (into {}))
  119. new-properties-tx (vec
  120. (mapcat
  121. (fn [[prop-name prop-m]]
  122. (if (:closed-values prop-m)
  123. (let [db-ident (get-ident all-idents prop-name)]
  124. (db-property-build/build-closed-values
  125. db-ident
  126. prop-name
  127. (assoc prop-m :db/ident db-ident)
  128. {:property-attributes
  129. {:db/id (or (property-db-ids (name prop-name))
  130. (throw (ex-info "No :db/id for property" {:property prop-name})))}}))
  131. [(merge
  132. (sqlite-util/build-new-property (get-ident all-idents prop-name)
  133. (:block/schema prop-m)
  134. {:block-uuid (:block/uuid prop-m)})
  135. {:db/id (or (property-db-ids (name prop-name))
  136. (throw (ex-info "No :db/id for property" {:property prop-name})))}
  137. (when-let [props (not-empty (:properties prop-m))]
  138. (merge
  139. (->block-properties props uuid-maps all-idents)
  140. {:block/refs (build-property-refs props all-idents)})))]))
  141. properties))]
  142. new-properties-tx))
  143. (defn- build-classes-tx [classes uuid-maps all-idents]
  144. (let [class-db-ids (->> (keys classes)
  145. (map #(vector (name %) (new-db-id)))
  146. (into {}))
  147. classes-tx (mapv
  148. (fn [[class-name {:keys [class-parent schema-properties] :as class-m}]]
  149. (merge
  150. (sqlite-util/build-new-class
  151. {:block/name (common-util/page-name-sanity-lc (name class-name))
  152. :block/original-name (name class-name)
  153. :block/uuid (d/squuid)
  154. :db/ident (get-ident all-idents class-name)
  155. :db/id (or (class-db-ids (name class-name))
  156. (throw (ex-info "No :db/id for class" {:class class-name})))})
  157. (dissoc class-m :properties :class-parent :schema-properties)
  158. (when-let [props (not-empty (:properties class-m))]
  159. (merge
  160. (->block-properties props uuid-maps all-idents)
  161. {:block/refs (build-property-refs props all-idents)}))
  162. (when class-parent
  163. {:class/parent
  164. (or (class-db-ids class-parent)
  165. (throw (ex-info (str "No :db/id for " class-parent) {})))})
  166. (when schema-properties
  167. {:class/schema.properties
  168. (mapv #(hash-map :db/ident (get-ident all-idents (keyword %)))
  169. schema-properties)})))
  170. classes)]
  171. classes-tx))
  172. (defn- validate-options
  173. [{:keys [pages-and-blocks properties classes]}]
  174. (let [page-block-properties (->> pages-and-blocks
  175. (map #(-> (:blocks %) vec (conj (:page %))))
  176. (mapcat #(->> % (map :properties) (mapcat keys)))
  177. set)
  178. property-class-properties (->> (vals properties)
  179. (concat (vals classes))
  180. (mapcat #(keys (:properties %)))
  181. set)
  182. undeclared-properties (-> page-block-properties
  183. (into property-class-properties)
  184. (set/difference (set (keys properties))))
  185. invalid-pages (remove #(or (:block/original-name %) (:block/name %))
  186. (map :page pages-and-blocks))]
  187. (assert (empty? invalid-pages)
  188. (str "The following pages did not have a name attribute: " invalid-pages))
  189. (assert (every? :block/schema (vals properties))
  190. "All properties must have :block/schema")
  191. (assert (empty? undeclared-properties)
  192. (str "The following properties used in EDN were not declared in :properties: " undeclared-properties))))
  193. ;; TODO: How to detect these idents don't conflict with existing? :db/add?
  194. (defn- create-all-idents
  195. [properties classes graph-namespace]
  196. (let [property-idents (->> (keys properties)
  197. (map #(vector %
  198. (if graph-namespace
  199. (db-property/create-db-ident-from-name (str (name graph-namespace) ".property")
  200. (name %))
  201. (db-property/create-user-property-ident-from-name (name %)))))
  202. (into {}))
  203. _ (assert (= (count (set (vals property-idents))) (count properties)) "All property db-idents must be unique")
  204. class-idents (->> (keys classes)
  205. (map #(vector %
  206. (if graph-namespace
  207. (db-property/create-db-ident-from-name (str (name graph-namespace) ".class")
  208. (name %))
  209. (db-property/create-db-ident-from-name "user.class" (name %)))))
  210. (into {}))
  211. _ (assert (= (count (set (vals class-idents))) (count classes)) "All class db-idents must be unique")
  212. all-idents (merge property-idents class-idents)]
  213. (assert (= (count all-idents) (+ (count property-idents) (count class-idents)))
  214. "Class and property db-idents have no overlap")
  215. all-idents))
  216. (defn create-blocks-tx
  217. "Given an EDN map for defining pages, blocks and properties, this creates a
  218. vector of transactable data for use with d/transact!. The blocks that can be created
  219. have the following limitations:
  220. * Only top level blocks can be easily defined. Other level blocks can be
  221. defined but they require explicit setting of attributes like :block/order and :block/parent
  222. * Block content containing page refs or tags is not supported yet
  223. The EDN map has the following keys:
  224. * :pages-and-blocks - This is a vector of maps containing a :page key and optionally a :blocks
  225. key when defining a page's blocks. More about each key:
  226. * :page - This is a datascript attribute map e.g. `{:block/name \"foo\"}` .
  227. :block/name is required and :properties can be passed to define page properties
  228. * :blocks - This is a vec of datascript attribute maps e.g. `{:block/content \"bar\"}`.
  229. :block/content is required and :properties can be passed to define block properties
  230. * :properties - This is a map to configure properties where the keys are property names
  231. and the values are maps of datascript attributes e.g. `{:block/schema {:type :checkbox}}`.
  232. Additional keys available:
  233. * :closed-values - Define closed values with a vec of maps. A map contains keys :uuid, :value and :icon.
  234. * :properties - Define properties on a property page.
  235. * :classes - This is a map to configure classes where the keys are class names
  236. and the values are maps of datascript attributes e.g. `{:block/original-name \"Foo\"}`.
  237. Additional keys available:
  238. * :properties - Define properties on a class page
  239. * :class-parent - Add a class parent by its name
  240. * :schema-properties - Vec of property names. Defines properties that a class gives to its objects
  241. * :graph-namespace - namespace to use for db-ident creation. Useful when importing an ontology
  242. * :page-id-fn - custom fn that returns ent lookup id for page refs e.g. `[:block/uuid X]`
  243. Default is :db/id
  244. The :properties in :pages-and-blocks, :properties and :classes is a map of
  245. property names to property values. Multiple property values for a many
  246. cardinality property are defined as a set. The following property types are
  247. supported: :default, :url, :checkbox, :number, :page and :date. :checkbox and
  248. :number values are written as booleans and integers/floats. :page references
  249. are written as vectors e.g. `[:page \"PAGE NAME\"]`"
  250. [{:keys [pages-and-blocks properties classes graph-namespace page-id-fn]
  251. :or {page-id-fn :db/id}
  252. :as options}]
  253. (let [_ (validate-options options)
  254. ;; add uuids before tx for refs in :properties
  255. pages-and-blocks' (mapv (fn [{:keys [page blocks]}]
  256. (cond-> {:page (merge {:block/uuid (random-uuid)} page)}
  257. (seq blocks)
  258. (assoc :blocks (mapv #(merge {:block/uuid (random-uuid)} %) blocks))))
  259. pages-and-blocks)
  260. uuid-maps (create-uuid-maps pages-and-blocks')
  261. all-idents (create-all-idents properties classes graph-namespace)
  262. properties-tx (build-properties-tx properties uuid-maps all-idents)
  263. classes-tx (build-classes-tx classes uuid-maps all-idents)
  264. pages-and-blocks-tx
  265. (vec
  266. (mapcat
  267. (fn [{:keys [page blocks]}]
  268. (let [new-page (merge
  269. {:db/id (or (:db/id page) (new-db-id))
  270. :block/original-name (or (:block/original-name page) (string/capitalize (:block/name page)))
  271. :block/name (or (:block/name page) (common-util/page-name-sanity-lc (:block/original-name page)))
  272. :block/format :markdown}
  273. (dissoc page :properties :db/id :block/name :block/original-name))]
  274. (into
  275. ;; page tx
  276. [(sqlite-util/block-with-timestamps
  277. (merge
  278. new-page
  279. (when (seq (:properties page))
  280. (->block-properties (:properties page) uuid-maps all-idents))
  281. (when (seq (:properties page))
  282. {:block/refs (build-property-refs (:properties page) all-idents)
  283. ;; app doesn't do this yet but it should to link property to page
  284. :block/path-refs (build-property-refs (:properties page) all-idents)})))]
  285. ;; blocks tx
  286. (reduce (fn [acc m]
  287. (conj acc
  288. (->block-tx m uuid-maps all-idents (page-id-fn new-page))))
  289. []
  290. blocks))))
  291. pages-and-blocks'))]
  292. ;; Properties first b/c they have schema. Then pages b/c they can be referenced by blocks
  293. (vec (concat properties-tx
  294. classes-tx
  295. (filter :block/name pages-and-blocks-tx)
  296. (remove :block/name pages-and-blocks-tx)))))