Skip to content

Commit

Permalink
Rollback Relationships
Browse files Browse the repository at this point in the history
  • Loading branch information
mmpestorich committed Mar 31, 2015
1 parent e1887fd commit b83c4c4
Show file tree
Hide file tree
Showing 13 changed files with 325 additions and 24 deletions.
20 changes: 20 additions & 0 deletions packages/ember-data/lib/system/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,26 @@ var Adapter = Ember.Object.extend({
*/
groupRecordsForFindMany: function(store, snapshots) {
return [snapshots];
},

dirtyRecordForAttrChange: function (record, context) {
return context.value !== context.originalValue;
},

dirtyRecordForBelongsToChange: function (record, context) {
return context.value !== context.originalValue;
},

dirtyRecordForHasManyChange: function (record, context) {
var relationshipType = record.constructor.determineRelationshipType({ key: context.key, kind: context.kind });

if (relationshipType === 'manyToMany' || relationshipType === 'manyToNone') {
if (context.recordAdded) {
return !context.originalValue.has(context.recordAdded);
}
return context.originalValue.has(context.recordRemoved);
}
return false;
}
});

Expand Down
7 changes: 5 additions & 2 deletions packages/ember-data/lib/system/model/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,9 @@ export default function attr(type, options) {

var meta = {
type: type,
kind: 'attr',
isAttribute: true,
key: null,
options: options
};

Expand All @@ -307,8 +309,9 @@ export default function attr(type, options) {
this._attributes[key] = value;

this.send('didSetProperty', {
name: key,
oldValue: oldValue,
key: key,
kind: 'attr',
isAttribute: true,
originalValue: this._data[key],
value: value
});
Expand Down
24 changes: 16 additions & 8 deletions packages/ember-data/lib/system/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,15 @@ var Model = Ember.Object.extend(Ember.Evented, {
});
},

rollbackRelationships: function() {
this.eachRelationship(function(name, relationship) {
this._relationships[name].rollback();
}, this);
var model = this;
forEach.call(Ember.keys(this._implicitRelationships), function(key) {
model._implicitRelationships[key].rollback();
});
},

/**
@method updateRecordArrays
Expand Down Expand Up @@ -1002,15 +1011,14 @@ var Model = Ember.Object.extend(Ember.Evented, {
set(this, 'isError', false);
}

//Eventually rollback will always work for relationships
//For now we support it only out of deleted state, because we
//have an explicit way of knowing when the server acked the relationship change
if (get(this, 'isDeleted')) {
this.reconnectRelationships();
}
var isDeleted = get(this, 'isDeleted');
var isNew = get(this, 'isNew');

if (get(this, 'isNew')) {
this.clearRelationships();
if (isDeleted || isNew) {
if (isDeleted) { this.reconnectRelationships(); }
if (isNew) { this.clearRelationships(); }
} else {
this.rollbackRelationships();
}

if (!get(this, 'isValid')) {
Expand Down
45 changes: 37 additions & 8 deletions packages/ember-data/lib/system/model/states.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

var get = Ember.get;
var set = Ember.set;
var classify = Ember.String.classify;

/*
This file encapsulates the various states that a record can transition
through during its lifecycle.
Expand Down Expand Up @@ -174,11 +176,21 @@ var set = Ember.set;
*/

function didSetProperty(record, context) {
if (context.value === context.originalValue) {
delete record._attributes[context.name];
record.send('propertyWasReset', context.name);
} else if (context.value !== context.oldValue) {
var adapter = get(record, 'store').adapterFor(record.constructor);
var fn = adapter['dirtyRecordFor' + classify(context.kind) + 'Change'];

if (fn(record, context)) {
if (context.isRelationship) {
record._relationships[context.key].isDirty = true;
}
record.send('becomeDirty');
} else {
if (context.isRelationship) {
record._relationships[context.key].isDirty = false;
} else {
delete record._attributes[context.key];
}
record.send('propertyWasReset', context.key);
}

record.updateRecordArraysLater();
Expand Down Expand Up @@ -247,8 +259,14 @@ var DirtyState = {
loadingData: Ember.K,

propertyWasReset: function(record, name) {
var length = Ember.keys(record._attributes).length;
var stillDirty = length > 0;
var stillDirty = Ember.keys(record._attributes).length > 0;

if (stillDirty) { return; }

var relationships = record._relationships;
record.constructor.eachRelationship(function (key) {
stillDirty |= relationships[key].isDirty;
});

if (!stillDirty) { record.send('rolledBack'); }
},
Expand Down Expand Up @@ -329,8 +347,7 @@ var DirtyState = {
},

didSetProperty: function(record, context) {
get(record, 'errors').remove(context.name);

get(record, 'errors').remove(context.key);
didSetProperty(record, context);
},

Expand Down Expand Up @@ -475,6 +492,9 @@ var RootState = {
isEmpty: true,

// EVENTS

didSetProperty: Ember.K,

loadingData: function(record, promise) {
record._loadingPromise = promise;
record.transitionTo('loading');
Expand Down Expand Up @@ -507,6 +527,9 @@ var RootState = {
},

// EVENTS

didSetProperty: Ember.K,

pushedData: function(record) {
record.transitionTo('loaded.saved');
record.triggerLater('didLoad');
Expand Down Expand Up @@ -624,6 +647,8 @@ var RootState = {

// EVENTS

didSetProperty: Ember.K,

willCommit: function(record) {
record.transitionTo('inFlight');
},
Expand Down Expand Up @@ -690,6 +715,10 @@ var RootState = {
record.triggerLater('didCommit', record);
},

// EVENTS

didSetProperty: Ember.K,

willCommit: Ember.K,

didCommit: Ember.K
Expand Down
8 changes: 8 additions & 0 deletions packages/ember-data/lib/system/relationships/belongs-to.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,15 @@ function belongsTo(type, options) {
*/
Model.reopen({
notifyBelongsToChanged: function(key) {
var relationship = this._relationships[key];
this.notifyPropertyChange(key);
this.send('didSetProperty', {
key: key,
kind: 'belongsTo',
isRelationship: true,
originalValue: relationship.canonicalState,
value: relationship.inverseRecord
});
}
});

Expand Down
2 changes: 1 addition & 1 deletion packages/ember-data/lib/system/relationships/ext.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Model.reopen({
// populated by the `DS.belongsTo` helper when it is creating
// the computed property.
var meta = value.meta();

meta.key = key;
meta.parentType = proto.constructor;
}
}
Expand Down
21 changes: 18 additions & 3 deletions packages/ember-data/lib/system/relationships/has-many.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,28 @@ function hasMany(type, options) {
}

Model.reopen({
notifyHasManyAdded: function(key) {
notifyHasManyAdded: function(key, record) {
//We need to notifyPropertyChange in the adding case because we need to make sure
//we fetch the newly added record in case it is unloaded
//TODO(Igor): Consider whether we could do this only if the record state is unloaded

//Goes away once hasMany is double promisified
this.notifyPropertyChange(key);
this.send('didSetProperty', {
key: key,
kind: 'hasMany',
isRelationship: true,
originalValue: this._relationships[key].canonicalMembers,
recordAdded: record
});
},

notifyHasManyRemoved: function(key, record) {
this.send('didSetProperty', {
key: key,
kind: 'hasMany',
isRelationship: true,
originalValue: this._relationships[key].canonicalMembers,
recordRemoved: record
});
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,8 @@ BelongsToRelationship.prototype.getRecord = function() {
}
};

BelongsToRelationship.prototype.rollback = function() {
this.setRecord(this.canonicalState);
};

export default BelongsToRelationship;
31 changes: 30 additions & 1 deletion packages/ember-data/lib/system/relationships/state/has-many.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ ManyRelationship.prototype.notifyRecordRelationshipAdded = function(record, idx)
this.record.notifyHasManyAdded(this.key, record, idx);
};

ManyRelationship.prototype.notifyRecordRelationshipRemoved = function(record) {
this.record.notifyHasManyRemoved(this.key, record);
};

ManyRelationship.prototype.reload = function() {
var self = this;
if (this.link) {
Expand Down Expand Up @@ -160,7 +164,7 @@ ManyRelationship.prototype.findRecords = function() {
});
};
ManyRelationship.prototype.notifyHasManyChanged = function() {
this.record.notifyHasManyAdded(this.key);
this.record.notifyPropertyChange(this.key);
};

ManyRelationship.prototype.getRecords = function() {
Expand Down Expand Up @@ -190,6 +194,31 @@ ManyRelationship.prototype.getRecords = function() {
}
};

ManyRelationship.prototype.rollback = function() {
var canonicalMembers = this.canonicalMembers;
var canonicalState = this.canonicalState;
var currentState = this.manyArray.currentState;
var l = canonicalMembers.size;
var i;

for (i = 0; i < l; i++) {
var canonicalRecord = canonicalState[i];
var currentRecord = currentState[i];

if (canonicalRecord === currentRecord) { continue; }

if (!canonicalMembers.has(currentRecord)) {
this.removeRecord(currentRecord);
}

this.removeRecord(canonicalRecord);
this.addRecord(canonicalRecord, i);
}
this.removeRecords(currentState.slice(canonicalState.length));
this.record.notifyPropertyChange(this.key);
this.record.send('propertyWasReset', this.key);
};

function setForArray(array) {
var set = new OrderedSet();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var Relationship = function(store, record, inverseKey, relationshipMeta) {
this.inverseKeyForImplicit = this.store.modelFor(this.record.constructor).typeKey + this.key;
this.linkPromise = null;
this.hasData = false;
this.isDirty = false;
};

Relationship.prototype = {
Expand Down Expand Up @@ -45,6 +46,8 @@ Relationship.prototype = {
}, this);
},

rollback: Ember.K,

removeRecords: function(records) {
var self = this;
forEach(records, function(record) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ test("Rollbacking a created record that has a ManyToMany relationship works corr
});
});

test("Deleting a record that has a hasMany relationship removes it from the otherMany array but does not remove the other record from itself - sync", function () {
test("Creating a record that has a hasMany relationship removes it from the otherMany array but does not remove the other record from itself - sync", function () {
var account, user;
run(function() {
account = store.push('account', { id: 2 , state: 'lonely' });
Expand All @@ -271,6 +271,46 @@ test("Deleting a record that has a hasMany relationship removes it from the othe
equal(user.get('accounts.length'), undefined, 'Accounts got rolledback correctly');
});

test("Rollbacking a record that has a ManyToMany relationship works correctly - async", function () {
var user, topic1, topic2;
run(function() {
user = store.push('user', { id: 1, name: 'Stanley', topics: [1] });
topic1 = store.push('topic', { id: 1, title: "This year's EmberFest was great" });
topic2 = store.push('topic', { id: 2, title: "Last year's EmberFest was great" });
});
run(function() {
topic2.get('users').addObject(user);
topic2.rollback();
});
run(function() {
topic1.get('users').then(async(function(fetchedUsers) {
deepEqual(fetchedUsers.toArray(), [user], 'Users are still there');
}));
topic2.get('users').then(async(function(fetchedUsers) {
deepEqual(fetchedUsers.toArray(), [], 'Users are still empty');
}));
user.get('topics').then(async(function(fetchedTopics) {
deepEqual(fetchedTopics.toArray(), [topic1], 'Topics are still there');
}));
});
});

test("Rollbacking a record that has a ManyToMany relationship works correctly - sync", function () {
var user, account1, account2;
run(function() {
user = store.push('user', { id: 1, name: 'Stanley', accounts: [1] });
account1 = store.push('account', { id: 1 , state: 'lonely' });
account2 = store.push('account', { id: 2 , state: 'content' });
});
run(function() {
account2.get('users').addObject(user);
account2.rollback();
});
deepEqual(user.get('accounts').toArray(), [account1], 'Accounts are still there');
deepEqual(account1.get('users').toArray(), [user], 'Users are still there');
deepEqual(account2.get('users').toArray(), [], 'Users are still empty');
});


test("Re-loading a removed record should re add it to the relationship when the removed record is the last one in the relationship", function () {
var account, ada, byron;
Expand Down
Loading

0 comments on commit b83c4c4

Please sign in to comment.