Skip to content

Instantly share code, notes, and snippets.

@ndemonner
Created October 23, 2013 16:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ndemonner/7121749 to your computer and use it in GitHub Desktop.
Save ndemonner/7121749 to your computer and use it in GitHub Desktop.
Data service from Concierge.
'use strict';
angular.module('neonConciergeApp')
.service 'Data', ($q, $timeout, $rootScope, $parse) ->
# Usage:
# Data.sync $scope,
# path: "users/#{$routeParams.id}"
# include: ["requests/*/room", "currentStay", "currentRoom/property"]
sync: (scope, args) ->
self = this
rootRef = new Firebase($rootScope.firebase)
deferrable = $q.defer()
path = args.path
scopeKey = args.key or args.path
includePaths = args.include
includePaths = [includePaths] if typeof includePaths is 'string'
thisRef = args.ref or rootRef.child path
# take some value, and correctly apply it to the angular model
apply = (value) ->
applyDefer = $q.defer()
$timeout ->
currentValue = $parse(scopeKey)(scope) or {}
if _(currentValue).isObject()
_(currentValue).extend value
else
currentValue = value
$parse(scopeKey).assign scope, currentValue
applyDefer.resolve()
applyDefer.promise
# take a child key and value, and apply it like above
applyChild = (key, value) ->
applyDefer = $q.defer()
$timeout ->
$parse(key).assign scope, value
applyDefer.resolve()
applyDefer.promise
get = ->
$parse(scopeKey)(scope)
# this is the meat of the Data service. It sets up a variety of special methods
# on the scope model object, and correctly retrieves the current remote value
# from Firebase. It also sets up handlers for subsequent Firebase events, and does
# the right thing with regard to putting the value into the scope in a performant
# manner. This could probably be improved by batching handler outputs for firing
# on a single DOM repaint.
# Links are indicies in Firebase which point to non-adjacent nodes in the database.
# They take two forms (the first for use in collections, the second in objects):
# Key: -J4GGK6Z4y2DP0rLr9Sx Value: pointer:customers
# Key: customer Value: customers:-J4GGK6Z4y2DP0rLr9Sx
#
# If a node value starts with pointer:, the service will go and sync the linke node
# to the value (in essence, replacing the value with the linked node)
#
# If a property name is put in the include option value, it will follow the value
# in the same way (by replacing the value for the property key with the linked node).
# See the usage comment above for information about how to use the include option.
apply
__synced: true
__loading: true
__raw: args.raw or null
_push: (value, opts) ->
ref = thisRef.push()
opts ?= {}
links = opts.links or []
_(value).extend
id: ref.name()
created: moment.utc().unix()
updated: moment.utc().unix()
ref.set clean value
parentPath = key.replace /\//g, ':'
_(links).each (link) ->
rootRef.child(link).child(value.id).set "pointer:#{parentPath}"
value
_set: (key, value) ->
ref = thisRef.child key
ref.set clean value
_remove: (key) ->
continuable = $q.defer()
ref = thisRef.child key
ref.remove ->
continuable.resolve(rootRef)
continuable.promise
_update: (newValue) ->
_(newValue).extend
updated: moment.utc().unix()
thisRef.update clean newValue
# get rid of functions in the object, and replace linked nodes with their
# old path values
_clean: (opts) ->
clean this, opts
_loading: ->
@__loading
_length: ->
cleaned = @_clean()
_(cleaned).keys().value().length
# Build an ng-repeatable version of this
_sequence: ->
cleaned = @_clean fnOnly: true
_(cleaned).values().value()
# get rid of falsy values
_compact: ->
_(@).each (v, k) =>
delete @[k] unless v
_loaded: ->
not @__loading
# Where the magic happens! Grab stuff from Firebase, and do the right thing with it
thisRef.once 'value', (snapshot) ->
value = snapshot.val()
return unless value
value.__loading = false
wait = apply value
wait.then ->
deferrable.resolve() # we've got an initial value at least
thisRef.on 'child_added', (snapshot) ->
childScopeKey = "#{scopeKey}['#{snapshot.name()}']"
if isPointer snapshot
self.sync scope,
key: childScopeKey
path: getPointerPath snapshot
include: childIncludesFor snapshot, includePaths
raw: snapshot.val()
else if isIncluded(snapshot, includePaths) and isObject(snapshot)
self.sync scope,
key: childScopeKey
ref: snapshot.ref()
include: childIncludesFor snapshot, includePaths
raw: snapshot.val()
else if isIncluded(snapshot, includePaths)
self.sync scope,
key: childScopeKey
path: getIncludePath snapshot
include: childIncludesFor snapshot, includePaths
raw: snapshot.val()
else
applyChild childScopeKey, snapshot.val()
thisRef.on 'child_removed', (snapshot) ->
childScopeKey = "#{scopeKey}['#{snapshot.name()}']"
applyChild childScopeKey, undefined
thisRef.on 'child_changed', (snapshot) ->
childScopeKey = "#{scopeKey}['#{snapshot.name()}']"
applyChild childScopeKey, snapshot.val()
deferrable.promise
startsWith = (str, starts) ->
return true if starts is ''
return false if not (str and starts)
str = String(str)
starts = String(starts)
str.length >= starts.length and str.slice(0, starts.length) is starts
isPointer = (snapshot) ->
if startsWith snapshot.val(), 'pointer:'
true
else
false
safe = (str) ->
str.replace(/\./g, ',')
getPointerPath = (snapshot) ->
id = snapshot.name()
pathComponents = snapshot.val().split(':')[1..]
pathComponents.push id
pathComponents.join '/'
getIncludePath = (snapshot) ->
pathComponents = snapshot.val().split(':')
pathComponents.join '/'
clean = (object, opts) ->
object._compact() if object._compact
fnOnly = opts?.fnOnly
wrapped = _(object)
fnless = wrapped
.omit((value, key) -> typeof value is 'function')
.omit((value, key) -> key[0] is '_')
.value()
unless fnOnly
_(fnless).each (value, key) ->
fnless[key] = if value.__synced then value.__raw else value
fnless
childIncludesFor = (snapshot, includes) ->
name = snapshot.name()
mapped = _(includes).map (path) ->
if path is name
null
else if startsWith(path, "#{name}/") or startsWith(path, '*/')
path.split('/')[1..].join('/')
else
null
mapped.compact().value()
isIncluded = (snapshot, includes) ->
mapped = _(includes).map (path) ->
if startsWith path, '*/'
true
else
false
return true if mapped.any()
name = snapshot.name()
_(includes).contains name
isObject = (snapshot) ->
_(snapshot.val()).isObject()
isEmpty = (obj) ->
_(obj).isEmpty()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment