forked from deg/re-frame-firebase
-
Notifications
You must be signed in to change notification settings - Fork 1
/
firestore.cljs
371 lines (327 loc) · 16.5 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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
(ns com.degel.re-frame-firebase.firestore
(:require
[clojure.spec.alpha :as s]
[clojure.string :as str]
[re-frame.core :as re-frame]
[reagent.ratom :as ratom :refer [make-reaction]]
[iron.re-utils :as re-utils :refer [<sub >evt event->fn sub->fn]]
[iron.utils :as utils]
[com.degel.re-frame-firebase.core :as core]
[com.degel.re-frame-firebase.specs :as specs]
[com.degel.re-frame-firebase.helpers :refer [promise-wrapper]]
["@firebase/firestore" :refer (initializeFirestore DocumentReference doc getDoc collection CollectionReference getDocs onSnapshot setDoc FieldPath)]))
(defn set-firestore-settings
[settings]
(swap! core/firebase-state assoc
:firestore (initializeFirestore (:app @core/firebase-state)
(clj->js (or settings {})))))
;; Extra public functions
(defn server-timestamp
"Returns a field value to be used to store the server timestamp.
See https://firebase.google.com/docs/firestore/manage-data/add-data#update_fields_in_nested_objects
You should use this as a field value when setting/updating/adding a document.
Example usage:
{:firestore/add {:path [:some-colection]
:data {:name \"document-with-timestamp\"
:timestamp (server-timestamp)}}"
[]
(.serverTimestamp js/firebase.firestore.FieldValue))
(defn delete-field-value
"Returns a field value to be used to delete a field.
See https://firebase.google.com/docs/firestore/manage-data/delete-data#fields
When updating a document, you should use this as a field value if you want to
delete such field.
Example usage:
{:firestore/update {:path [:my \"document\"]
:data {:field-to-delete (delete-field-value)}}}"
[]
(.delete js/firebase.firestore.FieldValue))
(defn document-id-field-path
"Returns a field path which can be used to refer to ID of a document.
See https://firebase.google.com/docs/reference/js/firebase.firestore.FieldPath#.documentId
It can be used in queries to sort or filter by the document ID.
Example usage:
{:firestore/get {:path-collection [:my-collection]
:where [[(document-id-field-path) :>= \"start\"]]}}"
[]
(.documentId js/firebase.firestore.FieldPath))
;; Type Conversion/Parsing
(defn clj->CollectionReference
"Converts a seq of keywords and/or strings into a CollectionReference.
The seq represents the path to the collection (e.g. [:path \"to\" :collection]).
See https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference"
[path]
{:pre [(utils/validate ::specs/path-collection path)]}
(if (instance? CollectionReference path)
path
(collection (:firestore @core/firebase-state)
(str/join "/" (clj->js path)))))
(defn clj->DocumentReference
"Converts a seq of keywords and/or strings into a DocumentReference.
The seq represents the path to the document (e.g. [:path-to \"document\"]).
See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference"
[path]
{:pre [(utils/validate ::specs/path-document path)]}
(if (instance? DocumentReference path)
path
(doc (:firestore @core/firebase-state)
(str/join "/" (clj->js path)))))
(defn clj->FieldPath
"Converts a string/keyword or a seq of string/keywords into a FieldPath.
Uses the FieldPath contructor.
Only tries conversion if the argument isn't a FieldPath already.
Possible arguments: \"string.dotted.path\", :keyword-path, [:path :in-a :seq], a FieldPath object.
See https://firebase.google.com/docs/reference/js/firebase.firestore.FieldPath"
[field-path]
(cond
(nil? field-path) nil
(instance? FieldPath field-path) field-path
(coll? field-path) (apply js/firebase.firestore.FieldPath. (clj->js field-path))
:else (FieldPath. (clj->js field-path))))
(defn clj->SetOptions
"Converts a clojure-style map into a SetOptions satisfying one.
The provided map can contain a :merge key with either true or false, and a
:merge-fields key with a seq of field paths to be passed to clj->FieldPath.
See https://firebase.google.com/docs/reference/js/firebase.firestore.SetOptions"
[set-options]
(as-> {} $
(if (:merge set-options) (assoc $ :merge (:merge set-options)) $)
(if (:marge-fields set-options)
(assoc $ :mergeFields (into-array (map clj->FieldPath (:merge-fields set-options))))
$)
(clj->js $)))
(defn clj->GetOptions
"Converts a clojure-style map into a GetOptions satisfying one.
The provided map can contain a :source key with one of the following values:
:default, :server or :cache. You can also provide a string like \"server\".
See https://firebase.google.com/docs/reference/js/firebase.firestore.GetOptions"
[get-options]
(if get-options
(clj->js {:source (:source get-options :default)})
#js {}))
(defn clj->SnapshotListenOptions
"Converts a clojure-style map into a SnapshotListenOptions satisfying one.
The provided map can contain a :include-metadata-changes key with either true or false.
See https://firebase.google.com/docs/reference/js/firebase.firestore.SnapshotListenOptions"
[snapshot-listen-options]
(if snapshot-listen-options
(clj->js {:includeMetadataChanges (:include-metadata-changes snapshot-listen-options false)})
#js {}))
(defn clj->SnapshotOptions
"Converts a clojure-style map into a SnapshotOptions satisfying one.
The provided map can containe a :server-timestamps key with one of the following values:
:estimate, :previous or :none. You can also provide a string like \"estimate\".
See https://firebase.google.com/docs/reference/js/firebase.firestore.SnapshotOptions"
[snapshot-options]
(clj->js {:serverTimestamps (:server-timestamps snapshot-options :none)}))
(defn PathReference->clj [reference]
;; [TODO]: Can this be optimized through some internal property of a Reference?
"Converts a CollectionReference/DocumentReference into a vector of strings representing its path."
(loop [ref reference
result '()]
(if ref
(recur (.-parent ref) (conj result (.-id ref)))
(vec result))))
(defn SnapshotMetadata->clj [metadata]
"Converts a SnapshotMetadata object into a clojure-style map."
{:from-cache (.-fromCache metadata)
:has-pending-writes (.-hasPendingWrites metadata)})
(defn DocumentSnapshot->clj
"Converts a DocumentSnapshot object into a clojure-style map.
:data the document's contents (nil if it doesn't exist).
:id a string representing document's id.
:metadata metadata converted with SnapshotMetadata->clj.
:ref the object's path converted with PathReference->clj.
:object the original DocumentSnapshot if expose-objects argument
is set to true (nil otherwise).
See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot"
([doc]
(DocumentSnapshot->clj doc nil nil nil))
([doc snapshot-options]
(DocumentSnapshot->clj doc snapshot-options nil nil))
([doc snapshot-options expose-objects]
(DocumentSnapshot->clj doc snapshot-options expose-objects nil))
([doc snapshot-options expose-objects sure-exists]
{:data (when (or sure-exists (.-exists doc))
(js->clj (.data doc (clj->SnapshotOptions snapshot-options))))
:id (.-id doc)
:metadata (SnapshotMetadata->clj (.-metadata doc))
:ref (PathReference->clj (.-ref doc))
:object (when expose-objects doc)}))
(defn DocumentChange->clj
"Converts a DocumentChange object into a clojure-style map.
:doc the DocumentSnapshot converted with DocumentSnapshot->clj.
:new-index a number.
:old-index a number.
:type a string.
:object the original DocumentChange if expose-objects argument
is set to true (nil otherwise).
See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentChange"
([change] (DocumentChange->clj change nil nil))
([change snapshot-options] (DocumentChange->clj change snapshot-options nil))
([change snapshot-options expose-objects]
{:doc (DocumentSnapshot->clj (.-doc change) snapshot-options expose-objects true)
:new-index (.-newIndex change)
:old-index (.-oldIndex change)
:type (.-type change)
:object (when expose-objects change)}))
(defn QuerySnapshot->clj
"Converts a QuerySnapshot object into a clojure-style map.
:docs vector of documents converted with DocumentSnapshot->clj.
:metadata metadata converted with SnapshotMetadata->clj.
:size the number of documents.
:doc-changes vector of DocumentChanges converted with DocumentChange->clj if
doc-changes argument is set to true (nil otherwise).
:object the original DocumentSnapshot if expose-objects argument
is set to true (nil otherwise).
See https://firebase.google.com/docs/reference/js/firebase.firestore.QuerySnapshot"
([query]
(QuerySnapshot->clj query nil nil nil nil))
([query snapshot-options]
(QuerySnapshot->clj query snapshot-options nil nil nil))
([query snapshot-options snapshot-listen-options]
(QuerySnapshot->clj query snapshot-options snapshot-listen-options nil nil))
([query snapshot-options snapshot-listen-options doc-changes]
(QuerySnapshot->clj query snapshot-options snapshot-listen-options doc-changes nil))
([query snapshot-options snapshot-listen-options doc-changes expose-objects]
{:docs (vec (map #(DocumentSnapshot->clj % snapshot-options expose-objects true)
(.-docs query)))
:metadata (SnapshotMetadata->clj (.-metadata query))
:size (.-size query)
:doc-changes (when doc-changes
(vec (map #(DocumentChange->clj % snapshot-options expose-objects)
(.docChanges query (clj->SnapshotListenOptions snapshot-listen-options)))))
:object (when expose-objects query)}))
(defn- document-parser-wrapper [callback snapshot-options expose-objects]
{:pre [(utils/validate (s/nilable :re-frame/vec-or-fn) callback)]}
(when callback
#((re-utils/event->fn callback)
(DocumentSnapshot->clj % snapshot-options expose-objects false))))
(defn- collection-parser-wrapper [callback snapshot-options snapshot-listen-options doc-changes expose-objects]
{:pre [(utils/validate (s/nilable :re-frame/vec-or-fn) callback)]}
(when callback
#((re-utils/event->fn callback)
(QuerySnapshot->clj % snapshot-options snapshot-listen-options doc-changes expose-objects))))
(defn- reference-parser-wrapper [callback]
{:pre [(utils/validate (s/nilable :re-frame/vec-or-fn) callback)]}
(when callback #((re-utils/event->fn callback) (PathReference->clj %))))
;; re-frame Effects/Subscriptions
(defn- setter
([path data set-options]
(setDoc (clj->DocumentReference path)
(clj->js data)
(clj->SetOptions set-options)))
([instance path data set-options]
(setDoc instance
(clj->DocumentReference path)
(clj->js data)
(clj->SetOptions set-options))))
(defn- updater
([path data] (.update (clj->DocumentReference path) (clj->js data)))
([instance path data] (.update instance (clj->DocumentReference path) (clj->js data))))
(defn- deleter
([path] (.delete (clj->DocumentReference path)))
([instance path] (.delete instance (clj->DocumentReference path))))
(defn set-effect [{:keys [path data set-options on-success on-failure]}]
(promise-wrapper (setter path data set-options) on-success on-failure))
(defn update-effect [{:keys [path data on-success on-failure]}]
(promise-wrapper (updater path data) on-success on-failure))
(defn delete-effect [{:keys [path on-success on-failure]}]
(promise-wrapper (deleter path) on-success on-failure))
(defn write-batch-effect [{:keys [operations on-success on-failure]}]
(let [batch-instance (.batch (js/firebase.firestore))]
(run! (fn [[event-type {:keys [path data set-options]}]]
(case event-type
:firestore/delete (deleter batch-instance path)
:firestore/set (setter batch-instance path data set-options)
:firestore/update (updater batch-instance path data)
(js/alert "Internal error: unknown write effect: " event-type)))
operations)
(promise-wrapper (.commit batch-instance) on-success on-failure)))
(defn- adder [path data]
(.add (clj->CollectionReference path) (clj->js data)))
(defn add-effect [{:keys [path data on-success on-failure]}]
(promise-wrapper (adder path data) (reference-parser-wrapper on-success) on-failure))
(defn- query [ref where order-by limit
start-at start-after end-at end-before]
(as-> ref $
#_(if where
(reduce
(fn [$$ [field-path op value]] (.where $$ (clj->FieldPath field-path) (clj->js op) (clj->js value)))
$ where)
$)
(if order-by
(reduce
(fn [$$ order] (.orderBy $$ (clj->js (nth order 0)) (clj->js (nth order 1 :asc))))
$ order-by)
$)
(if limit (.limit $ limit) $)
(if start-at (.apply (.-startAt $) $ (clj->js start-at)) $)
(if start-after (.apply (.-startAfter $) $ (clj->js start-after)) $)
(if end-at (.apply (.-endAt $) $ (clj->js end-at)) $)
(if end-before (.apply (.-endBefore $) $ (clj->js end-before)) $)))
(defn- getter-document [path get-options]
(getDoc (clj->DocumentReference path) (clj->GetOptions get-options)))
(defn- getter-collection [path get-options where order-by limit
start-at start-after end-at end-before]
(getDocs (query (clj->CollectionReference path) where order-by limit
start-at start-after end-at end-before)
(clj->GetOptions get-options)))
(defn get-effect [{:keys [path-document
path-collection where order-by limit
start-at start-after end-at end-before
doc-changes snapshot-listen-options
get-options snapshot-options expose-objects
on-success on-failure]}]
(if path-document
(promise-wrapper (getter-document path-document get-options)
(document-parser-wrapper on-success snapshot-options expose-objects)
on-failure)
(promise-wrapper (getter-collection path-collection get-options where order-by limit
start-at start-after end-at end-before)
(collection-parser-wrapper on-success snapshot-options snapshot-listen-options
doc-changes expose-objects)
on-failure)))
(defn- on-snapshotter [reference-or-query snapshot-listen-options on-next on-error register-listener]
(-> (onSnapshot reference-or-query
(clj->SnapshotListenOptions snapshot-listen-options)
on-next
(if on-error (event->fn on-error) (core/default-error-handler)))
register-listener))
(defn on-snapshot [{:keys [path-document
path-collection where order-by limit
start-at start-after end-at end-before doc-changes
snapshot-listen-options snapshot-options
expose-objects
on-next on-error register-listener]}]
{:pre [(utils/validate :re-frame/vec-or-fn on-next)
(utils/validate (s/nilable :re-frame/vec-or-fn) on-error)]}
(if path-document
(on-snapshotter (clj->DocumentReference path-document)
snapshot-listen-options
(document-parser-wrapper on-next snapshot-options expose-objects)
on-error
register-listener)
(on-snapshotter (query (clj->CollectionReference path-collection) where order-by limit
start-at start-after end-at end-before)
snapshot-listen-options
(collection-parser-wrapper on-next snapshot-options snapshot-listen-options
doc-changes expose-objects)
on-error
register-listener)))
(def on-snapshot-effect on-snapshot)
(defn on-snapshot-sub [app-db [_ params]]
;; [TODO] Potential bug alert:
;; This works the same way as database/on-value-sub, except for UUIDs.
(let [uuid (str (random-uuid))
callback #(>evt [::on-snapshot-handler uuid %])
unsubscribe (on-snapshot (assoc params :on-next callback))]
(ratom/make-reaction
(fn [] (get-in @app-db [::cache uuid] []))
:on-dispose #(do (unsubscribe) (>evt [::on-snapshot-handler uuid nil])))))
(re-frame/reg-event-db
::on-snapshot-handler
(fn [app-db [_ uuid value]]
(if value
(assoc-in app-db [::cache uuid] value)
(update app-db ::cache dissoc uuid))))