@taraai/read-write
Version:
Synchronous NoSQL/Firestore for React
275 lines (259 loc) • 8.83 kB
JavaScript
import { actionTypes } from '../constants'
import { promisesForPopulate } from './populate'
import { isNaN, forEach } from 'lodash'
import { isString } from './index'
/**
* @private
* @param {string|number} value - Item to attempt to parse to a number
* @returns {any} Number if parse to number was successful, otherwise,
* original value
*/
function tryParseToNumber(value) {
const result = Number(value)
if (isNaN(result)) {
return value
}
return result
}
/**
* @private
* @param {string} event - Type of event to watch for
* @param {string} path - Path to watch with watcher
* @returns {string} watchPath
*/
export function getWatchPath(event, path) {
if (!event || event === '' || !path) {
throw new Error('Event and path are required')
}
return `${event}:${path.substring(0, 1) === '/' ? '' : '/'}${path}`
}
/**
* @private
* @param {string} path - Path from which to get query id
* @param {string} event - Type of query event
* @returns {string} Query id
*/
export function getQueryIdFromPath(path, event) {
if (!isString(path)) {
throw new Error('Query path must be a string')
}
const origPath = path
const pathSplitted = path.split('#')
path = pathSplitted[0]
const isQuery = pathSplitted.length > 1
const queryParams = isQuery ? pathSplitted[1].split('&') : []
const queryId = isQuery
? queryParams
.map((param) => {
const splittedParam = param.split('=')
// Handle query id in path
if (splittedParam[0] === 'queryId') {
return splittedParam[1]
}
})
.filter((q) => q)
: undefined
return queryId && queryId.length > 0
? event
? `${event}:/${queryId}`
: queryId[0]
: isQuery
? origPath
: undefined
}
/**
* @private
* @param {object} firebase - Internal firebase object
* @param {Function} dispatch - Redux dispatch function
* @param {string} event - Type of event to watch for
* @param {string} path - Path to watch with watcher
* @param {string} queryId - Id of query
* @returns {number} watcherCount - count
*/
export function setWatcher(firebase, dispatch, event, path, queryId) {
const id =
queryId || getQueryIdFromPath(path, event) || getWatchPath(event, path)
if (firebase._.watchers[id]) {
firebase._.watchers[id]++
} else {
firebase._.watchers[id] = 1
}
dispatch({ type: actionTypes.SET_LISTENER, path, payload: { id } })
return firebase._.watchers[id]
}
/**
* @private
* @param {object} firebase - Internal firebase object
* @param {string} event - Type of event to watch for
* @param {string} path - Path to watch with watcher
* @param {string} queryId - Id of query
* @returns {number} watcherCount
*/
export function getWatcherCount(firebase, event, path, queryId) {
const id =
queryId || getQueryIdFromPath(path, event) || getWatchPath(event, path)
return firebase._.watchers[id]
}
/**
* @private
* @param {object} firebase - Internal firebase object
* @param {Function} dispatch - Redux's dispatch function
* @param {string} event - Type of event to watch for
* @param {string} path - Path to watch with watcher
* @param {string} queryId - Id of query
*/
export function unsetWatcher(firebase, dispatch, event, path, queryId) {
const id =
queryId || getQueryIdFromPath(path, event) || getWatchPath(event, path)
path = path.split('#')[0]
const { watchers } = firebase._
if (watchers[id] <= 1) {
delete watchers[id]
if (event !== 'first_child' && event !== 'once') {
firebase.database().ref().child(path).off(event)
}
} else if (watchers[id]) {
watchers[id]--
}
dispatch({ type: actionTypes.UNSET_LISTENER, path, payload: { id } })
}
/**
* Modify query to include methods based on query parameters (such
* as orderByChild).
* @param {Array} queryParams - Array of query parameters to apply to query
* @param {object} query - Query object on which to apply query parameters
* @returns {firebase.database.Query} Query with query params applied
*/
export function applyParamsToQuery(queryParams, query) {
let doNotParse = false
if (queryParams) {
queryParams.forEach((param) => {
param = param.split('=')
switch (param[0]) {
case 'orderByValue':
query = query.orderByValue()
doNotParse = true
break
case 'orderByPriority':
query = query.orderByPriority()
doNotParse = true
break
case 'orderByKey':
query = query.orderByKey()
doNotParse = true
break
case 'orderByChild':
query = query.orderByChild(param[1])
break
case 'limitToFirst':
// TODO: Handle number not being passed as param
query = query.limitToFirst(parseInt(param[1], 10))
break
case 'limitToLast':
// TODO: Handle number not being passed as param
query = query.limitToLast(parseInt(param[1], 10))
break
case 'notParsed':
// support disabling internal number parsing (number strings)
doNotParse = true
break
case 'parsed':
// support disabling internal number parsing (number strings)
doNotParse = false
break
case 'equalTo':
let equalToParam = !doNotParse ? tryParseToNumber(param[1]) : param[1] // eslint-disable-line no-case-declarations
equalToParam = equalToParam === 'null' ? null : equalToParam
equalToParam = equalToParam === 'false' ? false : equalToParam
equalToParam = equalToParam === 'true' ? true : equalToParam
query =
param.length === 3
? query.equalTo(equalToParam, param[2])
: query.equalTo(equalToParam)
break
case 'startAt':
let startAtParam = !doNotParse ? tryParseToNumber(param[1]) : param[1] // eslint-disable-line no-case-declarations
startAtParam = startAtParam === 'null' ? null : startAtParam
query =
param.length === 3
? query.startAt(startAtParam, param[2])
: query.startAt(startAtParam)
break
case 'endAt':
let endAtParam = !doNotParse ? tryParseToNumber(param[1]) : param[1] // eslint-disable-line no-case-declarations
endAtParam = endAtParam === 'null' ? null : endAtParam
query =
param.length === 3
? query.endAt(endAtParam, param[2])
: query.endAt(endAtParam)
break
}
})
}
return query
}
/**
* Get ordered array from snapshot
* @param {firebase.database.DataSnapshot} snap - Data for which to create
* an ordered array.
* @returns {Array|null} Ordered list of children from snapshot or null
*/
export function orderedFromSnapshot(snap) {
if (snap.hasChildren && !snap.hasChildren()) {
return null
}
const ordered = []
if (snap.forEach) {
snap.forEach((child) => {
ordered.push({ key: child.key, value: child.val() })
})
}
return ordered.length ? ordered : null
}
/**
* Get data associated with populate settings, and dispatch
*
* @param {object} firebase - Internal firebase object
* @param {Function} dispatch - Redux's dispatch function
* @param {object} config - Config object
* @param {any} config.data - Original query data result
* @param {Array} config.populates - List of populate settings
* @param {string} config.path - Base query path
* @param {string} config.storeAs - Location within redux in which to
* query results will be stored (path is used as default if not provided).
* @returns {Promise} Promise that resolves after data for populates has been
* loaded and associated actions have been dispatched
* @private
*/
export function populateAndDispatch(firebase, dispatch, config) {
const { data, populates, snapshot, path, storeAs } = config
// TODO: Allow setting of unpopulated data before starting population through config
return promisesForPopulate(firebase, snapshot.key, data, populates)
.then((results) => {
// dispatch child sets first so isLoaded is only set to true for
// populatedDataToJS after all data is in redux (Issue #121)
// TODO: Allow config to toggle Combining into one SET action
// TODO: Set ordered for populate queries
forEach(results, (result, path) => {
dispatch({
type: actionTypes.MERGE,
path,
data: result
})
})
dispatch({
type: actionTypes.SET,
path: storeAs || path,
data,
ordered: orderedFromSnapshot(snapshot)
})
return results
})
.catch((err) => {
dispatch({
type: actionTypes.ERROR,
payload: err
})
return Promise.reject(err)
})
}