From 14ac2be8c418f5649315b7e6eb45491b77de9258 Mon Sep 17 00:00:00 2001 From: Firebase Operations Date: Tue, 7 Oct 2014 16:45:25 +0000 Subject: [PATCH] [firebase-release] Updated EmberFire to 1.2.6 --- README.md | 2 +- bower.json | 2 +- dist/emberfire.js | 707 ++++++++++++++++++ dist/emberfire.min.js | 10 + lib/ember-addon/blueprints/emberfire/index.js | 2 +- package.json | 2 +- 6 files changed, 721 insertions(+), 4 deletions(-) create mode 100644 dist/emberfire.js create mode 100644 dist/emberfire.min.js diff --git a/README.md b/README.md index 5845c37e..4255d9cb 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ In order to use EmberFire in your project, you need to include the following fil - + ``` Use the URL above to download both the minified and non-minified versions of EmberFire from the diff --git a/bower.json b/bower.json index 5809edae..29ee70d7 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "emberfire", "description": "The officially supported Ember binding for Firebase", - "version": "0.0.0", + "version": "1.2.6", "authors": [ "Firebase (https://www.firebase.com/)" ], diff --git a/dist/emberfire.js b/dist/emberfire.js new file mode 100644 index 00000000..9efe6777 --- /dev/null +++ b/dist/emberfire.js @@ -0,0 +1,707 @@ +/*! + * EmberFire is the officially supported adapter for using Firebase with + * Ember Data. The DS.FirebaseAdapter provides all of the standard DS.Adapter + * methods and will automatically synchronize the store with Firebase. + * + * EmberFire 1.2.6 + * https://github.com/firebase/emberfire/ + * License: MIT + */ +(function() { + "use strict"; + + /* Only enable if Ember Data is included */ + if (window.DS === undefined) { + return; + } + + var EmberFire = Ember.Namespace.create({ + VERSION: '1.2.6' + }); + + if (Ember.libraries) { + Ember.libraries.registerCoreLibrary('EmberFire', EmberFire.VERSION); + } + + //Monkeypatch the store until ED gives us a good way to listen to push events + DS.Store.reopen({ + push: function(typeName, data, _partial) { + var record = this._super(typeName, data, _partial); + var adapter = this.adapterFor(record.constructor); + if (adapter.recordWasPushed) { + adapter.recordWasPushed(this, typeName, record); + } + return record; + }, + + recordWillUnload: function(record) { + var adapter = this.adapterFor(record.constructor); + if (adapter.recordWillUnload) { + adapter.recordWillUnload(this, record); + } + } + }); + DS.Model.reopen({ + unloadRecord: function() { + this.store.recordWillUnload(this); + return this._super(); + } + }); + // Shortcuts + var Promise = Ember.RSVP.Promise; + var map = Ember.EnumerableUtils.map; + var forEach = Ember.EnumerableUtils.forEach; + var fmt = Ember.String.fmt; + + + var toPromise = function(fn, context, _args, errorMsg) { + var args = _args || []; + return new Promise(function(resolve, reject) { + var callback = function(error) { + if (error) { + if (errorMsg && typeof error === 'object') { + error.location = errorMsg; + } + reject(error); + } else { + resolve(); + } + }; + args.push(callback); + fn.apply(context, args); + }); +}; + + /** + The Firebase serializer helps normalize relationships and can be extended on + a per model basis. + */ + DS.FirebaseSerializer = DS.JSONSerializer.extend(Ember.Evented, { + + //We need to account for Firebase turning key/value pairs with ids '1' and '0' into arrays + //See https://github.com/firebase/emberfire/issues/124 + _normalizeNumberIDs: function(hash, key) { + var newHash = []; + if (hash[key][0] === true) { + newHash.push('0'); + } + if (hash[key][1] === true) { + newHash.push('1'); + } + hash[key] = newHash; + }, + + normalizeHasMany: function(type, hash, relationship) { + var key = relationship.key; + if (typeof hash[key] === 'object' && !Ember.isArray(hash[key])) { + hash[key] = Ember.keys(hash[key]); + } + //We need to account for Firebase turning key/value pairs with ids '1' and '0' into arrays + //See https://github.com/firebase/emberfire/issues/124 + else if (Ember.isArray(hash[key]) && hash[key].length < 3 && (hash[key][0] === true || hash[key][1] === true)) { + this._normalizeNumberIDs(hash, key); + } + else if (Ember.isArray(hash[key])) { + throw new Error(fmt('%@ relationship %@(\'%@\') must be a key/value map in Firebase. Example: { "%@": { "%@_id": true } }', [type.toString(), relationship.kind, relationship.type.typeKey, key, relationship.type.typeKey])); + } + }, + + normalizeEmbeddedHasMany: function(type, hash, relationship) { + var key = relationship.key; + var embeddedRecordPayload = hash[key]; + var embeddedKey; + if (!hash[key]) { + return; + } + for (embeddedKey in embeddedRecordPayload) { + var record = embeddedRecordPayload[embeddedKey]; + if (record !== null && typeof record === 'object') { + record.id = embeddedKey; + } + this.store.push(relationship.type, this.normalize(relationship.type, record)); + } + hash[key] = Ember.keys(hash[key]); + }, + + normalizeEmbeddedBelongsTo: function(type, hash, relationship) { + var key = relationship.key; + if (!hash[key]) { + return; + } + var embeddedRecordPayload = hash[key]; + if (typeof embeddedRecordPayload.id !== 'string') { + throw new Error(fmt('Embedded relationship "%@" of "%@" must contain an "id" property in the payload', [relationship.type.typeKey, type])); + } + this.store.push(relationship.type, this.normalize(relationship.type, embeddedRecordPayload)); + hash[key] = embeddedRecordPayload.id; + }, + + normalizeBelongsTo: Ember.K, + /** + Called after `extractSingle()`. This method checks the model + for `hasMany` relationships and makes sure the value is an object. + The object is then converted to an Array using `Ember.keys` + */ + normalize: function(type, hash) { + var serializer = this; + // Check if the model contains any 'hasMany' relationships + type.eachRelationship(function(key, relationship) { + if (relationship.kind === 'hasMany') { + if (relationship.options.embedded) { + serializer.normalizeEmbeddedHasMany(type, hash, relationship); + } else { + serializer.normalizeHasMany(type, hash, relationship); + } + } else { + if (relationship.options.embedded) { + serializer.normalizeEmbeddedBelongsTo(type, hash, relationship); + } else { + serializer.normalizeBelongsTo(type, hash, relationship); + } + } + }); + return this._super.apply(this, arguments); + }, + + /** + Called on a records returned from `find()` and all records + returned from `findAll()` + + This method also checkes for `embedded: true`, extracts the + embedded records, pushes them into the store, and then replaces + the records with an array of ids + */ + extractSingle: function(store, type, payload) { + return this.normalize(type, payload); + }, + + /** + Called after the adpter runs `findAll()` or `findMany()`. This method runs + `extractSingle()` on each item in the payload and as a result each item + will have `normalize()` called on it + */ + extractArray: function(store, type, payload) { + return map(payload, function(item) { + return this.extractSingle(store, type, item); + }, this); + }, + + /** + Overrides ember-data's `serializeHasMany` to serialize oneToMany + relationships. + */ + serializeHasMany: function(record, json, relationship) { + var key = relationship.key; + var payloadKey = this.keyForRelationship ? this.keyForRelationship(key, "hasMany") : key; + json[payloadKey] = Ember.A(record.get(key)).mapBy('id'); + }, + + serializeBelongsTo: function(record, json, relationship) { + this._super(record, json, relationship); + var key = relationship.key; + var payloadKey = this.keyForRelationship ? this.keyForRelationship(key, "belongsTo") : relationship.key; + if (typeof json[key] === "undefined" || json[key] === '') { + delete json[key]; + } + } + + }); + + /** + The Firebase adapter allows your store to communicate with the Firebase + realtime service. To use the adapter in your app, extend DS.FirebaseAdapter + and customize the endpoint to point to the Firebase URL where you want this + data to be stored. + + The adapter will automatically communicate with Firebase to persist your + records as neccessary. Importantly, the adapter will also update the store + in realtime when changes are made to the Firebase by other clients or + otherwise. + */ + DS.FirebaseAdapter = DS.Adapter.extend(Ember.Evented, { + + defaultSerializer: '-firebase', + + /** + Endpoint paths can be customized by setting the Firebase property on the + adapter: + + ```js + DS.FirebaseAdapter.extend({ + firebase: new Firebase('https://.firebaseio.com/') + }); + ``` + + Requests for `App.Post` now target `https://.firebaseio.com/posts`. + + @property firebase + @type {Firebase} + */ + + init: function() { + if (!this.firebase || typeof this.firebase !== 'object') { + throw new Error('Please set the `firebase` property on the adapter.'); + } + // If provided Firebase reference was a query (eg: limits), make it a ref. + this._ref = this.firebase.ref(); + // Keep track of what types `.findAll()` has been called for + this._findAllMapForType = {}; + // Keep a cache to check modified relationships against + this._recordCacheForType = {}; + // Used to batch records into the store + this._queue = []; + }, + + /** + Uses push() to generate chronologically ordered unique IDs. + */ + generateIdForRecord: function() { + return this._ref.push().name(); + }, + + /** + Use the Firebase snapshot.name() as the record id + + @param {Object} snapshot - A Firebase snapshot + @param {Object} payload - The payload that will be pushed into the store + @return {Object} payload + */ + _assignIdToPayload: function(snapshot) { + var payload = snapshot.val(); + if (payload !== null && typeof payload === 'object' && typeof payload.id === 'undefined') { + payload.id = snapshot.name(); + } + return payload; + }, + + /** + Called by the store to retrieve the JSON for a given type and ID. The + method will return a promise which will resolve when the value is + successfully fetched from Firebase. + + Additionally, from this point on, the object's value in the store will + also be automatically updated whenever the remote value changes. + */ + find: function(store, type, id) { + var adapter = this; + var ref = this._getRef(type, id); + var serializer = store.serializerFor(type); + + return new Promise(function(resolve, reject) { + ref.once('value', function(snapshot) { + var payload = adapter._assignIdToPayload(snapshot); + adapter._updateRecordCacheForType(type, payload); + if (payload === null) { + var error = new Error(fmt('no record was found at %@', [ref.toString()])); + error.recordId = id; + reject(error); + } + else { + resolve(payload); + } + }, + function(err) { + reject(err); + }); + }, fmt('DS: FirebaseAdapter#find %@ to %@', [type, ref.toString()])); + }, + + recordWasPushed: function(store, type, record) { + if (!record.__listening) { + this.listenForChanges(store, type, record); + } + }, + + recordWillUnload: function(store, record) { + var ref = this._getRef(record.typeKey, record.get('id')); + ref.off('value'); + }, + + listenForChanges: function(store, type, record) { + record.__listening = true; + var serializer = store.serializerFor(type); + var adapter = this; + var ref = this._getRef(type, record.get('id')); + var called = false; + ref.on('value', function(snapshot) { + if (called) { + adapter._handleChildValue(store, type, serializer, snapshot); + } + called = true; + }); + }, + + /** + findMany + */ + findMany: undefined, + + /** + Called by the store to retrieve the JSON for all of the records for a + given type. The method will return a promise which will resolve when the + value is successfully fetched from Firebase. + + Additionally, from this point on, any records of this type that are added, + removed or modified from Firebase will automatically be reflected in the + store. + */ + findAll: function(store, type) { + var adapter = this; + var ref = this._getRef(type); + + return new Promise(function(resolve, reject) { + // Listen for child events on the type + ref.once('value', function(snapshot) { + if (!adapter._findAllHasEventsForType(type)) { + adapter._findAllAddEventListeners(store, type, ref); + } + var results = []; + snapshot.forEach(function(childSnapshot) { + var payload = adapter._assignIdToPayload(childSnapshot); + adapter._updateRecordCacheForType(type, payload); + results.push(payload); + }); + resolve(results); + }, function(error) { + reject(error); + }); + }, fmt('DS: FirebaseAdapter#findAll %@ to %@', [type, ref.toString()])); + }, + + /** + Keep track of what types `.findAll()` has been called for + so duplicate listeners aren't added + */ + _findAllMapForType: undefined, + + /** + Determine if the current type is already listening for children events + */ + _findAllHasEventsForType: function(type) { + return !Ember.isNone(this._findAllMapForType[type]); + }, + + /** + After `.findAll()` is called on a type, continue to listen for + `child_added`, `child_removed`, and `child_changed` + */ + _findAllAddEventListeners: function(store, type, ref) { + this._findAllMapForType[type] = true; + + var adapter = this; + var serializer = store.serializerFor(type); + + ref.on('child_added', function(snapshot) { + if (!store.hasRecordForId(type, snapshot.name())) { + adapter._handleChildValue(store, type, serializer, snapshot); + } + }); + }, + + /** + Push a new child record into the store + */ + _handleChildValue: function(store, type, serializer, snapshot) { + //No idea why we need this, we are alredy turning off the callback by + //calling ref.off in recordWillUnload. Something is fishy here + if (store.isDestroying) { + return; + } + var value = snapshot.val(); + if (value === null) { + var id = snapshot.name(); + var record = store.getById(type, id); + //TODO refactor using ED + if (!record.get('isDeleted')) { + record.deleteRecord(); + } + } else { + var payload = this._assignIdToPayload(snapshot); + this._enqueue(function() { + store.push(type, serializer.extractSingle(store, type, payload)); + }); + } + }, + + /** + `createRecord` is an alias for `updateRecord` because calling \ + `ref.set()` would wipe out any existing relationships + */ + createRecord: function(store, type, record) { + var adapter = this; + return this.updateRecord(store, type, record).then(function() { + adapter.listenForChanges(store, type, record); + }); + }, + + /** + Called by the store when a record is created/updated via the `save` + method on a model record instance. + + The `updateRecord` method serializes the record and performs an `update()` + at the the Firebase location and a `.set()` at any relationship locations + The method will return a promise which will be resolved when the data and + any relationships have been successfully saved to Firebase. + + We take an optional record reference, in order for this method to be usable + for saving nested records as well. + + */ + updateRecord: function(store, type, record, _recordRef) { + var adapter = this; + var recordRef = _recordRef || this._getRef(type, record.id); + var recordCache = Ember.get(adapter._recordCacheForType, fmt('%@.%@', [type.typeKey, record.get('id')])) || {}; + + var serializedRecord = record.serialize({includeId:false}); + + return new Promise(function(resolve, reject) { + var savedRelationships = Ember.A(); + record.eachRelationship(function(key, relationship) { + var save; + if (relationship.kind === 'hasMany') { + if (serializedRecord[key]) { + save = adapter._saveHasManyRelationship(store, type, relationship, serializedRecord[key], recordRef, recordCache); + savedRelationships.push(save); + // Remove the relationship from the serializedRecord because otherwise we would clobber the entire hasMany + delete serializedRecord[key]; + } + } else { + if (relationship.options.embedded === true && serializedRecord[key]) { + save = adapter._saveBelongsToRecord(store, type, relationship, serializedRecord[key], recordRef); + savedRelationships.push(save); + delete serializedRecord[key]; + } + } + }); + + var relationshipsPromise = Ember.RSVP.allSettled(savedRelationships); + var recordPromise = adapter._updateRecord(recordRef, serializedRecord); + + Ember.RSVP.hashSettled({relationships: relationshipsPromise, record: recordPromise}).then(function(promises) { + var rejected = Ember.A(promises.relationships.value).filterBy('state', 'rejected'); + if (promises.record.state === 'rejected') { + rejected.push(promises.record); + } + // Throw an error if any of the relationships failed to save + if (rejected.length !== 0) { + var error = new Error(fmt('Some errors were encountered while saving %@ %@', [type, record.id])); + error.errors = rejected.mapBy('reason'); + reject(error); + } else { + resolve(); + } + }); + }, fmt('DS: FirebaseAdapter#updateRecord %@ to %@', [type, recordRef.toString()])); + }, + + //Just update the record itself without caring for the relationships + _updateRecord: function(recordRef, serializedRecord) { + return toPromise(recordRef.update, recordRef, [serializedRecord]); + }, + + /** + Call _saveHasManyRelationshipRecord on each record in the relationship + and then resolve once they have all settled + */ + _saveHasManyRelationship: function(store, type, relationship, ids, recordRef, recordCache) { + if (!Ember.isArray(ids)) { + throw new Error('hasMany relationships must must be an array'); + } + var adapter = this; + var idsCache = Ember.A(recordCache[relationship.key]); + ids = Ember.A(ids); + var dirtyRecords = []; + + // Added + var addedRecords = ids.filter(function(id) { + return !idsCache.contains(id); + }); + + // Dirty + dirtyRecords = ids.filter(function(id) { + var type = relationship.type; + return store.hasRecordForId(type, id) && store.getById(type, id).get('isDirty') === true; + }); + dirtyRecords = Ember.A(dirtyRecords.concat(addedRecords)).uniq().map(function(id) { + return adapter._saveHasManyRecord(store, relationship, recordRef, id); + }); + // Removed + var removedRecords = idsCache.filter(function(id) { + return !ids.contains(id); + }); + removedRecords = Ember.A(removedRecords).map(function(id) { + return adapter._removeHasManyRecord(store, relationship, recordRef, id); + }); + // Combine all the saved records + var savedRecords = dirtyRecords.concat(removedRecords); + // Wait for all the updates to finish + return Ember.RSVP.allSettled(savedRecords).then(function(savedRecords) { + var rejected = Ember.A(Ember.A(savedRecords).filterBy('state', 'rejected')); + if (rejected.get('length') === 0) { + // Update the cache + recordCache[relationship.key] = ids; + return savedRecords; + } + else { + var error = new Error(fmt('Some errors were encountered while saving a hasMany relationship %@ -> %@', [relationship.parentType, relationship.type])); + error.errors = Ember.A(rejected).mapBy('reason'); + throw error; + } + }); + }, + + /** + If the relationship is `async: true`, create a child ref + named with the record id and set the value to true + + If the relationship is `embedded: true`, create a child ref + named with the record id and update the value to the serialized + version of the record + */ + _saveHasManyRecord: function(store, relationship, parentRef, id) { + var ref = this._getRelationshipRef(parentRef, relationship.key, id); + var record = store.getById(relationship.type, id); + var isEmbedded = relationship.options.embedded === true; + if (isEmbedded) { + return this.updateRecord(store, relationship.type, record, ref); + } + + return toPromise(ref.set, ref, [true]); + }, + + /** + Remove a relationship + */ + _removeHasManyRecord: function(store, relationship, parentRef, id) { + var ref = this._getRelationshipRef(parentRef, relationship.key, id); + return toPromise(ref.remove, ref, [], ref.toString()); + }, + + /** + Save an embedded record + */ + _saveBelongsToRecord: function(store, type, relationship, id, parentRef) { + var ref = parentRef.child(relationship.key); + var record = store.getById(relationship.type, id); + return this.updateRecord(store, relationship.type, record, ref); + }, + + /** + Called by the store when a record is deleted. + */ + deleteRecord: function(store, type, record) { + var ref = this._getRef(type, record.get('id')); + return toPromise(ref.remove, ref); + }, + + /** + Determines a path fo a given type + */ + pathForType: function(type) { + var camelized = Ember.String.camelize(type); + return Ember.String.pluralize(camelized); + }, + + /** + Return a Firebase reference for a given type and optional ID. + */ + _getRef: function(type, id) { + var ref = this._ref; + if (type) { + ref = ref.child(this.pathForType(type.typeKey)); + } + if (id) { + ref = ref.child(id); + } + return ref; + }, + + /** + Return a Firebase reference based on a relationship key and record id + */ + _getRelationshipRef: function(ref, key, id) { + return ref.child(key).child(id); + }, + + /** + The amount of time (ms) before the _queue is flushed + */ + _queueFlushDelay: (1000/60), // 60fps + + /** + Called after the first item is pushed into the _queue + */ + _queueScheduleFlush: function() { + Ember.run.later(this, this._queueFlush, this._queueFlushDelay); + }, + + /** + Call each function in the _queue and the reset the _queue + */ + _queueFlush: function() { + forEach(this._queue, function(queueItem) { + var fn = queueItem[0]; + var args = queueItem[1]; + fn.apply(null, args); + }); + this._queue.length = 0; + }, + + /** + Push a new function into the _queue and then schedule a + flush if the item is the first to be pushed + */ + _enqueue: function(callback, args) { + //Only do the queueing if we scheduled a delay + if (this._queueFlushDelay) { + var length = this._queue.push([callback, args]); + if (length === 1) { + this._queueScheduleFlush(); + } + } else { + callback.apply(null, args); + } + }, + + /** + A cache of hasMany relationships that can be used to + diff against new relationships when a model is saved + */ + _recordCacheForType: undefined, + + /** + _updateHasManyCacheForType + */ + _updateRecordCacheForType: function(type, payload) { + if (!payload) { return; } + var adapter = this; + var id = payload.id; + var cache = adapter._recordCacheForType; + var typeKey = type.typeKey; + // Only cache relationships for now + type.eachRelationship(function(key, relationship) { + if (relationship.kind === 'hasMany') { + var ids = payload[key]; + cache[typeKey] = cache[typeKey] || {}; + cache[typeKey][id] = cache[typeKey][id] || {}; + cache[typeKey][id][key] = !Ember.isNone(ids) ? Ember.A(Ember.keys(ids)) : Ember.A(); + } + }); + } + + }); + + /** + Register the serializer and adapter + */ + Ember.onLoad('Ember.Application', function(Application) { + Application.initializer({ + name: 'firebase', + initialize: function(container, application) { + application.register('adapter:-firebase', DS.FirebaseAdapter); + application.register('serializer:-firebase', DS.FirebaseSerializer); + } + }); + }); + +})(); diff --git a/dist/emberfire.min.js b/dist/emberfire.min.js new file mode 100644 index 00000000..9a6dbd55 --- /dev/null +++ b/dist/emberfire.min.js @@ -0,0 +1,10 @@ +/*! + * EmberFire is the officially supported adapter for using Firebase with + * Ember Data. The DS.FirebaseAdapter provides all of the standard DS.Adapter + * methods and will automatically synchronize the store with Firebase. + * + * EmberFire 1.2.6 + * https://github.com/firebase/emberfire/ + * License: MIT + */ +!function(){"use strict";if(void 0!==window.DS){var a=Ember.Namespace.create({VERSION:"1.2.6"});Ember.libraries&&Ember.libraries.registerCoreLibrary("EmberFire",a.VERSION),DS.Store.reopen({push:function(a,b,c){var d=this._super(a,b,c),e=this.adapterFor(d.constructor);return e.recordWasPushed&&e.recordWasPushed(this,a,d),d},recordWillUnload:function(a){var b=this.adapterFor(a.constructor);b.recordWillUnload&&b.recordWillUnload(this,a)}}),DS.Model.reopen({unloadRecord:function(){return this.store.recordWillUnload(this),this._super()}});var b=Ember.RSVP.Promise,c=Ember.EnumerableUtils.map,d=Ember.EnumerableUtils.forEach,e=Ember.String.fmt,f=function(a,c,d,e){var f=d||[];return new b(function(b,d){var g=function(a){a?(e&&"object"==typeof a&&(a.location=e),d(a)):b()};f.push(g),a.apply(c,f)})};DS.FirebaseSerializer=DS.JSONSerializer.extend(Ember.Evented,{_normalizeNumberIDs:function(a,b){var c=[];a[b][0]===!0&&c.push("0"),a[b][1]===!0&&c.push("1"),a[b]=c},normalizeHasMany:function(a,b,c){var d=c.key;if("object"!=typeof b[d]||Ember.isArray(b[d])){if(Ember.isArray(b[d])&&b[d].length<3&&(b[d][0]===!0||b[d][1]===!0))this._normalizeNumberIDs(b,d);else if(Ember.isArray(b[d]))throw new Error(e('%@ relationship %@(\'%@\') must be a key/value map in Firebase. Example: { "%@": { "%@_id": true } }',[a.toString(),c.kind,c.type.typeKey,d,c.type.typeKey]))}else b[d]=Ember.keys(b[d])},normalizeEmbeddedHasMany:function(a,b,c){var d,e=c.key,f=b[e];if(b[e]){for(d in f){var g=f[d];null!==g&&"object"==typeof g&&(g.id=d),this.store.push(c.type,this.normalize(c.type,g))}b[e]=Ember.keys(b[e])}},normalizeEmbeddedBelongsTo:function(a,b,c){var d=c.key;if(b[d]){var f=b[d];if("string"!=typeof f.id)throw new Error(e('Embedded relationship "%@" of "%@" must contain an "id" property in the payload',[c.type.typeKey,a]));this.store.push(c.type,this.normalize(c.type,f)),b[d]=f.id}},normalizeBelongsTo:Ember.K,normalize:function(a,b){var c=this;return a.eachRelationship(function(d,e){"hasMany"===e.kind?e.options.embedded?c.normalizeEmbeddedHasMany(a,b,e):c.normalizeHasMany(a,b,e):e.options.embedded?c.normalizeEmbeddedBelongsTo(a,b,e):c.normalizeBelongsTo(a,b,e)}),this._super.apply(this,arguments)},extractSingle:function(a,b,c){return this.normalize(b,c)},extractArray:function(a,b,d){return c(d,function(c){return this.extractSingle(a,b,c)},this)},serializeHasMany:function(a,b,c){var d=c.key,e=this.keyForRelationship?this.keyForRelationship(d,"hasMany"):d;b[e]=Ember.A(a.get(d)).mapBy("id")},serializeBelongsTo:function(a,b,c){this._super(a,b,c);{var d=c.key;this.keyForRelationship?this.keyForRelationship(d,"belongsTo"):c.key}("undefined"==typeof b[d]||""===b[d])&&delete b[d]}}),DS.FirebaseAdapter=DS.Adapter.extend(Ember.Evented,{defaultSerializer:"-firebase",init:function(){if(!this.firebase||"object"!=typeof this.firebase)throw new Error("Please set the `firebase` property on the adapter.");this._ref=this.firebase.ref(),this._findAllMapForType={},this._recordCacheForType={},this._queue=[]},generateIdForRecord:function(){return this._ref.push().name()},_assignIdToPayload:function(a){var b=a.val();return null!==b&&"object"==typeof b&&"undefined"==typeof b.id&&(b.id=a.name()),b},find:function(a,c,d){{var f=this,g=this._getRef(c,d);a.serializerFor(c)}return new b(function(a,b){g.once("value",function(h){var i=f._assignIdToPayload(h);if(f._updateRecordCacheForType(c,i),null===i){var j=new Error(e("no record was found at %@",[g.toString()]));j.recordId=d,b(j)}else a(i)},function(a){b(a)})},e("DS: FirebaseAdapter#find %@ to %@",[c,g.toString()]))},recordWasPushed:function(a,b,c){c.__listening||this.listenForChanges(a,b,c)},recordWillUnload:function(a,b){var c=this._getRef(b.typeKey,b.get("id"));c.off("value")},listenForChanges:function(a,b,c){c.__listening=!0;var d=a.serializerFor(b),e=this,f=this._getRef(b,c.get("id")),g=!1;f.on("value",function(c){g&&e._handleChildValue(a,b,d,c),g=!0})},findMany:void 0,findAll:function(a,c){var d=this,f=this._getRef(c);return new b(function(b,e){f.once("value",function(e){d._findAllHasEventsForType(c)||d._findAllAddEventListeners(a,c,f);var g=[];e.forEach(function(a){var b=d._assignIdToPayload(a);d._updateRecordCacheForType(c,b),g.push(b)}),b(g)},function(a){e(a)})},e("DS: FirebaseAdapter#findAll %@ to %@",[c,f.toString()]))},_findAllMapForType:void 0,_findAllHasEventsForType:function(a){return!Ember.isNone(this._findAllMapForType[a])},_findAllAddEventListeners:function(a,b,c){this._findAllMapForType[b]=!0;var d=this,e=a.serializerFor(b);c.on("child_added",function(c){a.hasRecordForId(b,c.name())||d._handleChildValue(a,b,e,c)})},_handleChildValue:function(a,b,c,d){if(!a.isDestroying){var e=d.val();if(null===e){var f=d.name(),g=a.getById(b,f);g.get("isDeleted")||g.deleteRecord()}else{var h=this._assignIdToPayload(d);this._enqueue(function(){a.push(b,c.extractSingle(a,b,h))})}}},createRecord:function(a,b,c){var d=this;return this.updateRecord(a,b,c).then(function(){d.listenForChanges(a,b,c)})},updateRecord:function(a,c,d,f){var g=this,h=f||this._getRef(c,d.id),i=Ember.get(g._recordCacheForType,e("%@.%@",[c.typeKey,d.get("id")]))||{},j=d.serialize({includeId:!1});return new b(function(b,f){var k=Ember.A();d.eachRelationship(function(b,d){var e;"hasMany"===d.kind?j[b]&&(e=g._saveHasManyRelationship(a,c,d,j[b],h,i),k.push(e),delete j[b]):d.options.embedded===!0&&j[b]&&(e=g._saveBelongsToRecord(a,c,d,j[b],h),k.push(e),delete j[b])});var l=Ember.RSVP.allSettled(k),m=g._updateRecord(h,j);Ember.RSVP.hashSettled({relationships:l,record:m}).then(function(a){var g=Ember.A(a.relationships.value).filterBy("state","rejected");if("rejected"===a.record.state&&g.push(a.record),0!==g.length){var h=new Error(e("Some errors were encountered while saving %@ %@",[c,d.id]));h.errors=g.mapBy("reason"),f(h)}else b()})},e("DS: FirebaseAdapter#updateRecord %@ to %@",[c,h.toString()]))},_updateRecord:function(a,b){return f(a.update,a,[b])},_saveHasManyRelationship:function(a,b,c,d,f,g){if(!Ember.isArray(d))throw new Error("hasMany relationships must must be an array");var h=this,i=Ember.A(g[c.key]);d=Ember.A(d);var j=[],k=d.filter(function(a){return!i.contains(a)});j=d.filter(function(b){var d=c.type;return a.hasRecordForId(d,b)&&a.getById(d,b).get("isDirty")===!0}),j=Ember.A(j.concat(k)).uniq().map(function(b){return h._saveHasManyRecord(a,c,f,b)});var l=i.filter(function(a){return!d.contains(a)});l=Ember.A(l).map(function(b){return h._removeHasManyRecord(a,c,f,b)});var m=j.concat(l);return Ember.RSVP.allSettled(m).then(function(a){var b=Ember.A(Ember.A(a).filterBy("state","rejected"));if(0===b.get("length"))return g[c.key]=d,a;var f=new Error(e("Some errors were encountered while saving a hasMany relationship %@ -> %@",[c.parentType,c.type]));throw f.errors=Ember.A(b).mapBy("reason"),f})},_saveHasManyRecord:function(a,b,c,d){var e=this._getRelationshipRef(c,b.key,d),g=a.getById(b.type,d),h=b.options.embedded===!0;return h?this.updateRecord(a,b.type,g,e):f(e.set,e,[!0])},_removeHasManyRecord:function(a,b,c,d){var e=this._getRelationshipRef(c,b.key,d);return f(e.remove,e,[],e.toString())},_saveBelongsToRecord:function(a,b,c,d,e){var f=e.child(c.key),g=a.getById(c.type,d);return this.updateRecord(a,c.type,g,f)},deleteRecord:function(a,b,c){var d=this._getRef(b,c.get("id"));return f(d.remove,d)},pathForType:function(a){var b=Ember.String.camelize(a);return Ember.String.pluralize(b)},_getRef:function(a,b){var c=this._ref;return a&&(c=c.child(this.pathForType(a.typeKey))),b&&(c=c.child(b)),c},_getRelationshipRef:function(a,b,c){return a.child(b).child(c)},_queueFlushDelay:1e3/60,_queueScheduleFlush:function(){Ember.run.later(this,this._queueFlush,this._queueFlushDelay)},_queueFlush:function(){d(this._queue,function(a){var b=a[0],c=a[1];b.apply(null,c)}),this._queue.length=0},_enqueue:function(a,b){if(this._queueFlushDelay){var c=this._queue.push([a,b]);1===c&&this._queueScheduleFlush()}else a.apply(null,b)},_recordCacheForType:void 0,_updateRecordCacheForType:function(a,b){if(b){var c=this,d=b.id,e=c._recordCacheForType,f=a.typeKey;a.eachRelationship(function(a,c){if("hasMany"===c.kind){var g=b[a];e[f]=e[f]||{},e[f][d]=e[f][d]||{},e[f][d][a]=Ember.isNone(g)?Ember.A():Ember.A(Ember.keys(g))}})}}}),Ember.onLoad("Ember.Application",function(a){a.initializer({name:"firebase",initialize:function(a,b){b.register("adapter:-firebase",DS.FirebaseAdapter),b.register("serializer:-firebase",DS.FirebaseSerializer)}})})}}(); \ No newline at end of file diff --git a/lib/ember-addon/blueprints/emberfire/index.js b/lib/ember-addon/blueprints/emberfire/index.js index c7f5ddab..116e8b57 100644 --- a/lib/ember-addon/blueprints/emberfire/index.js +++ b/lib/ember-addon/blueprints/emberfire/index.js @@ -8,6 +8,6 @@ module.exports = { }, afterInstall: function() { - return this.addBowerPackageToProject('emberfire', '~0.0.0'); + return this.addBowerPackageToProject('emberfire', '~1.2.6'); } }; diff --git a/package.json b/package.json index f05061f4..ce7c53bd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "emberfire", "description": "The officially supported Ember binding for Firebase", - "version": "0.0.0", + "version": "1.2.6", "author": "Firebase (https://www.firebase.com/)", "homepage": "https://github.com/firebase/emberfire/", "repository": {