(ns weepinbell.storage.h2 (:require [clojure.string :refer [blank? split]] [next.jdbc :as jdbc] [next.jdbc.result-set :refer [as-unqualified-lower-maps]] [next.jdbc.sql :as sql] [clojure.java.io :as io] [fast-edn.core :as edn] [weepinbell.utils.url :refer [get-domain get-path]])) ;; data trasnformation utilities (defn schema->map [{:keys [wm_source wm_target hash lastmod jf2_data photo_url]}] (let [parse #(if (string? %) (edn/read-string %) %) jf2 (parse jf2_data) photo photo_url] (cond-> {:webmention {:source wm_source :target wm_target :id hash :modified lastmod}} (some? jf2) (assoc :jf2 jf2) (some? photo) (assoc :extra {:proxied-photo photo})))) (defn wm->schema [data] (let [{:keys [webmention]} data {:keys [id modified source target]} webmention] {:hash id ;; weepinbell's id is hash in sqlite :wm_source source :wm_target target :wm_target_domain (get-domain target) :wm_source_domain (get-domain source) :wm_source_path (get-path source) :wm_target_path (get-path target) :lastmod (or modified (System/currentTimeMillis))})) (defn jf2->schema [wm_id data] (let [jf2 (:jf2 data) get-name #(if % (name %) "mention")] {:wm_id wm_id :jf2_type (->> jf2 :wm-property get-name) ;; conver EDN to raw string :jf2_data (pr-str jf2)})) ;; sql (defn insert-webmention [ds data] (jdbc/with-transaction [tx ds] (let [tx (jdbc/with-options tx {:builder-fn as-unqualified-lower-maps}) jf2 (:jf2 data) photo (get-in data [:extra :proxied-photo]) wm_result (sql/insert! tx :webmention (wm->schema data)) wm_id (:id wm_result)] (when jf2 (sql/insert! tx :jf2 (jf2->schema wm_id data))) (when photo (sql/insert! tx :proxied_photo {:wm_id wm_id :url photo}))))) (defn delete-webmention [ds hashcode] ;; jf2 data and photo will be deleted by ;; cascade rule by SQL, no need to handle it (sql/delete! ds :webmention {:hash hashcode})) (def base-sql "SELECT w.*, j.jf2_data, j.jf2_type, p.url AS photo_url FROM webmention w LEFT JOIN jf2 j ON w.id = j.wm_id LEFT JOIN proxied_photo p ON w.id = p.wm_id") (defn get-webmention-by-hash [ds hashcode] (first (jdbc/execute! ds [(str base-sql " WHERE hash = ?") hashcode]))) (defn get-webmention-by-pair [ds source target] (first (jdbc/execute! ds [(str base-sql " WHERE wm_source = ? AND wm_target = ? ORDER BY w.lastmod, w.id") source target]))) (defn get-webmention ([ds hashcode] (some->> (get-webmention-by-hash ds hashcode) schema->map)) ([ds source target] (some->> (get-webmention-by-pair ds source target) schema->map))) (defn get-webmentions-by-domain ([ds domain] (get-webmentions-by-domain ds domain nil)) ([ds domain path] (let [r (if (some? path) (jdbc/execute! ds [(str base-sql " WHERE wm_target_domain = ? AND wm_target_path = ? ORDER BY w.lastmod, w.id") domain path]) (jdbc/execute! ds [(str base-sql " WHERE wm_target_domain = ? ORDER BY w.lastmod, w.id") domain]))] (map schema->map r)))) (defn- update-related! [ds wm_id data] (let [jf2 (:jf2 data) photo (get-in data [:extra :proxied-photo])] (sql/delete! ds :jf2 {:wm_id wm_id}) (when jf2 (sql/insert! ds jf2 (jf2->schema wm_id data))) (sql/delete! ds :proxied_photo {:wm_id wm_id}) (when photo (sql/insert! ds :proxied_photo {:wm_id wm_id :url photo})))) (defn update-webmention ([ds hashcode new] (jdbc/with-transaction [tx ds] (let [tx (jdbc/with-options tx {:builder-fn as-unqualified-lower-maps})] (when-let [wm (get-webmention-by-hash tx hashcode)] (sql/update! tx :webmention (wm->schema new) {:id (:id wm)}) (update-related! tx (:id wm) new))))) ([ds source target new] (jdbc/with-transaction [tx ds] (let [tx (jdbc/with-options tx {:builder-fn as-unqualified-lower-maps})] (when-let [wm (get-webmention-by-pair tx source target)] (sql/update! tx :webmention (wm->schema new) {:id (:id wm)}) (update-related! tx (:id wm) new)))))) (defn make-db [db-file] (io/make-parents db-file) {:dbtype "h2" :dbname db-file}) (defn make-ds [db] (jdbc/get-datasource db)) (defn make-datasource [db-file] (jdbc/with-options (make-ds (make-db db-file)) {:builder-fn as-unqualified-lower-maps})) (def init-sql-script (io/resource "sql/h2_init.sql")) (defn create-tables [ds] (doseq [:let [statements (-> init-sql-script slurp (split #";"))] statement (remove blank? statements)] (jdbc/execute! ds [statement]))) (defn use-store [db-file] (let [ds (make-datasource db-file)] (create-tables ds) {:append (partial insert-webmention ds) :update (partial update-webmention ds) :delete (partial delete-webmention ds) :get (partial get-webmention ds) :get-by-domain (partial get-webmentions-by-domain ds)}))