diff --git a/src/uxbox/core.cljs b/src/uxbox/core.cljs index 70a2bcc..362280c 100644 --- a/src/uxbox/core.cljs +++ b/src/uxbox/core.cljs @@ -3,6 +3,7 @@ [uxbox.ui :as ui] [uxbox.ui.navigation :as nav] [uxbox.data.db :as db] + [uxbox.data.schema :as sch] [hodgepodge.core :refer [local-storage]])) (enable-console-print!) @@ -11,7 +12,7 @@ (defn start! [location] - (let [conn (db/create) + (let [conn (db/create sch/schema) storage local-storage] (nav/start-history!) (db/init! conn storage) diff --git a/src/uxbox/data/db.cljs b/src/uxbox/data/db.cljs index 54b8da0..cb55df3 100644 --- a/src/uxbox/data/db.cljs +++ b/src/uxbox/data/db.cljs @@ -4,9 +4,10 @@ [uxbox.data.schema :as sch])) (defn create - [] - (let [conn (d/create-conn sch/schema)] - conn)) + ([] + (create sch/schema)) + ([schema] + (d/create-conn schema))) (defn restore! [conn storage] diff --git a/src/uxbox/data/schema.cljs b/src/uxbox/data/schema.cljs index cd8ed11..86129c4 100644 --- a/src/uxbox/data/schema.cljs +++ b/src/uxbox/data/schema.cljs @@ -37,8 +37,7 @@ :shape/visible? {:db/cardinality :db.cardinality/one}}) (def user-schema - {:user/uuid {:db/unique :db.unique/identity} - :user/fullname {:db/cardinality :db.cardinality/one + {:user/fullname {:db/cardinality :db.cardinality/one :db/valueType :db.type/string} :user/avatar {:db/cardinality :db.cardinality/one :db/valueType :db.type/string}}) @@ -50,8 +49,8 @@ :event/user {:db/cardinality :db.cardinality/one}}) (def schema - {:uxbox/project project-schema - :uxbox/page page-schema - :uxbox/shape shape-schema - :uxbox/user user-schema - :uxbox/event event-schema}) + {:project project-schema + :page page-schema + :shape shape-schema + :user user-schema + :event event-schema}) diff --git a/src/uxbox/log/queries.cljs b/src/uxbox/log/queries.cljs index 4a17390..48286ff 100644 --- a/src/uxbox/log/queries.cljs +++ b/src/uxbox/log/queries.cljs @@ -3,6 +3,10 @@ [uxbox.log.core :as log] [datascript :as d])) +(def events-query + '[:find [?e ...] + :where [?e :event/type ?t]]) + (defn all-events [db] (map :e (d/datoms db :avet :event/timestamp))) diff --git a/src/uxbox/projects/queries.cljs b/src/uxbox/projects/queries.cljs index 2ea813d..589216c 100644 --- a/src/uxbox/projects/queries.cljs +++ b/src/uxbox/projects/queries.cljs @@ -63,7 +63,3 @@ [db] (let [eids (all-projects db)] (d/pull-many db '[*] eids))) - -(defn project-count - [db] - (count (d/q projects-query db))) diff --git a/src/uxbox/queries.cljs b/src/uxbox/queries.cljs new file mode 100644 index 0000000..86abcf6 --- /dev/null +++ b/src/uxbox/queries.cljs @@ -0,0 +1,79 @@ +(ns uxbox.queries + (:require + [datascript :as d] + [uxbox.streams :as s])) + +(defn- query + [q db] + (d/q q db)) + +;; reactive query + +(defn- eids-changed? + [q tx-report] + (let [before (query q (:db-before tx-report)) + after (query q (:db-after tx-report))] + (when (not= before after) + after))) + +(defn rquery + [q conn] + (let [k (gensym) + a (atom (query q @conn)) + sink #(reset! a %)] + (d/listen! conn + k + (fn [txr] + (when-let [after (eids-changed? q txr)] + (sink after)))) + a)) + +;; reactive pull + +(defn- pull-one-or-many + [eids p db] + (cond + (sequential? eids) + (d/pull-many db p eids) + + (not (nil? eids)) + (d/pull db p eids))) + +(defn rpull + [q p conn] + (let [k (gensym) + a (atom (pull-one-or-many (query q @conn) p @conn)) + sink #(reset! a %)] + (d/listen! conn + k + (fn [txr] + (let [after (query q (:db-after txr))] + (sink (pull-one-or-many after p (:db-after txr)))))) + a)) + +;; reactive entity + +(defn- pull-entity + [id p db] + (d/pull db p id)) + +(defn- entity-changed? + [id p tx-report] + (let [before (pull-entity id p (:db-before tx-report)) + after (pull-entity id p (:db-after tx-report))] + (when (not= before after) + after))) + +(defn rentity + ([id conn] + (rentity id '[*] conn)) + ([id p conn] + (let [k (gensym) + a (atom (pull-entity id p @conn)) + sink #(reset! a %)] + (d/listen! conn + k + (fn [txr] + (when-let [e (entity-changed? id p txr)] + (sink e)))) + a))) diff --git a/src/uxbox/ui/activity.cljs b/src/uxbox/ui/activity.cljs index 9d876a7..b362523 100644 --- a/src/uxbox/ui/activity.cljs +++ b/src/uxbox/ui/activity.cljs @@ -85,7 +85,7 @@ (create-project-activity ev))]) (rum/defcs timeline < (mx/pull-query :events - q/all-events + q/events-query '[:event/type :event/payload :event/author diff --git a/src/uxbox/ui/dashboard.cljs b/src/uxbox/ui/dashboard.cljs index e34df11..ff5c182 100644 --- a/src/uxbox/ui/dashboard.cljs +++ b/src/uxbox/ui/dashboard.cljs @@ -138,7 +138,8 @@ (rum/defc project-count < rum/static [n] - [:span.dashboard-projects n " projects"]) + [:span.dashboard-projects + (str n " projects")]) (rum/defc project-sort-selector < rum/reactive [sort-order] @@ -192,12 +193,12 @@ {:on-click #(set-lightbox! :new-project)} [:span "+ New project"]]) -(defn sorted-projects - [conn projects sort-order] - (let [project-cards (map (partial project-card conn) (sort-by sort-order projects))] +(defn sort-projects + [projects sort-order] + (let [sorted (sort-by sort-order projects)] (if (= sort-order :project/name) - project-cards - (reverse project-cards)))) + sorted + (reverse sorted)))) (rum/defc dashboard-grid < rum/reactive [conn projects sort-order] @@ -206,23 +207,20 @@ [:div.dashboard-grid-content (vec (concat [:div.dashboard-grid-content - (new-project conn)] - (sorted-projects conn - projects - (rum/react sort-order))))]]) + (new-project)] + (map (partial project-card conn) + (sort-projects projects (rum/react sort-order)))))]]) (rum/defcs dashboard* < (rum/local :project/name :project-sort-order) (mx/pull-query :projects - q/all-projects + q/projects-query '[*]) - (mx/query :project-count q/project-count) [{sort-order :project-sort-order - projects :projects - project-count :project-count} conn] + projects :projects} conn] [:main.dashboard-main (header conn) [:section.dashboard-content - (dashboard-bar sort-order @project-count) + (dashboard-bar sort-order (count @projects)) (dashboard-grid conn @projects sort-order)] (timeline conn)]) diff --git a/src/uxbox/ui/mixins.cljs b/src/uxbox/ui/mixins.cljs index 080e35d..86d5cb5 100644 --- a/src/uxbox/ui/mixins.cljs +++ b/src/uxbox/ui/mixins.cljs @@ -1,10 +1,13 @@ (ns uxbox.ui.mixins - (:require [rum] - [datascript :as d])) + (:require + [rum] + [datascript :as d] + [uxbox.queries :as qs])) ;; ================================================================================ ;; Queries + (defn query [key query] { :transfer-state @@ -39,30 +42,20 @@ :will-mount (fn [state] (let [[conn] (:rum/args state) - query! (fn [db] - (let [eids (query db)] - (cond - (sequential? eids) - (d/pull-many db pull eids) - - (not (nil? eids)) - (d/pull db pull eids)))) - local-state (atom (query! @conn)) + local-state (qs/rpull query pull conn) component (:rum/react-component state)] ;; sub - (d/listen! conn + (add-watch local-state key - (fn [tx-report] - (when-let [r (query! (:db-after tx-report))] - (when (not= @local-state r) - (reset! local-state r) - (rum/request-render component))))) + (fn [_ _ old new] + (when-not (= old new) + (rum/request-render component)))) (assoc state key local-state))) :will-unmount (fn [state] (let [[conn] (:rum/args state)] ;; unsub - (d/unlisten! conn key) + (remove-watch (state key) key) (dissoc state key)))}) ;; ================================================================================ diff --git a/src/uxbox/users/queries.cljs b/src/uxbox/users/queries.cljs index 1fb8041..927a51f 100644 --- a/src/uxbox/users/queries.cljs +++ b/src/uxbox/users/queries.cljs @@ -2,6 +2,5 @@ (defn pull-current-user [_] - {:user/uuid (random-uuid) - :user/fullname "Michael Buchannon" + {:user/fullname "Michael Buchannon" :user/avatar "/images/avatar.jpg"}) diff --git a/test/uxbox/runner.cljs b/test/uxbox/runner.cljs index 735f20f..9d10bd4 100644 --- a/test/uxbox/runner.cljs +++ b/test/uxbox/runner.cljs @@ -1,6 +1,7 @@ (ns uxbox.runner (:require [cljs.test :as test] [uxbox.test.data-test] + [uxbox.test.queries-test] [uxbox.test.projects.data-test] [uxbox.test.shapes.data-test] [uxbox.test.streams-test])) @@ -13,6 +14,7 @@ 'uxbox.test.data-test 'uxbox.test.projects.data-test 'uxbox.test.shapes.data-test + 'uxbox.test.queries-test 'uxbox.test.streams-test)) (set! *main-cli-fn* main) diff --git a/test/uxbox/test/queries_test.cljs b/test/uxbox/test/queries_test.cljs new file mode 100644 index 0000000..61d742f --- /dev/null +++ b/test/uxbox/test/queries_test.cljs @@ -0,0 +1,157 @@ +(ns uxbox.test.queries-test + (:require + [cljs.test :as t] + [datascript :as d] + [uxbox.streams :as s] + [uxbox.queries :as qs] + [uxbox.data.db :as db])) + +(t/deftest reactive-query + (t/testing "Reactive queries can be constructed" + (let [conn (db/create) + a (qs/rquery '[:find [?n ...] + :where + [?e :name ?n] + [?e :cool? true]] + conn)] + + (d/transact! conn [{:name "John" + :cool? true}]) + (t/is (= @a + ["John"])) + + (d/transact! conn [{:name "Mariano" + :cool? false}]) + (t/is (= @a + ["John"])) + + (d/transact! conn [{:db/id 42 + :name "Grace" + :cool? true} + {:name "Ada" + :cool? true}]) + + (t/is (= @a + ["John" "Grace" "Ada"])) + + (d/transact! conn [[:db/add 42 :cool? false]]) + + (t/is (= @a + ["John" "Ada"]))))) + +(t/deftest reactive-pull-query + (t/testing "Reactive pull queries can be constructed" + (let [conn (db/create) + query '[:find [?e ...] + :where [?e :name ?n] + [?e :cool? true]] + pull '[:name] + a (qs/rpull query pull conn)] + (d/transact! conn [{:name "John" + :cool? true}]) + (t/is (= @a + [{:name "John"}])) + + (d/transact! conn [{:name "Mariano" + :cool? false}]) + (t/is (= @a + [{:name "John"}])) + + (d/transact! conn [{:db/id 42 + :name "Ada" + :cool? true}]) + + (t/is (= @a + [{:name "John"} + {:name "Ada"}])) + + (d/transact! conn [[:db/add 42 :name "Ada Lovelace"]]) + + (t/is (= @a + [{:name "John"} + {:name "Ada Lovelace"}]))))) + +(t/deftest reactive-entity + (t/testing "Reactive entities can be queried" + (let [conn (db/create) + + id 42 + u (random-uuid) + name "UXBox" + width 400 + height 500 + + a (qs/rentity id conn)] + (t/is (= @a + {:db/id 42})) + + (d/transact! conn [[:db/add 42 :project/uuid u]]) + (t/is (= @a + {:db/id 42 + :project/uuid u + })) + + (d/transact! conn [[:db/add 42 :project/name name]]) + (t/is (= @a + {:db/id 42 + :project/uuid u + :project/name name})) + + (d/transact! conn [[:db/add 42 :project/width width]]) + (t/is (= @a + {:db/id 42 + :project/uuid u + :project/name name + :project/width width})) + + (d/transact! conn [[:db/add 42 :project/name "Neo UXBox"] + [:db/add 42 :project/height height]]) + (t/is (= @a + {:db/id 42 + :project/uuid u + :project/name "Neo UXBox" + :project/width width + :project/height height}))))) + +(t/deftest reactive-pull-entity + (t/testing "Reactive entities can be queried with a pull" + (let [conn (db/create) + + id 42 + u (random-uuid) + name "UXBox" + width 400 + height 500 + + pull '[:project/uuid + :project/name + :project/width + :project/height] + + a (qs/rentity id pull conn)] + (t/is (= @a + nil)) + + (d/transact! conn [[:db/add 42 :project/uuid u]]) + (t/is (= @a + {:project/uuid u + })) + + (d/transact! conn [[:db/add 42 :project/name name]]) + (t/is (= @a + {:project/uuid u + :project/name name})) + + (d/transact! conn [[:db/add 42 :project/width width]]) + (t/is (= @a + {:project/uuid u + :project/name name + :project/width width})) + + (d/transact! conn [[:db/add 42 :project/name "Neo UXBox"] + [:db/add 42 :project/height height]]) + (t/is (= @a + {:project/uuid u + :project/name "Neo UXBox" + :project/width width + :project/height height})))))