All files getChanges.js

96.55% Statements 28/29
96.55% Branches 28/29
100% Functions 4/4
100% Lines 20/20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87            1x 3x         45x     45x       3x           9x         9x         1x       24x 24x       2x         2x   3x 1x       1x       1x           16x       19x 22x 22x                            
// Get a list of actions necesarry to mirror the change between
// two objects as {location: String, value: Any}
// The absense of data doesn't indicate removal, it indicates
// the data is no longer interesting, so we don't act on that.
// For removal, we use a `value` of `null`
 
let emptyObjectError = () => {
  return new Error(`Encountered an empty object, this will translate
    into null on firebase, so use null instead to prevent confusing.
  `)
}
 
let getChanges = (prev, next, path = []) => {
  // Because it's used all over, we just transform
  // the path array into a string directly
  let location = path.join(`/`)
 
  // Undefined somewhere in the tree is always a mistake
  if (next === undefined) {
    throw new Error(`Undefined found at '${location}' in firebase tree!`)
  }
 
  // Set returns a description of the change, in an one-element array,
  // which is then executed by the code in .subscribe
  let set = value => {
    return [{location, value}]
  }
 
  // If the object didn't change... don't update it
  if (prev === next) {
    return []
  }
 
  // If null... set it to null!
  if (next === null) {
    return set(null)
  }
 
  // If it is an object, compare deeper!
  if (typeof next === `object`) {
    let keys = Object.keys(next)
 
    // Empty objects are a mistake, should use null instead
    if (keys.length === 0) {
      throw emptyObjectError()
    }
 
    // It's an object that looks like { $set: value },which will overwrite
    // the value in it, rather than deep merging it!
    if (typeof next.$set !== `undefined`) E{
      // Make sure though, it's the only key:
      if (keys.length !== 1) {
        throw new Error(`Encountered object with key $set, but also others keys at '${location}'!`)
      }
      // Disallow empty objects, even in $set
      if (typeof next.$set === `object` && next.$set !== null && Object.keys(next.$set).length === 0) {
        throw emptyObjectError()
      }
      // If the objects are the same, we don't update
      if (prev && prev.$set === next.$set) {
        return []
      }
      // And return a set operation with the value exact of next.$set
      return set(next.$set)
 
    // If not, just go and update it deeply
    } else {
      // Find changes inside the object, comparing the previous value
      // at this position (whether it is an object or not) to the current
      // value, which is guaranteed an object.
      let changes = keys.map(key => {
        let prevValue = prev[key] !== undefined ? prev[key] : {}
        return getChanges(prevValue, next[key], path.concat([key]))
      })
      // Because getChanges returns an array of actions to take,
      // `changes` will be :: [ [Action] ], so we have to flatten it,
      // And then just return it
      return [].concat(...changes)
    }
  }
 
  // If it is a plain value, set it to that value
  return set(next)
}
 
export default getChanges