Skip to content

Commit cb60dbd

Browse files
pavelglacfacebook-github-bot
authored andcommitted
fix(relay-runtime): prototype less payloads (#5043)
Summary: Relay throws an error when execution and consumption occur in the same thread without a serialization boundary (no JSON.parse()). This occurs because popular execution library have prototype less object (to allow keys like constructor etc.). Relay should not assume payload has any method available. graphql-js: https://github.com/graphql/graphql-js/blob/16.x.x/src/execution/execute.ts#L298 supermassive: https://github.com/microsoft/graphitation/blob/main/packages/supermassive/src/executeWithoutSchema.ts#L201 Pull Request resolved: #5043 Reviewed By: evanyeung Differential Revision: D79271386 Pulled By: captbaritone fbshipit-source-id: f68c8f789e959350afeaf01ab794053c6cba20f7
1 parent b536875 commit cb60dbd

File tree

5 files changed

+479
-2
lines changed

5 files changed

+479
-2
lines changed

packages/relay-runtime/store/RelayResponseNormalizer.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,11 @@ class RelayResponseNormalizer {
250250
this._traverseSelections(selection, record, data);
251251
}
252252
} else {
253-
const implementsInterface = data.hasOwnProperty(abstractKey);
253+
// $FlowFixMe[method-unbinding] - data could be prototype less
254+
const implementsInterface = Object.prototype.hasOwnProperty.call(
255+
data,
256+
abstractKey,
257+
);
254258
const typeName = RelayModernRecord.getType(record);
255259
const typeID = generateTypeID(typeName);
256260
let typeRecord = this._recordSource.get(typeID);
@@ -271,7 +275,11 @@ class RelayResponseNormalizer {
271275
}
272276
case 'TypeDiscriminator': {
273277
const {abstractKey} = selection;
274-
const implementsInterface = data.hasOwnProperty(abstractKey);
278+
// $FlowFixMe[method-unbinding] - data could be prototype less
279+
const implementsInterface = Object.prototype.hasOwnProperty.call(
280+
data,
281+
abstractKey,
282+
);
275283
const typeName = RelayModernRecord.getType(record);
276284
const typeID = generateTypeID(typeName);
277285
let typeRecord = this._recordSource.get(typeID);

packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4663,4 +4663,113 @@ describe('RelayResponseNormalizer', () => {
46634663
});
46644664
});
46654665
});
4666+
4667+
describe('Prototype-less objects (e.g., from graphql-js executor)', () => {
4668+
const createPrototypeLessObject = (data: {[string]: mixed}) => {
4669+
const obj = Object.create(null);
4670+
// $FlowFixMe[unsafe-object-assign] - assigning to prototype-less object
4671+
Object.assign(obj, data);
4672+
return obj;
4673+
};
4674+
4675+
it('normalizes prototype-less payloads with type discriminator', () => {
4676+
const query = graphql`
4677+
query RelayResponseNormalizerTest42Query($id: ID!) {
4678+
node(id: $id) {
4679+
...RelayResponseNormalizerTest42Fragment
4680+
}
4681+
}
4682+
`;
4683+
4684+
graphql`
4685+
fragment RelayResponseNormalizerTest42Fragment on Node {
4686+
id
4687+
... on User {
4688+
name
4689+
}
4690+
}
4691+
`;
4692+
4693+
const payload = {
4694+
node: createPrototypeLessObject({
4695+
__typename: 'Page',
4696+
id: '1',
4697+
}),
4698+
};
4699+
4700+
const recordSource = new RelayRecordSource();
4701+
recordSource.set(ROOT_ID, RelayModernRecord.create(ROOT_ID, ROOT_TYPE));
4702+
4703+
normalize(
4704+
recordSource,
4705+
createNormalizationSelector(query.operation, ROOT_ID, {id: '1'}),
4706+
payload,
4707+
defaultOptions,
4708+
);
4709+
4710+
expect(recordSource.toJSON()).toEqual({
4711+
'1': {
4712+
__id: '1',
4713+
__typename: 'Page',
4714+
id: '1',
4715+
},
4716+
'client:root': {
4717+
__id: 'client:root',
4718+
__typename: '__Root',
4719+
'node(id:"1")': {__ref: '1'},
4720+
},
4721+
'client:__type:Page': {
4722+
__id: 'client:__type:Page',
4723+
__typename: '__TypeSchema',
4724+
__isNode: false,
4725+
},
4726+
});
4727+
});
4728+
4729+
it('normalizes prototype-less payloads for union types', () => {
4730+
const query = graphql`
4731+
query RelayResponseNormalizerTest43Query($id: ID!) {
4732+
userOrPage(id: $id) {
4733+
... on User {
4734+
id
4735+
}
4736+
}
4737+
}
4738+
`;
4739+
4740+
const payload = {
4741+
userOrPage: createPrototypeLessObject({
4742+
__typename: 'Page',
4743+
id: '1',
4744+
}),
4745+
};
4746+
4747+
const recordSource = new RelayRecordSource();
4748+
recordSource.set(ROOT_ID, RelayModernRecord.create(ROOT_ID, ROOT_TYPE));
4749+
4750+
normalize(
4751+
recordSource,
4752+
createNormalizationSelector(query.operation, ROOT_ID, {id: '1'}),
4753+
payload,
4754+
defaultOptions,
4755+
);
4756+
4757+
expect(recordSource.toJSON()).toEqual({
4758+
'1': {
4759+
__id: '1',
4760+
__typename: 'Page',
4761+
},
4762+
'client:root': {
4763+
__id: 'client:root',
4764+
__typename: '__Root',
4765+
'userOrPage(id:"1")': {__ref: '1'},
4766+
},
4767+
'client:__type:Page': {
4768+
__id: 'client:__type:Page',
4769+
__typename: '__TypeSchema',
4770+
__isNode: false,
4771+
},
4772+
});
4773+
});
4774+
});
46664775
});

packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTest42Fragment.graphql.js

Lines changed: 74 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTest42Query.graphql.js

Lines changed: 147 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)