diff options
Diffstat (limited to 'public/js/libs/ember-data.js')
-rw-r--r-- | public/js/libs/ember-data.js | 883 |
1 files changed, 767 insertions, 116 deletions
diff --git a/public/js/libs/ember-data.js b/public/js/libs/ember-data.js index 6db1c0f..00a6e25 100644 --- a/public/js/libs/ember-data.js +++ b/public/js/libs/ember-data.js @@ -1,7 +1,10 @@ +// Last commit: 5fd6d65 (2013-03-28 01:13:50 +0100) + + (function() { window.DS = Ember.Namespace.create({ - // this one goes to 11 - CURRENT_API_REVISION: 11 + // this one goes past 11 + CURRENT_API_REVISION: 12 }); })(); @@ -746,7 +749,7 @@ var classify = Ember.String.classify, get = Ember.get; For example, the DS.Adapter class can behave like a map, with more semantic API, via the `map` API: - DS.Adapter.map('App.Person', { firstName: { keyName: 'FIRST' } }); + DS.Adapter.map('App.Person', { firstName: { key: 'FIRST' } }); Class configuration via a map-like API has a few common requirements that differentiate it from the standard Ember.Map implementation. @@ -846,7 +849,7 @@ DS._Mappable = Ember.Mixin.create({ instanceMap.set(transformedKey, newValue); } - }, + } }); @@ -970,6 +973,8 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // `isLoaded` and fires a `didLoad` event. this.loadingRecordArrays = {}; + this._recordsToSave = Ember.OrderedSet.create(); + set(this, 'defaultTransaction', this.transaction()); }, @@ -1542,7 +1547,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { if (!Ember.isArray(ids)) { var adapter = this.adapterForType(type); if (adapter && adapter.findHasMany) { adapter.findHasMany(this, record, relationship, ids); } - else { throw fmt("Adapter is either null or does not implement `findHasMany` method", this); } + else if (ids !== undefined) { throw fmt("Adapter is either null or does not implement `findHasMany` method", this); } return this.createManyArray(type, Ember.A()); } @@ -1780,6 +1785,39 @@ DS.Store = Ember.Object.extend(DS._Mappable, { } }, + // ................. + // . BASIC ADAPTER . + // ................. + + scheduleSave: function(record) { + this._recordsToSave.add(record); + Ember.run.once(this, 'flushSavedRecords'); + }, + + flushSavedRecords: function() { + var created = Ember.OrderedSet.create(); + var updated = Ember.OrderedSet.create(); + var deleted = Ember.OrderedSet.create(); + + this._recordsToSave.forEach(function(record) { + if (get(record, 'isNew')) { + created.add(record); + } else if (get(record, 'isDeleted')) { + deleted.add(record); + } else { + updated.add(record); + } + }); + + this._recordsToSave.clear(); + + get(this, '_adapter').commit(this, { + created: created, + updated: updated, + deleted: deleted + }); + }, + // .............. // . PERSISTING . // .............. @@ -2268,7 +2306,6 @@ DS.Store = Ember.Object.extend(DS._Mappable, { } var content = get(array, 'content'); - var alreadyInArray = content.indexOf(clientId) !== -1; var recordArrays = this.recordArraysForClientId(clientId); var reference = this.referenceForClientId(clientId); @@ -2413,7 +2450,6 @@ DS.Store = Ember.Object.extend(DS._Mappable, { if (prematerialized && prematerialized.id) { id = prematerialized.id; } else if (id === undefined) { - var adapter = this.adapterForType(type); id = this.preprocessData(type, data); } @@ -2470,7 +2506,10 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // TODO (tomdale) this assumes that loadHasMany *always* means // that the records for the provided IDs are loaded. - if (relationship) { set(relationship, 'isLoaded', true); } + if (relationship) { + set(relationship, 'isLoaded', true); + relationship.trigger('didLoad'); + } }, /** @private @@ -2492,6 +2531,8 @@ DS.Store = Ember.Object.extend(DS._Mappable, { clientIds = typeMap.clientIds, cidToData = this.clientIdToData; + Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToClientIdMap[id]); + var clientId = ++this.clientIdCounter; cidToData[clientId] = data; @@ -2518,7 +2559,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { this.recordCache[clientId] = record = type._create({ store: this, - clientId: clientId, + clientId: clientId }); set(record, 'id', id); @@ -2546,12 +2587,10 @@ DS.Store = Ember.Object.extend(DS._Mappable, { if (id) { delete typeMap.idToCid[id]; } }, - destroy: function() { + willDestroy: function() { if (get(DS, 'defaultStore') === this) { set(DS, 'defaultStore', null); } - - return this._super(); }, // ........................ @@ -2722,7 +2761,7 @@ DS.Store.reopenClass({ (function() { -var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, +var get = Ember.get, set = Ember.set, once = Ember.run.once, arrayMap = Ember.ArrayPolyfills.map; /** @@ -2883,14 +2922,6 @@ var stateProperty = Ember.computed(function(key) { } }).property(); -var isEmptyObject = function(object) { - for (var name in object) { - if (object.hasOwnProperty(name)) { return false; } - } - - return true; -}; - var hasDefinedProperties = function(object) { for (var name in object) { if (object.hasOwnProperty(name) && object[name]) { return true; } @@ -2917,12 +2948,6 @@ var didSetProperty = function(manager, context) { change.sync(); }; -// Whenever a property is set, recompute all dependent filters -var updateRecordArrays = function(manager) { - var record = manager.get('record'); - record.updateRecordArraysLater(); -}; - DS.State = Ember.State.extend({ isLoaded: stateProperty, isReloading: stateProperty, @@ -3267,7 +3292,7 @@ var states = { exit: function(manager) { var record = get(manager, 'record'); - Ember.run.once(function() { + once(function() { record.trigger('didLoad'); }); } @@ -3503,11 +3528,11 @@ DS.StateManager = Ember.StateManager.extend({ (function() { var LoadPromise = DS.LoadPromise; // system/mixins/load_promise -var get = Ember.get, set = Ember.set, none = Ember.isNone, map = Ember.EnumerableUtils.map; +var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map; -var retrieveFromCurrentState = Ember.computed(function(key) { +var retrieveFromCurrentState = Ember.computed(function(key, value) { return get(get(this, 'stateManager.currentState'), key); -}).property('stateManager.currentState'); +}).property('stateManager.currentState').readOnly(); DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { isLoaded: retrieveFromCurrentState, @@ -3542,6 +3567,11 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { return store.serialize(this, options); }, + toJSON: function(options) { + var serializer = DS.JSONSerializer.create(); + return serializer.serialize(this, options); + }, + didLoad: Ember.K, didReload: Ember.K, didUpdate: Ember.K, @@ -3634,6 +3664,9 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { clearRelationships: function() { this.eachRelationship(function(name, relationship) { + // if the relationship is unmaterialized, move on + if (this.cacheFor(name) === undefined) { return; } + if (relationship.kind === 'belongsTo') { set(this, name, null); } else if (relationship.kind === 'hasMany') { @@ -3782,6 +3815,12 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { becameInFlight: function() { }, + // FOR USE BY THE BASIC ADAPTER + + save: function() { + this.get('store').scheduleSave(this); + }, + // FOR USE DURING COMMIT PROCESS adapterDidUpdateAttribute: function(attributeName, value) { @@ -3835,6 +3874,7 @@ var storeAlias = function(methodName) { args = [].slice.call(arguments); args.unshift(this); + Ember.assert("Your application does not have a 'Store' property defined. Attempts to call '" + methodName + "' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'", !!store); return store[methodName].apply(store, args); }; }; @@ -3843,6 +3883,7 @@ DS.Model.reopenClass({ isLoaded: storeAlias('recordIsLoaded'), find: storeAlias('find'), all: storeAlias('all'), + query: storeAlias('findQuery'), filter: storeAlias('filter'), _create: DS.Model.create, @@ -3941,8 +3982,6 @@ DS.attr = function(type, options) { }; return Ember.computed(function(key, value, oldValue) { - var data; - if (arguments.length > 1) { Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.constructor.toString(), key !== 'id'); } else { @@ -4063,7 +4102,7 @@ var hasRelationship = function(type, options) { } ids = data[key]; - relationship = store.findMany(type, ids || [], this, meta); + relationship = store.findMany(type, ids, this, meta); set(relationship, 'owner', this); set(relationship, 'name', key); @@ -4180,7 +4219,6 @@ DS.Model.reopenClass({ App.Blog = DS.Model.extend({ users: DS.hasMany(App.User), owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) }); @@ -4257,6 +4295,52 @@ DS.Model.reopenClass({ }), /** + An array of types directly related to a model. Each type will be + included once, regardless of the number of relationships it has with + the model. + + For example, given a model with this definition: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + posts: DS.hasMany(App.Post) + }); + + This property would contain the following: + + var relatedTypes = Ember.get(App.Blog, 'relatedTypes'); + //=> [ App.User, App.Post ] + + @type Ember.Array + @readOnly + */ + relatedTypes: Ember.computed(function() { + var type, + types = Ember.A([]); + + // Loop through each computed property on the class, + // and create an array of the unique types involved + // in relationships + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + type = meta.type; + + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } + + if (!types.contains(type)) { + Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); + types.push(type); + } + } + }); + + return types; + }), + + /** A map whose keys are the relationships of a model and whose values are relationship descriptors. @@ -4357,6 +4441,21 @@ DS.Model.reopenClass({ get(this, 'relationshipsByName').forEach(function(name, relationship) { callback.call(binding, name, relationship); }); + }, + + /** + Given a callback, iterates over each of the types related to a model, + invoking the callback with the related type's class. Each type will be + returned just once, regardless of how many different relationships it has + with a model. + + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelatedType: function(callback, binding) { + get(this, 'relatedTypes').forEach(function(type) { + callback.call(binding, type); + }); } }); @@ -4447,6 +4546,8 @@ var get = Ember.get, set = Ember.set; var forEach = Ember.EnumerableUtils.forEach; DS.RelationshipChange = function(options) { + this.parentReference = options.parentReference; + this.childReference = options.childReference; this.firstRecordReference = options.firstRecordReference; this.firstRecordKind = options.firstRecordKind; this.firstRecordName = options.firstRecordName; @@ -4502,7 +4603,7 @@ DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide var knownContainerType = knownSide.kind; var options = recordType.metaForProperty(knownKey).options; var otherType = DS._inverseTypeFor(recordType, knownKey); - + if(options.inverse){ key = options.inverse; otherContainerType = get(otherType, 'relationshipsByName').get(key).kind; @@ -4536,10 +4637,10 @@ DS.RelationshipChange.createChange = function(firstRecordReference, secondRecord return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options); } else if (changeType === "oneToNone"){ - return DS.OneToNoneChange.createChange(firstRecordReference, {}, store, options); + return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); } else if (changeType === "manyToNone"){ - return DS.ManyToNoneChange.createChange(firstRecordReference, {}, store, options); + return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); } else if (changeType === "oneToOne"){ return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options); @@ -4553,6 +4654,8 @@ DS.RelationshipChange.createChange = function(firstRecordReference, secondRecord DS.OneToNoneChange.createChange = function(childReference, parentReference, store, options) { var key = options.key; var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, firstRecordReference: childReference, store: store, changeType: options.changeType, @@ -4563,12 +4666,14 @@ DS.OneToNoneChange.createChange = function(childReference, parentReference, stor store.addRelationshipChangeFor(childReference, key, parentReference, null, change); return change; -}; +}; /** @private */ DS.ManyToNoneChange.createChange = function(childReference, parentReference, store, options) { var key = options.key; var change = DS.RelationshipChange._createChange({ + parentReference: childReference, + childReference: parentReference, secondRecordReference: childReference, store: store, changeType: options.changeType, @@ -4578,14 +4683,14 @@ DS.ManyToNoneChange.createChange = function(childReference, parentReference, sto store.addRelationshipChangeFor(childReference, key, parentReference, null, change); return change; -}; +}; /** @private */ DS.ManyToManyChange.createChange = function(childReference, parentReference, store, options) { // Get the type of the child based on the child's client ID var childType = childReference.type, key; - + // If the name of the belongsTo side of the relationship is specified, // use that // If the type of the parent is specified, look it up on the child's type @@ -4593,6 +4698,8 @@ DS.ManyToManyChange.createChange = function(childReference, parentReference, sto key = options.key; var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, firstRecordReference: childReference, secondRecordReference: parentReference, firstRecordKind: "hasMany", @@ -4612,7 +4719,7 @@ DS.ManyToManyChange.createChange = function(childReference, parentReference, sto DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) { // Get the type of the child based on the child's client ID var childType = childReference.type, key; - + // If the name of the belongsTo side of the relationship is specified, // use that // If the type of the parent is specified, look it up on the child's type @@ -4627,6 +4734,8 @@ DS.OneToOneChange.createChange = function(childReference, parentReference, store } var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, firstRecordReference: childReference, secondRecordReference: parentReference, firstRecordKind: "belongsTo", @@ -4663,7 +4772,7 @@ DS.OneToOneChange.maintainInvariant = function(options, store, childReference, k DS.OneToManyChange.createChange = function(childReference, parentReference, store, options) { // Get the type of the child based on the child's client ID var childType = childReference.type, key; - + // If the name of the belongsTo side of the relationship is specified, // use that // If the type of the parent is specified, look it up on the child's type @@ -4678,6 +4787,8 @@ DS.OneToManyChange.createChange = function(childReference, parentReference, stor } var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, firstRecordReference: childReference, secondRecordReference: parentReference, firstRecordKind: "belongsTo", @@ -4764,13 +4875,13 @@ DS.RelationshipChange.prototype = { /** @private */ destroy: function() { - var childReference = this.firstRecordReference, + var childReference = this.childReference, belongsToName = this.getFirstRecordName(), hasManyName = this.getSecondRecordName(), store = this.store, child, oldParent, newParent, lastParent, transaction; - store.removeRelationshipChangeFor(childReference, belongsToName, this.secondRecordReference, hasManyName, this.changeType); + store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); if (transaction = this.transaction) { transaction.relationshipBecameClean(this); @@ -4920,7 +5031,9 @@ DS.RelationshipChangeRemove.prototype.sync = function() { if (secondRecord && firstRecord) { if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ set(secondRecord, secondRecordName, null); + }); } else if(this.secondRecordKind === "hasMany"){ secondRecord.suspendRelationshipObservers(function(){ @@ -5050,7 +5163,7 @@ Ember.onLoad('Ember.Application', function(Application) { name: "store", initialize: function(container, application) { - container.register('store', 'main', application.Store); + application.register('store:main', application.Store); // Eagerly generate the store so defaultStore is populated. // TODO: Do this in a finisher hook @@ -5061,9 +5174,9 @@ Ember.onLoad('Ember.Application', function(Application) { Application.initializer({ name: "injectStore", - initialize: function(container) { - container.typeInjection('controller', 'store', 'store:main'); - container.typeInjection('route', 'store', 'store:main'); + initialize: function(container, application) { + application.inject('controller', 'store', 'store:main'); + application.inject('route', 'store', 'store:main'); } }); } @@ -5138,20 +5251,29 @@ function mustImplement(name) { by implementing `keyForAttributeName`: ```javascript - keyForAttributeName: function(type, name) { - return name.underscore.toUpperCase(); - } + keyForAttributeName: function(type, name) { + return name.underscore.toUpperCase(); + } ``` If your attribute names are not predictable, you can re-map them - one-by-one using the `map` API: + one-by-one using the adapter's `map` API: ```javascript - App.Person.map('App.Person', { + App.Adapter.map('App.Person', { firstName: { key: '*API_USER_FIRST_NAME*' } }); ``` + This API will also work for relationships and primary keys. For + example: + + ```javascript + App.Adapter.map('App.Person', { + primaryKey: '_id' + }); + ``` + ## Serialization During the serialization process, a record or records are converted @@ -5608,7 +5730,7 @@ DS.Serializer = Ember.Object.extend({ primaryKey: function(type) { // If the type is `BlogPost`, this will return // `blog_post_id`. - var typeString = type.toString.split(".")[1].underscore(); + var typeString = type.toString().split(".")[1].underscore(); return typeString + "_id"; } }); @@ -5986,10 +6108,10 @@ DS.Serializer = Ember.Object.extend({ registerEnumTransform: function(type, objects) { var transform = { deserialize: function(deserialized) { - return objects.objectAt(deserialized); + return Ember.A(objects).objectAt(deserialized); }, serialize: function(serialized) { - return objects.indexOf(serialized); + return Ember.EnumerableUtils.indexOf(objects, serialized); }, values: objects }; @@ -6240,8 +6362,6 @@ DS.JSONTransforms = { return dayOfWeek + ", " + dayOfMonth + " " + month + " " + utcYear + " " + pad(utcHours) + ":" + pad(utcMinutes) + ":" + pad(utcSeconds) + " GMT"; - } else if (date === undefined) { - return undefined; } else { return null; } @@ -6283,6 +6403,13 @@ DS.JSONSerializer = DS.Serializer.extend({ if (sideloadAs) { this.sideloadMapping.set(sideloadAs, type); + + // Set a flag indicating that mappings may need to be normalized + // (i.e. converted from strings -> types) before sideloading. + // We can't do this conversion immediately here, because `configure` + // may be called before certain types have been defined. + this.sideloadMapping.normalized = false; + delete configuration.sideloadAs; } @@ -6352,7 +6479,7 @@ DS.JSONSerializer = DS.Serializer.extend({ if (this.embeddedType(type, name)) { if (embeddedChild = get(record, name)) { - value = this.serialize(embeddedChild, { include: true }); + value = this.serialize(embeddedChild, { includeId: true }); } hash[key] = value; @@ -6444,46 +6571,88 @@ DS.JSONSerializer = DS.Serializer.extend({ } }, + /** + @private + + Iterates over the `json` payload and attempts to load any data + included alongside `root`. + + The keys expected for sideloaded data are based upon the types related + to the root model. Recursion is used to ensure that types related to + related types can be loaded as well. Any custom keys specified by + `sideloadAs` mappings will also be respected. + + @param {DS.Store subclass} loader + @param {DS.Model subclass} type + @param {Object} json + @param {String} root + */ sideload: function(loader, type, json, root) { - var sideloadedType, mappings, loaded = {}; + var sideloadedType; - loaded[root] = true; + this.normalizeSideloadMappings(); + this.configureSideloadMappingForType(type); for (var prop in json) { - if (!json.hasOwnProperty(prop)) { continue; } - if (prop === root) { continue; } - if (prop === this.configOption(type, 'meta')) { continue; } + if (!json.hasOwnProperty(prop) || + prop === root || + prop === this.configOption(type, 'meta')) { + continue; + } - sideloadedType = type.typeForRelationship(prop); + sideloadedType = this.sideloadMapping.get(prop); + Ember.assert("Your server returned a hash with the key " + prop + + " but you have no mapping for it", + !!sideloadedType); - if (!sideloadedType) { - sideloadedType = this.sideloadMapping.get(prop); + this.loadValue(loader, sideloadedType, json[prop]); + } + }, - if (typeof sideloadedType === 'string') { - sideloadedType = get(Ember.lookup, sideloadedType); - } + /** + @private - Ember.assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType); - } + Iterates over all the `sideloadAs` mappings and converts any that are + strings to their equivalent types. - this.sideloadRelationships(loader, sideloadedType, json, prop, loaded); + This is an optimization used to avoid performing lookups for every + call to `sideload`. + */ + normalizeSideloadMappings: function() { + if (! this.sideloadMapping.normalized) { + this.sideloadMapping.forEach(function(key, value) { + if (typeof value === 'string') { + this.sideloadMapping.set(key, get(Ember.lookup, value)); + } + }, this); + this.sideloadMapping.normalized = true; } }, - sideloadRelationships: function(loader, type, json, prop, loaded) { - loaded[prop] = true; + /** + @private - get(type, 'relationshipsByName').forEach(function(key, meta) { - key = meta.key || key; - if (meta.kind === 'belongsTo') { - key = this.pluralize(key); - } - if (json[key] && !loaded[key]) { - this.sideloadRelationships(loader, meta.type, json, key, loaded); + Configures possible sideload mappings for the types related to a + particular model. This recursive method ensures that sideloading + works for related models as well. + + @param {DS.Model subclass} type + @param {Ember.A} configured an array of types that have already been configured + */ + configureSideloadMappingForType: function(type, configured) { + if (!configured) {configured = Ember.A([]);} + configured.pushObject(type); + + type.eachRelatedType(function(relatedType) { + if (!configured.contains(relatedType)) { + var root = this.sideloadMappingForType(relatedType); + if (!root) { + root = this.defaultSideloadRootForType(relatedType); + this.sideloadMapping.set(root, relatedType); + } + this.configureSideloadMappingForType(relatedType, configured); } }, this); - - this.loadValue(loader, type, json[prop]); }, loadValue: function(loader, type, value) { @@ -6505,6 +6674,36 @@ DS.JSONSerializer = DS.Serializer.extend({ return (plurals && plurals[name]) || name + "s"; }, + // use the same plurals hash to determine + // special-case singularization + singularize: function(name) { + var plurals = this.configurations.get('plurals'); + if (plurals) { + for (var i in plurals) { + if (plurals[i] === name) { + return i; + } + } + } + if (name.lastIndexOf('s') === name.length - 1) { + return name.substring(0, name.length - 1); + } else { + return name; + } + }, + + /** + @private + + Determines the singular root name for a particular type. + + This is an underscored, lowercase version of the model name. + For example, the type `App.UserGroup` will have the root + `user_group`. + + @param {DS.Model subclass} type + @returns {String} name of the root element + */ rootForType: function(type) { var typeString = type.toString(); @@ -6514,6 +6713,34 @@ DS.JSONSerializer = DS.Serializer.extend({ var parts = typeString.split("."); var name = parts[parts.length - 1]; return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1); + }, + + /** + @private + + Determines the root name mapped to a particular sideloaded type. + + @param {DS.Model subclass} type + @returns {String} name of the root element, if any is registered + */ + sideloadMappingForType: function(type) { + this.sideloadMapping.forEach(function(key, value) { + if (type === value) { + return key; + } + }); + }, + + /** + @private + + The default root name for a particular sideloaded type. + + @param {DS.Model subclass} type + @returns {String} name of the root element + */ + defaultSideloadRootForType: function(type) { + return this.pluralize(this.rootForType(type)); } }); @@ -6685,7 +6912,6 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { if (payload) { var loader = DS.loaderFor(store); - var serializer = get(this, 'serializer'); loader.load = function(type, data, prematerialized) { store.updateId(record, data); @@ -7236,14 +7462,122 @@ DS.Adapter.reopenClass({ (function() { -var get = Ember.get; +var get = Ember.get, set = Ember.set; + +DS.FixtureSerializer = DS.Serializer.extend({ + deserializeValue: function(value, attributeType) { + return value; + }, + + serializeValue: function(value, attributeType) { + return value; + }, + + addId: function(data, key, id) { + data[key] = id; + }, + + addAttribute: function(hash, key, value) { + hash[key] = value; + }, + + addBelongsTo: function(hash, record, key, relationship) { + var id = get(record, relationship.key+'.id'); + if (!Ember.isNone(id)) { hash[key] = id; } + }, + + addHasMany: function(hash, record, key, relationship) { + var ids = get(record, relationship.key).map(function(item) { + return item.get('id'); + }); + + hash[relationship.key] = ids; + }, + + /** + @private + + Creates an empty hash that will be filled in by the hooks called from the + `serialize()` method. + + @return {Object} + */ + createSerializedForm: function() { + return {}; + }, + + extract: function(loader, fixture, type, record) { + if (record) { loader.updateId(record, fixture); } + this.extractRecordRepresentation(loader, type, fixture); + }, + + extractMany: function(loader, fixtures, type, records) { + var objects = fixtures, references = []; + if (records) { records = records.toArray(); } + + for (var i = 0; i < objects.length; i++) { + if (records) { loader.updateId(records[i], objects[i]); } + var reference = this.extractRecordRepresentation(loader, type, objects[i]); + references.push(reference); + } + + loader.populateArray(references); + }, + + extractId: function(type, hash) { + var primaryKey = this._primaryKey(type); + + if (hash.hasOwnProperty(primaryKey)) { + // Ensure that we coerce IDs to strings so that record + // IDs remain consistent between application runs; especially + // if the ID is serialized and later deserialized from the URL, + // when type information will have been lost. + return hash[primaryKey]+''; + } else { + return null; + } + }, + + extractAttribute: function(type, hash, attributeName) { + var key = this._keyForAttributeName(type, attributeName); + return hash[key]; + }, + + extractHasMany: function(type, hash, key) { + return hash[key]; + }, + + extractBelongsTo: function(type, hash, key) { + return hash[key]; + } +}); +})(); + + + +(function() { +var get = Ember.get, fmt = Ember.String.fmt, + dump = Ember.get(window, 'JSON.stringify') || function(object) { return object.toString(); }; + +/** + `DS.FixtureAdapter` is an adapter that loads records from memory. + Its primarily used for development and testing. You can also use + `DS.FixtureAdapter` while working on the API but are not ready to + integrate yet. It is a fully functioning adapter. All CRUD methods + are implemented. You can also implement query logic that a remote + system would do. Its possible to do develop your entire application + with `DS.FixtureAdapter`. + +*/ DS.FixtureAdapter = DS.Adapter.extend({ simulateRemoteResponse: true, latency: 50, + serializer: DS.FixtureSerializer, + /* Implement this method in order to provide data associated with a type */ @@ -7252,7 +7586,7 @@ DS.FixtureAdapter = DS.Adapter.extend({ var fixtures = Ember.A(type.FIXTURES); return fixtures.map(function(fixture){ if(!fixture.id){ - throw new Error('the id property must be defined for fixture %@'.fmt(fixture)); + throw new Error(fmt('the id property must be defined for fixture %@', [dump(fixture)])); } fixture.id = fixture.id + ''; return fixture; @@ -7265,7 +7599,19 @@ DS.FixtureAdapter = DS.Adapter.extend({ Implement this method in order to query fixtures data */ queryFixtures: function(fixtures, query, type) { - return fixtures; + Ember.assert('Not implemented: You must override the DS.FixtureAdapter::queryFixtures method to support querying the fixture store.'); + }, + + updateFixtures: function(type, fixture) { + if(!type.FIXTURES) { + type.FIXTURES = []; + } + + var fixtures = type.FIXTURES; + + this.deleteLoadedFixture(type, fixture); + + fixtures.push(fixture); }, /* @@ -7283,18 +7629,19 @@ DS.FixtureAdapter = DS.Adapter.extend({ }, find: function(store, type, id) { - var fixtures = this.fixturesForType(type); + var fixtures = this.fixturesForType(type), + fixture; - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); + Ember.warn("Unable to find fixtures for model type " + type.toString(), fixtures); if (fixtures) { - fixtures = fixtures.findProperty('id', id); + fixture = Ember.A(fixtures).findProperty('id', id); } - if (fixtures) { + if (fixture) { this.simulateRemoteCall(function() { - store.load(type, fixtures); - }, store, type); + this.didFindRecord(store, type, fixture, id); + }, this); } }, @@ -7311,8 +7658,8 @@ DS.FixtureAdapter = DS.Adapter.extend({ if (fixtures) { this.simulateRemoteCall(function() { - store.loadMany(type, fixtures); - }, store, type); + this.didFindMany(store, type, fixtures); + }, this); } }, @@ -7322,9 +7669,8 @@ DS.FixtureAdapter = DS.Adapter.extend({ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); this.simulateRemoteCall(function() { - store.loadMany(type, fixtures); - store.didUpdateAll(type); - }, store, type); + this.didFindAll(store, type, fixtures); + }, this); }, findQuery: function(store, type, query, array) { @@ -7336,43 +7682,82 @@ DS.FixtureAdapter = DS.Adapter.extend({ if (fixtures) { this.simulateRemoteCall(function() { - array.load(fixtures); - }, store, type); + this.didFindQuery(store, type, fixtures, array); + }, this); } }, createRecord: function(store, type, record) { var fixture = this.mockJSON(type, record); - fixture.id = this.generateIdForRecord(store, record); + this.updateFixtures(type, fixture); this.simulateRemoteCall(function() { - store.didSaveRecord(record, fixture); - }, store, type, record); + this.didCreateRecord(store, type, record, fixture); + }, this); }, updateRecord: function(store, type, record) { var fixture = this.mockJSON(type, record); + this.updateFixtures(type, fixture); + this.simulateRemoteCall(function() { - store.didSaveRecord(record, fixture); - }, store, type, record); + this.didUpdateRecord(store, type, record, fixture); + }, this); }, deleteRecord: function(store, type, record) { + var fixture = this.mockJSON(type, record); + + this.deleteLoadedFixture(type, fixture); + this.simulateRemoteCall(function() { - store.didSaveRecord(record); - }, store, type, record); + this.didDeleteRecord(store, type, record); + }, this); }, /* @private */ - simulateRemoteCall: function(callback, store, type, record) { + deleteLoadedFixture: function(type, record) { + var id = this.extractId(type, record); + + var existingFixture = this.findExistingFixture(type, record); + + if(existingFixture) { + var index = type.FIXTURES.indexOf(existingFixture); + type.FIXTURES.splice(index, 1); + return true; + } + }, + + findExistingFixture: function(type, record) { + var fixtures = this.fixturesForType(type); + var id = this.extractId(type, record); + + return this.findFixtureById(fixtures, id); + }, + + findFixtureById: function(fixtures, id) { + var adapter = this; + + return Ember.A(fixtures).find(function(r) { + if(''+get(r, 'id') === ''+id) { + return true; + } else { + return false; + } + }); + }, + + simulateRemoteCall: function(callback, context) { if (get(this, 'simulateRemoteResponse')) { - setTimeout(callback, get(this, 'latency')); + // Schedule with setTimeout + Ember.run.later(context, callback, get(this, 'latency')); } else { - callback(); + // Asynchronous, but at the of the runloop with zero latency + Ember.run.once(context, callback); } } }); @@ -7395,6 +7780,16 @@ DS.RESTSerializer = DS.JSONSerializer.extend({ } return key + "_id"; + }, + + keyForHasMany: function(type, name) { + var key = this.keyForAttributeName(type, name); + + if (this.embeddedType(type, name)) { + return key; + } + + return this.singularize(key) + "_ids"; } }); @@ -7499,12 +7894,25 @@ DS.RESTAdapter = DS.Adapter.extend({ }, dirtyRecordsForRecordChange: function(dirtySet, record) { + this._dirtyTree(dirtySet, record); + }, + + dirtyRecordsForHasManyChange: function(dirtySet, record, relationship) { + var embeddedType = get(this, 'serializer').embeddedType(record.constructor, relationship.secondRecordName); + + if (embeddedType === 'always') { + relationship.childReference.parent = relationship.parentReference; + this._dirtyTree(dirtySet, record); + } + }, + + _dirtyTree: function(dirtySet, record) { dirtySet.add(record); get(this, 'serializer').eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { if (embeddedType !== 'always') { return; } if (dirtySet.has(embeddedRecord)) { return; } - this.dirtyRecordsForRecordChange(dirtySet, embeddedRecord); + this._dirtyTree(dirtySet, embeddedRecord); }, this); var reference = record.get('_reference'); @@ -7512,12 +7920,10 @@ DS.RESTAdapter = DS.Adapter.extend({ if (reference.parent) { var store = get(record, 'store'); var parent = store.recordForReference(reference.parent); - this.dirtyRecordsForRecordChange(dirtySet, parent); + this._dirtyTree(dirtySet, parent); } }, - dirtyRecordsForHasManyChange: Ember.K, - createRecords: function(store, type, records) { if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); @@ -7763,6 +8169,251 @@ DS.RESTAdapter = DS.Adapter.extend({ (function() { +var camelize = Ember.String.camelize, + capitalize = Ember.String.capitalize, + get = Ember.get, + map = Ember.ArrayPolyfills.map, + registeredTransforms; + +var passthruTransform = { + serialize: function(value) { return value; }, + deserialize: function(value) { return value; } +}; + +var defaultTransforms = { + string: passthruTransform, + boolean: passthruTransform, + number: passthruTransform +}; + +function camelizeKeys(json) { + var value; + + for (var prop in json) { + value = json[prop]; + delete json[prop]; + json[camelize(prop)] = value; + } +} + +function munge(json, callback) { + callback(json); +} + +function applyTransforms(json, type, transformType) { + var transforms = registeredTransforms[transformType]; + + Ember.assert("You are trying to apply the '" + transformType + "' transforms, but you didn't register any transforms with that name", transforms); + + get(type, 'attributes').forEach(function(name, attribute) { + var attributeType = attribute.type, + value = json[name]; + + var transform = transforms[attributeType] || defaultTransforms[attributeType]; + + Ember.assert("Your model specified the '" + attributeType + "' type for the '" + name + "' attribute, but no transform for that type was registered", transform); + + json[name] = transform.deserialize(value); + }); +} + +function ObjectProcessor(json, type, store) { + this.json = json; + this.type = type; + this.store = store; +} + +ObjectProcessor.prototype = { + camelizeKeys: function() { + camelizeKeys(this.json); + return this; + }, + + munge: function(callback) { + munge(this.json, callback); + return this; + }, + + applyTransforms: function(transformType) { + applyTransforms(this.json, this.type, transformType); + return this; + } +}; + +function LoadObjectProcessor() { + ObjectProcessor.apply(this, arguments); +} + +LoadObjectProcessor.prototype = Ember.create(ObjectProcessor.prototype); + +LoadObjectProcessor.prototype.load = function() { + this.store.load(this.type, {}, this.json); +}; + +function loadObjectProcessorFactory(store, type) { + return function(json) { + return new LoadObjectProcessor(json, type, store); + }; +} + +function ArrayProcessor(json, type, array, store) { + this.json = json; + this.type = type; + this.array = array; + this.store = store; +} + +ArrayProcessor.prototype = { + load: function() { + var store = this.store, + type = this.type; + + var references = this.json.map(function(object) { + return store.load(type, {}, object); + }); + + this.array.load(references); + }, + + camelizeKeys: function() { + this.json.forEach(camelizeKeys); + return this; + }, + + munge: function(callback) { + this.json.forEach(function(object) { + munge(object, callback); + }); + return this; + }, + + applyTransforms: function(transformType) { + var type = this.type; + + this.json.forEach(function(object) { + applyTransforms(object, type, transformType); + }); + + return this; + } +}; + +function arrayProcessorFactory(store, type, array) { + return function(json) { + return new ArrayProcessor(json, type, array, store); + }; +} + +var HasManyProcessor = function(json, store, record, relationship) { + this.json = json; + this.store = store; + this.record = record; + this.type = record.constructor; + this.relationship = relationship; +}; + +HasManyProcessor.prototype = Ember.create(ArrayProcessor.prototype); + +HasManyProcessor.prototype.load = function() { + var store = this.store; + var ids = map.call(this.json, function(obj) { return obj.id; }); + + store.loadMany(this.relationship.type, this.json); + store.loadHasMany(this.record, this.relationship.key, ids); +}; + +function hasManyProcessorFactory(store, record, relationship) { + return function(json) { + return new HasManyProcessor(json, store, record, relationship); + }; +} + +function SaveProcessor(record, store, type, includeId) { + this.record = record; + ObjectProcessor.call(this, record.toJSON({ includeId: includeId }), type, store); +} + +SaveProcessor.prototype = Ember.create(ObjectProcessor.prototype); + +SaveProcessor.prototype.save = function(callback) { + callback(this.json); +}; + +function saveProcessorFactory(store, type, includeId) { + return function(record) { + return new SaveProcessor(record, store, type, includeId); + }; +} + +DS.BasicAdapter = DS.Adapter.extend({ + find: function(store, type, id) { + var sync = type.sync; + + Ember.assert("You are trying to use the BasicAdapter to find id '" + id + "' of " + type + " but " + type + ".sync was not found", sync); + Ember.assert("The sync code on " + type + " does not implement find(), but you are trying to find id '" + id + "'.", sync.find); + + sync.find(id, loadObjectProcessorFactory(store, type)); + }, + + findQuery: function(store, type, query, recordArray) { + var sync = type.sync; + + Ember.assert("You are trying to use the BasicAdapter to query " + type + " but " + type + ".sync was not found", sync); + Ember.assert("The sync code on " + type + " does not implement query(), but you are trying to query " + type + ".", sync.query); + + sync.query(query, arrayProcessorFactory(store, type, recordArray)); + }, + + findHasMany: function(store, record, relationship, data) { + var name = capitalize(relationship.key), + sync = record.constructor.sync, + processor = hasManyProcessorFactory(store, record, relationship); + + var options = { + relationship: relationship.key, + data: data + }; + + if (sync['find'+name]) { + sync['find' + name](record, options, processor); + } else if (sync.findHasMany) { + sync.findHasMany(record, options, processor); + } else { + Ember.assert("You are trying to use the BasicAdapter to find the " + relationship.key + " has-many relationship, but " + record.constructor + ".sync did not implement findHasMany or find" + name + ".", false); + } + }, + + createRecord: function(store, type, record) { + var sync = type.sync; + sync.createRecord(record, saveProcessorFactory(store, type)); + }, + + updateRecord: function(store, type, record) { + var sync = type.sync; + sync.updateRecord(record, saveProcessorFactory(store, type, true)); + }, + + deleteRecord: function(store, type, record) { + var sync = type.sync; + sync.deleteRecord(record, saveProcessorFactory(store, type, true)); + } +}); + +DS.registerTransforms = function(kind, object) { + registeredTransforms[kind] = object; +}; + +DS.clearTransforms = function() { + registeredTransforms = {}; +}; + +DS.clearTransforms(); + +})(); + + + +(function() { })(); |