Skip to content

Commit 07e4646

Browse files
committed
some new semanticNullability execution tests
1 parent 95484ba commit 07e4646

File tree

4 files changed

+265
-6
lines changed

4 files changed

+265
-6
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"test": "npm run lint && npm run check && npm run testonly && npm run prettier:check && npm run check:spelling && npm run check:integrations",
3939
"lint": "eslint --cache --max-warnings 0 .",
4040
"check": "tsc --pretty",
41-
"testonly": "mocha --full-trace src/**/__tests__/**/*-test.ts",
41+
"testonly": "mocha --full-trace src/**/__tests__/**/*-test.ts -g 'SemanticNonNull halts null propagation'",
4242
"testonly:cover": "c8 npm run testonly",
4343
"prettier": "prettier --write --list-different .",
4444
"prettier:check": "prettier --check .",

src/execution/__tests__/executor-test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ import {
1616
GraphQLNonNull,
1717
GraphQLObjectType,
1818
GraphQLScalarType,
19+
GraphQLSemanticNonNull,
20+
GraphQLSemanticOptional,
1921
GraphQLUnionType,
2022
} from '../../type/definition';
2123
import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars';
2224
import { GraphQLSchema } from '../../type/schema';
2325

2426
import { execute, executeSync } from '../execute';
27+
import { GraphQLError } from '../../error';
28+
import { ExecutableDefinitionNode, FieldNode, SelectionSetNode } from '../../language';
2529

2630
describe('Execute: Handles basic execution tasks', () => {
2731
it('throws if no document is provided', () => {
@@ -1323,3 +1327,152 @@ describe('Execute: Handles basic execution tasks', () => {
13231327
expect(possibleTypes).to.deep.equal([fooObject]);
13241328
});
13251329
});
1330+
1331+
describe('Execute: Handles Semantic Nullability', () => {
1332+
const DeepDataType = new GraphQLObjectType({
1333+
name: 'DeepDataType',
1334+
fields: {
1335+
f: { type: new GraphQLNonNull(GraphQLString) }
1336+
},
1337+
});
1338+
1339+
const DataType: GraphQLObjectType = new GraphQLObjectType({
1340+
name: 'DataType',
1341+
fields: () => ({
1342+
a: { type: new GraphQLSemanticOptional(GraphQLString) },
1343+
b: { type: new GraphQLSemanticNonNull(GraphQLString) },
1344+
c: { type: new GraphQLNonNull(GraphQLString) },
1345+
d: { type: new GraphQLSemanticNonNull(DeepDataType) }
1346+
}),
1347+
});
1348+
1349+
it('SemanticNonNull throws error on null without error', async () => {
1350+
const data = {
1351+
a: () => 'Apple',
1352+
b: () => null,
1353+
c: () => 'Cookie'
1354+
};
1355+
1356+
const document = parse(`
1357+
query {
1358+
b
1359+
}
1360+
`);
1361+
1362+
const result = await execute({
1363+
schema: new GraphQLSchema({ query: DataType }),
1364+
document,
1365+
rootValue: data,
1366+
});
1367+
1368+
let executable = document.definitions?.values().next().value as ExecutableDefinitionNode;
1369+
let selectionSet = executable.selectionSet.selections.values().next().value;
1370+
1371+
expect(result).to.deep.equal({
1372+
data: {
1373+
b: null
1374+
},
1375+
errors: [
1376+
new GraphQLError(
1377+
'Cannot return null for semantic-non-nullable field DataType.b.',
1378+
{
1379+
nodes: selectionSet,
1380+
path: ['b']
1381+
}
1382+
)
1383+
]
1384+
});
1385+
});
1386+
1387+
it('SemanticNonNull succeeds on null with error', async () => {
1388+
const data = {
1389+
a: () => 'Apple',
1390+
b: () => { throw new Error(
1391+
`Something went wrong`,
1392+
); },
1393+
c: () => 'Cookie'
1394+
};
1395+
1396+
const document = parse(`
1397+
query {
1398+
b
1399+
}
1400+
`);
1401+
1402+
let executable = document.definitions?.values().next().value as ExecutableDefinitionNode;
1403+
let selectionSet = executable.selectionSet.selections.values().next().value;
1404+
1405+
const result = await execute({
1406+
schema: new GraphQLSchema({ query: DataType }),
1407+
document,
1408+
rootValue: data,
1409+
});
1410+
1411+
expect(result).to.deep.equal({
1412+
data: {
1413+
b: null
1414+
},
1415+
errors: [
1416+
new GraphQLError(
1417+
'Something went wrong',
1418+
{
1419+
nodes: selectionSet,
1420+
path: ['b']
1421+
}
1422+
)
1423+
]
1424+
});
1425+
});
1426+
1427+
it('SemanticNonNull halts null propagation', async () => {
1428+
const data = {
1429+
a: () => 'Apple',
1430+
b: () => null,
1431+
c: () => 'Cookie',
1432+
d: () => {
1433+
f: () => null
1434+
}
1435+
};
1436+
1437+
const document = parse(`
1438+
query {
1439+
d {
1440+
f
1441+
}
1442+
}
1443+
`);
1444+
1445+
const result = await execute({
1446+
schema: new GraphQLSchema({ query: DataType }),
1447+
document,
1448+
rootValue: data,
1449+
});
1450+
1451+
let executable = document.definitions?.values().next().value as ExecutableDefinitionNode;
1452+
let dSelectionSet = executable.selectionSet.selections.values().next().value as FieldNode;
1453+
let fSelectionSet = dSelectionSet.selectionSet?.selections.values().next().value;
1454+
1455+
expect(result).to.deep.equal({
1456+
data: {
1457+
d: null
1458+
},
1459+
errors: [
1460+
new GraphQLError(
1461+
'Cannot return null for non-nullable field DeepDataType.f.',
1462+
{
1463+
nodes: fSelectionSet,
1464+
path: ['d', 'f']
1465+
}
1466+
)
1467+
]
1468+
});
1469+
});
1470+
1471+
it('SemanticOptional allows null values', async () => {
1472+
1473+
});
1474+
1475+
it('SemanticOptional allows non-null values', async () => {
1476+
1477+
});
1478+
});

src/execution/execute.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
isNonNullType,
4545
isObjectType,
4646
isSemanticNonNullType,
47+
isSemanticOptionalType,
4748
} from '../type/definition';
4849
import {
4950
SchemaMetaFieldDef,
@@ -650,9 +651,12 @@ function completeValue(
650651
throw result;
651652
}
652653

654+
console.log("anything", path);
655+
653656
// If field type is NonNull, complete for inner type, and throw field error
654657
// if result is null.
655658
if (isNonNullType(returnType)) {
659+
console.log("is non null");
656660
const completed = completeValue(
657661
exeContext,
658662
returnType.ofType,
@@ -670,8 +674,9 @@ function completeValue(
670674
}
671675

672676
// If field type is SemanticNonNull, complete for inner type, and throw field error
673-
// if result is null.
677+
// if result is null and an error doesn't exist.
674678
if (isSemanticNonNullType(returnType)) {
679+
console.log("Is semantic non null")
675680
const completed = completeValue(
676681
exeContext,
677682
returnType.ofType,
@@ -688,8 +693,21 @@ function completeValue(
688693
return completed;
689694
}
690695

696+
// If field type is SemanticOptional, complete for inner type
697+
if (isSemanticOptionalType(returnType)) {
698+
return completeValue(
699+
exeContext,
700+
returnType.ofType,
701+
fieldNodes,
702+
info,
703+
path,
704+
result,
705+
);
706+
}
707+
691708
// If result value is null or undefined then return null.
692709
if (result == null) {
710+
console.log("is null")
693711
return null;
694712
}
695713

@@ -708,12 +726,14 @@ function completeValue(
708726
// If field type is a leaf type, Scalar or Enum, serialize to a valid value,
709727
// returning null if serialization is not possible.
710728
if (isLeafType(returnType)) {
729+
console.log("is leaf")
711730
return completeLeafValue(returnType, result);
712731
}
713732

714733
// If field type is an abstract type, Interface or Union, determine the
715734
// runtime Object type and complete for that type.
716735
if (isAbstractType(returnType)) {
736+
console.log("is abstract")
717737
return completeAbstractValue(
718738
exeContext,
719739
returnType,
@@ -726,6 +746,7 @@ function completeValue(
726746

727747
// If field type is Object, execute and complete all sub-selections.
728748
if (isObjectType(returnType)) {
749+
console.log("is object")
729750
return completeObjectValue(
730751
exeContext,
731752
returnType,

src/type/definition.ts

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,16 @@ export type GraphQLType =
7575
| GraphQLEnumType
7676
| GraphQLInputObjectType
7777
| GraphQLList<GraphQLType>
78-
>;
78+
>
79+
| GraphQLSemanticOptional<
80+
| GraphQLScalarType
81+
| GraphQLObjectType
82+
| GraphQLInterfaceType
83+
| GraphQLUnionType
84+
| GraphQLEnumType
85+
| GraphQLInputObjectType
86+
| GraphQLList<GraphQLType>
87+
>;
7988

8089
export function isType(type: unknown): type is GraphQLType {
8190
return (
@@ -239,6 +248,32 @@ export function assertSemanticNonNullType(
239248
return type;
240249
}
241250

251+
export function isSemanticOptionalType(
252+
type: GraphQLInputType,
253+
): type is GraphQLSemanticOptional<GraphQLInputType>;
254+
export function isSemanticOptionalType(
255+
type: GraphQLOutputType,
256+
): type is GraphQLSemanticOptional<GraphQLOutputType>;
257+
export function isSemanticOptionalType(
258+
type: unknown,
259+
): type is GraphQLSemanticOptional<GraphQLType>;
260+
export function isSemanticOptionalType(
261+
type: unknown,
262+
): type is GraphQLSemanticOptional<GraphQLType> {
263+
return instanceOf(type, GraphQLSemanticOptional);
264+
}
265+
266+
export function assertSemanticOptionalType(
267+
type: unknown,
268+
): GraphQLSemanticOptional<GraphQLType> {
269+
if (!isSemanticOptionalType(type)) {
270+
throw new Error(
271+
`Expected ${inspect(type)} to be a GraphQL Semantic-Non-Null type.`,
272+
);
273+
}
274+
return type;
275+
}
276+
242277
/**
243278
* These types may be used as input types for arguments and directives.
244279
*/
@@ -502,7 +537,56 @@ export class GraphQLSemanticNonNull<T extends GraphQLNullableType> {
502537
}
503538

504539
toString(): string {
505-
return String(this.ofType) + '*';
540+
return String(this.ofType);
541+
}
542+
543+
toJSON(): string {
544+
return this.toString();
545+
}
546+
}
547+
548+
/**
549+
* Semantic-Non-Null Type Wrapper
550+
*
551+
* A semantic-non-null is a wrapping type which points to another type.
552+
* Semantic-non-null types enforce that their values are never null unless
553+
* caused by an error being raised. It is useful for fields which you can make
554+
* a guarantee on non-nullability in a no-error case, for example when you know
555+
* that a related entity must exist (but acknowledge that retrieving it may
556+
* produce an error).
557+
*
558+
* Example:
559+
*
560+
* ```ts
561+
* const RowType = new GraphQLObjectType({
562+
* name: 'Row',
563+
* fields: () => ({
564+
* email: { type: new GraphQLSemanticNonNull(GraphQLString) },
565+
* })
566+
* })
567+
* ```
568+
* Note: the enforcement of non-nullability occurs within the executor.
569+
*
570+
* @experimental
571+
*/
572+
export class GraphQLSemanticOptional<T extends GraphQLNullableType> {
573+
readonly ofType: T;
574+
575+
constructor(ofType: T) {
576+
devAssert(
577+
isNullableType(ofType),
578+
`Expected ${inspect(ofType)} to be a GraphQL nullable type.`,
579+
);
580+
581+
this.ofType = ofType;
582+
}
583+
584+
get [Symbol.toStringTag]() {
585+
return 'GraphQLSemanticOptional';
586+
}
587+
588+
toString(): string {
589+
return String(this.ofType) + '?';
506590
}
507591

508592
toJSON(): string {
@@ -517,10 +601,11 @@ export class GraphQLSemanticNonNull<T extends GraphQLNullableType> {
517601
export type GraphQLWrappingType =
518602
| GraphQLList<GraphQLType>
519603
| GraphQLNonNull<GraphQLType>
520-
| GraphQLSemanticNonNull<GraphQLType>;
604+
| GraphQLSemanticNonNull<GraphQLType>
605+
| GraphQLSemanticOptional<GraphQLType>;
521606

522607
export function isWrappingType(type: unknown): type is GraphQLWrappingType {
523-
return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type);
608+
return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type) || isSemanticOptionalType(type);
524609
}
525610

526611
export function assertWrappingType(type: unknown): GraphQLWrappingType {

0 commit comments

Comments
 (0)