Skip to content

Commit afbe436

Browse files
authored
fix: avoid prototype-colliding names in execution values (#4653)
completes conversion of execution values to use of prototype-less objects begun in #4634 = adds tests for the new behavior = fixes out-of-date comments = small type-change
1 parent b4c1624 commit afbe436

File tree

2 files changed

+57
-10
lines changed

2 files changed

+57
-10
lines changed

src/execution/__tests__/variables-test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,15 @@ const TestType = new GraphQLObjectType({
164164
type: TestNestedInputObject,
165165
}),
166166
fieldWithJSONScalarInput: fieldWithInputArg({ type: TestJSONScalar }),
167+
fieldWithPrototypeNamedArgument: {
168+
type: GraphQLString,
169+
args: {
170+
toString: { type: GraphQLString },
171+
},
172+
resolve(_, args) {
173+
return args.toString === undefined ? 'missing' : inspect(args.toString);
174+
},
175+
},
167176
list: fieldWithInputArg({ type: new GraphQLList(GraphQLString) }),
168177
nested: {
169178
type: NestedType,
@@ -1225,6 +1234,20 @@ describe('Execute: Handles inputs', () => {
12251234
},
12261235
});
12271236
});
1237+
1238+
it('does not expose prototype argument names when omitted', () => {
1239+
const result = executeQuery(`
1240+
{
1241+
fieldWithPrototypeNamedArgument
1242+
}
1243+
`);
1244+
1245+
expect(result).to.deep.equal({
1246+
data: {
1247+
fieldWithPrototypeNamedArgument: 'missing',
1248+
},
1249+
});
1250+
});
12281251
});
12291252

12301253
describe('getVariableValues: limit maximum number of coercion errors', () => {
@@ -1661,4 +1684,31 @@ describe('Execute: Handles inputs', () => {
16611684
expect(result).to.include.keys('initialResult', 'subsequentResults');
16621685
});
16631686
});
1687+
1688+
describe('getVariableValues: own-property names', () => {
1689+
const doc = parse(`
1690+
query ($toString: String) {
1691+
fieldWithNullableStringInput(input: $toString)
1692+
}
1693+
`);
1694+
1695+
const operation = doc.definitions[0];
1696+
assert(operation.kind === Kind.OPERATION_DEFINITION);
1697+
const { variableDefinitions } = operation;
1698+
assert(variableDefinitions != null);
1699+
1700+
it('does not expose prototype variable names when omitted', () => {
1701+
const result = getVariableValues(schema, variableDefinitions, {});
1702+
assert('variableValues' in result);
1703+
expect(result.variableValues.coerced.toString).to.equal(undefined);
1704+
});
1705+
1706+
it('still returns provided variables with colliding names', () => {
1707+
const result = getVariableValues(schema, variableDefinitions, {
1708+
toString: 'value',
1709+
});
1710+
assert('variableValues' in result);
1711+
expect(result.variableValues.coerced.toString).to.equal('value');
1712+
});
1713+
});
16641714
});

src/execution/values.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,8 @@ type VariableValuesOrErrors =
5858
* provided variable definitions and arbitrary input. If the input cannot be
5959
* parsed to match the variable definitions, a GraphQLError will be thrown.
6060
*
61-
* Note: The returned value is a plain Object with a prototype, since it is
62-
* exposed to user code. Care should be taken to not pull values from the
63-
* Object prototype.
61+
* Note: The `coerced` and `sources` properties of VariableValues use null
62+
* prototype to avoid collisions with JavaScript's own property names.
6463
*/
6564
export function getVariableValues(
6665
schema: GraphQLSchema,
@@ -195,9 +194,8 @@ export function getFragmentVariableValues(
195194
* Prepares an object map of argument values given a list of argument
196195
* definitions and list of argument AST nodes.
197196
*
198-
* Note: The returned value is a plain Object with a prototype, since it is
199-
* exposed to user code. Care should be taken to not pull values from the
200-
* Object prototype.
197+
* Note: The returned value uses a null prototype to avoid collisions with
198+
* JavaScript's own property names.
201199
*/
202200
export function getArgumentValues(
203201
def: GraphQLField<unknown, unknown> | GraphQLDirective,
@@ -316,17 +314,16 @@ function coerceArgument(
316314
*
317315
* If the directive does not exist on the node, returns undefined.
318316
*
319-
* Note: The returned value is a plain Object with a prototype, since it is
320-
* exposed to user code. Care should be taken to not pull values from the
321-
* Object prototype.
317+
* Note: The returned value uses a null prototype to avoid collisions with
318+
* JavaScript's own property names.
322319
*/
323320
export function getDirectiveValues(
324321
directiveDef: GraphQLDirective,
325322
node: { readonly directives?: ReadonlyArray<DirectiveNode> | undefined },
326323
variableValues?: Maybe<VariableValues>,
327324
fragmentVariableValues?: Maybe<FragmentVariableValues>,
328325
hideSuggestions?: Maybe<boolean>,
329-
): undefined | { [argument: string]: unknown } {
326+
): undefined | ObjMap<unknown> {
330327
const directiveNode = node.directives?.find(
331328
(directive) => directive.name.value === directiveDef.name,
332329
);

0 commit comments

Comments
 (0)