UNPKG

fingular

Version:

A Firebase service provider for AngularJS designed for testability.

744 lines (586 loc) 18.5 kB
'use strict'; (function(angular, Firebase) { var noop = function() {}; angular.module('fingular', []) .factory('ProxiedFirebase', ['$timeout', function($timeout) { var timeoutElapsed = noop , timeoutLimit = 10000 , catchupTime = 50; function ProxiedSnapshot(snap) { this._snap = snap; } ProxiedSnapshot.prototype = { val: function() { return this._snap.val(); }, child: function() { return this._snap.child.apply(this._snap, arguments); }, forEach: function(cb) { return this._snap.forEach(function(childSnap) { if (cb(new ProxiedSnapshot(childSnap)) === true) { return true; } }); }, hasChild: function() { return this._snap.hasChild.apply(this._snap, arguments); }, hasChildren: function() { return this._snap.hasChildren.apply(this._snap, arguments); }, name: function() { return this._snap.name.apply(this._snap, arguments); }, numChildren: function() { return this._snap.numChildren(); }, ref: function() { return new ProxiedFirebase(this._snap.ref()); }, getPriority: function() { return this._snap.getPriority(); }, exportVal: function() { return this._snap.exportVal(); } }; function ProxiedQuery(query) { this._ref = query; } ProxiedQuery.prototype = { on: function(eventType, callback, cancelCallback, context) { callback = callback || noop; cancelCallback = cancelCallback || noop; var timeout = $timeout(timeoutElapsed, timeoutLimit); this._ref.on(eventType, function(snap, prevName) { var error; try { callback.call(context, new ProxiedSnapshot(snap), prevName); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }, function() { var error; try { cancelCallback.apply(context, arguments); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }, context); return callback; }, off: function() { this._ref.off.apply(this._ref, arguments); }, once: function(eventType, successCallback, failureCallback, context) { successCallback = successCallback || noop; failureCallback = failureCallback || noop; var timeout = $timeout(timeoutElapsed, timeoutLimit); this._ref.once(eventType, function(snap, prevName) { var error; try { successCallback.call(context, new ProxiedSnapshot(snap), prevName); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }, function() { var error; try { failureCallback.apply(context, arguments); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }, context); }, limit: function() { return new ProxiedQuery( this._ref.limit.apply(this._ref, arguments) ); }, startAt: function() { return new ProxiedQuery( this._ref.startAt.apply(this._ref, arguments) ); }, endAt: function() { return new ProxiedQuery( this._ref.endAt.apply(this._ref, arguments) ); }, ref: function() { return new ProxiedFirebase( this._ref.ref() ); } }; function ProxiedFirebase(ref) { this._ref = ref; if (typeof(this._ref.flush) === 'function') { this.flush = function() { this._ref.flush.apply(this._ref, arguments); }; } } ProxiedFirebase.prototype = { toString: function() { return this._ref.toString.apply(this._ref, arguments); }, auth: function(token, cb) { var that = this , timeout = $timeout(timeoutElapsed, timeoutLimit); cb = cb || noop; this._ref.auth(token, function(err) { var error; try { cb.apply(that, arguments); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }); }, unauth: function() { this._ref.unauth(); }, child: function(childPath) { return new ProxiedFirebase(this._ref.child(childPath)); }, parent: function() { return new ProxiedFirebase(this._ref.parent()); }, root: function() { return new ProxiedFirebase(this._ref.root()); }, name: function() { return this._ref.name(); }, set: function(value, onComplete) { var that = this , timeout = $timeout(timeoutElapsed, timeoutLimit); onComplete = onComplete || noop; this._ref.set(value, function(err) { var error; try { onComplete.apply(that, arguments); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }); }, update: function(value, onComplete) { var that = this , timeout = $timeout(timeoutElapsed, timeoutLimit); onComplete = onComplete || noop; this._ref.update(value, function(err) { var error; try { onComplete.apply(that, arguments); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }); }, remove: function(onComplete) { var that = this , timeout = $timeout(timeoutElapsed, timeoutLimit); onComplete = onComplete || noop; this._ref.remove(function(err) { var error; try { onComplete.apply(that, arguments); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }); }, push: function(value, onComplete) { var that = this , timeout = $timeout(timeoutElapsed, timeoutLimit); onComplete = onComplete || noop; return new ProxiedFirebase(this._ref.push(value, function(err) { var error; try { onComplete.apply(that, arguments); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } })); }, setWithPriority: function(value, priority, onComplete) { var that = this , timeout = $timeout(timeoutElapsed, timeoutLimit); onComplete = onComplete || noop; this._ref.setWithPriority(value, priority, function(err) { var error; try { onComplete.apply(that, arguments); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }); }, setPriority: function(priority, onComplete) { var that = this , timeout = $timeout(timeoutElapsed, timeoutLimit); onComplete = onComplete || noop; this._ref.setPriority(priority, function(err) { var error; try { onComplete.apply(that, arguments); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }); }, transaction: function(updateFn, onComplete, applyLocally) { var timeout , that = this; onComplete = onComplete || noop; this._ref.transaction(function() { // start timeout timeout = $timeout(timeoutElapsed, timeoutLimit); return updateFn.apply(that, arguments); }, function(err, committed, snap) { var error; try { onComplete.call(that, err, committed, new ProxiedSnapshot(snap)); } catch(e) { error = e; } finally { // cancel timeout if (timeout) { $timeout.cancel(timeout); } if (error) { throw(error); } } }) }, on: ProxiedQuery.prototype.on, off: ProxiedQuery.prototype.off, once: ProxiedQuery.prototype.once, limit: ProxiedQuery.prototype.limit, startAt: ProxiedQuery.prototype.startAt, endAt: ProxiedQuery.prototype.endAt, ref: function() { return this; } }; return ProxiedFirebase; }]) .provider('$firebaseRef', function FirebaseRefProvider() { var firebaseDomain , protocol = 'https' , mockMode , mockFirebase , mockData = {}; this['domain'] = function(newDomain) { firebaseDomain = newDomain; return this; }; this['protocol'] = function(newProtocol) { switch(protocol) { case 'http': case 'https': protocol = newProtocol; break; default: throw new Error('Invalid protocol. Only http and https can be used!'); break; } return this; } this['mockWith'] = function(mocker) { mockFirebase = mocker; mockMode = true; return this; }; this['mockOut'] = function(path, data) { mockData[path] = data; return this; }; this.$get = ['$timeout', '$injector', 'ProxiedFirebase', function($timeout, $injector, ProxiedFirebase) { if (!(typeof(firebaseDomain) === 'string')) { if ($injector.has('firebaseDomain')) { firebaseDomain = $injector.get('firebaseDomain'); } else { throw new Error('You must supply the domain name of your ' + 'Firebase account, either by providing the ' + 'constant \'firebaseDomain\' or by setting ' + 'the \'firebaseDomain\' property on the $firebaseProvider!'); } } if ($injector.has('firebaseProtocol')) { protocol = $injector.get('firebaseProtocol'); } if ($injector.has('firebaseMock') && mockFirebase === undefined) { mockFirebase = $injector.get('firebaseMock'); mockMode = true; } if ($injector.has('firebaseMockData')) { angular.forEach($injector.get('firebaseMockData'), function(mockDatum, key) { // FIXME(goldibex): deep introspection of mock data, this is pretty simplistic if (!mockData[key]) { mockData[key] = mockDatum; } }); } var injectable = function(path) { if (!path) { path = '/'; } else if (path[0] !== '/') { path = '/' + path; } if (mockMode) { return new ProxiedFirebase( new mockFirebase('Mock://' + firebaseDomain + path, mockData[path]) ); } else { return new ProxiedFirebase( new Firebase(protocol + '://' + firebaseDomain + path) ); } }; injectable.protocol = protocol; injectable.domain = firebaseDomain; injectable.mocked = mockMode === true; return injectable; }]; }) .provider('$firebaseUser', function FirebaseUserProvider() { var userPromise , mockUserConstructor , mockUserData , mockMode , authMethod , authObj; this['mockWith'] = function(mocker) { mockUserConstructor = mocker; mockMode = true; return this; }; this['mockUser'] = function(md) { mockUserData = md; return this; } this.$get = [ '$log', '$q', '$injector', '$rootScope', '$firebaseRef', function($log, $q, $injector, $rootScope, $firebaseRef) { if ($injector.has('firebaseUserMockData')) { mockUserData = $injector.get('firebaseUserMockData'); } if ($injector.has('firebaseUserMock')) { mockUserConstructor = $injector.get('firebaseUserMock'); mockMode = true; } var Constructor = mockMode ? mockUserConstructor : FirebaseSimpleLogin; function FirebaseUser() { var self = this; this._auth = new Constructor($firebaseRef(), function(err, authUser) { if (err) { $rootScope.$apply(function() { $rootScope.$broadcast('firebaseUser:error', err); }); } else if (authUser !== null) { $rootScope.$apply(function() { $rootScope.$broadcast('firebaseUser:auth', authUser); }); } else { $rootScope.$apply(function() { $rootScope.$broadcast('firebaseUser:unauth', authUser); }); } }, mockUserData); /** * Logs in using the given authType, or hands back the currently logged-in user. * @param {String} requestedAuthMethod A Firebase-supported auth method string, like 'facebook'. * @param {Object} [data] Additional data to pass into the login request, like email and password. */ this.login = function(requestedAuthMethod, data) { var deferred = $q.defer() , off1 , off2; off1 = $rootScope.$on('firebaseUser:auth', function(e, user) { off1(); off2(); deferred.resolve(user); }); off2 = $rootScope.$on('firebaseUser:error', function(e, err) { off1(); off2(); deferred.reject(err); }); self._auth.login(requestedAuthMethod, data); return deferred.promise; }; this.logout = function() { self._auth.logout(); }; this.createUser = function(email, password) { var deferred = $q.defer(); self._auth.createUser(email, password, function(err, userData) { if (err) { $log.debug('user create failed'); $log.debug(err); deferred.reject(err); } else { $rootScope.$apply(function() { var off = $rootScope.$on('firebaseUser:auth', function(e, user) { off(); deferred.resolve(user); }); self.login('password', { email: email, password: password }); }); } }); return deferred.promise; }; this.removeUser = function(email, password) { var deferred = $q.defer(); self._auth.changePassword(email, password, function(err, ok) { if (ok) { deferred.resolve(ok); } else { deferred.reject(err); } }); return deferred.promise; }; this.changePassword = function(email, oldpassword, newpassword) { var deferred = $q.defer(); self._auth.changePassword(email, oldpassword, newpassword, function(err, ok) { if (ok) { deferred.resolve(ok); } else { deferred.reject(err); } }); return deferred.promise; }; this.sendPasswordResetEmail = function(email) { var deferred = $q.defer(); self._auth.sendPasswordResetEmail(email, function(err, ok) { if (ok) { deferred.resolve(ok); } else { deferred.reject(err); } }); return deferred.promise; }; }; /** * Stamps a given Firebase reference with the current user's information, * and log the stamping on the user's own object. * @param {Firebase} reference A firebase reference pointing somewhere * @param {String} propName The name of the child on the reference to store the user's key. Defaults to 'user.' */ this.stamp = function(reference, propName) { // Get the absolute path of the reference. var hostLength = reference.root().toString().length , path = decodeURI(reference.toString().slice(hostLength + 1)) , logPath = [usersCollection, user.uid, 'log', path].join('/'); // Log the revision in the user history. $firebaseRef(logPath).set((new Date()).getTime()); }; return new FirebaseUser(); } ]; }); })(window.angular, window.Firebase);