├── .gitignore
├── CHANGELOG.md
├── README.md
├── examples
└── firestore
│ ├── .gitignore
│ ├── README.md
│ ├── firebase.json
│ ├── firestore.indexes.json
│ ├── firestore.rules
│ ├── project.clj
│ ├── resources
│ └── public
│ │ ├── index.html
│ │ └── style.css
│ └── src
│ └── firestore
│ ├── api-keys.cljs
│ └── core.cljs
├── project.clj
├── scripts
├── brepl
├── brepl.bat
├── brepl.clj
├── build
├── build.bat
├── build.clj
├── release
├── release.bat
├── release.clj
├── repl
├── repl.bat
├── repl.clj
├── watch
├── watch.bat
└── watch.clj
└── src
└── com
└── degel
├── re_frame_firebase.cljs
└── re_frame_firebase
├── auth.cljs
├── core.cljs
├── database.cljs
├── firestore.cljs
├── helpers.cljs
└── specs.cljc
/.gitignore:
--------------------------------------------------------------------------------
1 | *#
2 | *jar
3 | *~
4 | .lein-deps-sum
5 | .lein-plugins/
6 | .lein-repl-history
7 | .repl*
8 | /*.iml
9 | /.cljs_node_repl/
10 | /.idea
11 | /classes/
12 | /lib/
13 | /out/
14 | /pom.xml.asc
15 | /release/
16 | /target/
17 | checkouts/
18 | pom.xml
19 |
20 | .firebaserc
21 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [Unreleased]
4 | - (no changes yet)
5 |
6 | ## [0.8.0]
7 | - Fixes to Firestore implementation, docs, and example
8 | - Support for phone auth
9 | - Remove explicit dependency on reagent, to avoid conflicts
10 | - Make some private vars public since they were used cross-namespace, and had triggered
11 | new ClojureScript compiler warnings
12 | - Update dependencies, including to Clojure 1.10 and Firebase 5.7
13 |
14 | ## [0.7.0]
15 | Another volunteer-driven release. Thanks Mathew and Shen!
16 | - Support for anonymous sign-in and custom authentication
17 | - Doc cleanups
18 |
19 | ## [0.6.0]
20 | Many thanks to the project contributors. I did nothing this release but integrate the
21 | work from wonderful volunteers.
22 | - Support for Firestore.
23 | - Support for email authentication and registration
24 | - Return new key created by Firebase Push
25 | - Code reorganization and cleanups
26 |
27 | ## [0.5.0]
28 | - Update Clojure and Firebase dependencies
29 |
30 | ## [0.4.0]
31 | - Dependency change: Replace Sodium library with Iron
32 | - Make :-on handler more error-resistant
33 |
34 | ## [0.3.0]
35 | - Add account linking
36 | - Update dependencies
37 | - Add connection state monitoring
38 | - Use Firebase 4.4.0
39 |
40 | ## [0.2.0]
41 | - Add Facebook, Twitter, and Github authg
42 |
43 | ## [0.1.0]
44 | - Initial project: Wrapper around Firebase
45 | - Support basic read/write/watch
46 | - Google auth
47 |
48 | [Unreleased]: https://github.com/deg/re-frame-firebase/compare/c4ee44a...HEAD
49 | [0.8.0]: https://github.com/deg/re-frame-firebase/compare/684812e...c4ee44a
50 | [0.7.0]: https://github.com/deg/re-frame-firebase/compare/46f5630...684812e
51 | [0.6.0]: https://github.com/deg/re-frame-firebase/compare/7192dfc...46f5630
52 | [0.5.0]: https://github.com/deg/re-frame-firebase/compare/0c4cb21...7192dfc
53 | [0.4.0]: https://github.com/deg/re-frame-firebase/compare/41e6695...0c4cb21
54 | [0.3.0]: https://github.com/deg/re-frame-firebase/compare/90f163f...41e6695
55 | [0.2.0]: https://github.com/deg/re-frame-firebase/compare/4804b1f...90f163f
56 | [0.1.0]: https://github.com/deg/re-frame-firebase/compare/b2f1711...4804b1f
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # re-frame-firebase
2 |
3 | [Re-frame](https://github.com/Day8/re-frame) wrapper around Google's
4 | [Firebase](https://firebase.google.com) database.
5 |
6 |
7 | ## Overview
8 |
9 | There are already several ClojureScript wrappers of Firebase, most notably
10 | [Matchbox](https://github.com/crisptrutski/matchbox). However, I was not able to find
11 | any that work with recent version of Firebase, nor that smoothly integrate with
12 | re-frame.
13 |
14 | Re-frame-firebase is based on ideas, and some code, from Timothy Pratley's [blog
15 | post](http://timothypratley.blogspot.co.il/2016/07/reacting-to-changes-with-firebase-and.html)
16 | and [VoterX](https://github.com/timothypratley/voterx) project. I've added the packaging
17 | as a standalone project, the integration with re-frame and, I'm sure, any mistakes that
18 | I've not yet caught.
19 |
20 | _Note: This project is not under active development, and exists primarily to meet my
21 | immediate needs. Therefore, many Firebase features are still missing. I will probably
22 | only add them as I need. But, I am receptive to feature requests and happy to accept
23 | PRs. I would also like to thank the growing family of contributors who have added so
24 | much to this project. I've probably written less than half the code here!_
25 |
26 | ## Configuration
27 |
28 | [](https://clojars.org/com.degel/re-frame-firebase)
29 | [](https://versions.deps.co/deg/re-frame-firebase)
30 |
31 | - Add this project to your dependencies. The current version is
32 | `[com.degel/re-frame-firebase "0.8.0"]`. Note this automatically includes firebase
33 | too; currently v5.7.3-1.
34 | - Reference the main namespace in your code: `[com.degel.re-frame-firebase :as firebase]`
35 | - Initialize the library in your app initialization, probably just before you call
36 | `(mount-root)`. See below for details.
37 |
38 | ## Usage
39 |
40 | The public portions of the API are all in
41 | [re\_frame\_firebase.cljs](src/com/degel/re_frame_firebase.cljs). That file also
42 | includes API documentation that may sometimes be more current or complete than
43 | what is here.
44 |
45 | This is a re-frame API. It is primarily accessed through re-frame events and
46 | subscriptions.
47 |
48 | ### Initialization
49 |
50 | Initialize the library in your app initialization, probably just before you call `(mount-root)`.
51 |
52 |
53 | ```clojure
54 | ;;; From https://console.firebase.google.com/u/0/project/trilystro/overview - "Add Firebase to your web app"
55 | (defonce firebase-app-info
56 | {:apiKey "YOUR-KEY-HERE"
57 | :authDomain "YOUR-APP.firebaseapp.com"
58 | :databaseURL "https://YOUR-APP.firebaseio.com"
59 | :storageBucket "YOUR-APP.appspot.com"})
60 |
61 | (defn ^:export init []
62 | ,,,
63 | (firebase/init :firebase-app-info firebase-app-info
64 | ; See: https://firebase.google.com/docs/reference/js/firebase.firestore.Settings
65 | :firestore-settings {:timestampsInSnapshots true}
66 | :get-user-sub [:user]
67 | :set-user-event [:set-user]
68 | :default-error-handler [:firebase-error])
69 | ,,,
70 | )
71 |
72 | ```
73 |
74 | This initialization does two things:
75 |
76 | 1. It supplies your Firebase credentials to this library
77 | 2. It defines several callbacks to your project from the library
78 |
79 | #### Credentials
80 |
81 | You need to create your Firebase project on its
82 | [site](https://firebase.google.com). This will supply you with a set of credentials: an
83 | API key, domain, URL, and bucket. Mimicking the sample above, copy these into your code.
84 |
85 | Note that it is ok to have these credentials visible in your client-side code. But, you
86 | must configure Firebase rules to safely control access to your database.
87 |
88 | #### Callbacks
89 |
90 | This library relies on your code to implement two behaviors:
91 | - Storing the user object
92 | - Reporting errors
93 |
94 | It communicates with your code via three callbacks that you define in `firebase/init`:
95 | `:get-user-sub`, `:set-user-event`, and `:default-error-handler`. The first of these is
96 | normally a re-frame subscription vector, while the latter two are re-frame event
97 | vectors. As is typical in re-frame, info will be passed by appending it to the the vector.
98 |
99 | Note that re-frame-firebase uses the [Iron](https://github.com/deg/iron) library,
100 | which supports passings functions instead of re-frame subscriptions or events. Each of
101 | these callbacks can therefore also be plain functions.
102 |
103 | For more details, e.g. the parameters passed to each callback, see the documentation in
104 | the [source](src/com/degel/re_frame_firebase.cljs).
105 |
106 | You can also see some sample code in my toy project
107 | [trilystro](https://github.com/deg/trilystro). But, tread carefully here, this is my
108 | experimental stomping ground and things may be broken at any time.
109 |
110 |
111 | ### Authentication
112 |
113 | Firebase supports a variety of user authentication mechanisms. Currently,
114 | re-frame-firebase supports the following Firebase authentication providers:
115 | - Google
116 | - Facebook
117 | - Twitter
118 | - GitHub
119 | - Email/password
120 |
121 | (PRs welcome that add to this!)
122 |
123 | Before an authentication provider can be used, it has to be enabled and configured in
124 | the [Firebase Console](https://console.firebase.google.com/u/0/) (Authentication ->
125 | Sign-in method section).
126 |
127 | You need to write three events: two to handle login and logout requests from your views,
128 | and and one to store the user information returned to you from the library. You also
129 | need to write a subscription to return the user information to the library. For
130 | example:
131 |
132 | ```clojure
133 | ;;; Simple sign-in event. Just trampoline down to the re-frame-firebase
134 | ;;; fx handler.
135 | (re-frame/reg-event-fx
136 | :sign-in
137 | (fn [_ _] {:firebase/google-sign-in {:sign-in-method :popup}}))
138 |
139 |
140 | ;;; Ditto for sign-out
141 | (re-frame/reg-event-fx
142 | :sign-out
143 | (fn [_ _] {:firebase/sign-out nil}))
144 |
145 |
146 | ;;; Store the user object
147 | (re-frame/reg-event-db
148 | :set-user
149 | (fn [db [_ user]]
150 | (assoc db :user user)))
151 |
152 | ;;; A subscription to return the user to the library
153 | (re-frame/reg-sub
154 | :user
155 | (fn [db _] (:user db)))
156 |
157 | ```
158 |
159 | The user object contains several opaque fields used by the library and firebase,
160 | and also several fields that may be useful for your application, including:
161 |
162 | - `:display-name` - The user's full name
163 | - `:email` - The user's email address
164 | - `:photo-url` - The user's photo
165 | - `:uid` - The user's unique id, used by Firebase. Helpful for setting up private areas in
166 | the db
167 |
168 | #### Email Authentication
169 |
170 | When using email/password authentication, one usually has to register first (the alternative
171 | is to create an account using the [Firebase admin console](https://console.firebase.google.com/)).
172 | So the application could provide both a means of registering a new user, and to log in as the
173 | created user later on.
174 | When registering a new user, you should use the `:firebase/email-create-user` effect. If the
175 | information is valid (e.g. the user does not exist already) then it will automatically trigger
176 | a sign in.
177 |
178 | For authenticating an already existing account, use the `:firebase/email-sign-in` effect. Example:
179 |
180 | ```clojure
181 | ;;; Create a new user
182 | (re-frame/reg-event-fx
183 | :create-by-email
184 | (fn [_ [_ email pass]]
185 | {:firebase/email-create-user {:email email :password pass}}))
186 |
187 |
188 | ;;; Sign in by email
189 | (re-frame/reg-event-fx
190 | :sign-in-by-email
191 | (fn [_ [_ email pass]]
192 | {:firebase/email-sign-in {:email email :password pass}}))
193 |
194 | ```
195 |
196 | The rest of the procedure is the same as for the OAuth methods.
197 |
198 | ### Anonymous Authentication
199 |
200 | Anonymous authentication allows persisting of information before users sign up.
201 |
202 | ```clojure
203 | ;;; Create anonymous user
204 | (re-frame/reg-event-fx
205 | :sign-in-anonymous
206 | (fn [_ _]
207 | {:firebase/anonymous-sign-in nil}))
208 |
209 | ```
210 |
211 | ### Custom Authentication
212 |
213 | This relies on an external system to generate a JWT, signed with a
214 | private key that is generate by Firebase. See [Firebase docs][custom-auth]
215 | for details how to mint such a token.
216 |
217 | [custom-auth]: https://firebase.google.com/docs/auth/admin/create-custom-tokens
218 |
219 | ```clojure
220 | ;;; Sign in using a custom token
221 | (re-frame/reg-event-fx
222 | :sign-in-custom
223 | (fn [_ _]
224 | {:firebase/custom-token-sign-in {:token "eyJhbGciOiJS.."}}))
225 |
226 | ```
227 |
228 | ### Phone Authentication
229 |
230 | This sends an SMS to a cellphone, to authenticate against the number. One complication is
231 | that a reCaptcha setup is required to avoid abuse. Only the invisible reCaptcha is implemented
232 | at the moment.
233 |
234 | See [Firebase docs][phone-auth] for details.
235 |
236 | [phone-auth]: https://firebase.google.com/docs/auth/web/phone-auth
237 |
238 | ```clojure
239 | (re-frame/reg-event-fx
240 | ::init-captcha
241 | (fn [_ _]
242 | {:firebase/init-recaptcha {:on-solve [:msg "Welcome Human"]
243 | :container-id "sign-in-button"}}))
244 |
245 |
246 | (re-frame/reg-event-fx
247 | ::phone-sign-in
248 | (fn [_ [_ phone]]
249 | {:firebase/phone-number-sign-in {:phone-number phone
250 | :on-send [:msg "SMS code sent"]}}))
251 |
252 |
253 | (re-frame/reg-event-fx
254 | ::phone-confirm-code
255 | (fn [_ [_ code]]
256 | {:firebase/phone-number-confirm-code {:code code}}))
257 |
258 | ```
259 |
260 | ### Writing to the database
261 |
262 | The firebase database is a tree. You can write values to nodes in a tree, or push them
263 | to auto-generated unique sub-nodes of a node. In re-frame-firebase, these are exposed
264 | through the `:firebase/write` `:firebase/push` and `:firebase/update` effect handlers.
265 |
266 | Each takes parameters:
267 | - `:path` - A vector representing a node in the firebase tree, e.g. `[:my :node]`
268 | - `:value` - The value to write or push.
269 | - `:on-success` - Event vector or function to call when write succeeds.
270 | - `:on-failure` - Event vector or function to call with the error.
271 |
272 | Example:
273 |
274 | ```clojure
275 | (re-frame/reg-event-fx
276 | :write-status
277 | (fn [{db :db} [_ status]]
278 | {:firebase/write {:path [:status]
279 | :value status
280 | :on-success #(js/console.log "Wrote status")
281 | :on-failure [:handle-failure]}}))
282 |
283 | ;;; :firebase/push is treated the same but responds with the key of the created object
284 |
285 | ```
286 |
287 | Example (diff in bold):
288 |
289 |
290 | (re-frame/reg-event-fx
291 | :write-status
292 | (fn [{db :db} [_ status]]
293 | {:firebase/push {:path [:status]
294 | :value status
295 | :on-success #(js/console.log (str "New Status push key: " %) )
296 | :on-failure [:handle-failure]}}))
297 |
298 |
299 | > **Note:** Events will also receive the same creation key. `(rf/reg-event-fx :event-name (fn [ctx [_ key]])`
300 |
301 |
302 | `:firebase/update` can write to children subnodes, without overwriting children's siblings. Use
303 | a clojure map of children subnode(s) and their value(s). `:firebase/write` overwrites all children.
304 |
305 | Example (diff in bold):
306 |
307 |
308 | (re-frame/reg-event-fx
309 | :update-status
310 | (fn [{db :db} [_ status-children ]] ;; status-children is e.g. {:life 42, :universe 42, :everything 42}
311 | {:firebase/update {:path [:status]
312 | :value status-children
313 | :on-success #(js/console.log "Updated status-children" )
314 | :on-failure [:handle-failure]}}))
315 |
316 |
317 | > **Note:** Multi-location updates are [atomic][multi-location-update-blogpost].
318 |
319 | [multi-location-update-blogpost]: https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html
320 |
321 | Re-frame-firebase also supplies `:firebase/multi` to allow multiple write and/or
322 | pushes from a single event:
323 |
324 | ```clojure
325 | (re-frame/reg-event-fx
326 | :write-keyed-message
327 | (fn [{db :db} [_ message]]
328 | {:firebase/multi [[:firebase/write {:path [:latest-message] :value :message :on-,,,}]
329 | [:firebase/push {:path [:messages] :value :message :on-,,,}]]}))
330 | ```
331 |
332 | ### Reading from the database
333 |
334 | Firebase supports one-time reads of a node and also subscribing to a node to receive
335 | updates anytime its content changes. Both are supported by re-frame-firebase. (But,
336 | note, we don't yet support all the subscription variatons offered by Firebase).
337 |
338 | `firebase/read-once` handles one-time reads. Perhaps surprisingly, it is an event
339 | handler, not a subscription. This is because a one-time read is a sink not a
340 | source. Your application actively requests a value. The response then returns,
341 | triggering another event. Conceptually, this is very much like an http request.
342 |
343 | ```clojure
344 | (re-frame/reg-event-fx
345 | :read-motd
346 | (fn [{db :db} [_ status]]
347 | {:firebase/read-once {:path [:message-of-the-day]
348 | :on-success [:got-motd]
349 | :on-failure [:handle-failure]}}))
350 | ```
351 |
352 | Firebase '`:on`' subscriptions are handled as re-frame subscriptions:
353 |
354 | ```clojure
355 | (re-frame/subscribe [:firebase/on-value {:path [:latest-message]}])
356 | ```
357 |
358 | The firebase subscription will remain active only while the re-frame subscription is
359 | active. Effectively, this is when any variable bound to the subscription remains in
360 | scope.
361 |
362 | This, combined with re-frame 0.9's beautiful subscription caching leads to some very
363 | nice behavior: If you want to subscribe to a re-frame value for a long period of time,
364 | but want to access it deep inside a component, you can do this easily and efficiently by
365 | subscribing twice to the same path. _(If you are not familiar with this area,
366 | is a useful read)_
367 |
368 | You subscribe once in the outermost component of your page, which will, presumably,
369 | never be reloaded. This causes the subsription to become and remain active.
370 |
371 | You subcribe again within any component that wants to access the value. This causes
372 | _zero_ extra work. The firebase subscription only happens once. Firebase pushes any
373 | changes as they happen, precisely once per change. Re-frame-firebase caches the current
374 | value locally. The subscriptions read the value from the local cache.
375 |
376 | _Note well: It is not sufficient to just mention the subscription in the outer
377 | component. You must actually use it in the component, so that it is embedded in a
378 | mounted component._
379 |
380 | _Internal detail: The values are currently cached in your app db, under the key
381 | `com.degel.re-frame-firebase.core\cache`. But, this is an implementation detail, subject
382 | to change. Please do not rely on this for anything except, perhaps, debugging._
383 |
384 |
385 | ### Monitoring connectivity
386 |
387 | Your client's connection to the Firebase server may go down sometimes. You
388 | can detect this by subscribing to `:firebase/connection-state`. This
389 | subscription delivers a map, currently containing only one element:
390 | `:firebase/connected`. Its value will be `true` when the connection is up and
391 | `false` when down.
392 |
393 | Firebase's web/javascript client does a good job of handling offline
394 | conditions, so you can actually often ignore these state changes. For
395 | example, database values are cached locally and can be read even when the
396 | server is temporarily unaccessible. Writes, however, are a bit
397 | trickier. Firebase does cache and retry, but only while the client web page
398 | is up. If your web page is closed, I think that any writes done while offline
399 | will be lost. Therefore, it is advisable to check the connection state when
400 | attempting a write. If the connection is down, you can warn the user or store
401 | the results locally.
402 | ([re-frame-storage-fx](https://github.com/deg/re-frame-storage-fx)
403 | may be useful for this).
404 |
405 |
406 | ### Firestore
407 |
408 | [Firestore](https://firebase.google.com/docs/firestore/) is a beta database included in
409 | Firebase. re-frame-firebase exposes the Firestore SDK in a very similar way it does to
410 | the Realtime Database. It uses effects for most things, and a subscription for data.
411 | However, Firestore has a more complex structuring of data and querying system, using
412 | collections and documents. Thus, more options are provided and the returned data has
413 | more information attached to it besides a JSON object.
414 |
415 | We replace/wrap all JS objects into clojure-style maps (using-hyphens instead of camelCase);
416 | both the responses from Firestore and the parameters passed to it.
417 |
418 | You can find a simple introduction through examples below, but all the options
419 | are documented in [re\_frame\_firebase.cljs](src/com/degel/re_frame_firebase.cljs).
420 |
421 | There are also some well-documented public functions in the beginning of
422 | [firestore.cljs](src/com/degel/re_frame_firebase/firestore.cljs). Most users
423 | won't find them useful except for understanding how re-frame-firebase interacts
424 | with the Firebase SDK. However, if you find yourself needing to use Firestore's
425 | JS objects directly, you might find those useful.
426 |
427 | #### Set a document (`:firestore/set` effect)
428 |
429 | You should provide a vector of keywords and/or strings representing the path
430 | to the document under the `:path` argument. Also provide a clojure map representing
431 | the document data under the `:data` argument.
432 |
433 | ```clojure
434 | {:firestore/set {:path [:my-collection "my-document"]
435 | :data {:field1 "value1"
436 | :field2 {:inner1 "a" :inner2 "b"}}
437 | :set-options {:merge false
438 | :merge-fields [:field1 [:field2 :inner1]]}
439 | :on-success [:success-event]
440 | :on-failure #(prn "Error:" %)}}
441 | ```
442 |
443 | #### Update a document (`:firestore/update` effect)
444 |
445 | Works the same way as `:firestore/set`, except it doesn't take a `:set-options` parameter.
446 |
447 | #### Delete a document (`:firestore/delete` effect)
448 |
449 | Works the same way as `:firestore/set`, except it doesn't take `:data` and `:set-options` parameters.
450 |
451 | #### Execute multiple write operations using Firestore's WriteBatch (`:firestore/write-batch` effect)
452 |
453 | WriteBatches only support :firestore/set, :firestore/update and :firestore/delete.
454 |
455 | You should provide a vector of effect maps for each of the wanted operations under
456 | the `:operations` argument.
457 |
458 | ```clojure
459 | {:firestore/batch-write
460 | {:operations
461 | [[:firestore/set {:path [:cities "SF"] :data {:name "San Francisco" :state "CA"}}]
462 | [:firestore/set {:path [:cities "LA"] :data {:name "Los Angeles" :state "CA"}}]
463 | [:firestore/set {:path [:cities "DC"] :data {:name "Washington, D.C." :state nil}}]]
464 | :on-success #(prn "Cities added to database.")
465 | :on-failure #(prn "Couldn't add cities to database. Error:" %)}}
466 | ```
467 |
468 | Note you can also batch firestore effects through `:firebase/multi`. However,
469 | `:firestore/write-batch` sends a single request to the server, thus it is faster.
470 | `:firebase/multi`, on the other hand, supports all `:firestore` effects
471 | (not only write ones), but it works by dispatching the effects individually.
472 |
473 | #### Add a document to a Firestore collection (`:firestore/add` effect).
474 |
475 | Works the same way as the previous effects, but `:on-success` will be provided
476 | with a vector of strings representing the path to the created document.
477 |
478 | ```clojure
479 | {:firestore/add {:path [:my-collection]
480 | :data my-data
481 | :on-success #(prn "Added document ID:" (last %))}}
482 | ```
483 |
484 | #### Get a document or collection query from Firestore (`:firestore/get` effect).
485 |
486 | You should provide a vector of keywords/strings representing the path under either
487 | `:path-document` or `:path-collection`. The `:on-success` callback will be provided
488 | with the query result as an argument. The data will be transformed into a clojure
489 | object and the keys will be in clojure-style (using-hyphens instead of camelCase).
490 |
491 | Querying is full of options. They are documented in full in
492 | [re\_frame\_firebase.cljs](src/com/degel/re_frame_firebase.cljs).
493 |
494 | ```clojure
495 | {:firestore/get {:path-document [:my-collection :my-document]
496 | :expose-objects false
497 | :on-success #(prn "Objects's contents:" (:data %))}}
498 | ```
499 |
500 | ```clojure
501 | {:firestore/get {:path-collection [:cities]
502 | :where [[:state :>= "CA"]
503 | [:population :< 1000000]]
504 | :limit 1000
505 | :order-by [[:state :desc]
506 | [:population :desc]]
507 | :start-at ["CA" 1000]
508 | :doc-changes false
509 | :on-success #(prn "Number of documents:" (:size %))}}
510 | ```
511 |
512 | #### Set up a listener for changes in a Firestore collection/document query (`:firestore/on-snapshot` effect)
513 |
514 | You can use the `:firestore/on-snapshot` effect for this. It accepts most of
515 | the arguments from `:firestore/get`. You should provide `:on-next` instead of
516 | `:on-success`, which will be called every time a change happens with the retrieved
517 | data as argument. You should pass `:on-error` instead of `:on-failure`.
518 | (re-frame/reg-fx :firestore/on-snapshot firestore/on-snapshot-effect)
519 |
520 | #### Subscribe to a Firestore collection/document query (`:firestore/on-snapshot` subscription)
521 |
522 | Takes the same arguments as `:firestore/on-snapshot` effect, except for `:on-next`,
523 | as it is meant to be used as a subscription.
524 |
525 | ```clojure
526 | (re-frame/subscribe
527 | [:firestore/on-snapshot {:path-document [:my :document]}])
528 | ```
529 |
530 |
531 | ## Examples and projects
532 |
533 | There are examples provided in the [examples](examples) folder. It is great to
534 | check them in order to get used to the API.
535 |
536 | I have a toy project, [Trilystro](https://github.com/deg/trilystro) which
537 | uses re-frame-firebase. It is an evolving work, so I cannot offer any
538 | guarantees that it will always be stable. But, I have tried to keep the code
539 | reasonably clean and readable. It will also often be running at
540 |
541 |
542 | ## Setup
543 |
544 | This is a library project. Although it still includes some of the Mies
545 | templates's scaffolding for a standalone project, I have not used these
546 | features and they may have decayed.
547 |
548 | For development, I just include this project in the
549 | [checkouts directory](https://github.com/technomancy/leiningen/blob/master/doc/TUTORIAL.md#checkout-dependencies)
550 | of a project that uses it.
551 |
552 | Then, for deployment, simply:
553 |
554 | ```
555 | lein deploy clojars
556 | ```
557 |
558 | _The rest of this section is Mies boilerplate. Probably all correct, but not necesarily relevant._
559 |
560 | Most of the following scripts require [rlwrap](http://utopia.knoware.nl/~hlub/uck/rlwrap/) (on OS X installable via brew).
561 |
562 | Build your project once in dev mode with the following script and then open `index.html` in your browser.
563 |
564 | ./scripts/build
565 |
566 | To auto build your project in dev mode:
567 |
568 | ./scripts/watch
569 |
570 | To start an auto-building Node REPL:
571 |
572 | ./scripts/repl
573 |
574 | To get source map support in the Node REPL:
575 |
576 | lein npm install
577 |
578 | To start a browser REPL:
579 |
580 | 1. Uncomment the following lines in src/re_frame_firebase/core.cljs:
581 | ```clojure
582 | ;; (defonce conn
583 | ;; (repl/connect "http://localhost:9000/repl"))
584 | ```
585 | 2. Run `./scripts/brepl`
586 | 3. Browse to `http://localhost:9000` (you should see `Hello world!` in the web console)
587 | 4. (back to step 3) you should now see the REPL prompt: `cljs.user=>`
588 | 5. You may now evaluate ClojureScript statements in the browser context.
589 |
590 | For more info using the browser as a REPL environment, see
591 | [this](https://github.com/clojure/clojurescript/wiki/The-REPL-and-Evaluation-Environments#browser-as-evaluation-environment).
592 |
593 | Clean project specific out:
594 |
595 | lein clean
596 |
597 | Build a single release artifact with the following script and then open `index_release.html` in your browser.
598 |
599 | ./scripts/release
600 |
601 | ## Questions
602 |
603 | I can usually be found on the [Clojurians Slack](https://clojurians.net)
604 | #reagent or #re-frame slack channels. My handle is @deg. Email is also fine,
605 | or you can report issues or PRs directly to this project.
606 |
607 | ## License
608 |
609 | Copyright © 2017-9 David Goldfarb
610 |
611 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
612 |
--------------------------------------------------------------------------------
/examples/firestore/.gitignore:
--------------------------------------------------------------------------------
1 | /resources/public/js/
2 | target/
3 |
--------------------------------------------------------------------------------
/examples/firestore/README.md:
--------------------------------------------------------------------------------
1 | # An example firestore App
2 |
3 | This tiny application is meant to test and show you how to use the firestore API.
4 |
5 | All the code is in one namespace: `src/firestore/core.cljs`, api-keys are in
6 | `src/firestore/api-keys.cljs`.
7 |
8 | ## How to run it
9 |
10 | ### Firebase steps (from [here](https://github.com/firebase/quickstart-js/tree/master/firestore))
11 | 1. Create a Firebase project in the [Firebase Console](https://console.firebase.google.com).
12 | 2. In the Firebase console, enable Cloud Firestore on your project by doing: **Database > Create Database**
13 | 3. Select testing mode for the security rules
14 | 4. Copy/Download this repo and open this folder in a Terminal.
15 | 5. Install the Firebase CLI if you do not have it installed on your machine:
16 | ```bash
17 | npm -g i firebase-tools
18 | ```
19 | 6. Set the CLI to use the project you created in step 1:
20 | ```bash
21 | firebase use --add
22 | ```
23 | 7. Deploy the Firestore security rules and indexes:
24 | ```bash
25 | firebase deploy --only firestore
26 | ```
27 |
28 | ### Clojure steps
29 |
30 | 1. Copy the API keys from your firebase project console to `api-keys.cljs` (replace
31 | the existing dummy values).
32 | 2. Open a command line in this folder.
33 | 3. Compile the app and start figwheel's hot-reloading.
34 | ```bash
35 | lein do clean, figwheel
36 | ```
37 | 4. Open `http://localhost:3449/` to see the app.
38 |
39 | While step 3 is running, any changes you make to the ClojureScript source files
40 | (in `src`) will be re-compiled and reflected in the running page immediately.
41 |
42 | ## Production version
43 |
44 | To compile an optimized version, run:
45 |
46 | ```bash
47 | lein do clean, with-profile prod compile
48 | ```
49 |
50 | And then open `resources/public/index.html` in a browser.
51 |
--------------------------------------------------------------------------------
/examples/firestore/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firestore": {
3 | "rules": "firestore.rules",
4 | "indexes": "firestore.indexes.json"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/examples/firestore/firestore.indexes.json:
--------------------------------------------------------------------------------
1 | {
2 | "indexes": [
3 | {
4 | "collectionId": "cities",
5 | "state": "READY",
6 | "fields": [
7 | {
8 | "fieldPath": "state",
9 | "mode": "ASCENDING"
10 | },
11 | {
12 | "fieldPath": "population",
13 | "mode": "ASCENDING"
14 | }
15 | ]
16 | },
17 | {
18 | "collectionId": "cities",
19 | "state": "READY",
20 | "fields": [
21 | {
22 | "fieldPath": "name",
23 | "mode": "ASCENDING"
24 | },
25 | {
26 | "fieldPath": "state",
27 | "mode": "ASCENDING"
28 | }
29 | ]
30 | },
31 | {
32 | "collectionId": "cities",
33 | "state": "READY",
34 | "fields": [
35 | {
36 | "fieldPath": "name",
37 | "mode": "ASCENDING"
38 | },
39 | {
40 | "fieldPath": "state",
41 | "mode": "DESCENDING"
42 | }
43 | ]
44 | }
45 | ]
46 | }
--------------------------------------------------------------------------------
/examples/firestore/firestore.rules:
--------------------------------------------------------------------------------
1 | service cloud.firestore {
2 | match /databases/{database}/documents {
3 | match /{document=**} {
4 | allow read, write;
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/firestore/project.clj:
--------------------------------------------------------------------------------
1 | (defproject firestore "0.1.0-SNAPSHOT"
2 | :dependencies [[org.clojure/clojure "1.9.0"]
3 | [org.clojure/clojurescript "1.10.238"]
4 | [re-frame "0.10.5"]
5 | [com.degel/re-frame-firebase "0.7.0"]
6 | [com.degel/iron "0.4.0"]]
7 |
8 | :plugins [[lein-cljsbuild "1.1.7"]
9 | [lein-figwheel "0.5.16"]]
10 |
11 | :hooks [leiningen.cljsbuild]
12 |
13 | :profiles {:dev {:cljsbuild
14 | {:builds {:client {:figwheel {:on-jsload "firestore.core/run"}
15 | :compiler {:main "firestore.core"
16 | :asset-path "js"
17 | :optimizations :none
18 | :source-map true
19 | :source-map-timestamp true}}}}}
20 |
21 | :prod {:cljsbuild
22 | {:builds {:client {:compiler {:optimizations :advanced
23 | :elide-asserts true
24 | :pretty-print false}}}}}}
25 |
26 | :figwheel {:repl false}
27 |
28 | :clean-targets ^{:protect false} ["resources/public/js"]
29 |
30 | :cljsbuild {:builds {:client {:source-paths ["src" "../../src/"]
31 | :compiler {:output-dir "resources/public/js"
32 | :output-to "resources/public/js/client.js"}}}})
33 |
--------------------------------------------------------------------------------
/examples/firestore/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example
6 |
7 |
8 |
9 |
10 |
re-frame-firebase firestore example app – see README.md
11 |
12 |
13 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/examples/firestore/resources/public/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: HelveticaNeue, Helvetica;
3 | }
4 |
5 | .container {
6 | width: 80%;
7 | padding-right: 15px;
8 | padding-left: 15px;
9 | margin: auto;
10 | }
11 |
12 | pre {
13 | border: 1px solid;
14 | border-radius: 5px;
15 | padding: 10px;
16 | }
17 |
18 | button {
19 | margin-top: 15px;
20 | margin-bottom: 15px;
21 | margin-right: 5px;
22 | }
23 |
--------------------------------------------------------------------------------
/examples/firestore/src/firestore/api-keys.cljs:
--------------------------------------------------------------------------------
1 | (ns firestore.api-keys)
2 |
3 | ;; Provide your own app info
4 | (defonce firebase-app-info
5 | {:apiKey "MY-KEY-MY-KEY-MY-KEY-MY-KEY"
6 | :authDomain "my-app.firebaseapp.com"
7 | :databaseURL "https://my-app.firebaseio.com"
8 | :projectId "my-app"
9 | :storageBucket "my-app.appspot.com"
10 | :messagingSenderId "000000000000"})
11 |
--------------------------------------------------------------------------------
/examples/firestore/src/firestore/core.cljs:
--------------------------------------------------------------------------------
1 | (ns firestore.core
2 | (:require [clojure.pprint :refer [pprint]]
3 | [clojure.string :as str]
4 | [reagent.core :as reagent]
5 | [re-frame.core :as re-frame]
6 | [iron.re-utils :as re-utils :refer [evt event->fn sub->fn]]
7 | [com.degel.re-frame-firebase :as firebase]
8 | [firestore.api-keys :refer [firebase-app-info]]))
9 |
10 | ;; Global stuff
11 | (re-frame/reg-event-db :set-user (fn [db [_ user]] (assoc db :user user)))
12 |
13 | (re-frame/reg-sub :user (fn [db _] (:user db)))
14 |
15 | (defn code [language text & args]
16 | [:div
17 | [:p (str/join " " args)]
18 | [:pre [(keyword (str "code.border.language-" language)) text]]])
19 |
20 | ;; Example 1
21 | ;; This example will add a random field to a document every time the user clicks.
22 | (re-frame/reg-event-fx
23 | :example-1-set
24 | (fn [_ _] {:firestore/set {:path [:sample-collection :sample-document]
25 | :data {:sample-field (str (random-uuid))}}}))
26 | (re-frame/reg-event-fx
27 | :example-1-update
28 | (fn [_ _] {:firestore/update {:path [:sample-collection :sample-document]
29 | :data {(str (random-uuid)) "Random field"}}}))
30 | (re-frame/reg-event-fx
31 | :example-1-delete
32 | (fn [_ _] {:firestore/delete {:path [:sample-collection :sample-document]}}))
33 |
34 | (re-frame/reg-event-fx
35 | :example-1-get
36 | (fn [_ _] {:firestore/get {:path-document [:sample-collection :sample-document]
37 | :on-success [:example-1-updatedb]}}))
38 |
39 | (re-frame/reg-event-db
40 | :example-1-updatedb
41 | (fn [db [_ value]]
42 | (assoc db :example-1-value value)))
43 |
44 | (re-frame/reg-sub
45 | :example-1-value1-pre
46 | (fn [db _] (:example-1-value db)))
47 |
48 | (re-frame/reg-sub
49 | :example-1-value1
50 | (fn [_ _]
51 | (re-frame/subscribe [:example-1-value1-pre]))
52 | (fn [value _]
53 | (with-out-str (pprint value))))
54 |
55 | (re-frame/reg-sub
56 | :example-1-value2
57 | (fn [_ _]
58 | (re-frame/subscribe [:firestore/on-snapshot {:path-document [:sample-collection :sample-document]}]))
59 | (fn [value _]
60 | (with-out-str (pprint value))))
61 |
62 | (defn example-1
63 | []
64 | (let [value1 (evt [:example-1-set])} "Set"]
69 | [:button {:on-click #(>evt [:example-1-update])} "Update"]
70 | [:button {:on-click #(>evt [:example-1-delete])} "Delete"]
71 | [:button {:on-click #(>evt [:example-1-get])} "Get"]]
72 | [:div (code "clojure" value1 "This field will only update when you click \"Get\":")
73 | (code "clojure" value2 "This field auto-updates via \":firestore/on-snapshot\":")]]))
74 |
75 |
76 | ;; Example 2
77 | (re-frame/reg-event-fx
78 | :example-2-addsamples
79 | (fn [_ _]
80 | {:firestore/write-batch
81 | {:operations
82 | [[:firestore/set {:path [:cities "SF"] :data {:name "San Francisco" :state "CA" :country "USA" :capital false :population 860000}}]
83 | [:firestore/set {:path [:cities "LA"] :data {:name "Los Angeles" :state "CA" :country "USA" :capital false :population 3900000}}]
84 | [:firestore/set {:path [:cities "DC"] :data {:name "Washington, D.C." :state nil :country "USA" :capital true :population 680000}}]
85 | [:firestore/set {:path [:cities "TOK"] :data {:name "Tokyo" :state nil :country "Japan" :capital true :population 9000000}}]
86 | [:firestore/set {:path [:cities "BJ"] :data {:name "Beijing" :state nil :country "China" :capital true :population 2150000}}]
87 | [:firestore/set {:path [:cities "S1"] :data {:name "Springfield" :state "Massachusetts"}}]
88 | [:firestore/set {:path [:cities "S2"] :data {:name "Springfield" :state "Missouri"}}]
89 | [:firestore/set {:path [:cities "S3"] :data {:name "Springfield" :state "Wisconsin"}}]]}}))
90 |
91 | (re-frame/reg-event-fx
92 | :example-2-addrandom
93 | (fn [_ _] {:firestore/add {:path [:cities] :data {:name (str (random-uuid))
94 | :state nil
95 | :country "No Man's Land"
96 | :population (rand-int 10000000)}
97 | :on-success #(js/alert (str "Added random city to: " %))}}))
98 |
99 | (re-frame/reg-event-fx
100 | :example-2-delete
101 | (fn [_ [_ v]]
102 | {:firebase/multi (map #(as-> % $ (:ref $) {:path $} [:firestore/delete $])
103 | (:docs v))}))
104 |
105 | (re-frame/reg-event-fx
106 | :example-2-deleteall
107 | (fn [_ _] {:firestore/get {:path-collection [:cities]
108 | :on-success [:example-2-delete]}}))
109 |
110 | (re-frame/reg-event-db
111 | :example-2-updatedb
112 | (fn [db [_ value]]
113 | (assoc db :example-2-value value)))
114 |
115 | (re-frame/reg-event-fx
116 | :example-2-listen
117 | (fn [_ _] {:firestore/on-snapshot {:path-collection [:cities]
118 | :doc-changes true
119 | :on-next #(do (.log js/console "Changes:") (pprint (:doc-changes %)))}}))
120 |
121 | (re-frame/reg-event-fx
122 | :example-2-get
123 | (fn [_ [_ query]] {:firestore/get (assoc query :on-success [:example-2-updatedb])}))
124 |
125 | (re-frame/reg-sub
126 | :example-2-value
127 | (fn [db _] (with-out-str (pprint (:example-2-value db)))))
128 |
129 | (defn example-2
130 | []
131 | [:div.example
132 | [:h2 "Example 2"]
133 | [:div [:button {:on-click #(>evt [:example-2-addsamples])} "Add Samples"]
134 | [:button {:on-click #(>evt [:example-2-addrandom])} "Add Random"]
135 | [:button {:on-click #(>evt [:example-2-get {:path-collection [:cities]}])} "Get all"]
136 | [:button {:on-click #(>evt [:example-2-deleteall])} "Delete All"]
137 | [:button {:on-click #(>evt [:example-2-listen])} "Start listening to changes (check console)"]
138 | [:button {:on-click #(>evt [:example-2-get {:path-collection [:cities]
139 | :where [[:capital :== true]]}])}
140 | "Get capitals"]
141 | [:button {:on-click #(>evt [:example-2-get {:path-collection [:cities]
142 | :where [["state" "==" "CA"]
143 | [:population :> 1000000]]}])}
144 | "Get large in California"]
145 | [:button {:on-click #(>evt [:example-2-get {:path-collection [:cities]
146 | :order-by [[:population :desc]]
147 | :limit 2}])}
148 | "Get two largest populations"]
149 | [:button {:on-click #(>evt [:example-2-get {:path-collection [:cities]
150 | :order-by [[:name] [:state]]
151 | :start-at ["Springfield" "Missouri"]}])}
152 | "Get starting at Springfield, Missouri / ordering by [:name] and then [:state] (both ascendant by default)"]
153 | [:button {:on-click #(>evt [:example-2-get {:path-collection [:cities] :expose-objects true}])}
154 | "Get all exposing objects"]
155 | [:button {:on-click #(>evt [:example-2-get {:path-document [:cities "LA"] :expose-objects true}])}
156 | "Get LA exposing objects"]]
157 | [:div (code "clojure" (SetOptions.
217 | ;;; - :on-success Function or re-frame event vector to be dispatched.
218 | ;;; - :on-failure Function or re-frame event vector to be dispatched.
219 | ;;;
220 | ;;; Example FX:
221 | ;;; {:firestore/set {:path [:my-collection "my-document"]
222 | ;;; :data {:field1 "value1"
223 | ;;; :field2 {:inner1 "a" :inner2 "b"}}
224 | ;;; :set-options {:merge false
225 | ;;; :merge-fields [:field1 [:field2 :inner1]]}
226 | ;;; :on-success [:success-event]
227 | ;;; :on-failure #(prn "Error:" %)}}
228 | ;;;
229 | (re-frame/reg-fx :firestore/set firestore/set-effect)
230 |
231 |
232 | ;;; Update a document to Firestore.
233 | ;;; See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference#update
234 | ;;;
235 | ;;; Key arguments: :path, :data, :on-success, :on-failure
236 | ;;;
237 | (re-frame/reg-fx :firestore/update firestore/update-effect)
238 |
239 |
240 | ;;; Delete a document from Firestore.
241 | ;;; See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference#delete
242 | ;;;
243 | ;;; Key arguments: :path, :on-success, :on-failure
244 | ;;;
245 | (re-frame/reg-fx :firestore/delete firestore/delete-effect)
246 |
247 |
248 | ;;; Execute multiple write operations using Firestore's WriteBatch
249 | ;;; See https://firebase.google.com/docs/reference/js/firebase.firestore.WriteBatch
250 | ;;;
251 | ;;; WriteBatches only support :firestore/set, :firestore/update and :firestore/delete.
252 | ;;; Key arguments:
253 | ;;; - :operations Vector of effect maps for each of the wanted operations.
254 | ;;; - :on-success You should supply a single callback function/events here for all of the operations.
255 | ;;; - :on-failure
256 | ;;;
257 | ;;; Example FX:
258 | ;;; {:firestore/batch-write
259 | ;;; {:operations
260 | ;;; [[:firestore/set {:path [:cities "SF"] :data {:name "San Francisco" :state "CA"}}]
261 | ;;; [:firestore/set {:path [:cities "LA"] :data {:name "Los Angeles" :state "CA"}}]
262 | ;;; [:firestore/set {:path [:cities "DC"] :data {:name "Washington, D.C." :state nil}}]]
263 | ;;; :on-success #(prn "Cities added to database.")
264 | ;;; :on-failure #(prn "Couldn't add cities to database. Error:" %)}}
265 | ;;;
266 | (re-frame/reg-fx :firestore/write-batch firestore/write-batch-effect)
267 |
268 |
269 | ;;; Add a document to a Firestore collection.
270 | ;;;
271 | ;;; Key arguments: :path, :data, :on-success, :on-failure
272 | ;;;
273 | ;;; - :path Should be a path to a collection.
274 | ;;; - :on-success Will be provided with a vector of strings representing the path to the created document.
275 | ;;;
276 | ;;; Example FX:
277 | ;;; {:firestore/add {:path [:my-collection]
278 | ;;; :data my-data
279 | ;;; :on-success #(prn "Added document ID:" (last %))}}
280 | ;;;
281 | (re-frame/reg-fx :firestore/add firestore/add-effect)
282 |
283 |
284 | ;;; Get a document or collection query from Firestore.
285 | ;;; See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference#get
286 | ;;; See https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#get
287 | ;;; See https://firebase.google.com/docs/reference/js/firebase.firestore.Query
288 | ;;;
289 | ;;; When querying for a document, you can supply the following key arguments:
290 | ;;; - :path-document The same vector of keywords/strings as other effects.
291 | ;;; - :get-options Map containing additional options. See firestore/clj->GetOptions.
292 | ;;; - :snapshot-options Map to be passed when retrieving data from Snapshots.
293 | ;;; See firestore/clj->SnapshotOpions.
294 | ;;; - :expose-objects When set to true, the original Snapshot will be attached
295 | ;;; under the :object key, see firestore/DocumentSnapshot->clj.
296 | ;;; - :on-success The clojure object will be passed as an argument to the event or fn.
297 | ;;; - :on-failure
298 | ;;;
299 | ;;; When querying for a collection, you can supply the following key arguments:
300 | ;;; - :path-collection
301 | ;;; - :get-options
302 | ;;; - :where A seq of triples [field-path op value] where op should be
303 | ;;; :>, :>=, :< :<=, or :==. You can also provide strings like "<=".
304 | ;;; - :order-by A seq of pairs [field-path direction] where direction should
305 | ;;; either be :asc or :desc. Ascending is the default.
306 | ;;; You can also provide strings like "desc".
307 | ;;; - :limit Limit the number of documents to the specified number.
308 | ;;; - :start-at, :start-after, :end-at, :end-before
309 | ;;; Limit the query at the provided document. Either by providing
310 | ;;; a seq with a single DocumentSnapshot or multiple field values
311 | ;;; in the same order as :order-by.
312 | ;;; - :doc-changes If set to true, a vector of parsed DocumentChanges will be
313 | ;;; provided under :doc-changes. See firestore/DocumentChange->clj.
314 | ;;; - :snapshot-options
315 | ;;; - :snapshot-listen-options Map to be passed when retrieving doc changes.
316 | ;;; See firestore/SnapshotListenOptions->clj.
317 | ;;; - :expose-objects See firestore/QuerySnapshot->clj.
318 | ;;; - :on-success, :on-failure
319 | ;;;
320 | ;;; Example FX:
321 | ;;; {:firestore/get {:path-document [:my-collection :my-document]
322 | ;;; :expose-objects false
323 | ;;; :on-success #(prn "Objects's contents:" (:data %))}}
324 | ;;; {:firestore/get {:path-collection [:cities]
325 | ;;; :where [[:state :>= "CA"]
326 | ;;; [:population :< 1000000]]
327 | ;;; :limit 1000
328 | ;;; :order-by [[:state :desc]
329 | ;;; [:population :desc]]
330 | ;;; :start-at ["CA" 1000]
331 | ;;; :doc-changes false
332 | ;;; :on-success #(prn "Number of documents:" (:size %))}}
333 | ;;;
334 | (re-frame/reg-fx :firestore/get firestore/get-effect)
335 |
336 |
337 | ;;; Set up a listener for changes in a Firestore collection/document query.
338 | ;;; See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference#onSnapshot
339 | ;;; See https://firebase.google.com/docs/reference/js/firebase.firestore.Query#onSnapshot
340 | ;;;
341 | ;;; You can provide the same key-arguments as to :firestore/get, except for :get-options,
342 | ;;; :on-success and :on-failure. Instead, you can/should provide the following:
343 | ;;; - :snapshot-listen-options Map containing additional options.
344 | ;;; See firestore/clj->SnapshotListenOptions.
345 | ;;; - :on-next Event/function to be called every time a change happens.
346 | ;;; The clojure object will be passed as an argument to the event or fn.
347 | ;;; - :on-error
348 | ;;;
349 | (re-frame/reg-fx :firestore/on-snapshot firestore/on-snapshot-effect)
350 |
351 |
352 | ;;; Subscribe to a Firestore collection/document query.
353 | ;;;
354 | ;;; Takes the same arguments as :firestore/on-snapshot effect, except for :on-next,
355 | ;;; as it is meant to be used as a subscription.
356 | ;;;
357 | ;;; Example Subscription:
358 | ;;; (re-frame/subscribe
359 | ;;; [:firestore/on-snapshot {:path-document [:my :document]}])
360 | ;;;
361 | (re-frame/reg-sub-raw :firestore/on-snapshot firestore/on-snapshot-sub)
362 |
363 |
364 | ;;; Start library and register callbacks.
365 | ;;;
366 | ;;;
367 | ;;; In Iron style, most of the parameters can be either a function or a
368 | ;;; re-frame event/sub vector. If there is a parameter, it will passed to the
369 | ;;; function or conj'd onto the vector.
370 | ;;;
371 | ;;; - :firebase-app-info - Firebase application credentials. This is the one
372 | ;;; parameter that takes a map:
373 | ;;; {:apiKey "MY-KEY-MY-KEY-MY-KEY-MY-KEY"
374 | ;;; :authDomain "my-app.firebaseapp.com"
375 | ;;; :databaseURL "https://my-app.firebaseio.com"
376 | ;;; :projectId: "my-app"
377 | ;;; :storageBucket "my-app.appspot.com"
378 | ;;; :messagingSenderId: "000000000000"}
379 | ;;;
380 | ;;; - :set-user-event - Function or re-frame event that will be called back
381 | ;;; to receive and store the user object from us, when login succeeds.
382 | ;;; This object is a map that includes several fields that we need, plus
383 | ;;; the following that may be useful to the calling app:
384 | ;;; :display-name - The user's full name
385 | ;;; :email - The user's email address
386 | ;;; :photo-url - The user's photo
387 | ;;; :uid - The user's unique id, used by Firebase.
388 | ;;;
389 | ;;; - :get-user-sub - Function or re-frame subscription vector that this
390 | ;;; library will use to access the user object stored by :set-user-event
391 | ;;;
392 | ;;; - :default-error-handler - Function or re-frame event that will be called
393 | ;;; to handle any otherwise unhandled errors.
394 | ;;;
395 | (defn init [& {:keys [firebase-app-info
396 | firestore-settings
397 | get-user-sub
398 | set-user-event
399 | default-error-handler]}]
400 | (core/set-firebase-state :get-user-sub get-user-sub
401 | :set-user-event set-user-event
402 | :default-error-handler default-error-handler)
403 | (core/initialize-app firebase-app-info)
404 | (firestore/set-firestore-settings firestore-settings)
405 | (auth/init-auth))
406 |
--------------------------------------------------------------------------------
/src/com/degel/re_frame_firebase/auth.cljs:
--------------------------------------------------------------------------------
1 | ;;; Author: David Goldfarb (deg@degel.com)
2 | ;;; Copyright (c) 2017, David Goldfarb
3 |
4 | (ns com.degel.re-frame-firebase.auth
5 | (:require-macros [reagent.ratom :refer [reaction]])
6 | (:require
7 | [clojure.spec.alpha :as s]
8 | [re-frame.core :as re-frame]
9 | [iron.re-utils :refer [>evt]]
10 | [firebase.app :as firebase-app]
11 | [firebase.auth :as firebase-auth]
12 | [com.degel.re-frame-firebase.core :as core]))
13 |
14 |
15 | (defn- user
16 | "Extract interesting details from the Firebase JS user object."
17 | [firebase-user]
18 | (when firebase-user
19 | {:uid (.-uid firebase-user)
20 | :provider-data (.-providerData firebase-user)
21 | :display-name (.-displayName firebase-user)
22 | :photo-url (.-photoURL firebase-user)
23 | :email (let [provider-data (.-providerData firebase-user)]
24 | (when-not (empty? provider-data)
25 | (-> provider-data first .-email)))}))
26 |
27 | (defn- set-user
28 | [firebase-user]
29 | (-> firebase-user
30 | (user)
31 | (core/set-current-user)))
32 |
33 | (defn- init-auth []
34 | (.onAuthStateChanged
35 | (js/firebase.auth)
36 | set-user
37 | (core/default-error-handler))
38 |
39 | (-> (js/firebase.auth)
40 | (.getRedirectResult)
41 | (.then (fn on-user-credential [user-credential]
42 | (-> user-credential
43 | (.-user)
44 | set-user)))
45 | (.catch (core/default-error-handler))))
46 |
47 | (def ^:private sign-in-fns
48 | {:popup (memfn signInWithPopup auth-provider)
49 | :redirect (memfn signInWithRedirect auth-provider)})
50 |
51 | (defn- maybe-link-with-credential
52 | [pending-credential user-credential]
53 | (when (and pending-credential user-credential)
54 | (when-let [firebase-user (.-user user-credential)]
55 | (-> firebase-user
56 | (.linkWithCredential pending-credential)
57 | (.catch (core/default-error-handler))))))
58 |
59 | (defn- oauth-sign-in
60 | [auth-provider opts]
61 | (let [{:keys [sign-in-method scopes custom-parameters link-with-credential]
62 | :or {sign-in-method :redirect}} opts]
63 |
64 | (doseq [scope scopes]
65 | (.addScope auth-provider scope))
66 |
67 | (when custom-parameters
68 | (.setCustomParameters auth-provider (clj->js custom-parameters)))
69 |
70 | (if-let [sign-in (sign-in-fns sign-in-method)]
71 | (-> (js/firebase.auth)
72 | (sign-in auth-provider)
73 | (.then (partial maybe-link-with-credential link-with-credential))
74 | (.catch (core/default-error-handler)))
75 | (>evt [(core/default-error-handler)
76 | (js/Error. (str "Unsupported sign-in-method: " sign-in-method ". Either :redirect or :popup are supported."))]))))
77 |
78 |
79 | (defn google-sign-in
80 | [opts]
81 | ;; TODO: use Credential for mobile.
82 | (oauth-sign-in (js/firebase.auth.GoogleAuthProvider.) opts))
83 |
84 |
85 | (defn facebook-sign-in
86 | [opts]
87 | (oauth-sign-in (js/firebase.auth.FacebookAuthProvider.) opts))
88 |
89 |
90 | (defn twitter-sign-in
91 | [opts]
92 | (oauth-sign-in (js/firebase.auth.TwitterAuthProvider.) opts))
93 |
94 |
95 | (defn github-sign-in
96 | [opts]
97 | (oauth-sign-in (js/firebase.auth.GithubAuthProvider.) opts))
98 |
99 |
100 | (defn email-sign-in [{:keys [email password]}]
101 | (-> (js/firebase.auth)
102 | (.signInWithEmailAndPassword email password)
103 | (.then set-user)
104 | (.catch (core/default-error-handler))))
105 |
106 |
107 | (defn email-create-user [{:keys [email password]}]
108 | (-> (js/firebase.auth)
109 | (.createUserWithEmailAndPassword email password)
110 | (.then set-user)
111 | (.catch (core/default-error-handler))))
112 |
113 |
114 | (defn anonymous-sign-in [opts]
115 | (-> (js/firebase.auth)
116 | (.signInAnonymously)
117 | (.then set-user)
118 | (.catch (core/default-error-handler))))
119 |
120 |
121 | (defn custom-token-sign-in [{:keys [token]}]
122 | (-> (js/firebase.auth)
123 | (.signInWithCustomToken token)
124 | (.then set-user)
125 | (.catch (core/default-error-handler))))
126 |
127 |
128 | (defn init-recaptcha [{:keys [on-solve container-id]}]
129 | (let [recaptcha (js/firebase.auth.RecaptchaVerifier.
130 | container-id
131 | (clj->js {:size "invisible"
132 | :callback #(re-frame/dispatch on-solve)}))]
133 | (swap! core/firebase-state assoc
134 | :recaptcha-verifier recaptcha)))
135 |
136 |
137 | (defn phone-number-sign-in [{:keys [phone-number on-send]}]
138 | (if-let [verifier (:recaptcha-verifier @core/firebase-state)]
139 | (-> (js/firebase.auth)
140 | (.signInWithPhoneNumber phone-number verifier)
141 | (.then (fn [confirmation]
142 | (when on-send
143 | (re-frame/dispatch on-send))
144 | (swap! core/firebase-state assoc
145 | :recaptcha-confirmation-result confirmation)))
146 | (.catch (core/default-error-handler)))
147 | (.warn js/console "Initialise reCaptcha first")))
148 |
149 |
150 | (defn phone-number-confirm-code [{:keys [code]}]
151 | (if-let [confirmation (:recaptcha-confirmation-result @core/firebase-state)]
152 | (-> confirmation
153 | (.confirm code)
154 | (.then set-user)
155 | (.catch (core/default-error-handler)))
156 | (.warn js/console "reCaptcha confirmation missing")))
157 |
158 |
159 | (defn sign-out []
160 | (-> (js/firebase.auth)
161 | (.signOut)
162 | (.catch (core/default-error-handler))))
163 |
--------------------------------------------------------------------------------
/src/com/degel/re_frame_firebase/core.cljs:
--------------------------------------------------------------------------------
1 | ;;; Author: David Goldfarb (deg@degel.com)
2 | ;;; Copyright (c) 2017, David Goldfarb
3 |
4 | (ns com.degel.re-frame-firebase.core
5 | (:require
6 | [iron.re-utils :refer [evt event->fn sub->fn]]
7 | [firebase.app :as firebase-app]))
8 |
9 | ;;; Used mostly to register client handlers
10 | (defonce firebase-state (atom {}))
11 |
12 | (defn set-firebase-state [& {:keys [get-user-sub set-user-event default-error-handler]}]
13 | (swap! firebase-state assoc
14 | :set-user-fn (event->fn set-user-event)
15 | :get-user-fn (sub->fn get-user-sub)
16 | :default-error-handler (event->fn (or default-error-handler js/alert))))
17 |
18 | (defn initialize-app [firebase-app-info]
19 | (js/firebase.initializeApp (clj->js firebase-app-info)))
20 |
21 | ;;; [TODO] Consider adding a default atom to hold the user state when :get-user-fn and
22 | ;;; and :set-user-fn are not defined. Need to do this carefully, so as not to cause any
23 | ;;; surprises for users who accidentally defined just one of the two callbacks.
24 | (defn current-user []
25 | (when-let [handler (:get-user-fn @firebase-state)]
26 | (handler)))
27 |
28 | (defn set-current-user [user]
29 | (when-let [handler (:set-user-fn @firebase-state)]
30 | (handler user)))
31 |
32 | (defn default-error-handler []
33 | (:default-error-handler @firebase-state))
34 |
--------------------------------------------------------------------------------
/src/com/degel/re_frame_firebase/database.cljs:
--------------------------------------------------------------------------------
1 | ;;; Author: David Goldfarb (deg@degel.com)
2 | ;;; Copyright (c) 2017, David Goldfarb
3 |
4 | (ns com.degel.re-frame-firebase.database
5 | (:require
6 | [clojure.spec.alpha :as s]
7 | [clojure.string :as str]
8 | [re-frame.core :as re-frame]
9 | [re-frame.loggers :refer [console]]
10 | [reagent.ratom :as ratom :refer [make-reaction]]
11 | [iron.re-utils :refer [evt event->fn sub->fn]]
12 | [iron.utils :as utils]
13 | [firebase.app :as firebase-app]
14 | [firebase.database :as firebase-database]
15 | [com.degel.re-frame-firebase.helpers :refer [js->clj-tree success-failure-wrapper]]
16 | [com.degel.re-frame-firebase.core :as core]
17 | [com.degel.re-frame-firebase.specs :as specs]))
18 |
19 |
20 | (s/def ::cache (s/nilable (s/keys)))
21 |
22 | (defn- fb-ref [path]
23 | {:pre [(utils/validate ::specs/path path)]}
24 | (.ref (js/firebase.database)
25 | (str/join "/" (clj->js path))))
26 |
27 | (defn- setter [{:keys [path value on-success on-failure]}]
28 | (.set (fb-ref path)
29 | (clj->js value)
30 | (success-failure-wrapper on-success on-failure)))
31 |
32 | (def write-effect setter)
33 |
34 | (defn- updater [{:keys [path value on-success on-failure]}]
35 | (.update (fb-ref path)
36 | (clj->js value)
37 | (success-failure-wrapper on-success on-success)))
38 |
39 | (def ^:private update-effect updater)
40 |
41 | (defn push-effect [{:keys [path value on-success on-failure] :as all}]
42 | (let [key (.-key (.push (fb-ref path)))]
43 | (setter (assoc all
44 | :on-success #((event->fn on-success) key)
45 | :path (conj path key)))))
46 |
47 | (defn once-effect [{:keys [path on-success on-failure]}]
48 | (.once (fb-ref path)
49 | "value"
50 | #((event->fn on-success) (js->clj-tree %))
51 | #((event->fn on-failure) %)))
52 |
53 | (defn on-value-sub [app-db [_ {:keys [path on-failure]}]]
54 | (if path
55 | (let [ref (fb-ref path)
56 | ;; [TODO] Potential bug alert:
57 | ;; We are caching the results, keyed only by path, and we clear
58 | ;; the cache entry in :on-dispose. I can imagine situations
59 | ;; where this would be problematic if someone tried watching the
60 | ;; same path from two code locations. If this becomes an issue, we
61 | ;; might need to add an optional disambiguation argument to the
62 | ;; subscription.
63 | ;; Note that firebase itself seems to guard against this by using
64 | ;; the callback itself as a unique key to .off. We can't do that
65 | ;; (modulo some reflection hack), since we use the id as part of
66 | ;; the callback closure.
67 | id path
68 | callback #(>evt [::on-value-handler id (js->clj-tree %)])]
69 | (.on ref "value" callback (event->fn (or on-failure (core/default-error-handler))))
70 | (make-reaction
71 | (fn [] (get-in @app-db [::cache id] []))
72 | :on-dispose #(do (.off ref "value" callback)
73 | (>evt [::on-value-handler id nil]))))
74 | (do
75 | (console :error "Received null Firebase on-value request")
76 | (make-reaction
77 | (fn []
78 | ;; Minimal dummy response, to avoid blowing up caller
79 | nil)))))
80 |
81 | (re-frame/reg-event-db
82 | ::on-value-handler
83 | (fn [app-db [_ id value]]
84 | (if value
85 | (assoc-in app-db [::cache id] value)
86 | (update app-db ::cache dissoc id))))
87 |
--------------------------------------------------------------------------------
/src/com/degel/re_frame_firebase/firestore.cljs:
--------------------------------------------------------------------------------
1 | (ns com.degel.re-frame-firebase.firestore
2 | (:require
3 | [clojure.spec.alpha :as s]
4 | [clojure.string :as str]
5 | [re-frame.core :as re-frame]
6 | [reagent.ratom :as ratom :refer [make-reaction]]
7 | [iron.re-utils :as re-utils :refer [evt event->fn sub->fn]]
8 | [iron.utils :as utils]
9 | [firebase.app :as firebase-app]
10 | [firebase.firestore :as firebase-firestore]
11 | [com.degel.re-frame-firebase.core :as core]
12 | [com.degel.re-frame-firebase.specs :as specs]
13 | [com.degel.re-frame-firebase.helpers :refer [promise-wrapper]]))
14 |
15 |
16 | (defn set-firestore-settings
17 | [settings]
18 | (.settings (js/firebase.firestore) (clj->js (or settings {}))))
19 |
20 | ;; Extra public functions
21 | (defn server-timestamp
22 | "Returns a field value to be used to store the server timestamp.
23 | See https://firebase.google.com/docs/firestore/manage-data/add-data#update_fields_in_nested_objects
24 | You should use this as a field value when setting/updating/adding a document.
25 |
26 | Example usage:
27 | {:firestore/add {:path [:some-colection]
28 | :data {:name \"document-with-timestamp\"
29 | :timestamp (server-timestamp)}}"
30 | []
31 | (.serverTimestamp js/firebase.firestore.FieldValue))
32 |
33 | (defn delete-field-value
34 | "Returns a field value to be used to delete a field.
35 | See https://firebase.google.com/docs/firestore/manage-data/delete-data#fields
36 | When updating a document, you should use this as a field value if you want to
37 | delete such field.
38 |
39 | Example usage:
40 | {:firestore/update {:path [:my \"document\"]
41 | :data {:field-to-delete (delete-field-value)}}}"
42 | []
43 | (.delete js/firebase.firestore.FieldValue))
44 |
45 | (defn document-id-field-path
46 | "Returns a field path which can be used to refer to ID of a document.
47 | See https://firebase.google.com/docs/reference/js/firebase.firestore.FieldPath#.documentId
48 | It can be used in queries to sort or filter by the document ID.
49 |
50 | Example usage:
51 | {:firestore/get {:path-collection [:my-collection]
52 | :where [[(document-id-field-path) :>= \"start\"]]}}"
53 | []
54 | (.documentId firebase.firestore.FieldPath))
55 |
56 |
57 | ;; Type Conversion/Parsing
58 | (defn clj->CollectionReference
59 | "Converts a seq of keywords and/or strings into a CollectionReference.
60 | The seq represents the path to the collection (e.g. [:path \"to\" :collection]).
61 | See https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference"
62 | [path]
63 | {:pre [(utils/validate ::specs/path-collection path)]}
64 | (if (instance? js/firebase.firestore.CollectionReference path)
65 | path
66 | (.collection (js/firebase.firestore)
67 | (str/join "/" (clj->js path)))))
68 |
69 | (defn clj->DocumentReference
70 | "Converts a seq of keywords and/or strings into a DocumentReference.
71 | The seq represents the path to the document (e.g. [:path-to \"document\"]).
72 | See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference"
73 | [path]
74 | {:pre [(utils/validate ::specs/path-document path)]}
75 | (if (instance? js/firebase.firestore.DocumentReference path)
76 | path
77 | (.doc (js/firebase.firestore)
78 | (str/join "/" (clj->js path)))))
79 |
80 | (defn clj->FieldPath
81 | "Converts a string/keyword or a seq of string/keywords into a FieldPath.
82 | Uses the FieldPath contructor.
83 | Only tries conversion if the argument isn't a FieldPath already.
84 | Possible arguments: \"string.dotted.path\", :keyword-path, [:path :in-a :seq], a FieldPath object.
85 | See https://firebase.google.com/docs/reference/js/firebase.firestore.FieldPath"
86 | [field-path]
87 | (cond
88 | (nil? field-path) nil
89 | (instance? js/firebase.firestore.FieldPath field-path) field-path
90 | (coll? field-path) (apply js/firebase.firestore.FieldPath. (clj->js field-path))
91 | :else (js/firebase.firestore.FieldPath. (clj->js field-path))))
92 |
93 | (defn clj->SetOptions
94 | "Converts a clojure-style map into a SetOptions satisfying one.
95 | The provided map can contain a :merge key with either true or false, and a
96 | :merge-fields key with a seq of field paths to be passed to clj->FieldPath.
97 | See https://firebase.google.com/docs/reference/js/firebase.firestore.SetOptions"
98 | [set-options]
99 | (as-> {} $
100 | (if (:merge set-options) (assoc $ :merge (:merge set-options)) $)
101 | (if (:marge-fields set-options)
102 | (assoc $ :mergeFields (into-array (map clj->FieldPath (:merge-fields set-options))))
103 | $)
104 | (clj->js $)))
105 |
106 | (defn clj->GetOptions
107 | "Converts a clojure-style map into a GetOptions satisfying one.
108 | The provided map can contain a :source key with one of the following values:
109 | :default, :server or :cache. You can also provide a string like \"server\".
110 | See https://firebase.google.com/docs/reference/js/firebase.firestore.GetOptions"
111 | [get-options]
112 | (if get-options
113 | (clj->js {:source (:source get-options :default)})
114 | #js {}))
115 |
116 | (defn clj->SnapshotListenOptions
117 | "Converts a clojure-style map into a SnapshotListenOptions satisfying one.
118 | The provided map can contain a :include-metadata-changes key with either true or false.
119 | See https://firebase.google.com/docs/reference/js/firebase.firestore.SnapshotListenOptions"
120 | [snapshot-listen-options]
121 | (if snapshot-listen-options
122 | (clj->js {:includeMetadataChanges (:include-metadata-changes snapshot-listen-options false)})
123 | #js {}))
124 |
125 | (defn clj->SnapshotOptions
126 | "Converts a clojure-style map into a SnapshotOptions satisfying one.
127 | The provided map can containe a :server-timestamps key with one of the following values:
128 | :estimate, :previous or :none. You can also provide a string like \"estimate\".
129 | See https://firebase.google.com/docs/reference/js/firebase.firestore.SnapshotOptions"
130 | [snapshot-options]
131 | (clj->js {:serverTimestamps (:server-timestamps snapshot-options :none)}))
132 |
133 | (defn PathReference->clj [reference]
134 | ;; [TODO]: Can this be optimized through some internal property of a Reference?
135 | "Converts a CollectionReference/DocumentReference into a vector of strings representing its path."
136 | (loop [ref reference
137 | result '()]
138 | (if ref
139 | (recur (.-parent ref) (conj result (.-id ref)))
140 | (vec result))))
141 |
142 | (defn SnapshotMetadata->clj [metadata]
143 | "Converts a SnapshotMetadata object into a clojure-style map."
144 | {:from-cache (.-fromCache metadata)
145 | :has-pending-writes (.-hasPendingWrites metadata)})
146 |
147 | (defn DocumentSnapshot->clj
148 | "Converts a DocumentSnapshot object into a clojure-style map.
149 | :data the document's contents (nil if it doesn't exist).
150 | :id a string representing document's id.
151 | :metadata metadata converted with SnapshotMetadata->clj.
152 | :ref the object's path converted with PathReference->clj.
153 | :object the original DocumentSnapshot if expose-objects argument
154 | is set to true (nil otherwise).
155 | See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot"
156 | ([doc]
157 | (DocumentSnapshot->clj doc nil nil nil))
158 | ([doc snapshot-options]
159 | (DocumentSnapshot->clj doc snapshot-options nil nil))
160 | ([doc snapshot-options expose-objects]
161 | (DocumentSnapshot->clj doc snapshot-options expose-objects nil))
162 | ([doc snapshot-options expose-objects sure-exists]
163 | {:data (when (or sure-exists (.-exists doc))
164 | (js->clj (.data doc (clj->SnapshotOptions snapshot-options))))
165 | :id (.-id doc)
166 | :metadata (SnapshotMetadata->clj (.-metadata doc))
167 | :ref (PathReference->clj (.-ref doc))
168 | :object (when expose-objects doc)}))
169 |
170 | (defn DocumentChange->clj
171 | "Converts a DocumentChange object into a clojure-style map.
172 | :doc the DocumentSnapshot converted with DocumentSnapshot->clj.
173 | :new-index a number.
174 | :old-index a number.
175 | :type a string.
176 | :object the original DocumentChange if expose-objects argument
177 | is set to true (nil otherwise).
178 | See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentChange"
179 | ([change] (DocumentChange->clj change nil nil))
180 | ([change snapshot-options] (DocumentChange->clj change snapshot-options nil))
181 | ([change snapshot-options expose-objects]
182 | {:doc (DocumentSnapshot->clj (.-doc change) snapshot-options expose-objects true)
183 | :new-index (.-newIndex change)
184 | :old-index (.-oldIndex change)
185 | :type (.-type change)
186 | :object (when expose-objects change)}))
187 |
188 | (defn QuerySnapshot->clj
189 | "Converts a QuerySnapshot object into a clojure-style map.
190 | :docs vector of documents converted with DocumentSnapshot->clj.
191 | :metadata metadata converted with SnapshotMetadata->clj.
192 | :size the number of documents.
193 | :doc-changes vector of DocumentChanges converted with DocumentChange->clj if
194 | doc-changes argument is set to true (nil otherwise).
195 | :object the original DocumentSnapshot if expose-objects argument
196 | is set to true (nil otherwise).
197 | See https://firebase.google.com/docs/reference/js/firebase.firestore.QuerySnapshot"
198 | ([query]
199 | (QuerySnapshot->clj query nil nil nil nil))
200 | ([query snapshot-options]
201 | (QuerySnapshot->clj query snapshot-options nil nil nil))
202 | ([query snapshot-options snapshot-listen-options]
203 | (QuerySnapshot->clj query snapshot-options snapshot-listen-options nil nil))
204 | ([query snapshot-options snapshot-listen-options doc-changes]
205 | (QuerySnapshot->clj query snapshot-options snapshot-listen-options doc-changes nil))
206 | ([query snapshot-options snapshot-listen-options doc-changes expose-objects]
207 | {:docs (vec (map #(DocumentSnapshot->clj % snapshot-options expose-objects true)
208 | (.-docs query)))
209 | :metadata (SnapshotMetadata->clj (.-metadata query))
210 | :size (.-size query)
211 | :doc-changes (when doc-changes
212 | (vec (map #(DocumentChange->clj % snapshot-options expose-objects)
213 | (.docChanges query (clj->SnapshotListenOptions snapshot-listen-options)))))
214 | :object (when expose-objects query)}))
215 |
216 |
217 | (defn- document-parser-wrapper [callback snapshot-options expose-objects]
218 | {:pre [(utils/validate (s/nilable :re-frame/vec-or-fn) callback)]}
219 | (when callback
220 | #((re-utils/event->fn callback)
221 | (DocumentSnapshot->clj % snapshot-options expose-objects false))))
222 |
223 | (defn- collection-parser-wrapper [callback snapshot-options snapshot-listen-options doc-changes expose-objects]
224 | {:pre [(utils/validate (s/nilable :re-frame/vec-or-fn) callback)]}
225 | (when callback
226 | #((re-utils/event->fn callback)
227 | (QuerySnapshot->clj % snapshot-options snapshot-listen-options doc-changes expose-objects))))
228 |
229 | (defn- reference-parser-wrapper [callback]
230 | {:pre [(utils/validate (s/nilable :re-frame/vec-or-fn) callback)]}
231 | (when callback #((re-utils/event->fn callback) (PathReference->clj %))))
232 |
233 |
234 | ;; re-frame Effects/Subscriptions
235 | (defn- setter
236 | ([path data set-options]
237 | (.set (clj->DocumentReference path)
238 | (clj->js data)
239 | (clj->SetOptions set-options)))
240 | ([instance path data set-options]
241 | (.set instance
242 | (clj->DocumentReference path)
243 | (clj->js data)
244 | (clj->SetOptions set-options))))
245 |
246 | (defn- updater
247 | ([path data] (.update (clj->DocumentReference path) (clj->js data)))
248 | ([instance path data] (.update instance (clj->DocumentReference path) (clj->js data))))
249 |
250 | (defn- deleter
251 | ([path] (.delete (clj->DocumentReference path)))
252 | ([instance path] (.delete instance (clj->DocumentReference path))))
253 |
254 | (defn set-effect [{:keys [path data set-options on-success on-failure]}]
255 | (promise-wrapper (setter path data set-options) on-success on-failure))
256 |
257 | (defn update-effect [{:keys [path data on-success on-failure]}]
258 | (promise-wrapper (updater path data) on-success on-failure))
259 |
260 | (defn delete-effect [{:keys [path on-success on-failure]}]
261 | (promise-wrapper (deleter path) on-success on-failure))
262 |
263 | (defn write-batch-effect [{:keys [operations on-success on-failure]}]
264 | (let [batch-instance (.batch (js/firebase.firestore))]
265 | (run! (fn [[event-type {:keys [path data set-options]}]]
266 | (case event-type
267 | :firestore/delete (deleter batch-instance path)
268 | :firestore/set (setter batch-instance path data set-options)
269 | :firestore/update (updater batch-instance path data)
270 | (js/alert "Internal error: unknown write effect: " event-type)))
271 | operations)
272 | (promise-wrapper (.commit batch-instance) on-success on-failure)))
273 |
274 | (defn- adder [path data]
275 | (.add (clj->CollectionReference path) (clj->js data)))
276 |
277 | (defn add-effect [{:keys [path data on-success on-failure]}]
278 | (promise-wrapper (adder path data) (reference-parser-wrapper on-success) on-failure))
279 |
280 | (defn- query [ref where order-by limit
281 | start-at start-after end-at end-before]
282 | (as-> ref $
283 | (if where
284 | (reduce
285 | (fn [$$ [field-path op value]] (.where $$ (clj->FieldPath field-path) (clj->js op) (clj->js value)))
286 | $ where)
287 | $)
288 | (if order-by
289 | (reduce
290 | (fn [$$ order] (.orderBy $$ (clj->js (nth order 0)) (clj->js (nth order 1 :asc))))
291 | $ order-by)
292 | $)
293 | (if limit (.limit $ limit) $)
294 | (if start-at (.apply (.-startAt $) $ (clj->js start-at)) $)
295 | (if start-after (.apply (.-startAfter $) $ (clj->js start-after)) $)
296 | (if end-at (.apply (.-endAt $) $ (clj->js end-at)) $)
297 | (if end-before (.apply (.-endBefore $) $ (clj->js end-before)) $)))
298 |
299 | (defn- getter-document [path get-options]
300 | (.get (clj->DocumentReference path) (clj->GetOptions get-options)))
301 |
302 | (defn- getter-collection [path get-options where order-by limit
303 | start-at start-after end-at end-before]
304 | (.get (query (clj->CollectionReference path) where order-by limit
305 | start-at start-after end-at end-before)
306 | (clj->GetOptions get-options)))
307 |
308 | (defn get-effect [{:keys [path-document
309 | path-collection where order-by limit
310 | start-at start-after end-at end-before
311 | doc-changes snapshot-listen-options
312 | get-options snapshot-options expose-objects
313 | on-success on-failure]}]
314 | (if path-document
315 | (promise-wrapper (getter-document path-document get-options)
316 | (document-parser-wrapper on-success snapshot-options expose-objects)
317 | on-failure)
318 | (promise-wrapper (getter-collection path-collection get-options where order-by limit
319 | start-at start-after end-at end-before)
320 | (collection-parser-wrapper on-success snapshot-options snapshot-listen-options
321 | doc-changes expose-objects)
322 | on-failure)))
323 |
324 | (defn- on-snapshotter [reference-or-query snapshot-listen-options on-next on-error]
325 | (.onSnapshot reference-or-query
326 | (clj->SnapshotListenOptions snapshot-listen-options)
327 | on-next
328 | (if on-error (event->fn on-error) (core/default-error-handler))))
329 |
330 | (defn on-snapshot [{:keys [path-document
331 | path-collection where order-by limit
332 | start-at start-after end-at end-before doc-changes
333 | snapshot-listen-options snapshot-options
334 | expose-objects
335 | on-next on-error]}]
336 | {:pre [(utils/validate :re-frame/vec-or-fn on-next)
337 | (utils/validate (s/nilable :re-frame/vec-or-fn) on-error)]}
338 | (if path-document
339 | (on-snapshotter (clj->DocumentReference path-document)
340 | snapshot-listen-options
341 | (document-parser-wrapper on-next snapshot-options expose-objects)
342 | on-error)
343 | (on-snapshotter (query (clj->CollectionReference path-collection) where order-by limit
344 | start-at start-after end-at end-before)
345 | snapshot-listen-options
346 | (collection-parser-wrapper on-next snapshot-options snapshot-listen-options
347 | doc-changes expose-objects)
348 | on-error)))
349 |
350 | (def on-snapshot-effect on-snapshot)
351 |
352 | (defn on-snapshot-sub [app-db [_ params]]
353 | ;; [TODO] Potential bug alert:
354 | ;; This works the same way as database/on-value-sub, except for UUIDs.
355 | (let [uuid (str (random-uuid))
356 | callback #(>evt [::on-snapshot-handler uuid %])
357 | unsubscribe (on-snapshot (assoc params :on-next callback))]
358 | (ratom/make-reaction
359 | (fn [] (get-in @app-db [::cache uuid] []))
360 | :on-dispose #(do (unsubscribe) (>evt [::on-snapshot-handler uuid nil])))))
361 |
362 | (re-frame/reg-event-db
363 | ::on-snapshot-handler
364 | (fn [app-db [_ uuid value]]
365 | (if value
366 | (assoc-in app-db [::cache uuid] value)
367 | (update app-db ::cache dissoc uuid))))
368 |
--------------------------------------------------------------------------------
/src/com/degel/re_frame_firebase/helpers.cljs:
--------------------------------------------------------------------------------
1 | ;;; Author: David Goldfarb (deg@degel.com)
2 | ;;; Copyright (c) 2017, David Goldfarb
3 |
4 | (ns com.degel.re-frame-firebase.helpers
5 | (:require
6 | [clojure.spec.alpha :as s]
7 | [iron.re-utils :as re-utils]
8 | [iron.utils :as utils]
9 | [com.degel.re-frame-firebase.core :as core]))
10 |
11 |
12 | ;;; Helper functions that straddle the line between this library and Iron
13 | ;;; utils. These may move, change, or be abandoned, as I get more comfortable
14 | ;;; with them.
15 |
16 |
17 | (defn js->clj-tree [x]
18 | (-> (.val x)
19 | js->clj
20 | clojure.walk/keywordize-keys))
21 |
22 |
23 | (defn promise-wrapper [promise on-success on-failure]
24 | {:pre [(utils/validate (s/nilable :re-frame/vec-or-fn) on-success)
25 | (utils/validate (s/nilable :re-frame/vec-or-fn) on-failure)]}
26 | (when on-success
27 | (.then promise (re-utils/event->fn on-success)))
28 | (if on-failure
29 | (.catch promise (re-utils/event->fn on-failure))
30 | (.catch promise (core/default-error-handler))))
31 |
32 |
33 | (defn success-failure-wrapper [on-success on-failure]
34 | {:pre [(utils/validate (s/nilable :re-frame/vec-or-fn) on-success)
35 | (utils/validate (s/nilable :re-frame/vec-or-fn) on-failure)]
36 | :post (fn? %)}
37 | (let [on-success (and on-success (re-utils/event->fn on-success))
38 | on-failure (and on-failure (re-utils/event->fn on-failure))]
39 | (fn [err]
40 | (cond (nil? err) (when on-success (on-success))
41 | on-failure (on-failure err)
42 | :else ((core/default-error-handler) err)))))
43 |
--------------------------------------------------------------------------------
/src/com/degel/re_frame_firebase/specs.cljc:
--------------------------------------------------------------------------------
1 | ;;; Author: David Goldfarb (deg@degel.com)
2 | ;;; Copyright (c) 2017, David Goldfarb
3 |
4 | (ns com.degel.re-frame-firebase.specs
5 | (:require
6 | [clojure.spec.alpha :as s]))
7 |
8 | ;; Database
9 | (s/def ::path (s/coll-of (s/or :string string? :keyword keyword?) :into []))
10 |
11 | ;; Firestore
12 | (s/def ::path-collection (s/and ::path #(odd? (count %))))
13 |
14 | (s/def ::path-document (s/and ::path #(even? (count %))))
15 |
--------------------------------------------------------------------------------