Skip to content

Instantly share code, notes, and snippets.

@robneville73
Created November 3, 2015 16:23
Show Gist options
  • Save robneville73/f18e88468de75274a1d7 to your computer and use it in GitHub Desktop.
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
//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