Skip to content

Commit 84b9351

Browse files
committed
new files
1 parent 4d9a2dd commit 84b9351

File tree

4 files changed

+378
-0
lines changed

4 files changed

+378
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { Parser, parseType } from '../../language/parser';
5+
import { TokenKind } from '../../language/tokenKind';
6+
7+
import { assertOutputType } from '../../type/definition';
8+
import { GraphQLInt } from '../../type/scalars';
9+
import { GraphQLSchema } from '../../type/schema';
10+
11+
import { applyRequiredStatus } from '../applyRequiredStatus';
12+
import { typeFromAST } from '../typeFromAST';
13+
14+
function applyRequiredStatusTest(
15+
typeStr: string,
16+
nullabilityStr: string,
17+
): string {
18+
const schema = new GraphQLSchema({ types: [GraphQLInt] });
19+
const type = assertOutputType(typeFromAST(schema, parseType(typeStr)));
20+
21+
const parser = new Parser(nullabilityStr, {
22+
experimentalClientControlledNullability: true,
23+
});
24+
25+
parser.expectToken(TokenKind.SOF);
26+
const nullabilityNode = parser.parseNullabilityModifier();
27+
parser.expectToken(TokenKind.EOF);
28+
29+
const outputType = applyRequiredStatus(type, nullabilityNode);
30+
return outputType.toString();
31+
}
32+
33+
describe('applyRequiredStatus', () => {
34+
it('applyRequiredStatus smoke test', () => {
35+
expect(applyRequiredStatusTest('Int', '')).to.equal('Int');
36+
});
37+
38+
it('applyRequiredStatus produces correct output types with no overrides', () => {
39+
expect(applyRequiredStatusTest('[[[Int!]]!]!', '[[[]]]')).to.equal(
40+
'[[[Int!]]!]!',
41+
);
42+
});
43+
44+
it('applyRequiredStatus produces correct output types with required overrides', () => {
45+
expect(applyRequiredStatusTest('[[[Int!]]!]!', '[[[!]!]!]!')).to.equal(
46+
'[[[Int!]!]!]!',
47+
);
48+
});
49+
50+
it('applyRequiredStatus produces correct output types with optional overrides', () => {
51+
expect(applyRequiredStatusTest('[[[Int!]]!]!', '[[[?]?]?]?')).to.equal(
52+
'[[[Int]]]',
53+
);
54+
});
55+
56+
it('applyRequiredStatus throws error when modifier is too deep', () => {
57+
expect(() => {
58+
applyRequiredStatusTest('[[[Int!]]!]!', '[[[[]]]]');
59+
}).to.throw('List nullability modifier is too deep.');
60+
});
61+
62+
it('applyRequiredStatus throws error when modifier is too shallow', () => {
63+
expect(() => {
64+
applyRequiredStatusTest('[[[Int!]]!]!', '[[]]');
65+
}).to.throw('List nullability modifier is too shallow.');
66+
});
67+
68+
it('applyRequiredStatus with required designator functions when list syntax is excluded', () => {
69+
expect(applyRequiredStatusTest('[[[Int!]]!]', '!')).to.equal(
70+
'[[[Int!]]!]!',
71+
);
72+
});
73+
74+
it('applyRequiredStatus with optional designator functions when list syntax is excluded', () => {
75+
expect(applyRequiredStatusTest('[[[Int!]]!]!', '?')).to.equal(
76+
'[[[Int!]]!]',
77+
);
78+
});
79+
});

src/utilities/applyRequiredStatus.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { GraphQLError } from '../error/GraphQLError';
2+
3+
import type { NullabilityAssertionNode } from '../language/ast';
4+
import { Kind } from '../language/kinds';
5+
import type { ASTReducer } from '../language/visitor';
6+
import { visit } from '../language/visitor';
7+
8+
import type { GraphQLOutputType } from '../type/definition';
9+
import {
10+
assertListType,
11+
getNullableType,
12+
GraphQLList,
13+
GraphQLNonNull,
14+
isListType,
15+
isNonNullType,
16+
} from '../type/definition';
17+
18+
/**
19+
* Implements the "Accounting For Client Controlled Nullability Designators"
20+
* section of the spec. In particular, this function figures out the true return
21+
* type of a field by taking into account both the nullability listed in the
22+
* schema, and the nullability providing by an operation.
23+
*
24+
* @internal
25+
*/
26+
export function applyRequiredStatus(
27+
type: GraphQLOutputType,
28+
nullabilityNode: NullabilityAssertionNode | undefined,
29+
): GraphQLOutputType {
30+
// If the field is marked with 0 or 1 nullability designator
31+
// short-circuit
32+
if (nullabilityNode === undefined) {
33+
return type;
34+
} else if (nullabilityNode?.nullabilityAssertion === undefined) {
35+
if (nullabilityNode?.kind === Kind.NON_NULL_ASSERTION) {
36+
return new GraphQLNonNull(getNullableType(type));
37+
} else if (nullabilityNode?.kind === Kind.ERROR_BOUNDARY) {
38+
return getNullableType(type);
39+
}
40+
}
41+
42+
const typeStack: [GraphQLOutputType] = [type];
43+
44+
// Load the nullable version each type in the type definition to typeStack
45+
while (isListType(getNullableType(typeStack[typeStack.length - 1]))) {
46+
const list = assertListType(
47+
getNullableType(typeStack[typeStack.length - 1]),
48+
);
49+
const elementType = list.ofType as GraphQLOutputType;
50+
typeStack.push(elementType);
51+
}
52+
53+
// Re-apply nullability to each level of the list from the outside in
54+
const applyStatusReducer: ASTReducer<GraphQLOutputType> = {
55+
NonNullAssertion: {
56+
leave({ nullabilityAssertion }) {
57+
if (nullabilityAssertion) {
58+
return new GraphQLNonNull(getNullableType(nullabilityAssertion));
59+
}
60+
61+
// We're working with the inner-most type
62+
const nextType = typeStack.pop();
63+
64+
// There's no way for nextType to be null if both type and nullabilityNode are valid
65+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
66+
return new GraphQLNonNull(getNullableType(nextType!));
67+
},
68+
},
69+
ErrorBoundary: {
70+
leave({ nullabilityAssertion }) {
71+
if (nullabilityAssertion) {
72+
return getNullableType(nullabilityAssertion);
73+
}
74+
75+
// We're working with the inner-most type
76+
const nextType = typeStack.pop();
77+
78+
// There's no way for nextType to be null if both type and nullabilityNode are valid
79+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
80+
return getNullableType(nextType!);
81+
},
82+
},
83+
ListNullabilityOperator: {
84+
leave({ nullabilityAssertion }) {
85+
let listType = typeStack.pop();
86+
// Skip to the inner-most list
87+
if (!isListType(getNullableType(listType))) {
88+
listType = typeStack.pop();
89+
}
90+
91+
if (!listType) {
92+
throw new GraphQLError(
93+
'List nullability modifier is too deep.',
94+
{
95+
nodes: nullabilityNode
96+
},
97+
);
98+
}
99+
const isRequired = isNonNullType(listType);
100+
if (nullabilityAssertion) {
101+
return isRequired
102+
? new GraphQLNonNull(new GraphQLList(nullabilityAssertion))
103+
: new GraphQLList(nullabilityAssertion);
104+
}
105+
106+
// We're working with the inner-most list
107+
return listType;
108+
},
109+
},
110+
};
111+
112+
const modified = visit(nullabilityNode, applyStatusReducer);
113+
// modifiers must be exactly the same depth as the field type
114+
if (typeStack.length > 0) {
115+
throw new GraphQLError(
116+
'List nullability modifier is too shallow.',
117+
{
118+
nodes: nullabilityNode
119+
},
120+
);
121+
}
122+
return modified;
123+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, it } from 'mocha';
2+
3+
import { buildSchema } from '../../utilities/buildASTSchema';
4+
5+
import { RequiredStatusOnFieldMatchesDefinitionRule } from '../rules/RequiredStatusOnFieldMatchesDefinitionRule';
6+
7+
import { expectValidationErrorsWithSchema } from './harness';
8+
9+
function expectErrors(queryStr: string) {
10+
return expectValidationErrorsWithSchema(
11+
testSchema,
12+
RequiredStatusOnFieldMatchesDefinitionRule,
13+
queryStr,
14+
{ experimentalClientControlledNullability: true },
15+
);
16+
}
17+
18+
function expectValid(queryStr: string) {
19+
expectErrors(queryStr).toDeepEqual([]);
20+
}
21+
22+
const testSchema = buildSchema(`
23+
type Lists {
24+
nonList: Int
25+
list: [Int]
26+
requiredList: [Int]!
27+
mixedThreeDList: [[[Int]]]
28+
}
29+
type Query {
30+
lists: Lists
31+
}
32+
`);
33+
34+
describe('Validate: Field uses correct list depth', () => {
35+
it('Fields are valid', () => {
36+
expectValid(`
37+
fragment listFragment on Lists {
38+
list[!]
39+
nonList!
40+
nonList?
41+
mixedThreeDList[[[!]!]!]!
42+
requiredList[]
43+
unmodifiedList: list
44+
}
45+
`);
46+
});
47+
48+
it('reports errors when list depth is too high', () => {
49+
expectErrors(`
50+
fragment listFragment on Lists {
51+
notAList: nonList[!]
52+
list[[]]
53+
}
54+
`).toDeepEqual([
55+
{
56+
message: 'List nullability modifier is too deep.',
57+
locations: [{ line: 3, column: 26 }],
58+
},
59+
{
60+
message: 'List nullability modifier is too deep.',
61+
locations: [{ line: 4, column: 13 }],
62+
},
63+
]);
64+
});
65+
66+
it('reports errors when list depth is too low', () => {
67+
expectErrors(`
68+
fragment listFragment on Lists {
69+
list!
70+
mixedThreeDList[[]!]!
71+
}
72+
`).toDeepEqual([
73+
{
74+
message: 'List nullability modifier is too shallow.',
75+
locations: [{ line: 3, column: 13 }],
76+
},
77+
{
78+
message: 'List nullability modifier is too shallow.',
79+
locations: [{ line: 4, column: 24 }],
80+
},
81+
]);
82+
});
83+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { GraphQLError } from '../../error/GraphQLError';
2+
3+
import type {
4+
FieldNode,
5+
ListNullabilityOperatorNode,
6+
NullabilityAssertionNode,
7+
} from '../../language/ast';
8+
import type { ASTReducer, ASTVisitor } from '../../language/visitor';
9+
import { visit } from '../../language/visitor';
10+
11+
import type { GraphQLOutputType } from '../../type/definition';
12+
import {
13+
assertListType,
14+
getNullableType,
15+
isListType,
16+
} from '../../type/definition';
17+
18+
import type { ValidationContext } from '../ValidationContext';
19+
20+
/**
21+
* List element nullability designators need to use a depth that is the same as or less than the
22+
* type of the field it's applied to.
23+
*
24+
* Otherwise the GraphQL document is invalid.
25+
*
26+
* See https://spec.graphql.org/draft/#sec-Field-Selections
27+
*/
28+
export function RequiredStatusOnFieldMatchesDefinitionRule(
29+
context: ValidationContext,
30+
): ASTVisitor {
31+
return {
32+
Field(node: FieldNode) {
33+
const fieldDef = context.getFieldDef();
34+
const requiredNode = node.nullabilityAssertion;
35+
if (fieldDef && requiredNode) {
36+
const typeDepth = getTypeDepth(fieldDef.type);
37+
const designatorDepth = getDesignatorDepth(requiredNode);
38+
39+
if (typeDepth > designatorDepth) {
40+
context.reportError(
41+
new GraphQLError('List nullability modifier is too shallow.', {
42+
nodes: node.nullabilityAssertion,
43+
}),
44+
);
45+
} else if (typeDepth < designatorDepth) {
46+
context.reportError(
47+
new GraphQLError('List nullability modifier is too deep.', {
48+
nodes: node.nullabilityAssertion,
49+
}),
50+
);
51+
}
52+
}
53+
},
54+
};
55+
56+
function getTypeDepth(type: GraphQLOutputType): number {
57+
let currentType = type;
58+
let depthCount = 0;
59+
while (isListType(getNullableType(currentType))) {
60+
const list = assertListType(getNullableType(currentType));
61+
const elementType = list.ofType as GraphQLOutputType;
62+
currentType = elementType;
63+
depthCount += 1;
64+
}
65+
return depthCount;
66+
}
67+
68+
function getDesignatorDepth(
69+
designator: ListNullabilityOperatorNode | NullabilityAssertionNode,
70+
): number {
71+
const getDepth: ASTReducer<number> = {
72+
NonNullAssertion: {
73+
leave({ nullabilityAssertion }) {
74+
return nullabilityAssertion ?? 0;
75+
},
76+
},
77+
78+
ErrorBoundary: {
79+
leave({ nullabilityAssertion }) {
80+
return nullabilityAssertion ?? 0;
81+
},
82+
},
83+
84+
ListNullabilityOperator: {
85+
leave({ nullabilityAssertion }) {
86+
return (nullabilityAssertion ?? 0) + 1;
87+
},
88+
},
89+
};
90+
91+
return visit(designator, getDepth);
92+
}
93+
}

0 commit comments

Comments
 (0)