Created
November 3, 2015 16:23
-
-
Save robneville73/f18e88468de75274a1d7 to your computer and use it in GitHub Desktop.
combined functions from ember data 1.13.9 dealing with persisting records to the store with heavy comments
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//record (internal-model) | |
save: function(options) { | |
var promiseLabel = "DS: Model#save " + this; | |
var resolver = Ember.RSVP.defer(promiseLabel); //http://emberjs.com/api/classes/RSVP.html#method_defer | |
this.store.scheduleSave(this, resolver, options); | |
return resolver.promise; | |
}, | |
//store.js | |
scheduleSave: function(internalModel, resolver, options) { | |
var snapshot = internalModel.createSnapshot(options); | |
internalModel.flushChangedAttributes(); | |
internalModel.adapterWillCommit(); | |
this._pendingSave.push({ //this is just an array created during init of store | |
snapshot: snapshot, | |
resolver: resolver | |
}); | |
once(this, 'flushPendingSave'); //Ember.run.once schedules specified function at end of run loop in queue 'actions' | |
}, | |
//I don't think we need to keep a list of autoupdates to do in a list like that. I think we'll be good | |
//with just calling once(this, 'someNewAutoUpdateFunction')...no, we do need to do it (or something like it), | |
//because we can't pass that context to the function being called from once...it has to be data on the store itself, which | |
//is what we're already doing with the autoupdate context object. | |
//so... | |
//1. create new function on internal-model for autoUpdate and have it call store.scheduleAutoUpdate | |
//2. scheduleAutoUpdate gets the snapshot, etc. and updates autoUpdateContext that lives on store (like _pendingSave sort of) and calls flushPendingAutoUpdate with once | |
//3. flushPendingAutoUpdate mimics flushPendingSave and calls _commit on store with an operation of 'autoupdate' | |
//4. new autoupdate stubs on adapter take care of building URLs and creating ajax calls | |
//5. store's _commit basically handles it otherwise like any persistence call to back end | |
//initializer to re-open internal-model | |
//add autoUpdate method (modeled after save method above) | |
//store re-open | |
//add scheduleAutoUpdate (modeled after scheduledSave) | |
//add flushPendingAutoUpdate (modeled after flushPendingSave) | |
flushPendingSave: function() { | |
var pending = this._pendingSave.slice(); | |
this._pendingSave = []; | |
forEach.call(pending, function(pendingItem) { | |
var snapshot = pendingItem.snapshot; | |
var resolver = pendingItem.resolver; | |
var record = snapshot._internalModel; | |
var adapter = this.adapterFor(record.type.modelName); | |
var operation; | |
if (get(record, 'currentState.stateName') === 'root.deleted.saved') { | |
return resolver.resolve(); | |
} else if (record.isNew()) { | |
operation = 'createRecord'; | |
} else if (record.isDeleted()) { | |
operation = 'deleteRecord'; | |
} else { | |
operation = 'updateRecord'; | |
} | |
resolver.resolve(_commit(adapter, this, operation, snapshot)); | |
}, this); | |
}, | |
function _commit(adapter, store, operation, snapshot) { | |
var internalModel = snapshot._internalModel; | |
var modelName = snapshot.modelName; | |
var typeClass = store.modelFor(modelName); | |
//basically calls the appropriate function to get an ajax promise | |
var promise = adapter[operation](store, typeClass, snapshot); //createRecord, deleteRecord, updateRecord | |
var serializer = serializerForAdapter(store, adapter, modelName); | |
var label = "DS: Extract and notify about " + operation + " completion of " + internalModel; | |
Ember.assert("Your adapter's '" + operation + "' method must return a value, but it returned `undefined", promise !==undefined); | |
promise = Promise.cast(promise, label); //see https://github.com/emberjs/ember.js/pull/4034 don't think this is important though | |
promise = _guard(promise, _bind(_objectIsAlive, store)); //don't think these are important either...looks like they come into play if the | |
promise = _guard(promise, _bind(_objectIsAlive, internalModel)); //record's in progress of being destroyed or something | |
return promise.then(function(adapterPayload) { | |
store._adapterRun(function() { //runs it through backburner is all... | |
var payload, data; | |
if (adapterPayload) { | |
payload = normalizeResponseHelper(serializer, store, typeClass, adapterPayload, snapshot.id, operation); //just normalizes the response via the serializer, i.e. calls the normalizeResponse hook. | |
if (payload.included) { //if compound document with included records | |
store.push({ data: payload.included }); //pushes included objects directly into store | |
} | |
data = convertResourceObject(payload.data); // munges payload data in json-api sort of | |
} | |
store.didSaveRecord(internalModel, _normalizeSerializerPayload(internalModel.type, data)); | |
//workhorse step.... normalizes the data into JSON-API doc then updates the id of the internal model if not already set | |
//and complains if it is already set. Then merges changed attributes into internal model, which in-turn updates the state | |
//of the model if successful to saved from uncommitted. | |
}); | |
return internalModel; | |
}, function(error) { | |
if (error instanceof InvalidError) { | |
var errors = serializer.extractErrors(store, typeClass, error, snapshot.id); | |
store.recordWasInvalid(internalModel, errors); | |
} else { | |
store.recordWasError(internalModel, error); | |
} | |
throw error; | |
}, label); | |
} | |
/** | |
This method is called once the promise returned by an | |
adapter's `createRecord`, `updateRecord` or `deleteRecord` | |
is resolved. | |
If the data provides a server-generated ID, it will | |
update the record and the store's indexes. | |
@method didSaveRecord | |
@private | |
@param {InternalModel} internalModel the in-flight internal model | |
@param {Object} data optional data (see above) | |
*/ | |
didSaveRecord: function(internalModel, dataArg) { | |
var data; | |
if (dataArg) { | |
data = dataArg.data; | |
} | |
if (data) { | |
// normalize relationship IDs into records | |
this._backburner.schedule('normalizeRelationships', this, '_setupRelationships', internalModel, internalModel.type, data); | |
this.updateId(internalModel, data); //sets the id returned from a save on the internal model in the store and complains if it's already set to | |
//something other than what it already was | |
} | |
//We first make sure the primary data has been updated | |
//TODO try to move notification to the user to the end of the runloop | |
internalModel.adapterDidCommit(data); //merges data into existing internal model in store | |
}, | |
/** | |
If the adapter did not return a hash in response to a commit, | |
merge the changed attributes and relationships into the existing | |
saved data. | |
@method adapterDidCommit | |
*/ | |
adapterDidCommit: function(data) { | |
if (data) { | |
data = data.attributes; | |
} | |
this.didCleanError(); | |
var changedKeys = this._changedKeys(data); | |
merge(this._data, this._inFlightAttributes); | |
if (data) { | |
merge(this._data, data); | |
} | |
this._inFlightAttributes = create(null); | |
this.send('didCommit'); | |
this.updateRecordArraysLater(); | |
if (!data) { return; } | |
this.record._notifyProperties(changedKeys); | |
}, | |
/** | |
When an adapter's `createRecord`, `updateRecord` or `deleteRecord` | |
resolves with data, this method extracts the ID from the supplied | |
data. | |
@method updateId | |
@private | |
@param {InternalModel} internalModel | |
@param {Object} data | |
*/ | |
updateId: function(internalModel, data) { | |
var oldId = internalModel.id; | |
var id = coerceId(data.id); | |
Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + internalModel + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === null || id === oldId); | |
this.typeMapFor(internalModel.type).idToRecord[id] = internalModel; | |
internalModel.setId(id); | |
}, | |
push: function(modelNameArg, dataArg) { | |
var data, modelName; | |
if (Ember.typeOf(modelNameArg) === 'object' && Ember.typeOf(dataArg) === 'undefined') { | |
data = modelNameArg; | |
} else { | |
Ember.deprecate('store.push(type, data) has been deprecated. Please provide a JSON-API document object as the first and only argument to store.push.', false, { | |
id: 'ds.store.push-with-type-and-data-deprecated', | |
until: '2.0.0' | |
}); | |
Ember.assert("Expected an object as `data` in a call to `push` for " + modelNameArg + " , but was " + Ember.typeOf(dataArg), Ember.typeOf(dataArg) === 'object'); | |
Ember.assert("You must include an `id` for " + modelNameArg + " in an object passed to `push`", dataArg.id != null && dataArg.id !== ''); | |
data = _normalizeSerializerPayload(this.modelFor(modelNameArg), dataArg); | |
modelName = modelNameArg; | |
Ember.assert('Passing classes to store methods has been removed. Please pass a dasherized string instead of '+ Ember.inspect(modelName), typeof modelName === 'string' || typeof data === 'undefined'); | |
} | |
if (data.included) { | |
forEach.call(data.included, (recordData) => this._pushInternalModel(recordData)); | |
} | |
if (Ember.typeOf(data.data) === 'array') { | |
var internalModels = map.call(data.data, (recordData) => this._pushInternalModel(recordData)); | |
return map.call(internalModels, function(internalModel) { | |
return internalModel.getRecord(); | |
}); | |
} | |
var internalModel = this._pushInternalModel(data.data || data); | |
return internalModel.getRecord(); | |
}, | |
_pushInternalModel: function(data) { | |
var modelName = data.type; | |
Ember.assert(`Expected an object as 'data' in a call to 'push' for ${modelName}, but was ${Ember.typeOf(data)}`, Ember.typeOf(data) === 'object'); | |
Ember.assert(`You must include an 'id' for ${modelName} in an object passed to 'push'`, data.id != null && data.id !== ''); | |
Ember.assert(`You tried to push data with a type '${modelName}' but no model could be found with that name.`, this._hasModelFor(modelName)); | |
var type = this.modelFor(modelName); | |
var filter = Ember.ArrayPolyfills.filter; | |
// If Ember.ENV.DS_WARN_ON_UNKNOWN_KEYS is set to true and the payload | |
// contains unknown keys, log a warning. | |
if (Ember.ENV.DS_WARN_ON_UNKNOWN_KEYS) { | |
Ember.warn("The payload for '" + type.modelName + "' contains these unknown keys: " + | |
Ember.inspect(filter.call(keysFunc(data), function(key) { | |
return !(key === 'id' || key === 'links' || get(type, 'fields').has(key) || key.match(/Type$/)); | |
})) + ". Make sure they've been defined in your model.", | |
filter.call(keysFunc(data), function(key) { | |
return !(key === 'id' || key === 'links' || get(type, 'fields').has(key) || key.match(/Type$/)); | |
}).length === 0, | |
{ id: 'ds.store.unknown-keys-in-payload' } | |
); | |
} | |
// Actually load the record into the store. | |
var internalModel = this._load(data); | |
var store = this; | |
this._backburner.join(function() { | |
store._backburner.schedule('normalizeRelationships', store, '_setupRelationships', internalModel, type, data); | |
}); | |
return internalModel; | |
}, | |
_load: function(data) { | |
var id = coerceId(data.id); //just makes sure this is a string | |
var internalModel = this._internalModelForId(data.type, id); | |
internalModel.setupData(data); | |
this.recordArrayManager.recordDidChange(internalModel); | |
return internalModel; | |
}, | |
_internalModelForId: function(typeName, inputId) { | |
var typeClass = this.modelFor(typeName); //gets model class | |
var id = coerceId(inputId); //makes sure id is a string | |
var idToRecord = this.typeMapFor(typeClass).idToRecord; //store hash by type with link from id's to records | |
var record = idToRecord[id]; //pull the record off that hash with the supplied inputId | |
if (!record || !idToRecord[id]) { //if record found on typeMap, use it | |
record = this.buildInternalModel(typeClass, id); //otherwise....make the internal model | |
} | |
return record; | |
}, | |
buildInternalModel: function(type, id, data) { | |
var typeMap = this.typeMapFor(type); | |
var idToRecord = typeMap.idToRecord; //get hash with id's to record for this type | |
Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToRecord[id]); | |
Ember.assert("`" + Ember.inspect(type)+ "` does not appear to be an ember-data model", (typeof type._create === 'function') ); | |
// lookupFactory should really return an object that creates | |
// instances with the injections applied | |
var internalModel = new InternalModel(type, id, this, this.container, data); | |
// if we're creating an item, this process will be done | |
// later, once the object has been persisted. | |
if (id) { | |
idToRecord[id] = internalModel; //make the association in the hash with supplied id to the record | |
} | |
typeMap.records.push(internalModel); //push internalModel onto records array of typeMap | |
//this implies that we can call this thing WITHOUT an id and it will happily shove it into the | |
//records array without it. Not sure how we find the bugger then later unless we're expected | |
//to keep a handle on that record object somehow. | |
return internalModel; | |
}, | |
typeMapFor: function(typeClass) { | |
var typeMaps = get(this, 'typeMaps'); //store level hash | |
var guid = Ember.guidFor(typeClass); //retrieve or create a guid for any object (adds _guid to underlying object) | |
var typeMap = typeMaps[guid]; //if there's a key corresponding to the guid for the model type in question.... | |
if (typeMap) { return typeMap; } //then return that | |
typeMap = { //otherwise, we're going to make one | |
idToRecord: create(null), //object-polyfills....whatever those are.. | |
records: [], | |
metadata: create(null), | |
type: typeClass | |
}; | |
typeMaps[guid] = typeMap; | |
return typeMap; | |
}, | |
var _bind = function (fn) { | |
var args = Array.prototype.slice.call(arguments, 1); | |
return function() { | |
return fn.apply(undefined, args); | |
}; | |
}; | |
var _guard = function (promise, test) { | |
var guarded = promise['finally'](function() { | |
if (!test()) { | |
guarded._subscribers.length = 0; | |
} | |
}); | |
return guarded; | |
}; | |
var _objectIsAlive = function(object) { | |
return !(get(object, "isDestroyed") || get(object, "isDestroying")); | |
}; | |
var normalizeResponseHelper = function(serializer, store, modelClass, payload, id, requestType) { | |
if (get(serializer, 'isNewSerializerAPI')) { | |
let normalizedResponse = serializer.normalizeResponse(store, modelClass, payload, id, requestType); | |
let validationErrors = []; | |
Ember.runInDebug(() => { | |
validationErrors = validateDocumentStructure(normalizedResponse); | |
}); | |
Ember.assert(`normalizeResponse must return a valid JSON API document:\n\t* ${validationErrors.join('\n\t* ')}`, Ember.isEmpty(validationErrors)); | |
// TODO: Remove after metadata refactor | |
if (normalizedResponse.meta) { | |
store._setMetadataFor(modelClass.modelName, normalizedResponse.meta); | |
} | |
return normalizedResponse; | |
} else { | |
Ember.deprecate('Your custom serializer uses the old version of the Serializer API, with `extract` hooks. Please upgrade your serializers to the new Serializer API using `normalizeResponse` hooks instead.', false, { | |
id: 'ds.serializer.extract-hooks-deprecated', | |
until: '2.0.0' | |
}); | |
let serializerPayload = serializer.extract(store, modelClass, payload, id, requestType); | |
return _normalizeSerializerPayload(modelClass, serializerPayload); | |
} | |
}; | |
var convertResourceObject = function (payload) { | |
if (!payload) { | |
return payload; | |
} | |
var data = { | |
id: payload.id, | |
type: payload.type, | |
links: {} | |
}; | |
if (payload.attributes) { | |
var attributeKeys = keysFunc(payload.attributes); | |
forEach.call(attributeKeys, function(key) { | |
var attribute = payload.attributes[key]; | |
data[key] = attribute; | |
}); | |
} | |
if (payload.relationships) { | |
var relationshipKeys = keysFunc(payload.relationships); | |
forEach.call(relationshipKeys, function(key) { | |
var relationship = payload.relationships[key]; | |
if (relationship.hasOwnProperty('data')) { | |
data[key] = relationship.data; | |
} else if (relationship.links && relationship.links.related) { | |
data.links[key] = relationship.links.related; | |
} | |
}); | |
} | |
return data; | |
}; | |
//rest-adapter... I think I just create a custom one of these along with a custom buildURL type | |
//to build the promise and then call _commit directly on the store. | |
createRecord: function(store, type, snapshot) { | |
var data = {}; | |
var serializer = store.serializerFor(type.modelName); | |
var url = this.buildURL(type.modelName, null, snapshot, 'createRecord'); | |
serializer.serializeIntoHash(data, type, snapshot, { includeId: true }); | |
return this.ajax(url, "POST", { data: data }); | |
}, |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment