diff --git a/.gitignore b/.gitignore index d4a3ab9..2c4896e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ .lein-deps-sum .lein-plugins/ .lein-repl-history +.clj-kondo +.lsp .repl* /*.iml /.cljs_node_repl/ diff --git a/README.md b/README.md index 38e9891..adde1ba 100644 --- a/README.md +++ b/README.md @@ -528,6 +528,97 @@ as it is meant to be used as a subscription. ``` +### Storage + +[Storage](https://firebase.google.com/docs/storage) Cloud Storage for Firebase is a powerful, simple, and cost-effective object storage service. +It uses effects for put, delete, download operations. + +#### Put +Puts a collection of File objects into a Firebase Storage bucket. +See: https://firebase.google.com/docs/storage/web/upload-files + +Required arguments :path :file + + - :path Path to object in the Storage bucket + - :file File (https://developer.mozilla.org/en-US/docs/Web/API/File) + - :bucket If not supplied, will assume the default Firebase allocated bucket + - :metadata Map of metadata to set on Storage object + - :on-progress Will be provided with the percentage complete + - :on-success + - :on-error + + Example FX: + ```clojure + {:storage/put [{:path "path/to/object" + :file File + :metadata {:customMetadata {"some-key" "some-value"}} + :on-progress #(.log js/console (str "Upload is " % "% complete.")) + :on-success #(js/alert "Success!") + :on-error #(js/alert "Error: " %)}]} +``` + +#### Delete +Deletes a collection of object paths/keys from a Firebase Storage bucket. +See: https://firebase.google.com/docs/storage/web/delete-files + +Required arguments :path + +- :path Path to object in the Storage bucket +- :bucket If not supplied, will assume the default Firebase allocated bucket +- :on-success +- :on-error + +Example FX: + ```clojure + {:storage/delete [{:path "path/to/object" + :on-success #(js/alert "Success!") + :on-error #(js/alert "Error: " %)}]} + ``` + + +#### Download URL +Generates a url with which the browser can download an object from Firebase Storage +See: https://firebase.google.com/docs/storage/web/download-files + +Required arguments: :path + +- :path Path to object in the Storage bucket +- :bucket If not supplied, will assume the default Firebase allocated bucket +- :on-success Will be provided with the download url +- :on-error + +Example FX: + ```clojure + {:storage/download-url {:path "path/to/object" + :on-success #(js/window.open %) + :on-error #(js/alert "Error: " %)}} + ``` + + +### Functions + +[Functions](https://firebase.google.com/docs/functions) Cloud Functions for Firebase is a serverless framework that lets you automatically run backend code in response to events. +It uses effects for call. + +#### Call +Executes a Callable Firebase Cloud Function +See: https://firebase.google.com/docs/functions/callable + +Required arguments: :cfn-name :data +- :cfn-name Cloud Function name +- :data Map containing request data +- :on-success Will be called with a clojure Map containing the response data +- :on-error + +Example FX: + ```clojure + {:functions/call {:cfn-name "my-function-name" + :data {:foo "bar"} + :on-success #(js/alert (:foobar %)) + :on-error #(js/alert "Error: " %)}} + ``` + + ## Examples and projects There are examples provided in the [examples](examples) folder. It is great to diff --git a/project.clj b/project.clj index eaae33b..b760358 100644 --- a/project.clj +++ b/project.clj @@ -6,11 +6,11 @@ :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"] - [re-frame "0.10.6"] - [com.degel/iron "0.4.0"]] + ;; :dependencies [[org.clojure/clojure "1.10.0"] + ;; [org.clojure/clojurescript "1.10.439"] + ;; [cljsjs/firebase "5.7.3-1"] + ;; [re-frame "0.10.6"] + ;; [com.degel/iron "0.4.0"]] :jvm-opts ^:replace ["-Xmx1g" "-server"] :cljsbuild {:builds {}} ; prevent https://github.com/emezeske/lein-cljsbuild/issues/413 :plugins [[lein-npm "0.6.2"]] diff --git a/src/com/degel/re_frame_firebase.cljs b/src/com/degel/re_frame_firebase.cljs index e71dbc8..45bdece 100644 --- a/src/com/degel/re_frame_firebase.cljs +++ b/src/com/degel/re_frame_firebase.cljs @@ -11,7 +11,11 @@ [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] + [com.degel.re-frame-firebase.functions :as functions] + [com.degel.re-frame-firebase.analytics :as analytics] + [com.degel.re-frame-firebase.app-check :as app-check])) ;;; Write a value to Firebase. ;;; See https://firebase.google.com/docs/reference/js/firebase.database.Reference#set @@ -141,6 +145,50 @@ (re-frame/reg-fx :firebase/email-create-user auth/email-create-user) +;;; Updates the user's display name and profile photo URL +;;; +;;; Accepts a map with :profile :on-success :on-error +;;; +;;; Example FX: +;;; {:firebase/update-profile {:profile {:displayName "Joe Soap" +;;; :photoURL "http://my.photo.com"} +;;; :on-success #(js/alert "Success!") +;;; :on-error #(js/alert "Error!")}} +;;; +(re-frame/reg-fx :firebase/update-profile auth/update-profile) + +;;; Updates the user's email address +;;; +;;; Accepts a map with :email :on-success :on-error +;;; +;;; Example FX: +;;; {:firebase/update-email {:email "joe@soap.com" +;;; :on-success #(js/alert "Success!") +;;; :on-error #(js/alert "Error!")}} +;;; +(re-frame/reg-fx :firebase/update-email auth/update-email) + +;;; Send a user a verification email +;;; +;;; Accepts a map with :on-success :on-error +;;; +;;; Example FX: +;;; {:firebase/send-email-verification {:on-success #(js/alert "Success!") +;;; :on-error #(js/alert "Error!")}} +;;; +(re-frame/reg-fx :firebase/send-email-verification auth/send-email-verification) + +;;; Applies a verification code sent to the user by email +;;; +;;; Accepts a map with :action-code :on-success :on-error +;;; +;;; Example FX: +;;; {:firebase/apply-action-code {:action-code "1234567" +;;; :on-success #(js/alert "Success!") +;;; :on-error #(js/alert "Error!")}} +;;; +(re-frame/reg-fx :firebase/apply-action-code auth/apply-action-code) + ;;; Login to firebase anonymously ;;; ;;; Parameter is not used @@ -361,6 +409,103 @@ (re-frame/reg-sub-raw :firestore/on-snapshot firestore/on-snapshot-sub) +;;; Puts a collection of File objects into a Firebase Storage bucket. +;;; See: https://firebase.google.com/docs/storage/web/upload-files +;;; +;;; Required arguments: :path :file +;;; +;;; - :path Path to object in the Storage bucket +;;; - :file File (https://developer.mozilla.org/en-US/docs/Web/API/File) +;;; - :bucket If not supplied, will assume the default Firebase allocated bucket +;;; - :metadata Map of metadata to set on Storage object +;;; - :on-progress Will be provided with the percentage complete +;;; - :on-success +;;; - :on-error +;;; +;;; Example FX: +;;; {:storage/put [{:path "path/to/object" +;;; :file File +;;; :metadata {:customMetadata {"some-key" "some-value"}} +;;; :on-progress #(.log js/console (str "Upload is " % "% complete.")) +;;; :on-success #(js/alert "Success!") +;;; :on-error #(js/alert "Error: " %)}]} +;;; +(re-frame/reg-fx :storage/put storage/put-effect) + + +;;; Deletes a collection of object paths/keys from a Firebase Storage bucket. +;;; See: https://firebase.google.com/docs/storage/web/delete-files +;;; +;;; Required arguments: :path +;;; +;;; - :path Path to object in the Storage bucket +;;; - :bucket If not supplied, will assume the default Firebase allocated bucket +;;; - :on-success +;;; - :on-error +;;; +;;; Example FX: +;;; {:storage/delete [{:path "path/to/object" +;;; :on-success #(js/alert "Success!") +;;; :on-error #(js/alert "Error: " %)}]} +;;; +(re-frame/reg-fx :storage/delete storage/delete-effect) + + +;;; Generates a url with which the browser can download an object from Firebase Storage +;;; See: https://firebase.google.com/docs/storage/web/download-files +;;; +;;; Required arguments: :path +;;; +;;; - :path Path to object in the Storage bucket +;;; - :bucket If not supplied, will assume the default Firebase allocated bucket +;;; - :on-success Will be provided with the download url +;;; - :on-error +;;; +;;; Example FX: +;;; {:storage/download-url {:path "path/to/object" +;;; :on-success #(js/window.open %) +;;; :on-error #(js/alert "Error: " %)}} +;;; +(re-frame/reg-fx :storage/download-url storage/download-url-effect) + + +;;; Executes a Callable Firebase Cloud Function +;;; See: https://firebase.google.com/docs/functions/callable +;;; +;;; Required arguments: :cfn-name :data +;;; +;;; - :cfn-name Cloud Function name +;;; - :data Map containing request data +;;; - :on-success Will be called with a clojure Map containing the response data +;;; - :on-error +;;; +;;; Example FX: +;;; {:functions/call {:cfn-name "my-function-name" +;;; :data {:foo "bar"} +;;; :on-success #(js/alert (:foobar %)) +;;; :on-error #(js/alert "Error: " %)}} +;;; +(re-frame/reg-fx :functions/call functions/call-effect) + + + +;;; Logs an event in Firebase Analytics +;;; See: https://firebase.google.com/docs/analytics/events?authuser=0&platform=web +;;; +;;; Required arguments: :event :props +;;; +;;; - :event Name of the event (as keyword) +;;; - :props Map of key/value pairs of interesting information +;;; +;;; Example FX: +;;; {:analytics/log {:event :purchase-completed +;;; :props {:amount "123.45" +;;; :currency "USD"}}} +;;; +(re-frame/reg-fx :analytics/log analytics/log-effect) + + + ;;; Start library and register callbacks. ;;; ;;; @@ -394,12 +539,20 @@ ;;; (defn init [& {:keys [firebase-app-info firestore-settings + app-check-settings get-user-sub set-user-event - default-error-handler]}] + default-error-handler + firebase-products]}] (core/set-firebase-state :get-user-sub get-user-sub :set-user-event set-user-event :default-error-handler default-error-handler) (core/initialize-app firebase-app-info) - (firestore/set-firestore-settings firestore-settings) - (auth/init-auth)) + (when (firebase-products :firestore) + (firestore/set-firestore-settings firestore-settings)) + (when (firebase-products :analytics) + (analytics/init)) + (when (firebase-products :auth) + (auth/init-auth)) + (when (firebase-products :app-check) + (app-check/init app-check-settings))) diff --git a/src/com/degel/re_frame_firebase/analytics.cljs b/src/com/degel/re_frame_firebase/analytics.cljs new file mode 100644 index 0000000..94e784b --- /dev/null +++ b/src/com/degel/re_frame_firebase/analytics.cljs @@ -0,0 +1,17 @@ +(ns com.degel.re-frame-firebase.analytics + (:require + ["@firebase/analytics" :refer (getAnalytics logEvent)] + [com.degel.re-frame-firebase.core :as core])) + +(defn init + [] + (swap! core/firebase-state assoc + :analytics (-> @core/firebase-state + :app + getAnalytics))) + +(defn log-effect + [{:keys [event props]} _] + (-> @core/firebase-state + :analytics + (logEvent (name event) (clj->js props)))) diff --git a/src/com/degel/re_frame_firebase/app_check.cljs b/src/com/degel/re_frame_firebase/app_check.cljs new file mode 100644 index 0000000..742c9f9 --- /dev/null +++ b/src/com/degel/re_frame_firebase/app_check.cljs @@ -0,0 +1,14 @@ +(ns com.degel.re-frame-firebase.app-check + (:require + ["@firebase/app-check" :refer (initializeAppCheck ReCaptchaV3Provider)] + [com.degel.re-frame-firebase.core :as core])) + +(defn init + [settings] + (when (:debug-provider settings) + (set! js/FIREBASE_APPCHECK_DEBUG_TOKEN true)) + (swap! core/firebase-state assoc + :app-check (initializeAppCheck (:app @core/firebase-state) + (clj->js + {:provider (ReCaptchaV3Provider. (:site-key settings)) + :isTokenAutoRefreshEnabled true})))) diff --git a/src/com/degel/re_frame_firebase/auth.cljs b/src/com/degel/re_frame_firebase/auth.cljs index f459f27..8a1e25c 100644 --- a/src/com/degel/re_frame_firebase/auth.cljs +++ b/src/com/degel/re_frame_firebase/auth.cljs @@ -7,9 +7,9 @@ [clojure.spec.alpha :as s] [re-frame.core :as re-frame] [iron.re-utils :refer [>evt]] - [firebase.app :as firebase-app] - [firebase.auth :as firebase-auth] - [com.degel.re-frame-firebase.core :as core])) + [com.degel.re-frame-firebase.core :as core] + ["@firebase/auth" :refer (getAuth onAuthStateChanged getRedirectResult updateProfile + sendEmailVerification applyActionCode updateEmail)])) (defn- user @@ -31,19 +31,19 @@ (core/set-current-user))) (defn- init-auth [] - (.onAuthStateChanged - (js/firebase.auth) - set-user - (core/default-error-handler)) - (-> (js/firebase.auth) - (.getRedirectResult) + (onAuthStateChanged (getAuth) set-user (core/default-error-handler)) + + (-> (getAuth) + getRedirectResult (.then (fn on-user-credential [user-credential] - (-> user-credential - (.-user) - set-user))) + (when user-credential + (-> user-credential + (.-user) + set-user)))) (.catch (core/default-error-handler)))) + (def ^:private sign-in-fns {:popup (memfn signInWithPopup auth-provider) :redirect (memfn signInWithRedirect auth-provider)}) @@ -62,13 +62,13 @@ :or {sign-in-method :redirect}} opts] (doseq [scope scopes] - (.addScope auth-provider scope)) + (.addScope ^js auth-provider scope)) (when custom-parameters - (.setCustomParameters auth-provider (clj->js custom-parameters))) + (.setCustomParameters ^js auth-provider (clj->js custom-parameters))) (if-let [sign-in (sign-in-fns sign-in-method)] - (-> (js/firebase.auth) + (-> (getAuth) (sign-in auth-provider) (.then (partial maybe-link-with-credential link-with-credential)) (.catch (core/default-error-handler))) @@ -98,28 +98,28 @@ (defn email-sign-in [{:keys [email password]}] - (-> (js/firebase.auth) + (-> (getAuth) (.signInWithEmailAndPassword email password) (.then set-user) (.catch (core/default-error-handler)))) (defn email-create-user [{:keys [email password]}] - (-> (js/firebase.auth) + (-> (getAuth) (.createUserWithEmailAndPassword email password) (.then set-user) (.catch (core/default-error-handler)))) (defn anonymous-sign-in [opts] - (-> (js/firebase.auth) + (-> (getAuth) (.signInAnonymously) (.then set-user) (.catch (core/default-error-handler)))) (defn custom-token-sign-in [{:keys [token]}] - (-> (js/firebase.auth) + (-> (getAuth) (.signInWithCustomToken token) (.then set-user) (.catch (core/default-error-handler)))) @@ -136,7 +136,7 @@ (defn phone-number-sign-in [{:keys [phone-number on-send]}] (if-let [verifier (:recaptcha-verifier @core/firebase-state)] - (-> (js/firebase.auth) + (-> (getAuth) (.signInWithPhoneNumber phone-number verifier) (.then (fn [confirmation] (when on-send @@ -156,7 +156,40 @@ (.warn js/console "reCaptcha confirmation missing"))) -(defn sign-out [] - (-> (js/firebase.auth) +(defn sign-out + [{:keys [on-success on-error]}] + (-> (getAuth) (.signOut) - (.catch (core/default-error-handler)))) + (.then on-success) + (.catch #(on-error (-> % js->clj .-message))))) + +(defn update-profile + [{:keys [profile on-success on-error]}] + (-> (getAuth) + (.-currentUser) + (updateProfile (clj->js profile)) + (.then on-success) + (.catch #(on-error (-> % js->clj .-message))))) + +(defn update-email + [{:keys [email on-success on-error]}] + (-> (getAuth) + (.-currentUser) + (updateEmail email) + (.then on-success) + (.catch #(on-error (-> % js->clj .-message))))) + +(defn send-email-verification + [{:keys [action-code-settings on-success on-error]}] + (-> (getAuth) + (.-currentUser) + (sendEmailVerification (clj->js action-code-settings)) + (.then on-success) + (.catch #(on-error (-> % js->clj .-message))))) + +(defn apply-action-code + [{:keys [action-code on-success on-error]}] + (-> (getAuth) + (applyActionCode action-code) + (.then on-success) + (.catch #(on-error (-> % js->clj .-message))))) diff --git a/src/com/degel/re_frame_firebase/core.cljs b/src/com/degel/re_frame_firebase/core.cljs index 07167c6..6d2a50e 100644 --- a/src/com/degel/re_frame_firebase/core.cljs +++ b/src/com/degel/re_frame_firebase/core.cljs @@ -4,7 +4,7 @@ (ns com.degel.re-frame-firebase.core (:require [iron.re-utils :refer [evt event->fn sub->fn]] - [firebase.app :as firebase-app])) + ["@firebase/app" :refer (initializeApp)])) ;;; Used mostly to register client handlers (defonce firebase-state (atom {})) @@ -16,7 +16,8 @@ :default-error-handler (event->fn (or default-error-handler js/alert)))) (defn initialize-app [firebase-app-info] - (js/firebase.initializeApp (clj->js firebase-app-info))) + (swap! firebase-state assoc + :app (initializeApp (clj->js firebase-app-info)))) ;;; [TODO] Consider adding a default atom to hold the user state when :get-user-fn and ;;; and :set-user-fn are not defined. Need to do this carefully, so as not to cause any diff --git a/src/com/degel/re_frame_firebase/database.cljs b/src/com/degel/re_frame_firebase/database.cljs index 2208695..8c2c547 100644 --- a/src/com/degel/re_frame_firebase/database.cljs +++ b/src/com/degel/re_frame_firebase/database.cljs @@ -10,8 +10,6 @@ [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])) diff --git a/src/com/degel/re_frame_firebase/firestore.cljs b/src/com/degel/re_frame_firebase/firestore.cljs index c73ddee..998cc03 100644 --- a/src/com/degel/re_frame_firebase/firestore.cljs +++ b/src/com/degel/re_frame_firebase/firestore.cljs @@ -6,16 +6,17 @@ [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.firestore :as firebase-firestore] [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]])) + [com.degel.re-frame-firebase.helpers :refer [promise-wrapper]] + ["@firebase/firestore" :refer (initializeFirestore DocumentReference doc getDoc collection CollectionReference getDocs onSnapshot setDoc FieldPath)])) (defn set-firestore-settings [settings] - (.settings (js/firebase.firestore) (clj->js (or settings {})))) + (swap! core/firebase-state assoc + :firestore (initializeFirestore (:app @core/firebase-state) + (clj->js (or settings {}))))) ;; Extra public functions (defn server-timestamp @@ -51,7 +52,7 @@ {:firestore/get {:path-collection [:my-collection] :where [[(document-id-field-path) :>= \"start\"]]}}" [] - (.documentId firebase.firestore.FieldPath)) + (.documentId js/firebase.firestore.FieldPath)) ;; Type Conversion/Parsing @@ -61,9 +62,9 @@ See https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference" [path] {:pre [(utils/validate ::specs/path-collection path)]} - (if (instance? js/firebase.firestore.CollectionReference path) + (if (instance? CollectionReference path) path - (.collection (js/firebase.firestore) + (collection (:firestore @core/firebase-state) (str/join "/" (clj->js path))))) (defn clj->DocumentReference @@ -72,10 +73,10 @@ See https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference" [path] {:pre [(utils/validate ::specs/path-document path)]} - (if (instance? js/firebase.firestore.DocumentReference path) + (if (instance? DocumentReference path) path - (.doc (js/firebase.firestore) - (str/join "/" (clj->js path))))) + (doc (:firestore @core/firebase-state) + (str/join "/" (clj->js path))))) (defn clj->FieldPath "Converts a string/keyword or a seq of string/keywords into a FieldPath. @@ -86,9 +87,9 @@ [field-path] (cond (nil? field-path) nil - (instance? js/firebase.firestore.FieldPath field-path) field-path + (instance? FieldPath field-path) field-path (coll? field-path) (apply js/firebase.firestore.FieldPath. (clj->js field-path)) - :else (js/firebase.firestore.FieldPath. (clj->js field-path)))) + :else (FieldPath. (clj->js field-path)))) (defn clj->SetOptions "Converts a clojure-style map into a SetOptions satisfying one. @@ -234,11 +235,11 @@ ;; re-frame Effects/Subscriptions (defn- setter ([path data set-options] - (.set (clj->DocumentReference path) + (setDoc (clj->DocumentReference path) (clj->js data) (clj->SetOptions set-options))) ([instance path data set-options] - (.set instance + (setDoc instance (clj->DocumentReference path) (clj->js data) (clj->SetOptions set-options)))) @@ -280,7 +281,7 @@ (defn- query [ref where order-by limit start-at start-after end-at end-before] (as-> ref $ - (if where + #_(if where (reduce (fn [$$ [field-path op value]] (.where $$ (clj->FieldPath field-path) (clj->js op) (clj->js value))) $ where) @@ -297,11 +298,11 @@ (if end-before (.apply (.-endBefore $) $ (clj->js end-before)) $))) (defn- getter-document [path get-options] - (.get (clj->DocumentReference path) (clj->GetOptions get-options))) + (getDoc (clj->DocumentReference path) (clj->GetOptions get-options))) (defn- getter-collection [path get-options where order-by limit start-at start-after end-at end-before] - (.get (query (clj->CollectionReference path) where order-by limit + (getDocs (query (clj->CollectionReference path) where order-by limit start-at start-after end-at end-before) (clj->GetOptions get-options))) @@ -321,31 +322,34 @@ doc-changes expose-objects) on-failure))) -(defn- on-snapshotter [reference-or-query snapshot-listen-options on-next on-error] - (.onSnapshot reference-or-query - (clj->SnapshotListenOptions snapshot-listen-options) - on-next - (if on-error (event->fn on-error) (core/default-error-handler)))) +(defn- on-snapshotter [reference-or-query snapshot-listen-options on-next on-error register-listener] + (-> (onSnapshot reference-or-query + (clj->SnapshotListenOptions snapshot-listen-options) + on-next + (if on-error (event->fn on-error) (core/default-error-handler))) + register-listener)) (defn on-snapshot [{:keys [path-document path-collection where order-by limit start-at start-after end-at end-before doc-changes snapshot-listen-options snapshot-options expose-objects - on-next on-error]}] + on-next on-error register-listener]}] {:pre [(utils/validate :re-frame/vec-or-fn on-next) (utils/validate (s/nilable :re-frame/vec-or-fn) on-error)]} (if path-document (on-snapshotter (clj->DocumentReference path-document) snapshot-listen-options (document-parser-wrapper on-next snapshot-options expose-objects) - on-error) + on-error + register-listener) (on-snapshotter (query (clj->CollectionReference path-collection) where order-by limit start-at start-after end-at end-before) snapshot-listen-options (collection-parser-wrapper on-next snapshot-options snapshot-listen-options doc-changes expose-objects) - on-error))) + on-error + register-listener))) (def on-snapshot-effect on-snapshot) diff --git a/src/com/degel/re_frame_firebase/functions.cljs b/src/com/degel/re_frame_firebase/functions.cljs new file mode 100644 index 0000000..a9b8d5c --- /dev/null +++ b/src/com/degel/re_frame_firebase/functions.cljs @@ -0,0 +1,19 @@ +(ns com.degel.re-frame-firebase.functions + (:require + [clojure.walk :as w] + [com.degel.re-frame-firebase.core :as core] + ["@firebase/functions" :refer (httpsCallable getFunctions)])) + +(defn call-effect [options] + (let [{:keys [cfn-name data on-success on-error]} options + cfn (-> @core/firebase-state + :app + getFunctions + (httpsCallable cfn-name))] + (.catch + (.then + (cfn (clj->js data)) + #(on-success (-> (.. % -data) + js->clj + w/keywordize-keys))) + #(on-error %)))) diff --git a/src/com/degel/re_frame_firebase/helpers.cljs b/src/com/degel/re_frame_firebase/helpers.cljs index de79167..f27b619 100644 --- a/src/com/degel/re_frame_firebase/helpers.cljs +++ b/src/com/degel/re_frame_firebase/helpers.cljs @@ -14,7 +14,7 @@ ;;; with them. -(defn js->clj-tree [x] +(defn js->clj-tree [^js x] (-> (.val x) js->clj clojure.walk/keywordize-keys)) 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..d5245b7 --- /dev/null +++ b/src/com/degel/re_frame_firebase/storage.cljs @@ -0,0 +1,58 @@ +(ns com.degel.re-frame-firebase.storage + (:require + ["@firebase/storage" :refer (getStorage uploadBytesResumable ref deleteObject getDownloadURL)] + [com.degel.re-frame-firebase.core :as core] + [com.degel.re-frame-firebase.helpers :refer [promise-wrapper]])) + +(defn- get-storage [bucket] + (if bucket + (-> @core/firebase-state + :app + (getStorage (str "gs://" bucket))) + (-> @core/firebase-state + :app + getStorage))) ;default firebase bucket + +(defn- put [path file metadata on-success on-error on-progress bucket] + (let [upload-task (uploadBytesResumable (ref (get-storage bucket) path) + file metadata)] + (.on + upload-task + "state_changed" + #(if on-progress + (on-progress (* (/ (.-bytesTransferred %) (.-totalBytes %)) 100)) + (fn [])) + #(if on-error + (on-error %) + (fn [])) + #(if on-success + (on-success) + (fn []))))) + +(defn- delete [path on-success on-error bucket] + (promise-wrapper (deleteObject (ref (get-storage bucket) path)) + on-success + on-error)) + +(defn download-url-effect [{:keys [path on-success on-error bucket]}] + (promise-wrapper (getDownloadURL (ref (get-storage bucket) path)) + on-success + #(on-error (-> % js->clj .-message)))) + +(defn put-effect [items _] + (doseq [item items] + (let [{:keys [path file metadata on-success on-error on-progress bucket]} item] + (put path file + (clj->js metadata) + on-success + #(on-error (-> % js->clj .-message)) + on-progress + bucket)))) + +(defn delete-effect [items _] + (doseq [item items] + (let [{:keys [path on-success on-error bucket]} item] + (delete path + on-success + #(on-error (-> % js->clj .-message)) + bucket))))