/
firestore.cljs
285 lines (252 loc) · 7.61 KB
/
firestore.cljs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
(ns firemore.firestore
(:require
[cljs.core.async :as async]
[clojure.string :as string]
[firemore.config :as config]
[firemore.firebase :as firebase])
(:require-macros
[cljs.core.async.macros :refer [go-loop go]]
[firemore.firestore-macros :refer [transact-db!]]))
(def FB firebase/FB)
(def ^:dynamic *transaction* nil)
(def ^:dynamic *transaction-unwritten-docs* nil)
(def server-timestamp (.serverTimestamp js/firebase.firestore.FieldValue))
(defn disj-reference
"Remove 'reference from list of items that must be written in transaction"
[reference]
(swap! *transaction-unwritten-docs* disj reference))
(defn ref
"Convert a firemore reference to a firebase reference"
([path] (ref FB path))
([fb path]
(loop [[p & ps] path
collection? true
obj (firebase/db fb)]
(let [new-obj (if collection?
(.collection obj (name p))
(.doc obj p))]
(if (empty? ps)
new-obj
(recur ps (not collection?) new-obj))))))
(defn str->keywordize
"If s begins with ':' then convert into a keyword, else returns 's"
{:pre [(string? s)]}
[s]
(if (= (subs s 0 1) ":")
(as-> s $
(subs $ 1)
(string/split $ "/")
(apply keyword $))
s))
(defn keywordize->str
"Mirror function for str->keywordize"
{:pre [(keyword? k)]}
[k]
(str k))
(defn jsonify [value]
(clj->js value :keyword-fn keywordize->str))
(defn clojurify [json-document]
(reduce-kv
#(assoc %1 (str->keywordize %2) %3)
{}
(js->clj json-document)))
(defn replace-timestamp
"Replace `config/TIMESTAMP (keyword) with firebase Server Timestamp"
[m]
(reduce-kv
(fn [m k v]
(if (= v config/TIMESTAMP)
(assoc m k server-timestamp)
m))
m
m))
(defn build-path [fb path]
(merge
{:ref (ref fb path)
:path path}
(when (-> path count even?)
{:id (peek path)})))
(defn expand-where [where]
(when where
(let [[expression-1] where]
(when-not (vector? expression-1)
{:where [where]}))))
(defn convert-if-string [order-expression]
(if (string? order-expression)
[order-expression "asc"]
order-expression))
(defn expand-order [order]
(when order
{:order (mapv convert-if-string order)}))
(defn expand-query [query]
(let [{:keys [where order limit start-at start-after end-at end-before]} query]
(merge
query
(expand-where where)
(expand-order order))))
(defn build-query [fb path query]
(merge
(build-path fb path)
{:query (expand-query query)}))
(defn shared-db
([fb reference]
{:pre [(vector? reference)]}
(cond
(-> reference peek map?) (build-query fb (pop reference) (peek reference))
(-> reference count odd?) (build-query fb reference {})
:else (build-path fb reference)))
([fb reference value]
(merge
(shared-db fb reference)
{:js-value (-> value replace-timestamp jsonify)})))
(defn promise->chan
([fx]
(promise->chan
fx
(fn [c] (async/close! c))))
([fx on-success]
(promise->chan
fx
on-success
(fn [c error]
(js/console.log error)
(async/put! c error)
(async/close! c))))
([fx on-success on-failure]
(if *transaction*
(fx)
(let [c (async/chan)]
(..
(fx)
(then (partial on-success c))
(catch (partial on-failure c)))
c))))
(defn set-db!
([reference value] (set-db! FB reference value))
([fb reference value]
(let [{:keys [ref js-value]} (shared-db fb reference value)]
(promise->chan
(if *transaction*
#(do (.set *transaction* ref js-value)
(disj-reference reference))
#(.set ref js-value))))))
(defn add-db!
([reference value] (add-db! FB reference value))
([fb reference value]
(let [{:keys [ref js-value]} (shared-db fb reference value)]
(promise->chan
(if *transaction*
#(do (.add *transaction* ref js-value)
(disj-reference reference))
#(.add ref js-value))
(fn [c docRef]
(async/put! c {:id (.-id docRef)})
(async/close! c))))))
(defn update-db!
([reference value] (update-db! FB reference value))
([fb reference value]
(let [{:keys [ref js-value]} (shared-db fb reference value)]
(promise->chan
(if *transaction*
#(do (.update *transaction* ref js-value)
(disj-reference reference))
#(.update ref js-value))))))
(defn delete-db!
([reference] (delete-db! FB reference))
([fb reference]
(let [{:keys [ref]} (shared-db fb reference nil)]
(promise->chan
(if *transaction*
#(do (.delete *transaction* ref)
(disj-reference reference))
#(.delete ref))))))
(defn add-where-to-ref [ref query]
(reduce
(fn [ref [k op v]]
(.where ref (str k) op (if (coll? v) (clj->js v) v)))
ref
(:where query)))
(defn add-order-to-ref [ref query]
(reduce
(fn [ref [k direction]] (.orderBy ref (str k) direction))
ref
(:order query)))
(defn add-limit-to-ref [ref query]
(if-let [limit (:limit query)]
(.limit ref limit)
ref))
(defn filter-by-query [ref query]
(-> ref
(add-where-to-ref query)
(add-order-to-ref query)
(add-limit-to-ref query)))
(defn doc-upgrader
([doc] (doc-upgrader doc nil))
([doc removed?]
(if-let [exists? (.-exists doc)]
(with-meta
(clojurify (.data doc))
{:id (.-id doc)
:removed? removed?
:exists? exists?
:pending? (.. doc -metadata -hasPendingWrites)})
config/NO_DOCUMENT)))
(defn get-db
([reference]
(get-db FB reference))
([fb reference]
(let [{:keys [ref query]} (shared-db fb reference)]
(if query
(promise->chan
#(.get (filter-by-query ref query))
(fn [c snapshot]
(let [a (atom [])]
(.forEach snapshot #(swap! a conj (doc-upgrader %)))
(async/put! c @a)
(async/close! c))))
(promise->chan
#(.get ref)
(fn [c doc]
(->> doc doc-upgrader (async/put! c))
(async/close! c)))))))
;; TODO: This removed? argument is nonsense, but for some reason `exists` in the document
;; does not agree with "removed" from the change...
(defn doc-handler
([c doc] (doc-handler c doc nil))
([c doc removed?]
(async/put!
c
(doc-upgrader doc removed?))))
(defn listen-to-document
([reference] (listen-to-document FB reference))
([fb reference]
(let [{:keys [ref query]} (shared-db fb reference nil)
c (async/chan)
doc-fx (partial doc-handler c)
fx (if query
(fn [snapshot]
(.forEach (.docChanges snapshot)
(fn [change]
(doc-fx (.-doc change)
;; TODO: More of the same nonsense
(= "removed" (.-type change))))))
doc-fx)
unsubscribe (.onSnapshot (if query (filter-by-query ref query) ref)
fx)
unsubscribe-fx #(do (async/close! c) (unsubscribe))]
{:c c :unsubscribe unsubscribe-fx})))
(defn listen-to-collection
([reference] (listen-to-collection FB reference))
([fb reference]
(let [{:keys [ref query]} (shared-db fb reference nil)
c (async/chan)
fx (fn [snapshot]
(let [a (atom [])]
(.forEach snapshot #(swap! a conj (doc-upgrader %)))
(async/put! c @a)))
unsubscribe (.onSnapshot (filter-by-query ref query)
fx)
unsubscribe-fx #(do (async/close! c) (unsubscribe))]
{:c c :unsubscribe unsubscribe-fx})))
(defn unlisten-db [{:keys [unsubscribe]}]
(unsubscribe))