Skip to content
This repository has been archived by the owner on May 15, 2021. It is now read-only.

Commit

Permalink
v0.2.0 - Add trident.firestore
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobobryant committed Mar 6, 2020
1 parent 15c4670 commit 1d235f3
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 6 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
```clojure
trident/<artifact> {:mvn/version "0.1.21"}
trident/<artifact> {:mvn/version "0.2.0"}
```

# Trident
Expand Down Expand Up @@ -43,6 +43,7 @@ to see what's available, but here's a list of the artifacts for convenience:
- [`trident/datomic-cloud`](https://cljdoc.org/d/trident/docs/CURRENT/api/trident.datomic-cloud) (includes `util`). Tools for Datomic Cloud.
- [`trident/ring`](https://cljdoc.org/d/trident/docs/CURRENT/api/trident.ring). Some Ring middleware.
- [`trident/firebase`](https://cljdoc.org/d/trident/docs/CURRENT/api/trident.firebase). Functions for authenticating Firebase user tokens.
- [`trident/firestore`](https://cljdoc.org/d/trident/docs/CURRENT/api/trident.firestore). A Clojurey wrapper for Firestore.
- [`trident/ion`](https://cljdoc.org/d/trident/docs/CURRENT/api/trident.ion) (includes `util`). Utilities for working with Datomic Ions
- [`trident/web`](https://cljdoc.org/d/trident/docs/CURRENT/api/trident.web) (includes `util`, `datomic-cloud`, `firebase`, `ion`, `ring`). Highly contrived web framework.
- [`trident/staticweb`](https://cljdoc.org/d/trident/docs/CURRENT/api/trident.staticweb). Tools for making static websites
Expand All @@ -62,8 +63,8 @@ of the way, if you're interested in anything Trident does, I'd love to chat.

Projects using Trident:

- [Findka](https://findka.com), a content curation system.
- [Lagukan](https://lagukan.com), a music recommendation service.
- [Findka](https://findka.com), a cross-domain recommender system.
- [Lagukan](https://lagukan.com), a music recommender system.
- [FlexBudget](https://github.com/jacobobryant/flexbudget), a [flexible budgeting app](https://notjust.us).

## Contact
Expand Down
11 changes: 8 additions & 3 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{:trident/mono
{:version "0.1.21"
{:version "0.2.0"
:group-id "trident"
:github-repo "jacobobryant/trident"
:cljdoc-dir "/home/arch/dev/cljdoc/"
Expand Down Expand Up @@ -47,6 +47,10 @@
mount]}
firebase {:desc "Functions for authenticating Firebase user tokens."
:deps [com.google.firebase/firebase-admin]}
firestore {:desc "A Clojurey wrapper for Firestore"
:local-deps [util]
:deps [binaryage/oops
org.clojure/core.async]}
views {:desc "Some Reagent components and stuff."
:local-deps [util staticweb]
:deps [re-com]}
Expand Down Expand Up @@ -95,7 +99,8 @@
:deps [jobryant/mock]
:exclude [com.datomic/ion com.datomic/ion-dev]}}
:managed-deps
{cljs-http {:mvn/version "0.1.46"}
{binaryage/oops {:mvn/version "0.7.0"}
cljs-http {:mvn/version "0.1.46"}
com.andrewmcveigh/cljs-time {:mvn/version "0.5.2"}
com.cemerick/url {:mvn/version "0.1.1"}
com.datomic/client-cloud {:mvn/version "0.8.78"}
Expand All @@ -118,7 +123,7 @@
nrepl {:mvn/version "0.6.0"}
orchestra {:mvn/version "2019.02.06-1"}
org.clojure/clojure {:mvn/version "1.10.0"}
org.clojure/core.async {:mvn/version "0.4.490"}
org.clojure/core.async {:mvn/version "1.0.567"}
org.clojure/data.xml {:mvn/version "0.2.0-alpha5"}
org.clojure/data.zip {:mvn/version "0.1.3"}
org.clojure/tools.cli {:mvn/version "0.4.2"}
Expand Down
5 changes: 5 additions & 0 deletions src/trident/firestore.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(ns trident.firestore)

(defmacro write
[& args]
`((fn [] (trident.firestore.util/write-unsafe ~@args))))
56 changes: 56 additions & 0 deletions src/trident/firestore.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
(ns trident.firestore
(:require-macros
[cljs.core.async.macros :refer [go]]
[trident.firestore :refer [write]])
(:require
[trident.firestore.util :as util]
[cljs.core.async :refer [chan put!]]
[oops.core :refer [oget ocall]]
[trident.util :as u]))

(defn subscribe [firestore queries]
(let [c (chan)
*unsubscribe (atom nil)
put-changeset! (fn [changeset]
(when-not (put! c changeset)
(some-> @*unsubscribe #(%))))
fns (for [q queries]
(ocall (util/query->ref firestore q) :onSnapshot
(comp put-changeset!
(if (util/doc-query? q)
util/doc->changeset
util/query-snapshot->changeset))))
unsubscribe (fn [] (mapv #(%) fns))]
(reset! *unsubscribe unsubscribe)
(doall fns)
c))

(defn query [firestore q]
(assert (not (util/doc-query? q))
"You cannot query for a specific document; use pull instead.")
(let [result (ocall (util/query->ref firestore q) :get)]
(go (map util/doc->clj (oget (u/js<! result) :docs)))))

(defn pull [firestore ident]
(assert (and (not (map? ident)) (util/doc-query? ident))
"You cannot pull multiple documents; use query instead.")
(let [result (ocall (util/ident->ref firestore ident) :get)]
(go
(let [doc (u/js<! result)]
(when (oget doc :exists)
(util/doc->clj doc))))))

(defn doc-exists? [firestore ident]
(let [result (ocall (util/ident->ref firestore ident) :get)]
(go (oget (u/js<! result) :exists))))

(defn merge-changeset [db changeset]
(reduce (fn [db [[table id] ent]]
(if ent
(assoc-in db [table id] ent)
(update db table dissoc id)))
db
changeset))

(defn doc->key [{[table id] :ident}]
(u/pred-> id coll? last))
170 changes: 170 additions & 0 deletions src/trident/firestore/util.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
(ns trident.firestore.util
(:require-macros
[cljs.core.async.macros :refer [go]])
(:require
[clojure.set :as set]
[clojure.spec.alpha :as s]
[clojure.string :as str]
[clojure.walk :as walk]
[oops.core :refer [oget ocall oapply+]]
[trident.util :as u]))

; Will this fail in the browser if Firebase is included as an NPM dependency?
(def Timestamp
(or
(u/catchall-js js/firebase.firestore.Timestamp)
(oget (js/require "firebase-admin") "firestore.Timestamp")))

(defn path->ident [path]
(let [table-pos (- (count path) 2)
table (nth path table-pos)
id (u/pred-> (u/dissoc-vec path table-pos)
#(= 1 (count %)) first)]
[table id]))

(defn keywordize-path [path]
(vec (map-indexed (fn [i x] (cond-> x (even? i) keyword)) path)))

(defn path-str->ident [path-str]
(-> path-str
(str/split "/")
keywordize-path
path->ident))

(defn ident->path [[table id]]
(let [id (u/pred-> id string? vector)
doc-id (when (odd? (count id))
(last id))
parent-collection (cond-> id
doc-id butlast)]
(concat parent-collection
[table]
(when doc-id
[doc-id]))))

(defn path->ref [firestore path]
(->> path
(map-indexed vector)
(reduce (fn [obj [i id]]
(if (even? i)
(ocall obj :collection (name id))
(ocall obj :doc id)))
firestore)))

(defn ->query [ident-or-query]
(-> (cond->> ident-or-query
(not (map? ident-or-query)) (hash-map :ident))
(update :ident ident->path)
(set/rename-keys {:ident :path})))

(defn ident->ref [firestore ident]
(path->ref firestore (ident->path ident)))

(defn query->ref [firestore q]
(let [{:keys [path collection-group where order-by limit limit-to-last
start-at start-after end-at end-after]} (->query q)
r (if collection-group
(ocall firestore :collectionGroup (name collection-group))
(path->ref firestore path))
r (reduce (fn [r [attr op value]]
(ocall r :where (name attr) (name op) (clj->js value)))
r
where)
r (reduce (fn [r [arg method]]
(if arg
(oapply+ r method (u/wrap-vec arg))
r))
r
[[(some->> order-by u/wrap-vec (map name)) :orderBy]
[start-at :startAt]
[start-after :startAfter]
[end-at :endAt]
[end-after :endAfter]
[limit :limit]
[limit-to-last :limitToLast]])]
r))

(s/def ::doc-ident
(s/tuple keyword?
(s/or
:doc string?
:subcoll (s/cat
:coll (s/+ (s/cat :table keyword? :id string?))
:doc string?))))

(defn coerce-from-fb [doc]
(walk/postwalk
(fn [x]
(if-some [path-str (u/catchall-js (oget x :path))]
(path-str->ident path-str)
(cond-> x
(= (type x) Timestamp) (ocall :toDate))))
doc))

(defn coerce-coll-idents [firestore coll]
((if (map? coll)
u/map-vals
mapv)
#(cond->> %
(s/valid? ::doc-ident %) (ident->ref firestore))
coll))

(defn coerce-to-fb [firestore x]
(walk/postwalk
(fn [x]
; We can't just check (s/valid? ::doc-ident x) here because it matches
; key-value pairs in maps.
(cond
(inst? x) (ocall Timestamp :fromDate x)
(coll? x) (coerce-coll-idents firestore x)
:default x))
x))

(defn doc->clj [^js doc]
(let [ident (-> doc
(oget "ref.path")
path-str->ident)]
(-> (ocall doc :data)
(js->clj :keywordize-keys true)
coerce-from-fb
(assoc :ident ident))))

(defn doc->changeitem [{:keys [doc exists]}]
(let [{:keys [ident] :as ent} (doc->clj doc)]
[ident (when exists ent)]))

(defn query-snapshot->changeset [snapshot]
(for [change (ocall snapshot :docChanges)
:let [exists (not= "removed" (oget change :type))]]
(doc->changeitem {:doc (oget change :doc)
:exists exists})))

(defn doc->changeset [doc]
[(doc->changeitem {:doc doc
:exists (oget doc :exists)})])

(defn doc-query? [q]
(let [{:keys [collection-group path]} (->query q)]
(and (nil? collection-group)
(even? (count path)))))

(defn write-unsafe
"See https://clojure.atlassian.net/browse/ASYNC-192"
[firestore changeset]
(let [promises
(for [[ident ent] changeset
:let [have-doc-id (doc-query? ident)
r (ident->ref firestore ident)
js-ent (clj->js (coerce-to-fb firestore ent))
merge-ent (:merge (meta ent))
update-ent (:update (meta ent))]]
(cond
(nil? ent) (ocall r :delete)
(not have-doc-id) (ocall r :add js-ent)
merge-ent (ocall r :set js-ent #js {:merge true})
update-ent (ocall r :update js-ent)
:default (ocall r :set js-ent)))]
(doall promises)
(go
(doseq [p promises]
(u/js<! p)))))
10 changes: 10 additions & 0 deletions src/trident/util.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -566,3 +566,13 @@

(defn ceil-at [x n]
(int (* (Math/ceil (/ x n)) n)))

(defn dissoc-vec [v i]
(into (subvec v 0 i) (subvec v (inc i))))

(defn wrap-vec
"Use judiciously."
[x]
(if (coll? x)
x
(vector x)))

0 comments on commit 1d235f3

Please sign in to comment.