├── .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 | [![Clojars Project](https://img.shields.io/clojars/v/com.degel/re-frame-firebase.svg)](https://clojars.org/com.degel/re-frame-firebase) 29 | [![Dependencies Status](https://versions.deps.co/deg/re-frame-firebase/status.svg)](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 | --------------------------------------------------------------------------------