Skip to content

Commit 40c160e

Browse files
OverlappingFieldsCanBeMerged: sort argument values before comparing (#3455)
1 parent 0a654cc commit 40c160e

File tree

5 files changed

+138
-19
lines changed

5 files changed

+138
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { parseValue } from '../../language/parser';
5+
import { print } from '../../language/printer';
6+
7+
import { sortValueNode } from '../sortValueNode';
8+
9+
describe('sortValueNode', () => {
10+
function expectSortedValue(source: string) {
11+
return expect(print(sortValueNode(parseValue(source))));
12+
}
13+
14+
it('do not change non-object values', () => {
15+
expectSortedValue('1').to.equal('1');
16+
expectSortedValue('3.14').to.equal('3.14');
17+
expectSortedValue('null').to.equal('null');
18+
expectSortedValue('true').to.equal('true');
19+
expectSortedValue('false').to.equal('false');
20+
expectSortedValue('"cba"').to.equal('"cba"');
21+
expectSortedValue('"""cba"""').to.equal('"""cba"""');
22+
expectSortedValue('[1, 3.14, null, false, "cba"]').to.equal(
23+
'[1, 3.14, null, false, "cba"]',
24+
);
25+
expectSortedValue('[[1, 3.14, null, false, "cba"]]').to.equal(
26+
'[[1, 3.14, null, false, "cba"]]',
27+
);
28+
});
29+
30+
it('sort input object fields', () => {
31+
expectSortedValue('{ b: 2, a: 1 }').to.equal('{a: 1, b: 2}');
32+
expectSortedValue('{ a: { c: 3, b: 2 } }').to.equal('{a: {b: 2, c: 3}}');
33+
expectSortedValue('[{ b: 2, a: 1 }, { d: 4, c: 3}]').to.equal(
34+
'[{a: 1, b: 2}, {c: 3, d: 4}]',
35+
);
36+
expectSortedValue(
37+
'{ b: { g: 7, f: 6 }, c: 3 , a: { d: 4, e: 5 } }',
38+
).to.equal('{a: {d: 4, e: 5}, b: {f: 6, g: 7}, c: 3}');
39+
});
40+
});

src/utilities/findBreakingChanges.ts

+2-16
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { inspect } from '../jsutils/inspect';
22
import { invariant } from '../jsutils/invariant';
33
import { keyMap } from '../jsutils/keyMap';
4-
import { naturalCompare } from '../jsutils/naturalCompare';
54

65
import { print } from '../language/printer';
7-
import { visit } from '../language/visitor';
86

97
import type {
108
GraphQLEnumType,
@@ -34,6 +32,7 @@ import { isSpecifiedScalarType } from '../type/scalars';
3432
import type { GraphQLSchema } from '../type/schema';
3533

3634
import { astFromValue } from './astFromValue';
35+
import { sortValueNode } from './sortValueNode';
3736

3837
export enum BreakingChangeType {
3938
TYPE_REMOVED = 'TYPE_REMOVED',
@@ -536,20 +535,7 @@ function typeKindName(type: GraphQLNamedType): string {
536535
function stringifyValue(value: unknown, type: GraphQLInputType): string {
537536
const ast = astFromValue(value, type);
538537
invariant(ast != null);
539-
540-
const sortedAST = visit(ast, {
541-
ObjectValue(objectNode) {
542-
// Make a copy since sort mutates array
543-
const fields = [...objectNode.fields];
544-
545-
fields.sort((fieldA, fieldB) =>
546-
naturalCompare(fieldA.name.value, fieldB.name.value),
547-
);
548-
return { ...objectNode, fields };
549-
},
550-
});
551-
552-
return print(sortedAST);
538+
return print(sortValueNode(ast));
553539
}
554540

555541
function diff<T extends { name: string }>(

src/utilities/sortValueNode.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { naturalCompare } from '../jsutils/naturalCompare';
2+
3+
import type { ObjectFieldNode, ValueNode } from '../language/ast';
4+
import { Kind } from '../language/kinds';
5+
6+
/**
7+
* Sort ValueNode.
8+
*
9+
* This function returns a sorted copy of the given ValueNode.
10+
*
11+
* @internal
12+
*/
13+
export function sortValueNode(valueNode: ValueNode): ValueNode {
14+
switch (valueNode.kind) {
15+
case Kind.OBJECT:
16+
return {
17+
...valueNode,
18+
fields: sortFields(valueNode.fields),
19+
};
20+
case Kind.LIST:
21+
return {
22+
...valueNode,
23+
values: valueNode.values.map(sortValueNode),
24+
};
25+
case Kind.INT:
26+
case Kind.FLOAT:
27+
case Kind.STRING:
28+
case Kind.BOOLEAN:
29+
case Kind.NULL:
30+
case Kind.ENUM:
31+
case Kind.VARIABLE:
32+
return valueNode;
33+
}
34+
}
35+
36+
function sortFields(
37+
fields: ReadonlyArray<ObjectFieldNode>,
38+
): Array<ObjectFieldNode> {
39+
return fields
40+
.map((fieldNode) => ({
41+
...fieldNode,
42+
value: sortValueNode(fieldNode.value),
43+
}))
44+
.sort((fieldA, fieldB) =>
45+
naturalCompare(fieldA.name.value, fieldB.name.value),
46+
);
47+
}

src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts

+45
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,51 @@ describe('Validate: Overlapping fields can be merged', () => {
236236
`);
237237
});
238238

239+
it('allows different order of args', () => {
240+
const schema = buildSchema(`
241+
type Query {
242+
someField(a: String, b: String): String
243+
}
244+
`);
245+
246+
// This is valid since arguments are unordered, see:
247+
// https://spec.graphql.org/draft/#sec-Language.Arguments.Arguments-are-unordered
248+
expectValidWithSchema(
249+
schema,
250+
`
251+
{
252+
someField(a: null, b: null)
253+
someField(b: null, a: null)
254+
}
255+
`,
256+
);
257+
});
258+
259+
it('allows different order of input object fields in arg values', () => {
260+
const schema = buildSchema(`
261+
input SomeInput {
262+
a: String
263+
b: String
264+
}
265+
266+
type Query {
267+
someField(arg: SomeInput): String
268+
}
269+
`);
270+
271+
// This is valid since input object fields are unordered, see:
272+
// https://spec.graphql.org/draft/#sec-Input-Object-Values.Input-object-fields-are-unordered
273+
expectValidWithSchema(
274+
schema,
275+
`
276+
{
277+
someField(arg: { a: null, b: null })
278+
someField(arg: { b: null, a: null })
279+
}
280+
`,
281+
);
282+
});
283+
239284
it('encounters conflict in fragments', () => {
240285
expectErrors(`
241286
{

src/validation/rules/OverlappingFieldsCanBeMergedRule.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
isObjectType,
3030
} from '../../type/definition';
3131

32+
import { sortValueNode } from '../../utilities/sortValueNode';
3233
import { typeFromAST } from '../../utilities/typeFromAST';
3334

3435
import type { ValidationContext } from '../ValidationContext';
@@ -652,12 +653,12 @@ function sameArguments(
652653
if (!argument2) {
653654
return false;
654655
}
655-
return sameValue(argument1.value, argument2.value);
656+
return stringifyValue(argument1.value) === stringifyValue(argument2.value);
656657
});
657658
}
658659

659-
function sameValue(value1: ValueNode, value2: ValueNode): boolean {
660-
return print(value1) === print(value2);
660+
function stringifyValue(value: ValueNode): string {
661+
return print(sortValueNode(value));
661662
}
662663

663664
// Two types conflict if both types could not apply to a value simultaneously.

0 commit comments

Comments
 (0)