diff --git a/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js b/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js index 2a6554a5ee4..8430bb73b9d 100644 --- a/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js @@ -10,10 +10,11 @@ import { setupRenderingTest } from 'ember-qunit'; import { ServerError } from '@ember-data/adapter/error'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import { implicitRelationshipsFor } from '@ember-data/record-data/-private'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; +import { implicitRelationshipsFor } from '../../helpers/accessors'; + class Person extends Model { @attr() name; @@ -511,19 +512,20 @@ module('async belongs-to rendering tests', function (hooks) { assert.equal(this.element.textContent.trim(), '', 'we have no parent'); - let relationshipState = sedona.belongsTo('parent').belongsToRelationship; + const relationship = sedona.belongsTo('parent').belongsToRelationship; + const { state, definition } = relationship; let RelationshipPromiseCache = sedona._internalModel._relationshipPromisesCache; let RelationshipProxyCache = sedona._internalModel._relationshipProxyCache; - assert.true(relationshipState.isAsync, 'The relationship is async'); - assert.false(relationshipState.relationshipIsEmpty, 'The relationship is not empty'); - assert.true(relationshipState.hasDematerializedInverse, 'The relationship inverse is dematerialized'); - assert.true(relationshipState.hasAnyRelationshipData, 'The relationship knows which record it needs'); + assert.true(definition.isAsync, 'The relationship is async'); + assert.false(state.isEmpty, 'The relationship is not empty'); + assert.true(state.hasDematerializedInverse, 'The relationship inverse is dematerialized'); + assert.true(state.hasReceivedData, 'The relationship knows which record it needs'); assert.false(!!RelationshipPromiseCache['parent'], 'The relationship has no fetch promise'); - assert.true(relationshipState.hasFailedLoadAttempt === true, 'The relationship has attempted a load'); - assert.true(relationshipState.shouldForceReload === false, 'The relationship will not force a reload'); + assert.true(state.hasFailedLoadAttempt === true, 'The relationship has attempted a load'); + assert.true(state.shouldForceReload === false, 'The relationship will not force a reload'); assert.true(!!RelationshipProxyCache['parent'], 'The relationship has a promise proxy'); - assert.false(!!relationshipState.link, 'The relationship does not have a link'); + assert.false(!!relationship.link, 'The relationship does not have a link'); try { let result = await sedona.get('parent.content'); diff --git a/packages/-ember-data/tests/acceptance/relationships/has-many-test.js b/packages/-ember-data/tests/acceptance/relationships/has-many-test.js index 730194b8fd9..924bf350bfb 100644 --- a/packages/-ember-data/tests/acceptance/relationships/has-many-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/has-many-test.js @@ -316,12 +316,12 @@ module('async has-many rendering tests', function (hooks) { let RelationshipPromiseCache = parent._internalModel._relationshipPromisesCache; let RelationshipProxyCache = parent._internalModel._relationshipProxyCache; - assert.true(relationshipState.isAsync, 'The relationship is async'); - assert.false(relationshipState.relationshipIsEmpty, 'The relationship is not empty'); - assert.true(relationshipState.hasDematerializedInverse, 'The relationship has a dematerialized inverse'); - assert.true(relationshipState.hasAnyRelationshipData, 'The relationship knows which record it needs'); + assert.true(relationshipState.definition.isAsync, 'The relationship is async'); + assert.false(relationshipState.state.isEmpty, 'The relationship is not empty'); + assert.true(relationshipState.state.hasDematerializedInverse, 'The relationship has a dematerialized inverse'); + assert.true(relationshipState.state.hasReceivedData, 'The relationship knows which record it needs'); assert.false(!!RelationshipPromiseCache['children'], 'The relationship has no fetch promise'); - assert.true(relationshipState.hasFailedLoadAttempt === true, 'The relationship has attempted a load'); + assert.true(relationshipState.state.hasFailedLoadAttempt === true, 'The relationship has attempted a load'); assert.true(!!RelationshipProxyCache['children'], 'The relationship has a promise proxy'); assert.false(!!relationshipState.link, 'The relationship does not have a link'); @@ -432,16 +432,16 @@ module('async has-many rendering tests', function (hooks) { let RelationshipPromiseCache = parent._internalModel._relationshipPromisesCache; let RelationshipProxyCache = parent._internalModel._relationshipProxyCache; - assert.true(relationshipState.isAsync, 'The relationship is async'); + assert.true(relationshipState.definition.isAsync, 'The relationship is async'); assert.true( - relationshipState.relationshipIsEmpty, + relationshipState.state.isEmpty, 'The relationship is empty because no signal has been received as to true state' ); - assert.true(relationshipState.relationshipIsStale, 'The relationship is still stale'); - assert.false(relationshipState.hasAnyRelationshipData, 'The relationship knows which record it needs'); + assert.true(relationshipState.state.isStale, 'The relationship is still stale'); + assert.false(relationshipState.state.hasReceivedData, 'The relationship knows which record it needs'); assert.false(!!RelationshipPromiseCache['children'], 'The relationship has no fetch promise'); assert.true(!!RelationshipProxyCache['children'], 'The relationship has a promise proxy'); - assert.true(relationshipState.hasFailedLoadAttempt === true, 'The relationship has attempted a load'); + assert.true(relationshipState.state.hasFailedLoadAttempt === true, 'The relationship has attempted a load'); assert.true(!!(relationshipState.links && relationshipState.links.related), 'The relationship has a link'); Ember.onerror = originalOnError; diff --git a/packages/-ember-data/tests/helpers/accessors.ts b/packages/-ember-data/tests/helpers/accessors.ts new file mode 100644 index 00000000000..7d60d6ab95c --- /dev/null +++ b/packages/-ember-data/tests/helpers/accessors.ts @@ -0,0 +1,47 @@ +import { graphFor } from '@ember-data/record-data/-private'; +import { recordIdentifierFor } from '@ember-data/store'; + +type CoreStore = import('@ember-data/store/-private/system/core-store').default; +type BelongsToRelationship = import('@ember-data/record-data/-private').BelongsToRelationship; +type ManyRelationship = import('@ember-data/record-data/-private').ManyRelationship; +type Relationship = import('@ember-data/record-data/-private').Relationship; +type RecordDataStoreWrapper = import('@ember-data/store/-private').RecordDataStoreWrapper; +type StableRecordIdentifier = import('@ember-data/store/-private/ts-interfaces/identifier').StableRecordIdentifier; +type RelationshipDict = import('@ember-data/store/-private/ts-interfaces/utils').ConfidentDict; + +export function getRelationshipStateForRecord( + record: { store: CoreStore }, + propertyName: string +): BelongsToRelationship | ManyRelationship | Relationship { + const identifier = recordIdentifierFor(record); + return graphFor(record.store._storeWrapper).get(identifier, propertyName); +} + +export function hasRelationshipForRecord( + record: { + store: CoreStore; + }, + propertyName: string +): boolean { + const identifier = recordIdentifierFor(record); + const relationships = graphFor(record.store._storeWrapper).identifiers.get(identifier); + return relationships ? propertyName in relationships : false; +} + +export function implicitRelationshipsFor( + storeWrapper: RecordDataStoreWrapper, + identifier: StableRecordIdentifier +): RelationshipDict { + const rels = graphFor(storeWrapper).identifiers.get(identifier); + if (!rels) { + throw new Error(`Expected at least one relationship to be populated`); + } + let implicits = Object.create(null); + Object.keys(rels).forEach((key) => { + let rel = rels[key]!; + if (rel.definition.isImplicit) { + implicits[key] = rel; + } + }); + return implicits; +} diff --git a/packages/-ember-data/tests/integration/polymorphic-belongs-to-test.js b/packages/-ember-data/tests/integration/polymorphic-belongs-to-test.js index ca1e0f37fb8..74ac65b38ed 100644 --- a/packages/-ember-data/tests/integration/polymorphic-belongs-to-test.js +++ b/packages/-ember-data/tests/integration/polymorphic-belongs-to-test.js @@ -14,7 +14,7 @@ module('integration/polymorphic-belongs-to - Polymorphic BelongsTo', function (h @attr() title; - @belongsTo('person', { polymorphic: true, async: false }) + @belongsTo('author', { polymorphic: true, async: false }) author; } @@ -26,7 +26,7 @@ module('integration/polymorphic-belongs-to - Polymorphic BelongsTo', function (h class Person extends Model {} class AsyncBook extends Model { - @belongsTo('person', { polymorphic: true }) + @belongsTo('author', { polymorphic: true }) author; } owner.register('model:book', Book); diff --git a/packages/-ember-data/tests/integration/references/belongs-to-test.js b/packages/-ember-data/tests/integration/references/belongs-to-test.js index 877a995e856..4d044e73f63 100644 --- a/packages/-ember-data/tests/integration/references/belongs-to-test.js +++ b/packages/-ember-data/tests/integration/references/belongs-to-test.js @@ -48,7 +48,7 @@ module('integration/references/belongs-to', function (hooks) { run(function () { person.belongsTo('unknown-relationship'); }); - }, "There is no belongsTo relationship named 'unknown-relationship' on a model of modelClass 'person'"); + }, 'Expected to find a relationship definition for person.unknown-relationship but none was found'); }); testInDebug( diff --git a/packages/-ember-data/tests/integration/references/has-many-test.js b/packages/-ember-data/tests/integration/references/has-many-test.js index 2e54c1da93e..3c300b293dd 100755 --- a/packages/-ember-data/tests/integration/references/has-many-test.js +++ b/packages/-ember-data/tests/integration/references/has-many-test.js @@ -50,7 +50,7 @@ module('integration/references/has-many', function (hooks) { run(function () { family.hasMany('unknown-relationship'); }); - }, "There is no hasMany relationship named 'unknown-relationship' on a model of modelClass 'family'"); + }, 'Expected to find a relationship definition for family.unknown-relationship but none was found'); }); testInDebug( diff --git a/packages/-ember-data/tests/integration/relationships/belongs-to-test.js b/packages/-ember-data/tests/integration/relationships/belongs-to-test.js index ebc0c68dc2d..d167617d34f 100644 --- a/packages/-ember-data/tests/integration/relationships/belongs-to-test.js +++ b/packages/-ember-data/tests/integration/relationships/belongs-to-test.js @@ -3,23 +3,19 @@ import { run } from '@ember/runloop'; import { setupContext, teardownContext } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import RSVP, { resolve } from 'rsvp'; +import { hash, resolve } from 'rsvp'; -import DS from 'ember-data'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; -import Model from '@ember-data/model'; -import { relationshipsFor, relationshipStateFor } from '@ember-data/record-data/-private'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; -const { attr: DSattr, hasMany: DShasMany, belongsTo: DSbelongsTo } = DS; -const { hash } = RSVP; -const { attr, belongsTo } = DS; +import { getRelationshipStateForRecord, hasRelationshipForRecord } from '../../helpers/accessors'; -let store, User, Message, Post, Comment, Book, Book1, Chapter, Author, Section; +let Book, Chapter; module('integration/relationship/belongs-to BelongsTo Relationships (new-style)', function (hooks) { let store; @@ -188,49 +184,49 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function setupTest(hooks); hooks.beforeEach(function () { - User = DS.Model.extend({ - name: DSattr('string'), - messages: DShasMany('message', { polymorphic: true, async: false }), - favouriteMessage: DSbelongsTo('message', { polymorphic: true, inverse: null, async: false }), + const User = Model.extend({ + name: attr('string'), + messages: hasMany('message', { polymorphic: true, async: false }), + favouriteMessage: belongsTo('message', { polymorphic: true, inverse: null, async: false }), }); - Message = DS.Model.extend({ - user: DSbelongsTo('user', { inverse: 'messages', async: false }), - created_at: DSattr('date'), + const Message = Model.extend({ + user: belongsTo('user', { inverse: 'messages', async: false }), + created_at: attr('date'), }); - Post = Message.extend({ - title: DSattr('string'), - comments: DShasMany('comment', { async: false, inverse: null }), + const Post = Message.extend({ + title: attr('string'), + comments: hasMany('comment', { async: false, inverse: null }), }); - Comment = Message.extend({ - body: DS.attr('string'), - message: DS.belongsTo('message', { polymorphic: true, async: false, inverse: null }), + const Comment = Message.extend({ + body: attr('string'), + message: belongsTo('message', { polymorphic: true, async: false, inverse: null }), }); - Book = DS.Model.extend({ - name: DSattr('string'), - author: DSbelongsTo('author', { async: false }), - chapters: DShasMany('chapters', { async: false, inverse: 'book' }), + Book = Model.extend({ + name: attr('string'), + author: belongsTo('author', { async: false }), + chapters: hasMany('chapters', { async: false, inverse: 'book' }), }); - Book1 = DS.Model.extend({ - name: DSattr('string'), + const Book1 = Model.extend({ + name: attr('string'), }); - Chapter = DS.Model.extend({ - title: DSattr('string'), - book: DSbelongsTo('book', { async: false, inverse: 'chapters' }), + Chapter = Model.extend({ + title: attr('string'), + book: belongsTo('book', { async: false, inverse: 'chapters' }), }); - Author = DS.Model.extend({ - name: DSattr('string'), - books: DShasMany('books', { async: false }), + const Author = Model.extend({ + name: attr('string'), + books: hasMany('books', { async: false }), }); - Section = DS.Model.extend({ - name: DSattr('string'), + const Section = Model.extend({ + name: attr('string'), }); this.owner.register('model:user', User); @@ -248,36 +244,30 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function this.owner.register( 'serializer:user', - DS.JSONAPISerializer.extend({ + JSONAPISerializer.extend({ attrs: { favouriteMessage: { embedded: 'always' }, }, }) ); - store = this.owner.lookup('service:store'); - - User = store.modelFor('user'); - Post = store.modelFor('post'); - Comment = store.modelFor('comment'); - Message = store.modelFor('message'); + const store = this.owner.lookup('service:store'); Book = store.modelFor('book'); Chapter = store.modelFor('chapter'); - Author = store.modelFor('author'); }); test('returning a null relationship from payload sets the relationship to null on both sides', function (assert) { this.owner.register( 'model:app', - DS.Model.extend({ - name: DSattr('string'), - team: DSbelongsTo('team', { async: true }), + Model.extend({ + name: attr('string'), + team: belongsTo('team', { async: true }), }) ); this.owner.register( 'model:team', - DS.Model.extend({ - apps: DShasMany('app', { async: true }), + Model.extend({ + apps: hasMany('app', { async: true }), }) ); @@ -324,7 +314,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function adapter.shouldBackgroundReloadRecord = () => false; adapter.updateRecord = (store, type, snapshot) => { - return RSVP.resolve({ + return resolve({ data: { id: '1', type: 'app', @@ -356,7 +346,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function let adapter = store.adapterFor('application'); store.modelFor('post').reopen({ - user: DS.belongsTo('user', { + user: belongsTo('user', { async: true, inverse: 'messages', }), @@ -633,12 +623,12 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function adapter.shouldBackgroundReloadRecord = () => false; - let Group = DS.Model.extend({ - people: DS.hasMany('person', { async: false }), + let Group = Model.extend({ + people: hasMany('person', { async: false }), }); - let Person = DS.Model.extend({ - group: DS.belongsTo({ async: true }), + let Person = Model.extend({ + group: belongsTo({ async: true }), }); this.owner.register('model:group', Group); @@ -701,12 +691,12 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function adapter.shouldBackgroundReloadRecord = () => false; - let Seat = DS.Model.extend({ - person: DS.belongsTo('person', { async: false }), + let Seat = Model.extend({ + person: belongsTo('person', { async: false }), }); - let Person = DS.Model.extend({ - seat: DS.belongsTo('seat', { async: true }), + let Person = Model.extend({ + seat: belongsTo('seat', { async: true }), }); this.owner.register('model:seat', Seat); @@ -756,12 +746,12 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function adapter.shouldBackgroundReloadRecord = () => false; - let Group = DS.Model.extend({ - people: DS.hasMany('person', { async: false }), + let Group = Model.extend({ + people: hasMany('person', { async: false }), }); - let Person = DS.Model.extend({ - group: DS.belongsTo({ async: true }), + let Person = Model.extend({ + group: belongsTo({ async: true }), }); this.owner.register('model:group', Group); @@ -809,12 +799,12 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function adapter.shouldBackgroundReloadRecord = () => false; - let Group = DS.Model.extend({ - people: DS.hasMany('person', { async: false }), + let Group = Model.extend({ + people: hasMany('person', { async: false }), }); - let Person = DS.Model.extend({ - group: DS.belongsTo({ async: true }), + let Person = Model.extend({ + group: belongsTo({ async: true }), }); this.owner.register('model:group', Group); @@ -859,6 +849,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function let store = this.owner.lookup('service:store'); let message = store.createRecord('message', { id: 1 }); let comment = store.createRecord('comment', { id: 2, message: message }); + const Message = store.modelFor('message'); assert.ok(comment instanceof Message, 'a comment is an instance of a message'); }); @@ -866,7 +857,19 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test('relationshipsByName does not cache a factory', async function (assert) { // The model is loaded up via a container. It has relationshipsByName // called on it. - let modelViaFirstFactory = this.owner.lookup('service:store').modelFor('user'); + const User = Model.extend({ + name: attr('string'), + messages: hasMany('message', { polymorphic: true, async: false }), + favouriteMessage: belongsTo('message', { polymorphic: true, inverse: null, async: false }), + }); + const Message = Model.extend({ + user: belongsTo('user', { inverse: 'messages', async: false }), + }); + + this.owner.register('model:user', User); + this.owner.register('model:message', Message); + let store = this.owner.lookup('service:store'); + let modelViaFirstFactory = store.modelFor('user'); get(modelViaFirstFactory, 'relationshipsByName'); // An app is reset, or the container otherwise destroyed. @@ -875,12 +878,11 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function // A new model for a relationship is created. const NewMessage = Message.extend(); - - this.owner.register('model:message', NewMessage); this.owner.register('model:user', User); + this.owner.register('model:message', NewMessage); // A new store is created. - let store = this.owner.lookup('service:store'); + store = this.owner.lookup('service:store'); // relationshipsByName is called again. let modelViaSecondFactory = store.modelFor('user'); @@ -919,14 +921,14 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function let adapter = store.adapterFor('application'); store.modelFor('post').reopen({ - comments: DS.hasMany('comment', { + comments: hasMany('comment', { async: true, inverse: 'post', }), }); store.modelFor('comment').reopen({ - post: DS.belongsTo('post', { async: false }), + post: belongsTo('post', { async: false }), }); let comment; @@ -998,13 +1000,13 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function let post; store.modelFor('message').reopen({ - user: DS.hasMany('user', { + user: hasMany('user', { async: true, }), }); store.modelFor('post').reopen({ - user: DS.belongsTo('user', { + user: belongsTo('user', { async: true, inverse: 'messages', }), @@ -1085,7 +1087,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test('Rollbacking attributes for a deleted record restores implicit relationship - async', function (assert) { Book.reopen({ - author: DS.belongsTo('author', { async: true }), + author: belongsTo('author', { async: true }), }); let store = this.owner.lookup('service:store'); @@ -1175,10 +1177,10 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.expect(1); assert.expectAssertion(() => { - User = DS.Model.extend(); + const User = Model.extend(); - DS.Model.extend({ - user: DSbelongsTo(User, { async: false }), + Model.extend({ + user: belongsTo(User, { async: false }), }); }, /The first argument to belongsTo must be a string/); }); @@ -1187,7 +1189,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.expect(1); Book.reopen({ - author: DSbelongsTo('author', { async: true }), + author: belongsTo('author', { async: true }), }); let store = this.owner.lookup('service:store'); @@ -1208,8 +1210,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function return run(() => { return store.findRecord('book', 1).then((book) => { - let relationship = relationshipStateFor(book, 'author'); - assert.true(relationship.hasAnyRelationshipData, 'relationship has data'); + let relationship = getRelationshipStateForRecord(book, 'author'); + assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); }); }); @@ -1235,8 +1237,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function return run(() => { return store.findRecord('book', 1).then((book) => { - let relationship = relationshipStateFor(book, 'author'); - assert.true(relationship.hasAnyRelationshipData, 'relationship has data'); + let relationship = getRelationshipStateForRecord(book, 'author'); + assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); }); }); @@ -1245,7 +1247,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.expect(1); Book.reopen({ - author: DSbelongsTo('author', { async: true }), + author: belongsTo('author', { async: true }), }); let store = this.owner.lookup('service:store'); @@ -1266,8 +1268,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function return run(() => { return store.findRecord('book', 1).then((book) => { - let relationship = relationshipStateFor(book, 'author'); - assert.false(relationship.hasAnyRelationshipData, 'relationship does not have data'); + let relationship = getRelationshipStateForRecord(book, 'author'); + assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); }); }); }); @@ -1290,8 +1292,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function return run(() => { return store.findRecord('book', 1).then((book) => { - let relationship = relationshipStateFor(book, 'author'); - assert.false(relationship.hasAnyRelationshipData, 'relationship does not have data'); + let relationship = getRelationshipStateForRecord(book, 'author'); + assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); }); }); }); @@ -1300,7 +1302,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.expect(2); Book.reopen({ - author: DSbelongsTo('author', { async: true }), + author: belongsTo('author', { async: true }), }); let store = this.owner.lookup('service:store'); @@ -1308,18 +1310,18 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function run(() => { let author = store.createRecord('author'); let book = store.createRecord('book', { name: 'The Greatest Book' }); - let relationship = relationshipStateFor(book, 'author'); + let relationship = getRelationshipStateForRecord(book, 'author'); - assert.false(relationship.hasAnyRelationshipData, 'relationship does not have data'); + assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); book = store.createRecord('book', { name: 'The Greatest Book', author, }); - relationship = relationshipStateFor(book, 'author'); + relationship = getRelationshipStateForRecord(book, 'author'); - assert.true(relationship.hasAnyRelationshipData, 'relationship has data'); + assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); }); @@ -1334,16 +1336,16 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function name: 'The Greatest Book', }); - let relationship = relationshipStateFor(book, 'author'); - assert.false(relationship.hasAnyRelationshipData, 'relationship does not have data'); + let relationship = getRelationshipStateForRecord(book, 'author'); + assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); book = store.createRecord('book', { name: 'The Greatest Book', author, }); - relationship = relationshipStateFor(book, 'author'); - assert.true(relationship.hasAnyRelationshipData, 'relationship has data'); + relationship = getRelationshipStateForRecord(book, 'author'); + assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); }); @@ -1360,7 +1362,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); assert.notOk( - relationshipsFor(user).has('favouriteMessage'), + hasRelationshipForRecord(user, 'favouriteMessage'), 'Newly created record should not have relationships' ); }); @@ -1375,7 +1377,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); assert.ok( - relationshipsFor(user).has('favouriteMessage'), + hasRelationshipForRecord(user, 'favouriteMessage'), 'Newly created record with relationships in params passed in its constructor should have relationships' ); }); @@ -1389,7 +1391,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function user = store.createRecord('user'); user.set('favouriteMessage', message); assert.ok( - relationshipsFor(user).has('favouriteMessage'), + hasRelationshipForRecord(user, 'favouriteMessage'), 'Newly created record with relationships in params passed in its constructor should have relationships' ); }); @@ -1403,7 +1405,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function user = store.createRecord('user'); user.get('favouriteMessage'); assert.ok( - relationshipsFor(user).has('favouriteMessage'), + hasRelationshipForRecord(user, 'favouriteMessage'), 'Newly created record with relationships in params passed in its constructor should have relationships' ); }); @@ -1413,7 +1415,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.expect(3); Book.reopen({ - author: DS.belongsTo('author', { async: true }), + author: belongsTo('author', { async: true }), }); let store = this.owner.lookup('service:store'); @@ -1456,7 +1458,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.expect(2); Book.reopen({ - author: DS.belongsTo('author', { async: true }), + author: belongsTo('author', { async: true }), }); let store = this.owner.lookup('service:store'); @@ -1501,7 +1503,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.expect(1); Book.reopen({ - author: DS.belongsTo('author', { async: true }), + author: belongsTo('author', { async: true }), }); let store = this.owner.lookup('service:store'); @@ -1551,7 +1553,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.expect(3); Book.reopen({ - author: DS.belongsTo('author', { async: true }), + author: belongsTo('author', { async: true }), }); let store = this.owner.lookup('service:store'); @@ -1613,7 +1615,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.expect(4); Book.reopen({ - author: DS.belongsTo('author', { async: true }), + author: belongsTo('author', { async: true }), }); let store = this.owner.lookup('service:store'); @@ -1693,7 +1695,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.expect(2); Book.reopen({ - author: DS.belongsTo('author', { async: true }), + author: belongsTo('author', { async: true }), }); let store = this.owner.lookup('service:store'); @@ -1761,7 +1763,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test('A belongsTo relationship can be reloaded using the reference if it was fetched via link', function (assert) { Chapter.reopen({ - book: DS.belongsTo({ async: true }), + book: belongsTo({ async: true }), }); let store = this.owner.lookup('service:store'); @@ -1824,7 +1826,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test('A synchronous belongsTo relationship can be reloaded using a reference if it was fetched via id', function (assert) { Chapter.reopen({ - book: DS.belongsTo({ async: false }), + book: belongsTo({ async: false }), }); let store = this.owner.lookup('service:store'); @@ -1879,7 +1881,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test('A belongsTo relationship can be reloaded using a reference if it was fetched via id', function (assert) { Chapter.reopen({ - book: DS.belongsTo({ async: true }), + book: belongsTo({ async: true }), }); let store = this.owner.lookup('service:store'); @@ -1957,7 +1959,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test("belongsTo relationship with links doesn't trigger extra change notifications - #4942", function (assert) { Chapter.reopen({ - book: DS.belongsTo({ async: true }), + book: belongsTo({ async: true }), }); let store = this.owner.lookup('service:store'); 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 322df652228..5e36bdfffee 100644 --- a/packages/-ember-data/tests/integration/relationships/has-many-test.js +++ b/packages/-ember-data/tests/integration/relationships/has-many-test.js @@ -12,12 +12,13 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import RESTAdapter from '@ember-data/adapter/rest'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import { relationshipsFor, relationshipStateFor } from '@ember-data/record-data/-private'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import RESTSerializer from '@ember-data/serializer/rest'; import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { getRelationshipStateForRecord, hasRelationshipForRecord } from '../../helpers/accessors'; + module('integration/relationships/has_many - Has-Many Relationships', function (hooks) { setupTest(hooks); @@ -1591,55 +1592,58 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }); - test('Type can be inferred from the key of a hasMany relationship', function (assert) { + test('Type can be inferred from the key of a hasMany relationship', async function (assert) { assert.expect(1); + const User = Model.extend({ + name: attr(), + contacts: hasMany({ inverse: null, async: false }), + }); + + const Contact = Model.extend({ + name: attr(), + user: belongsTo('user', { async: false }), + }); + + this.owner.register('model:user', User); + this.owner.register('model:contact', Contact); + let store = this.owner.lookup('service:store'); let adapter = store.adapterFor('application'); - adapter.findRecord = function (store, type, ids, snapshots) { + adapter.findRecord = function () { return { data: { - id: 1, + id: '1', type: 'user', relationships: { contacts: { - data: [{ id: 1, type: 'contact' }], + data: [{ id: '1', type: 'contact' }], }, }, }, }; }; - run(function () { - store.push({ - data: { - type: 'user', - id: '1', - relationships: { - contacts: { - data: [{ type: 'contact', id: '1' }], - }, + const user = store.push({ + data: { + type: 'user', + id: '1', + relationships: { + contacts: { + data: [{ type: 'contact', id: '1' }], }, }, - included: [ - { - type: 'contact', - id: '1', - }, - ], - }); - }); - run(function () { - store - .findRecord('user', 1) - .then(function (user) { - return user.get('contacts'); - }) - .then(function (contacts) { - assert.equal(contacts.get('length'), 1, 'The contacts relationship is correctly set up'); - }); + }, + included: [ + { + type: 'contact', + id: '1', + }, + ], }); + const contacts = await user.contacts; + assert.equal(contacts.get('length'), 1, 'The contacts relationship is correctly set up'); }); test('Type can be inferred from the key of an async hasMany relationship', function (assert) { @@ -2835,7 +2839,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); run(() => { - relationshipStateFor(post, 'comments').clear(); + getRelationshipStateForRecord(post, 'comments').clear(); let comments = A(store.peekAll('comment')); assert.deepEqual(comments.mapBy('post'), [null, null, null]); }); @@ -3052,8 +3056,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return run(() => { return store.findRecord('chapter', 1).then((chapter) => { - let relationship = relationshipStateFor(chapter, 'pages'); - assert.true(relationship.hasAnyRelationshipData, 'relationship has data'); + let relationship = getRelationshipStateForRecord(chapter, 'pages'); + assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); }); }); @@ -3084,8 +3088,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return run(() => { return store.findRecord('chapter', 1).then((chapter) => { - let relationship = relationshipStateFor(chapter, 'pages'); - assert.true(relationship.hasAnyRelationshipData, 'relationship has data'); + let relationship = getRelationshipStateForRecord(chapter, 'pages'); + assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); }); }); @@ -3117,8 +3121,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return run(() => { return store.findRecord('chapter', 1).then((chapter) => { - let relationship = relationshipStateFor(chapter, 'pages'); - assert.false(relationship.hasAnyRelationshipData, 'relationship does not have data'); + let relationship = getRelationshipStateForRecord(chapter, 'pages'); + assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); }); }); }); @@ -3141,8 +3145,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return run(() => { return store.findRecord('chapter', 1).then((chapter) => { - let relationship = relationshipStateFor(chapter, 'pages'); - assert.false(relationship.hasAnyRelationshipData, 'relationship does not have data'); + let relationship = getRelationshipStateForRecord(chapter, 'pages'); + assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); }); }); }); @@ -3159,16 +3163,16 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( let chapter = store.createRecord('chapter', { title: 'The Story Begins' }); let page = store.createRecord('page'); - let relationship = relationshipStateFor(chapter, 'pages'); - assert.false(relationship.hasAnyRelationshipData, 'relationship does not have data'); + let relationship = getRelationshipStateForRecord(chapter, 'pages'); + assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); chapter = store.createRecord('chapter', { title: 'The Story Begins', pages: [page], }); - relationship = relationshipStateFor(chapter, 'pages'); - assert.true(relationship.hasAnyRelationshipData, 'relationship has data'); + relationship = getRelationshipStateForRecord(chapter, 'pages'); + assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); test('hasMany hasAnyRelationshipData sync created', function (assert) { @@ -3176,17 +3180,17 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( let store = this.owner.lookup('service:store'); let chapter = store.createRecord('chapter', { title: 'The Story Begins' }); - let relationship = relationshipStateFor(chapter, 'pages'); + let relationship = getRelationshipStateForRecord(chapter, 'pages'); - assert.false(relationship.hasAnyRelationshipData, 'relationship does not have data'); + assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); chapter = store.createRecord('chapter', { title: 'The Story Begins', pages: [store.createRecord('page')], }); - relationship = relationshipStateFor(chapter, 'pages'); + relationship = getRelationshipStateForRecord(chapter, 'pages'); - assert.true(relationship.hasAnyRelationshipData, 'relationship has data'); + assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); test("Model's hasMany relationship should not be created during model creation", function (assert) { @@ -3201,7 +3205,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, }); user = store.peekRecord('user', 1); - assert.notOk(relationshipsFor(user).has('messages'), 'Newly created record should not have relationships'); + assert.notOk(hasRelationshipForRecord(user, 'messages'), 'Newly created record should not have relationships'); }); }); @@ -3213,7 +3217,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( user = store.createRecord('user'); user.get('messages'); assert.ok( - relationshipsFor(user).has('messages'), + hasRelationshipForRecord(user, 'messages'), 'Newly created record with relationships in params passed in its constructor should have relationships' ); }); @@ -3254,7 +3258,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); run(() => { - assert.equal(relationshipStateFor(book, 'chapters').meta.where, 'the lefkada sea', 'meta is there'); + assert.equal(getRelationshipStateForRecord(book, 'chapters').meta.where, 'the lefkada sea', 'meta is there'); }); }); diff --git a/packages/-ember-data/tests/integration/relationships/inverse-relationship-load-test.js b/packages/-ember-data/tests/integration/relationships/inverse-relationship-load-test.js index 1a824d01b02..8239df60ef8 100644 --- a/packages/-ember-data/tests/integration/relationships/inverse-relationship-load-test.js +++ b/packages/-ember-data/tests/integration/relationships/inverse-relationship-load-test.js @@ -91,10 +91,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 2, 'hasMany relationship has correct number of records'); let dog1 = dogs.get('firstObject'); @@ -180,10 +177,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 2, 'hasMany relationship has correct number of records'); let dog1 = dogs.get('firstObject'); @@ -270,10 +264,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 2, 'hasMany relationship has correct number of records'); let dog1 = dogs.get('firstObject'); @@ -361,10 +352,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 2, 'hasMany relationship has correct number of records'); let dog1 = dogs.get('firstObject'); @@ -456,7 +444,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.equal(dogs.get('length'), 2); assert.deepEqual(dogs.mapBy('id'), ['1', '2']); @@ -525,7 +513,7 @@ module('inverse relationship load test', function (hooks) { }); let favoriteDog = await person.get('favoriteDog'); - assert.false(person.belongsTo('favoriteDog').belongsToRelationship.relationshipIsEmpty); + assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); assert.equal(favoriteDog.get('id'), '1', 'favoriteDog id is set correctly'); let favoriteDogPerson = await favoriteDog.get('person'); assert.equal( @@ -597,7 +585,7 @@ module('inverse relationship load test', function (hooks) { }); let favoriteDog = await person.get('favoriteDog'); - assert.false(person.belongsTo('favoriteDog').belongsToRelationship.relationshipIsEmpty); + assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); assert.equal(favoriteDog.get('id'), '1', 'favoriteDog id is set correctly'); let favoriteDogPerson = await favoriteDog.get('person'); assert.equal( @@ -669,7 +657,7 @@ module('inverse relationship load test', function (hooks) { }); let favoriteDog = await person.get('favoriteDog'); - assert.false(person.belongsTo('favoriteDog').belongsToRelationship.relationshipIsEmpty); + assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); assert.equal(favoriteDog.get('id'), '1', 'favoriteDog id is set correctly'); let favoriteDogPerson = await favoriteDog.get('pal'); assert.equal( @@ -741,7 +729,7 @@ module('inverse relationship load test', function (hooks) { }); let favoriteDog = await person.get('favoriteDog'); - assert.false(person.belongsTo('favoriteDog').belongsToRelationship.relationshipIsEmpty); + assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); assert.equal(favoriteDog.get('id'), '1', 'favoriteDog id is set correctly'); let favoriteDogPerson = await favoriteDog.get('pal'); assert.equal( @@ -811,7 +799,7 @@ module('inverse relationship load test', function (hooks) { }); let favoriteDog = await person.get('favoriteDog'); - assert.false(person.belongsTo('favoriteDog').belongsToRelationship.relationshipIsEmpty); + assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); assert.equal(favoriteDog.get('id'), '1', 'favoriteDog id is set correctly'); await favoriteDog.destroyRecord(); favoriteDog = await person.get('favoriteDog'); @@ -882,7 +870,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.equal(dogs.get('length'), 2, 'left hand side relationship is set up with correct number of records'); let [dog1, dog2] = dogs.toArray(); @@ -963,7 +951,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.equal(dogs.get('length'), 2, 'left hand side relationship is set up with correct number of records'); let [dog1, dog2] = dogs.toArray(); @@ -1045,7 +1033,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.equal(dogs.get('length'), 2, 'left hand side relationship is set up with correct number of records'); let [dog1, dog2] = dogs.toArray(); @@ -1127,7 +1115,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.equal(dogs.get('length'), 2, 'left hand side relationship is set up with correct number of records'); let [dog1, dog2] = dogs.toArray(); @@ -1200,7 +1188,7 @@ module('inverse relationship load test', function (hooks) { let person = await dog.get('person'); assert.false( - dog.belongsTo('person').belongsToRelationship.relationshipIsEmpty, + dog.belongsTo('person').belongsToRelationship.state.isEmpty, 'belongsTo relationship state was populated' ); assert.equal(person.get('id'), '1', 'dog.person relationship is correctly set up'); @@ -1272,7 +1260,7 @@ module('inverse relationship load test', function (hooks) { let person = await dog.get('person'); assert.false( - dog.belongsTo('person').belongsToRelationship.relationshipIsEmpty, + dog.belongsTo('person').belongsToRelationship.state.isEmpty, 'belongsTo relationship state was populated' ); assert.equal(person.get('id'), '1', 'dog.person relationship is correctly set up'); @@ -1345,7 +1333,7 @@ module('inverse relationship load test', function (hooks) { let person = await dog.get('pal'); assert.false( - dog.belongsTo('pal').belongsToRelationship.relationshipIsEmpty, + dog.belongsTo('pal').belongsToRelationship.state.isEmpty, 'belongsTo relationship state was populated' ); assert.equal(person.get('id'), '1', 'dog.person relationship is correctly set up'); @@ -1418,7 +1406,7 @@ module('inverse relationship load test', function (hooks) { let person = await dog.get('pal'); assert.false( - dog.belongsTo('pal').belongsToRelationship.relationshipIsEmpty, + dog.belongsTo('pal').belongsToRelationship.state.isEmpty, 'belongsTo relationship state was populated' ); assert.equal(person.get('id'), '1', 'dog.person relationship is correctly set up'); @@ -1520,7 +1508,7 @@ module('inverse relationship load test', function (hooks) { id: 'mismatched-inverse-relationship-data-from-payload', count: 2, }); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false); assert.equal(dogs.get('length'), 2); let dog1 = dogs.get('firstObject'); @@ -1629,7 +1617,7 @@ module('inverse relationship load test', function (hooks) { id: 'mismatched-inverse-relationship-data-from-payload', count: 2, }); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false); assert.equal(dogs.get('length'), 2); let dog1 = dogs.get('firstObject'); @@ -1732,7 +1720,7 @@ module('inverse relationship load test', function (hooks) { id: 'mismatched-inverse-relationship-data-from-payload', count: 2, }); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false); assert.equal(dogs.get('length'), 2); let dog1 = dogs.get('firstObject'); @@ -1835,7 +1823,7 @@ module('inverse relationship load test', function (hooks) { id: 'mismatched-inverse-relationship-data-from-payload', count: 2, }); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false); assert.equal(dogs.get('length'), 2); let dog1 = dogs.get('firstObject'); @@ -1934,11 +1922,7 @@ module('inverse relationship load test', function (hooks) { assert.equal(dogFromStore.belongsTo('person').id(), '1'); assert.equal(person.belongsTo('dog').id(), '1'); assert.equal(dog.id, '1', 'dog.person relationship loaded correctly'); - assert.equal( - person.belongsTo('dog').belongsToRelationship.relationshipIsEmpty, - false, - 'relationship is not empty' - ); + assert.equal(person.belongsTo('dog').belongsToRelationship.state.isEmpty, false, 'relationship is not empty'); let dogPerson1 = await dog.get('person'); assert.equal( @@ -2030,11 +2014,7 @@ module('inverse relationship load test', function (hooks) { assert.equal(person.belongsTo('dog').id(), '1'); assert.equal(dog.id, '1', 'dog.person relationship loaded correctly'); - assert.equal( - person.belongsTo('dog').belongsToRelationship.relationshipIsEmpty, - false, - 'relationship is not empty' - ); + assert.equal(person.belongsTo('dog').belongsToRelationship.state.isEmpty, false, 'relationship is not empty'); let dogPerson1 = dog.get('person'); assert.equal( @@ -2123,11 +2103,7 @@ module('inverse relationship load test', function (hooks) { assert.equal(person.belongsTo('dog').id(), '1'); assert.equal(dog.id, '1', 'dog.person relationship loaded correctly'); - assert.equal( - person.belongsTo('dog').belongsToRelationship.relationshipIsEmpty, - false, - 'relationship is not empty' - ); + assert.equal(person.belongsTo('dog').belongsToRelationship.state.isEmpty, false, 'relationship is not empty'); let dogPerson1 = await dog.get('person'); assert.equal( @@ -2216,11 +2192,7 @@ module('inverse relationship load test', function (hooks) { assert.equal(person.belongsTo('dog').id(), '1'); assert.equal(dog.id, '1', 'dog.person relationship loaded correctly'); - assert.equal( - person.belongsTo('dog').belongsToRelationship.relationshipIsEmpty, - false, - 'relationship is not empty' - ); + assert.equal(person.belongsTo('dog').belongsToRelationship.state.isEmpty, false, 'relationship is not empty'); let dogPerson1 = await dog.get('person'); assert.equal( @@ -2315,7 +2287,7 @@ module('inverse relationship load test', function (hooks) { '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false, 'relationship is not empty'); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false, 'relationship is not empty'); await dog.destroyRecord(); dog = await person.get('dog'); @@ -2403,7 +2375,7 @@ module('inverse relationship load test', function (hooks) { '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false, 'relationship is not empty'); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false, 'relationship is not empty'); await dog.destroyRecord(); dog = await person.get('dog'); @@ -2486,7 +2458,7 @@ module('inverse relationship load test', function (hooks) { '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false, 'relationship is not empty'); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false, 'relationship is not empty'); await dog.destroyRecord(); dog = await person.get('dog'); @@ -2569,7 +2541,7 @@ module('inverse relationship load test', function (hooks) { '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false, 'relationship is not empty'); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false, 'relationship is not empty'); await dog.destroyRecord(); dog = await person.get('dog'); @@ -2677,7 +2649,7 @@ module('inverse relationship load test', function (hooks) { id: 'mismatched-inverse-relationship-data-from-payload', count: 2, }); - assert.equal(person1.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false); + assert.equal(person1.hasMany('dogs').hasManyRelationship.state.isEmpty, false); let dog1 = store.peekRecord('dog', '1'); let dog2 = store.peekRecord('dog', '2'); @@ -2853,7 +2825,7 @@ module('inverse relationship load test', function (hooks) { id: 'mismatched-inverse-relationship-data-from-payload', count: 2, }); - assert.equal(person1.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false); + assert.equal(person1.hasMany('dogs').hasManyRelationship.state.isEmpty, false); let dog1 = store.peekRecord('dog', '1'); let dog2 = store.peekRecord('dog', '2'); @@ -3010,7 +2982,7 @@ module('inverse relationship load test', function (hooks) { id: 'mismatched-inverse-relationship-data-from-payload', count: 2, }); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false); let dog1 = store.peekRecord('dog', '1'); let dog2 = store.peekRecord('dog', '2'); @@ -3132,7 +3104,7 @@ module('inverse relationship load test', function (hooks) { id: 'mismatched-inverse-relationship-data-from-payload', count: 2, }); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false); let dog1 = store.peekRecord('dog', '1'); let dog2 = store.peekRecord('dog', '2'); @@ -3254,7 +3226,7 @@ module('inverse relationship load test', function (hooks) { id: 'mismatched-inverse-relationship-data-from-payload', count: 2, }); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false); let dog1 = store.peekRecord('dog', '1'); let dog2 = store.peekRecord('dog', '2'); @@ -3375,7 +3347,7 @@ module('inverse relationship load test', function (hooks) { id: 'mismatched-inverse-relationship-data-from-payload', count: 2, }); - assert.equal(person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, false); + assert.equal(person.hasMany('dogs').hasManyRelationship.state.isEmpty, false); let dog1 = store.peekRecord('dog', '1'); let dog2 = store.peekRecord('dog', '2'); @@ -3488,10 +3460,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 2, 'hasMany relationship has correct number of records'); let dog1 = dogs.get('firstObject'); @@ -3586,10 +3555,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 2, 'hasMany relationship has correct number of records'); let dog1 = dogs.get('firstObject'); @@ -3685,10 +3651,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 2, 'hasMany relationship has correct number of records'); let dog1 = dogs.get('firstObject'); @@ -3784,10 +3747,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 2, 'hasMany relationship has correct number of records'); let dog1 = dogs.get('firstObject'); @@ -3878,10 +3838,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 2, 'hasMany relationship has correct number of records'); let dog1 = dogs.get('firstObject'); @@ -3990,10 +3947,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 0, 'hasMany relationship for parent is empty'); @@ -4116,10 +4070,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 0, 'hasMany relationship for parent is empty'); @@ -4226,10 +4177,7 @@ module('inverse relationship load test', function (hooks) { }); let personDogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(personDogs.get('length'), 0, 'hasMany relationship for parent is empty'); @@ -4329,10 +4277,7 @@ module('inverse relationship load test', function (hooks) { }); let personDogs = await person.get('dogs'); - assert.false( - person.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(personDogs.get('length'), 0, 'hasMany relationship for parent is empty'); @@ -4448,10 +4393,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await pal.get('dogs'); - assert.false( - pal.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(pal.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 0, 'hasMany relationship for parent is empty'); @@ -4574,10 +4516,7 @@ module('inverse relationship load test', function (hooks) { }); let dogs = await pal.get('dogs'); - assert.false( - pal.hasMany('dogs').hasManyRelationship.relationshipIsEmpty, - 'relationship state was set up correctly' - ); + assert.false(pal.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.equal(dogs.get('length'), 0, 'hasMany relationship for parent is empty'); diff --git a/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js b/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js index 097df2df36b..6cbb5fefd86 100644 --- a/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js +++ b/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js @@ -627,8 +627,7 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' post; } - register('model:User', User); - + register('model:user', User); assert.expectAssertion(() => { store.createRecord('user', { post: null }); }, /No model was found for 'post' and no schema handles the type/); @@ -655,7 +654,7 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' }); test('Unload a destroyed record should clean the relations', async function (assert) { - assert.expect(3); + assert.expect(2); class Post extends Model { @hasMany('comment', { async: true }) @@ -667,8 +666,8 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' post; } - register('model:Post', Post); - register('model:Comment', Comment); + register('model:post', Post); + register('model:comment', Comment); const comment = store.createRecord('comment'); const post = store.createRecord('post'); @@ -681,12 +680,8 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' await settled(); const identifier = comment._internalModel.identifier; - const relationships = graphFor(store._storeWrapper).get(identifier); - const implicitRelationships = graphFor(store._storeWrapper).getImplicit(identifier); - const initializedRelationships = relationships.initializedRelationships; - assert.deepEqual(Object.keys(initializedRelationships), [], 'relationships are cleared'); - assert.deepEqual(Object.keys(implicitRelationships), [], 'implicitRelationships are cleared'); + assert.false(graphFor(store._storeWrapper).identifiers.has(identifier), 'relationships are cleared'); assert.ok(comment._internalModel.__recordData.isDestroyed, 'recordData is destroyed'); }); }); diff --git a/packages/-ember-data/tests/integration/relationships/json-api-links-test.js b/packages/-ember-data/tests/integration/relationships/json-api-links-test.js index 43af90ed616..c2d7617b835 100644 --- a/packages/-ember-data/tests/integration/relationships/json-api-links-test.js +++ b/packages/-ember-data/tests/integration/relationships/json-api-links-test.js @@ -82,7 +82,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f return user1.get('organisation').then((orgFromUser) => { assert.false( - user1.belongsTo('organisation').belongsToRelationship.relationshipIsStale, + user1.belongsTo('organisation').belongsToRelationship.state.isStale, 'user should have loaded its belongsTo relationship' ); @@ -95,11 +95,11 @@ module('integration/relationship/json-api-links | Relationship state updates', f }); test('Pushing child record should not mark parent:children as loaded', function (assert) { - let Child = DS.Model.extend({ + const Child = DS.Model.extend({ parent: belongsTo('parent', { inverse: 'children' }), }); - let Parent = DS.Model.extend({ + const Parent = DS.Model.extend({ children: hasMany('child', { inverse: 'parent' }), }); @@ -110,44 +110,39 @@ module('integration/relationship/json-api-links | Relationship state updates', f let store = this.owner.lookup('service:store'); - Parent = store.modelFor('parent'); - Child = store.modelFor('child'); - - return run(() => { - const parent = store.push({ - data: { - id: 'p1', - type: 'parent', - relationships: { - children: { - links: { - related: '/parent/1/children', - }, + const parent = store.push({ + data: { + id: 'p1', + type: 'parent', + relationships: { + children: { + links: { + related: '/parent/1/children', }, }, }, - }); + }, + }); - store.push({ - data: { - id: 'c1', - type: 'child', - relationships: { - parent: { - data: { - id: 'p1', - type: 'parent', - }, + const state = parent.hasMany('children').hasManyRelationship.state; + assert.true(state.isStale, 'initial: parent should think that children still needs to be loaded'); + + store.push({ + data: { + id: 'c1', + type: 'child', + relationships: { + parent: { + data: { + id: 'p1', + type: 'parent', }, }, }, - }); - - assert.true( - parent.hasMany('children').hasManyRelationship.relationshipIsStale, - 'parent should think that children still needs to be loaded' - ); + }, }); + + assert.true(state.isStale, 'final: parent should think that children still needs to be loaded'); }); test('pushing has-many payloads with data (no links), then more data (no links) works as expected', function (assert) { diff --git a/packages/-ember-data/tests/integration/relationships/one-to-one-test.js b/packages/-ember-data/tests/integration/relationships/one-to-one-test.js index 6dd1856a3b9..19425a3e512 100644 --- a/packages/-ember-data/tests/integration/relationships/one-to-one-test.js +++ b/packages/-ember-data/tests/integration/relationships/one-to-one-test.js @@ -289,12 +289,12 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun let user1bestFriendState = user1.belongsTo('bestFriend').belongsToRelationship; - assert.equal(user1bestFriendState.canonicalState, null, '.job is canonically empty'); - assert.equal(user1bestFriendState.currentState, null, '.job is locally empty'); - assert.true(user1bestFriendState.relationshipIsEmpty, 'The relationship is empty'); - assert.false(user1bestFriendState.relationshipIsStale, 'The relationship is not stale'); - assert.false(user1bestFriendState.shouldForceReload, 'The relationship does not require reload'); - assert.true(user1bestFriendState.hasAnyRelationshipData, 'The relationship considers its canonical data complete'); + assert.equal(user1bestFriendState.remoteState, null, '.job is canonically empty'); + assert.equal(user1bestFriendState.localState, null, '.job is locally empty'); + assert.true(user1bestFriendState.state.isEmpty, 'The relationship is empty'); + assert.false(user1bestFriendState.state.isStale, 'The relationship is not stale'); + assert.false(user1bestFriendState.state.shouldForceReload, 'The relationship does not require reload'); + assert.true(user1bestFriendState.state.hasReceivedData, 'The relationship considers its canonical data complete'); }); test('Fetching a belongsTo that is set to a different record, sets the old relationship to null - sync', async function (assert) { @@ -375,10 +375,10 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun assert.equal(user1JobState.canonicalState, null, '.job is canonically empty'); assert.equal(user1JobState.currentState, null, '.job is locally empty'); - assert.true(user1JobState.relationshipIsEmpty, 'The relationship is empty'); - assert.false(user1JobState.relationshipIsStale, 'The relationship is not stale'); - assert.false(user1JobState.shouldForceReload, 'The relationship does not require reload'); - assert.true(user1JobState.hasAnyRelationshipData, 'The relationship considers its canonical data complete'); + assert.true(user1JobState.state.isEmpty, 'The relationship is empty'); + assert.false(user1JobState.state.isStale, 'The relationship is not stale'); + assert.false(user1JobState.state.shouldForceReload, 'The relationship does not require reload'); + assert.true(user1JobState.state.hasReceivedData, 'The relationship considers its canonical data complete'); }); /* diff --git a/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js b/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js index 7f311978706..88d0bde54cf 100644 --- a/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js +++ b/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js @@ -1642,7 +1642,7 @@ module('integration/embedded-records-mixin', function (hooks) { calledSerializeHasMany = true; let key = relationship.key; let payloadKey = this.keyForRelationship ? this.keyForRelationship(key, 'hasMany') : key; - let relationshipType = snapshot.type.determineRelationshipType(relationship); + let relationshipType = snapshot.type.determineRelationshipType(relationship, store); // "manyToOne" not supported in ActiveModelSerializer.prototype.serializeHasMany let relationshipTypes = ['manyToNone', 'manyToMany', 'manyToOne']; if (relationshipTypes.indexOf(relationshipType) > -1) { diff --git a/packages/record-data/addon/-private/accessors.ts b/packages/record-data/addon/-private/accessors.ts deleted file mode 100644 index 99ec7506cd1..00000000000 --- a/packages/record-data/addon/-private/accessors.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { graphFor } from './graph/index'; - -type RecordDataStoreWrapper = import('@ember-data/store/-private').RecordDataStoreWrapper; -type StableRecordIdentifier = import('@ember-data/store/-private/ts-interfaces/identifier').StableRecordIdentifier; -type InternalModel = import('@ember-data/store/-private').InternalModel; -type RelationshipRecordData = import('./ts-interfaces/relationship-record-data').RelationshipRecordData; -type RelationshipDict = import('@ember-data/store/-private/ts-interfaces/utils').ConfidentDict; -type ManyRelationship = import('./relationships/state/has-many').default; -type BelongsToRelationship = import('./relationships/state/belongs-to').default; -type Relationship = import('./relationships/state/relationship').default; -type Relationships = import('./relationships/state/create').default; - -type MappableToRelationships = { _internalModel: InternalModel }; - -export function relationshipsFor(instance: RecordDataStoreWrapper, identifier: StableRecordIdentifier): Relationships; -export function relationshipsFor(instance: MappableToRelationships): Relationships; -export function relationshipsFor( - instance: MappableToRelationships | RecordDataStoreWrapper, - identifier?: StableRecordIdentifier -): Relationships { - if (!identifier) { - let internalModel = ((instance as unknown) as MappableToRelationships)._internalModel; - identifier = internalModel.identifier; - - // safe to upgrade as relationshipsFor is a private util used only for our own things - instance = (internalModel._recordData as RelationshipRecordData).storeWrapper; - } - - return graphFor(instance as RecordDataStoreWrapper).get(identifier); -} - -export function relationshipStateFor( - instance: RecordDataStoreWrapper, - identifier: StableRecordIdentifier, - propertyName: string -): BelongsToRelationship | ManyRelationship; -export function relationshipStateFor( - instance: MappableToRelationships, - identifier: string -): BelongsToRelationship | ManyRelationship; -export function relationshipStateFor( - instance: RecordDataStoreWrapper | MappableToRelationships, - identifier: string | StableRecordIdentifier, - propertyName?: string -): BelongsToRelationship | ManyRelationship { - if (!propertyName) { - let internalModel = ((instance as unknown) as MappableToRelationships)._internalModel; - propertyName = (identifier as unknown) as string; - identifier = internalModel.identifier; - instance = (internalModel._recordData as RelationshipRecordData).storeWrapper; - } - return relationshipsFor(instance as RecordDataStoreWrapper, identifier as StableRecordIdentifier).get(propertyName); -} - -export function implicitRelationshipsFor( - storeWrapper: RecordDataStoreWrapper, - identifier: StableRecordIdentifier -): RelationshipDict { - return graphFor(storeWrapper).getImplicit(identifier); -} - -export function implicitRelationshipStateFor( - storeWrapper: RecordDataStoreWrapper, - identifier: StableRecordIdentifier, - propertyName: string -): Relationship { - return implicitRelationshipsFor(storeWrapper, identifier)[propertyName]; -} diff --git a/packages/record-data/addon/-private/graph/-edge-definition.ts b/packages/record-data/addon/-private/graph/-edge-definition.ts new file mode 100644 index 00000000000..61016c5dcb4 --- /dev/null +++ b/packages/record-data/addon/-private/graph/-edge-definition.ts @@ -0,0 +1,301 @@ +import { assert } from '@ember/debug'; + +import { expandingGet, expandingSet } from './-utils'; + +type Graph = import('.').Graph; + +type RelationshipSchema = import('@ember-data/store/-private/ts-interfaces/record-data-schemas').RelationshipSchema; +type Dict = import('@ember-data/store/-private/ts-interfaces/utils').Dict; +type StableRecordIdentifier = import('@ember-data/store/-private/ts-interfaces/identifier').StableRecordIdentifier; + +export type EdgeCache = Dict>; + +export interface UpgradedMeta { + kind: 'hasMany' | 'belongsTo' | 'implicit'; + key: string; + type: string; + isAsync: boolean; + isImplicit: boolean; + isCollection: boolean; + isPolymorphic: boolean; + + inverseKey: string; + inverseType: string; + inverseIsAsync: boolean; + inverseIsImplicit: boolean; + inverseIsCollection: boolean; + inverseIsPolymorphic: boolean; +} + +export interface EdgeDefinition { + lhs_key: string; + lhs_modelNames: string[]; + lhs_baseModelName: string; + lhs_relationshipName: string; + lhs_definition: UpgradedMeta; + lhs_isPolymorphic: boolean; + + rhs_key: string; + rhs_modelNames: string[]; + rhs_baseModelName: string; + rhs_relationshipName: string; + rhs_definition: UpgradedMeta | null; + rhs_isPolymorphic: boolean; + + hasInverse: boolean; + isSelfReferential: boolean; + isReflexive: boolean; +} + +const BOOL_LATER = (null as unknown) as boolean; +const STR_LATER = ''; +const IMPLICIT_KEY_RAND = Date.now(); + +function implicitKeyFor(type: string, key: string): string { + return `implicit-${type}:${key}${IMPLICIT_KEY_RAND}`; +} + +function syncMeta(definition: UpgradedMeta, inverseDefinition: UpgradedMeta) { + definition.inverseKey = inverseDefinition.key; + definition.inverseType = inverseDefinition.type; + definition.inverseIsAsync = inverseDefinition.isAsync; + definition.inverseIsCollection = inverseDefinition.isCollection; + definition.inverseIsPolymorphic = inverseDefinition.isPolymorphic; + definition.inverseIsImplicit = inverseDefinition.isImplicit; +} + +function upgradeMeta(meta: RelationshipSchema): UpgradedMeta { + let niceMeta: UpgradedMeta = {} as UpgradedMeta; + let options = meta.options; + niceMeta.kind = meta.kind; + niceMeta.key = meta.name; + niceMeta.type = meta.type; + niceMeta.isAsync = options && options.async !== undefined ? !!options.async : true; + niceMeta.isImplicit = false; + niceMeta.isCollection = meta.kind === 'hasMany'; + niceMeta.isPolymorphic = options && !!options.polymorphic; + + niceMeta.inverseKey = options && options.inverse; + niceMeta.inverseType = STR_LATER; + niceMeta.inverseIsAsync = BOOL_LATER; + niceMeta.inverseIsImplicit = (options && options.inverse === null) || BOOL_LATER; + niceMeta.inverseIsCollection = BOOL_LATER; + + return niceMeta; +} + +export function isLHS(info: EdgeDefinition, type: string, key: string): boolean { + let isSelfReferential = info.isSelfReferential; + let isRelationship = key === info.lhs_relationshipName; + + if (isRelationship === true) { + return ( + isSelfReferential === true || // itself + type === info.lhs_baseModelName || // base or non-polymorphic + // if the other side is polymorphic then we need to scan our modelNames + (info.rhs_isPolymorphic && info.lhs_modelNames.indexOf(type) !== -1) // polymorphic + ); + } + + return false; +} + +export function isRHS(info: EdgeDefinition, type: string, key: string): boolean { + let isSelfReferential = info.isSelfReferential; + let isRelationship = key === info.rhs_relationshipName; + + if (isRelationship === true) { + return ( + isSelfReferential === true || // itself + type === info.rhs_baseModelName || // base or non-polymorphic + // if the other side is polymorphic then we need to scan our modelNames + (info.lhs_isPolymorphic && info.rhs_modelNames.indexOf(type) !== -1) // polymorphic + ); + } + + return false; +} + +export function upgradeDefinition( + graph: Graph, + identifier: StableRecordIdentifier, + propertyName: string, + isImplicit: boolean = false +): EdgeDefinition | null { + const cache = graph._definitionCache; + const storeWrapper = graph.store; + const polymorphicLookup = graph._potentialPolymorphicTypes; + + const { type } = identifier; + let cached = expandingGet(cache, type, propertyName); + + // CASE: We have a cached resolution (null if no relationship exists) + if (cached !== undefined) { + return cached; + } + + assert( + `Expected to find relationship definition in the cache for the implicit relationship ${propertyName}`, + !isImplicit + ); + + let relationships = storeWrapper.relationshipsDefinitionFor(type); + assert(`Expected to have a relationship definition for ${type} but none was found.`, relationships); + let meta = relationships[propertyName]; + + if (!meta) { + if (polymorphicLookup[type]) { + const altTypes = Object.keys(polymorphicLookup[type] as {}); + for (let i = 0; i < altTypes.length; i++) { + let cached = expandingGet(cache, altTypes[i], propertyName); + if (cached) { + expandingSet(cache, type, propertyName, cached); + return cached; + } + } + } + + // CASE: We don't have a relationship at all + // we should only hit this in prod + assert(`Expected to find a relationship definition for ${type}.${propertyName} but none was found.`, meta); + + cache[type]![propertyName] = null; + return null; + } + const definition = upgradeMeta(meta); + + let inverseDefinition; + let inverseKey; + const inverseType = definition.type; + + // CASE: Inverse is explicitly null + if (definition.inverseKey === null) { + assert(`Expected the inverse model to exist`, storeWrapper._store.modelFor(inverseType)); + inverseDefinition = null; + } else { + inverseKey = storeWrapper.inverseForRelationship(type, propertyName); + + // CASE: Inverse resolves to null + if (!inverseKey) { + inverseDefinition = null; + } else { + // CASE: We have an explicit inverse or were able to resolve one + let inverseDefinitions = storeWrapper.relationshipsDefinitionFor(inverseType); + assert(`Expected to have a relationship definition for ${inverseType} but none was found.`, inverseDefinitions); + let meta = inverseDefinitions[inverseKey]; + assert(`Expected to find a relationship definition for ${inverseType}.${inverseKey} but none was found.`, meta); + inverseDefinition = upgradeMeta(meta); + } + } + + // CASE: We have no inverse + if (!inverseDefinition) { + // polish off meta + inverseKey = implicitKeyFor(type, propertyName); + inverseDefinition = { + kind: 'implicit', + key: inverseKey, + type: type, + isAsync: false, + isImplicit: true, + isCollection: false, + isPolymorphic: false, + }; + + syncMeta(definition, inverseDefinition); + syncMeta(inverseDefinition, definition); + + const info = { + lhs_key: `${type}:${propertyName}`, + lhs_modelNames: [type], + lhs_baseModelName: type, + lhs_relationshipName: propertyName, + lhs_definition: definition, + lhs_isPolymorphic: definition.isPolymorphic, + + rhs_key: '', + rhs_modelNames: [], + rhs_baseModelName: inverseType, + rhs_relationshipName: '', + rhs_definition: inverseDefinition, + rhs_isPolymorphic: false, + + hasInverse: false, + isSelfReferential: type === inverseType, // this could be wrong if we are self-referential but also polymorphic + isReflexive: false, // we can't be reflexive if we don't define an inverse + }; + + expandingSet(cache, inverseType, inverseKey, info); + expandingSet(cache, type, propertyName, info); + return info; + } + + // CASE: We do have an inverse + const baseType = inverseDefinition.type; + + // TODO we want to assert this but this breaks all of our shoddily written tests + /* + if (DEBUG) { + let inverseDoubleCheck = inverseMeta.type.inverseFor(inverseRelationshipName, store); + + assert(`The ${inverseBaseModelName}:${inverseRelationshipName} relationship declares 'inverse: null', but it was resolved as the inverse for ${baseModelName}:${relationshipName}.`, inverseDoubleCheck); + } + */ + // CASE: We may have already discovered the inverse for the baseModelName + // CASE: We have already discovered the inverse + cached = expandingGet(cache, baseType, propertyName) || expandingGet(cache, inverseType, inverseKey); + + if (cached) { + // TODO this assert can be removed if the above assert is enabled + assert( + `The ${inverseType}:${inverseKey} relationship declares 'inverse: null', but it was resolved as the inverse for ${type}:${propertyName}.`, + cached.hasInverse !== false + ); + + let isLHS = cached.lhs_baseModelName === baseType; + let modelNames = isLHS ? cached.lhs_modelNames : cached.rhs_modelNames; + // make this lookup easier in the future by caching the key + modelNames.push(type); + expandingSet(cache, type, propertyName, cached); + + return cached; + } + + // this is our first time so polish off the metas + syncMeta(definition, inverseDefinition); + syncMeta(inverseDefinition, definition); + + const lhs_modelNames = [type]; + if (type !== baseType) { + lhs_modelNames.push(baseType); + } + const isSelfReferential = type === inverseType; + const info = { + lhs_key: `${baseType}:${propertyName}`, + lhs_modelNames, + lhs_baseModelName: baseType, + lhs_relationshipName: propertyName, + lhs_definition: definition, + lhs_isPolymorphic: definition.isPolymorphic, + + rhs_key: `${inverseType}:${inverseKey}`, + rhs_modelNames: [inverseType], + rhs_baseModelName: inverseType, + rhs_relationshipName: inverseKey, + rhs_definition: inverseDefinition, + rhs_isPolymorphic: inverseDefinition.isPolymorphic, + hasInverse: true, + isSelfReferential, + isReflexive: isSelfReferential && propertyName === inverseKey, + }; + + // Create entries for the baseModelName as well as modelName to speed up + // inverse lookups + expandingSet(cache, baseType, propertyName, info); + expandingSet(cache, type, propertyName, info); + + // Greedily populate the inverse + expandingSet(cache, inverseType, inverseKey, info); + + return info; +} diff --git a/packages/record-data/addon/-private/graph/-state.ts b/packages/record-data/addon/-private/graph/-state.ts new file mode 100644 index 00000000000..ad3c9b45576 --- /dev/null +++ b/packages/record-data/addon/-private/graph/-state.ts @@ -0,0 +1,114 @@ +export interface RelationshipState { + /* + This flag indicates whether we should consider the content + of this relationship "known". + + If we have no relationship knowledge, and the relationship + is `async`, we will attempt to fetch the relationship on + access if it is also stale. + + Snapshot uses this to tell the difference between unknown + (`undefined`) or empty (`null`). The reason for this is that + we wouldn't want to serialize unknown relationships as `null` + as that might overwrite remote state. + + All relationships for a newly created (`store.createRecord()`) are + considered known (`hasReceivedData === true`). + + true when + => we receive a push with either new data or explicit empty (`[]` or `null`) + => the relationship is a belongsTo and we have received data from + the other side. + + false when + => we have received no signal about what data belongs in this relationship + => the relationship is a hasMany and we have only received data from + the other side. + */ + hasReceivedData: boolean; + /* + Flag that indicates whether an empty relationship is explicitly empty + (signaled by push giving us an empty array or null relationship) + e.g. an API response has told us that this relationship is empty. + + Thus far, it does not appear that we actually need this flag; however, + @runspired has found it invaluable when debugging relationship tests + to determine whether (and why if so) we are in an incorrect state. + + true when + => we receive a push with explicit empty (`[]` or `null`) + => we have received no signal about what data belongs in this relationship + => on initial create (as no signal is known yet) + + false at all other times + */ + + isEmpty: boolean; + /* + This flag indicates whether we should + re-fetch the relationship the next time + it is accessed. + + The difference between this flag and `shouldForceReload` + is in how we treat the presence of partially missing data: + - for a forced reload, we will reload the link or EVERY record + - for a stale reload, we will reload the link (if present) else only MISSING records + + Ideally these flags could be merged, but because we don't give the + request layer the option of deciding how to resolve the data being queried + we are forced to differentiate for now. + + It is also possible for a relationship to remain stale after a forced reload; however, + in this case `state.hasFailedLoadAttempt` ought to be `true`. + + false when + => recordData.isNew() on initial setup + => a previously triggered request has resolved + => we get relationship data via push + + true when + => !recordData.isNew() on initial setup + => an inverse has been unloaded + => we get a new link for the relationship + + TODO @runspired unskip the acceptance tests and fix these flags + */ + isStale: boolean; + + hasFailedLoadAttempt: boolean; + /* + This flag forces fetch. `true` for a single request once `reload()` + has been called `false` at all other times. + */ + shouldForceReload: boolean; + /* + This flag indicates whether we should + **partially** re-fetch the relationship the + next time it is accessed. + + false when + => initial setup + => a previously triggered request has resolved + + true when + => an inverse has been unloaded + */ + hasDematerializedInverse: boolean; + + // TODO do we want this anymore? Seems somewhat useful + // especially if we rename to `hasUpdatedLink` + // which would tell us slightly more about why the + // relationship is stale + // updatedLink: boolean; +} + +export function createState(): RelationshipState { + return { + hasReceivedData: false, + isEmpty: true, + isStale: false, + hasFailedLoadAttempt: false, + shouldForceReload: false, + hasDematerializedInverse: false, + }; +} diff --git a/packages/record-data/addon/-private/graph/-utils.ts b/packages/record-data/addon/-private/graph/-utils.ts new file mode 100644 index 00000000000..3e60b1e51a2 --- /dev/null +++ b/packages/record-data/addon/-private/graph/-utils.ts @@ -0,0 +1,11 @@ +type Dict = import('@ember-data/store/-private/ts-interfaces/utils').Dict; + +export function expandingGet(cache: Dict>, key1: string, key2: string): T | undefined { + let mainCache = (cache[key1] = cache[key1] || Object.create(null)); + return mainCache[key2]; +} + +export function expandingSet(cache: Dict>, key1: string, key2: string, value: T): void { + let mainCache = (cache[key1] = cache[key1] || Object.create(null)); + mainCache[key2] = value; +} diff --git a/packages/record-data/addon/-private/graph/index.ts b/packages/record-data/addon/-private/graph/index.ts index c21e676ea34..ee0745868b9 100644 --- a/packages/record-data/addon/-private/graph/index.ts +++ b/packages/record-data/addon/-private/graph/index.ts @@ -1,11 +1,18 @@ -import Relationships from '../relationships/state/create'; +import { assert } from '@ember/debug'; +import BelongsToRelationship from '../relationships/state/belongs-to'; +import ManyRelationship from '../relationships/state/has-many'; +import Relationship from '../relationships/state/relationship'; +import { isLHS, upgradeDefinition } from './-edge-definition'; + +type Dict = import('@ember-data/store/-private/ts-interfaces/utils').Dict; +type EdgeCache = import('./-edge-definition').EdgeCache; type StableRecordIdentifier = import('@ember-data/store/-private/ts-interfaces/identifier').StableRecordIdentifier; type Store = import('@ember-data/store/-private/system/core-store').default; type RecordDataStoreWrapper = import('@ember-data/store/-private').RecordDataStoreWrapper; -type Relationship = import('../relationships/state/relationship').default; type JsonApiRelationship = import('@ember-data/store/-private/ts-interfaces/record-data-json-api').JsonApiRelationship; -type RelationshipDict = import('@ember-data/store/-private/ts-interfaces/utils').ConfidentDict; + +type RelationshipEdge = Relationship | ManyRelationship | BelongsToRelationship; const Graphs = new WeakMap(); @@ -44,32 +51,79 @@ export function graphFor(store: RecordDataStoreWrapper | Store): Graph { * @internal */ export class Graph { - declare identifiers: Map; + declare _definitionCache: EdgeCache; + declare _potentialPolymorphicTypes: Dict>; + declare identifiers: Map>; declare store: RecordDataStoreWrapper; declare _queued: { belongsTo: any[]; hasMany: any[] }; declare _nextFlush: boolean; - declare implicitMap: Map; constructor(store: RecordDataStoreWrapper) { - this.store = store; + this._definitionCache = Object.create(null); + this._potentialPolymorphicTypes = Object.create(null); this.identifiers = new Map(); - this._nextFlush = false; + this.store = store; this._queued = { belongsTo: [], hasMany: [] }; - this.implicitMap = new Map(); + this._nextFlush = false; } - get(identifier: StableRecordIdentifier) { + has(identifier: StableRecordIdentifier, propertyName: string): boolean { let relationships = this.identifiers.get(identifier); + if (!relationships) { + return false; + } + return relationships[propertyName] !== undefined; + } - if (relationships === undefined) { - relationships = new Relationships(identifier, this); + get(identifier: StableRecordIdentifier, propertyName: string): RelationshipEdge { + assert(`expected propertyName`, propertyName); + let relationships = this.identifiers.get(identifier); + if (!relationships) { + relationships = Object.create(null) as Dict; this.identifiers.set(identifier, relationships); } - return relationships; + let relationship = relationships[propertyName]; + if (!relationship) { + const info = upgradeDefinition(this, identifier, propertyName); + assert(`Could not determine relationship information for ${identifier.type}.${propertyName}`, info !== null); + const meta = isLHS(info, identifier.type, propertyName) ? info.lhs_definition : info.rhs_definition!; + const Klass = + meta.kind === 'hasMany' ? ManyRelationship : meta.kind === 'belongsTo' ? BelongsToRelationship : Relationship; + relationship = relationships[propertyName] = new Klass(this, meta, identifier); + } + + return relationship; + } + + /** + * Allows for the graph to dynamically discover polymorphic connections + * without needing to walk prototype chains. + * + * Used by edges when an added `type` does not match the expected `type` + * for that edge. + * + * Currently we assert before calling this. For a public API we will want + * to call out to the schema manager to ask if we should consider these + * types as equivalent for a given relationship. + */ + registerPolymorphicType(type1: string, type2: string): void { + const typeCache = this._potentialPolymorphicTypes; + let t1 = typeCache[type1]; + if (!t1) { + t1 = typeCache[type1] = Object.create(null); + } + t1![type2] = true; + + let t2 = typeCache[type2]; + if (!t2) { + t2 = typeCache[type2] = Object.create(null); + } + t2![type1] = true; } /* + TODO move this comment somewhere else implicit relationships are relationships which have not been declared but the inverse side exists on another record somewhere @@ -97,46 +151,33 @@ export class Graph { Then we would have a implicit 'post' relationship for the comment record in order to be do things like remove the comment from the post if the comment were to be deleted. */ - getImplicit(identifier: StableRecordIdentifier): RelationshipDict { - let relationships = this.implicitMap.get(identifier); - - if (relationships === undefined) { - relationships = Object.create(null) as RelationshipDict; - this.implicitMap.set(identifier, relationships); - } - - return relationships; - } unload(identifier: StableRecordIdentifier) { const relationships = this.identifiers.get(identifier); if (relationships) { - // cleanup doesn't mean the graph is invalid - relationships.forEach((name, rel) => destroyRelationship(rel)); - } - - const implicit = this.implicitMap.get(identifier); - if (implicit) { - Object.keys(implicit).forEach((key) => { - let rel = implicit[key]; + // cleans up the graph but retains some nodes + // to allow for rematerialization + Object.keys(relationships).forEach((key) => { + let rel = relationships[key]!; destroyRelationship(rel); + if (rel.definition.isImplicit) { + delete relationships[key]; + } }); - this.implicitMap.delete(identifier); } } remove(identifier: StableRecordIdentifier) { this.unload(identifier); this.identifiers.delete(identifier); - this.implicitMap.delete(identifier); } push(identifier: StableRecordIdentifier, propertyName: string, payload: JsonApiRelationship) { - const relationship = this.get(identifier).get(propertyName); + const relationship = this.get(identifier, propertyName); const backburner = this.store._store._backburner; - this._queued[relationship.kind].push(relationship, payload); + this._queued[relationship.definition.kind].push(relationship, payload); if (this._nextFlush === false) { backburner.join(() => { // TODO this join seems to only be necessary for @@ -165,7 +206,6 @@ export class Graph { destroy() { this.identifiers.clear(); - this.implicitMap.clear(); Graphs.delete(this.store); this.store = (null as unknown) as RecordDataStoreWrapper; } @@ -184,7 +224,7 @@ export class Graph { function destroyRelationship(rel) { rel.recordDataDidDematerialize(); - if (rel._inverseIsSync()) { + if (!rel.definition.inverseIsImplicit && !rel.definition.inverseIsAsync) { rel.removeAllRecordDatasFromOwn(); rel.removeAllCanonicalRecordDatasFromOwn(); } diff --git a/packages/record-data/addon/-private/index.ts b/packages/record-data/addon/-private/index.ts index b14513e89c9..18063a4e366 100644 --- a/packages/record-data/addon/-private/index.ts +++ b/packages/record-data/addon/-private/index.ts @@ -2,5 +2,4 @@ export { default as RecordData } from './record-data'; export { default as Relationship } from './relationships/state/relationship'; export { default as BelongsToRelationship } from './relationships/state/belongs-to'; export { default as ManyRelationship } from './relationships/state/has-many'; -export { relationshipStateFor, relationshipsFor, implicitRelationshipsFor } from './accessors'; export { graphFor, peekGraph } from './graph/index'; diff --git a/packages/record-data/addon/-private/ordered-set.ts b/packages/record-data/addon/-private/ordered-set.ts index 0343dd75355..5ff870f54a4 100644 --- a/packages/record-data/addon/-private/ordered-set.ts +++ b/packages/record-data/addon/-private/ordered-set.ts @@ -19,7 +19,7 @@ const NULL_POINTER = `null-${Date.now()}`; While we convert relationships to identifiers this will be something we will be forced to address. */ -function guidFor(obj): string { +export function guidFor(obj): string { if (obj === null) { return NULL_POINTER; } diff --git a/packages/record-data/addon/-private/record-data.ts b/packages/record-data/addon/-private/record-data.ts index 49a8c51b916..1d13365545a 100644 --- a/packages/record-data/addon/-private/record-data.ts +++ b/packages/record-data/addon/-private/record-data.ts @@ -192,10 +192,10 @@ export default class RecordDataDefault implements RelationshipRecordData { if (relationshipData.links) { let isAsync = relationshipMeta.options && relationshipMeta.options.async !== false; - let relationship = graphFor(this.storeWrapper).get(this.identifier).get(relationshipName); + let relationship = graphFor(this.storeWrapper).get(this.identifier, relationshipName); warn( `You pushed a record of type '${this.modelName}' with a relationship '${relationshipName}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, - isAsync || relationshipData.data || relationship.hasAnyRelationshipData, + isAsync || relationshipData.data || relationship.state.hasReceivedData, { id: 'ds.store.push-link-for-sync-relationship', } @@ -354,25 +354,27 @@ export default class RecordDataDefault implements RelationshipRecordData { } // get ResourceIdentifiers for "current state" - getHasMany(key): DefaultCollectionResourceRelationship { - return (graphFor(this.storeWrapper).get(this.identifier).get(key) as ManyRelationship).getData(); + getHasMany(key: string): DefaultCollectionResourceRelationship { + return (graphFor(this.storeWrapper).get(this.identifier, key) as ManyRelationship).getData(); } // set a new "current state" via ResourceIdentifiers - setDirtyHasMany(key, recordDatas: RecordData[]) { - let relationship = graphFor(this.storeWrapper).get(this.identifier).get(key); + setDirtyHasMany(key: string, recordDatas: RecordData[]) { + let relationship = graphFor(this.storeWrapper).get(this.identifier, key); relationship.clear(); - relationship.addRecordDatas(recordDatas.map(recordIdentifierFor)); + (relationship as ManyRelationship).addRecordDatas(recordDatas.map(recordIdentifierFor)); } // append to "current state" via RecordDatas - addToHasMany(key, recordDatas: RecordData[], idx) { - graphFor(this.storeWrapper).get(this.identifier).get(key).addRecordDatas(recordDatas.map(recordIdentifierFor), idx); + addToHasMany(key: string, recordDatas: RecordData[], idx) { + let relationship = graphFor(this.storeWrapper).get(this.identifier, key); + (relationship as ManyRelationship).addRecordDatas(recordDatas.map(recordIdentifierFor), idx); } // remove from "current state" via RecordDatas - removeFromHasMany(key, recordDatas: RecordData[]) { - graphFor(this.storeWrapper).get(this.identifier).get(key).removeRecordDatas(recordDatas.map(recordIdentifierFor)); + removeFromHasMany(key: string, recordDatas: RecordData[]) { + let relationship = graphFor(this.storeWrapper).get(this.identifier, key); + (relationship as ManyRelationship).removeRecordDatas(recordDatas.map(recordIdentifierFor)); } commitWasRejected(identifier?, errors?: JsonApiValidationError[]) { @@ -395,11 +397,11 @@ export default class RecordDataDefault implements RelationshipRecordData { } getBelongsTo(key: string): DefaultSingleResourceRelationship { - return (graphFor(this.storeWrapper).get(this.identifier).get(key) as BelongsToRelationship).getData(); + return (graphFor(this.storeWrapper).get(this.identifier, key) as BelongsToRelationship).getData(); } setDirtyBelongsTo(key: string, recordData: RecordData) { - (graphFor(this.storeWrapper).get(this.identifier).get(key) as BelongsToRelationship).setRecordData( + (graphFor(this.storeWrapper).get(this.identifier, key) as BelongsToRelationship).setRecordData( recordData ? recordIdentifierFor(recordData) : null ); } @@ -483,14 +485,19 @@ export default class RecordDataDefault implements RelationshipRecordData { */ _directlyRelatedRecordDatasIterable = () => { const graph = graphFor(this.storeWrapper); - if (!graph.identifiers.has(this.identifier)) { + const initializedRelationships = graph.identifiers.get(this.identifier); + + if (!initializedRelationships) { return EMPTY_ITERATOR; } - const relationships = graphFor(this.storeWrapper).get(this.identifier); - const initializedRelationships = relationships.initializedRelationships; - const initializedRelationshipsArr = Object.keys(initializedRelationships).map( - (key) => initializedRelationships[key] - ); + + const initializedRelationshipsArr = Object.keys(initializedRelationships) + .map((key) => initializedRelationships[key]!) + .filter((rel) => { + // TODO @runspired, not clear we need this distinction but + // we used to have it. + return !rel.definition.isImplicit; + }); let i = 0; let j = 0; @@ -500,9 +507,7 @@ export default class RecordDataDefault implements RelationshipRecordData { while (i < initializedRelationshipsArr.length) { while (j < 2) { let members = - j === 0 - ? initializedRelationshipsArr[i].members.list - : initializedRelationshipsArr[i].canonicalMembers.list; + j === 0 ? getLocalState(initializedRelationshipsArr[i]) : getRemoteState(initializedRelationshipsArr[i]); while (k < members.length) { let member = members[k++]; if (member !== null) { @@ -626,10 +631,10 @@ export default class RecordDataDefault implements RelationshipRecordData { let createOptions = {}; if (options !== undefined) { - let { modelName, storeWrapper } = this; + const { modelName, storeWrapper, identifier } = this; let attributeDefs = storeWrapper.attributesDefinitionFor(modelName); let relationshipDefs = storeWrapper.relationshipsDefinitionFor(modelName); - let relationships = graphFor(storeWrapper).get(this.identifier); + const graph = graphFor(storeWrapper); let propertyNames = Object.keys(options); for (let i = 0; i < propertyNames.length; i++) { @@ -651,14 +656,14 @@ export default class RecordDataDefault implements RelationshipRecordData { break; case 'belongsTo': this.setDirtyBelongsTo(name, propertyValue); - relationship = relationships.get(name); - relationship.setHasAnyRelationshipData(true); + relationship = graph.get(identifier, name); + relationship.setHasReceivedData(true); relationship.setRelationshipIsEmpty(false); break; case 'hasMany': this.setDirtyHasMany(name, propertyValue); - relationship = relationships.get(name); - relationship.setHasAnyRelationshipData(true); + relationship = graph.get(identifier, name); + relationship.setHasReceivedData(true); relationship.setRelationshipIsEmpty(false); break; default: @@ -692,27 +697,17 @@ export default class RecordDataDefault implements RelationshipRecordData { const graph = graphFor(this.storeWrapper); const { identifier } = this; - if (graph.identifiers.has(identifier)) { - graph.get(identifier).forEach((name, rel) => { - rel.removeCompletelyFromInverse(); - if (isNew === true) { - rel.clear(); - } - }); - graph.identifiers.delete(identifier); - } - - if (graph.implicitMap.has(identifier)) { - const implicitRelationships = graph.getImplicit(identifier); - Object.keys(implicitRelationships).forEach((key) => { - const rel = implicitRelationships[key]; + const relationships = graph.identifiers.get(identifier); + if (relationships) { + Object.keys(relationships).forEach((key) => { + const rel = relationships[key]!; rel.removeCompletelyFromInverse(); if (isNew === true) { rel.clear(); } }); - graph.implicitMap.delete(identifier); + graph.identifiers.delete(identifier); } } @@ -846,3 +841,16 @@ function areAllModelsUnloaded(recordDatas) { } return true; } + +function getLocalState(rel) { + if (rel.definition.kind === 'belongsTo') { + return rel.localState ? [rel.localState] : []; + } + return rel.members.list; +} +function getRemoteState(rel) { + if (rel.definition.kind === 'belongsTo') { + return rel.remoteState ? [rel.remoteState] : []; + } + return rel.canonicalMembers.list; +} diff --git a/packages/record-data/addon/-private/relationships/state/belongs-to.ts b/packages/record-data/addon/-private/relationships/state/belongs-to.ts index 3ededb8145f..a5d2615da3e 100644 --- a/packages/record-data/addon/-private/relationships/state/belongs-to.ts +++ b/packages/record-data/addon/-private/relationships/state/belongs-to.ts @@ -1,206 +1,502 @@ -import { assert, inspect } from '@ember/debug'; -import { isNone } from '@ember/utils'; -import { DEBUG } from '@glimmer/env'; +import { assert, inspect, warn } from '@ember/debug'; +import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features'; import { assertPolymorphicType } from '@ember-data/store/-debug'; import { identifierCacheFor } from '@ember-data/store/-private'; -import Relationship, { isNew } from './relationship'; +import { createState } from '../../graph/-state'; +import _normalizeLink from '../../normalize-link'; +import { isNew } from './relationship'; -type RecordDataStoreWrapper = import('@ember-data/store/-private').RecordDataStoreWrapper; +type UpgradedMeta = import('../../graph/-edge-definition').UpgradedMeta; +type Graph = import('../../graph').Graph; type ExistingResourceIdentifierObject = import('@ember-data/store/-private/ts-interfaces/ember-data-json-api').ExistingResourceIdentifierObject; -type RelationshipSchema = import('@ember-data/store/-private/ts-interfaces/record-data-schemas').RelationshipSchema; type StableRecordIdentifier = import('@ember-data/store/-private/ts-interfaces/identifier').StableRecordIdentifier; type DefaultSingleResourceRelationship = import('../../ts-interfaces/relationship-record-data').DefaultSingleResourceRelationship; -export default class BelongsToRelationship extends Relationship { - declare inverseRecordData: StableRecordIdentifier | null; - declare canonicalState: StableRecordIdentifier | null; +type Links = import('@ember-data/store/-private/ts-interfaces/ember-data-json-api').Links; + +type Meta = import('@ember-data/store/-private/ts-interfaces/ember-data-json-api').Meta; + +type RelationshipState = import('../../graph/-state').RelationshipState; +type RecordDataStoreWrapper = import('@ember-data/store/-private').RecordDataStoreWrapper; +type PaginationLinks = import('@ember-data/store/-private/ts-interfaces/ember-data-json-api').PaginationLinks; +type JsonApiRelationship = import('@ember-data/store/-private/ts-interfaces/record-data-json-api').JsonApiRelationship; + +export default class BelongsToRelationship { + declare localState: StableRecordIdentifier | null; + declare remoteState: StableRecordIdentifier | null; - constructor( - store: RecordDataStoreWrapper, - inverseKey: string | null, - relationshipMeta: RelationshipSchema, - identifier: StableRecordIdentifier, - inverseIsAsync: boolean - ) { - super(store, inverseKey, relationshipMeta, identifier, inverseIsAsync); - this.inverseRecordData = null; - this.canonicalState = null; + declare graph: Graph; + declare store: RecordDataStoreWrapper; + declare definition: UpgradedMeta; + declare identifier: StableRecordIdentifier; + declare _state: RelationshipState | null; + + declare meta: Meta | null; + declare links: Links | PaginationLinks | null; + declare willSync: boolean; + + constructor(graph: Graph, definition: UpgradedMeta, identifier: StableRecordIdentifier) { + this.graph = graph; + this.store = graph.store; + this.definition = definition; + this.identifier = identifier; + this._state = null; + + this.meta = null; + this.links = null; + this.willSync = false; + + this.localState = null; + this.remoteState = null; } - setRecordData(recordData: StableRecordIdentifier | null) { - if (recordData) { - this.addRecordData(recordData); - } else if (this.inverseRecordData) { - this.removeRecordData(this.inverseRecordData); + get state(): RelationshipState { + let { _state } = this; + if (!_state) { + _state = this._state = createState(); } + return _state; + } - this.setHasAnyRelationshipData(true); - this.setRelationshipIsStale(false); - this.setRelationshipIsEmpty(false); + get isNew(): boolean { + return isNew(this.identifier); } - setCanonicalRecordData(recordData: StableRecordIdentifier) { - if (recordData) { - this.addCanonicalRecordData(recordData); - } else if (this.canonicalState) { - this.removeCanonicalRecordData(this.canonicalState); - } - this.flushCanonicalLater(); + setHasReceivedData(value: boolean) { + this.state.hasReceivedData = value; } - addCanonicalRecordData(recordData: StableRecordIdentifier) { - if (this.canonicalMembers.has(recordData)) { + setHasDematerializedInverse(value: boolean) { + this.state.hasDematerializedInverse = value; + } + + setRelationshipIsStale(value: boolean) { + this.state.isStale = value; + } + + setRelationshipIsEmpty(value: boolean) { + this.state.isEmpty = value; + } + + setShouldForceReload(value: boolean) { + this.state.shouldForceReload = value; + } + + setHasFailedLoadAttempt(value: boolean) { + this.state.hasFailedLoadAttempt = value; + } + + recordDataDidDematerialize() { + if (this.definition.inverseIsImplicit) { return; } - if (this.canonicalState) { - this.removeCanonicalRecordData(this.canonicalState); - } + const inverseKey = this.definition.inverseKey; + const callback = (inverseIdentifier) => { + if (!inverseIdentifier || !this.graph.has(inverseIdentifier, inverseKey)) { + return; + } - this.canonicalState = recordData; - super.addCanonicalRecordData(recordData); - this.setHasAnyRelationshipData(true); - this.setRelationshipIsEmpty(false); + let relationship = this.graph.get(inverseIdentifier, inverseKey); + + // For canonical members, it is possible that inverseRecordData has already been associated to + // to another record. For such cases, do not dematerialize the inverseRecordData + if ( + relationship.definition.kind !== 'belongsTo' || + !(relationship as BelongsToRelationship).localState || + this.identifier === (relationship as BelongsToRelationship).localState + ) { + relationship.inverseDidDematerialize(this.identifier); + } + }; + + if (this.remoteState) { + callback(this.remoteState); + } + if (this.localState && this.localState !== this.remoteState) { + callback(this.localState); + } } inverseDidDematerialize() { - super.inverseDidDematerialize(this.inverseRecordData); + const inverseRecordData = this.localState; + if (!this.definition.isAsync || (inverseRecordData && isNew(inverseRecordData))) { + // unloading inverse of a sync relationship is treated as a client-side + // delete, so actually remove the models don't merely invalidate the cp + // cache. + // if the record being unloaded only exists on the client, we similarly + // treat it as a client side delete + this.removeRecordDataFromOwn(inverseRecordData!); + if (this.remoteState === inverseRecordData && inverseRecordData !== null) { + this.remoteState = null; + this.setHasReceivedData(true); + this.setRelationshipIsEmpty(true); + this.flushCanonicalLater(); + this.setRelationshipIsEmpty(true); + } + } else { + this.setHasDematerializedInverse(true); + } this.notifyBelongsToChange(); } - removeCompletelyFromOwn(recordData: StableRecordIdentifier) { - super.removeCompletelyFromOwn(recordData); + getData(): DefaultSingleResourceRelationship { + let data; + let payload: any = {}; + if (this.localState) { + data = this.localState; + } + if (this.localState === null && this.state.hasReceivedData) { + data = null; + } + if (this.links) { + payload.links = this.links; + } + if (data !== undefined) { + payload.data = data; + } + if (this.meta) { + payload.meta = this.meta; + } + + payload._relationship = this; + return payload; + } - if (this.canonicalState === recordData) { - this.canonicalState = null; + /* + `push` for a relationship allows the store to push a JSON API Relationship + Object onto the relationship. The relationship will then extract and set the + meta, data and links of that relationship. + + `push` use `updateMeta`, `updateData` and `updateLink` to update the state + of the relationship. + */ + push(payload: JsonApiRelationship) { + let hasRelationshipDataProperty = false; + let hasLink = false; + + if (payload.meta) { + this.updateMeta(payload.meta); } - if (this.inverseRecordData === recordData) { - this.inverseRecordData = null; - this.notifyBelongsToChange(); + if (payload.data !== undefined) { + hasRelationshipDataProperty = true; + this.updateData(payload.data as ExistingResourceIdentifierObject); + } else if (this.definition.isAsync === false && !this.state.hasReceivedData) { + hasRelationshipDataProperty = true; + + this.updateData(null!); + } + + if (payload.links) { + let originalLinks = this.links; + this.updateLinks(payload.links); + if (payload.links.related) { + let relatedLink = _normalizeLink(payload.links.related); + let currentLink = originalLinks && originalLinks.related ? _normalizeLink(originalLinks.related) : null; + let currentLinkHref = currentLink ? currentLink.href : null; + + if (relatedLink && relatedLink.href && relatedLink.href !== currentLinkHref) { + warn( + `You pushed a record of type '${this.identifier.type}' with a relationship '${this.definition.key}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, + this.definition.isAsync || this.state.hasReceivedData, + { + id: 'ds.store.push-link-for-sync-relationship', + } + ); + assert( + `You have pushed a record of type '${this.identifier.type}' with '${this.definition.key}' as a link, but the value of that link is not a string.`, + typeof relatedLink.href === 'string' || relatedLink.href === null + ); + hasLink = true; + } + } + } + + /* + Data being pushed into the relationship might contain only data or links, + or a combination of both. + + IF contains only data + IF contains both links and data + state.isEmpty -> true if is empty array (has-many) or is null (belongs-to) + state.hasReceivedData -> true + hasDematerializedInverse -> false + state.isStale -> false + allInverseRecordsAreLoaded -> run-check-to-determine + + IF contains only links + state.isStale -> true + */ + this.setHasFailedLoadAttempt(false); + if (hasRelationshipDataProperty) { + let relationshipIsEmpty = payload.data === null; + + this.setHasReceivedData(true); + this.setRelationshipIsStale(false); + this.setHasDematerializedInverse(false); + this.setRelationshipIsEmpty(relationshipIsEmpty); + } else if (hasLink) { + this.setRelationshipIsStale(true); + + let recordData = this.identifier; + let storeWrapper = this.store; + if (CUSTOM_MODEL_CLASS) { + storeWrapper.notifyBelongsToChange(recordData.type, recordData.id, recordData.lid, this.definition.key); + } else { + storeWrapper.notifyPropertyChange(recordData.type, recordData.id, recordData.lid, this.definition.key); + } } } - removeCompletelyFromInverse() { - super.removeCompletelyFromInverse(); - this.inverseRecordData = null; + updateLinks(links: PaginationLinks): void { + this.links = links; } - flushCanonical() { - //temporary fix to not remove newly created records if server returned null. - //TODO remove once we have proper diffing - if (this.inverseRecordData && isNew(this.inverseRecordData) && !this.canonicalState) { - this.willSync = false; + updateMeta(meta: any) { + this.meta = meta; + } + + updateData(resource: ExistingResourceIdentifierObject) { + assert( + `Ember Data expected the data for the ${ + this.definition.key + } relationship on a ${this.identifier.toString()} to be in a JSON API format and include an \`id\` and \`type\` property but it found ${inspect( + resource + )}. Please check your serializer and make sure it is serializing the relationship payload into a JSON API format.`, + resource === null || (resource.id !== undefined && resource.type !== undefined) + ); + + const identifier = resource ? identifierCacheFor(this.store._store).getOrCreateRecordIdentifier(resource) : null; + + if (identifier) { + this.addCanonicalRecordData(identifier); + } else if (this.remoteState) { + this.removeCanonicalRecordData(this.remoteState); + } + this.flushCanonicalLater(); + } + + /** + * External method for updating local state + */ + setRecordData(recordData: StableRecordIdentifier | null) { + if (recordData) { + this.addRecordData(recordData); + } else if (this.localState) { + this.removeRecordData(this.localState); + } + + this.setHasReceivedData(true); + this.setRelationshipIsEmpty(false); + } + + addCanonicalRecordData(recordData: StableRecordIdentifier) { + if (this.remoteState === recordData) { return; } - if (this.inverseRecordData !== this.canonicalState) { - this.inverseRecordData = this.canonicalState; - this.notifyBelongsToChange(); + + if (this.remoteState) { + this.removeCanonicalRecordData(this.remoteState); + } + + this.remoteState = recordData; + if (this.definition.type !== recordData.type) { + assertPolymorphicType( + this.store.recordDataFor(this.identifier.type, this.identifier.id, this.identifier.lid), + this.definition, + this.store.recordDataFor(recordData.type, recordData.id, recordData.lid), + this.store._store + ); + this.graph.registerPolymorphicType(this.definition.type, recordData.type); } - super.flushCanonical(); + this.graph.get(recordData, this.definition.inverseKey).addCanonicalRecordData(this.identifier); + this.flushCanonicalLater(); + this.setHasReceivedData(true); + this.setRelationshipIsEmpty(false); } addRecordData(recordData: StableRecordIdentifier) { - if (this.members.has(recordData)) { + let existingState = this.localState; + if (existingState === recordData) { return; } - // TODO @runspired can we just delete this now? - if (DEBUG && this.relationshipMeta.type !== recordData.type) { + if (this.definition.type !== recordData.type) { assertPolymorphicType( - this.store.recordDataFor(this.recordData.type, this.recordData.id, this.recordData.lid), - this.relationshipMeta, + this.store.recordDataFor(this.identifier.type, this.identifier.id, this.identifier.lid), + this.definition, this.store.recordDataFor(recordData.type, recordData.id, recordData.lid), this.store._store ); + this.graph.registerPolymorphicType(this.definition.type, recordData.type); } - if (this.inverseRecordData) { - this.removeRecordData(this.inverseRecordData); + if (existingState) { + this.removeRecordData(existingState); } - this.inverseRecordData = recordData; - super.addRecordData(recordData); + this.localState = recordData; + this.graph.get(recordData, this.definition.inverseKey).addRecordData(this.identifier); + + this.setHasReceivedData(true); this.notifyBelongsToChange(); } + removeRecordData(inverseIdentifier: StableRecordIdentifier | null) { + if (this.localState === inverseIdentifier && inverseIdentifier !== null) { + const { inverseKey } = this.definition; + this.localState = null; + this.notifyBelongsToChange(); + if (this.graph.has(inverseIdentifier, inverseKey)) { + if (!this.definition.inverseIsImplicit) { + this.graph.get(inverseIdentifier, inverseKey).removeRecordDataFromOwn(this.identifier); + } else { + this.graph.get(inverseIdentifier, inverseKey).removeRecordData(this.identifier); + } + } + } + } + + /* + Removes the given RecordData from BOTH canonical AND current state. + + This method is useful when either a deletion or a rollback on a new record + needs to entirely purge itself from an inverse relationship. + */ + removeCompletelyFromOwn(recordData: StableRecordIdentifier) { + if (this.remoteState === recordData) { + this.remoteState = null; + } + + if (this.localState === recordData) { + this.localState = null; + this.notifyBelongsToChange(); + } + } + + /** + * can be called by the other side + */ removeRecordDataFromOwn(recordData: StableRecordIdentifier) { - if (!this.members.has(recordData)) { + if (this.localState !== recordData || recordData === null) { return; } - this.inverseRecordData = null; - super.removeRecordDataFromOwn(recordData); + this.localState = null; this.notifyBelongsToChange(); } + /** + * can be called by the graph + */ removeAllRecordDatasFromOwn() { - super.removeAllRecordDatasFromOwn(); - this.inverseRecordData = null; + this.setRelationshipIsStale(true); + this.localState = null; this.notifyBelongsToChange(); } - notifyBelongsToChange() { - let recordData = this.recordData; - this.store.notifyBelongsToChange(recordData.type, recordData.id, recordData.lid, this.key); + /** + * can be called by the graph + */ + removeAllCanonicalRecordDatasFromOwn() { + this.remoteState = null; + this.flushCanonicalLater(); } - removeCanonicalRecordDataFromOwn(recordData: StableRecordIdentifier, idx?: number) { - if (!this.canonicalMembers.has(recordData)) { - return; + /* + Can be called from the other side + */ + removeCanonicalRecordData(inverseIdentifier: StableRecordIdentifier | null) { + if (this.remoteState === inverseIdentifier && inverseIdentifier !== null) { + this.remoteState = null; + this.setHasReceivedData(true); + this.setRelationshipIsEmpty(true); + this.flushCanonicalLater(); + this.setRelationshipIsEmpty(true); + + const { inverseKey } = this.definition; + if (!this.definition.isImplicit && this.graph.has(inverseIdentifier, inverseKey)) { + this.graph.get(inverseIdentifier, inverseKey).removeCanonicalRecordData(this.identifier); + } } - this.canonicalState = null; - this.setHasAnyRelationshipData(true); - this.setRelationshipIsEmpty(true); - super.removeCanonicalRecordDataFromOwn(recordData, idx); } - removeAllCanonicalRecordDatasFromOwn() { - super.removeAllCanonicalRecordDatasFromOwn(); - this.canonicalState = null; - } + /* + Call this method once a record deletion has been persisted + to purge it from BOTH current and canonical state of all + relationships. + + @method removeCompletelyFromInverse + @private + */ + removeCompletelyFromInverse() { + const seen = Object.create(null); + const { identifier } = this; + const { inverseKey } = this.definition; - getData(): DefaultSingleResourceRelationship { - let data; - let payload: any = {}; - if (this.inverseRecordData) { - data = this.inverseRecordData; + const unload = (inverseIdentifier: StableRecordIdentifier) => { + const id = inverseIdentifier.lid; + + if (seen[id] === undefined) { + if (this.graph.has(inverseIdentifier, inverseKey)) { + this.graph.get(inverseIdentifier, inverseKey).removeCompletelyFromOwn(identifier); + } + seen[id] = true; + } + }; + + if (this.localState) { + unload(this.localState); } - if (this.inverseRecordData === null && this.hasAnyRelationshipData) { - data = null; + if (this.remoteState) { + unload(this.remoteState); } - if (this.links) { - payload.links = this.links; + + if (!this.definition.isAsync) { + this.clear(); } - if (data !== undefined) { - payload.data = data; + + this.localState = null; + } + + flushCanonical() { + //temporary fix to not remove newly created records if server returned null. + //TODO remove once we have proper diffing + if (this.localState && isNew(this.localState) && !this.remoteState) { + this.willSync = false; + return; } - if (this.meta) { - payload.meta = this.meta; + if (this.localState !== this.remoteState) { + this.localState = this.remoteState; + this.notifyBelongsToChange(); } - - payload._relationship = this; - return payload; + this.willSync = false; } - updateData(data: ExistingResourceIdentifierObject) { - let recordData; - if (isNone(data)) { - recordData = null; + flushCanonicalLater() { + if (this.willSync) { + return; } - assert( - `Ember Data expected the data for the ${ - this.key - } relationship on a ${this.recordData.toString()} to be in a JSON API format and include an \`id\` and \`type\` property but it found ${inspect( - data - )}. Please check your serializer and make sure it is serializing the relationship payload into a JSON API format.`, - data === null || (data.id !== undefined && data.type !== undefined) - ); + this.willSync = true; + // Reaching back into the store to use ED's runloop + this.store._store._updateRelationshipState(this); + } - if (recordData !== null) { - recordData = identifierCacheFor(this.store._store).getOrCreateRecordIdentifier(data); + notifyBelongsToChange() { + let recordData = this.identifier; + this.store.notifyBelongsToChange(recordData.type, recordData.id, recordData.lid, this.definition.key); + } + + clear() { + if (this.localState) { + this.removeRecordData(this.localState); + } + if (this.remoteState) { + this.removeCanonicalRecordData(this.remoteState); } - this.setCanonicalRecordData(recordData); } + + destroy() {} } diff --git a/packages/record-data/addon/-private/relationships/state/create.ts b/packages/record-data/addon/-private/relationships/state/create.ts deleted file mode 100644 index 9b504b2f68d..00000000000 --- a/packages/record-data/addon/-private/relationships/state/create.ts +++ /dev/null @@ -1,67 +0,0 @@ -import BelongsToRelationship from './belongs-to'; -import ManyRelationship from './has-many'; - -type StableRecordIdentifier = import('@ember-data/store/-private/ts-interfaces/identifier').StableRecordIdentifier; -type RecordDataStoreWrapper = import('@ember-data/store/-private').RecordDataStoreWrapper; -type RelationshipSchema = import('@ember-data/store/-private/ts-interfaces/record-data-schemas').RelationshipSchema; -type Graph = import('../../graph').Graph; - -function createRelationshipFor( - relationshipMeta: RelationshipSchema, - storeWrapper: RecordDataStoreWrapper, - identifier: StableRecordIdentifier, - key: string -) { - let inverseKey = storeWrapper.inverseForRelationship(identifier.type, key); - let inverseIsAsync = storeWrapper.inverseIsAsyncForRelationship(identifier.type, key); - - if (relationshipMeta.kind === 'hasMany') { - return new ManyRelationship(storeWrapper, inverseKey, relationshipMeta, identifier, inverseIsAsync); - } else { - return new BelongsToRelationship(storeWrapper, inverseKey, relationshipMeta, identifier, inverseIsAsync); - } -} - -export default class Relationships { - declare graph: Graph; - declare _storeWrapper: RecordDataStoreWrapper; - declare initializedRelationships: { - [key: string]: BelongsToRelationship | ManyRelationship; - }; - declare identifier: StableRecordIdentifier; - - constructor(identifier: StableRecordIdentifier, graph: Graph) { - this.graph = graph; - this.identifier = identifier; - this.initializedRelationships = Object.create(null); - let storeWrapper = graph.store; - this._storeWrapper = storeWrapper; - } - - has(key: string) { - return !!this.initializedRelationships[key]; - } - - forEach(cb) { - let rels = this.initializedRelationships; - Object.keys(rels).forEach((name) => { - cb(name, rels[name]); - }); - } - - get(key: string) { - let relationships = this.initializedRelationships; - let relationship = relationships[key]; - - if (!relationship) { - let rel = this._storeWrapper.relationshipsDefinitionFor(this.identifier.type)[key]; - - if (rel) { - // lazily instantiate relationship - relationship = relationships[key] = createRelationshipFor(rel, this._storeWrapper, this.identifier, key); - } - } - - return relationship; - } -} diff --git a/packages/record-data/addon/-private/relationships/state/has-many.ts b/packages/record-data/addon/-private/relationships/state/has-many.ts index 949fc06cee4..be58dc325b5 100755 --- a/packages/record-data/addon/-private/relationships/state/has-many.ts +++ b/packages/record-data/addon/-private/relationships/state/has-many.ts @@ -1,5 +1,4 @@ import { isNone } from '@ember/utils'; -import { DEBUG } from '@glimmer/env'; import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features'; import { assertPolymorphicType } from '@ember-data/store/-debug'; @@ -7,9 +6,9 @@ import { identifierCacheFor } from '@ember-data/store/-private'; import Relationship, { isNew } from './relationship'; -type RecordDataStoreWrapper = import('@ember-data/store/-private').RecordDataStoreWrapper; +type UpgradedMeta = import('../../graph/-edge-definition').UpgradedMeta; +type Graph = import('../../graph').Graph; type StableRecordIdentifier = import('@ember-data/store/-private/ts-interfaces/identifier').StableRecordIdentifier; -type RelationshipSchema = import('@ember-data/store/-private/ts-interfaces/record-data-schemas').RelationshipSchema; type DefaultCollectionResourceRelationship = import('../../ts-interfaces/relationship-record-data').DefaultCollectionResourceRelationship; /** @@ -21,14 +20,8 @@ export default class ManyRelationship extends Relationship { declare currentState: StableRecordIdentifier[]; declare _willUpdateManyArray: boolean; declare _pendingManyArrayUpdates: any; - constructor( - store: RecordDataStoreWrapper, - inverseKey: string | null, - relationshipMeta: RelationshipSchema, - recordData: StableRecordIdentifier, - inverseIsAsync: boolean - ) { - super(store, inverseKey, relationshipMeta, recordData, inverseIsAsync); + constructor(graph: Graph, definition: UpgradedMeta, identifier: StableRecordIdentifier) { + super(graph, definition, identifier); // persisted state this.canonicalState = []; // local client state @@ -51,7 +44,7 @@ export default class ManyRelationship extends Relationship { inverseDidDematerialize(inverseRecordData: StableRecordIdentifier) { super.inverseDidDematerialize(inverseRecordData); - if (this.isAsync) { + if (this.definition.isAsync) { this.notifyManyArrayIsStale(); } } @@ -62,14 +55,14 @@ export default class ManyRelationship extends Relationship { } const { store } = this; - // TODO Type this - if (DEBUG && this.relationshipMeta.type !== recordData.type) { + if (this.definition.type !== recordData.type) { assertPolymorphicType( - store.recordDataFor(this.recordData.type, this.recordData.id, this.recordData.lid), - this.relationshipMeta, + store.recordDataFor(this.identifier.type, this.identifier.id, this.identifier.lid), + this.definition, store.recordDataFor(recordData.type, recordData.id, recordData.lid), store._store ); + this.graph.registerPolymorphicType(this.definition.type, recordData.type); } super.addRecordData(recordData, idx); // make lazy later @@ -99,8 +92,6 @@ export default class ManyRelationship extends Relationship { } removeAllCanonicalRecordDatasFromOwn() { - super.removeAllCanonicalRecordDatasFromOwn(); - this.canonicalMembers.clear(); this.canonicalState.splice(0, this.canonicalState.length); super.removeAllCanonicalRecordDatasFromOwn(); } @@ -189,22 +180,22 @@ export default class ManyRelationship extends Relationship { - @runspired */ notifyManyArrayIsStale() { - const { store, recordData } = this; + const { store, identifier: recordData } = this; if (CUSTOM_MODEL_CLASS) { - store.notifyHasManyChange(recordData.type, recordData.id, recordData.lid, this.key); + store.notifyHasManyChange(recordData.type, recordData.id, recordData.lid, this.definition.key); } else { - store.notifyPropertyChange(recordData.type, recordData.id, recordData.lid, this.key); + store.notifyPropertyChange(recordData.type, recordData.id, recordData.lid, this.definition.key); } } notifyHasManyChange() { - const { store, recordData } = this; - store.notifyHasManyChange(recordData.type, recordData.id, recordData.lid, this.key); + const { store, identifier: recordData } = this; + store.notifyHasManyChange(recordData.type, recordData.id, recordData.lid, this.definition.key); } getData(): DefaultCollectionResourceRelationship { let payload: any = {}; - if (this.hasAnyRelationshipData) { + if (this.state.hasReceivedData) { payload.data = this.currentState.slice(); } if (this.links) { @@ -234,4 +225,11 @@ export default class ManyRelationship extends Relationship { } this.updateRecordDatasFromAdapter(recordDatas); } + + updateRecordDatasFromAdapter(recordDatas?: StableRecordIdentifier[]) { + this.setHasReceivedData(true); + //TODO(Igor) move this to a proper place + //TODO Once we have adapter support, we need to handle updated and canonical changes + this.computeChanges(recordDatas); + } } diff --git a/packages/record-data/addon/-private/relationships/state/relationship.ts b/packages/record-data/addon/-private/relationships/state/relationship.ts index 7ab4e29426e..f022357baaf 100644 --- a/packages/record-data/addon/-private/relationships/state/relationship.ts +++ b/packages/record-data/addon/-private/relationships/state/relationship.ts @@ -1,20 +1,25 @@ import { assert, warn } from '@ember/debug'; -import { get } from '@ember/object'; -import { guidFor } from '@ember/object/internals'; import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features'; +import { assertPolymorphicType } from '@ember-data/store/-debug'; import { recordDataFor as peekRecordData } from '@ember-data/store/-private'; -import { implicitRelationshipsFor, implicitRelationshipStateFor, relationshipStateFor } from '../../accessors'; +import { createState } from '../../graph/-state'; import _normalizeLink from '../../normalize-link'; -import OrderedSet from '../../ordered-set'; +import OrderedSet, { guidFor } from '../../ordered-set'; +type Links = import('@ember-data/store/-private/ts-interfaces/ember-data-json-api').Links; + +type Meta = import('@ember-data/store/-private/ts-interfaces/ember-data-json-api').Meta; + +type Graph = import('../../graph').Graph; +type UpgradedMeta = import('../../graph/-edge-definition').UpgradedMeta; +type RelationshipState = import('../../graph/-state').RelationshipState; type BelongsToRelationship = import('../..').BelongsToRelationship; type RecordDataStoreWrapper = import('@ember-data/store/-private').RecordDataStoreWrapper; type RecordData = import('@ember-data/store/-private/ts-interfaces/record-data').RecordData; type RelationshipRecordData = import('../../ts-interfaces/relationship-record-data').RelationshipRecordData; type PaginationLinks = import('@ember-data/store/-private/ts-interfaces/ember-data-json-api').PaginationLinks; -type RelationshipSchema = import('@ember-data/store/-private/ts-interfaces/record-data-schemas').RelationshipSchema; type JsonApiRelationship = import('@ember-data/store/-private/ts-interfaces/record-data-json-api').JsonApiRelationship; type StableRecordIdentifier = import('@ember-data/store/-private/ts-interfaces/identifier').StableRecordIdentifier; @@ -35,260 +40,74 @@ export function isRelationshipRecordData( return typeof (recordData as RelationshipRecordData).isNew === 'function'; } -const IMPLICIT_KEY_RAND = Date.now(); - -function implicitKeyFor(key: string): string { - return `implicit-inverse:${key}:${IMPLICIT_KEY_RAND}`; -} - -export interface ImplicitRelationshipMeta { - kind: 'implicit'; - name: string; // a generated randomized key - type: string; // the expected type for this implicit inverse - inverse: string; // we must always have a key on the inverse - options: { - async: boolean; - polymorphic?: boolean; - }; -} - -export type RelationshipMeta = ImplicitRelationshipMeta | RelationshipSchema; - export default class Relationship { - declare inverseIsAsync: boolean | undefined; - declare kind: 'hasMany' | 'belongsTo' | 'implicit'; - declare recordData: StableRecordIdentifier; + declare graph: Graph; + declare store: RecordDataStoreWrapper; + declare definition: UpgradedMeta; + declare identifier: StableRecordIdentifier; + declare _state: RelationshipState | null; + declare members: OrderedSet; declare canonicalMembers: OrderedSet; - declare store: RecordDataStoreWrapper; - declare key: string; - declare inverseKey: string; - declare isAsync: boolean; - declare isPolymorphic: boolean; - declare inverseIsImplicit: boolean; - declare relationshipMeta: RelationshipMeta; - declare meta: any; - declare __inverseMeta: any; - declare shouldForceReload: boolean; - declare relationshipIsStale: boolean; - declare hasDematerializedInverse: boolean; - declare hasAnyRelationshipData: boolean; - declare relationshipIsEmpty: boolean; - declare hasFailedLoadAttempt: boolean; - declare links?: PaginationLinks; + declare meta: Meta | null; + declare links: Links | PaginationLinks | null; declare willSync: boolean; - constructor( - storeWrapper: RecordDataStoreWrapper, - inverseKey: string | null, - relationshipMeta: RelationshipSchema | ImplicitRelationshipMeta, - identifier: StableRecordIdentifier, - inverseIsAsync?: boolean - ) { - this.inverseIsAsync = inverseIsAsync; - this.kind = relationshipMeta.kind; - let async = relationshipMeta.options.async; - let polymorphic = relationshipMeta.options.polymorphic; - this.recordData = identifier; + constructor(graph: Graph, definition: UpgradedMeta, identifier: StableRecordIdentifier) { + this.graph = graph; + this.store = graph.store; + this.definition = definition; + this.identifier = identifier; + this._state = null; + this.members = new OrderedSet(); this.canonicalMembers = new OrderedSet(); - this.store = storeWrapper; - this.key = relationshipMeta.name; - this.inverseIsImplicit = !inverseKey; - this.inverseKey = inverseKey || implicitKeyFor(this.key); - this.isAsync = typeof async === 'undefined' ? true : async; - this.isPolymorphic = typeof polymorphic === 'undefined' ? false : polymorphic; - this.relationshipMeta = relationshipMeta; - //This probably breaks for polymorphic relationship in complex scenarios, due to - //multiple possible modelNames - this.meta = null; - this.__inverseMeta = undefined; - this.links = undefined; - this.hasFailedLoadAttempt = false; - this.shouldForceReload = false; + this.meta = null; + this.links = null; this.willSync = false; - - /* - This flag forces fetch. `true` for a single request once `reload()` - has been called `false` at all other times. - */ - // this.shouldForceReload = false; - - /* - This flag indicates whether we should - re-fetch the relationship the next time - it is accessed. - - The difference between this flag and `shouldForceReload` - is in how we treat the presence of partially missing data: - - for a forced reload, we will reload the link or EVERY record - - for a stale reload, we will reload the link (if present) else only MISSING records - - Ideally these flags could be merged, but because we don't give the - request layer the option of deciding how to resolve the data being queried - we are forced to differentiate for now. - - It is also possible for a relationship to remain stale after a forced reload; however, - in this case `hasFailedLoadAttempt` ought to be `true`. - - false when - => recordData.isNew() on initial setup - => a previously triggered request has resolved - => we get relationship data via push - - true when - => !recordData.isNew() on initial setup - => an inverse has been unloaded - => we get a new link for the relationship - - TODO @runspired unskip the acceptance tests and fix these flags - */ - this.relationshipIsStale = false; - - /* - This flag indicates whether we should - **partially** re-fetch the relationship the - next time it is accessed. - - false when - => initial setup - => a previously triggered request has resolved - - true when - => an inverse has been unloaded - */ - this.hasDematerializedInverse = false; - - /* - This flag indicates whether we should consider the content - of this relationship "known". - - If we have no relationship knowledge, and the relationship - is `async`, we will attempt to fetch the relationship on - access if it is also stale. - - Snapshot uses this to tell the difference between unknown - (`undefined`) or empty (`null`). The reason for this is that - we wouldn't want to serialize unknown relationships as `null` - as that might overwrite remote state. - - All relationships for a newly created (`store.createRecord()`) are - considered known (`hasAnyRelationshipData === true`). - - true when - => we receive a push with either new data or explicit empty (`[]` or `null`) - => the relationship is a belongsTo and we have received data from - the other side. - - false when - => we have received no signal about what data belongs in this relationship - => the relationship is a hasMany and we have only received data from - the other side. - */ - this.hasAnyRelationshipData = false; - - /* - Flag that indicates whether an empty relationship is explicitly empty - (signaled by push giving us an empty array or null relationship) - e.g. an API response has told us that this relationship is empty. - - Thus far, it does not appear that we actually need this flag; however, - @runspired has found it invaluable when debugging relationship tests - to determine whether (and why if so) we are in an incorrect state. - - true when - => we receive a push with explicit empty (`[]` or `null`) - => we have received no signal about what data belongs in this relationship - => on initial create (as no signal is known yet) - - false at all other times - */ - this.relationshipIsEmpty = true; - - /* - Flag def here for reference, defined as getter in has-many.js / belongs-to.js - - true when - => hasAnyRelationshipData is true - AND - => members (NOT canonicalMembers) @each !isEmpty - - TODO, consider changing the conditional here from !isEmpty to !hiddenFromRecordArrays - */ - - // TODO do we want this anymore? Seems somewhat useful - // especially if we rename to `hasUpdatedLink` - // which would tell us slightly more about why the - // relationship is stale - // this.updatedLink = false; - } - - get isNew(): boolean { - return isNew(this.recordData); } - _inverseIsAsync(): boolean { - return !!this.inverseIsAsync; - } - - _inverseIsSync(): boolean { - return !this.inverseIsImplicit && !this.inverseIsAsync; + get state(): RelationshipState { + let { _state } = this; + if (!_state) { + _state = this._state = createState(); + } + return _state; } - get _inverseMeta(): RelationshipMeta | null { - if (this.__inverseMeta === undefined) { - let inverseMeta: RelationshipSchema | null = null; - - if (!this.inverseIsImplicit) { - // We know we have a full inverse relationship - let type = this.relationshipMeta.type; - let inverseModelClass = this.store._store.modelFor(type); - let inverseRelationships = get(inverseModelClass, 'relationshipsByName'); - inverseMeta = inverseRelationships.get(this.inverseKey) || null; - } - - this.__inverseMeta = inverseMeta; - } - return this.__inverseMeta; + get isNew(): boolean { + return isNew(this.identifier); } recordDataDidDematerialize() { - if (this.inverseIsImplicit) { + if (this.definition.inverseIsImplicit) { return; } - const inverseKey = this.inverseKey; - - // we actually want a union of members and canonicalMembers - // they should be disjoint but currently are not due to a bug + const inverseKey = this.definition.inverseKey; this.forAllMembers((inverseIdentifier) => { - let recordData = inverseIdentifier && peekRecordData(inverseIdentifier); - if (!recordData || !inverseIdentifier) { + inverseIdentifier; + if (!inverseIdentifier || !this.graph.has(inverseIdentifier, inverseKey)) { return; } - - let relationship = relationshipStateFor(this.store, inverseIdentifier, inverseKey); - // TODO DO we need to grab implicit inverse and do this? + let relationship = this.graph.get(inverseIdentifier, inverseKey); // For canonical members, it is possible that inverseRecordData has already been associated to // to another record. For such cases, do not dematerialize the inverseRecordData if ( - !relationship || //we are implicit - relationship.kind === 'implicit' || - relationship.kind === 'hasMany' || // the inverse is a hasMany - !(relationship as BelongsToRelationship).inverseRecordData || - this.recordData === (relationship as BelongsToRelationship).inverseRecordData + relationship.definition.kind !== 'belongsTo' || + !(relationship as BelongsToRelationship).localState || + this.identifier === (relationship as BelongsToRelationship).localState ) { - if (!relationship) { - return; // TODO wtf happened here in all the rebasing - } - relationship.inverseDidDematerialize(this.recordData); + relationship.inverseDidDematerialize(this.identifier); } }); } forAllMembers(callback: (im: StableRecordIdentifier | null) => void) { + // ensure we don't walk anything twice if an entry is + // in both members and canonicalMembers let seen = Object.create(null); for (let i = 0; i < this.members.list.length; i++) { @@ -311,7 +130,7 @@ export default class Relationship { } inverseDidDematerialize(inverseRecordData: StableRecordIdentifier | null) { - if (!this.isAsync || (inverseRecordData && isNew(inverseRecordData))) { + if (!this.definition.isAsync || (inverseRecordData && isNew(inverseRecordData))) { // unloading inverse of a sync relationship is treated as a client-side // delete, so actually remove the models don't merely invalidate the cp // cache. @@ -368,45 +187,20 @@ export default class Relationship { addCanonicalRecordData(recordData: StableRecordIdentifier, idx?: number) { if (!this.canonicalMembers.has(recordData)) { - this.canonicalMembers.add(recordData); - this.setupInverseRelationship(recordData); - } - this.flushCanonicalLater(); - this.setHasAnyRelationshipData(true); - } - - setupInverseRelationship(recordData: StableRecordIdentifier) { - if (!this.inverseIsImplicit) { - let relationship = relationshipStateFor(this.store, recordData, this.inverseKey); - // if we have only just initialized the inverse relationship, then it - // already has this.recordData in its canonicalMembers, so skip the - // unnecessary work. The exception to this is polymorphic - // relationships whose members are determined by their inverse, as those - // relationships cannot efficiently find their inverse payloads. - if (relationship) { - relationship.addCanonicalRecordData(this.recordData); - } - } else { - const relationships = implicitRelationshipsFor(this.store, recordData); - let relationship = implicitRelationshipStateFor(this.store, recordData, this.inverseKey); - - if (!relationship) { - relationship = relationships[this.inverseKey] = new Relationship( - this.store, - this.key, - { - kind: 'implicit', - name: implicitKeyFor(this.key), - type: this.recordData.type, - inverse: this.key, - options: { async: false }, // our inverse must always be present since we are implicit - }, - recordData, - this.isAsync + if (this.definition.type !== recordData.type) { + assertPolymorphicType( + this.store.recordDataFor(this.identifier.type, this.identifier.id, this.identifier.lid), + this.definition, + this.store.recordDataFor(recordData.type, recordData.id, recordData.lid), + this.store._store ); + this.graph.registerPolymorphicType(this.definition.type, recordData.type); } - relationship.addCanonicalRecordData(this.recordData); + this.canonicalMembers.add(recordData); + this.graph.get(recordData, this.definition.inverseKey).addCanonicalRecordData(this.identifier); } + this.flushCanonicalLater(); + this.setHasReceivedData(true); } removeCanonicalRecordDatas(recordDatas: StableRecordIdentifier[], idx?: number) { @@ -423,17 +217,15 @@ export default class Relationship { if (this.canonicalMembers.has(recordData)) { this.removeCanonicalRecordDataFromOwn(recordData, idx); - if (!this.inverseIsImplicit) { - this.removeCanonicalRecordDataFromInverse(recordData); - } else { - if (!recordData) { - return; - } - const implicitRelationships = implicitRelationshipStateFor(this.store, recordData, this.inverseKey); - if (implicitRelationships[this.inverseKey]) { - implicitRelationships[this.inverseKey].removeCanonicalRecordData(this.recordData); - } + if (!recordData || this.definition.isImplicit) { + return; } + + const { inverseKey } = this.definition; + if (this.graph.has(recordData, inverseKey)) { + this.graph.get(recordData, inverseKey).removeCanonicalRecordData(this.identifier); + } + this.flushCanonicalLater(); // TODO does this need to be in the outer context } } @@ -442,45 +234,25 @@ export default class Relationship { if (!this.members.has(recordData)) { this.members.addWithIndex(recordData, idx); this.notifyRecordRelationshipAdded(recordData, idx); - if (!this.inverseIsImplicit) { - relationshipStateFor(this.store, recordData, this.inverseKey).addRecordData(this.recordData); - } else { - const implicitRelationships = implicitRelationshipsFor(this.store, recordData); - let relationship = implicitRelationshipStateFor(this.store, recordData, this.inverseKey); - if (!relationship) { - relationship = implicitRelationships[this.inverseKey] = new Relationship( - this.store, - this.key, - { - kind: 'implicit', - name: implicitKeyFor(this.key), - type: this.recordData.type, - inverse: this.key, - options: { async: false }, // our inverse must always be present since we are implicit - }, - recordData, - this.isAsync - ); - } - relationship.addRecordData(this.recordData); - } + + this.graph.get(recordData, this.definition.inverseKey).addRecordData(this.identifier); } - this.setHasAnyRelationshipData(true); + this.setHasReceivedData(true); } removeRecordData(recordData: StableRecordIdentifier | null) { if (this.members.has(recordData)) { this.removeRecordDataFromOwn(recordData); - if (!this.inverseIsImplicit) { + if (!this.definition.inverseIsImplicit) { this.removeRecordDataFromInverse(recordData); } else { if (!recordData) { return; } - - const relationship = implicitRelationshipStateFor(this.store, recordData, this.inverseKey); - if (relationship) { - relationship.removeRecordData(this.recordData); + const { inverseKey } = this.definition; + // TODO is this check ever false? + if (this.graph.has(recordData, inverseKey)) { + this.graph.get(recordData, inverseKey).removeRecordData(this.identifier); } } } @@ -490,11 +262,11 @@ export default class Relationship { if (!recordData) { return; } - if (!this.inverseIsImplicit) { - let inverseRelationship = relationshipStateFor(this.store, recordData, this.inverseKey); + if (!this.definition.inverseIsImplicit) { + let inverseRelationship = this.graph.get(recordData, this.definition.inverseKey); //Need to check for existence, as the record might unloading at the moment if (inverseRelationship) { - inverseRelationship.removeRecordDataFromOwn(this.recordData); + inverseRelationship.removeRecordDataFromOwn(this.identifier); } } } @@ -503,19 +275,6 @@ export default class Relationship { this.members.delete(recordData); } - removeCanonicalRecordDataFromInverse(recordData: StableRecordIdentifier | null) { - if (!recordData) { - return; - } - if (!this.inverseIsImplicit) { - let inverseRelationship = relationshipStateFor(this.store, recordData, this.inverseKey); - //Need to check for existence, as the record might unloading at the moment - if (inverseRelationship) { - inverseRelationship.removeCanonicalRecordDataFromOwn(this.recordData); - } - } - } - removeCanonicalRecordDataFromOwn(recordData: StableRecordIdentifier | null, idx?: number) { this.canonicalMembers.deleteWithIndex(recordData, idx); this.flushCanonicalLater(); @@ -532,38 +291,25 @@ export default class Relationship { removeCompletelyFromInverse() { // we actually want a union of members and canonicalMembers // they should be disjoint but currently are not due to a bug - let seen = Object.create(null); - const recordData = this.recordData; - - let unload; - if (!this.inverseIsImplicit) { - unload = (inverseRecordData) => { - const id = guidFor(inverseRecordData); - - if (seen[id] === undefined) { - if (!this.inverseIsImplicit) { - const relationship = relationshipStateFor(this.store, inverseRecordData, this.inverseKey); - relationship.removeCompletelyFromOwn(recordData); - } - seen[id] = true; - } - }; - } else { - unload = (inverseRecordData) => { - const id = guidFor(inverseRecordData); + const seen = Object.create(null); + const { identifier } = this; + const { inverseKey } = this.definition; - if (seen[id] === undefined) { - const relationship = implicitRelationshipStateFor(this.store, inverseRecordData, this.inverseKey); - relationship.removeCompletelyFromOwn(recordData); - seen[id] = true; + const unload = (inverseIdentifier: StableRecordIdentifier) => { + const id = inverseIdentifier.lid; + + if (seen[id] === undefined) { + if (this.graph.has(inverseIdentifier, inverseKey)) { + this.graph.get(inverseIdentifier, inverseKey).removeCompletelyFromOwn(identifier); } - }; - } + seen[id] = true; + } + }; this.members.toArray().forEach(unload); this.canonicalMembers.toArray().forEach(unload); - if (!this.isAsync) { + if (!this.definition.isAsync) { this.clear(); } } @@ -612,39 +358,30 @@ export default class Relationship { this.links = links; } - updateRecordDatasFromAdapter(recordDatas?: StableRecordIdentifier[]) { - this.setHasAnyRelationshipData(true); - //TODO(Igor) move this to a proper place - //TODO Once we have adapter support, we need to handle updated and canonical changes - this.computeChanges(recordDatas); - } - - computeChanges(recordDatas?: StableRecordIdentifier[]) {} - notifyRecordRelationshipAdded(recordData?, idxs?) {} - setHasAnyRelationshipData(value: boolean) { - this.hasAnyRelationshipData = value; + setHasReceivedData(value: boolean) { + this.state.hasReceivedData = value; } setHasDematerializedInverse(value: boolean) { - this.hasDematerializedInverse = value; + this.state.hasDematerializedInverse = value; } setRelationshipIsStale(value: boolean) { - this.relationshipIsStale = value; + this.state.isStale = value; } setRelationshipIsEmpty(value: boolean) { - this.relationshipIsEmpty = value; + this.state.isEmpty = value; } setShouldForceReload(value: boolean) { - this.shouldForceReload = value; + this.state.shouldForceReload = value; } setHasFailedLoadAttempt(value: boolean) { - this.hasFailedLoadAttempt = value; + this.state.hasFailedLoadAttempt = value; } /* @@ -666,9 +403,9 @@ export default class Relationship { if (payload.data !== undefined) { hasRelationshipDataProperty = true; this.updateData(payload.data); - } else if (this.isAsync === false && !this.hasAnyRelationshipData) { + } else if (this.definition.isAsync === false && !this.state.hasReceivedData) { hasRelationshipDataProperty = true; - let data = this.kind === 'hasMany' ? [] : null; + let data = this.definition.kind === 'hasMany' ? [] : null; this.updateData(data); } @@ -683,14 +420,14 @@ export default class Relationship { if (relatedLink && relatedLink.href && relatedLink.href !== currentLinkHref) { warn( - `You pushed a record of type '${this.recordData.type}' with a relationship '${this.key}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, - this.isAsync || this.hasAnyRelationshipData, + `You pushed a record of type '${this.identifier.type}' with a relationship '${this.definition.key}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, + this.definition.isAsync || this.state.hasReceivedData, { id: 'ds.store.push-link-for-sync-relationship', } ); assert( - `You have pushed a record of type '${this.recordData.type}' with '${this.key}' as a link, but the value of that link is not a string.`, + `You have pushed a record of type '${this.identifier.type}' with '${this.definition.key}' as a link, but the value of that link is not a string.`, typeof relatedLink.href === 'string' || relatedLink.href === null ); hasLink = true; @@ -704,44 +441,36 @@ export default class Relationship { IF contains only data IF contains both links and data - relationshipIsEmpty -> true if is empty array (has-many) or is null (belongs-to) - hasAnyRelationshipData -> true + state.isEmpty -> true if is empty array (has-many) or is null (belongs-to) + state.hasReceivedData -> true hasDematerializedInverse -> false - relationshipIsStale -> false + state.isStale -> false allInverseRecordsAreLoaded -> run-check-to-determine IF contains only links - relationshipIsStale -> true + state.isStale -> true */ this.setHasFailedLoadAttempt(false); if (hasRelationshipDataProperty) { let relationshipIsEmpty = payload.data === null || (Array.isArray(payload.data) && payload.data.length === 0); - this.setHasAnyRelationshipData(true); + this.setHasReceivedData(true); this.setRelationshipIsStale(false); this.setHasDematerializedInverse(false); this.setRelationshipIsEmpty(relationshipIsEmpty); } else if (hasLink) { this.setRelationshipIsStale(true); - let recordData = this.recordData; + let recordData = this.identifier; let storeWrapper = this.store; if (CUSTOM_MODEL_CLASS) { - storeWrapper.notifyBelongsToChange(recordData.type, recordData.id, recordData.lid, this.key!); + storeWrapper.notifyBelongsToChange(recordData.type, recordData.id, recordData.lid, this.definition.key); } else { - storeWrapper.notifyPropertyChange( - recordData.type, - recordData.id, - recordData.lid, - // We know we are not an implicit relationship here - this.key! - ); + storeWrapper.notifyPropertyChange(recordData.type, recordData.id, recordData.lid, this.definition.key); } } } - localStateIsEmpty() {} - updateData(payload?) {} destroy() {} diff --git a/packages/record-data/tests/integration/graph/edge-removal/helpers.ts b/packages/record-data/tests/integration/graph/edge-removal/helpers.ts index 508f3e7671a..036c553e09e 100644 --- a/packages/record-data/tests/integration/graph/edge-removal/helpers.ts +++ b/packages/record-data/tests/integration/graph/edge-removal/helpers.ts @@ -148,8 +148,8 @@ export async function setInitialState(context: Context, config: TestConfig, asse await settled(); const chrisIdentifier = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const chrisBestFriend = graph.get(chrisIdentifier).get('bestFriends'); - const johnBestFriend = graph.get(johnIdentifier).get('bestFriends'); + const chrisBestFriend = graph.get(chrisIdentifier, 'bestFriends'); + const johnBestFriend = graph.get(johnIdentifier, 'bestFriends'); // pre-conds assert.strictEqual(chris.name, 'Chris', 'PreCond: We have chris'); @@ -189,8 +189,8 @@ export async function setInitialState(context: Context, config: TestConfig, asse assert.strictEqual(Object.keys(chrisImplicits).length, 1, 'PreCond: Chris has one implicit relationship'); - const chrisImplicitFriend = chrisImplicits[chrisBestFriend.inverseKey] as Relationship; - const johnImplicitFriend = johnImplicits[johnBestFriend.inverseKey] as Relationship; + const chrisImplicitFriend = chrisImplicits[chrisBestFriend.definition.inverseKey] as Relationship; + const johnImplicitFriend = johnImplicits[johnBestFriend.definition.inverseKey] as Relationship; assert.ok(chrisImplicitFriend, 'PreCond: Chris has an implicit best friend'); @@ -243,8 +243,8 @@ export async function setInitialState(context: Context, config: TestConfig, asse john, chrisIdentifier, johnIdentifier, - chrisInverseKey: chrisBestFriend.inverseKey, - johnInverseKey: johnBestFriend.inverseKey, + chrisInverseKey: chrisBestFriend.definition.inverseKey, + johnInverseKey: johnBestFriend.definition.inverseKey, }; } @@ -258,18 +258,22 @@ export async function testFinalState( const { graph } = context; const { chrisIdentifier, johnIdentifier } = testState; - const chrisBestFriend = graph.get(chrisIdentifier).get('bestFriends'); + const chrisBestFriend = graph.get(chrisIdentifier, 'bestFriends'); const chrisState = stateOf(chrisBestFriend); // this specific case gets it's own WAT // this is something ideally a refactor should do away with. const isUnloadOfImplictAsyncHasManyWithLocalChange = - config.isUnloadAsDelete && config.dirtyLocal && config.async && config.relType === 'hasMany' && config.inverseNull; + !!config.isUnloadAsDelete && + !!config.dirtyLocal && + !!config.async && + config.relType === 'hasMany' && + !!config.inverseNull; // related to above another WAT that refactor should cleanup // in this case we don't care if sync/async const isUnloadOfImplictHasManyWithLocalChange = - config.isUnloadAsDelete && config.dirtyLocal && config.relType === 'hasMany' && config.inverseNull; + !!config.isUnloadAsDelete && !!config.dirtyLocal && config.relType === 'hasMany' && !!config.inverseNull; // a final WAT likely related to the first two, persisted delete w/o unload of // a sync hasMany with local changes is not cleared. This final WAT is handled @@ -326,7 +330,7 @@ export async function testFinalState( if (OUTCOMES.johnCleared) { assert.false(graph.identifiers.has(johnIdentifier), 'Result: Relationships for John were cleared from the cache'); } else { - const johnBestFriend = graph.get(johnIdentifier).get('bestFriends'); + const johnBestFriend = graph.get(johnIdentifier, 'bestFriends'); const johnState = stateOf(johnBestFriend); assert.deepEqual( diff --git a/packages/record-data/tests/integration/graph/edge-removal/setup.ts b/packages/record-data/tests/integration/graph/edge-removal/setup.ts index bfa9d24aa5d..63630883991 100644 --- a/packages/record-data/tests/integration/graph/edge-removal/setup.ts +++ b/packages/record-data/tests/integration/graph/edge-removal/setup.ts @@ -5,6 +5,8 @@ import { graphFor } from '@ember-data/record-data/-private'; import Store from '@ember-data/store'; import { DSModel } from '@ember-data/store/-private/ts-interfaces/ds-model'; +type ManyRelationship = import('@ember-data/record-data/-private').ManyRelationship; + type CollectionResourceDocument = import('@ember-data/store/-private/ts-interfaces/ember-data-json-api').CollectionResourceDocument; type EmptyResourceDocument = import('@ember-data/store/-private/ts-interfaces/ember-data-json-api').EmptyResourceDocument; type JsonApiDocument = import('@ember-data/store/-private/ts-interfaces/ember-data-json-api').JsonApiDocument; @@ -13,7 +15,6 @@ type BelongsToRelationship = import('@ember-data/record-data/-private').BelongsT type CoreStore = import('@ember-data/store/-private/system/core-store').default; type Dict = import('@ember-data/store/-private/ts-interfaces/utils').Dict; type Relationship = import('@ember-data/record-data/-private').Relationship; -type Relationships = import('@ember-data/record-data/-private/relationships/state/create').default; type StableRecordIdentifier = import('@ember-data/store/-private/ts-interfaces/identifier').StableRecordIdentifier; class AbstractMap { @@ -21,29 +22,42 @@ class AbstractMap { has(identifier: StableRecordIdentifier) { let graph = graphFor(this.store._storeWrapper); - return this.isImplicit ? graph.implicitMap.has(identifier) : graph.identifiers.has(identifier); + return graph.identifiers.has(identifier); } } class AbstractGraph { public identifiers: AbstractMap; - public implicit: AbstractMap; - private cachedRelationships: WeakMap; - private cachedImplicits: WeakMap>; + public implicit: { has(identifier: StableRecordIdentifier): boolean }; constructor(private store: CoreStore) { this.identifiers = new AbstractMap(store, false); - this.implicit = new AbstractMap(store, true); - this.cachedRelationships = new WeakMap(); - this.cachedImplicits = new WeakMap(); + this.implicit = { + has: (identifier) => { + return Object.keys(this.getImplicit(identifier)).length > 0; + }, + }; } - get(identifier: StableRecordIdentifier): Relationships { - return graphFor(this.store._storeWrapper).get(identifier); + get( + identifier: StableRecordIdentifier, + propertyName: string + ): ManyRelationship | BelongsToRelationship | Relationship { + return graphFor(this.store._storeWrapper).get(identifier, propertyName); } getImplicit(identifier: StableRecordIdentifier): Dict { - return graphFor(this.store._storeWrapper).getImplicit(identifier); + const rels = graphFor(this.store._storeWrapper).identifiers.get(identifier); + let implicits = Object.create(null); + if (rels) { + Object.keys(rels).forEach((key) => { + let rel = rels[key]!; + if (rel.definition.isImplicit) { + implicits[key] = rel; + } + }); + } + return implicits; } } @@ -51,16 +65,16 @@ function graphForTest(store: CoreStore) { return new AbstractGraph(store); } -function isBelongsTo(rel: Relationship): rel is BelongsToRelationship { - return rel.kind === 'belongsTo'; +function isBelongsTo(rel: BelongsToRelationship | ManyRelationship | Relationship): rel is BelongsToRelationship { + return rel.definition.kind === 'belongsTo'; } -export function stateOf(rel: Relationship) { +export function stateOf(rel: BelongsToRelationship | ManyRelationship | Relationship) { let local, remote; if (isBelongsTo(rel)) { // we cast these to array form to make the tests more legible - local = rel.inverseRecordData ? [rel.inverseRecordData] : []; - remote = rel.canonicalState ? [rel.canonicalState] : []; + local = rel.localState ? [rel.localState] : []; + remote = rel.remoteState ? [rel.remoteState] : []; } else { local = rel.members.list.map((m) => (m ? m : null)); remote = rel.canonicalMembers.list.map((m) => (m ? m : null)); diff --git a/packages/record-data/tests/integration/graph/edge-test.ts b/packages/record-data/tests/integration/graph/edge-test.ts index 0aaa56b9c9a..2569631ba80 100644 --- a/packages/record-data/tests/integration/graph/edge-test.ts +++ b/packages/record-data/tests/integration/graph/edge-test.ts @@ -7,6 +7,8 @@ import { graphFor, RecordData } from '@ember-data/record-data/-private'; import Store from '@ember-data/store'; import { recordDataFor } from '@ember-data/store/-private'; +import { stateOf } from './edge-removal/setup'; + module('Integration | Graph | Edges', function (hooks) { setupTest(hooks); @@ -38,11 +40,7 @@ module('Integration | Graph | Edges', function (hooks) { const identifier = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); const identifier2 = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - const relationships = graph.get(identifier); - - assert.ok(relationships, `We can access relationships`); - - const bestFriend = relationships.get('bestFriend'); + const bestFriend = graph.get(identifier, 'bestFriend'); assert.true( recordDataFor(identifier) === null, @@ -70,8 +68,9 @@ module('Integration | Graph | Edges', function (hooks) { 'We still have no record data instance after push of only an identifier within a relationship' ); - assert.strictEqual(bestFriend.canonicalState, identifier2, 'Our initial canonical state is correct'); - assert.strictEqual(bestFriend.getData().data, identifier2, 'Our initial current state is correct'); + let state = stateOf(bestFriend); + assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); + assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); store.push({ data: { @@ -98,8 +97,7 @@ module('Integration | Graph | Edges', function (hooks) { const identifier = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); const identifier2 = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - const relationships = graph.get(identifier); - const bestFriend = relationships.get('bestFriend'); + const bestFriend = graph.get(identifier, 'bestFriend'); store.push({ data: { @@ -115,18 +113,16 @@ module('Integration | Graph | Edges', function (hooks) { 'We have no record data instance after push of only an identifier within a relationship' ); - assert.strictEqual(bestFriend.canonicalState, identifier2, 'Our initial canonical state is correct'); - assert.strictEqual(bestFriend.getData().data, identifier2, 'Our initial current state is correct'); + let state = stateOf(bestFriend); + assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); + assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); const identifier3 = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '3' }); bestFriend.push({ data: identifier3 }); - assert.strictEqual( - bestFriend.canonicalState, - identifier3, - 'Our canonical state is correct after canonical update' - ); - assert.strictEqual(bestFriend.getData().data, identifier3, 'Our current state is correct after canonical update'); + state = stateOf(bestFriend); + assert.deepEqual(state.remote, [identifier3], 'Our canonical state is correct after canonical update'); + assert.deepEqual(state.local, [identifier3], 'Our current state is correct after canonical update'); assert.true( recordDataFor(identifier) === null, @@ -135,8 +131,9 @@ module('Integration | Graph | Edges', function (hooks) { bestFriend.setRecordData(identifier2); - assert.strictEqual(bestFriend.canonicalState, identifier3, 'Our canonical state is correct after local update'); - assert.strictEqual(bestFriend.getData().data, identifier2, 'Our current state is correct after local update'); + state = stateOf(bestFriend); + assert.deepEqual(state.remote, [identifier3], 'Our canonical state is correct after local update'); + assert.deepEqual(state.local, [identifier2], 'Our current state is correct after local update'); assert.true( recordDataFor(identifier) === null, @@ -168,8 +165,7 @@ module('Integration | Graph | Edges', function (hooks) { const identifier = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); const identifier2 = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - const relationships = graph.get(identifier); - const bestFriend = relationships.get('bestFriend'); + const bestFriend = graph.get(identifier, 'bestFriend'); store.push({ data: { @@ -185,18 +181,16 @@ module('Integration | Graph | Edges', function (hooks) { 'We have no record data instance after push of only an identifier within a relationship' ); - assert.strictEqual(bestFriend.canonicalState, identifier2, 'Our initial canonical state is correct'); - assert.strictEqual(bestFriend.getData().data, identifier2, 'Our initial current state is correct'); + let state = stateOf(bestFriend); + assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); + assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); const identifier3 = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '3' }); bestFriend.push({ data: identifier3 }); - assert.strictEqual( - bestFriend.canonicalState, - identifier3, - 'Our canonical state is correct after canonical update' - ); - assert.strictEqual(bestFriend.getData().data, identifier3, 'Our current state is correct after canonical update'); + state = stateOf(bestFriend); + assert.deepEqual(state.remote, [identifier3], 'Our canonical state is correct after canonical update'); + assert.deepEqual(state.local, [identifier3], 'Our current state is correct after canonical update'); assert.true( recordDataFor(identifier) === null, @@ -205,8 +199,9 @@ module('Integration | Graph | Edges', function (hooks) { bestFriend.setRecordData(identifier2); - assert.strictEqual(bestFriend.canonicalState, identifier3, 'Our canonical state is correct after local update'); - assert.strictEqual(bestFriend.getData().data, identifier2, 'Our current state is correct after local update'); + state = stateOf(bestFriend); + assert.deepEqual(state.remote, [identifier3], 'Our canonical state is correct after local update'); + assert.deepEqual(state.local, [identifier2], 'Our current state is correct after local update'); assert.true( recordDataFor(identifier) === null, @@ -239,8 +234,7 @@ module('Integration | Graph | Edges', function (hooks) { const identifier = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); const identifier2 = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); const identifier3 = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '3' }); - const relationships = graph.get(identifier); - const bestFriends = relationships.get('bestFriends'); + const bestFriends = graph.get(identifier, 'bestFriends'); store.push({ data: { @@ -260,21 +254,19 @@ module('Integration | Graph | Edges', function (hooks) { 'We have no record data instance after push of only an identifier within a relationship' ); - assert.deepEqual(bestFriends.canonicalState, [identifier2], 'Our initial canonical state is correct'); - assert.deepEqual(bestFriends.getData().data, [identifier2], 'Our initial current state is correct'); + let state = stateOf(bestFriends); + assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); + assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); bestFriends.push({ data: [identifier2, identifier3] }); + state = stateOf(bestFriends); assert.deepEqual( - bestFriends.canonicalState, + state.remote, [identifier2, identifier3], 'Our canonical state is correct after canonical update' ); - assert.deepEqual( - bestFriends.getData().data, - [identifier2, identifier3], - 'Our current state is correct after canonical update' - ); + assert.deepEqual(state.local, [identifier2, identifier3], 'Our current state is correct after canonical update'); assert.true( recordDataFor(identifier) === null, @@ -284,13 +276,10 @@ module('Integration | Graph | Edges', function (hooks) { bestFriends.addRecordData(identifier4); + state = stateOf(bestFriends); + assert.deepEqual(state.remote, [identifier2, identifier3], 'Our canonical state is correct after local update'); assert.deepEqual( - bestFriends.canonicalState, - [identifier2, identifier3], - 'Our canonical state is correct after local update' - ); - assert.deepEqual( - bestFriends.getData().data, + state.local, [identifier2, identifier3, identifier4], 'Our current state is correct after local update' ); @@ -326,8 +315,7 @@ module('Integration | Graph | Edges', function (hooks) { const identifier = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); const identifier2 = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); const identifier3 = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '3' }); - const relationships = graph.get(identifier); - const bestFriends = relationships.get('bestFriends'); + const bestFriends = graph.get(identifier, 'bestFriends'); store.push({ data: { @@ -347,21 +335,19 @@ module('Integration | Graph | Edges', function (hooks) { 'We have no record data instance after push of only an identifier within a relationship' ); - assert.deepEqual(bestFriends.canonicalState, [identifier2], 'Our initial canonical state is correct'); - assert.deepEqual(bestFriends.getData().data, [identifier2], 'Our initial current state is correct'); + let state = stateOf(bestFriends); + assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); + assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); bestFriends.push({ data: [identifier2, identifier3] }); + state = stateOf(bestFriends); assert.deepEqual( - bestFriends.canonicalState, + state.remote, [identifier2, identifier3], 'Our canonical state is correct after canonical update' ); - assert.deepEqual( - bestFriends.getData().data, - [identifier2, identifier3], - 'Our current state is correct after canonical update' - ); + assert.deepEqual(state.local, [identifier2, identifier3], 'Our current state is correct after canonical update'); assert.true( recordDataFor(identifier) === null, @@ -371,13 +357,10 @@ module('Integration | Graph | Edges', function (hooks) { bestFriends.addRecordData(identifier4); + state = stateOf(bestFriends); + assert.deepEqual(state.remote, [identifier2, identifier3], 'Our canonical state is correct after local update'); assert.deepEqual( - bestFriends.canonicalState, - [identifier2, identifier3], - 'Our canonical state is correct after local update' - ); - assert.deepEqual( - bestFriends.getData().data, + state.local, [identifier2, identifier3, identifier4], 'Our current state is correct after local update' ); diff --git a/packages/record-data/tests/integration/graph/polymorphism/implicit-keys-test.ts b/packages/record-data/tests/integration/graph/polymorphism/implicit-keys-test.ts new file mode 100644 index 00000000000..c5bd48c6d66 --- /dev/null +++ b/packages/record-data/tests/integration/graph/polymorphism/implicit-keys-test.ts @@ -0,0 +1,77 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model, { attr, belongsTo } from '@ember-data/model'; +import { graphFor } from '@ember-data/record-data/-private'; +import Store, { recordIdentifierFor } from '@ember-data/store'; + +module('Integration | Graph | Implicit Keys', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + const { owner } = this; + owner.register('service:store', Store); + }); + + test('Non-polymorphic records do not trigger polymorphic assertions when they share the same key with another record', async function (assert) { + const { owner } = this; + class User extends Model { + @attr name; + @belongsTo('organization', { async: false, inverse: null }) organization; + } + class Product extends Model { + @attr name; + @belongsTo('organization', { async: false, inverse: null }) organization; + } + class Organization extends Model { + @attr name; + } + owner.register('model:user', User); + owner.register('model:product', Product); + owner.register('model:organization', Organization); + + const store = owner.lookup('service:store'); + const graph = graphFor(store); + let user, product, organization; + + assert.expectNoAssertion(() => { + [user, product, organization] = store.push({ + data: [ + { + type: 'user', + id: '1', + attributes: { name: 'Chris' }, + relationships: { + organization: { data: { type: 'organization', id: '1 ' } }, + }, + }, + { + type: 'product', + id: '1', + attributes: { name: 'Awesome Relationships' }, + relationships: { + organization: { data: { type: 'organization', id: '1 ' } }, + }, + }, + { + type: 'organization', + id: '1', + attributes: { name: 'Ember.js' }, + }, + ], + }); + }); + + const userIdentifier = recordIdentifierFor(user); + const productIdentifier = recordIdentifierFor(product); + const organizationIdentifier = recordIdentifierFor(organization); + + const userOrg = graph.get(userIdentifier, 'organization'); + const userImpl = graph.get(organizationIdentifier, userOrg.definition.inverseKey); + const productOrg = graph.get(productIdentifier, 'organization'); + const productImpl = graph.get(organizationIdentifier, productOrg.definition.inverseKey); + + assert.notStrictEqual(userImpl, productImpl, 'We have separate implicit caches'); + }); +}); diff --git a/packages/store/addon/-debug/index.js b/packages/store/addon/-debug/index.js index 366f9b0defd..0c8eae73f22 100644 --- a/packages/store/addon/-debug/index.js +++ b/packages/store/addon/-debug/index.js @@ -49,8 +49,9 @@ if (DEBUG) { let addedClass = store.modelFor(addedInternalModel.modelName); let assertionMessage = `The '${addedModelName}' type does not implement '${relationshipModelName}' and thus cannot be assigned to the '${key}' relationship in '${parentModelName}'. Make it a descendant of '${relationshipModelName}' or use a mixin of the same name.`; + let isPolymorphic = checkPolymorphic(relationshipClass, addedClass); - assert(assertionMessage, checkPolymorphic(relationshipClass, addedClass)); + assert(assertionMessage, isPolymorphic); }; } diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index e2b70e65748..77d9b9252d5 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -63,6 +63,10 @@ import { internalModelFactoryFor, recordIdentifierFor, setRecordIdentifier } fro import RecordDataStoreWrapper from './store/record-data-store-wrapper'; import { normalizeResponseHelper } from './store/serializer-response'; +type BelongsToRelationship = import('@ember-data/record-data/-private').BelongsToRelationship; +type ManyRelationship = import('@ember-data/record-data/-private').ManyRelationship; + +type RelationshipState = import('@ember-data/record-data/-private/graph/-state').RelationshipState; type ShimModelClass = import('./model/shim-model-class').default; type Snapshot = import('./snapshot').default; type Backburner = import('@ember/runloop/-private/backburner').Backburner; @@ -224,6 +228,8 @@ interface CoreStore { adapter: string; } +type RelationshipEdge = Relationship | BelongsToRelationship | ManyRelationship; + abstract class CoreStore extends Service { /** * EmberData specific backburner instance @@ -246,7 +252,7 @@ abstract class CoreStore extends Service { // used for coalescing record save requests private _pendingSave: PendingSaveItem[] = []; // used for coalescing relationship updates - private _updatedRelationships: Relationship[] = []; + private _updatedRelationships: RelationshipEdge[] = []; // used for coalescing internal model updates private _updatedInternalModels: InternalModel[] = []; @@ -1709,42 +1715,34 @@ abstract class CoreStore extends Service { } let adapter = this.adapterFor(relationshipMeta.type); - let { - relationshipIsStale, - hasDematerializedInverse, - hasAnyRelationshipData, - relationshipIsEmpty, - shouldForceReload, - } = resource._relationship; + let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = resource._relationship + .state as RelationshipState; const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this, resource); let shouldFindViaLink = resource.links && resource.links.related && (typeof adapter.findHasMany === 'function' || typeof resource.data === 'undefined') && - (shouldForceReload || - hasDematerializedInverse || - relationshipIsStale || - (!allInverseRecordsAreLoaded && !relationshipIsEmpty)); + (shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty)); // fetch via link if (shouldFindViaLink) { return this.findHasMany(parentInternalModel, resource.links.related, relationshipMeta, options); } - let preferLocalCache = hasAnyRelationshipData && !relationshipIsEmpty; + let preferLocalCache = hasReceivedData && !isEmpty; let hasLocalPartialData = - hasDematerializedInverse || (relationshipIsEmpty && Array.isArray(resource.data) && resource.data.length > 0); + hasDematerializedInverse || (isEmpty && Array.isArray(resource.data) && resource.data.length > 0); // fetch using data, pulling from local cache if possible - if (!shouldForceReload && !relationshipIsStale && (preferLocalCache || hasLocalPartialData)) { + if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) { let internalModels = resource.data.map((json) => this._internalModelForResource(json)); return this.findMany(internalModels, options); } - let hasData = hasAnyRelationshipData && !relationshipIsEmpty; + let hasData = hasReceivedData && !isEmpty; // fetch by data if (hasData || hasLocalPartialData) { @@ -1810,22 +1808,14 @@ abstract class CoreStore extends Service { } const internalModel = resource.data ? this._internalModelForResource(resource.data) : null; - let { - relationshipIsStale, - hasDematerializedInverse, - hasAnyRelationshipData, - relationshipIsEmpty, - shouldForceReload, - } = resource._relationship; + let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = resource._relationship + .state as RelationshipState; const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this, resource); let shouldFindViaLink = resource.links && resource.links.related && - (shouldForceReload || - hasDematerializedInverse || - relationshipIsStale || - (!allInverseRecordsAreLoaded && !relationshipIsEmpty)); + (shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty)); if (internalModel) { // short circuit if we are already loading @@ -1852,13 +1842,13 @@ abstract class CoreStore extends Service { return this._fetchBelongsToLinkFromResource(resource, parentInternalModel, relationshipMeta, options); } - let preferLocalCache = hasAnyRelationshipData && allInverseRecordsAreLoaded && !relationshipIsEmpty; - let hasLocalPartialData = hasDematerializedInverse || (relationshipIsEmpty && resource.data); + let preferLocalCache = hasReceivedData && allInverseRecordsAreLoaded && !isEmpty; + let hasLocalPartialData = hasDematerializedInverse || (isEmpty && resource.data); // null is explicit empty, undefined is "we don't know anything" let localDataIsEmpty = resource.data === undefined || resource.data === null; // fetch using data, pulling from local cache if possible - if (!shouldForceReload && !relationshipIsStale && (preferLocalCache || hasLocalPartialData)) { + if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) { /* We have canonical data, but our local state is empty */ @@ -3625,7 +3615,7 @@ abstract class CoreStore extends Service { } } - _updateRelationshipState(relationship: Relationship) { + _updateRelationshipState(relationship: Relationship | BelongsToRelationship | ManyRelationship) { if (this._updatedRelationships.push(relationship) !== 1) { return; } diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 231499c0612..4d90ccbaf0c 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -637,7 +637,7 @@ export default class InternalModel { if (isAsync) { let internalModel = identifier !== null ? store._internalModelForResource(identifier) : null; - if (resource._relationship.hasFailedLoadAttempt) { + if (resource._relationship.state.hasFailedLoadAttempt) { return this._relationshipProxyCache[key]; } @@ -683,7 +683,7 @@ export default class InternalModel { if (!manyArray) { let initialState = this.store._getHasManyByJsonApiResource(jsonApi); // TODO move this to a public api - let inverseIsAsync = jsonApi._relationship ? jsonApi._relationship._inverseIsAsync() : false; + let inverseIsAsync = jsonApi._relationship ? jsonApi._relationship.definition.inverseIsAsync : false; manyArray = ManyArray.create({ store: this.store, type: this.store.modelFor(relationshipMeta.type), @@ -740,7 +740,7 @@ export default class InternalModel { let manyArray = this.getManyArray(key, isAsync); if (isAsync) { - if (jsonApi!._relationship!.hasFailedLoadAttempt) { + if (jsonApi!._relationship!.state.hasFailedLoadAttempt) { return this._relationshipProxyCache[key]; } @@ -1447,7 +1447,7 @@ export default class InternalModel { return `<${this.modelName}:${this.id}>`; } - referenceFor(kind, name) { + referenceFor(kind: string | null, name: string) { let reference = this.references[name]; if (!reference) { @@ -1457,24 +1457,19 @@ export default class InternalModel { // because of the intimate API access involved. This is something we will need to redesign. assert(`snapshot.belongsTo only supported for @ember-data/record-data`); } - const relationshipStateFor = require('@ember-data/record-data/-private').relationshipStateFor; - const relationship = relationshipStateFor(this.store._storeWrapper, this.identifier, name); + const graphFor = require('@ember-data/record-data/-private').graphFor; + const relationship = graphFor(this.store._storeWrapper).get(this.identifier, name); if (DEBUG && kind) { let modelName = this.modelName; - assert( - `There is no ${kind} relationship named '${name}' on a model of modelClass '${modelName}'`, - !!relationship - ); - - let actualRelationshipKind = relationship.relationshipMeta.kind; + let actualRelationshipKind = relationship.definition.kind; assert( `You tried to get the '${name}' relationship on a '${modelName}' via record.${kind}('${name}'), but the relationship is of kind '${actualRelationshipKind}'. Use record.${actualRelationshipKind}('${name}') instead.`, actualRelationshipKind === kind ); } - let relationshipKind = relationship.relationshipMeta.kind; + let relationshipKind = relationship.definition.kind; let identifierOrInternalModel = this.identifier; if (relationshipKind === 'belongsTo') { @@ -1514,7 +1509,7 @@ function handleCompletedRelationshipRequest(internalModel, key, relationship, va // for the sync belongsTo reload case there will be no proxy // for the async reload case there will be no proxy if the ui // has never been accessed - if (proxy && relationship.kind === 'belongsTo') { + if (proxy && relationship.definition.kind === 'belongsTo') { if (proxy.content && proxy.content.isDestroying) { proxy.set('content', null); } diff --git a/packages/store/addon/-private/system/references/belongs-to.js b/packages/store/addon/-private/system/references/belongs-to.js index 3a8d8b2e249..c6d29dd786e 100644 --- a/packages/store/addon/-private/system/references/belongs-to.js +++ b/packages/store/addon/-private/system/references/belongs-to.js @@ -25,7 +25,7 @@ export default class BelongsToReference extends Reference { super(store, parentIMOrIdentifier); this.key = key; this.belongsToRelationship = belongsToRelationship; - this.type = belongsToRelationship.relationshipMeta.type; + this.type = belongsToRelationship.definition.type; this.parent = internalModelFactoryFor(store).peek(parentIMOrIdentifier).recordReference; this.parentIdentifier = parentIMOrIdentifier; @@ -151,13 +151,13 @@ export default class BelongsToReference extends Reference { assertPolymorphicType( internalModelForReference(this), - this.belongsToRelationship.relationshipMeta, + this.belongsToRelationship.definition, record._internalModel, this.store ); //TODO Igor cleanup, maybe move to relationship push - this.belongsToRelationship.setCanonicalRecordData(recordIdentifierFor(record)); + this.belongsToRelationship.updateData(recordIdentifierFor(record)); return record; }); diff --git a/packages/store/addon/-private/system/references/has-many.js b/packages/store/addon/-private/system/references/has-many.js index 15a6105ba92..f5d5eb3d539 100644 --- a/packages/store/addon/-private/system/references/has-many.js +++ b/packages/store/addon/-private/system/references/has-many.js @@ -23,7 +23,7 @@ export default class HasManyReference extends Reference { super(store, parentIMOrIdentifier); this.key = key; this.hasManyRelationship = hasManyRelationship; - this.type = hasManyRelationship.relationshipMeta.type; + this.type = hasManyRelationship.definition.type; this.parent = internalModelFactoryFor(store).peek(parentIMOrIdentifier).recordReference; @@ -187,7 +187,7 @@ export default class HasManyReference extends Reference { let record = this.store.push(obj); if (DEBUG) { - let relationshipMeta = this.hasManyRelationship.relationshipMeta; + let relationshipMeta = this.hasManyRelationship.definition; assertPolymorphicType(internalModel, relationshipMeta, record._internalModel, this.store); } return recordIdentifierFor(record); @@ -195,14 +195,14 @@ export default class HasManyReference extends Reference { this.hasManyRelationship.computeChanges(identifiers); - return internalModel.getHasMany(this.hasManyRelationship.key); + return internalModel.getHasMany(this.hasManyRelationship.definition.key); // TODO IGOR it seems wrong that we were returning the many array here //return this.hasManyRelationship.manyArray; }); } _isLoaded() { - let hasRelationshipDataProperty = this.hasManyRelationship.hasAnyRelationshipData; + let hasRelationshipDataProperty = this.hasManyRelationship.state.hasReceivedData; if (!hasRelationshipDataProperty) { return false; } diff --git a/packages/store/addon/-private/system/snapshot.ts b/packages/store/addon/-private/system/snapshot.ts index a690a8f42c2..398dd6b46ef 100644 --- a/packages/store/addon/-private/system/snapshot.ts +++ b/packages/store/addon/-private/system/snapshot.ts @@ -20,8 +20,6 @@ type DSModelSchema = import('../ts-interfaces/ds-model').DSModelSchema; type ModelSchema = import('../ts-interfaces/ds-model').ModelSchema; type AttributeSchema = import('../ts-interfaces/record-data-schemas').AttributeSchema; type RelationshipSchema = import('../ts-interfaces/record-data-schemas').RelationshipSchema; -type HasManyRelationship = import('@ember-data/record-data/-private/relationships/state/has-many').default; -type BelongsToRelationship = import('@ember-data/record-data/-private/relationships/state/belongs-to').default; type Store = import('./core-store').default; type RecordId = string | null; @@ -286,7 +284,6 @@ export default class Snapshot implements Snapshot { */ belongsTo(keyName: string, options?: { id?: boolean }): Snapshot | RecordId | undefined { let returnModeIsId = !!(options && options.id); - let relationship: BelongsToRelationship; let inverseInternalModel: InternalModel | null; let result: Snapshot | RecordId | undefined; let store = this._internalModel.store; @@ -314,16 +311,17 @@ export default class Snapshot implements Snapshot { assert(`snapshot.belongsTo only supported when using the package @ember-data/record-data`); } - const relationshipStateFor = require('@ember-data/record-data/-private').relationshipStateFor; + const graphFor = require('@ember-data/record-data/-private').graphFor; const { identifier } = CUSTOM_MODEL_CLASS ? this : this._internalModel; - relationship = relationshipStateFor(this._store._storeWrapper, identifier, keyName) as BelongsToRelationship; + const relationship = graphFor(this._store._storeWrapper).get(identifier, keyName); + assert( `You looked up the ${keyName} belongsTo relationship for { type: ${identifier.type}, id: ${identifier.id}, lid: ${identifier.lid} but no such relationship was found.`, relationship ); assert( `You looked up the ${keyName} belongsTo relationship for { type: ${identifier.type}, id: ${identifier.id}, lid: ${identifier.lid} but that relationship is a hasMany.`, - relationship.kind === 'belongsTo' + relationship.definition.kind === 'belongsTo' ); let value = relationship.getData(); @@ -383,7 +381,6 @@ export default class Snapshot implements Snapshot { */ hasMany(keyName: string, options?: { ids?: boolean }): RecordId[] | Snapshot[] | undefined { let returnModeIsIds = !!(options && options.ids); - let relationship: HasManyRelationship; let results: RecordId[] | Snapshot[] | undefined; let cachedIds: RecordId[] | undefined = this._hasManyIds[keyName]; let cachedSnapshots: Snapshot[] | undefined = this._hasManyRelationships[keyName]; @@ -412,16 +409,16 @@ export default class Snapshot implements Snapshot { assert(`snapshot.hasMany only supported when using the package @ember-data/record-data`); } - const relationshipStateFor = require('@ember-data/record-data/-private').relationshipStateFor; + const graphFor = require('@ember-data/record-data/-private').graphFor; const { identifier } = CUSTOM_MODEL_CLASS ? this : this._internalModel; - relationship = relationshipStateFor(this._store._storeWrapper, identifier, keyName) as HasManyRelationship; + const relationship = graphFor(this._store._storeWrapper).get(identifier, keyName); assert( `You looked up the ${keyName} hasMany relationship for { type: ${identifier.type}, id: ${identifier.id}, lid: ${identifier.lid} but no such relationship was found.`, relationship ); assert( `You looked up the ${keyName} hasMany relationship for { type: ${identifier.type}, id: ${identifier.id}, lid: ${identifier.lid} but that relationship is a belongsTo.`, - relationship.kind === 'hasMany' + relationship.definition.kind === 'hasMany' ); let value = relationship.getData(); diff --git a/packages/store/addon/-private/system/store/internal-model-factory.ts b/packages/store/addon/-private/system/store/internal-model-factory.ts index 31e41e0a7c1..ee63144aa93 100644 --- a/packages/store/addon/-private/system/store/internal-model-factory.ts +++ b/packages/store/addon/-private/system/store/internal-model-factory.ts @@ -129,6 +129,16 @@ export default class InternalModelFactory { // otherIm.destroy(); } + /* + TODO @runspired consider adding this to make polymorphism even nicer + if (HAS_RECORD_DATA_PACKAGE) { + if (identifier.type !== matchedIdentifier.type) { + const graphFor = require('@ember-data/record-data/-private').graphFor; + graphFor(this).registerPolymorphicType(identifier.type, matchedIdentifier.type); + } + } + */ + return intendedIdentifier; }); this._identityMap = new IdentityMap();