Skip to content
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

Semantic nullability document directive #4271

Open
wants to merge 31 commits into
base: 16.x.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7ca49b2
New GraphQLSemanticNonNull type
benjie Sep 14, 2024
16a2114
Handle isNonNullType
benjie Sep 14, 2024
2b13389
More fixes
benjie Sep 14, 2024
04a8e91
More fixes
benjie Sep 14, 2024
076a735
Yet more updates
benjie Sep 14, 2024
c2196a0
Recognize in introspection, enable disabling null bubbling
benjie Sep 14, 2024
f588046
Lint fixes
benjie Sep 14, 2024
fa3f177
More missing pieces
benjie Sep 14, 2024
b5e81bd
More fixes
benjie Sep 14, 2024
1f6a019
Fix schema
benjie Sep 14, 2024
491f49b
Fix another test
benjie Sep 14, 2024
3a91590
More minor test fixes
benjie Sep 14, 2024
56db880
Fix introspection test
benjie Sep 14, 2024
593ce44
Add support for * to lexer
benjie Sep 14, 2024
1311906
Allow specifying errorPropagation at top level
benjie Sep 14, 2024
9d706d2
Factor into getIntrospectionQuery
benjie Sep 14, 2024
e9f9628
Lint
benjie Sep 14, 2024
eb9b6c8
Prettier
benjie Sep 14, 2024
3a900a9
parser tests passing
twof Oct 30, 2024
557a986
Add semantic optional type
twof Nov 7, 2024
95484ba
printer and parser tests passing
twof Nov 7, 2024
07e4646
some new semanticNullability execution tests
twof Nov 8, 2024
6fac3b5
SemanticNonNull halts null propagation
twof Nov 8, 2024
d11a647
SemanticOptional cleared
twof Nov 8, 2024
0a8b68f
logging cleanup
twof Nov 8, 2024
24474fa
rename to SemanticNullable
twof Nov 8, 2024
af58560
better SemanticNullable docs
twof Nov 8, 2024
668f3dc
move semantic nullability tests to their own file
twof Nov 8, 2024
63345de
fix git status
twof Nov 8, 2024
c472b9e
run prettier
twof Nov 8, 2024
163785d
Add comment to parser about document directive
twof Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/__tests__/starWarsIntrospection-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('Star Wars Introspection Tests', () => {
{ name: '__TypeKind' },
{ name: '__Field' },
{ name: '__InputValue' },
{ name: '__TypeNullability' },
{ name: '__EnumValue' },
{ name: '__Directive' },
{ name: '__DirectiveLocation' },
Expand Down
2 changes: 2 additions & 0 deletions src/execution/__tests__/executor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ describe('Execute: Handles basic execution tasks', () => {
'rootValue',
'operation',
'variableValues',
'errorPropagation',
);

const operation = document.definitions[0];
Expand All @@ -275,6 +276,7 @@ describe('Execute: Handles basic execution tasks', () => {
schema,
rootValue,
operation,
errorPropagation: true,
});

const field = operation.selectionSet.selections[0];
Expand Down
220 changes: 220 additions & 0 deletions src/execution/__tests__/semantic-nullability-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
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,
GraphQLSemanticNullable,
} 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: new GraphQLSemanticNullable(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',
},
});
});
});
45 changes: 44 additions & 1 deletion src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import {
isListType,
isNonNullType,
isObjectType,
isSemanticNonNullType,
isSemanticNullableType,
} from '../type/definition';
import {
SchemaMetaFieldDef,
Expand Down Expand Up @@ -115,6 +117,7 @@ export interface ExecutionContext {
typeResolver: GraphQLTypeResolver<any, any>;
subscribeFieldResolver: GraphQLFieldResolver<any, any>;
errors: Array<GraphQLError>;
errorPropagation: boolean;
}

/**
Expand Down Expand Up @@ -152,6 +155,12 @@ 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.
*
* @experimental
*/
errorPropagation?: boolean;
}

/**
Expand Down Expand Up @@ -286,6 +295,7 @@ export function buildExecutionContext(
fieldResolver,
typeResolver,
subscribeFieldResolver,
errorPropagation,
} = args;

let operation: OperationDefinitionNode | undefined;
Expand Down Expand Up @@ -347,6 +357,7 @@ export function buildExecutionContext(
typeResolver: typeResolver ?? defaultTypeResolver,
subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
errors: [],
errorPropagation: errorPropagation ?? true,
};
}

Expand Down Expand Up @@ -585,6 +596,7 @@ export function buildResolveInfo(
rootValue: exeContext.rootValue,
operation: exeContext.operation,
variableValues: exeContext.variableValues,
errorPropagation: exeContext.errorPropagation,
};
}

Expand All @@ -595,7 +607,7 @@ function handleFieldError(
): null {
// If the field type is non-nullable, then it is resolved without any
// protection from errors, however it still properly locates the error.
if (isNonNullType(returnType)) {
if (exeContext.errorPropagation && isNonNullType(returnType)) {
throw error;
}

Expand Down Expand Up @@ -658,6 +670,37 @@ function completeValue(
return completed;
}

// If field type is SemanticNonNull, complete for inner type, and throw field error
// if result is null and an error doesn't exist.
if (isSemanticNonNullType(returnType)) {
const completed = completeValue(
exeContext,
returnType.ofType,
fieldNodes,
info,
path,
result,
);
if (completed === null) {
throw new Error(
`Cannot return null for semantic-non-nullable field ${info.parentType.name}.${info.fieldName}.`,
);
}
return completed;
}

// If field type is SemanticNullable, complete for inner type
if (isSemanticNullableType(returnType)) {
return completeValue(
exeContext,
returnType.ofType,
fieldNodes,
info,
path,
result,
);
}

// If result value is null or undefined then return null.
if (result == null) {
return null;
Expand Down
8 changes: 8 additions & 0 deletions src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ export interface GraphQLArgs {
operationName?: Maybe<string>;
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
/**
* Set to `false` to disable error propagation. Experimental.
*
* @experimental
*/
errorPropagation?: boolean;
}

export function graphql(args: GraphQLArgs): Promise<ExecutionResult> {
Expand Down Expand Up @@ -106,6 +112,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
operationName,
fieldResolver,
typeResolver,
errorPropagation,
} = args;

// Validate Schema
Expand Down Expand Up @@ -138,5 +145,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
operationName,
fieldResolver,
typeResolver,
errorPropagation,
});
}
Loading
Loading