-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Semantic nullability rfc implementation #4337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
17461b9
0f13010
869ca46
e0c2425
7121006
2321f93
4c8a02b
ac213fa
1861f71
855e4d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
import { expect } from 'chai'; | ||
import { describe, it } from 'mocha'; | ||
|
||
import { GraphQLError } from '../../error/GraphQLError'; | ||
|
||
import type { ExecutableDefinitionNode, FieldNode } from '../../language/ast'; | ||
import { parse } from '../../language/parser'; | ||
|
||
import { | ||
GraphQLNonNull, | ||
GraphQLObjectType, | ||
GraphQLSemanticNonNull, | ||
} from '../../type/definition'; | ||
import { GraphQLString } from '../../type/scalars'; | ||
import { GraphQLSchema } from '../../type/schema'; | ||
|
||
import { execute } from '../execute'; | ||
|
||
describe('Execute: Handles Semantic Nullability', () => { | ||
const DeepDataType = new GraphQLObjectType({ | ||
name: 'DeepDataType', | ||
fields: { | ||
f: { type: new GraphQLNonNull(GraphQLString) }, | ||
}, | ||
}); | ||
|
||
const DataType: GraphQLObjectType = new GraphQLObjectType({ | ||
name: 'DataType', | ||
fields: () => ({ | ||
a: { type: GraphQLString }, | ||
b: { type: new GraphQLSemanticNonNull(GraphQLString) }, | ||
c: { type: new GraphQLNonNull(GraphQLString) }, | ||
d: { type: new GraphQLSemanticNonNull(DeepDataType) }, | ||
}), | ||
}); | ||
|
||
it('SemanticNonNull throws error on null without error', async () => { | ||
const data = { | ||
a: () => 'Apple', | ||
b: () => null, | ||
c: () => 'Cookie', | ||
}; | ||
|
||
const document = parse(` | ||
query { | ||
b | ||
} | ||
`); | ||
|
||
const result = await execute({ | ||
schema: new GraphQLSchema({ query: DataType }), | ||
document, | ||
rootValue: data, | ||
}); | ||
|
||
const executable = document.definitions?.values().next() | ||
.value as ExecutableDefinitionNode; | ||
const selectionSet = executable.selectionSet.selections | ||
.values() | ||
.next().value; | ||
|
||
expect(result).to.deep.equal({ | ||
data: { | ||
b: null, | ||
}, | ||
errors: [ | ||
new GraphQLError( | ||
'Cannot return null for semantic-non-nullable field DataType.b.', | ||
{ | ||
nodes: selectionSet, | ||
path: ['b'], | ||
}, | ||
), | ||
], | ||
}); | ||
}); | ||
|
||
it('SemanticNonNull succeeds on null with error', async () => { | ||
const data = { | ||
a: () => 'Apple', | ||
b: () => { | ||
throw new Error('Something went wrong'); | ||
}, | ||
c: () => 'Cookie', | ||
}; | ||
|
||
const document = parse(` | ||
query { | ||
b | ||
} | ||
`); | ||
|
||
const executable = document.definitions?.values().next() | ||
.value as ExecutableDefinitionNode; | ||
const selectionSet = executable.selectionSet.selections | ||
.values() | ||
.next().value; | ||
|
||
const result = await execute({ | ||
schema: new GraphQLSchema({ query: DataType }), | ||
document, | ||
rootValue: data, | ||
}); | ||
|
||
expect(result).to.deep.equal({ | ||
data: { | ||
b: null, | ||
}, | ||
errors: [ | ||
new GraphQLError('Something went wrong', { | ||
nodes: selectionSet, | ||
path: ['b'], | ||
}), | ||
], | ||
}); | ||
}); | ||
|
||
it('SemanticNonNull halts null propagation', async () => { | ||
const deepData = { | ||
f: () => null, | ||
}; | ||
|
||
const data = { | ||
a: () => 'Apple', | ||
b: () => null, | ||
c: () => 'Cookie', | ||
d: () => deepData, | ||
}; | ||
|
||
const document = parse(` | ||
query { | ||
d { | ||
f | ||
} | ||
} | ||
`); | ||
|
||
const result = await execute({ | ||
schema: new GraphQLSchema({ query: DataType }), | ||
document, | ||
rootValue: data, | ||
}); | ||
|
||
const executable = document.definitions?.values().next() | ||
.value as ExecutableDefinitionNode; | ||
const dSelectionSet = executable.selectionSet.selections.values().next() | ||
.value as FieldNode; | ||
const fSelectionSet = dSelectionSet.selectionSet?.selections | ||
.values() | ||
.next().value; | ||
|
||
expect(result).to.deep.equal({ | ||
data: { | ||
d: null, | ||
}, | ||
errors: [ | ||
new GraphQLError( | ||
'Cannot return null for non-nullable field DeepDataType.f.', | ||
{ | ||
nodes: fSelectionSet, | ||
path: ['d', 'f'], | ||
}, | ||
), | ||
], | ||
}); | ||
}); | ||
|
||
it('SemanticNullable allows null values', async () => { | ||
const data = { | ||
a: () => null, | ||
b: () => null, | ||
c: () => 'Cookie', | ||
}; | ||
|
||
const document = parse(` | ||
query { | ||
a | ||
} | ||
`); | ||
|
||
const result = await execute({ | ||
schema: new GraphQLSchema({ query: DataType }), | ||
document, | ||
rootValue: data, | ||
}); | ||
|
||
expect(result).to.deep.equal({ | ||
data: { | ||
a: null, | ||
}, | ||
}); | ||
}); | ||
|
||
it('SemanticNullable allows non-null values', async () => { | ||
const data = { | ||
a: () => 'Apple', | ||
b: () => null, | ||
c: () => 'Cookie', | ||
}; | ||
|
||
const document = parse(` | ||
query { | ||
a | ||
} | ||
`); | ||
|
||
const result = await execute({ | ||
schema: new GraphQLSchema({ query: DataType }), | ||
document, | ||
rootValue: data, | ||
}); | ||
|
||
expect(result).to.deep.equal({ | ||
data: { | ||
a: 'Apple', | ||
}, | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -43,6 +43,7 @@ import { | |||||||||||
isListType, | ||||||||||||
isNonNullType, | ||||||||||||
isObjectType, | ||||||||||||
isSemanticNonNullType, | ||||||||||||
} from '../type/definition'; | ||||||||||||
import { | ||||||||||||
SchemaMetaFieldDef, | ||||||||||||
|
@@ -115,6 +116,7 @@ export interface ExecutionContext { | |||||||||||
typeResolver: GraphQLTypeResolver<any, any>; | ||||||||||||
subscribeFieldResolver: GraphQLFieldResolver<any, any>; | ||||||||||||
errors: Array<GraphQLError>; | ||||||||||||
errorPropagation: boolean; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/** | ||||||||||||
|
@@ -152,6 +154,13 @@ export interface ExecutionArgs { | |||||||||||
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>; | ||||||||||||
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>; | ||||||||||||
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>; | ||||||||||||
/** | ||||||||||||
* Set to `false` to disable error propagation. Experimental. | ||||||||||||
* TODO: describe what this does | ||||||||||||
|
// If the field type is non-nullable, then it is resolved without any | |
// protection from errors, however it still properly locates the error. | |
if (exeContext.errorPropagation && isNonNullType(returnType)) { | |
throw error; | |
} |
Uh oh!
There was an error while loading. Please reload this page.