Skip to content

Commit

Permalink
[BUGFIX] enable lazy-relationship payloads to work with polymorphic r…
Browse files Browse the repository at this point in the history
…elationships (#5230)

* [BUGFIX] enable lazy-relationship payloads to work with polymorphic relationships

(cherry picked from commit b93b3e7)
  • Loading branch information
runspired authored and bmac committed Mar 1, 2018
1 parent 81c96b7 commit 7986dbc
Show file tree
Hide file tree
Showing 8 changed files with 1,191 additions and 215 deletions.
4 changes: 4 additions & 0 deletions addon/-private/system/model/model.js
Expand Up @@ -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;
}
}
Expand Down
11 changes: 2 additions & 9 deletions 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;
Expand All @@ -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;
}
197 changes: 149 additions & 48 deletions 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
Expand Down Expand Up @@ -59,6 +61,7 @@ export default class RelationshipPayloadsManager {
this._store = store;
// cache of `RelationshipPayload`s
this._cache = Object.create(null);
this._inverseLookupCache = new TypeCache();
}

/**
Expand All @@ -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);
}

Expand Down Expand Up @@ -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]);
}
Expand All @@ -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);
}
Expand All @@ -156,23 +155,139 @@ 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`
@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;
}

/**
Expand All @@ -181,46 +296,32 @@ 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`.
//
// 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;
}
}

0 comments on commit 7986dbc

Please sign in to comment.