UNPKG

@taraai/read-write

Version:

Synchronous NoSQL/Firestore for React

660 lines (622 loc) 26.4 kB
import { isObject } from 'lodash' import { merge } from 'lodash/fp' import { getEventsFromInput, createCallable } from './utils' import { mapWithFirebaseAndDispatch } from './utils/actions' import * as authActions from './actions/auth' import * as queryActions from './actions/query' import * as storageActions from './actions/storage' let firebaseInstance /** * Create an extended firebase instance that has methods attached * which dispatch redux actions. * @param {object} firebase - Firebase instance which to extend * @param {object} configs - Configuration object * @param {Function} dispatch - Action dispatch function * @returns {object} Extended Firebase instance */ export default function createFirebaseInstance(firebase, configs, dispatch) { /* istanbul ignore next: Logging is external */ // Enable Logging based on config (handling instances without i.e RNFirebase) // NOTE: This will be removed in a future version if ( configs && configs.enableLogging && firebase.database && typeof firebase.database.enableLogging === 'function' ) { /* eslint-disable no-console */ console.warn( 'The enableLogging config option is disabled and will be removed in a future version of react-redux-firebase. Enable logging as part of instance initialization.' ) /* eslint-enable no-console */ firebase.database.enableLogging(configs.enableLogging) } // Add internal variables to firebase instance const defaultInternals = { watchers: {}, listeners: {}, callbacks: {}, queries: {}, config: configs, authUid: null } firebase._ = merge(defaultInternals, firebase._) // eslint-disable-line no-param-reassign /** * @private * @param {string} method - Method to run with meta attached * @param {string} path - Path to location on Firebase which to set * @param {object|string|boolean|number} value - Value to write to Firebase * @param {Function} onComplete - Function to run on complete * @returns {Promise} Containing reference snapshot */ const withMeta = (method, path, value, onComplete) => { if (isObject(value)) { const prefix = method === 'update' ? 'updated' : 'created' const dataWithMeta = { ...value, [`${prefix}At`]: firebase.database.ServerValue.TIMESTAMP } if (firebase.auth().currentUser) { dataWithMeta[`${prefix}By`] = firebase.auth().currentUser.uid } return firebase.database().ref(path)[method](dataWithMeta, onComplete) } return firebase.database().ref(path)[method](value, onComplete) } /** * Sets data to Firebase. * @param {string} path - Path to location on Firebase which to set * @param {object|string|boolean|number} value - Value to write to Firebase * @param {Function} onComplete - Function to run on complete (`not required`) * @returns {Promise} Containing reference snapshot * @example <caption>Basic</caption> * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#set * import React, { Component } from 'react' * import PropTypes from 'prop-types' * import { firebaseConnect } from 'react-redux-firebase' * function Example({ firebase: { set } }) { * return ( * <button onClick={() => set('some/path', { here: 'is a value' })}> * Set To Firebase * </button> * ) * } * export default firebaseConnect()(Example) */ const set = (path, value, onComplete) => firebase.database().ref(path).set(value, onComplete) /** * Sets data to Firebase along with meta data. Currently, * this includes createdAt and createdBy. *Warning* using this function * may have unintented consequences (setting createdAt even if data already * exists). * @param {string} path - Path to location on Firebase which to set * @param {object|string|boolean|number} value - Value to write to Firebase * @param {Function} onComplete - Function to run on complete (`not required`) * @returns {Promise} Containing reference snapshot * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#update */ const setWithMeta = (path, value, onComplete) => withMeta('set', path, value, onComplete) /** * Pushes data to Firebase. * @param {string} path - Path to location on Firebase which to push * @param {object|string|boolean|number} value - Value to push to Firebase * @param {Function} onComplete - Function to run on complete (`not required`) * @returns {Promise} Containing reference snapshot * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#push * @example <caption>Basic</caption> * import React from 'react' * import PropTypes from 'prop-types' * import { firebaseConnect } from 'react-redux-firebase' * * function Example({ firebase: { push } }) { * return ( * <button onClick={() => push('some/path', true)}> * Push To Firebase * </button> * ) * } * export default firebaseConnect()(Example) */ const push = (path, value, onComplete) => firebase.database().ref(path).push(value, onComplete) /** * Pushes data to Firebase along with meta data. Currently, * this includes createdAt and createdBy. * @param {string} path - Path to location on Firebase which to set * @param {object|string|boolean|number} value - Value to write to Firebase * @param {Function} onComplete - Function to run on complete (`not required`) * @returns {Promise} Containing reference snapshot * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#pushwithmeta */ const pushWithMeta = (path, value, onComplete) => withMeta('push', path, value, onComplete) /** * Updates data on Firebase and sends new data. More info * available in [the docs](https://react-redux-firebase.com/docs/api/firebaseInstance.html#update). * @param {string} path - Path to location on Firebase which to update * @param {object|string|boolean|number} value - Value to update to Firebase * @param {Function} onComplete - Function to run on complete (`not required`) * @returns {Promise} Containing reference snapshot * @example <caption>Basic</caption> * import React from 'react' * import PropTypes from 'prop-types' * import { firebaseConnect } from 'react-redux-firebase' * * function Example({ firebase: { update } }) { * function updateData() { * update('some/path', { here: 'is a value' }) * } * } * return ( * <button onClick={updateData}> * Update To Firebase * </button> * ) * } * export default firebaseConnect()(Example) */ const update = (path, value, onComplete) => firebase.database().ref(path).update(value, onComplete) /** * Updates data on Firebase along with meta. *Warning* * using this function may have unintented consequences (setting * createdAt even if data already exists). * @param {string} path - Path to location on Firebase which to update * @param {object|string|boolean|number} value - Value to update to Firebase * @param {Function} onComplete - Function to run on complete (`not required`) * @returns {Promise} Containing reference snapshot * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#updatewithmeta */ const updateWithMeta = (path, value, onComplete) => withMeta('update', path, value, onComplete) /** * Removes data from Firebase at a given path. **NOTE** A * seperate action is not dispatched unless `dispatchRemoveAction: true` is * provided to config on store creation. That means that a listener must * be attached in order for state to be updated when calling remove. * @param {string} path - Path to location on Firebase which to remove * @param {Function} onComplete - Function to run on complete (`not required`) * @param {Function} options - Options object * @returns {Promise} Containing reference snapshot * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#remove * @example <caption>Basic</caption> * import React from 'react' * import PropTypes from 'prop-types' * import { firebaseConnect } from 'react-redux-firebase' * * function Example({ firebase: { remove } }) { * return ( * <button onClick={() => remove('some/path')}> * Remove From Firebase * </button> * ) * } * export default firebaseConnect()(Example) */ const remove = (path, onComplete, options) => queryActions.remove(firebase, dispatch, path, options).then(() => { if (typeof onComplete === 'function') onComplete() return path }) /** * Sets data to Firebase only if the path does not already * exist, otherwise it rejects. Internally uses a Firebase transaction to * prevent a race condition between seperate clients calling uniqueSet. * @param {string} path - Path to location on Firebase which to set * @param {object|string|boolean|number} value - Value to write to Firebase * @param {Function} onComplete - Function to run on complete (`not required`) * @returns {Promise} Containing reference snapshot * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#uniqueset * @example <caption>Basic</caption> * import React, { Component } from 'react' * import PropTypes from 'prop-types' * import { firebaseConnect } from 'react-redux-firebase' * * function Example({ firebase: { uniqueSet } }) { * return ( * <button onClick={() => uniqueSet('some/unique/path', true)}> * Unique Set To Firebase * </button> * ) * } * export default firebaseConnect()(Example) */ const uniqueSet = (path, value, onComplete) => firebase .database() .ref(path) .transaction((d) => (d === null ? value : undefined)) .then(({ committed, snapshot }) => { if (!committed) { const newError = new Error('Path already exists.') if (onComplete) onComplete(newError) return Promise.reject(newError) } if (onComplete) onComplete(snapshot) return snapshot }) /** * Upload a file to Firebase Storage with the option to store * its metadata in Firebase Database. More info available * in [the docs](). * @param {string} path - Path to location on Firebase which to set * @param {File} file - File object to upload (usually first element from * array output of select-file or a drag/drop `onDrop`) * @param {string} dbPath - Database path to place uploaded file metadata * @param {object} options - Options * @param {string} options.name - Name of the file * @param {object} options.metdata - Metadata for the file (passed as second * argument to storage.put calls) * @returns {Promise} Containing the File object * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#uploadfile */ const uploadFile = (path, file, dbPath, options) => storageActions.uploadFile(dispatch, firebase, { path, file, dbPath, options }) /** * Upload multiple files to Firebase Storage with the option * to store their metadata in Firebase Database. * @param {string} path - Path to location on Firebase which to set * @param {Array} files - Array of File objects to upload (usually from * a select-file or a drag/drop `onDrop`) * @param {string} dbPath - Database path to place uploaded files metadata. * @param {object} options - Options * @param {string} options.name - Name of the file * @returns {Promise} Containing an array of File objects * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#uploadfiles */ const uploadFiles = (path, files, dbPath, options) => storageActions.uploadFiles(dispatch, firebase, { path, files, dbPath, options }) /** * Delete a file from Firebase Storage with the option to * remove its metadata in Firebase Database. * @param {string} path - Path to location on Firebase which to set * @param {string} dbPath - Database path to place uploaded file metadata * @returns {Promise} Containing the File object * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#deletefile */ const deleteFile = (path, dbPath) => storageActions.deleteFile(dispatch, firebase, { path, dbPath }) /** * Watch event. **Note:** this method is used internally * so examples have not yet been created, and it may not work as expected. * @param {string} type - Type of watch event * @param {string} path - Path to location on Firebase which to set listener * @param {string} storeAs - Name of listener results within redux store * @param {object} options - Event options object * @param {Array} options.queryParams - List of parameters for the query * @param {string} options.queryId - id of the query * @returns {Promise|void} Results of calling watch event * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#watchevent */ const watchEvent = (type, path, storeAs, options = {}) => queryActions.watchEvent(firebase, dispatch, { type, path, storeAs, ...options }) /** * Unset a listener watch event. **Note:** this method is used * internally so examples have not yet been created, and it may not work * as expected. * @param {string} type - Type of watch event * @param {string} path - Path to location on Firebase which to unset listener * @param {string} queryId - Id of the listener * @param {object} options - Event options object * @returns {void} * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#unwatchevent */ const unWatchEvent = (type, path, queryId, options = {}) => queryActions.unWatchEvent(firebase, dispatch, { type, path, queryId, ...options }) /** * Similar to the firebaseConnect Higher Order Component but * presented as a function (not a React Component). Useful for populating * your redux state without React, e.g., for server side rendering. Only * `once` type should be used as other query types such as `value` do not * return a Promise. * @param {Array} watchArray - Array of objects or strings for paths to sync * from Firebase. Can also be a function that returns the array. The function * is passed the props object specified as the next parameter. * @param {object} options - The options object that you would like to pass to * your watchArray generating function. * @returns {Promise} Resolves with an array of watchEvent results */ const promiseEvents = (watchArray, options) => { const inputAsFunc = createCallable(watchArray) const prevData = inputAsFunc(options, firebase) const queryConfigs = getEventsFromInput(prevData) // TODO: Handle calling with non promise queries (must be once or first_child) return Promise.all( queryConfigs.map((queryConfig) => queryActions.watchEvent(firebase, dispatch, queryConfig) ) ) } /** * Logs user into Firebase. For examples, visit the * [auth section of the docs](https://react-redux-firebase.com/docs/auth.html) or the * [auth recipes section](https://react-redux-firebase.com/docs/recipes/auth.html). * @param {object} credentials - Credentials for authenticating * @param {string} credentials.provider - External provider (google | * facebook | twitter) * @param {string} credentials.type - Type of external authentication * (popup | redirect) (only used with provider) * @param {string} credentials.email - Credentials for authenticating * @param {string} credentials.password - Credentials for authenticating (only used with email) * @param {string} credentials.emailLink - emailLink for authenticating (only used with passwordless sign-in) * @returns {Promise} Containing user's auth data * @see https://react-redux-firebase.com/docs/auth.html#logincredentials * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#login */ const login = (credentials) => authActions.login(dispatch, firebase, credentials) /** * Reauthenticate user into Firebase. For examples, visit the * [auth section of the docs](https://react-redux-firebase.com/docs/auth.html) or the * [auth recipes section](https://react-redux-firebase.com/docs/recipes/auth.html). * @param {object} credentials - Credentials for authenticating * @param {string} credentials.provider - External provider (google | * facebook | twitter) * @param {string} credentials.type - Type of external authentication * (popup | redirect) (only used with provider) * @returns {Promise} Containing user's auth data * @see https://react-redux-firebase.com/docs/auth.html#logincredentials * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#login */ const reauthenticate = (credentials) => authActions.reauthenticate(dispatch, firebase, credentials) /** * Logs user into Firebase using external. For examples, visit the * [auth section](/docs/recipes/auth.md) * @param {object} authData - Auth data from Firebase's getRedirectResult * @returns {Promise} Containing user's profile */ const handleRedirectResult = (authData) => authActions.handleRedirectResult(dispatch, firebase, authData) /** * Logs user out of Firebase and empties firebase state from * redux store * @returns {Promise} Resolves after logout is complete * @see https://react-redux-firebase.com/docs/auth.html#logout */ const logout = () => authActions.logout(dispatch, firebase) /** * Creates a new user in Firebase authentication. If * `userProfile` config option is set, user profiles will be set to this * location. * @param {object} credentials - Credentials for authenticating * @param {string} credentials.email - Credentials for authenticating * @param {string} credentials.password - Credentials for authenticating (only used with email) * @param {object} profile - Data to include within new user profile * @returns {Promise} Containing user's auth data * @see https://react-redux-firebase.com/docs/auth.html#createuser */ const createUser = (credentials, profile) => authActions.createUser(dispatch, firebase, credentials, profile) /** * Sends password reset email * @param {string} email - Email to send recovery email to * @returns {Promise} Resolves after password reset email is sent * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#resetpassword */ const resetPassword = (email) => authActions.resetPassword(dispatch, firebase, email) /** * Confirm that a user's password has been reset * @param {string} code - Password reset code to verify * @param {string} password - New Password to confirm reset to * @returns {Promise} Resolves after password reset is confirmed * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#confirmpasswordreset */ const confirmPasswordReset = (code, password) => authActions.confirmPasswordReset(dispatch, firebase, code, password) /** * Verify that a password reset code from a password reset * email is valid * @param {string} code - Password reset code to verify * @returns {Promise} Containing user auth info * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#verifypasswordreset */ const verifyPasswordResetCode = (code) => authActions.verifyPasswordResetCode(dispatch, firebase, code) /** * Apply verification code * @param {string} code - Verification code * @returns {Promise} Resolves on success * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#applyactioncode */ const applyActionCode = (code) => authActions.applyActionCode(dispatch, firebase, code) /** * Update user profile on Firebase Real Time Database or * Firestore (if `useFirestoreForProfile: true` config included). * Real Time Database update uses `update` method internally while * updating profile on Firestore uses `set`. * @param {object} profileUpdate - Profile data to place in new profile * @param {object} options - Options object (used to change how profile * update occurs) * @param {boolean} [options.useSet=true] - Use set with merge instead of * update. Setting to `false` uses update (can cause issue of profile document * does not exist). Note: Only used when updating profile on Firestore * @param {boolean} [options.merge=true] - Whether or not to use merge when * setting profile. Note: Only used when updating profile on Firestore * @returns {Promise} Returns after updating profile within database * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#updateprofile */ const updateProfile = (profileUpdate, options) => authActions.updateProfile(dispatch, firebase, profileUpdate, options) /** * Update Auth profile object * @param {object} authUpdate - Update to be auth object * @param {boolean} updateInProfile - Update in profile * @returns {Promise} Returns after updating auth profile * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#updateauth */ const updateAuth = (authUpdate, updateInProfile) => authActions.updateAuth(dispatch, firebase, authUpdate, updateInProfile) /** * Update user's email * @param {string} newEmail - Update to be auth object * @param {boolean} updateInProfile - Update in profile * @returns {Promise} Resolves after email is updated in user's auth * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#updateemail */ const updateEmail = (newEmail, updateInProfile) => authActions.updateEmail(dispatch, firebase, newEmail, updateInProfile) /** * Reload user's auth object. Must be authenticated. * @returns {Promise} Resolves after reloading firebase auth * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#reloadauth */ const reloadAuth = () => authActions.reloadAuth(dispatch, firebase) /** * Links the user account with the given credentials. * @param {firebase.auth.AuthCredential} credential - The auth credential * @returns {Promise} Resolves after linking auth with a credential * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#linkwithcredential */ const linkWithCredential = (credential) => authActions.linkWithCredential(dispatch, firebase, credential) /** * @name signInWithPhoneNumber * Asynchronously signs in using a phone number. This method * sends a code via SMS to the given phone number, and returns a modified * firebase.auth.ConfirmationResult. The `confirm` method * authenticates and does profile handling. * @param {firebase.auth.ConfirmationResult} credential - The auth credential * @returns {Promise} * @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#signinwithphonenumber */ /** * @name initializeAuth * Initialize auth to work with build in profile support */ const actionCreators = mapWithFirebaseAndDispatch( firebase, dispatch, // Actions with arg order (firebase, dispatch) { signInWithPhoneNumber: authActions.signInWithPhoneNumber }, // Actions with arg order (dispatch, firebase) { initializeAuth: authActions.init } ) /** * @name ref * @description Firebase ref function * @returns {firebase.database.Reference} */ /** * @name database * @description Firebase database service instance including all Firebase storage methods * @returns {firebase.database.Database} Firebase database service */ /** * @name storage * @description Firebase storage service instance including all Firebase storage methods * @returns {firebase.database.Storage} Firebase storage service */ /** * @name auth * @description Firebase auth service instance including all Firebase auth methods * @returns {firebase.database.Auth} */ firebaseInstance = Object.assign(firebase, { _reactReduxFirebaseExtended: true, ref: (path) => firebase.database().ref(path), set, setWithMeta, uniqueSet, push, pushWithMeta, remove, update, updateWithMeta, login, reauthenticate, handleRedirectResult, logout, updateAuth, updateEmail, updateProfile, uploadFile, uploadFiles, deleteFile, createUser, resetPassword, confirmPasswordReset, verifyPasswordResetCode, applyActionCode, watchEvent, unWatchEvent, reloadAuth, linkWithCredential, promiseEvents, dispatch, ...actionCreators }) return firebaseInstance } /** * Get internal Firebase instance with methods which are wrapped with action dispatches. Useful for * integrations into external libraries such as redux-thunk and redux-observable. * @returns {object} Firebase instance with methods which dispatch redux actions * @see http://react-redux-firebase.com/api/getFirebase.html * @example <caption>redux-thunk integration</caption> * import { applyMiddleware, compose, createStore } from 'redux'; * import thunk from 'redux-thunk'; * import { getFirebase } from 'react-redux-firebase'; * import makeRootReducer from './reducers'; * * const fbConfig = {} // your firebase config * * const store = createStore( * makeRootReducer(), * initialState, * compose( * applyMiddleware([ * // Pass getFirebase function as extra argument * thunk.withExtraArgument(getFirebase) * ]) * ) * ); * // then later * export function addTodo(newTodo) { * return (dispatch, getState, getFirebase) => { * const firebase = getFirebase() * firebase * .push('todos', newTodo) * .then(() => { * dispatch({ type: 'SOME_ACTION' }) * }) * } * } */ export function getFirebase() { /* istanbul ignore next: Firebase instance always exists during tests */ if (!firebaseInstance) { throw new Error( 'Firebase instance does not yet exist. Check your compose function.' ) // eslint-disable-line no-console } return firebaseInstance }