diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
new file mode 100644
index 0000000..f56af6f
--- /dev/null
+++ b/.github/workflows/cd.yml
@@ -0,0 +1,95 @@
+name: 'cd'
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+ paths:
+ - 'project.clj'
+
+permissions:
+ contents: read
+ # Needed for the 'trilom/file-changes-action' action
+ pull-requests: read
+
+# This allows a subsequently queued workflow run to interrupt previous runs
+concurrency:
+ group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ${{ fromJSON('["ubuntu-latest", "self-hosted"]')[github.repository == 'github/docs-internal'] }}
+ steps:
+
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+
+ - name: Setup java
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'zulu' # See 'Supported distributions' for available options
+ java-version: '11'
+
+ - name: Display (print) node and java versions
+ run:
+ node -v && java --version
+
+ - name: Install clojure tools
+ uses: DeLaGuardo/setup-clojure@9.5
+ with:
+ # Install just one or all simultaneously
+ # The value must indicate a particular version of the tool, or use 'latest'
+ # to always provision the latest version
+ cli: 1.11.1.1149 # Clojure CLI based on tools.deps
+ lein: 2.9.8 # Leiningen
+ clj-kondo: 2022.05.31 # Clj-kondo
+
+ - name: Get clj-kondo version. Clj-kondo searches for opportunities of optimizations
+ run: clj-kondo --version
+
+ # # Optional step:
+ # - name: Cache clojure dependencies
+ # uses: actions/cache@v3
+ # with:
+ # path: |
+ # ~/.m2/repository
+ # ~/.gitlibs
+ # ~/.deps.clj
+ # # List all files containing dependencies:
+ # key: cljdeps-${{ hashFiles('deps.edn') }}
+ # # key: cljdeps-${{ hashFiles('deps.edn', 'bb.edn') }}
+ # # key: cljdeps-${{ hashFiles('project.clj') }}
+ # # key: cljdeps-${{ hashFiles('build.boot') }}
+ # restore-keys: cljdeps-
+
+ - name: Test if vanilla clojure code is working
+ run: clojure -e "(+ 1 1)"
+
+ - name: Display Leiningen version
+ run: lein -v
+
+ # - name: Run cljfmt formatter
+ # run: lein cljfmt check
+
+ # - name: Run clj-kondo
+ # run: clj-kondo --lint src
+
+ - name: lein install on this version of firebase-re-frame
+ run:
+ lein install
+
+ # - name: Run lein tests
+ # run: lein test
+
+ - name: Deploy to Github Package Registry
+ env:
+ GITHUB_TOKEN: ${{ secrets.RE_FRAME_FIREBASE_REPOSITORY_SECRET_ACTIONS }}
+ run: |
+ mkdir -p ~/.m2
+ echo "
(re-frame/reg-event-fx
:update-status
- (fn [{db :db} [_ status-children]] ;; status-children is e.g. {:life 42, :universe 42, :everything 42}
+ (fn [{db :db} [_ status-children]] ;; status-children is e.g. {:life 42, "universe/subuniverse" 42, :everything 42}
{:firebase/update {:path [:status]
:value status-children
:on-success #(js/console.log "Updated status-children")
@@ -318,8 +325,57 @@ Example (diff in bold):
[multi-location-update-blogpost]: https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html
-Re-frame-firebase also supplies `:firebase/multi` to allow multiple write and/or
-pushes from a single event:
+The `:firebase/transaction` effect handler and its more Clojure-y variant
+`:firebase/swap` perform atomic modifications to the tree.
+
+In `:firebase/transaction`, the `:transaction-update` parameter is a function that takes
+one parameter that is the old value at the `:path` location and returns the new
+value. Note that the function may be called multiple times, so should be free of side
+effects.
+
+The function must also tolerate a `nil` input gracefully. To abort a transaction, say to
+avoid overwriting an existing value, the function returns `js/undefined`.
+
+Finally, note that the `:apply-locally` boolean indicates whether the local
+firebase-system cached value should be applied optimistically, which may result in more
+than one update event to be emitted if the function needs to be run more than once. The
+default value is `true`.
+
+```clojure
+{:firebase/transaction {:path [:my :data]
+ :transaction-update (fn [old-val] (if old-val (inc old-val)))
+ :apply-locally false ;; default is true = multiple update events may be received if transaction-update needs to be run more than once.
+ ;; The on-* handlers can also take a re-frame event
+ :on-success (fn [snapshot committed] (if committed (prn "Transaction committed: " snapshot)))
+ :on-failure (fn [err snapshot committed] (prn "Error: " err))}}
+```
+
+`:firebase/swap` is similar to `:firebase/transaction` but takes an `:argv` argument,
+typically a vector. The argument `:f` is the renamed `:transaction-update`, the
+update function. The old value at the `:path` is prepended to `:argv` and then `:f`
+is applied much like `clojure.core\swap!` does for atoms.
+
+Both atomic effect handlers are provided to appeal to users coming from Firebase-first
+or Clojure-first backgrounds, respectively. For those coming from Firebase, note that
+the `snapshot` and `committed` parameters are reversed in on-*. This is to facilitate
+re-frame event handlers as they receive only the first passed parameter, ignoring the
+rest. Passing snapshot rather than committed makes for more useful possibilities.
+
+Example (diff in bold):
+
+
+{:firebase/swap {:path [:my :data]
+ :f +
+ :argv [2 3] ;; So the swap will perform (+ old-value 2 3)
+ :apply-locally false ;; default is true = multiple update events may be received if transaction-update needs to be run more than once.
+ ;; The on-* handlers can also take a re-frame event
+ :on-success (fn [snapshot committed] (if committed (prn "Transaction committed: " snapshot)))
+ :on-failure [:handle-failure]}}
+
+
+
+Re-frame-firebase also supplies `:firebase/multi` to allow multiple writes and other
+effects from a single event:
```clojure
(re-frame/reg-event-fx
diff --git a/project.clj b/project.clj
index eaae33b..95a5a93 100644
--- a/project.clj
+++ b/project.clj
@@ -1,20 +1,42 @@
;;; Author: David Goldfarb (deg@degel.com)
;;; Copyright (c) 2017-8, David Goldfarb
-(defproject com.degel/re-frame-firebase "0.9.0-SNAPSHOT"
+(defproject tallyfor/re-frame-firebase "0.10.3"
:description "A re-frame wrapper around firebase"
:url "https://github.com/deg/re-frame-firebase"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.10.0"]
[org.clojure/clojurescript "1.10.439"]
- [cljsjs/firebase "5.7.3-1"]
+ [cljsjs/firebase "7.5.0-0"] ;;"5.7.3-1"
[re-frame "0.10.6"]
- [com.degel/iron "0.4.0"]]
+ [com.degel/iron "0.4.0"]
+ [lein-pprint "1.3.2"]
+ [lein-cljsbuild "1.1.8"]
+ [lein-bump-version "0.1.6"]]
+ ;; run lein install with LEIN_SNAPSHOTS_IN_RELEASE=true lein install
+ :lein-tools-deps/config {:config-files [:install :user :project]}
+
:jvm-opts ^:replace ["-Xmx1g" "-server"]
:cljsbuild {:builds {}} ; prevent https://github.com/emezeske/lein-cljsbuild/issues/413
:plugins [[lein-npm "0.6.2"]]
:npm {:dependencies [[source-map-support "0.5.6"]]}
+ :profiles {:dev {:dependencies [[clj-stacktrace "0.2.8"]
+ [binaryage/devtools "0.9.10"]
+ [org.clojure/tools.namespace "1.1.0"]]}}
:source-paths ["src" "target/classes"]
+ ;; Change your environment variables (maybe editing .zshrc or .bashrc) to have:
+ ;; export LEIN_USERNAME="pdelfino"
+ ;; export LEIN_PASSWORD="your-personal-access-token-the-same-used-on-.npmrc"
+ ;; LEIN_PASSWORD should use the same Token used by .npmrc
+ ;; Also, do "LEIN_SNAPSHOTS_IN_RELEASE=true lein install" or edit your .zshrc:
+ ;; export LEIN_SNAPSHOTS_IN_RELEASE=true
+ :repositories {"releases" {:url "https://maven.pkg.github.com/tallyfor/*"
+ :username :env/LEIN_USERNAME ;; change your env
+ :password :env/LEIN_PASSWORD}}
+
+ :pom-addition [:distribution-management [:repository [:id "github"]
+ [:name "GitHub Packages"]
+ [:url "https://maven.pkg.github.com/tallyfor/re-frame-firebase"]]]
:clean-targets ["out" "release"]
:target-path "target")
diff --git a/src/com/degel/re_frame_firebase.cljs b/src/com/degel/re_frame_firebase.cljs
index e71dbc8..cbb0527 100644
--- a/src/com/degel/re_frame_firebase.cljs
+++ b/src/com/degel/re_frame_firebase.cljs
@@ -11,7 +11,8 @@
[com.degel.re-frame-firebase.core :as core]
[com.degel.re-frame-firebase.auth :as auth]
[com.degel.re-frame-firebase.database :as database]
- [com.degel.re-frame-firebase.firestore :as firestore]))
+ [com.degel.re-frame-firebase.firestore :as firestore]
+ [com.degel.re-frame-firebase.storage :as storage]))
;;; Write a value to Firebase.
;;; See https://firebase.google.com/docs/reference/js/firebase.database.Reference#set
@@ -35,6 +36,28 @@
;;;
(re-frame/reg-fx :firebase/update database/update-effect)
+;;; Transactionally reads and writes a value to Firebase. NB: :transaction-update function
+;;; may run more than once so must be free of side effects. Importantly, it must be able
+;;; to handle null data. To abort a transaction, return js/undefined.
+;;; See https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction
+;;;
+;;; Examples FX:
+;;; {:firebase/transaction {:path [:my :data]
+;;; :transaction-update (fn [old-val] (if old-val (inc old-val)))
+;;; :apply-locally false ;; default is true = multiple update events may be received if transaction-update needs to be run more than once.
+;;; ;; The on-* handlers can also take a re-frame event
+;;; :on-success (fn [snapshot committed] (if committed (prn "Transaction committed: " snapshot)))
+;;; :on-failure (fn [err snapshot committed] (prn "Error: " err))}}
+;;;
+;;; {:firebase/swap {:path [:my :data]
+;;; :f +
+;;; :argv [2 3]
+;;; :apply-locally false ;; default is true = multiple update events may be received if transaction-update needs to be run more than once.
+;;; ;; The on-* handlers can also take a re-frame event
+;;; :on-success (fn [snapshot committed] (if committed (prn "Transaction committed: " snapshot)))
+;;; :on-failure [:firebase-error]}}
+(re-frame/reg-fx :firebase/transaction database/transaction-effect)
+(re-frame/reg-fx :firebase/swap database/swap-effect) ; A synonym with :argv for update function :f
;;; Write a value to a Firebase list.
;;; See https://firebase.google.com/docs/reference/js/firebase.database.Reference#push
@@ -74,6 +97,8 @@
:firebase/write (database/write-effect args)
:firebase/update (database/update-effect args)
:firebase/push (database/push-effect args)
+ :firebase/transaction (database/transaction-effect args)
+ :firebase/swap (database/swap-effect args)
:firebase/read-once (database/once-effect args)
:firestore/delete (firestore/delete-effect args)
:firestore/set (firestore/set-effect args)
@@ -120,10 +145,11 @@
;;; "https://www.googleapis.com/auth/calendar.readonly"]
;;; :custom-parameters {"login_hint" "user@example.com"}}}
;;;
-(re-frame/reg-fx :firebase/google-sign-in auth/google-sign-in)
-(re-frame/reg-fx :firebase/facebook-sign-in auth/facebook-sign-in)
-(re-frame/reg-fx :firebase/twitter-sign-in auth/twitter-sign-in)
-(re-frame/reg-fx :firebase/github-sign-in auth/github-sign-in)
+(re-frame/reg-fx :firebase/google-sign-in auth/google-sign-in)
+(re-frame/reg-fx :firebase/facebook-sign-in auth/facebook-sign-in)
+(re-frame/reg-fx :firebase/twitter-sign-in auth/twitter-sign-in)
+(re-frame/reg-fx :firebase/github-sign-in auth/github-sign-in)
+(re-frame/reg-fx :firebase/microsoft-sign-in auth/microsoft-sign-in)
;;; Login to firebase using email/password authentication
@@ -360,6 +386,11 @@
;;;
(re-frame/reg-sub-raw :firestore/on-snapshot firestore/on-snapshot-sub)
+;;; Firebase Storage, an online object store, different from the similarly named Firestore.
+
+(re-frame/reg-fx :storage/put storage/put-effect)
+(re-frame/reg-fx :storage/delete storage/delete-effect)
+
;;; Start library and register callbacks.
;;;
diff --git a/src/com/degel/re_frame_firebase/auth.cljs b/src/com/degel/re_frame_firebase/auth.cljs
index f459f27..4e1aacd 100644
--- a/src/com/degel/re_frame_firebase/auth.cljs
+++ b/src/com/degel/re_frame_firebase/auth.cljs
@@ -75,7 +75,6 @@
(>evt [(core/default-error-handler)
(js/Error. (str "Unsupported sign-in-method: " sign-in-method ". Either :redirect or :popup are supported."))]))))
-
(defn google-sign-in
[opts]
;; TODO: use Credential for mobile.
@@ -97,6 +96,12 @@
(oauth-sign-in (js/firebase.auth.GithubAuthProvider.) opts))
+(defn microsoft-sign-in
+ [opts]
+ (oauth-sign-in (js/firebase.auth.OAuthProvider. "microsoft.com") opts))
+
+
+
(defn email-sign-in [{:keys [email password]}]
(-> (js/firebase.auth)
(.signInWithEmailAndPassword email password)
diff --git a/src/com/degel/re_frame_firebase/database.cljs b/src/com/degel/re_frame_firebase/database.cljs
index 2208695..48e18e4 100644
--- a/src/com/degel/re_frame_firebase/database.cljs
+++ b/src/com/degel/re_frame_firebase/database.cljs
@@ -3,18 +3,18 @@
(ns com.degel.re-frame-firebase.database
(:require
- [clojure.spec.alpha :as s]
- [clojure.string :as str]
- [re-frame.core :as re-frame]
- [re-frame.loggers :refer [console]]
- [reagent.ratom :as ratom :refer [make-reaction]]
- [iron.re-utils :refer [evt event->fn sub->fn]]
- [iron.utils :as utils]
- [firebase.app :as firebase-app]
- [firebase.database :as firebase-database]
- [com.degel.re-frame-firebase.helpers :refer [js->clj-tree success-failure-wrapper]]
- [com.degel.re-frame-firebase.core :as core]
- [com.degel.re-frame-firebase.specs :as specs]))
+ [clojure.spec.alpha :as s]
+ [clojure.string :as str]
+ [re-frame.core :as re-frame]
+ [re-frame.loggers :refer [console]]
+ [reagent.ratom :as ratom :refer [make-reaction]]
+ [iron.re-utils :refer [evt event->fn sub->fn]]
+ [iron.utils :as utils]
+ [firebase.app :as firebase-app]
+ [firebase.database :as firebase-database]
+ [com.degel.re-frame-firebase.helpers :refer [js->clj-tree success-failure-wrapper]]
+ [com.degel.re-frame-firebase.core :as core]
+ [com.degel.re-frame-firebase.specs :as specs]))
(s/def ::cache (s/nilable (s/keys)))
@@ -38,6 +38,47 @@
(def ^:private update-effect updater)
+(defn- transaction->js
+ [retval]
+ ;; Preserve js/undefined as it signals to abort the transaction.
+ ;; https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction
+ (if (= js/undefined retval)
+ retval
+ (clj->js retval)))
+
+(defn- transaction-update-wrapper [transaction-update]
+ (fn [old-value]
+ (-> old-value
+ js->clj
+ clojure.walk/keywordize-keys
+ transaction-update
+ transaction->js)))
+
+(defn- transactioner [{:keys [path transaction-update on-success on-failure apply-locally]}]
+ (try
+ (.transaction (fb-ref path)
+ (transaction-update-wrapper transaction-update)
+ (success-failure-wrapper on-success on-failure)
+ ;; Force apply-locally to be a boolean, as required by .transaction.
+ (if (or (false? apply-locally)
+ (nil? apply-locally))
+ false
+ true))
+ (catch :default e (on-failure e))))
+
+
+(def transaction-effect transactioner)
+
+(defn- swapper [{:keys [path f argv on-success on-failure apply-locally]}]
+ (transactioner
+ {:path path
+ :transaction-update (fn [old-val] (apply f old-val argv))
+ :on-success on-success
+ :on-failure on-failure
+ :apply-locally apply-locally}))
+
+(def swap-effect swapper)
+
(defn push-effect [{:keys [path value on-success on-failure] :as all}]
(let [key (.-key (.push (fb-ref path)))]
(setter (assoc all
@@ -68,19 +109,19 @@
callback #(>evt [::on-value-handler id (js->clj-tree %)])]
(.on ref "value" callback (event->fn (or on-failure (core/default-error-handler))))
(make-reaction
- (fn [] (get-in @app-db [::cache id] []))
- :on-dispose #(do (.off ref "value" callback)
- (>evt [::on-value-handler id nil]))))
+ (fn [] (get-in @app-db [::cache id] nil))
+ :on-dispose #(do (.off ref "value" callback)
+ (>evt [::on-value-handler id nil]))))
(do
(console :error "Received null Firebase on-value request")
(make-reaction
- (fn []
- ;; Minimal dummy response, to avoid blowing up caller
- nil)))))
+ (fn []
+ ;; Minimal dummy response, to avoid blowing up caller
+ nil)))))
(re-frame/reg-event-db
- ::on-value-handler
- (fn [app-db [_ id value]]
- (if value
- (assoc-in app-db [::cache id] value)
- (update app-db ::cache dissoc id))))
+ ::on-value-handler
+ (fn [app-db [_ id value]]
+ (if value
+ (assoc-in app-db [::cache id] value)
+ (update app-db ::cache dissoc id))))
diff --git a/src/com/degel/re_frame_firebase/helpers.cljs b/src/com/degel/re_frame_firebase/helpers.cljs
index de79167..3b7a222 100644
--- a/src/com/degel/re_frame_firebase/helpers.cljs
+++ b/src/com/degel/re_frame_firebase/helpers.cljs
@@ -29,14 +29,50 @@
(.catch promise (re-utils/event->fn on-failure))
(.catch promise (core/default-error-handler))))
-
(defn success-failure-wrapper [on-success on-failure]
{:pre [(utils/validate (s/nilable :re-frame/vec-or-fn) on-success)
(utils/validate (s/nilable :re-frame/vec-or-fn) on-failure)]
:post (fn? %)}
(let [on-success (and on-success (re-utils/event->fn on-success))
- on-failure (and on-failure (re-utils/event->fn on-failure))]
- (fn [err]
- (cond (nil? err) (when on-success (on-success))
- on-failure (on-failure err)
- :else ((core/default-error-handler) err)))))
+ on-failure (and on-failure (re-utils/event->fn on-failure))
+ wrapped-handler (fn
+ ([err] (cond (nil? err) (when on-success (on-success))
+ on-failure (on-failure err)
+ :else ((core/default-error-handler) err)))
+
+ ;; I am unable to find in the Google Firebase documentation* a 2-arity
+ ;; callback for .set .update or .transaction that uses this wrapper. Yet, I've
+ ;; observed that such a callback exists specifically on .update. With
+ ;; trepidation arising from minimal ad hoc testing, I am forwarding the second
+ ;; parameter, assuming that this behavior was undetected and inconsequential before
+ ;; I wrote wrapped-handler to be multi-arity.
+ ;;
+ ;; [TODO] Find the reason for this 2-arity version and properly dispatch it.
+ ;;
+ ;; * https://firebase.google.com/docs/reference/js/firebase.database.Reference
+ ([err other]
+ (cond (nil? err) (when on-success (on-success other))
+ on-failure (on-failure err other)
+ :else ((core/default-error-handler) err)))
+
+ ;; onComplete invoked in :firebase/transaction and :firebase/swap accepts an
+ ;; error code, a boolean indicating committed status, and a snapshot of the
+ ;; data at that path.
+ ;;
+ ;; This is useful for exposing state changes upon completion of the
+ ;; transaction, as the transaction-update or f functions must be side-effect
+ ;; free. Notably here, we reverse the order of committed and snapshot in the
+ ;; cljs versions on-success and on-failure. So, if the on-success handler is
+ ;; a re-frame event vector (in iron.re-utils/re-utils they only take the first
+ ;; parameter), it gets the snapshotted data. An on-failure event handler
+ ;; would get the error code; it has snapshot and committed reversed for
+ ;; continuity.
+ ([err committed snapshot]
+ (cond (nil? err) (when on-success (on-success (js->clj-tree snapshot) committed))
+ on-failure (on-failure err (js->clj-tree snapshot) committed)
+ :else ((core/default-error-handler) err))))]
+ wrapped-handler))
+
+
+
+
diff --git a/src/com/degel/re_frame_firebase/storage.cljs b/src/com/degel/re_frame_firebase/storage.cljs
new file mode 100644
index 0000000..875e879
--- /dev/null
+++ b/src/com/degel/re_frame_firebase/storage.cljs
@@ -0,0 +1,43 @@
+(ns com.degel.re-frame-firebase.storage
+ (:require
+ [clojure.spec.alpha :as s]
+ [clojure.string :as str]
+ [re-frame.core :as re-frame]
+ [reagent.ratom :as ratom :refer [make-reaction]]
+ [iron.re-utils :as re-utils :refer [evt event->fn sub->fn]]
+ [iron.utils :as utils]
+ [firebase.app :as firebase-app]
+ [firebase.storage :as firebase-storage]
+ [com.degel.re-frame-firebase.core :as core]
+ [com.degel.re-frame-firebase.specs :as specs]
+ [com.degel.re-frame-firebase.helpers :refer [promise-wrapper]]))
+
+
+;;; 1. Create a root reference
+;;; 2. Create reference to end object
+;;; 3. Upload blob/file
+
+(defn clj->StorageReference
+ "Converts path, a string/keyword or seq of string/keywords, into a StorageReference"
+ [path]
+ {:pre [(utils/validate ::specs/path path)]}
+ (if (instance? js/firebase.storage.Reference path)
+ path
+ (.child
+ (.ref (js/firebase.storage))
+ (str/join "/" (clj->js path)))))
+
+(defn- putter
+ [path blob]
+ (.put (clj->StorageReference path)
+ blob))
+
+(defn put-effect [{:keys [path data on-success on-failure]}]
+ (promise-wrapper (putter path data) on-success on-failure))
+
+(defn- deleter
+ [path]
+ (.delete (clj->StorageReference path)))
+
+(defn delete-effect [{:keys [path on-success on-failure]}]
+ (promise-wrapper (deleter path) on-success on-failure))