Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bugfix beta] Add inverse relationship on payload when missing #5608

Merged
214 changes: 212 additions & 2 deletions addon/-private/system/store/finders.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { A } from '@ember/array';
import { Promise } from 'rsvp';
import { assert, warn } from '@ember/debug';
import { assert, warn, deprecate } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import Ember from 'ember';

import { _bind, _guard, _objectIsAlive, guardDestroyedStore } from './common';

import { normalizeResponseHelper } from './serializer-response';
import { serializerForAdapter } from './serializers';
import { merge } from '@ember/polyfills';

function payloadIsNotBlank(adapterPayload) {
if (Array.isArray(adapterPayload)) {
Expand Down Expand Up @@ -105,6 +108,209 @@ export function _findMany(adapter, store, modelName, ids, internalModels, option
);
}

function iterateData(data, fn) {
if (Array.isArray(data)) {
return data.map(fn);
} else {
return fn(data);
}
}

// sync
// iterate over records in payload.data
// for each record
// assert that record.relationships[inverse] is either undefined (so we can fix it)
// or provide a data: {id, type} that matches the record that requested it
// return the relationship data for the parent
function syncRelationshipDataFromLink(store, payload, parentInternalModel, relationship) {
// ensure the right hand side (incoming payload) points to the parent record that
// requested this relationship
let relationshipData = iterateData(payload.data, (data, index) => {
const { id, type } = data;
ensureRelationshipIsSetToParent(data, parentInternalModel, store, relationship, index);
return { id, type };
});

// now, push the left hand side (the parent record) to ensure things are in sync, since
// the payload will be pushed with store._push
store.push({
data: {
id: parentInternalModel.id,
type: parentInternalModel.modelName,
relationships: {
[relationship.key]: {
data: relationshipData,
},
},
},
});
}

function ensureRelationshipIsSetToParent(
payload,
parentInternalModel,
store,
parentRelationship,
index
) {
let { id, type } = payload;

if (!payload.relationships) {
payload.relationships = {};
}
let { relationships } = payload;

let inverse = getInverse(store, parentInternalModel, parentRelationship, type);
if (inverse) {
let { inverseKey, kind } = inverse;

let relationshipData = relationships[inverseKey] && relationships[inverseKey].data;

if (
DEBUG &&
typeof relationshipData !== 'undefined' &&
!relationshipDataPointsToParent(relationshipData, parentInternalModel)
) {
let quotedType = Ember.inspect(type);
let quotedInverse = Ember.inspect(inverseKey);
let expected = Ember.inspect({
id: parentInternalModel.id,
type: parentInternalModel.modelName,
});
let expectedModel = Ember.inspect(parentInternalModel);
let got = Ember.inspect(relationshipData);
let prefix = typeof index === 'number' ? `data[${index}]` : `data`;
let path = `${prefix}.relationships.${inverse}.data`;
let other = relationshipData ? `<${relationshipData.type}:${relationshipData.id}>` : null;
let relationshipFetched = `${Ember.inspect(parentInternalModel)}.${
parentRelationship.kind
}("${parentRelationship.name}")`;
let includedRecord = `<${type}:${id}>`;
let message = [
`Encountered mismatched relationship: Ember Data expected ${path} in the payload from ${relationshipFetched} to include ${expected} but got ${got} instead.\n`,
`The ${includedRecord} record loaded at ${prefix} in the payload specified ${other} as its ${quotedInverse}, but should have specified ${expectedModel} (the record the relationship is being loaded from) as its ${quotedInverse} instead.`,
`This could mean that the response for ${relationshipFetched} may have accidentally returned ${quotedType} records that aren't related to ${expectedModel} and could be related to a different ${
parentInternalModel.modelName
} record instead.`,
`Ember Data has corrected the ${includedRecord} record's ${quotedInverse} relationship to ${expectedModel} so that ${relationshipFetched} will include ${includedRecord}.`,
`Please update the response from the server or change your serializer to either ensure that the response for only includes ${quotedType} records that specify ${expectedModel} as their ${quotedInverse}, or omit the ${quotedInverse} relationship from the response.`,
].join('\n');

// this should eventually throw instead of deprecating.
deprecate(message + '\n', false, {
id: 'mismatched-inverse-relationship-data-from-payload',
until: '3.8',
});
}
relationships[inverseKey] = relationships[inverseKey] || {};
relationships[inverseKey].data = fixRelationshipData(
relationshipData,
kind,
parentInternalModel
);
}
}

function getInverse(store, parentInternalModel, parentRelationship, type) {
if (store.recordDataWrapper) {
return recordDataFindInverseRelationshipInfo(
store,
parentInternalModel,
parentRelationship,
type
);
} else {
return legacyFindInverseRelationshipInfo(store, parentInternalModel, parentRelationship);
}
}

function recordDataFindInverseRelationshipInfo(
{ recordDataWrapper },
parentInternalModel,
parentRelationship,
type
) {
let { name: lhs_relationshipName } = parentRelationship;
let { modelName } = parentInternalModel;
let inverseKey = recordDataWrapper.inverseForRelationship(modelName, lhs_relationshipName);

if (inverseKey) {
let {
meta: { kind },
} = recordDataWrapper.relationshipsDefinitionFor(type)[inverseKey];
return {
inverseKey,
kind,
};
}
}

function legacyFindInverseRelationshipInfo(store, parentInternalModel, parentRelationship) {
let { name: lhs_relationshipName } = parentRelationship;
let { modelName } = parentInternalModel;

let relationshipInfo = store._relationshipsPayloads.getRelationshipInfo(
modelName,
lhs_relationshipName
);
let { hasInverse, rhs_relationshipName: inverseKey, rhs_relationshipMeta } = relationshipInfo;

if (hasInverse) {
let {
meta: { kind },
} = rhs_relationshipMeta;
return {
inverseKey,
kind,
};
}
}

function relationshipDataPointsToParent(relationshipData, internalModel) {
if (relationshipData === null) {
return false;
}

if (Array.isArray(relationshipData)) {
if (relationshipData.length === 0) {
return false;
}
for (let i = 0; i < relationshipData.length; i++) {
let entry = relationshipData[i];
if (validateRelationshipEntry(entry, internalModel)) {
return true;
}
}
} else {
return validateRelationshipEntry(relationshipData, internalModel);
}

return false;
}

function fixRelationshipData(relationshipData, relationshipKind, { id, modelName }) {
let parentRelationshipData = {
id,
type: modelName,
};

let payload;

if (relationshipKind === 'hasMany') {
payload = relationshipData || [];
payload.push(parentRelationshipData);
} else {
payload = relationshipData || {};
merge(payload, parentRelationshipData);
}

return payload;
}

function validateRelationshipEntry({ id }, { id: parentModelID }) {
return id && id.toString() === parentModelID;
}

export function _findHasMany(adapter, store, internalModel, link, relationship, options) {
let snapshot = internalModel.createSnapshot(options);
let modelClass = store.modelFor(relationship.type);
Expand Down Expand Up @@ -133,8 +339,10 @@ export function _findHasMany(adapter, store, internalModel, link, relationship,
null,
'findHasMany'
);
let internalModelArray = store._push(payload);

syncRelationshipDataFromLink(store, payload, internalModel, relationship);

let internalModelArray = store._push(payload);
internalModelArray.meta = payload.meta;
return internalModelArray;
},
Expand Down Expand Up @@ -170,6 +378,8 @@ export function _findBelongsTo(adapter, store, internalModel, link, relationship
return null;
}

syncRelationshipDataFromLink(store, payload, internalModel, relationship);

return store._push(payload);
},
null,
Expand Down
Loading