(ns net.deertopia.doerg.roam (:require [babashka.fs :as fs] [net.deertopia.doerg.config :as cfg] [net.deertopia.doerg.elisp :as elisp] [net.deertopia.doerg.slug :as slug] [next.jdbc :as sql]) (:import (java.util UUID))) ;;; Global database (defonce ^:dynamic *use-db-cache?* true) (defn ds [] (sql/get-datasource {:dbtype "sqlite" :dbname (-> cfg/*cfg* ::cfg/org-roam-db-path str)})) ;;; Elisp sexp (de)serialisation (defn id [node] (-> node :id)) (defn slug [node] (-> node :id slug/from-uuid)) (defn- print-id [node] (-> node id elisp/print)) ;;; Node (defrecord Node [id cache]) (defn uuid-exists? [uuid] (sql/execute-one! (ds) ["select 1 from nodes where id = ? limit 1" (-> uuid str elisp/print)])) (defn make-node ([uuid] (make-node uuid {})) ([uuid props] (and (uuid-exists? uuid) (->Node uuid (atom props))))) (defn- fetch-with-cache [node field fetch] (if *use-db-cache?* (-> (:cache node) (swap! (fn [cache] (update cache field #(or % (fetch node))))) (get field)) (fetch node))) (defn org-file [node] (fetch-with-cache node :org-file (fn [node] (when-some [r (sql/execute-one! (ds) ["select file from nodes where id = ?" (-> node :id str elisp/print)])] (-> r :nodes/file elisp/read-string))))) (defn title [node] (fetch-with-cache node :title #(when-some [r (sql/execute-one! (ds) ["select title from nodes where id = ?" (print-id %)])] (-> r :nodes/title elisp/read-string)))) (defprotocol GetNode (get-node [this] "Return the node associated with `this` or nil.")) (extend-protocol GetNode String (get-node [this] (or (some-> this slug/from-string get-node) (some-> this parse-uuid get-node) (throw (IllegalArgumentException. "Give `get-node` a UUID or slug string plz. }:)")))) java.util.UUID (get-node [this] (make-node this)) net.deertopia.doerg.slug.Slug (get-node [this] (-> this slug/to-uuid make-node)) Node (get-node [this] this)) (comment (def node (get-node "68XqhHerTWCbE--RYLEdHw")) (fetch-with-cache node :title #(do (println "fetch") (sql/execute-one! (ds) ["select title from nodes where id = ?" (elisp/print (:id %))])))) ;;; Node operations (defn level [node] (fetch-with-cache node :level #(-> (sql/execute-one! (ds) ["select level from nodes where id = ?" (print-id %)]) :nodes/level))) (defn top-level? [node] (zero? (level node))) (defn file [node] (fetch-with-cache node :file #(-> (sql/execute-one! (ds) ["select file from nodes where id = ?" (print-id %)]) :nodes/file elisp/read-string))) (defn properties [node] (fetch-with-cache node :properties #(-> (sql/execute-one! (ds) ["select properties from nodes where id = ?" (print-id %)]) :nodes/properties elisp/read-alist))) (defn public? [node] (some-> (properties node) (get "DEERTOPIAVISIBILITY") (= "public"))) (defn graph-visible? [node] (#{"public" "graphonly"} (some-> (properties node) (get "DEERTOPIAVISIBILITY")))) (defn backlinks "Returns a collection of nodes linking to `node`." [node] (for [{id :nodes/id title :nodes/title} (sql/execute! (ds) ["select distinct nodes.id, nodes.title from links inner join nodes on nodes.id = links.source where links.dest = ?" (elisp/print (str (:id node)))]) :let [id' (elisp/read-string id)] :when (some-> (-> id' parse-uuid get-node) public?)] (make-node id' {:title (elisp/read-string title)}))) ;;; Graph support (defn- read-string-field [n field] (-> n (get field) elisp/read-string)) (defn- uuid-graph-visible? [uuid] (-> uuid parse-uuid get-node graph-visible?)) (defn get-graph [] (let [nodes (sql/execute! (ds) ["select id, title from nodes"]) links (sql/execute! (ds) ["select n1.id as source, nodes.id as target from ((nodes as n1) join links on n1.id = links.source) join (nodes as n2) on links.dest = nodes.id where links.type = '\"id\"'"])] {:nodes (for [n nodes :let [id (read-string-field n :nodes/id)] :when (uuid-graph-visible? id)] {:id id :title (read-string-field n :nodes/title)}) :links (for [l links :let [source (read-string-field l :nodes/source) target (read-string-field l :nodes/target)] :when (and (uuid-graph-visible? source) (uuid-graph-visible? target))] {:source source :target target})}))