8000 Semantic nullability document directive by twof · Pull Request #4271 · graphql/graphql-js · GitHub
[go: up one dir, main page]

Skip to content

Semantic nullability document directive #4271

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

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 1 commit
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
Prev Previous commit
Next Next commit
Handle isNonNullType
  • Loading branch information
benjie committed Sep 20, 2024
commit 16a2114f3e203cef115aaf944d11ea37605fd6db
23 changes: 22 additions & 1 deletion src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
isLeafType,
isListType,
isNonNullType,
isSemanticNonNullType,
isObjectType,
} from '../type/definition';
import {
Expand Down Expand Up @@ -115,6 +116,7 @@ export interface ExecutionContext {
typeResolver: GraphQLTypeResolver<any, any>;
subscribeFieldResolver: GraphQLFieldResolver<any, any>;
errors: Array<GraphQLError>;
errorPropagation: boolean;
}

/**
Expand Down Expand Up @@ -595,7 +597,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 +660,25 @@ function completeValue(
return completed;
}

// If field type is SemanticNonNull, complete for inner type, and throw field error
// if result is null.
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 result value is null or undefined then return null.
if (result == null) {
return null;
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export {
GraphQLInputObjectType,
GraphQLList,
GraphQLNonNull,
GraphQLSemanticNonNull,
// Standard GraphQL Scalars
specifiedScalarTypes,
GraphQLInt,
Expand Down Expand Up @@ -95,6 +96,7 @@ export {
isInputObjectType,
isListType,
isNonNullType,
isSemanticNonNullType,
isInputType,
isOutputType,
isLeafType,
Expand All @@ -120,6 +122,7 @@ export {
assertInputObjectType,
assertListType,
assertNonNullType,
assertSemanticNonNullType,
assertInputType,
assertOutputType,
assertLeafType,
Expand Down Expand Up @@ -287,6 +290,7 @@ export type {
NamedTypeNode,
ListTypeNode,
NonNullTypeNode,
SemanticNonNullTypeNode,
TypeSystemDefinitionNode,
SchemaDefinitionNode,
OperationTypeDefinitionNode,
Expand Down Expand Up @@ -480,6 +484,7 @@ export type {
IntrospectionNamedTypeRef,
IntrospectionListTypeRef,
IntrospectionNonNullTypeRef,
IntrospectionSemanticNonNullTypeRef,
IntrospectionField,
IntrospectionInputValue,
IntrospectionEnumValue,
Expand Down
6 changes: 6 additions & 0 deletions src/language/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,12 @@ export interface NonNullTypeNode {
readonly type: NamedTypeNode | ListTypeNode;
}

export interface SemanticNonNullTypeNode {
readonly kind: Kind.SEMANTIC_NON_NULL_TYPE;
readonly loc?: Location;
readonly type: NamedTypeNode | ListTypeNode;
}

/** Type System Definition */

export type TypeSystemDefinitionNode =
Expand Down
1 change: 1 addition & 0 deletions src/language/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type {
NamedTypeNode,
ListTypeNode,
NonNullTypeNode,
SemanticNonNullTypeNode,
TypeSystemDefinitionNode,
SchemaDefinitionNode,
OperationTypeDefinitionNode,
Expand Down
1 change: 1 addition & 0 deletions src/language/kinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ enum Kind {
NAMED_TYPE = 'NamedType',
LIST_TYPE = 'ListType',
NON_NULL_TYPE = 'NonNullType',
SEMANTIC_NON_NULL_TYPE = 'SemanticNonNullType',

/** Type System Definitions */
SCHEMA_DEFINITION = 'SchemaDefinition',
Expand Down
42 changes: 42 additions & 0 deletions src/type/__tests__/predicate-test.ts
10000
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
assertListType,
assertNamedType,
assertNonNullType,
assertSemanticNonNullType,
assertNullableType,
assertObjectType,
assertOutputType,
Expand All @@ -33,6 +34,7 @@ import {
GraphQLInterfaceType,
GraphQLList,
GraphQLNonNull,
GraphQLSemanticNonNull,
GraphQLObjectType,
GraphQLScalarType,
GraphQLUnionType,
Expand All @@ -46,6 +48,7 @@ import {
isListType,
isNamedType,
isNonNullType,
isSemanticNonNullType,
isNullableType,
isObjectType,
isOutputType,
Expand Down Expand Up @@ -298,6 +301,45 @@ describe('Type predicates', () => {
expect(() =>
assertNonNullType(new GraphQLList(new GraphQLNonNull(ObjectType))),
).to.throw();
expect(isNonNullType(new GraphQLSemanticNonNull(ObjectType))).to.equal(
false,
);
expect(() =>
assertNonNullType(new GraphQLSemanticNonNull(ObjectType)),
).to.throw();
});
});

describe('isSemanticNonNullType', () => {
it('returns true for a semantic-non-null wrapped type', () => {
expect(
isSemanticNonNullType(new GraphQLSemanticNonNull(ObjectType)),
).to.equal(true);
expect(() =>
assertSemanticNonNullType(new GraphQLSemanticNonNull(ObjectType)),
).to.not.throw();
});

it('returns false for an unwrapped type', () => {
expect(isSemanticNonNullType(ObjectType)).to.equal(false);
expect(() => assertSemanticNonNullType(ObjectType)).to.throw();
});

it('returns false for a not non-null wrapped type', () => {
expect(
isSemanticNonNullType(
new GraphQLList(new GraphQLSemanticNonNull(ObjectType)),
),
).to.equal(false);
expect(() =>
assertSemanticNonNullType(
new GraphQLList(new GraphQLSemanticNonNull(ObjectType)),
),
).to.throw();
expect(isNonNullType(new GraphQLNonNull(ObjectType))).to.equal(false);
expect(() =>
assertNonNullType(new GraphQLNonNull(ObjectType)),
).to.throw();
});
});

Expand Down
6 changes: 5 additions & 1 deletion src/type/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@ export function isInputType(type: unknown): type is GraphQLInputType {
isScalarType(type) ||
isEnumType(type) ||
isInputObjectType(type) ||
(isWrappingType(type) && isInputType(type.ofType))
(!isSemanticNonNullType(type) &&
isWrappingType(type) &&
isInputType(type.ofType))
);
}

Expand Down Expand Up @@ -1167,6 +1169,7 @@ export interface GraphQLArgument {
}

export function isRequiredArgument(arg: GraphQLArgument): boolean {
// Note: input types cannot be SemanticNonNull
return isNonNullType(arg.type) && arg.defaultValue === undefined;
}

Expand Down Expand Up @@ -1858,6 +1861,7 @@ export interface GraphQLInputField {
}

export function isRequiredInputField(field: GraphQLInputField): boolean {
// Note: input types cannot be SemanticNonNull
return isNonNullType(field.type) && field.defaultValue === undefined;
}

Expand Down
3 changes: 3 additions & 0 deletions src/type/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export {
isInputObjectType,
isListType,
isNonNullType,
isSemanticNonNullType,
isInputType,
isOutputType,
isLeafType,
Expand All @@ -43,6 +44,7 @@ export {
assertInputObjectType,
assertListType,
assertNonNullType,
assertSemanticNonNullType,
assertInputType,
assertOutputType,
assertLeafType,
Expand All @@ -64,6 +66,7 @@ export {
// Type Wrappers
GraphQLList,
GraphQLNonNull,
GraphQLSemanticNonNull,
} from './definition';

export type {
Expand Down
7 changes: 6 additions & 1 deletion src/type/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { print } from '../language/printer';

import { astFromValue } from '../utilities/astFromValue';

import type {
import {
GraphQLEnumValue,
GraphQLField,
GraphQLFieldConfigMap,
GraphQLInputField,
GraphQLNamedType,
GraphQLType,
isSemanticNonNullType,
} from './definition';
import {
GraphQLEnumType,
Expand Down Expand Up @@ -237,6 +238,9 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({
if (isNonNullType(type)) {
return TypeKind.NON_NULL;
}
if (isSemanticNonNullType(type)) {
return TypeKind.SEMANTIC_NON_NULL;
}
/* c8 ignore next 3 */
// Not reachable, all possible types have been considered)
invariant(false, `Unexpected type: "${inspect(type)}".`);
Expand Down Expand Up @@ -452,6 +456,7 @@ enum TypeKind {
INPUT_OBJECT = 'INPUT_OBJECT',
LIST = 'LIST',
NON_NULL = 'NON_NULL',
SEMANTIC_NON_NULL = 'SEMANTIC_NON_NULL',
}
export { TypeKind };

Expand Down
1 change: 1 addition & 0 deletions src/utilities/astFromValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function astFromValue(
value: unknown,
type: GraphQLInputType,
): Maybe<ValueNode> {
// Note: input types cannot be SemanticNonNull
if (isNonNullType(type)) {
const astValue = astFromValue(value, type.ofType);
if (astValue?.kind === Kind.NULL) {
Expand Down
5 changes: 5 additions & 0 deletions src/utilities/extendSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
isInterfaceType,
isListType,
isNonNullType,
isSemanticNonNullType,
isObjectType,
isScalarType,
isUnionType,
Expand Down Expand Up @@ -225,6 +226,10 @@ export function extendSchemaImpl(
// @ts-expect-error
return new GraphQLNonNull(replaceType(type.ofType));
}
if (isSemanticNonNullType(type)) {
// @ts-expect-error
return new GraphQLSemanticNonNull(replaceType(type.ofType));
}
// @ts-expect-error FIXME
return replaceNamedType(type);
}
Expand Down
20 changes: 19 additions & 1 deletion src/utilities/findBreakingChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
isListType,
isNamedType,
isNonNullType,
isSemanticNonNullType,
isObjectType,
isRequiredArgument,
isRequiredInputField,
Expand Down Expand Up @@ -458,7 +459,10 @@ function isChangeSafeForObjectOrInterfaceField(
)) ||
// moving from nullable to non-null of the same underlying type is safe
(isNonNullType(newType) &&
isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) ||
// moving from nullable to semantic-non-null of the same underlying type is safe
(isSemanticNonNullType(newType) &&
isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) ||
);
}

Expand All @@ -470,11 +474,25 @@ function isChangeSafeForObjectOrInterfaceField(
);
}

if (isSemanticNonNullType(oldType)) {
return (
// if they're both semantic-non-null, make sure the underlying types are compatible
(isSemanticNonNullType(newType) &&
isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType)) ||
// moving from semantic-non-null to non-null of the same underlying type is safe
isNonNullType(newType) &&
isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType)
);
}

return (
// if they're both named types, see if their names are equivalent
(isNamedType(newType) && oldType.name === newType.name) ||
// moving from nullable to non-null of the same underlying type is safe
(isNonNullType(newType) &&
isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) ||
// moving from nullable to semantic-non-null of the same underlying type is safe
(isSemanticNonNullType(newType) &&
isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
);
}
Expand Down
7 changes: 7 additions & 0 deletions src/utilities/getIntrospectionQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,13 @@ export interface IntrospectionNonNullTypeRef<
readonly ofType: T;
}

export interface IntrospectionSemanticNonNullTypeRef<
T extends IntrospectionTypeRef = IntrospectionTypeRef,
> {
readonly kind: 'SEMANTIC_NON_NULL';
readonly ofType: T;
}

export type IntrospectionTypeRef =
| IntrospectionNamedTypeRef
| IntrospectionListTypeRef
Expand Down
1 change: 1 addition & 0 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type {
IntrospectionNamedTypeRef,
IntrospectionListTypeRef,
IntrospectionNonNullTypeRef,
IntrospectionSemanticNonNullTypeRef,
IntrospectionField,
IntrospectionInputValue,
IntrospectionEnumValue,
Expand Down
4 changes: 4 additions & 0 deletions src/utilities/lexicographicSortSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
isInterfaceType,
isListType,
isNonNullType,
isSemanticNonNullType,
isObjectType,
isScalarType,
isUnionType,
Expand Down Expand Up @@ -62,6 +63,9 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema {
} else if (isNonNullType(type)) {
// @ts-expect-error
return new GraphQLNonNull(replaceType(type.ofType));
} else if (isSemanticNonNullType(type)) {
// @ts-expect-error
return new GraphQLSemanticNonNull(replaceType(type.ofType));
}
// @ts-expect-error FIXME: TS Conversion
return replaceNamedType<GraphQLNamedType>(type);
Expand Down
Loading
0