├── .gitignore
├── .jshintrc
├── Gruntfile.js
├── README.md
├── firebase-as-array.js
├── firebase-as-array.min.js
├── package.json
├── src
└── firebase-as-array.js
└── test
├── lib
└── MockFirebase.js
└── test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "bitwise" : true,
3 | "boss" : true,
4 | "browser" : true,
5 | "curly" : true,
6 | "devel" : true,
7 | "eqnull" : true,
8 | "globals" : {
9 | "angular" : false,
10 | "Firebase" : false,
11 | "FirebaseSimpleLogin" : false
12 | },
13 | "globalstrict" : true,
14 | "indent" : 2,
15 | "latedef" : true,
16 | "maxlen" : 115,
17 | "noempty" : true,
18 | "nonstandard" : true,
19 | "undef" : true,
20 | "unused" : true,
21 | "trailing" : true
22 | }
23 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /* global module */
2 |
3 | module.exports = function(grunt) {
4 | 'use strict';
5 |
6 | grunt.initConfig({
7 | pkg: grunt.file.readJSON('package.json'),
8 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' +
9 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
10 | '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' +
11 | '* Copyright (c) <%= grunt.template.today("yyyy") %> Kato\n' +
12 | '* MIT LICENSE */\n\n',
13 |
14 | concat: {
15 | app: {
16 | options: { banner: '<%= banner %>' },
17 | src: [
18 | 'src/firebase-as-array.js'
19 | ],
20 | dest: 'firebase-as-array.js'
21 | }
22 | },
23 |
24 | uglify: {
25 | options: {
26 | preserveComments: 'some'
27 | },
28 | app: {
29 | files: {
30 | 'firebase-as-array.min.js': ['firebase-as-array.js']
31 | }
32 | }
33 | },
34 |
35 | watch: {
36 | build: {
37 | files: ['src/**/*.js', 'Gruntfile.js'],
38 | tasks: ['make'],
39 | options: {
40 | interrupt: true
41 | }
42 | },
43 | test: {
44 | files: ['src/**/*.js', 'Gruntfile.js', 'test/**'],
45 | tasks: ['test']
46 | }
47 | },
48 |
49 | // Configure a mochaTest task
50 | mochaTest: {
51 | test: {
52 | options: {
53 | growl: true,
54 | timeout: 5000,
55 | reporter: 'spec'
56 | },
57 | require: [
58 | "chai"
59 | ],
60 | log: true,
61 | src: ['test/*.js']
62 | }
63 | },
64 |
65 | notify: {
66 | watch: {
67 | options: {
68 | title: 'Grunt Watch',
69 | message: 'Build Finished'
70 | }
71 | }
72 | }
73 |
74 | });
75 |
76 | require('load-grunt-tasks')(grunt);
77 |
78 | grunt.loadNpmTasks('grunt-contrib-uglify');
79 | grunt.loadNpmTasks('grunt-contrib-concat');
80 | grunt.loadNpmTasks('grunt-contrib-watch');
81 | grunt.loadNpmTasks('grunt-notify');
82 | grunt.loadNpmTasks('grunt-mocha-test');
83 |
84 | grunt.registerTask('make', ['concat', 'uglify']);
85 | grunt.registerTask('test', ['make', 'mochaTest']);
86 |
87 | grunt.registerTask('default', ['make', 'test']);
88 | };
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Firebase.getAsArray
2 |
3 | **This uses an outdated version of the Firebase SDK and is no longer applicable.**
4 |
5 | A simple library to demonstrate how arrays can be synchronized to a real-time, distributed system like Firebase.
6 |
7 | This library demonstrates the following best practices for using arrays with collaborative, real-time data:
8 |
9 | - make the array read only (don't use splice, pop, etc; have setter methods)
10 | - when possible, refer to records using a uniqueId rather than array index
11 | - synchronize changes from the server directly into our array
12 | - push local changes to the server and letting them trickle back
13 |
14 | In other words, our array is essentialy one-directional. Changes come from the server into the array, we read them out, we push our local edits to the server, they trickle back into the array.
15 |
16 | Read more about synchronized arrays and this lib on the [Firebase Blog](https://www.firebase.com/blog/).
17 |
18 | ## Installation
19 |
20 | Download the firebase-as-array.min.js file and include it in your HTML:
21 |
22 |
23 |
27 |
28 | Or in your node.js project:
29 |
30 | var Firebase = require('firebase');
31 | var getAsArray = require('./firebase-as-array.js');
32 |
33 | var ref = new Firebase(URL);
34 | var list = getAsArray(ref);
35 |
36 | ## Usage
37 |
38 | var ref = new Firebase(URL);
39 |
40 | // create a synchronized array
41 | var list = getAsArray(ref);
42 |
43 | // add a new record
44 | var ref = list.$add({foo: 'bar'});
45 |
46 | // remove record
47 | list.$remove( key );
48 |
49 | // set priority on a record
50 | list.$move( key, newPriority );
51 |
52 | // find position of a key in the list
53 | list.$indexOf( key );
54 |
55 | // find key for a record at position 1
56 | list[1].$id;
57 |
58 | ## Limitations
59 |
60 | All the records stored in the array are objects. Primitive values get stored as { '.value': primitive }
61 |
62 | Does not support IE 8 and below by default. To support these, simply include polyfills for
63 | [Array.prototype.forEach](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Polyfill)
64 | and [Function.prototype.bind](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility) in your code base.
65 |
66 | ## API
67 |
68 | ### getAsArray(ref[, eventCallback])
69 |
70 | @param {Firebase} ref
71 | @param {Function} [callback]
72 | @returns {Array}
73 |
74 | Creates a new array and synchronizes it to the ref provided.
75 |
76 | ### $id
77 |
78 | The record ID. This is the unique URL key used to store the record in Firebase (the equivalent of firebaseRef.name()).
79 |
80 | ### $indexOf(key)
81 |
82 | @param {String} key the path name for a record to find in the array
83 | @returns {int} the index of the element in the array or -1 if not found
84 |
85 | A convenience method to find the array position of a given key.
86 |
87 | ### $add(data)
88 |
89 | @param data the data to be put in Firebase as a new child record
90 | @returns {Firebase} the ref to the newly created record
91 |
92 | Adds a record to Firebase and returns the reference. To obtain its id, use `ref.name()`, assuming `ref` is the variable assigned to the return value.
93 |
94 | ### $remove(key)
95 |
96 | @param {string} key a record id to be removed locally and remotely
97 |
98 | Removes a record locally and from Firebase
99 |
100 | ### $set(key, data)
101 |
102 | @param {string} key a record id to be replaced
103 | @param data what goes into it
104 |
105 | Replaces the value of a record locally and in Firebase
106 |
107 | ### $update(key, data)
108 |
109 | @param {string} key a record id to be updated
110 | @param {object} data some keys to be replaced
111 |
112 | Updates the value of a record locally, replacing any keys that are in `data` with the values provided and leaving the rest of the record alone.
113 |
114 | ### $move(key, newPriority)
115 |
116 | @param {string} key record id to be moved
117 | @param {string|int} newPriority the sort order to be applied
118 |
119 | Moves a record locally and in the remote data list.
120 |
121 | ## Development
122 |
123 | This lib is intended primarily to be an example. However, pull requests will be happily accepted.
124 |
125 | git clone git@github.com:yourname/Firebase.getAsArray
126 | npm install
127 | grunt
128 | grunt watch
129 | # make your fixes
130 | # verify tests are at 100%
131 | grunt make
132 | git commit -m "your changes"
133 | git push
134 | # create a pull request!
135 |
136 | ## Support
137 |
138 | Use the issue tracker if you have questions or problems. Since this is meant primarily as an example, do not expect prompt replies!
139 |
--------------------------------------------------------------------------------
/firebase-as-array.js:
--------------------------------------------------------------------------------
1 | /*! Firebase.getAsArray - v0.1.0 - 2014-04-21
2 | * Copyright (c) 2014 Kato
3 | * MIT LICENSE */
4 |
5 | (function(exports) {
6 |
7 | exports.getAsArray = function(ref, eventCallback) {
8 | return new ReadOnlySynchronizedArray(ref, eventCallback).getList();
9 | };
10 |
11 | function ReadOnlySynchronizedArray(ref, eventCallback) {
12 | this.list = [];
13 | this.subs = []; // used to track event listeners for dispose()
14 | this.ref = ref;
15 | this.eventCallback = eventCallback;
16 | this._wrapList();
17 | this._initListeners();
18 | }
19 |
20 | ReadOnlySynchronizedArray.prototype = {
21 | getList: function() {
22 | return this.list;
23 | },
24 |
25 | add: function(data) {
26 | var key = this.ref.push().name();
27 | var ref = this.ref.child(key);
28 | if( arguments.length > 0 ) { ref.set(parseForJson(data), this._handleErrors.bind(this, key)); }
29 | return ref;
30 | },
31 |
32 | set: function(key, newValue) {
33 | this.ref.child(key).set(parseForJson(newValue), this._handleErrors.bind(this, key));
34 | },
35 |
36 | update: function(key, newValue) {
37 | this.ref.child(key).update(parseForJson(newValue), this._handleErrors.bind(this, key));
38 | },
39 |
40 | setPriority: function(key, newPriority) {
41 | this.ref.child(key).setPriority(newPriority);
42 | },
43 |
44 | remove: function(key) {
45 | this.ref.child(key).remove(this._handleErrors.bind(null, key));
46 | },
47 |
48 | posByKey: function(key) {
49 | return findKeyPos(this.list, key);
50 | },
51 |
52 | placeRecord: function(key, prevId) {
53 | if( prevId === null ) {
54 | return 0;
55 | }
56 | else {
57 | var i = this.posByKey(prevId);
58 | if( i === -1 ) {
59 | return this.list.length;
60 | }
61 | else {
62 | return i+1;
63 | }
64 | }
65 | },
66 |
67 | getRecord: function(key) {
68 | var i = this.posByKey(key);
69 | if( i === -1 ) return null;
70 | return this.list[i];
71 | },
72 |
73 | dispose: function() {
74 | var ref = this.ref;
75 | this.subs.forEach(function(s) {
76 | ref.off(s[0], s[1]);
77 | });
78 | this.subs = [];
79 | },
80 |
81 | _serverAdd: function(snap, prevId) {
82 | var data = parseVal(snap.name(), snap.val());
83 | this._moveTo(snap.name(), data, prevId);
84 | this._handleEvent('child_added', snap.name(), data);
85 | },
86 |
87 | _serverRemove: function(snap) {
88 | var pos = this.posByKey(snap.name());
89 | if( pos !== -1 ) {
90 | this.list.splice(pos, 1);
91 | this._handleEvent('child_removed', snap.name(), this.list[pos]);
92 | }
93 | },
94 |
95 | _serverChange: function(snap) {
96 | var pos = this.posByKey(snap.name());
97 | if( pos !== -1 ) {
98 | this.list[pos] = applyToBase(this.list[pos], parseVal(snap.name(), snap.val()));
99 | this._handleEvent('child_changed', snap.name(), this.list[pos]);
100 | }
101 | },
102 |
103 | _serverMove: function(snap, prevId) {
104 | var id = snap.name();
105 | var oldPos = this.posByKey(id);
106 | if( oldPos !== -1 ) {
107 | var data = this.list[oldPos];
108 | this.list.splice(oldPos, 1);
109 | this._moveTo(id, data, prevId);
110 | this._handleEvent('child_moved', snap.name(), data);
111 | }
112 | },
113 |
114 | _moveTo: function(id, data, prevId) {
115 | var pos = this.placeRecord(id, prevId);
116 | this.list.splice(pos, 0, data);
117 | },
118 |
119 | _handleErrors: function(key, err) {
120 | if( err ) {
121 | this._handleEvent('error', null, key);
122 | console.error(err);
123 | }
124 | },
125 |
126 | _handleEvent: function(eventType, recordId, data) {
127 | // console.log(eventType, recordId);
128 | this.eventCallback && this.eventCallback(eventType, recordId, data);
129 | },
130 |
131 | _wrapList: function() {
132 | this.list.$indexOf = this.posByKey.bind(this);
133 | this.list.$add = this.add.bind(this);
134 | this.list.$remove = this.remove.bind(this);
135 | this.list.$set = this.set.bind(this);
136 | this.list.$update = this.update.bind(this);
137 | this.list.$move = this.setPriority.bind(this);
138 | this.list.$rawData = function(key) { return parseForJson(this.getRecord(key)) }.bind(this);
139 | this.list.$off = this.dispose.bind(this);
140 | },
141 |
142 | _initListeners: function() {
143 | this._monit('child_added', this._serverAdd);
144 | this._monit('child_removed', this._serverRemove);
145 | this._monit('child_changed', this._serverChange);
146 | this._monit('child_moved', this._serverMove);
147 | },
148 |
149 | _monit: function(event, method) {
150 | this.subs.push([event, this.ref.on(event, method.bind(this))]);
151 | }
152 | };
153 |
154 | function applyToBase(base, data) {
155 | // do not replace the reference to objects contained in the data
156 | // instead, just update their child values
157 | if( isObject(base) && isObject(data) ) {
158 | var key;
159 | for(key in base) {
160 | if( key !== '$id' && base.hasOwnProperty(key) && !data.hasOwnProperty(key) ) {
161 | delete base[key];
162 | }
163 | }
164 | for(key in data) {
165 | if( data.hasOwnProperty(key) ) {
166 | base[key] = data[key];
167 | }
168 | }
169 | return base;
170 | }
171 | else {
172 | return data;
173 | }
174 | }
175 |
176 | function isObject(x) {
177 | return typeof(x) === 'object' && x !== null;
178 | }
179 |
180 | function findKeyPos(list, key) {
181 | for(var i = 0, len = list.length; i < len; i++) {
182 | if( list[i].$id === key ) {
183 | return i;
184 | }
185 | }
186 | return -1;
187 | }
188 |
189 | function parseForJson(data) {
190 | if( data && typeof(data) === 'object' ) {
191 | delete data['$id'];
192 | if( data.hasOwnProperty('.value') ) {
193 | data = data['.value'];
194 | }
195 | }
196 | if( data === undefined ) {
197 | data = null;
198 | }
199 | return data;
200 | }
201 |
202 | function parseVal(id, data) {
203 | if( typeof(data) !== 'object' || !data ) {
204 | data = { '.value': data };
205 | }
206 | data['$id'] = id;
207 | return data;
208 | }
209 | })(typeof(window)==='undefined'? exports : window.Firebase);
--------------------------------------------------------------------------------
/firebase-as-array.min.js:
--------------------------------------------------------------------------------
1 | /*! Firebase.getAsArray - v0.1.0 - 2014-04-21
2 | * Copyright (c) 2014 Kato
3 | * MIT LICENSE */
4 | !function(a){function b(a,b){this.list=[],this.subs=[],this.ref=a,this.eventCallback=b,this._wrapList(),this._initListeners()}function c(a,b){if(d(a)&&d(b)){var c;for(c in a)"$id"!==c&&a.hasOwnProperty(c)&&!b.hasOwnProperty(c)&&delete a[c];for(c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return a}return b}function d(a){return"object"==typeof a&&null!==a}function e(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c].$id===b)return c;return-1}function f(a){return a&&"object"==typeof a&&(delete a.$id,a.hasOwnProperty(".value")&&(a=a[".value"])),void 0===a&&(a=null),a}function g(a,b){return"object"==typeof b&&b||(b={".value":b}),b.$id=a,b}a.getAsArray=function(a,c){return new b(a,c).getList()},b.prototype={getList:function(){return this.list},add:function(a){var b=this.ref.push().name(),c=this.ref.child(b);return arguments.length>0&&c.set(f(a),this._handleErrors.bind(this,b)),c},set:function(a,b){this.ref.child(a).set(f(b),this._handleErrors.bind(this,a))},update:function(a,b){this.ref.child(a).update(f(b),this._handleErrors.bind(this,a))},setPriority:function(a,b){this.ref.child(a).setPriority(b)},remove:function(a){this.ref.child(a).remove(this._handleErrors.bind(null,a))},posByKey:function(a){return e(this.list,a)},placeRecord:function(a,b){if(null===b)return 0;var c=this.posByKey(b);return-1===c?this.list.length:c+1},getRecord:function(a){var b=this.posByKey(a);return-1===b?null:this.list[b]},dispose:function(){var a=this.ref;this.subs.forEach(function(b){a.off(b[0],b[1])}),this.subs=[]},_serverAdd:function(a,b){var c=g(a.name(),a.val());this._moveTo(a.name(),c,b),this._handleEvent("child_added",a.name(),c)},_serverRemove:function(a){var b=this.posByKey(a.name());-1!==b&&(this.list.splice(b,1),this._handleEvent("child_removed",a.name(),this.list[b]))},_serverChange:function(a){var b=this.posByKey(a.name());-1!==b&&(this.list[b]=c(this.list[b],g(a.name(),a.val())),this._handleEvent("child_changed",a.name(),this.list[b]))},_serverMove:function(a,b){var c=a.name(),d=this.posByKey(c);if(-1!==d){var e=this.list[d];this.list.splice(d,1),this._moveTo(c,e,b),this._handleEvent("child_moved",a.name(),e)}},_moveTo:function(a,b,c){var d=this.placeRecord(a,c);this.list.splice(d,0,b)},_handleErrors:function(a,b){b&&(this._handleEvent("error",null,a),console.error(b))},_handleEvent:function(a,b,c){this.eventCallback&&this.eventCallback(a,b,c)},_wrapList:function(){this.list.$indexOf=this.posByKey.bind(this),this.list.$add=this.add.bind(this),this.list.$remove=this.remove.bind(this),this.list.$set=this.set.bind(this),this.list.$update=this.update.bind(this),this.list.$move=this.setPriority.bind(this),this.list.$rawData=function(a){return f(this.getRecord(a))}.bind(this),this.list.$off=this.dispose.bind(this)},_initListeners:function(){this._monit("child_added",this._serverAdd),this._monit("child_removed",this._serverRemove),this._monit("child_changed",this._serverChange),this._monit("child_moved",this._serverMove)},_monit:function(a,b){this.subs.push([a,this.ref.on(a,b.bind(this))])}}}("undefined"==typeof window?exports:window.Firebase);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Firebase.getAsArray",
3 | "version": "0.1.0",
4 | "description": "",
5 | "main": "firebase-as-array.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/firebase/angularFire.git"
9 | },
10 | "bugs": {
11 | "url": "https://github.com/firebase/angularFire/issues"
12 | },
13 | "devDependencies": {
14 | "chai": "~1.9.1",
15 | "grunt": "~0.4.4",
16 | "grunt-contrib-concat": "^0.4.0",
17 | "grunt-contrib-jshint": "~0.6.2",
18 | "grunt-contrib-uglify": "~0.4.0",
19 | "grunt-contrib-watch": "~0.6.1",
20 | "grunt-mocha-test": "~0.10.2",
21 | "grunt-notify": "~0.2.20",
22 | "load-grunt-tasks": "~0.4.0",
23 | "lodash": "~2.4.1",
24 | "sinon": "~1.9.1",
25 | "sinon-chai": "~2.5.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/firebase-as-array.js:
--------------------------------------------------------------------------------
1 | (function(exports) {
2 |
3 | exports.getAsArray = function(ref, eventCallback) {
4 | return new ReadOnlySynchronizedArray(ref, eventCallback).getList();
5 | };
6 |
7 | function ReadOnlySynchronizedArray(ref, eventCallback) {
8 | this.list = [];
9 | this.subs = []; // used to track event listeners for dispose()
10 | this.ref = ref;
11 | this.eventCallback = eventCallback;
12 | this._wrapList();
13 | this._initListeners();
14 | }
15 |
16 | ReadOnlySynchronizedArray.prototype = {
17 | getList: function() {
18 | return this.list;
19 | },
20 |
21 | add: function(data) {
22 | var key = this.ref.push().name();
23 | var ref = this.ref.child(key);
24 | if( arguments.length > 0 ) { ref.set(parseForJson(data), this._handleErrors.bind(this, key)); }
25 | return ref;
26 | },
27 |
28 | set: function(key, newValue) {
29 | this.ref.child(key).set(parseForJson(newValue), this._handleErrors.bind(this, key));
30 | },
31 |
32 | update: function(key, newValue) {
33 | this.ref.child(key).update(parseForJson(newValue), this._handleErrors.bind(this, key));
34 | },
35 |
36 | setPriority: function(key, newPriority) {
37 | this.ref.child(key).setPriority(newPriority);
38 | },
39 |
40 | remove: function(key) {
41 | this.ref.child(key).remove(this._handleErrors.bind(null, key));
42 | },
43 |
44 | posByKey: function(key) {
45 | return findKeyPos(this.list, key);
46 | },
47 |
48 | placeRecord: function(key, prevId) {
49 | if( prevId === null ) {
50 | return 0;
51 | }
52 | else {
53 | var i = this.posByKey(prevId);
54 | if( i === -1 ) {
55 | return this.list.length;
56 | }
57 | else {
58 | return i+1;
59 | }
60 | }
61 | },
62 |
63 | getRecord: function(key) {
64 | var i = this.posByKey(key);
65 | if( i === -1 ) return null;
66 | return this.list[i];
67 | },
68 |
69 | dispose: function() {
70 | var ref = this.ref;
71 | this.subs.forEach(function(s) {
72 | ref.off(s[0], s[1]);
73 | });
74 | this.subs = [];
75 | },
76 |
77 | _serverAdd: function(snap, prevId) {
78 | var data = parseVal(snap.name(), snap.val());
79 | this._moveTo(snap.name(), data, prevId);
80 | this._handleEvent('child_added', snap.name(), data);
81 | },
82 |
83 | _serverRemove: function(snap) {
84 | var pos = this.posByKey(snap.name());
85 | if( pos !== -1 ) {
86 | this.list.splice(pos, 1);
87 | this._handleEvent('child_removed', snap.name(), this.list[pos]);
88 | }
89 | },
90 |
91 | _serverChange: function(snap) {
92 | var pos = this.posByKey(snap.name());
93 | if( pos !== -1 ) {
94 | this.list[pos] = applyToBase(this.list[pos], parseVal(snap.name(), snap.val()));
95 | this._handleEvent('child_changed', snap.name(), this.list[pos]);
96 | }
97 | },
98 |
99 | _serverMove: function(snap, prevId) {
100 | var id = snap.name();
101 | var oldPos = this.posByKey(id);
102 | if( oldPos !== -1 ) {
103 | var data = this.list[oldPos];
104 | this.list.splice(oldPos, 1);
105 | this._moveTo(id, data, prevId);
106 | this._handleEvent('child_moved', snap.name(), data);
107 | }
108 | },
109 |
110 | _moveTo: function(id, data, prevId) {
111 | var pos = this.placeRecord(id, prevId);
112 | this.list.splice(pos, 0, data);
113 | },
114 |
115 | _handleErrors: function(key, err) {
116 | if( err ) {
117 | this._handleEvent('error', null, key);
118 | console.error(err);
119 | }
120 | },
121 |
122 | _handleEvent: function(eventType, recordId, data) {
123 | // console.log(eventType, recordId);
124 | this.eventCallback && this.eventCallback(eventType, recordId, data);
125 | },
126 |
127 | _wrapList: function() {
128 | this.list.$indexOf = this.posByKey.bind(this);
129 | this.list.$add = this.add.bind(this);
130 | this.list.$remove = this.remove.bind(this);
131 | this.list.$set = this.set.bind(this);
132 | this.list.$update = this.update.bind(this);
133 | this.list.$move = this.setPriority.bind(this);
134 | this.list.$rawData = function(key) { return parseForJson(this.getRecord(key)) }.bind(this);
135 | this.list.$off = this.dispose.bind(this);
136 | },
137 |
138 | _initListeners: function() {
139 | this._monit('child_added', this._serverAdd);
140 | this._monit('child_removed', this._serverRemove);
141 | this._monit('child_changed', this._serverChange);
142 | this._monit('child_moved', this._serverMove);
143 | },
144 |
145 | _monit: function(event, method) {
146 | this.subs.push([event, this.ref.on(event, method.bind(this))]);
147 | }
148 | };
149 |
150 | function applyToBase(base, data) {
151 | // do not replace the reference to objects contained in the data
152 | // instead, just update their child values
153 | if( isObject(base) && isObject(data) ) {
154 | var key;
155 | for(key in base) {
156 | if( key !== '$id' && base.hasOwnProperty(key) && !data.hasOwnProperty(key) ) {
157 | delete base[key];
158 | }
159 | }
160 | for(key in data) {
161 | if( data.hasOwnProperty(key) ) {
162 | base[key] = data[key];
163 | }
164 | }
165 | return base;
166 | }
167 | else {
168 | return data;
169 | }
170 | }
171 |
172 | function isObject(x) {
173 | return typeof(x) === 'object' && x !== null;
174 | }
175 |
176 | function findKeyPos(list, key) {
177 | for(var i = 0, len = list.length; i < len; i++) {
178 | if( list[i].$id === key ) {
179 | return i;
180 | }
181 | }
182 | return -1;
183 | }
184 |
185 | function parseForJson(data) {
186 | if( data && typeof(data) === 'object' ) {
187 | delete data['$id'];
188 | if( data.hasOwnProperty('.value') ) {
189 | data = data['.value'];
190 | }
191 | }
192 | if( data === undefined ) {
193 | data = null;
194 | }
195 | return data;
196 | }
197 |
198 | function parseVal(id, data) {
199 | if( typeof(data) !== 'object' || !data ) {
200 | data = { '.value': data };
201 | }
202 | data['$id'] = id;
203 | return data;
204 | }
205 | })(typeof(window)==='undefined'? exports : window.Firebase);
--------------------------------------------------------------------------------
/test/lib/MockFirebase.js:
--------------------------------------------------------------------------------
1 | /**
2 | * MockFirebase: A Firebase stub/spy library for writing unit tests
3 | * https://github.com/katowulf/mockfirebase
4 | * @version 0.0.3
5 | */
6 | (function(exports) {
7 | var DEBUG = false;
8 | var PUSH_COUNTER = 0;
9 | var _ = requireLib('lodash', '_');
10 | var sinon = requireLib('sinon');
11 | var _Firebase = exports.Firebase;
12 | var _FirebaseSimpleLogin = exports.FirebaseSimpleLogin;
13 |
14 | /**
15 | * A mock that simulates Firebase operations for use in unit tests.
16 | *
17 | * ## Setup
18 | *
19 | * // in windows
20 | *
21 | *
22 | *
23 | *
24 | * // in node.js
25 | * var Firebase = require('../lib/MockFirebase');
26 | *
27 | * ## Usage Examples
28 | *
29 | * var fb = new Firebase('Mock://foo/bar');
30 | * fb.on('value', function(snap) {
31 | * console.log(snap.val());
32 | * });
33 | *
34 | * // do something async or synchronously...
35 | *
36 | * // trigger callbacks and event listeners
37 | * fb.flush();
38 | *
39 | * // spy on methods
40 | * expect(fb.on.called).toBe(true);
41 | *
42 | * ## Trigger events automagically instead of calling flush()
43 | *
44 | * var fb = new MockFirebase('Mock://hello/world');
45 | * fb.autoFlush(1000); // triggers events after 1 second (asynchronous)
46 | * fb.autoFlush(); // triggers events immediately (synchronous)
47 | *
48 | * ## Simulating Errors
49 | *
50 | * var fb = new MockFirebase('Mock://fails/a/lot');
51 | * fb.failNext('set', new Error('PERMISSION_DENIED'); // create an error to be invoked on the next set() op
52 | * fb.set({foo: bar}, function(err) {
53 | * // err.message === 'PERMISSION_DENIED'
54 | * });
55 | * fb.flush();
56 | *
57 | * @param {string} [currentPath] use a relative path here or a url, all .child() calls will append to this
58 | * @param {Object} [data] specify the data in this Firebase instance (defaults to MockFirebase.DEFAULT_DATA)
59 | * @param {MockFirebase} [parent] for internal use
60 | * @param {string} [name] for internal use
61 | * @constructor
62 | */
63 | function MockFirebase(currentPath, data, parent, name) {
64 | // these are set whenever startAt(), limit() or endAt() get invoked
65 | this._queryProps = { limit: undefined, startAt: undefined, endAt: undefined };
66 |
67 | // represents the fake url
68 | this.currentPath = currentPath || 'Mock://';
69 |
70 | // do not modify this directly, use set() and flush(true)
71 | this.data = _.cloneDeep(arguments.length > 1? data||null : MockFirebase.DEFAULT_DATA);
72 |
73 | // see failNext()
74 | this.errs = {};
75 |
76 | // used for setPriorty and moving records
77 | this.priority = null;
78 |
79 | // null for the root path
80 | this.myName = parent? name : extractName(currentPath);
81 |
82 | // see autoFlush()
83 | this.flushDelay = false;
84 |
85 | // stores the listeners for various event types
86 | this._events = { value: [], child_added: [], child_removed: [], child_changed: [], child_moved: [] };
87 |
88 | // allows changes to be propagated between child/parent instances
89 | this.parent = parent||null;
90 | this.children = [];
91 | parent && parent.children.push(this);
92 |
93 | // stores the operations that have been queued until a flush() event is triggered
94 | this.ops = [];
95 |
96 | // turn all our public methods into spies so they can be monitored for calls and return values
97 | // see jasmine spies: https://github.com/pivotal/jasmine/wiki/Spies
98 | // the Firebase constructor can be spied on using spyOn(window, 'Firebase') from within the test unit
99 | for(var key in this) {
100 | if( !key.match(/^_/) && typeof(this[key]) === 'function' ) {
101 | sinon.spy(this, key);
102 | }
103 | }
104 | }
105 |
106 | MockFirebase.prototype = {
107 | /*****************************************************
108 | * Test Unit tools (not part of Firebase API)
109 | *****************************************************/
110 |
111 | /**
112 | * Invoke all the operations that have been queued thus far. If a numeric delay is specified, this
113 | * occurs asynchronously. Otherwise, it is a synchronous event.
114 | *
115 | * This allows Firebase to be used in synchronous tests without waiting for async callbacks. It also
116 | * provides a rudimentary mechanism for simulating locally cached data (events are triggered
117 | * synchronously when you do on('value') or on('child_added') against locally cached data)
118 | *
119 | * If you call this multiple times with different delay values, you could invoke the events out
120 | * of order; make sure that is your intention.
121 | *
122 | * @param {boolean|int} [delay]
123 | * @returns {MockFirebase}
124 | */
125 | flush: function(delay) {
126 | var self = this, list = self.ops;
127 | self.ops = [];
128 | if( _.isNumber(delay) ) {
129 | setTimeout(process, delay);
130 | }
131 | else {
132 | process();
133 | }
134 | function process() {
135 | list.forEach(function(parts) {
136 | parts[0].apply(self, parts.slice(1));
137 | });
138 |
139 | self.children.forEach(function(c) {
140 | c.flush();
141 | });
142 | }
143 | return self;
144 | },
145 |
146 | /**
147 | * Automatically trigger a flush event after each operation. If a numeric delay is specified, this is an
148 | * asynchronous event. If value is set to true, it is synchronous (flush is triggered immediately). Setting
149 | * this to false disables autoFlush
150 | *
151 | * @param {int|boolean} [delay]
152 | */
153 | autoFlush: function(delay){
154 | this.flushDelay = _.isUndefined(delay)? true : delay;
155 | this.children.forEach(function(c) {
156 | c.autoFlush(delay);
157 | });
158 | delay !== false && this.flush(delay);
159 | return this;
160 | },
161 |
162 | /**
163 | * Simulate a failure by specifying that the next invocation of methodName should
164 | * fail with the provided error.
165 | *
166 | * @param {String} methodName currently only supports `set` and `transaction`
167 | * @param {String|Error} error
168 | */
169 | failNext: function(methodName, error) {
170 | this.errs[methodName] = error;
171 | },
172 |
173 | /**
174 | * Returns a copy of the current data
175 | * @returns {*}
176 | */
177 | getData: function() {
178 | return _.cloneDeep(this.data);
179 | },
180 |
181 | /**
182 | * Returns the last automatically generated ID
183 | * @returns {string|string|*}
184 | */
185 | getLastAutoId: function() {
186 | return 'mock'+PUSH_COUNTER;
187 | },
188 |
189 | /*****************************************************
190 | * Firebase API methods
191 | *****************************************************/
192 |
193 | toString: function() {
194 | return this.currentPath;
195 | },
196 |
197 | child: function(childPath) {
198 | if( !childPath ) { throw new Error('bad child path '+this.toString()); }
199 | var parts = _.isArray(childPath)? childPath : childPath.split('/');
200 | var childKey = parts.shift();
201 | var child = _.find(this.children, function(c) {
202 | return c.name() === childKey;
203 | });
204 | if( !child ) {
205 | child = new MockFirebase(mergePaths(this.currentPath, childKey), this._childData(childKey), this, childKey);
206 | child.flushDelay = this.flushDelay;
207 | }
208 | if( parts.length ) {
209 | child = child.child(parts);
210 | }
211 | return child;
212 | },
213 |
214 | set: function(data, callback) {
215 | var self = this;
216 | var err = this._nextErr('set');
217 | data = _.cloneDeep(data);
218 | DEBUG && console.log('set called',this.toString(), data); //debug
219 | this._defer(function() {
220 | DEBUG && console.log('set completed',this.toString(), data); //debug
221 | if( err === null ) {
222 | self._dataChanged(data);
223 | }
224 | callback && callback(err);
225 | });
226 | return this;
227 | },
228 |
229 | update: function(changes, callback) {
230 | if( !_.isObject(changes) ) {
231 | throw new Error('First argument must be an object when calling $update');
232 | }
233 | var self = this;
234 | var err = this._nextErr('update');
235 | var base = this.getData();
236 | var data = _.assign(_.isObject(base)? base : {}, changes);
237 | DEBUG && console.log('update called', this.toString(), data); //debug
238 | this._defer(function() {
239 | DEBUG && console.log('update completed', this.toString(), data); //debug
240 | if( err === null ) {
241 | self._dataChanged(data);
242 | }
243 | callback && callback(err);
244 | });
245 | },
246 |
247 | setPriority: function(newPriority) {
248 | this._priChanged(newPriority);
249 | },
250 |
251 | name: function() {
252 | return this.myName;
253 | },
254 |
255 | ref: function() {
256 | return this;
257 | },
258 |
259 | parent: function() {
260 | return this.parent? this.parent : this;
261 | },
262 |
263 | root: function() {
264 | var next = this;
265 | while(next.parent()) {
266 | next = next.parent();
267 | }
268 | return next;
269 | },
270 |
271 | push: function(data) {
272 | var id = 'mock'+(++PUSH_COUNTER);
273 | var child = this.child(id);
274 | if( data ) {
275 | child.set(data);
276 | }
277 | return child;
278 | },
279 |
280 | once: function(event, callback) {
281 | function fn(snap) {
282 | this.off(event, fn);
283 | callback(snap);
284 | }
285 | this.on(event, fn);
286 | },
287 |
288 | remove: function() {
289 | this._dataChanged(null);
290 | },
291 |
292 | on: function(event, callback) { //todo cancelCallback?
293 | this._events[event].push(callback);
294 | var data = this.getData(), self = this, pri = this.priority;
295 | if( event === 'value' ) {
296 | this._defer(function() {
297 | callback(makeSnap(self, data, pri));
298 | });
299 | }
300 | else if( event === 'child_added' ) {
301 | this._defer(function() {
302 | var prev = null;
303 | _.each(data, function(v, k) {
304 | callback(makeSnap(self.child(k), v, pri), prev);
305 | prev = k;
306 | });
307 | });
308 | }
309 | },
310 |
311 | off: function(event, callback) {
312 | if( !event ) {
313 | for (var key in this._events)
314 | if( this._events.hasOwnProperty(key) )
315 | this.off(key);
316 | }
317 | else if( callback ) {
318 | this._events[event] = _.without(this._events[event], callback);
319 | }
320 | else {
321 | this._events[event] = [];
322 | }
323 | },
324 |
325 | transaction: function(valueFn, finishedFn, applyLocally) {
326 | var valueSpy = sinon.spy(valueFn);
327 | var finishedSpy = sinon.spy(finishedFn);
328 | this._defer(function() {
329 | var err = this._nextErr('transaction');
330 | // unlike most defer methods, this will use the value as it exists at the time
331 | // the transaction is actually invoked, which is the eventual consistent value
332 | // it would have in reality
333 | var res = valueSpy(this.getData());
334 | var newData = _.isUndefined(res) || err? this.getData() : res;
335 | finishedSpy(err, err === null && !_.isUndefined(res), makeSnap(this, newData, this.priority));
336 | this._dataChanged(newData);
337 | });
338 | return [valueSpy, finishedSpy, applyLocally];
339 | },
340 |
341 | /**
342 | * If token is valid and parses, returns the contents of token as exected. If not, the error is returned.
343 | * Does not change behavior in any way (since we don't really auth anywhere)
344 | *
345 | * @param {String} token
346 | * @param {Function} [callback]
347 | */
348 | auth: function(token, callback) {
349 | //todo invoke callback with the parsed token contents
350 | callback && this._defer(callback);
351 | },
352 |
353 | /**
354 | * Just a stub at this point.
355 | * @param {int} limit
356 | */
357 | limit: function(limit) {
358 | this._queryProps.limit = limit;
359 | //todo
360 | },
361 |
362 | startAt: function(priority, recordId) {
363 | this._queryProps.startAt = [priority, recordId];
364 | //todo
365 | },
366 |
367 | endAt: function(priority, recordId) {
368 | this._queryProps.endAt = [priority, recordId];
369 | //todo
370 | },
371 |
372 | /*****************************************************
373 | * Private/internal methods
374 | *****************************************************/
375 |
376 | _childChanged: function(ref) {
377 | var data = ref.getData(), pri = ref.priority;
378 | if( !_.isObject(this.data) && data !== null ) { this.data = {}; }
379 | var exists = this.data.hasOwnProperty(ref.name());
380 | DEBUG && console.log('_childChanged', this.toString() + ' -> ' + ref.name(), data); //debug
381 | if( data === null && exists ) {
382 | delete this.data[ref.name()];
383 | this._trigger('child_removed', data, pri, ref.name());
384 | this._trigger('value', this.data, this.priority);
385 | }
386 | else if( data !== null ) {
387 | this.data[ref.name()] = _.cloneDeep(data);
388 | this._trigger(exists? 'child_changed' : 'child_added', data, pri, ref.name());
389 | this._trigger('value', this.data, this.priority);
390 | this.parent && this.parent._childChanged(this);
391 | }
392 | },
393 |
394 | _dataChanged: function(data) {
395 | var self = this;
396 | if(_.isObject(data) && _.has(data, '.priority')) {
397 | this._priChanged(data['.priority']);
398 | delete data['.priority'];
399 | }
400 | if( !_.isEqual(data, this.data) ) {
401 | this.data = _.cloneDeep(data);
402 | DEBUG && console.log('_dataChanged', this.toString(), data); //debug
403 | this._trigger('value', this.data, this.priority);
404 | if(this.children.length) {
405 | this._resort();
406 | _.each(this.children, function(child) {
407 | child._dataChanged(self._childData(child.name()));
408 | });
409 | }
410 | if( this.parent && _.isObject(this.parent.data) ) {
411 | this.parent._childChanged(this);
412 | }
413 | }
414 | },
415 |
416 | _priChanged: function(newPriority) {
417 | DEBUG && console.log('_priChanged', this.toString(), newPriority); //debug
418 | this.priority = newPriority;
419 | if( this.parent ) {
420 | this.parent._resort(this.name());
421 | }
422 | },
423 |
424 | _resort: function(childKeyMoved) {
425 | this.children.sort(childComparator);
426 | if( !_.isUndefined(childKeyMoved) ) {
427 | var child = this.child(childKeyMoved);
428 | this._trigger('child_moved', child.getData(), child.priority, childKeyMoved);
429 | }
430 | },
431 |
432 | _defer: function(fn) {
433 | //todo should probably be taking some sort of snapshot of my data here and passing
434 | //todo that into `fn` for reference
435 | this.ops.push(Array.prototype.slice.call(arguments, 0));
436 | if( this.flushDelay !== false ) { this.flush(this.flushDelay); }
437 | },
438 |
439 | _trigger: function(event, data, pri, key) {
440 | var self = this, ref = event==='value'? self : self.child(key);
441 | var snap = makeSnap(ref, data, pri);
442 | _.each(self._events[event], function(fn) {
443 | if(_.contains(['child_added', 'child_moved'], event)) {
444 | fn(snap, self._getPrevChild(key, pri));
445 | }
446 | else {
447 | //todo allow scope by changing fn to an array? for use with on() and once() which accept scope?
448 | fn(snap);
449 | }
450 | });
451 | },
452 |
453 | _nextErr: function(type) {
454 | var err = this.errs[type];
455 | delete this.errs[type];
456 | return err||null;
457 | },
458 |
459 | _childData: function(key) {
460 | return _.isObject(this.data) && _.has(this.data, key)? this.data[key] : null;
461 | },
462 |
463 | _getPrevChild: function(key, pri) {
464 | function keysMatch(c) { return c.name() === key }
465 | var recs = this.children;
466 | var i = _.findIndex(recs, keysMatch);
467 | if( i === -1 ) {
468 | recs = this.children.slice();
469 | child = {name: function() { return key; }, priority: pri===undefined? null : pri };
470 | recs.push(child);
471 | recs.sort(childComparator);
472 | i = _.findIndex(recs, keysMatch);
473 | }
474 | return i > 0? i : null;
475 | }
476 | };
477 |
478 |
479 | /*******************************************************************************
480 | * SIMPLE LOGIN
481 | ******************************************************************************/
482 | function MockFirebaseSimpleLogin(ref, callback, resultData) {
483 | // allows test units to monitor the callback function to make sure
484 | // it is invoked (even if one is not declared)
485 | this.callback = sinon.spy(callback||function() {});
486 | this.attempts = [];
487 | this.failMethod = MockFirebaseSimpleLogin.DEFAULT_FAIL_WHEN;
488 | this.ref = ref; // we don't use ref for anything
489 | this.autoFlushTime = MockFirebaseSimpleLogin.DEFAULT_AUTO_FLUSH;
490 | this.resultData = _.cloneDeep(MockFirebaseSimpleLogin.DEFAULT_RESULT_DATA);
491 | resultData && _.assign(this.resultData, resultData);
492 | }
493 |
494 | MockFirebaseSimpleLogin.prototype = {
495 |
496 | /*****************************************************
497 | * Test Unit Methods
498 | *****************************************************/
499 |
500 | /**
501 | * When this method is called, any outstanding login()
502 | * attempts will be immediately resolved. If this method
503 | * is called with an integer value, then the login attempt
504 | * will resolve asynchronously after that many milliseconds.
505 | *
506 | * @param {int|boolean} [milliseconds]
507 | * @returns {MockFirebaseSimpleLogin}
508 | */
509 | flush: function(milliseconds) {
510 | var self = this;
511 | if(_.isNumber(milliseconds) ) {
512 | setTimeout(self.flush.bind(self), milliseconds);
513 | }
514 | else {
515 | var attempts = self.attempts;
516 | self.attempts = [];
517 | _.each(attempts, function(x) {
518 | x[0].apply(self, x.slice(1));
519 | });
520 | }
521 | return self;
522 | },
523 |
524 | /**
525 | * Automatically queue the flush() event
526 | * each time login() is called. If this method
527 | * is called with `true`, then the callback
528 | * is invoked synchronously.
529 | *
530 | * If this method is called with an integer,
531 | * the callback is triggered asynchronously
532 | * after that many milliseconds.
533 | *
534 | * If this method is called with false, then
535 | * autoFlush() is disabled.
536 | *
537 | * @param {int|boolean} [milliseconds]
538 | * @returns {MockFirebaseSimpleLogin}
539 | */
540 | autoFlush: function(milliseconds) {
541 | this.autoFlushTime = milliseconds;
542 | if( this.autoFlushTime !== false ) {
543 | this.flush(this.autoFlushTime);
544 | }
545 | return this;
546 | },
547 |
548 | /**
549 | * `testMethod` is passed the {string}provider, {object}options, {object}user
550 | * for each call to login(). If it returns anything other than
551 | * null, then that is passed as the error message to the
552 | * callback and the login call fails.
553 | *
554 | *
555 | * // this is a simplified example of the default implementation (MockFirebaseSimpleLogin.DEFAULT_FAIL_WHEN)
556 | * auth.failWhen(function(provider, options, user) {
557 | * if( user.email !== options.email ) {
558 | * return MockFirebaseSimpleLogin.createError('INVALID_USER');
559 | * }
560 | * else if( user.password !== options.password ) {
561 | * return MockFirebaseSimpleLogin.createError('INVALID_PASSWORD');
562 | * }
563 | * else {
564 | * return null;
565 | * }
566 | * });
567 | *
568 | *
569 | * Multiple calls to this method replace the old failWhen criteria.
570 | *
571 | * @param testMethod
572 | * @returns {MockFirebaseSimpleLogin}
573 | */
574 | failWhen: function(testMethod) {
575 | this.failMethod = testMethod;
576 | return this;
577 | },
578 |
579 | /**
580 | * Retrieves a user account from the mock user data on this object
581 | *
582 | * @param provider
583 | * @param options
584 | */
585 | getUser: function(provider, options) {
586 | var data = this.resultData[provider];
587 | if( provider === 'password' ) {
588 | data = (data||{})[options.email];
589 | }
590 | return data||{};
591 | },
592 |
593 | /*****************************************************
594 | * Public API
595 | *****************************************************/
596 | login: function(provider, options) {
597 | var err = this.failMethod(provider, options||{}, this.getUser(provider, options));
598 | this._notify(err, err===null? this.resultData[provider]: null);
599 | },
600 |
601 | logout: function() {
602 | this._notify(null, null);
603 | },
604 |
605 | createUser: function(email, password, callback) {
606 | callback || (callback = _.noop);
607 | this._defer(function() {
608 | var user = this.resultData['password'][email] = createEmailUser(email, password);
609 | this.callback(null, user);
610 | });
611 | },
612 |
613 | changePassword: function(email, oldPassword, newPassword, callback) {
614 | callback || (callback = _.noop);
615 | this._defer(function() {
616 | var user = this.getUser('password', {email: email});
617 | if( !user ) {
618 | callback(MockFirebaseSimpleLogin.createError('INVALID_USER'), false);
619 | }
620 | else if( oldPassword !== user.password ) {
621 | callback(MockFirebaseSimpleLogin.createError('INVALID_PASSWORD'), false);
622 | }
623 | else {
624 | user.password = newPassword;
625 | callback(null, true);
626 | }
627 | });
628 | },
629 |
630 | sendPasswordResetEmail: function(email, callback) {
631 | callback || (callback = _.noop);
632 | this._defer(function() {
633 | var user = this.getUser('password', {email: email});
634 | if( user ) {
635 | callback(null, true);
636 | }
637 | else {
638 | callback(MockFirebaseSimpleLogin.createError('INVALID_USER'), false);
639 | }
640 | });
641 | },
642 |
643 | removeUser: function(email, password, callback) {
644 | callback || (callback = _.noop);
645 | this._defer(function() {
646 | var user = this.getUser('password', {email: email});
647 | if( !user ) {
648 | callback(MockFirebaseSimpleLogin.createError('INVALID_USER'), false);
649 | }
650 | else if( user.password !== password ) {
651 | callback(MockFirebaseSimpleLogin.createError('INVALID_PASSWORD'), false);
652 | }
653 | else {
654 | delete this.resultData['password'][email];
655 | callback(null, true);
656 | }
657 | });
658 | },
659 |
660 | /*****************************************************
661 | * Private/internal methods
662 | *****************************************************/
663 | _notify: function(error, user) {
664 | this._defer(this.callback, error, user);
665 | },
666 |
667 | _defer: function() {
668 | var args = _.toArray(arguments);
669 | this.attempts.push(args);
670 | if( this.autoFlushTime !== false ) {
671 | this.flush(this.autoFlushTime);
672 | }
673 | }
674 | };
675 |
676 | /*** UTIL FUNCTIONS ***/
677 | var USER_COUNT = 100;
678 | function createEmailUser(email, password) {
679 | var id = USER_COUNT++;
680 | return {
681 | uid: 'password:'+id,
682 | id: id,
683 | email: email,
684 | password: password,
685 | provider: 'password',
686 | md5_hash: MD5(email),
687 | firebaseAuthToken: 'FIREBASE_AUTH_TOKEN' //todo
688 | };
689 | }
690 |
691 | function createDefaultUser(provider, i) {
692 | var id = USER_COUNT++;
693 |
694 | var out = {
695 | uid: provider+':'+id,
696 | id: id,
697 | password: id,
698 | provider: provider,
699 | firebaseAuthToken: 'FIREBASE_AUTH_TOKEN' //todo
700 | };
701 | switch(provider) {
702 | case 'password':
703 | out.email = 'email@firebase.com';
704 | out.md5_hash = MD5(out.email);
705 | break;
706 | case 'persona':
707 | out.email = 'email@firebase.com';
708 | out.md5_hash = MD5(out.email);
709 | break;
710 | case 'twitter':
711 | out.accessToken = 'ACCESS_TOKEN'; //todo
712 | out.accessTokenSecret = 'ACCESS_TOKEN_SECRET'; //todo
713 | out.displayName = 'DISPLAY_NAME';
714 | out.thirdPartyUserData = {}; //todo
715 | out.username = 'USERNAME';
716 | break;
717 | case 'google':
718 | out.accessToken = 'ACCESS_TOKEN'; //todo
719 | out.displayName = 'DISPLAY_NAME';
720 | out.email = 'email@firebase.com';
721 | out.thirdPartyUserData = {}; //todo
722 | break;
723 | case 'github':
724 | out.accessToken = 'ACCESS_TOKEN'; //todo
725 | out.displayName = 'DISPLAY_NAME';
726 | out.thirdPartyUserData = {}; //todo
727 | out.username = 'USERNAME';
728 | break;
729 | case 'facebook':
730 | out.accessToken = 'ACCESS_TOKEN'; //todo
731 | out.displayName = 'DISPLAY_NAME';
732 | out.thirdPartyUserData = {}; //todo
733 | break;
734 | case 'anonymous':
735 | break;
736 | default:
737 | throw new Error('Invalid auth provider', provider);
738 | }
739 |
740 | return out;
741 | }
742 |
743 | function ref(path, autoSyncDelay) {
744 | var ref = new MockFirebase();
745 | ref.flushDelay = _.isUndefined(autoSyncDelay)? true : autoSyncDelay;
746 | if( path ) { ref = ref.child(path); }
747 | return ref;
748 | }
749 |
750 | function mergePaths(base, add) {
751 | return base.replace(/\/$/, '')+'/'+add.replace(/^\//, '');
752 | }
753 |
754 | function makeSnap(ref, data, pri) {
755 | data = _.cloneDeep(data);
756 | return {
757 | val: function() { return data; },
758 | ref: function() { return ref; },
759 | name: function() { return ref.name() },
760 | getPriority: function() { return pri; }, //todo
761 | forEach: function(cb, scope) {
762 | _.each(data, function(v, k, list) {
763 | var child = ref.child(k);
764 | //todo the priority here is inaccurate if child pri modified
765 | //todo between calling makeSnap and forEach() on that snap
766 | var res = cb.call(scope, makeSnap(child, v, child.priority));
767 | return !(res === true);
768 | });
769 | }
770 | }
771 | }
772 |
773 | function extractName(path) {
774 | return ((path || '').match(/\/([^.$\[\]#\/]+)$/)||[null, null])[1];
775 | }
776 |
777 | function childComparator(a, b) {
778 | if(a.priority === b.priority) {
779 | var key1 = a.name(), key2 = b.name();
780 | return ( ( key1 == key2 ) ? 0 : ( ( key1 > key2 ) ? 1 : -1 ) );
781 | }
782 | else {
783 | return a.priority < b.priority? -1 : 1;
784 | }
785 | }
786 |
787 | // a polyfill for window.atob to allow JWT token parsing
788 | // credits: https://github.com/davidchambers/Base64.js
789 | ;(function (object) {
790 | var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
791 |
792 | function InvalidCharacterError(message) {
793 | this.message = message;
794 | }
795 | InvalidCharacterError.prototype = new Error;
796 | InvalidCharacterError.prototype.name = 'InvalidCharacterError';
797 |
798 | // encoder
799 | // [https://gist.github.com/999166] by [https://github.com/nignag]
800 | object.btoa || (
801 | object.btoa = function (input) {
802 | for (
803 | // initialize result and counter
804 | var block, charCode, idx = 0, map = chars, output = '';
805 | // if the next input index does not exist:
806 | // change the mapping table to "="
807 | // check if d has no fractional digits
808 | input.charAt(idx | 0) || (map = '=', idx % 1);
809 | // "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8
810 | output += map.charAt(63 & block >> 8 - idx % 1 * 8)
811 | ) {
812 | charCode = input.charCodeAt(idx += 3/4);
813 | if (charCode > 0xFF) {
814 | throw new InvalidCharacterError("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
815 | }
816 | block = block << 8 | charCode;
817 | }
818 | return output;
819 | });
820 |
821 | // decoder
822 | // [https://gist.github.com/1020396] by [https://github.com/atk]
823 | object.atob || (
824 | object.atob = function (input) {
825 | input = input.replace(/=+$/, '')
826 | if (input.length % 4 == 1) {
827 | throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.");
828 | }
829 | for (
830 | // initialize result and counters
831 | var bc = 0, bs, buffer, idx = 0, output = '';
832 | // get next character
833 | buffer = input.charAt(idx++);
834 | // character found in table? initialize bit storage and add its ascii value;
835 | ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
836 | // and if not first of each 4 characters,
837 | // convert the first 8 bits to one ascii character
838 | bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
839 | ) {
840 | // try to find character in table (0-63, not found => -1)
841 | buffer = chars.indexOf(buffer);
842 | }
843 | return output;
844 | });
845 |
846 | }(exports));
847 |
848 | // MD5 (Message-Digest Algorithm) by WebToolkit
849 | //
850 |
851 | var MD5=function(s){function L(k,d){return(k<>>(32-d))}function K(G,k){var I,d,F,H,x;F=(G&2147483648);H=(k&2147483648);I=(G&1073741824);d=(k&1073741824);x=(G&1073741823)+(k&1073741823);if(I&d){return(x^2147483648^F^H)}if(I|d){if(x&1073741824){return(x^3221225472^F^H)}else{return(x^1073741824^F^H)}}else{return(x^F^H)}}function r(d,F,k){return(d&F)|((~d)&k)}function q(d,F,k){return(d&k)|(F&(~k))}function p(d,F,k){return(d^F^k)}function n(d,F,k){return(F^(d|(~k)))}function u(G,F,aa,Z,k,H,I){G=K(G,K(K(r(F,aa,Z),k),I));return K(L(G,H),F)}function f(G,F,aa,Z,k,H,I){G=K(G,K(K(q(F,aa,Z),k),I));return K(L(G,H),F)}function D(G,F,aa,Z,k,H,I){G=K(G,K(K(p(F,aa,Z),k),I));return K(L(G,H),F)}function t(G,F,aa,Z,k,H,I){G=K(G,K(K(n(F,aa,Z),k),I));return K(L(G,H),F)}function e(G){var Z;var F=G.length;var x=F+8;var k=(x-(x%64))/64;var I=(k+1)*16;var aa=Array(I-1);var d=0;var H=0;while(H>>29;return aa}function B(x){var k="",F="",G,d;for(d=0;d<=3;d++){G=(x>>>(d*8))&255;F="0"+G.toString(16);k=k+F.substr(F.length-2,2)}return k}function J(k){k=k.replace(/rn/g,"n");var d="";for(var F=0;F127)&&(x<2048)){d+=String.fromCharCode((x>>6)|192);d+=String.fromCharCode((x&63)|128)}else{d+=String.fromCharCode((x>>12)|224);d+=String.fromCharCode(((x>>6)&63)|128);d+=String.fromCharCode((x&63)|128)}}}return d}var C=Array();var P,h,E,v,g,Y,X,W,V;var S=7,Q=12,N=17,M=22;var A=5,z=9,y=14,w=20;var o=4,m=11,l=16,j=23;var U=6,T=10,R=15,O=21;s=J(s);C=e(s);Y=1732584193;X=4023233417;W=2562383102;V=271733878;for(P=0;P