diff --git a/addon/-private/system/model/model.js b/addon/-private/system/model/model.js index 9e2dad3953b..cd6c33cdf0a 100644 --- a/addon/-private/system/model/model.js +++ b/addon/-private/system/model/model.js @@ -1907,6 +1907,10 @@ if (DEBUG) { // the computed property. let meta = value.meta(); + /* + This is buggy because if the parent has never been looked up + via `modelFor` it will not have `modelName` set. + */ meta.parentType = proto.constructor; } } diff --git a/addon/-private/system/relationship-meta.js b/addon/-private/system/relationship-meta.js index 740eff604ee..3f10d7c6265 100644 --- a/addon/-private/system/relationship-meta.js +++ b/addon/-private/system/relationship-meta.js @@ -1,6 +1,5 @@ import { singularize } from 'ember-inflector'; import normalizeModelName from './normalize-model-name'; -import { DEBUG } from '@glimmer/env'; export function typeForRelationshipMeta(meta) { let modelName; @@ -13,19 +12,13 @@ export function typeForRelationshipMeta(meta) { } export function relationshipFromMeta(meta) { - let result = { + return { key: meta.key, kind: meta.kind, type: typeForRelationshipMeta(meta), - options: meta.options, + options: meta.options, name: meta.name, parentType: meta.parentType, isRelationship: true }; - - if (DEBUG) { - result.parentType = meta.parentType; - } - - return result; } diff --git a/addon/-private/system/relationships/relationship-payloads-manager.js b/addon/-private/system/relationships/relationship-payloads-manager.js index 3bf03430a0b..f64e976fe0a 100644 --- a/addon/-private/system/relationships/relationship-payloads-manager.js +++ b/addon/-private/system/relationships/relationship-payloads-manager.js @@ -1,5 +1,7 @@ import { get } from '@ember/object'; -import RelationshipPayloads from './relationship-payloads'; +// import { DEBUG } from '@glimmer/env'; +import { assert } from '@ember/debug'; +import { default as RelationshipPayloads, TypeCache } from './relationship-payloads'; /** Manages relationship payloads for a given store, for uninitialized @@ -59,6 +61,7 @@ export default class RelationshipPayloadsManager { this._store = store; // cache of `RelationshipPayload`s this._cache = Object.create(null); + this._inverseLookupCache = new TypeCache(); } /** @@ -81,9 +84,7 @@ export default class RelationshipPayloadsManager { @method */ get(modelName, id, relationshipName) { - let modelClass = this._store._modelFor(modelName); - let relationshipsByName = get(modelClass, 'relationshipsByName'); - let relationshipPayloads = this._getRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName, false); + let relationshipPayloads = this._getRelationshipPayloads(modelName, relationshipName, false); return relationshipPayloads && relationshipPayloads.get(modelName, id, relationshipName); } @@ -113,10 +114,8 @@ export default class RelationshipPayloadsManager { push(modelName, id, relationshipsData) { if (!relationshipsData) { return; } - let modelClass = this._store._modelFor(modelName); - let relationshipsByName = get(modelClass, 'relationshipsByName'); Object.keys(relationshipsData).forEach(key => { - let relationshipPayloads = this._getRelationshipPayloads(modelName, key, modelClass, relationshipsByName, true); + let relationshipPayloads = this._getRelationshipPayloads(modelName, key, true); if (relationshipPayloads) { relationshipPayloads.push(modelName, id, key, relationshipsData[key]); } @@ -132,7 +131,7 @@ export default class RelationshipPayloadsManager { let modelClass = this._store._modelFor(modelName); let relationshipsByName = get(modelClass, 'relationshipsByName'); relationshipsByName.forEach((_, relationshipName) => { - let relationshipPayloads = this._getRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName, false); + let relationshipPayloads = this._getRelationshipPayloads(modelName, relationshipName, false); if (relationshipPayloads) { relationshipPayloads.unload(modelName, id, relationshipName); } @@ -156,7 +155,7 @@ export default class RelationshipPayloadsManager { relationshipPayloads.get('user', 'hobbies') === relationshipPayloads.get('hobby', 'user'); The signature has a somewhat large arity to avoid extra work, such as - a) string maipulation & allocation with `modelName` and + a) string manipulation & allocation with `modelName` and `relationshipName` b) repeatedly getting `relationshipsByName` via `Ember.get` @@ -164,15 +163,131 @@ export default class RelationshipPayloadsManager { @private @method */ - _getRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName, init) { - if (!relationshipsByName.has(relationshipName)) { return; } + _getRelationshipPayloads(modelName, relationshipName, init) { + let relInfo = this.getRelationshipInfo(modelName, relationshipName); + + if (relInfo === null) { + return; + } + + let cache = this._cache[relInfo.lhs_key]; + + if (!cache && init) { + return this._initializeRelationshipPayloads(relInfo); + } + + return cache; + } + + getRelationshipInfo(modelName, relationshipName) { + let inverseCache = this._inverseLookupCache; + let store = this._store; + let cached = inverseCache.get(modelName, relationshipName); + + // CASE: We have a cached resolution (null if no relationship exists) + if (cached !== undefined) { + return cached; + } + + let modelClass = store._modelFor(modelName); + let relationshipsByName = get(modelClass, 'relationshipsByName'); + + // CASE: We don't have a relationship at all + if (!relationshipsByName.has(relationshipName)) { + inverseCache.set(modelName, relationshipName, null); + return null; + } + + let inverseMeta = modelClass.inverseFor(relationshipName, store); + let relationshipMeta = relationshipsByName.get(relationshipName); + let selfIsPolymorphic = relationshipMeta.options !== undefined && relationshipMeta.options.polymorphic === true; + let inverseBaseModelName = relationshipMeta.type; + + // CASE: We have no inverse + if (!inverseMeta) { + let info = { + lhs_key: `${modelName}:${relationshipName}`, + lhs_modelNames: [modelName], + lhs_baseModelName: modelName, + lhs_relationshipName: relationshipName, + lhs_relationshipMeta: relationshipMeta, + lhs_isPolymorphic: selfIsPolymorphic, + rhs_key: '', + rhs_modelNames: [], + rhs_baseModelName: inverseBaseModelName, + rhs_relationshipName: '', + rhs_relationshipMeta: null, + rhs_isPolymorphic: false, + hasInverse: false, + isSelfReferential: false, // modelName === inverseBaseModelName, + isReflexive: false + }; + + inverseCache.set(modelName, relationshipName, info); + + return info; + } + + // CASE: We do have an inverse - let key = `${modelName}:${relationshipName}`; - if (!this._cache[key] && init) { - return this._initializeRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName); + let inverseRelationshipName = inverseMeta.name; + let inverseRelationshipMeta = get(inverseMeta.type, 'relationshipsByName').get(inverseRelationshipName); + let baseModelName = inverseRelationshipMeta.type; + let isSelfReferential = baseModelName === inverseBaseModelName; + + // 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 = inverseCache.get(baseModelName, relationshipName) || + inverseCache.get(inverseBaseModelName, inverseRelationshipName); + if (cached) { + // TODO this assert can be removed if the above assert is enabled + assert(`The ${inverseBaseModelName}:${inverseRelationshipName} relationship declares 'inverse: null', but it was resolved as the inverse for ${baseModelName}:${relationshipName}.`, cached.hasInverse !== false); + + let isLHS = cached.lhs_baseModelName === baseModelName; + let modelNames = isLHS ? cached.lhs_modelNames : cached.rhs_modelNames; + // make this lookup easier in the future by caching the key + modelNames.push(modelName); + inverseCache.set(modelName, relationshipName, cached); + + return cached; } - return this._cache[key]; + let info = { + lhs_key: `${baseModelName}:${relationshipName}`, + lhs_modelNames: [modelName], + lhs_baseModelName: baseModelName, + lhs_relationshipName: relationshipName, + lhs_relationshipMeta: relationshipMeta, + lhs_isPolymorphic: selfIsPolymorphic, + rhs_key: `${inverseBaseModelName}:${inverseRelationshipName}`, + rhs_modelNames: [], + rhs_baseModelName: inverseBaseModelName, + rhs_relationshipName: inverseRelationshipName, + rhs_relationshipMeta: inverseRelationshipMeta, + rhs_isPolymorphic: inverseRelationshipMeta.options !== undefined && inverseRelationshipMeta.options.polymorphic === true, + hasInverse: true, + isSelfReferential, + isReflexive: isSelfReferential && relationshipName === inverseRelationshipName + }; + + // Create entries for the baseModelName as well as modelName to speed up + // inverse lookups + inverseCache.set(baseModelName, relationshipName, info); + inverseCache.set(modelName, relationshipName, info); + + // Greedily populate the inverse + inverseCache.set(inverseBaseModelName, inverseRelationshipName, info); + + return info; } /** @@ -181,29 +296,19 @@ export default class RelationshipPayloadsManager { @private @method */ - _initializeRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName) { - let relationshipMeta = relationshipsByName.get(relationshipName); - let inverseMeta = modelClass.inverseFor(relationshipName, this._store); - - let inverseModelName; - let inverseRelationshipName; - let inverseRelationshipMeta; - - // figure out the inverse relationship; we need two things - // a) the inverse model name - //- b) the name of the inverse relationship - if (inverseMeta) { - inverseRelationshipName = inverseMeta.name - inverseModelName = relationshipMeta.type; - inverseRelationshipMeta = get(inverseMeta.type, 'relationshipsByName').get(inverseRelationshipName); - } else { - // relationship has no inverse - inverseModelName = inverseRelationshipName = ''; - inverseRelationshipMeta = null; - } + _initializeRelationshipPayloads(relInfo) { + let lhsKey = relInfo.lhs_key; + let rhsKey = relInfo.rhs_key; + let existingPayloads = this._cache[lhsKey]; + + if (relInfo.hasInverse === true && relInfo.rhs_isPolymorphic === true) { + existingPayloads = this._cache[rhsKey]; - let lhsKey = `${modelName}:${relationshipName}`; - let rhsKey = `${inverseModelName}:${inverseRelationshipName}`; + if (existingPayloads !== undefined) { + this._cache[lhsKey] = existingPayloads; + return existingPayloads; + } + } // populate the cache for both sides of the relationship, as they both use // the same `RelationshipPayloads`. @@ -211,16 +316,12 @@ export default class RelationshipPayloadsManager { // This works out better than creating a single common key, because to // compute that key we would need to do work to look up the inverse // - return this._cache[lhsKey] = - this._cache[rhsKey] = - new RelationshipPayloads( - this._store, - modelName, - relationshipName, - relationshipMeta, - inverseModelName, - inverseRelationshipName, - inverseRelationshipMeta - ); + let cache = this._cache[lhsKey] = new RelationshipPayloads(relInfo); + + if (relInfo.hasInverse === true) { + this._cache[rhsKey] = cache; + } + + return cache; } } diff --git a/addon/-private/system/relationships/relationship-payloads.js b/addon/-private/system/relationships/relationship-payloads.js index d0815007048..4233d461cbe 100644 --- a/addon/-private/system/relationships/relationship-payloads.js +++ b/addon/-private/system/relationships/relationship-payloads.js @@ -1,22 +1,56 @@ import { assert } from '@ember/debug'; +// TODO this is now VERY similar to the identity/internal-model map +// so we should probably generalize +export class TypeCache { + constructor() { + this.types = Object.create(null); + } + get(modelName, id) { + let { types } = this; + + if (types[modelName] !== undefined) { + return types[modelName][id]; + } + } + + set(modelName, id, payload) { + let { types } = this; + let typeMap = types[modelName]; + + if (typeMap === undefined) { + typeMap = types[modelName] = Object.create(null); + } + + typeMap[id] = payload; + } + + delete(modelName, id) { + let { types } = this; + + if (types[modelName] !== undefined) { + delete types[modelName][id]; + } + } +} + /** - Manages the payloads for both sides of a single relationship, across all model - instances. + Manages the payloads for both sides of a single relationship, across all model + instances. - For example, with + For example, with - const User = DS.Model.extend({ + const User = DS.Model.extend({ hobbies: DS.hasMany('hobby') }); - const Hobby = DS.Model.extend({ + const Hobby = DS.Model.extend({ user: DS.belongsTo('user') }); - let relationshipPayloads = new RelationshipPayloads('user', 'hobbies', 'hobby', 'user'); + let relationshipPayloads = new RelationshipPayloads('user', 'hobbies', 'hobby', 'user'); - let userPayload = { + let userPayload = { data: { id: 1, type: 'user', @@ -31,44 +65,22 @@ import { assert } from '@ember/debug'; } }; - // here we expect the payload of the individual relationship - relationshipPayloads.push('user', 1, 'hobbies', userPayload.data.relationships.hobbies); + // here we expect the payload of the individual relationship + relationshipPayloads.push('user', 1, 'hobbies', userPayload.data.relationships.hobbies); - relationshipPayloads.get('user', 1, 'hobbies'); - relationshipPayloads.get('hobby', 2, 'user'); + relationshipPayloads.get('user', 1, 'hobbies'); + relationshipPayloads.get('hobby', 2, 'user'); - @class RelationshipPayloads - @private -*/ + @class RelationshipPayloads + @private + */ export default class RelationshipPayloads { - constructor(store, modelName, relationshipName, relationshipMeta, inverseModelName, inverseRelationshipName, inverseRelationshipMeta) { - this._store = store; - - this._lhsModelName = modelName; - this._lhsRelationshipName = relationshipName; - this._lhsRelationshipMeta = relationshipMeta; - - this._rhsModelName = inverseModelName; - this._rhsRelationshipName = inverseRelationshipName; - this._rhsRelationshipMeta = inverseRelationshipMeta; + constructor(relInfo) { + this._relInfo = relInfo; // a map of id -> payloads for the left hand side of the relationship. - this._lhsPayloads = Object.create(null); - if (modelName !== inverseModelName || relationshipName !== inverseRelationshipName) { - // The common case of a non-reflexive relationship, or a reflexive - // relationship whose inverse is not itself - this._rhsPayloads = Object.create(null); - this._isReflexive = false; - } else { - // Edge case when we have a reflexive relationship to itself - // eg user hasMany friends inverse friends - // - // In this case there aren't really two sides to the relationship, but - // we set `_rhsPayloads = _lhsPayloads` to make things easier to reason - // about - this._rhsPayloads = this._lhsPayloads; - this._isReflexive = true; - } + this.lhs_payloads = new TypeCache(); + this.rhs_payloads = relInfo.isReflexive ? this.lhs_payloads : new TypeCache(); // When we push relationship payloads, just stash them in a queue until // somebody actually asks for one of them. @@ -79,71 +91,91 @@ export default class RelationshipPayloads { } /** - Get the payload for the relationship of an individual record. + Get the payload for the relationship of an individual record. - This might return the raw payload as pushed into the store, or one computed - from the payload of the inverse relationship. + This might return the raw payload as pushed into the store, or one computed + from the payload of the inverse relationship. - @method - */ + @method + */ get(modelName, id, relationshipName) { this._flushPending(); if (this._isLHS(modelName, relationshipName)) { - return this._lhsPayloads[id]; + return this.lhs_payloads.get(modelName, id); } else { - assert(`${modelName}:${relationshipName} is not either side of this relationship, ${this._lhsModelName}:${this._lhsRelationshipName}<->${this._rhsModelName}:${this._rhsRelationshipName}`, this._isRHS(modelName, relationshipName)); - return this._rhsPayloads[id]; + assert(`${modelName}:${relationshipName} is not either side of this relationship, ${this._relInfo.lhs_key}<->${this._relInfo.rhs_key}`, this._isRHS(modelName, relationshipName)); + return this.rhs_payloads.get(modelName, id); } } /** - Push a relationship payload for an individual record. + Push a relationship payload for an individual record. - This will make the payload available later for both this relationship and its inverse. + This will make the payload available later for both this relationship and its inverse. - @method - */ + @method + */ push(modelName, id, relationshipName, relationshipData) { this._pendingPayloads.push([modelName, id, relationshipName, relationshipData]); } /** - Unload the relationship payload for an individual record. + Unload the relationship payload for an individual record. - This does not unload the inverse relationship payload. + This does not unload the inverse relationship payload. - @method - */ + @method + */ unload(modelName, id, relationshipName) { this._flushPending(); if (this._isLHS(modelName, relationshipName)) { - delete this._lhsPayloads[id]; + delete this.lhs_payloads.delete(modelName, id); } else { - assert(`${modelName}:${relationshipName} is not either side of this relationship, ${this._lhsModelName}:${this._lhsRelationshipName}<->${this._rhsModelName}:${this._rhsRelationshipName}`, this._isRHS(modelName, relationshipName)); - delete this._rhsPayloads[id]; + assert(`${modelName}:${relationshipName} is not either side of this relationship, ${this._relInfo.lhs_baseModelName}:${this._relInfo.lhs_relationshipName}<->${this._relInfo.rhs_baseModelName}:${this._relInfo.rhs_relationshipName}`, this._isRHS(modelName, relationshipName)); + delete this.rhs_payloads.delete(modelName, id); } } /** - @return {boolean} true iff `modelName` and `relationshipName` refer to the - left hand side of this relationship, as opposed to the right hand side. + @return {boolean} true iff `modelName` and `relationshipName` refer to the + left hand side of this relationship, as opposed to the right hand side. - @method - */ + @method + */ _isLHS(modelName, relationshipName) { - return modelName === this._lhsModelName && relationshipName === this._lhsRelationshipName; + let relInfo = this._relInfo; + let isSelfReferential = relInfo.isSelfReferential; + let isRelationship = relationshipName === relInfo.lhs_relationshipName; + + if (isRelationship === true) { + return isSelfReferential === true || // itself + modelName === relInfo.lhs_baseModelName || // base or non-polymorphic + relInfo.lhs_modelNames.indexOf(modelName) !== -1; // polymorphic + } + + return false; } /** - @return {boolean} true iff `modelName` and `relationshipName` refer to the - right hand side of this relationship, as opposed to the left hand side. + @return {boolean} true iff `modelName` and `relationshipName` refer to the + right hand side of this relationship, as opposed to the left hand side. - @method - */ + @method + */ _isRHS(modelName, relationshipName) { - return modelName === this._rhsModelName && relationshipName === this._rhsRelationshipName; + let relInfo = this._relInfo; + let isSelfReferential = relInfo.isSelfReferential; + let isRelationship = relationshipName === relInfo.rhs_relationshipName; + + if (isRelationship === true) { + return isSelfReferential === true || // itself + modelName === relInfo.rhs_baseModelName || // base or non-polymorphic + relInfo.rhs_modelNames.indexOf(modelName) !== -1; // polymorphic + } + + return false; } _flushPending() { @@ -162,26 +194,27 @@ export default class RelationshipPayloads { id: id, type: modelName } - } + }; // start flushing this individual payload. The logic is the same whether // it's for the left hand side of the relationship or the right hand side, // except the role of primary and inverse idToPayloads is reversed // let previousPayload; - let idToPayloads; - let inverseIdToPayloads; + let payloadMap; + let inversePayloadMap; let inverseIsMany; + if (this._isLHS(modelName, relationshipName)) { - previousPayload = this._lhsPayloads[id]; - idToPayloads = this._lhsPayloads; - inverseIdToPayloads = this._rhsPayloads; + previousPayload = this.lhs_payloads.get(modelName, id); + payloadMap = this.lhs_payloads; + inversePayloadMap = this.rhs_payloads; inverseIsMany = this._rhsRelationshipIsMany; } else { - assert(`${modelName}:${relationshipName} is not either side of this relationship, ${this._lhsModelName}:${this._lhsRelationshipName}<->${this._rhsModelName}:${this._rhsRelationshipName}`, this._isRHS(modelName, relationshipName)); - previousPayload = this._rhsPayloads[id]; - idToPayloads = this._rhsPayloads; - inverseIdToPayloads = this._lhsPayloads; + assert(`${modelName}:${relationshipName} is not either side of this relationship, ${this._relInfo.lhs_key}<->${this._relInfo.rhs_key}`, this._isRHS(modelName, relationshipName)); + previousPayload = this.rhs_payloads.get(modelName, id); + payloadMap = this.rhs_payloads; + inversePayloadMap = this.lhs_payloads; inverseIsMany = this._lhsRelationshipIsMany; } @@ -225,24 +258,24 @@ export default class RelationshipPayloads { // * undefined is NOT considered new information, we should keep original state // * anything else is considered new information, and it should win if (relationshipData.data !== undefined) { - this._removeInverse(id, previousPayload, inverseIdToPayloads); + this._removeInverse(id, previousPayload, inversePayloadMap); } - idToPayloads[id] = relationshipData; - this._populateInverse(relationshipData, inverseRelationshipData, inverseIdToPayloads, inverseIsMany); + payloadMap.set(modelName, id, relationshipData); + this._populateInverse(relationshipData, inverseRelationshipData, inversePayloadMap, inverseIsMany); } } /** - Populate the inverse relationship for `relationshipData`. + Populate the inverse relationship for `relationshipData`. - If `relationshipData` is an array (eg because the relationship is hasMany) - this means populate each inverse, otherwise populate only the single - inverse. + If `relationshipData` is an array (eg because the relationship is hasMany) + this means populate each inverse, otherwise populate only the single + inverse. - @private - @method - */ - _populateInverse(relationshipData, inversePayload, inverseIdToPayloads, inverseIsMany) { + @private + @method + */ + _populateInverse(relationshipData, inversePayload, inversePayloadMap, inverseIsMany) { if (!relationshipData.data) { // This id doesn't have an inverse, eg a belongsTo with a payload // { data: null }, so there's nothing to populate @@ -251,33 +284,35 @@ export default class RelationshipPayloads { if (Array.isArray(relationshipData.data)) { for (let i=0; i.friends = [{ id: 1, type: 'user' }] return; } - let existingPayload = inverseIdToPayloads[inverseId]; + let existingPayload = inversePayloadMap.get(resourceIdentifier.type, resourceIdentifier.id); let existingData = existingPayload && existingPayload.data; if (existingData) { @@ -287,38 +322,40 @@ export default class RelationshipPayloads { if (Array.isArray(existingData)) { existingData.push(inversePayload.data); } else { - inverseIdToPayloads[inverseId] = inversePayload; + inversePayloadMap.set(resourceIdentifier.type, resourceIdentifier.id, inversePayload); } } else { // first time we're populating the inverse side // if (inverseIsMany) { - inverseIdToPayloads[inverseId] = { + inversePayloadMap.set(resourceIdentifier.type, resourceIdentifier.id, { data: [inversePayload.data] - } + }); } else { - inverseIdToPayloads[inverseId] = inversePayload; + inversePayloadMap.set(resourceIdentifier.type, resourceIdentifier.id, inversePayload); } } } get _lhsRelationshipIsMany() { - return this._lhsRelationshipMeta && this._lhsRelationshipMeta.kind === 'hasMany'; + let meta = this._relInfo.lhs_relationshipMeta; + return meta !== null && meta.kind === 'hasMany'; } get _rhsRelationshipIsMany() { - return this._rhsRelationshipMeta && this._rhsRelationshipMeta.kind === 'hasMany'; + let meta = this._relInfo.rhs_relationshipMeta; + return meta !== null && meta.kind === 'hasMany'; } /** - Remove the relationship in `previousPayload` from its inverse(s), because - this relationship payload has just been updated (eg because the same - relationship had multiple payloads pushed before the relationship was - initialized). - - @method - */ - _removeInverse(id, previousPayload, inverseIdToPayloads) { + Remove the relationship in `previousPayload` from its inverse(s), because + this relationship payload has just been updated (eg because the same + relationship had multiple payloads pushed before the relationship was + initialized). + + @method + */ + _removeInverse(id, previousPayload, inversePayloadMap) { let data = previousPayload && previousPayload.data; if (!data) { // either this is the first time we've seen a payload for this id, or its @@ -333,22 +370,23 @@ export default class RelationshipPayloads { if (Array.isArray(data)) { // TODO: diff rather than removeall addall? for (let i=0; i x.id !== id); } else { - inversePayloads[inverseId] = { + inversePayloads.set(resourceIdentifier.type, resourceIdentifier.id, { data: null - }; + }); } } } diff --git a/tests/integration/relationships/belongs-to-test.js b/tests/integration/relationships/belongs-to-test.js index f10d71fa0b1..236dd9f3ff2 100644 --- a/tests/integration/relationships/belongs-to-test.js +++ b/tests/integration/relationships/belongs-to-test.js @@ -32,12 +32,12 @@ module("integration/relationship/belongs_to Belongs-To Relationships", { Post = Message.extend({ title: attr('string'), - comments: hasMany('comment', { async: false }) + comments: hasMany('comment', { async: false, inverse: null }) }); Comment = Message.extend({ body: DS.attr('string'), - message: DS.belongsTo('message', { polymorphic: true, async: false }) + message: DS.belongsTo('message', { polymorphic: true, async: false, inverse: null }) }); Book = DS.Model.extend({ diff --git a/tests/integration/relationships/has-many-test.js b/tests/integration/relationships/has-many-test.js index 1f46d79f96b..53b568a6f73 100644 --- a/tests/integration/relationships/has-many-test.js +++ b/tests/integration/relationships/has-many-test.js @@ -3159,7 +3159,10 @@ test("PromiseArray proxies createRecord to its ManyArray before the hasMany is l test("deleteRecord + unloadRecord fun", function(assert) { User.reopen({ - posts: DS.hasMany('posts', { inverse: null }) + posts: DS.hasMany('post', { inverse: null }) + }); + Post.reopen({ + user: DS.belongsTo('user', { inverse: null, async: false }) }); run(() => { diff --git a/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js b/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js new file mode 100644 index 00000000000..b1398af49b0 --- /dev/null +++ b/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js @@ -0,0 +1,825 @@ +import { run } from '@ember/runloop'; +import { copy } from '@ember/object/internals'; +import { RelationshipPayloadsManager } from 'ember-data/-private'; +import DS from 'ember-data'; +import { createStore } from 'dummy/tests/helpers/store'; +import { module, test } from 'qunit'; +import testInDebug from '../../../helpers/test-in-debug'; + +const { Model, hasMany, belongsTo, attr } = DS; + +module('unit/system/relationships/relationship-payloads-manager (polymorphic)', { + beforeEach() { + const User = DS.Model.extend({ + hats: hasMany('hat', { async: false, polymorphic: true, inverse: 'user' }), + sharedHats: hasMany('hat', { async: false, polymorphic: true, inverse: 'sharingUsers' }) + }); + User.toString = () => 'User'; + + const Alien = User.extend({}); + Alien.toString = () => 'Alien'; + + const Hat = Model.extend({ + type: attr('string'), + user: belongsTo('user', { async: false, inverse: 'hats', polymorphic: true }), + sharingUsers: belongsTo('users', { async: false, inverse: 'sharedHats', polymorphic: true }), + hat: belongsTo('hat', { async: false, inverse: 'hats', polymorphic: true }), + hats: hasMany('hat', { async: false, inverse: 'hat', polymorphic: true }) + }); + const BigHat = Hat.extend({}); + const SmallHat = Hat.extend({}); + + let store = this.store = createStore({ + user: User, + alien: Alien, + hat: Hat, + 'big-hat': BigHat, + 'small-hat': SmallHat + }); + + this.relationshipPayloadsManager = new RelationshipPayloadsManager(store); + } +}); + +test('push one side is polymorphic, baseType then subTypes', function(assert) { + let id = 1; + + function makeHat(type, props) { + const resource = copy(props, true); + resource.id = `${id++}`; + resource.type = type; + resource.attributes.type = type; + return resource; + } + + const hatData = { + attributes: {}, + relationships: { + user: { + data: { id: '1' , type: 'user' } + } + } + }; + + const hatData1 = makeHat('hat', hatData), + bigHatData1 = makeHat('big-hat', hatData), + smallHatData1 = makeHat('small-hat', hatData); + + const userData = { + data: { + id: '1', + type: 'user', + attributes: {} + }, + included: [ + hatData1, + bigHatData1, + smallHatData1 + ] + }; + + const user = run(() => this.store.push(userData)); + + const finalResult = user.get('hats').mapBy('type'); + + assert.deepEqual(finalResult, ['hat', 'big-hat', 'small-hat'], 'We got all our hats!'); +}); + +test('push one side is polymorphic, subType then baseType', function(assert) { + let id = 1; + + function makeHat(type, props) { + const resource = copy(props, true); + resource.id = `${id++}`; + resource.type = type; + resource.attributes.type = type; + return resource; + } + + const hatData = { + attributes: {}, + relationships: { + user: { + data: { id: '1' , type: 'user' } + } + } + }; + + const bigHatData1 = makeHat('hat', hatData), + smallHatData1 = makeHat('small-hat', hatData), + hatData1 = makeHat('big-hat', hatData), + included = [bigHatData1, smallHatData1, hatData1]; + + const userData = { + data: { + id: '1', + type: 'user', + attributes: {} + }, + included + }; + + const user = run(() => this.store.push(userData)), + finalResult = user.get('hats').mapBy('type'), + expectedResults = included.map(m=>m.type); + + assert.deepEqual(finalResult, expectedResults, 'We got all our hats!'); +}); + +test('push one side is polymorphic, different subtypes', function(assert) { + let id = 1; + + function makeHat(type, props) { + const resource = copy(props, true); + resource.id = `${id++}`; + resource.type = type; + resource.attributes.type = type; + return resource; + } + + const hatData = { + attributes:{}, + relationships: { + user: { + data: { id: '1' , type: 'user' } + } + } + }; + + const bigHatData1 = makeHat('big-hat', hatData), + smallHatData1 = makeHat('small-hat', hatData), + bigHatData2 = makeHat('big-hat', hatData), + smallHatData2 = makeHat('small-hat', hatData), + included = [ + bigHatData1, + smallHatData1, + bigHatData2, + smallHatData2 + ]; + + const userData = { + data: { + id: '1', + type: 'user', + attributes: {} + }, + included + }; + + const user = run(() => this.store.push(userData)), + finalResult = user.get('hats').mapBy('type'), + expectedResults = included.map(m => m.type); + + assert.deepEqual(finalResult, expectedResults, 'We got all our hats!'); +}); + +test('push both sides are polymorphic', function(assert) { + let id = 1; + + function makeHat(type, props) { + const resource = copy(props, true); + resource.id = `${id++}`; + resource.type = type; + resource.attributes.type = type; + return resource; + } + + const alienHatData = { + attributes: {}, + relationships: { + user: { + data: { id: '1' , type: 'alien' } + } + } + }; + + const bigHatData1 = makeHat('hat', alienHatData), + hatData1 = makeHat('big-hat', alienHatData), + alienIncluded = [bigHatData1, hatData1]; + + const alienData = { + data: { + id: '1', + type: 'alien', + attributes: {} + }, + included: alienIncluded + }; + + const expectedAlienResults = alienIncluded.map(m => m.type), + alien = run(() => this.store.push(alienData)), + alienFinalHats = alien.get('hats').mapBy('type'); + + assert.deepEqual(alienFinalHats, expectedAlienResults, 'We got all alien hats!'); +}); + +test('handles relationships where both sides are polymorphic', function(assert) { + let id = 1; + function makePolymorphicHatForPolymorphicPerson(type, isForBigPerson = true) { + return { + id: `${id++}`, + type, + relationships: { + person: { + data: { + id: isForBigPerson ? '1' : '2', + type: isForBigPerson ? 'big-person' : 'small-person' + } + } + } + }; + } + + const bigHatData1 = makePolymorphicHatForPolymorphicPerson('big-hat'); + const bigHatData2 = makePolymorphicHatForPolymorphicPerson('big-hat'); + const bigHatData3 = makePolymorphicHatForPolymorphicPerson('big-hat', false); + const smallHatData1 = makePolymorphicHatForPolymorphicPerson('small-hat'); + const smallHatData2 = makePolymorphicHatForPolymorphicPerson('small-hat'); + const smallHatData3 = makePolymorphicHatForPolymorphicPerson('small-hat', false); + + const bigPersonData = { + data: { + id: '1', + type: 'big-person', + attributes: {} + }, + included: [ + bigHatData1, + smallHatData1, + bigHatData2, + smallHatData2 + ] + }; + + const smallPersonData = { + data: { + id: '2', + type: 'small-person', + attributes: {} + }, + included: [ + bigHatData3, + smallHatData3 + ] + }; + + const PersonModel = Model.extend({ + hats: hasMany('hat', { + async: false, + polymorphic: true, + inverse: 'person' + }) + }); + const HatModel = Model.extend({ + type: attr('string'), + person: belongsTo('person', { + async: false, + inverse: 'hats', + polymorphic: true + }) + }); + const BigHatModel = HatModel.extend({}); + const SmallHatModel = HatModel.extend({}); + + const BigPersonModel = PersonModel.extend({}); + const SmallPersonModel = PersonModel.extend({}); + + const store = this.store = createStore({ + person: PersonModel, + bigPerson: BigPersonModel, + smallPerson: SmallPersonModel, + hat: HatModel, + bigHat: BigHatModel, + smallHat: SmallHatModel + }); + + const bigPerson = run(() => { + return store.push(bigPersonData); + }); + + const smallPerson = run(() => { + return store.push(smallPersonData); + }); + + const finalBigResult = bigPerson.get('hats').toArray(); + const finalSmallResult = smallPerson.get('hats').toArray(); + + assert.equal(finalBigResult.length, 4, 'We got all our hats!'); + assert.equal(finalSmallResult.length, 2, 'We got all our hats!'); +}); + +test('handles relationships where both sides are polymorphic reflexive', function(assert) { + function link(a, b, relationshipName, recurse = true) { + a.relationships = a.relationships || {}; + const rel = a.relationships[relationshipName] = a.relationships[relationshipName] || {}; + + if (Array.isArray(b)) { + rel.data = b.map((i) => { + let {type, id} = i; + + if (recurse === true) { + link(i, [a], relationshipName, false); + } + + return { type, id }; + }); + } else { + rel.data = { + type: b.type, + id: b.id + }; + + if (recurse === true) { + link(b, a, relationshipName, false); + } + } + } + + let id = 1; + const Person = Model.extend({ + name: attr(), + family: hasMany('person', { async: false, polymorphic: true, inverse: 'family' }), + twin: belongsTo('person', { async: false, polymorphic: true, inverse: 'twin' }) + }); + const Girl = Person.extend({}); + const Boy = Person.extend({}); + const Grownup = Person.extend({}); + + const brotherPayload = { + type: 'boy', + id: `${id++}`, + attributes: { + name: 'Gavin' + } + }; + const sisterPayload = { + type: 'girl', + id: `${id++}`, + attributes: { + name: 'Rose' + } + }; + const fatherPayload = { + type: 'grownup', + id: `${id++}`, + attributes: { + name: 'Garak' + } + }; + const motherPayload = { + type: 'grownup', + id: `${id++}`, + attributes: { + name: 'Kira' + } + }; + + link(brotherPayload, sisterPayload, 'twin'); + link(brotherPayload, [sisterPayload, fatherPayload, motherPayload], 'family'); + + const payload = { + data: brotherPayload, + included: [ + sisterPayload, + fatherPayload, + motherPayload + ] + }; + const expectedFamilyReferences = [ + { type: 'girl', id: sisterPayload.id }, + { type: 'grownup', id: fatherPayload.id }, + { type: 'grownup', id: motherPayload.id } + ]; + const expectedTwinReference = { type: 'girl', id: sisterPayload.id }; + + const store = this.store = createStore({ + person: Person, + grownup: Grownup, + boy: Boy, + girl: Girl + }); + + const boyInstance = run(() => { + return store.push(payload); + }); + + const familyResultReferences = boyInstance.get('family').toArray() + .map((i) => { + return { type: i.constructor.modelName, id: i.id }; + }); + const twinResult = boyInstance.get('twin'); + const twinResultReference = { type: twinResult.constructor.modelName, id: twinResult.id }; + + assert.deepEqual(familyResultReferences, expectedFamilyReferences, 'We linked family correctly'); + assert.deepEqual(twinResultReference, expectedTwinReference, 'We linked twin correctly'); +}); + +test('handles relationships where both sides are polymorphic reflexive but the primary payload does not include linkage', function(assert) { + function link(a, b, relationshipName, recurse = true) { + a.relationships = a.relationships || {}; + const rel = a.relationships[relationshipName] = a.relationships[relationshipName] || {}; + + if (Array.isArray(b)) { + rel.data = b.map((i) => { + let {type, id} = i; + + if (recurse === true) { + link(i, [a], relationshipName, false); + } + + return { type, id }; + }); + } else { + rel.data = { + type: b.type, + id: b.id + }; + + if (recurse === true) { + link(b, a, relationshipName, false); + } + } + } + + let id = 1; + const Person = Model.extend({ + name: attr(), + family: hasMany('person', { async: false, polymorphic: true, inverse: 'family' }), + twin: belongsTo('person', { async: false, polymorphic: true, inverse: 'twin' }) + }); + const Girl = Person.extend({}); + const Boy = Person.extend({}); + const Grownup = Person.extend({}); + + const brotherPayload = { + type: 'boy', + id: `${id++}`, + attributes: { + name: 'Gavin' + } + }; + const sisterPayload = { + type: 'girl', + id: `${id++}`, + attributes: { + name: 'Rose' + } + }; + const fatherPayload = { + type: 'grownup', + id: `${id++}`, + attributes: { + name: 'Garak' + } + }; + const motherPayload = { + type: 'grownup', + id: `${id++}`, + attributes: { + name: 'Kira' + } + }; + + link(brotherPayload, sisterPayload, 'twin'); + link(brotherPayload, [sisterPayload, fatherPayload, motherPayload], 'family'); + + // unlink all relationships from the primary payload + delete brotherPayload.relationships; + + const payload = { + data: brotherPayload, + included: [ + sisterPayload, + fatherPayload, + motherPayload + ] + }; + const expectedFamilyReferences = [ + { type: 'girl', id: sisterPayload.id }, + { type: 'grownup', id: fatherPayload.id }, + { type: 'grownup', id: motherPayload.id } + ]; + const expectedTwinReference = { type: 'girl', id: sisterPayload.id }; + + const store = this.store = createStore({ + person: Person, + grownup: Grownup, + boy: Boy, + girl: Girl + }); + + const boyInstance = run(() => { + return store.push(payload); + }); + + const familyResultReferences = boyInstance.get('family').toArray() + .map((i) => { + return { type: i.constructor.modelName, id: i.id }; + }); + const twinResult = boyInstance.get('twin'); + const twinResultReference = twinResult && { type: twinResult.constructor.modelName, id: twinResult.id }; + + assert.deepEqual(familyResultReferences, expectedFamilyReferences, 'We linked family correctly'); + assert.deepEqual(twinResultReference, expectedTwinReference, 'We linked twin correctly'); +}); + +test('push polymorphic self-referential non-reflexive relationship', function(assert) { + const store = this.store; + const hat1Data = { + data: { + id: '1', + type: 'big-hat', + attributes: {} + } + }; + const hat2Data = { + data: { + id: '2', + type: 'big-hat', + attributes: {}, + relationships: { + hats: { + data: [{ id: '1', type: 'big-hat' }] + } + } + } + }; + + const hat1 = run(() => store.push(hat1Data)); + const hat2 = run(() => store.push(hat2Data)); + + const expectedHatReference = { id: '2', type: 'big-hat' }; + const expectedHatsReferences = [{ id: '1', type: 'big-hat' }]; + + const finalHatsReferences = hat2.get('hats').toArray() + .map((i) => { + return { type: i.constructor.modelName, id: i.id }; + }); + const hatResult = hat1.get('hat'); + const finalHatReference = hatResult && { type: hatResult.constructor.modelName, id: hatResult.id }; + + + assert.deepEqual(finalHatReference, expectedHatReference, 'we set hat on hat:1'); + assert.deepEqual(finalHatsReferences, expectedHatsReferences, 'We have hats on hat:2'); +}); + + +test('push polymorphic self-referential circular non-reflexive relationship', function(assert) { + const store = this.store; + const hatData = { + data: { + id: '1', + type: 'big-hat', + attributes: {}, + relationships: { + hat: { + data: { id: '1', type: 'big-hat' } + }, + hats: { + data: [{ id: '1', type: 'big-hat' }] + } + } + } + }; + + const hat = run(() => store.push(hatData)); + + const expectedHatReference = { id: '1', type: 'big-hat' }; + const expectedHatsReferences = [{ id: '1', type: 'big-hat' }]; + + const finalHatsReferences = hat.get('hats').toArray() + .map((i) => { + return { type: i.constructor.modelName, id: i.id }; + }); + const hatResult = hat.get('hat'); + const finalHatReference = hatResult && { type: hatResult.constructor.modelName, id: hatResult.id }; + + + assert.deepEqual(finalHatReference, expectedHatReference, 'we set hat on hat:1'); + assert.deepEqual(finalHatsReferences, expectedHatsReferences, 'We have hats on hat:2'); +}); + +test('polymorphic hasMany to types with separate id-spaces', function(assert) { + const user = run(() => this.store.push({ + data: { + id: '1', + type: 'user', + relationships: { + hats: { + data: [ + { id: '1', type: 'big-hat' }, + { id: '1', type: 'small-hat' } + ] + } + } + }, + included: [{ + id: '1', + type: 'big-hat' + }, { + id: '1', + type: 'small-hat' + }] + })); + + const hats = user.get('hats'); + + assert.deepEqual( + hats.map(h => h.constructor.modelName), + ['big-hat', 'small-hat'] + ); + assert.deepEqual( + hats.map(h => h.id), + ['1', '1'] + ); +}); + +test('polymorphic hasMany to types with separate id-spaces, from inverse payload', function (assert) { + const user = run(() => this.store.push({ + data: { + id: '1', + type: 'user' + }, + included: [{ + id: '1', + type: 'big-hat', + relationships: { + user: { + data: { id: '1', type: 'user' } + } + } + }, { + id: '1', + type: 'small-hat', + relationships: { + user: { + data: { id: '1', type: 'user' } + } + } + }] + })); + + const hats = user.get('hats'); + + assert.deepEqual( + hats.map(h => h.constructor.modelName), + ['big-hat', 'small-hat'] + ); + assert.deepEqual( + hats.map(h => h.id), + ['1', '1'] + ); +}); + +test('polymorphic hasMany to polymorphic hasMany types with separate id-spaces', function (assert) { + let bigHatId = 1; + let smallHatId = 1; + function makePolymorphicHatForPolymorphicPerson(type, isForBigPerson = true) { + const isSmallHat = type === 'small-hat'; + return { + id: `${isSmallHat ? smallHatId++ : bigHatId++}`, + type, + relationships: { + person: { + data: { + id: '1', + type: isForBigPerson ? 'big-person' : 'small-person' + } + } + } + }; + } + + const bigHatData1 = makePolymorphicHatForPolymorphicPerson('big-hat'); + const bigHatData2 = makePolymorphicHatForPolymorphicPerson('big-hat'); + const bigHatData3 = makePolymorphicHatForPolymorphicPerson('big-hat', false); + const smallHatData1 = makePolymorphicHatForPolymorphicPerson('small-hat'); + const smallHatData2 = makePolymorphicHatForPolymorphicPerson('small-hat'); + const smallHatData3 = makePolymorphicHatForPolymorphicPerson('small-hat', false); + + const bigPersonData = { + data: { + id: '1', + type: 'big-person', + attributes: {} + }, + included: [ + bigHatData1, + smallHatData1, + bigHatData2, + smallHatData2 + ] + }; + + const smallPersonData = { + data: { + id: '1', + type: 'small-person', + attributes: {} + }, + included: [ + bigHatData3, + smallHatData3 + ] + }; + + const PersonModel = Model.extend({ + hats: hasMany('hat', { + async: false, + polymorphic: true, + inverse: 'person' + }) + }); + const HatModel = Model.extend({ + type: attr('string'), + person: belongsTo('person', { + async: false, + inverse: 'hats', + polymorphic: true + }) + }); + const BigHatModel = HatModel.extend({}); + const SmallHatModel = HatModel.extend({}); + + const BigPersonModel = PersonModel.extend({}); + const SmallPersonModel = PersonModel.extend({}); + + const store = this.store = createStore({ + person: PersonModel, + bigPerson: BigPersonModel, + smallPerson: SmallPersonModel, + hat: HatModel, + bigHat: BigHatModel, + smallHat: SmallHatModel + }); + + const bigPerson = run(() => { + return store.push(bigPersonData); + }); + + const smallPerson = run(() => { + return store.push(smallPersonData); + }); + + const finalBigResult = bigPerson.get('hats').toArray(); + const finalSmallResult = smallPerson.get('hats').toArray(); + + assert.deepEqual( + finalBigResult.map(h => ({ type: h.constructor.modelName, id: h.get('id') })), + [{ type: 'big-hat', id: '1'}, { type: 'small-hat', id: '1'}, { type: 'big-hat', id: '2'}, { type: 'small-hat', id: '2'}], + 'big-person hats is all good' + ); + + assert.deepEqual( + finalSmallResult.map(h => ({ type: h.constructor.modelName, id: h.get('id') })), + [{ type: 'big-hat', id: '3'}, { type: 'small-hat', id: '3'}], + 'small-person hats is all good' + ); +}); + +testInDebug('Invalid inverses throw errors', function(assert) { + let PostModel = Model.extend({ + comments: hasMany('comment', { async: false }) + }); + let CommentModel = Model.extend({ + post: belongsTo('post', { async: false, inverse: null }) + }); + let store = createStore({ + post: PostModel, + comment: CommentModel + }); + + function runInvalidPush() { + return run(() => { + return store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + data: [ + { type: 'comment', id: '1' } + ] + } + } + }, + included: [ + { + type: 'comment', + id: '1', + relationships: { + post: { + data: { + type: 'post', + id: '1' + } + } + } + } + ] + }); + }); + } + + assert.throws(runInvalidPush, /The comment:post relationship declares 'inverse: null', but it was resolved as the inverse for post:comments/, 'We detected the invalid inverse'); +}); diff --git a/tests/unit/system/relationships/relationship-payload-manager-test.js b/tests/unit/system/relationships/relationship-payload-manager-test.js index 9ade62feb01..af7159f0291 100644 --- a/tests/unit/system/relationships/relationship-payload-manager-test.js +++ b/tests/unit/system/relationships/relationship-payload-manager-test.js @@ -4,28 +4,30 @@ import DS from 'ember-data'; import { createStore } from 'dummy/tests/helpers/store'; import { module, test } from 'qunit'; +const { Model, hasMany, belongsTo } = DS; + module('unit/system/relationships/relationship-payloads-manager', { beforeEach() { - const User = DS.Model.extend({ - purpose: DS.belongsTo('purpose', { inverse: 'user' }), - hobbies: DS.hasMany('hobby', { inverse: 'user'}), - friends: DS.hasMany('user', { inverse: 'friends' }) + const User = Model.extend({ + purpose: belongsTo('purpose', { inverse: 'user' }), + hobbies: hasMany('hobby', { inverse: 'user'}), + friends: hasMany('user', { inverse: 'friends' }) }); User.toString = () => 'User'; - const Hobby = DS.Model.extend({ + const Hobby = Model.extend({ user: DS.belongsTo('user', { inverse: 'hobbies' }) }); Hobby.toString = () => 'Hobby'; - const Purpose = DS.Model.extend({ + const Purpose = Model.extend({ user: DS.belongsTo('user', { inverse: 'purpose' }) }); Purpose.toString = () => 'Purpose'; let store = this.store = createStore({ user: User, - Hobby: Hobby, + hobby: Hobby, purpose: Purpose }); @@ -33,13 +35,12 @@ module('unit/system/relationships/relationship-payloads-manager', { } }); - test('get throws for invalid models', function(assert) { this.relationshipPayloadsManager._store._modelFor = (name) => { if (name === 'fish') { throw new Error('What is fish?'); } - } + }; assert.throws(() => { this.relationshipPayloadsManager.get('fish', 9, 'hobbies'); @@ -471,7 +472,7 @@ test('push populates the same RelationshipPayloads for either side of a relation 'hobbies', userModel, get(userModel, 'relationshipsByName') - ); + ); let hobbyModel = this.store.modelFor('hobby'); let hobbyPayloads = @@ -486,14 +487,15 @@ test('push populates the same RelationshipPayloads for either side of a relation }); test('push does not eagerly populate inverse payloads', function(assert) { - this.relationshipPayloadsManager.push('user', 1, { + const relData = { hobbies: { data: [{ id: 2, type: 'hobby' }] } - }); + }; + this.relationshipPayloadsManager.push('user', 1, relData); let userModel = this.store.modelFor('user'); let relationshipPayloads = @@ -502,31 +504,41 @@ test('push does not eagerly populate inverse payloads', function(assert) { 'hobbies', userModel, get(userModel, 'relationshipsByName') - ); + ); assert.deepEqual( - Object.keys(relationshipPayloads._lhsPayloads), - [] , + Object.keys(relationshipPayloads.lhs_payloads.types), + [], 'user.hobbies payloads not eagerly populated' ); assert.deepEqual( - Object.keys(relationshipPayloads._rhsPayloads), - [] , + Object.keys(relationshipPayloads.rhs_payloads.types), + [], 'hobby.user payloads not eagerly populated' - ); + ); relationshipPayloads.get('user', 1, 'hobbies'); assert.deepEqual( - Object.keys(relationshipPayloads._lhsPayloads), - ['1'] , + Object.keys(relationshipPayloads.lhs_payloads.types), + ['user'], + 'user.hobbies payloads lazily populated' + ); + assert.deepEqual( + Object.keys(relationshipPayloads.lhs_payloads.types.user), + ['1'], 'user.hobbies payloads lazily populated' ); assert.deepEqual( - Object.keys(relationshipPayloads._rhsPayloads), + Object.keys(relationshipPayloads.rhs_payloads.types), + ['hobby'] , + 'hobby.user payloads lazily populated' + ); + assert.deepEqual( + Object.keys(relationshipPayloads.rhs_payloads.types.hobby), ['2'] , 'hobby.user payloads lazily populated' - ); + ); }); test('push populates each individual relationship in a payload', function(assert) {