Skip to content

Commit

Permalink
Rollback Relationships
Browse files Browse the repository at this point in the history
This commit:

1. Allows one to rollback belongsTo and hasMany relationships.
2. Reintroduces dirtyRecordFor*Change hooks on the adapter that allow
   one to customize when a record becomes dirty.
3. Added 'removeDeletedFromRelationshipsPriorToSave' flag to Adapter
   that allows one to opt back into the old deleted record from many
   array behavior (pre emberjs#3539).

Known issues:

1. Rolling back a hasMany relationship from the parent side of the
   relationship does not work (doing the same from the child side works
   fine). See test that is commented out below as well as the discussion
   at the end of emberjs#2881#issuecomment-204634262

   This was previously emberjs#2881 and is related to emberjs#3698
  • Loading branch information
mmpestorich committed Oct 24, 2019
1 parent df469d4 commit 5423b99
Show file tree
Hide file tree
Showing 20 changed files with 1,405 additions and 32 deletions.
2 changes: 1 addition & 1 deletion packages/-ember-data/addon/-debug/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function instrument(method) {
@param {InternalModel} addedRecord record which
should be added/set for the relationship
*/
let assertPolymorphicType;
let assertPolymorphicType = (parentInternalModel, relationshipMeta, addedInternalModel, store) => {};

if (DEBUG) {
let checkPolymorphic = function checkPolymorphic(modelClass, addedModelClass) {
Expand Down
116 changes: 116 additions & 0 deletions packages/-ember-data/tests/integration/record-array-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,67 @@ module('unit/record-array - RecordArray', function(hooks) {
assert.equal(get(recordArray, 'length'), 0, 'record is removed from the array when it is saved');
});

test('a loaded record is removed from a record array when it is deleted (remove deleted prior to save)', async function(assert) {
assert.expect(5);
this.owner.register(
'adapter:application',
Adapter.extend({
removeDeletedFromRelationshipsPriorToSave: true,
deleteRecord() {
return resolve({ data: null });
},
shouldBackgroundReloadRecord() {
return false;
}
})
);

store.push({
data: [{
type: 'person',
id: '1',
attributes: {
name: 'Scumbag Dale'
}
}, {
type: 'person',
id: '2',
attributes: {
name: 'Scumbag Katz'
}
}, {
type: 'person',
id: '3',
attributes: {
name: 'Scumbag Bryn'
}
}, {
type: 'tag',
id: '1'
}]
});

let scumbag = store.peekRecord('person', 1);
let tag = store.peekRecord('tag', 1);

tag.get('people').addObject(scumbag);

assert.equal(get(scumbag, 'tag'), tag, "precond - the scumbag's tag has been set");

let people = tag.get('people');

assert.equal(get(people, 'length'), 1, 'precond - record array has one item');
assert.equal(get(people.objectAt(0), 'name'), 'Scumbag Dale', "item at index 0 is record with id 1");

await scumbag.deleteRecord();

assert.equal(get(people, 'length'), 0, "record is removed from the record array");

await scumbag.save();

assert.equal(get(people, 'length'), 0, 'record is still removed from the array when it is saved');
});

test("a loaded record is not removed from a record array when it is deleted even if the belongsTo side isn't defined", async function(assert) {
class Person extends Model {
@attr()
Expand Down Expand Up @@ -376,6 +437,61 @@ module('unit/record-array - RecordArray', function(hooks) {
assert.equal(tool.get('person'), scumbag, 'the tool still belongs to the record');
});

test("a loaded record is not removed from both the record array and from the belongs to, even if the belongsTo side isn't defined (remove deleted prior to save)", async function(assert) {
assert.expect(4);
this.owner.register(
'adapter:application',
Adapter.extend({
removeDeletedFromRelationshipsPriorToSave: true,
deleteRecord() {
return Promise.resolve({ data: null });
}
})
);

store.push({
data: [
{
type: 'person',
id: '1',
attributes: {
name: 'Scumbag Tom',
},
},
{
type: 'tag',
id: '1',
relationships: {
people: {
data: [{ type: 'person', id: '1' }],
},
},
},
{
type: 'tool',
id: '1',
relationships: {
person: {
data: { type: 'person', id: '1' },
},
},
}
]
});

let scumbag = store.peekRecord('person', 1);
let tag = store.peekRecord('tag', 1);
let tool = store.peekRecord('tool', 1);

assert.equal(tag.get('people.length'), 1, 'person is in the record array');
assert.equal(tool.get('person'), scumbag, 'the tool belongs to the person');

scumbag.deleteRecord();

assert.equal(tag.get('people.length'), 0, 'person is not in the record array');
assert.equal(tool.get('person'), null, 'the tool does not belong to the person');
});

// GitHub Issue #168
test('a newly created record is removed from a record array when it is deleted', async function(assert) {
let recordArray = store.peekAll('person');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class TestRecordData {
isAttrDirty(key: string) {
return false;
}
isRelationshipDirty(key: string) {
return false;
}
removeFromInverseRelationships(isNew: boolean) {}

_initRecordCreateOptions(options) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,51 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function(
assert.equal(book.get('author'), author, 'Book has an author after rollback attributes');
});

test("Rollbacking for a deleted record restores implicit relationship - async (remove deleted prior to save)", function(assert) {
env.adapter.removeDeletedFromRelationshipsPriorToSave = true;
Book.reopen({
author: DS.belongsTo('author', { async: true })
});
var book, author;
run(function() {
book = env.store.push({
data: {
id: '1',
type: 'book',
attributes: {
name: "Stanley's Amazing Adventures"
},
relationships: {
author: {
data: {
id: '2',
type: 'author'
}
}
}
}
});
author = env.store.push({
data: {
id: '2',
type: 'author',
attributes: {
name: 'Stanley'
}
}
});

});
run(() => {
author.deleteRecord();
author.rollback();
book.get('author').then((fetchedAuthor) => {
assert.equal(fetchedAuthor, author, 'Book has an author after rollback');
});
});
env.adapter.removeDeletedFromRelationshipsPriorToSave = false;
});

testInDebug('Passing a model as type to belongsTo should not work', function(assert) {
assert.expect(1);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,99 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships',
assert.equal(user.get('accounts.length'), 0, 'Accounts got rolledback correctly');
});

/* Relationship isDirty Tests */

test("Relationship isDirty at correct times when adding back removed values", function (assert) {
let user, topic1, topic2;
run(() => {
user = store.push({
data: {
type: 'user',
id: 1,
attributes: {name: 'Stanley'},
relationships: {topics: {data: [{type: 'topic', id: 1}]}}
}
});
// NOTE SB Pushing topics into store (even with updated values) does not dirty the user relationship
topic1 = store.push({data: {type: 'topic', id: 1, attributes: {title: "This year's EmberFest was great"}}});
topic2 = store.push({data: {type: 'topic', id: 2, attributes: {title: "Last year's EmberFest was great"}}});
user.get('topics').then(function (topics) {
const relationship = user._internalModel._recordData._relationships.get('topics');
assert.equal(relationship.isDirty, false, 'pushing topic1 into store does not dirty relationship');
topics.removeObject(topic1);
assert.equal(relationship.isDirty, true, 'removing topic1 dirties the relationship');
topics.addObjects([topic1, topic2]);
assert.equal(relationship.isDirty, true, 'adding topic1 and topic2 keeps the relationship dirty');
topics.removeObject(topic2);
assert.equal(relationship.isDirty, false, 'removing topic2 make the relationship not dirty again');
});
});
});
test("Relationship isDirty at correct times when removing values that were added", function (assert) {
let user, topic1, topic2, topic3;
run(() => {
user = store.push({
data: {
type: 'user',
id: 1,
attributes: {name: 'Stanley'},
relationships: {topics: {data: [{type: 'topic', id: 1}]}}
}
});
// NOTE SB Pushing topics into store (even with updated values) does not dirty the user relationship
topic1 = store.push({data: {type: 'topic', id: 1, attributes: {title: "This year's EmberFest was great"}}});
topic2 = store.push({data: {type: 'topic', id: 2, attributes: {title: "Last year's EmberFest was great"}}});
user.get('topics').then(function (topics) {
const relationship = user._internalModel._recordData._relationships.get('topics');
assert.equal(relationship.isDirty, false, 'pushing topic1 into store does not dirty relationship');
topics.addObject(topic2);
assert.equal(relationship.isDirty, true, 'adding topic2 dirties the relationship');
topics.removeObjects([topic1, topic2]);
assert.equal(relationship.isDirty, true, 'removing topic1 and topic2 keeps the relationship dirty');
topics.addObject(topic1);
assert.equal(relationship.isDirty, false, 'adding back topic1 makes relationship not dirty again');
});
});
});

/* Rollback Relationships Tests */

test("Rollback many-to-many relationships works correctly - async", function (assert) {
let user, topic1, topic2;
run(() => {
user = store.push({ data: { type: 'user', id: 1, attributes: { name: 'Stanley' }, relationships: { topics: { data: [{ type: 'topic', id: 1 }] } } } });
topic1 = store.push({ data: { type: 'topic', id: 1, attributes: { title: "This year's EmberFest was great" } } });
topic2 = store.push({ data: { type: 'topic', id: 2, attributes: { title: "Last year's EmberFest was great" } } });
topic2.get('users').addObject(user);
});
run(() => {
topic2.rollback();
topic1.get('users').then(function (fetchedUsers) {
assert.deepEqual(fetchedUsers.toArray(), [user], 'Users are still there');
});
topic2.get('users').then(function (fetchedUsers) {
assert.deepEqual(fetchedUsers.toArray(), [], 'Users are still empty');
});
user.get('topics').then(function (fetchedTopics) {
assert.deepEqual(fetchedTopics.toArray(), [topic1], 'Topics are still there');
});
});
});

test("Rollback many-to-many relationships works correctly - sync", function (assert) {
let user, account1, account2;
run(() => {
user = store.push({ data: { type: 'user', id: 1, attributes: { name: 'Stanley' }, relationships: { accounts: { data: [{ type: 'account', id: 1 }] } } } });
account1 = store.push({ data: { type: 'account', id: 1, attributes: { state: 'lonely' } } });
account2 = store.push({ data: { type: 'account', id: 2, attributes: { state: 'content' } } });
account2.get('users').addObject(user);
});
run(account2, 'rollback');
assert.deepEqual(user.get('accounts').toArray(), [account1], 'Accounts are still there');
assert.deepEqual(account1.get('users').toArray(), [user], 'Users are still there');
assert.deepEqual(account2.get('users').toArray(), [], 'Users are still empty');
});

todo(
'Re-loading a removed record should re add it to the relationship when the removed record is the last one in the relationship',
function(assert) {
Expand Down
Loading

0 comments on commit 5423b99

Please sign in to comment.