Created
October 23, 2013 16:19
-
-
Save ndemonner/7121749 to your computer and use it in GitHub Desktop.
Data service from Concierge.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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