All files / src model.js

82.43% Statements 61/74
81.58% Branches 31/38
85.71% Functions 30/35
82.35% Lines 56/68
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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203                          31x           96x         60x       62x             58x     58x     58x     58x 16x   42x 6x   36x           22x               11x       11x 11x                 4x               2x       2x 2x               26x       12x       26x               6x       6x 6x       6x   6x       6x 6x             54x       24x 59x 59x         22x 56x                 12x 1x     11x 11x       12x   12x 1x     11x 11x         96x 34x   62x 4x   58x 22x   36x 12x   24x 24x             2x    
import { Observable } from 'rxjs/Observable'
 
import 'rxjs/add/observable/of'
import 'rxjs/add/observable/forkJoin'
import 'rxjs/add/observable/combineLatest'
import 'rxjs/add/operator/map'
 
// Other imports
import isPlainObject from 'is-plain-object'
 
// Unlike normal queries' .watch(), we don't support rawChanges: true
// for aggregates
function checkWatchArgs(args) {
  Iif (args.length > 0) {
    throw new Error(".watch() on aggregates doesn't support arguments!")
  }
}
 
function isTerm(term) {
  return typeof term.fetch === 'function' &&
         typeof term.watch === 'function'
}
 
function isPromise(term) {
  return typeof term.then === 'function'
}
 
function isObservable(term) {
  return typeof term.subscribe === 'function' &&
         typeof term.lift === 'function'
}
 
// Whether an object is primitive. We consider functions
// non-primitives, lump Dates and ArrayBuffers into primitives.
function isPrimitive(value) {
  Iif (value === null) {
    return true
  }
  Iif (value === undefined) {
    return false
  }
  Iif (typeof value === 'function') {
    return false
  }
  if ([ 'boolean', 'number', 'string' ].indexOf(typeof value) !== -1) {
    return true
  }
  if (value instanceof Date || value instanceof ArrayBuffer) {
    return true
  }
  return false
}
 
// Simple wrapper for primitives. Just emits the primitive
class PrimitiveTerm {
  constructor(value) {
    this._value = value
  }
 
  toString() {
    return this._value.toString()
  }
 
  fetch() {
    return Observable.of(this._value)
  }
 
  watch(...watchArgs) {
    checkWatchArgs(watchArgs)
    return Observable.of(this._value)
  }
}
 
// Simple wrapper for observables to normalize the
// interface. Everything in an aggregate tree should be one of these
// term-likes
class ObservableTerm {
  constructor(value) {
    this._value = value
  }
 
  toString() {
    return this._value.toString()
  }
 
  fetch() {
    return Observable.from(this._value)
  }
 
  watch(...watchArgs) {
    checkWatchArgs(watchArgs)
    return Observable.from(this._value)
  }
}
 
// Handles aggregate syntax like [ query1, query2 ]
class ArrayTerm {
  constructor(value) {
    // Ensure this._value is an array of Term
    this._value = value.map(x => aggregate(x))
  }
 
  _reducer(...args) {
    return args
  }
 
  _query(operation) {
    return this._value.map(x => x[operation]())
  }
 
  toString() {
    return `[ ${this._query('toString').join(', ')} ]`
  }
 
  fetch() {
    Iif (this._value.length === 0) {
      return Observable.empty()
    }
 
    const qs = this._query('fetch')
    return Observable.forkJoin(...qs, this._reducer)
  }
 
  watch(...watchArgs) {
    checkWatchArgs(watchArgs)
 
    Iif (this._value.length === 0) {
      return Observable.empty()
    }
 
    const qs = this._query('watch')
    return Observable.combineLatest(...qs, this._reducer)
  }
}
 
class AggregateTerm {
  constructor(value) {
    // Ensure this._value is an array of [ key, Term ] pairs
    this._value = Object.keys(value).map(k => [ k, aggregate(value[k]) ])
  }
 
  _reducer(...pairs) {
    return pairs.reduce((prev, [ k, x ]) => {
      prev[k] = x
      return prev
    }, {})
  }
 
  _query(operation) {
    return this._value.map(
      ([ k, term ]) => term[operation]().map(x => [ k, x ]))
  }
 
  toString() {
    const s = this._value.map(([ k, term ]) => `'${k}': ${term}`)
    return `{ ${s.join(', ')} }`
  }
 
  fetch() {
    if (this._value.length === 0) {
      return Observable.of({})
    }
 
    const qs = this._query('fetch')
    return Observable.forkJoin(...qs, this._reducer)
  }
 
  watch(...watchArgs) {
    checkWatchArgs(watchArgs)
 
    if (this._value.length === 0) {
      return Observable.of({})
    }
 
    const qs = this._query('watch')
    return Observable.combineLatest(...qs, this._reducer)
  }
}
 
export function aggregate(spec) {
  if (isTerm(spec)) {
    return spec
  }
  if (isObservable(spec) || isPromise(spec)) {
    return new ObservableTerm(spec)
  }
  if (isPrimitive(spec)) {
    return new PrimitiveTerm(spec)
  }
  if (Array.isArray(spec)) {
    return new ArrayTerm(spec)
  }
  Eif (isPlainObject(spec)) {
    return new AggregateTerm(spec)
  }
 
  throw new Error(`Can't make an aggregate with ${spec} in it`)
}
 
export function model(constructor) {
  return (...args) => aggregate(constructor(...args))
}