diff --git a/packages/-ember-data/tests/acceptance/relationships/tracking-record-state-test.js b/packages/-ember-data/tests/acceptance/relationships/tracking-record-state-test.js index a3b249660ce..537f2b44187 100644 --- a/packages/-ember-data/tests/acceptance/relationships/tracking-record-state-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/tracking-record-state-test.js @@ -34,7 +34,7 @@ module('tracking state flags on a record', function (hooks) { enumerable: true, configurable: true, get() { - tag.reg; // subscribe + tag.rev; // subscribe if (_isDirty && !_isUpdating) { _isUpdating = true; resolve(desc.get.call(this)).then((v) => { diff --git a/packages/-ember-data/tests/integration/records/relationship-changes-test.js b/packages/-ember-data/tests/integration/records/relationship-changes-test.js index e3c7d707ec2..272ad8d50dc 100644 --- a/packages/-ember-data/tests/integration/records/relationship-changes-test.js +++ b/packages/-ember-data/tests/integration/records/relationship-changes-test.js @@ -4,6 +4,7 @@ import { run } from '@ember/runloop'; import { module, test } from 'qunit'; +import { gte } from 'ember-compatibility-helpers'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; @@ -106,60 +107,62 @@ module('integration/records/relationship-changes - Relationship changes', functi this.owner.register('serializer:application', JSONAPISerializer.extend()); }); - test('Calling push with relationship triggers observers once if the relationship was empty and is added to', function (assert) { - assert.expect(1); - - let store = this.owner.lookup('service:store'); - let person = null; - let observerCount = 0; - - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - siblings: { - data: [], + if (!gte('4.0.0')) { + test('Calling push with relationship triggers observers once if the relationship was empty and is added to', function (assert) { + assert.expect(1); + + let store = this.owner.lookup('service:store'); + let person = null; + let observerCount = 0; + + run(() => { + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [], + }, }, }, - }, + }); + person = store.peekRecord('person', 'wat'); }); - person = store.peekRecord('person', 'wat'); - }); - run(() => { - person.addObserver('siblings.[]', function () { - observerCount++; + run(() => { + person.addObserver('siblings.[]', function () { + observerCount++; + }); + // prime the pump + person.get('siblings'); }); - // prime the pump - person.get('siblings'); - }); - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: {}, - relationships: { - siblings: { - data: [sibling1Ref], + run(() => { + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref], + }, }, }, - }, - included: [sibling1], + included: [sibling1], + }); }); - }); - run(() => { - assert.ok(observerCount >= 1, 'siblings observer should be triggered at least once'); + run(() => { + assert.ok(observerCount >= 1, 'siblings observer should be triggered at least once'); + }); }); - }); + } test('Calling push with relationship recalculates computed alias property if the relationship was empty and is added to', function (assert) { assert.expect(1); @@ -210,7 +213,7 @@ module('integration/records/relationship-changes - Relationship changes', functi run(() => { let cpResult = get(obj, 'siblings').toArray(); - assert.equal(cpResult.length, 1, 'siblings cp should have recalculated'); + assert.strictEqual(cpResult.length, 1, 'siblings cp should have recalculated'); obj.destroy(); }); }); @@ -264,7 +267,7 @@ module('integration/records/relationship-changes - Relationship changes', functi run(() => { let cpResult = get(obj, 'firstSibling'); - assert.equal(get(cpResult, 'id'), 1, 'siblings cp should have recalculated'); + assert.strictEqual(get(cpResult, 'id'), '1', 'siblings cp should have recalculated'); obj.destroy(); }); }); @@ -491,466 +494,490 @@ module('integration/records/relationship-changes - Relationship changes', functi }); run(() => { - assert.equal(observerCount, 0, 'siblings observer should not be triggered'); + assert.strictEqual(observerCount, 0, 'siblings observer should not be triggered'); }); person.removeObserver('siblings.[]', observerMethod); }); - test('Calling push with relationship triggers willChange and didChange with detail when appending', async function (assert) { - let store = this.owner.lookup('service:store'); - - let willChangeCount = 0; - let didChangeCount = 0; - - let observer = { - arrayWillChange(array, start, removing, adding) { - willChangeCount++; - assert.equal(start, 1, 'willChange.start'); - assert.equal(removing, 0, 'willChange.removing'); - assert.equal(adding, 1, 'willChange.adding'); - }, - - arrayDidChange(array, start, removed, added) { - didChangeCount++; - assert.equal(start, 1, 'didChange.start'); - assert.equal(removed, 0, 'didChange.removed'); - assert.equal(added, 1, 'didChange.added'); - }, - }; - - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - siblings: { - data: [sibling1Ref], - }, - }, - }, - included: [sibling1], - }); - - let person = store.peekRecord('person', 'wat'); - let siblings = await person.siblings; - - // flush initial state since - // nothing is consuming us. - // else the test will fail because we will - // (correctly) not notify the array observer - // as there is still a pending notification - siblings.length; - - siblings.addArrayObserver(observer); - - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: {}, - relationships: { - siblings: { - data: [sibling1Ref, sibling2Ref], - }, - }, - }, - included: [sibling2], - }); - - assert.equal(willChangeCount, 1, 'willChange observer should be triggered once'); - assert.equal(didChangeCount, 1, 'didChange observer should be triggered once'); - - siblings.removeArrayObserver(observer); - }); - - test('Calling push with relationship triggers willChange and didChange with detail when truncating', function (assert) { - let store = this.owner.lookup('service:store'); - - let willChangeCount = 0; - let didChangeCount = 0; - - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - siblings: { - data: [sibling1Ref, sibling2Ref], + if (!gte('4.0.0')) { + test('Calling push with relationship triggers willChange and didChange with detail when appending', async function (assert) { + assert.expectDeprecation( + async () => { + let store = this.owner.lookup('service:store'); + + let willChangeCount = 0; + let didChangeCount = 0; + + let observer = { + arrayWillChange(array, start, removing, adding) { + willChangeCount++; + assert.strictEqual(start, 1, 'willChange.start'); + assert.strictEqual(removing, 0, 'willChange.removing'); + assert.strictEqual(adding, 1, 'willChange.adding'); }, - }, - }, - included: [sibling1, sibling2], - }); - }); - - let person = store.peekRecord('person', 'wat'); - let siblings = run(() => person.get('siblings')); - - // flush initial state since - // nothing is consuming us. - // else the test will fail because we will - // (correctly) not notify the array observer - // as there is still a pending notification - siblings.length; - - let observer = { - arrayWillChange(array, start, removing, adding) { - willChangeCount++; - assert.equal(start, 1); - assert.equal(removing, 1); - assert.equal(adding, 0); - }, - - arrayDidChange(array, start, removed, added) { - didChangeCount++; - assert.equal(start, 1); - assert.equal(removed, 1); - assert.equal(added, 0); - }, - }; - - siblings.addArrayObserver(observer); - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: {}, - relationships: { - siblings: { - data: [sibling1Ref], + arrayDidChange(array, start, removed, added) { + didChangeCount++; + assert.strictEqual(start, 1, 'didChange.start'); + assert.strictEqual(removed, 0, 'didChange.removed'); + assert.strictEqual(added, 1, 'didChange.added'); }, - }, - }, - included: [], - }); - }); - - assert.equal(willChangeCount, 1, 'willChange observer should be triggered once'); - assert.equal(didChangeCount, 1, 'didChange observer should be triggered once'); - - siblings.removeArrayObserver(observer); - }); - - test('Calling push with relationship triggers willChange and didChange with detail when inserting at front', async function (assert) { - let store = this.owner.lookup('service:store'); - - let willChangeCount = 0; - let didChangeCount = 0; - - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - siblings: { - data: [sibling2Ref], + }; + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [sibling1Ref], + }, + }, }, - }, - }, - included: [sibling2], - }); - }); - let person = store.peekRecord('person', 'wat'); - - let observer = { - arrayWillChange(array, start, removing, adding) { - willChangeCount++; - assert.equal(start, 0, 'change will start at the beginning'); - assert.equal(removing, 0, 'we have no removals'); - assert.equal(adding, 1, 'we have one insertion'); - }, - - arrayDidChange(array, start, removed, added) { - didChangeCount++; - assert.equal(start, 0, 'change did start at the beginning'); - assert.equal(removed, 0, 'change had no removals'); - assert.equal(added, 1, 'change had one insertion'); - }, - }; - - const siblingsProxy = person.siblings; - const siblings = await siblingsProxy; - - // flush initial state since - // nothing is consuming us. - // else the test will fail because we will - // (correctly) not notify the array observer - // as there is still a pending notification - siblingsProxy.length; - - siblingsProxy.addArrayObserver(observer); - - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: {}, - relationships: { - siblings: { - data: [sibling1Ref, sibling2Ref], - }, - }, - }, - included: [sibling1], - }); - - assert.equal(willChangeCount, 1, 'willChange observer should be triggered once'); - assert.equal(didChangeCount, 1, 'didChange observer should be triggered once'); - assert.deepEqual( - siblings.map((i) => i.id), - ['1', '2'], - 'We have the correct siblings' - ); - - siblingsProxy.removeArrayObserver(observer); - }); - - test('Calling push with relationship triggers willChange and didChange with detail when inserting in middle', function (assert) { - let store = this.owner.lookup('service:store'); + included: [sibling1], + }); + + let person = store.peekRecord('person', 'wat'); + let siblings = await person.siblings; + + // flush initial state since + // nothing is consuming us. + // else the test will fail because we will + // (correctly) not notify the array observer + // as there is still a pending notification + siblings.length; + + siblings.addArrayObserver(observer); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref, sibling2Ref], + }, + }, + }, + included: [sibling2], + }); - let willChangeCount = 0; - let didChangeCount = 0; + assert.strictEqual(willChangeCount, 1, 'willChange observer should be triggered once'); + assert.strictEqual(didChangeCount, 1, 'didChange observer should be triggered once'); - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - siblings: { - data: [sibling1Ref, sibling3Ref], - }, - }, + siblings.removeArrayObserver(observer); }, - included: [sibling1, sibling3], - }); - }); - let person = store.peekRecord('person', 'wat'); - let observer = { - arrayWillChange(array, start, removing, adding) { - willChangeCount++; - assert.equal(start, 1); - assert.equal(removing, 0); - assert.equal(adding, 1); - }, - arrayDidChange(array, start, removed, added) { - didChangeCount++; - assert.equal(start, 1); - assert.equal(removed, 0); - assert.equal(added, 1); - }, - }; + { id: 'array-observers', count: 2, when: { ember: '>=3.26.0' } } + ); + }); + test('Calling push with relationship triggers willChange and didChange with detail when truncating', async function (assert) { + assert.expectDeprecation( + async () => { + let store = this.owner.lookup('service:store'); + + let willChangeCount = 0; + let didChangeCount = 0; + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [sibling1Ref, sibling2Ref], + }, + }, + }, + included: [sibling1, sibling2], + }); + + let person = store.peekRecord('person', 'wat'); + let siblingsPromise = person.siblings; + + await siblingsPromise; + + // flush initial state since + // nothing is consuming us. + // else the test will fail because we will + // (correctly) not notify the array observer + // as there is still a pending notification + siblingsPromise.length; + + let observer = { + arrayWillChange(array, start, removing, adding) { + willChangeCount++; + assert.strictEqual(start, 1); + assert.strictEqual(removing, 1); + assert.strictEqual(adding, 0); + }, - let siblings = run(() => person.get('siblings')); + arrayDidChange(array, start, removed, added) { + didChangeCount++; + assert.strictEqual(start, 1); + assert.strictEqual(removed, 1); + assert.strictEqual(added, 0); + }, + }; + + siblingsPromise.addArrayObserver(observer); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref], + }, + }, + }, + included: [], + }); - // flush initial state since - // nothing is consuming us. - // else the test will fail because we will - // (correctly) not notify the array observer - // as there is still a pending notification - siblings.length; + assert.strictEqual(willChangeCount, 1, 'willChange observer should be triggered once'); + assert.strictEqual(didChangeCount, 1, 'didChange observer should be triggered once'); - siblings.addArrayObserver(observer); + siblingsPromise.removeArrayObserver(observer); + }, + { id: 'array-observers', count: 2, when: { ember: '>=3.26.0' } } + ); + }); + + test('Calling push with relationship triggers willChange and didChange with detail when inserting at front', async function (assert) { + assert.expectDeprecation( + async () => { + let store = this.owner.lookup('service:store'); + + let willChangeCount = 0; + let didChangeCount = 0; + + run(() => { + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [sibling2Ref], + }, + }, + }, + included: [sibling2], + }); + }); + let person = store.peekRecord('person', 'wat'); + + let observer = { + arrayWillChange(array, start, removing, adding) { + willChangeCount++; + assert.strictEqual(start, 0, 'change will start at the beginning'); + assert.strictEqual(removing, 0, 'we have no removals'); + assert.strictEqual(adding, 1, 'we have one insertion'); + }, - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: {}, - relationships: { - siblings: { - data: [sibling1Ref, sibling2Ref, sibling3Ref], + arrayDidChange(array, start, removed, added) { + didChangeCount++; + assert.strictEqual(start, 0, 'change did start at the beginning'); + assert.strictEqual(removed, 0, 'change had no removals'); + assert.strictEqual(added, 1, 'change had one insertion'); }, - }, + }; + + const siblingsProxy = person.siblings; + const siblings = await siblingsProxy; + + // flush initial state since + // nothing is consuming us. + // else the test will fail because we will + // (correctly) not notify the array observer + // as there is still a pending notification + siblingsProxy.length; + + siblingsProxy.addArrayObserver(observer); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref, sibling2Ref], + }, + }, + }, + included: [sibling1], + }); + + assert.strictEqual(willChangeCount, 1, 'willChange observer should be triggered once'); + assert.strictEqual(didChangeCount, 1, 'didChange observer should be triggered once'); + assert.deepEqual( + siblings.map((i) => i.id), + ['1', '2'], + 'We have the correct siblings' + ); + + siblingsProxy.removeArrayObserver(observer); }, - included: [sibling2], - }); - }); - - assert.equal(willChangeCount, 1, 'willChange observer should be triggered once'); - assert.equal(didChangeCount, 1, 'didChange observer should be triggered once'); - - siblings.removeArrayObserver(observer); - }); - - test('Calling push with relationship triggers willChange and didChange with detail when replacing different length in middle', function (assert) { - let store = this.owner.lookup('service:store'); - - let willChangeCount = 0; - let didChangeCount = 0; - - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - siblings: { - data: [sibling1Ref, sibling2Ref, sibling3Ref], + { id: 'array-observers', count: 2, when: { ember: '>=3.26.0' } } + ); + }); + + test('Calling push with relationship triggers willChange and didChange with detail when inserting in middle', function (assert) { + assert.expectDeprecation( + async () => { + let store = this.owner.lookup('service:store'); + + let willChangeCount = 0; + let didChangeCount = 0; + + run(() => { + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [sibling1Ref, sibling3Ref], + }, + }, + }, + included: [sibling1, sibling3], + }); + }); + let person = store.peekRecord('person', 'wat'); + let observer = { + arrayWillChange(array, start, removing, adding) { + willChangeCount++; + assert.strictEqual(start, 1); + assert.strictEqual(removing, 0); + assert.strictEqual(adding, 1); }, - }, + arrayDidChange(array, start, removed, added) { + didChangeCount++; + assert.strictEqual(start, 1); + assert.strictEqual(removed, 0); + assert.strictEqual(added, 1); + }, + }; + + let siblings = run(() => person.get('siblings')); + + // flush initial state since + // nothing is consuming us. + // else the test will fail because we will + // (correctly) not notify the array observer + // as there is still a pending notification + siblings.length; + + siblings.addArrayObserver(observer); + + run(() => { + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref, sibling2Ref, sibling3Ref], + }, + }, + }, + included: [sibling2], + }); + }); + + assert.strictEqual(willChangeCount, 1, 'willChange observer should be triggered once'); + assert.strictEqual(didChangeCount, 1, 'didChange observer should be triggered once'); + + siblings.removeArrayObserver(observer); }, - included: [sibling1, sibling2, sibling3], - }); - }); - - let person = store.peekRecord('person', 'wat'); - let observer = { - arrayWillChange(array, start, removing, adding) { - willChangeCount++; - assert.equal(start, 1); - assert.equal(removing, 1); - assert.equal(adding, 2); - }, - - arrayDidChange(array, start, removed, added) { - didChangeCount++; - assert.equal(start, 1); - assert.equal(removed, 1); - assert.equal(added, 2); - }, - }; - - let siblings = run(() => person.get('siblings')); - // flush initial state since - // nothing is consuming us. - // else the test will fail because we will - // (correctly) not notify the array observer - // as there is still a pending notification - siblings.length; - siblings.addArrayObserver(observer); + { id: 'array-observers', count: 2, when: { ember: '>=3.26.0' } } + ); + }); + + test('Calling push with relationship triggers willChange and didChange with detail when replacing different length in middle', function (assert) { + assert.expectDeprecation( + async () => { + let store = this.owner.lookup('service:store'); + + let willChangeCount = 0; + let didChangeCount = 0; + + run(() => { + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [sibling1Ref, sibling2Ref, sibling3Ref], + }, + }, + }, + included: [sibling1, sibling2, sibling3], + }); + }); + + let person = store.peekRecord('person', 'wat'); + let observer = { + arrayWillChange(array, start, removing, adding) { + willChangeCount++; + assert.strictEqual(start, 1); + assert.strictEqual(removing, 1); + assert.strictEqual(adding, 2); + }, - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: {}, - relationships: { - siblings: { - data: [sibling1Ref, sibling4Ref, sibling5Ref, sibling3Ref], + arrayDidChange(array, start, removed, added) { + didChangeCount++; + assert.strictEqual(start, 1); + assert.strictEqual(removed, 1); + assert.strictEqual(added, 2); }, - }, + }; + + let siblings = run(() => person.get('siblings')); + // flush initial state since + // nothing is consuming us. + // else the test will fail because we will + // (correctly) not notify the array observer + // as there is still a pending notification + siblings.length; + siblings.addArrayObserver(observer); + + run(() => { + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref, sibling4Ref, sibling5Ref, sibling3Ref], + }, + }, + }, + included: [sibling4, sibling5], + }); + }); + + assert.strictEqual(willChangeCount, 1, 'willChange observer should be triggered once'); + assert.strictEqual(didChangeCount, 1, 'didChange observer should be triggered once'); + + siblings.removeArrayObserver(observer); }, - included: [sibling4, sibling5], - }); + { id: 'array-observers', count: 2, when: { ember: '>=3.26.0' } } + ); }); - assert.equal(willChangeCount, 1, 'willChange observer should be triggered once'); - assert.equal(didChangeCount, 1, 'didChange observer should be triggered once'); - - siblings.removeArrayObserver(observer); - }); - - test('Calling push with updated belongsTo relationship trigger observer', function (assert) { - assert.expect(1); + test('Calling push with updated belongsTo relationship trigger observer', function (assert) { + assert.expect(1); - let store = this.owner.lookup('service:store'); - let observerCount = 0; + let store = this.owner.lookup('service:store'); + let observerCount = 0; - run(() => { - let post = store.push({ - data: { - type: 'post', - id: '1', - relationships: { - author: { - data: { type: 'author', id: '2' }, + run(() => { + let post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + author: { + data: { type: 'author', id: '2' }, + }, }, }, - }, - included: [ - { - id: 2, - type: 'author', - }, - ], - }); - - post.get('author'); - - post.addObserver('author', function () { - observerCount++; - }); - - store.push({ - data: { - type: 'post', - id: '1', - relationships: { - author: { - data: { type: 'author', id: '3' }, + included: [ + { + id: 2, + type: 'author', + }, + ], + }); + + post.get('author'); + + post.addObserver('author', function () { + observerCount++; + }); + + store.push({ + data: { + type: 'post', + id: '1', + relationships: { + author: { + data: { type: 'author', id: '3' }, + }, }, }, - }, + }); }); + + assert.strictEqual(observerCount, 1, 'author observer should be triggered once'); }); - assert.equal(observerCount, 1, 'author observer should be triggered once'); - }); + test('Calling push with same belongsTo relationship does not trigger observer', function (assert) { + assert.expect(1); - test('Calling push with same belongsTo relationship does not trigger observer', function (assert) { - assert.expect(1); - - let store = this.owner.lookup('service:store'); - let observerCount = 0; + let store = this.owner.lookup('service:store'); + let observerCount = 0; - run(() => { - let post = store.push({ - data: { - type: 'post', - id: '1', - relationships: { - author: { - data: { type: 'author', id: '2' }, + run(() => { + let post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + author: { + data: { type: 'author', id: '2' }, + }, }, }, - }, - }); + }); - post.addObserver('author', function () { - observerCount++; - }); + post.addObserver('author', function () { + observerCount++; + }); - store.push({ - data: { - type: 'post', - id: '1', - relationships: { - author: { - data: { type: 'author', id: '2' }, + store.push({ + data: { + type: 'post', + id: '1', + relationships: { + author: { + data: { type: 'author', id: '2' }, + }, }, }, - }, + }); }); - }); - assert.equal(observerCount, 0, 'author observer should not be triggered'); - }); -}); + assert.strictEqual(observerCount, 0, 'author observer should not be triggered'); + }); + } +}); \ No newline at end of file diff --git a/packages/-ember-data/tests/integration/relationships/has-many-test.js b/packages/-ember-data/tests/integration/relationships/has-many-test.js index 173790fc267..52d6c49c816 100644 --- a/packages/-ember-data/tests/integration/relationships/has-many-test.js +++ b/packages/-ember-data/tests/integration/relationships/has-many-test.js @@ -848,7 +848,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }); - test('A hasMany relationship can be reloaded if it was fetched via a link', function (assert) { + test('A hasMany relationship can be reloaded if it was fetched via a link', async function (assert) { let store = this.owner.lookup('service:store'); let adapter = store.adapterFor('application'); @@ -888,35 +888,28 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }; - run(function () { - run(store, 'findRecord', 'post', 1) - .then(function (post) { - return post.get('comments'); - }) - .then(function (comments) { - assert.true(comments.get('isLoaded'), 'comments are loaded'); - assert.equal(comments.get('length'), 2, 'comments have 2 length'); + let post = await store.findRecord('post', 1); + let comments = await post.comments; + assert.true(comments.get('isLoaded'), 'comments are loaded'); + assert.strictEqual(comments.get('length'), 2, 'comments have 2 length'); - adapter.findHasMany = function (store, snapshot, link, relationship) { - assert.equal(relationship.type, 'comment', 'findHasMany relationship type was Comment'); - assert.equal(relationship.key, 'comments', 'findHasMany relationship key was comments'); - assert.equal(link, '/posts/1/comments', 'findHasMany link was /posts/1/comments'); + adapter.findHasMany = function (store, snapshot, link, relationship) { + assert.strictEqual(relationship.type, 'comment', 'findHasMany relationship type was Comment'); + assert.strictEqual(relationship.key, 'comments', 'findHasMany relationship key was comments'); + assert.strictEqual(link, '/posts/1/comments', 'findHasMany link was /posts/1/comments'); - return resolve({ - data: [ - { id: 1, type: 'comment', attributes: { body: 'First' } }, - { id: 2, type: 'comment', attributes: { body: 'Second' } }, - { id: 3, type: 'comment', attributes: { body: 'Thirds' } }, - ], - }); - }; + return resolve({ + data: [ + { id: 1, type: 'comment', attributes: { body: 'First' } }, + { id: 2, type: 'comment', attributes: { body: 'Second' } }, + { id: 3, type: 'comment', attributes: { body: 'Thirds' } }, + ], + }); + }; - return comments.reload(); - }) - .then(function (newComments) { - assert.equal(newComments.get('length'), 3, 'reloaded comments have 3 length'); - }); - }); + await comments.reload(); + + assert.strictEqual(comments.length, 3, 'reloaded comments have 3 length'); }); test('A sync hasMany relationship can be reloaded if it was fetched via ids', function (assert) { diff --git a/packages/-ember-data/tests/integration/relationships/one-to-many-test.js b/packages/-ember-data/tests/integration/relationships/one-to-many-test.js index 4924af96b97..77f2a3e49c4 100644 --- a/packages/-ember-data/tests/integration/relationships/one-to-many-test.js +++ b/packages/-ember-data/tests/integration/relationships/one-to-many-test.js @@ -1624,7 +1624,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f assert.equal(account.get('user'), null, 'Account does not have the user anymore'); }); - test('createRecord updates inverse record array which has observers', function (assert) { + test('createRecord updates inverse record array which has observers', async function (assert) { let store = this.owner.lookup('service:store'); let adapter = store.adapterFor('application'); @@ -1642,21 +1642,26 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }; }; - return store.findAll('user').then((users) => { - assert.equal(users.get('length'), 1, 'Exactly 1 user'); + const users = await store.findAll('user'); + assert.strictEqual(users.get('length'), 1, 'Exactly 1 user'); - let user = users.get('firstObject'); - assert.equal(user.get('messages.length'), 0, 'Record array is initially empty'); + let user = users.get('firstObject'); + assert.strictEqual(user.get('messages.length'), 0, 'Record array is initially empty'); - // set up an observer - user.addObserver('messages.@each.title', () => {}); - user.get('messages.firstObject'); + // set up an observer + user.addObserver('messages.@each.title', () => {}); + user.get('messages.firstObject'); - let message = store.createRecord('message', { user, title: 'EmberFest was great' }); - assert.equal(user.get('messages.length'), 1, 'The message is added to the record array'); + const messages = await user.messages; - let messageFromArray = user.get('messages.firstObject'); - assert.ok(message === messageFromArray, 'Only one message record instance should be created'); - }); + assert.strictEqual(messages.length, 0, 'we have no messages'); + assert.strictEqual(user.messages.length, 0, 'we have no messages'); + + let message = store.createRecord('message', { user, title: 'EmberFest was great' }); + assert.strictEqual(messages.length, 1, 'The message is added to the record array'); + assert.strictEqual(user.messages.length, 1, 'The message is added to the record array'); + + let messageFromArray = user.messages.firstObject; + assert.true(message === messageFromArray, 'Only one message record instance should be created'); }); }); diff --git a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js index 18fe25d7f30..a2018d0bef2 100644 --- a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js +++ b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js @@ -1,4 +1,4 @@ -import { get, observer } from '@ember/object'; +import EmberObject, { get, observer } from '@ember/object'; import { run } from '@ember/runloop'; import settled from '@ember/test-helpers/settled'; @@ -8,6 +8,7 @@ import { hash, Promise as EmberPromise } from 'rsvp'; import DS from 'ember-data'; import { setupTest } from 'ember-qunit'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; import todo from '@ember-data/unpublished-test-infra/test-support/todo'; @@ -2393,6 +2394,237 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { }); }); + test('findHasMany - can push the same record in twice and fetch the link', async function (assert) { + assert.expect(5); + const { owner } = this; + + owner.register( + 'adapter:post', + class extends EmberObject { + shouldBackgroundReloadRecord() { + return false; + } + async findHasMany() { + assert.ok(true, 'findHasMany called'); + return { + data: [ + { id: '1', type: 'comment', attributes: { name: 'FIRST' } }, + { id: '2', type: 'comment', attributes: { name: 'Rails is unagi' } }, + { id: '3', type: 'comment', attributes: { name: 'What is omakase?' } }, + ], + }; + } + } + ); + + owner.register( + 'model:post', + class extends Model { + @attr name; + @hasMany('comment', { async: true, inverse: null }) comments; + } + ); + owner.register( + 'model:comment', + class extends Model { + @attr name; + } + ); + + const store = owner.lookup('service:store'); + + // preload post:1 with a related link + store.push({ + data: { + type: 'post', + id: '1', + attributes: { + name: 'Rails is omakase', + }, + relationships: { + comments: { + links: { + related: '/posts/1/comments', + }, + }, + }, + }, + }); + + // update post:1 with same related link + store.push({ + data: { + type: 'post', + id: '1', + attributes: { + name: 'Rails is still omakase', + }, + relationships: { + comments: { + links: { + related: '/posts/1/comments', + }, + }, + }, + }, + }); + + let post = store.peekRecord('post', '1'); + + const promise = post.comments; + const promise2 = post.comments; + assert.strictEqual(promise, promise2, 'we return the same PromiseManyArray each time'); + const comments = await promise; + + assert.true(promise.isFulfilled, 'comments promise is fulfilled'); + assert.strictEqual(comments.length, 3, 'The correct records are in the array'); + const promise3 = post.comments; + assert.strictEqual(promise, promise3, 'we return the same PromiseManyArray each time'); + }); + + test('fetch records with chained async has-many, ensure the leafs are retrieved', async function (assert) { + assert.expect(8); + const { owner } = this; + + owner.register( + 'adapter:application', + class extends EmberObject { + coalesceFindRequests = true; + shouldBackgroundReloadRecord() { + return false; + } + async findRecord() { + assert.ok(true, 'findRecord called'); + return { + data: { + type: 'post-author', + id: '1', + relationships: { + posts: { + data: [ + { type: 'authored-post', id: '1' }, + { type: 'authored-post', id: '2' }, + ], + }, + }, + }, + }; + } + + async findMany() { + assert.ok(true, 'findMany called'); + return { + data: [ + { + type: 'authored-post', + id: '1', + attributes: { + name: 'A post', + }, + relationships: { + author: { + data: { type: 'post-author', id: '1' }, + }, + comments: { + links: { + related: './comments', + }, + }, + }, + }, + { + type: 'authored-post', + id: '2', + attributes: { + name: 'A second post', + }, + relationships: { + author: { + data: { type: 'post-author', id: '1' }, + }, + comments: { + links: { + related: './comments', + }, + }, + }, + }, + ], + }; + } + + async findHasMany() { + assert.ok('findHasMany called'); + return { + data: [ + { + type: 'post-comment', + id: '1', + attributes: { + body: 'Some weird words', + }, + }, + { + type: 'post-comment', + id: '2', + attributes: { + body: 'Some mean words', + }, + }, + { + type: 'post-comment', + id: '3', + attributes: { + body: 'Some kind words', + }, + }, + ], + }; + } + } + ); + + owner.register( + 'model:post-author', + class extends Model { + @attr name; + @hasMany('authored-post', { async: true, inverse: 'author' }) posts; + } + ); + owner.register( + 'model:authored-post', + class extends Model { + @attr name; + @belongsTo('post-author', { async: false, inverse: 'posts' }) author; + @hasMany('post-comment', { async: true, inverse: 'post' }) comments; + } + ); + owner.register( + 'model:post-comment', + class extends Model { + @attr body; + @belongsTo('authored-post', { async: true, inverse: 'comments' }) post; + } + ); + + const store = owner.lookup('service:store'); + + const user = await store.findRecord('post-author', '1'); + const posts = await user.posts; + assert.strictEqual(posts.length, 2, 'we loaded two posts'); + const firstPost = posts.objectAt(0); + const firstPostCommentsPromise = firstPost.comments; + const originalPromise = firstPostCommentsPromise.promise; + firstPost.comments; // trigger an extra access + const firstPostComments = await firstPostCommentsPromise; + firstPost.comments; // trigger an extra access + assert.true(firstPostCommentsPromise.isFulfilled, 'comments relationship is fulfilled'); + assert.true(firstPostCommentsPromise.promise === originalPromise, 'we did not re-trigger the property'); + assert.strictEqual(firstPostComments.length, 3, 'we loaded three comments'); + firstPost.comments; // trigger an extra access + assert.true(firstPostCommentsPromise.isFulfilled, 'comments relationship is fulfilled'); + }); + test('DS.ManyArray is lazy', async function (assert) { let peopleDidChange = 0; const Tag = DS.Model.extend({ diff --git a/packages/model/addon/-private/has-many.js b/packages/model/addon/-private/has-many.js index 8910db5e5db..43416ecf429 100644 --- a/packages/model/addon/-private/has-many.js +++ b/packages/model/addon/-private/has-many.js @@ -24,17 +24,17 @@ import { computedMacroWithOptionalParams } from './util'; ```app/models/post.js import Model, { hasMany } from '@ember-data/model'; - + export default class PostModel extends Model { - @hasMany('comment') comments; + @hasMany('comment') comments; } ``` ```app/models/comment.js import Model, { belongsTo } from '@ember-data/model'; - + export default class CommentModel extends Model { - @belongsTo('post') post; + @belongsTo('post') post; } ``` @@ -54,7 +54,7 @@ import { computedMacroWithOptionalParams } from './util'; import Model, { hasMany } from '@ember-data/model'; export default class TagModel extends Model { - @hasMany('post') posts; + @hasMany('post') posts; } ``` diff --git a/packages/model/addon/-private/system/promise-many-array.ts b/packages/model/addon/-private/system/promise-many-array.ts index 6c0a6d58d38..1858f5daa2d 100644 --- a/packages/model/addon/-private/system/promise-many-array.ts +++ b/packages/model/addon/-private/system/promise-many-array.ts @@ -16,7 +16,7 @@ import { DEPRECATE_EVENTED_API_USAGE } from '@ember-data/private-build-infra/dep A PromiseManyArray is an array-like proxy that also proxies certain method calls to the underlying ManyArray in addition to being "promisified". - + Right now we proxy: * `reload()` diff --git a/packages/record-data/addon/-private/graph/operations/update-relationship.ts b/packages/record-data/addon/-private/graph/operations/update-relationship.ts index f49ff703275..eafe5d1fb7e 100644 --- a/packages/record-data/addon/-private/graph/operations/update-relationship.ts +++ b/packages/record-data/addon/-private/graph/operations/update-relationship.ts @@ -153,7 +153,5 @@ export default function updateRelationshipOperation(graph: Graph, op: UpdateRela } else { relationship.state.isStale = false; } - } else { - relationship.state.isStale = false; } } diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 9dd6a33351c..537206a1e0a 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -806,6 +806,7 @@ export default class InternalModel { setupData(data) { let changedKeys = this._recordData.pushData(data, this.hasRecord); if (this.hasRecord) { + // TODO @runspired should this be going through the notification manager? this._record._notifyProperties(changedKeys); } this.send('pushedData'); @@ -905,11 +906,18 @@ export default class InternalModel { notifyHasManyChange(key: string) { if (this.hasRecord) { + let manyArray = this._manyArrayCache[key]; + let hasPromise = !!this._relationshipPromisesCache[key]; + + if (manyArray && hasPromise) { + // do nothing, we will notify the ManyArray directly + // once the fetch has completed. + return; + } + if (CUSTOM_MODEL_CLASS) { this.store._notificationManager.notify(this.identifier, 'relationships', key); } else { - let manyArray = this._manyArrayCache[key]; - if (manyArray) { manyArray.notify();