Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rollback Relationships #2881

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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