Skip to content

Commit

Permalink
Cleanup and code coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Feb 12, 2025
1 parent 7121006 commit 4cf86ed
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 85 deletions.
26 changes: 0 additions & 26 deletions src/execution/__tests__/semantic-nullability-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,32 +165,6 @@ describe('Execute: Handles Semantic Nullability', () => {
});
});

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',
Expand Down
114 changes: 103 additions & 11 deletions src/type/__tests__/introspection-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ describe('Introspection', () => {
ofType: null,
},
},
defaultValue: 'AUTO',
defaultValue: 'TRADITIONAL',
},
],
type: {
Expand Down Expand Up @@ -667,21 +667,11 @@ describe('Introspection', () => {
inputFields: null,
interfaces: null,
enumValues: [
{
name: 'AUTO',
isDeprecated: false,
deprecationReason: null,
},
{
name: 'TRADITIONAL',
isDeprecated: false,
deprecationReason: null,
},
{
name: 'SEMANTIC',
isDeprecated: false,
deprecationReason: null,
},
{
name: 'FULL',
isDeprecated: false,
Expand Down Expand Up @@ -1804,4 +1794,106 @@ describe('Introspection', () => {
});
expect(result).to.not.have.property('errors');
});

describe('semantic nullability', () => {
it('casts semantic-non-null types to nullable types in traditional mode', () => {
const schema = buildSchema(`
@SemanticNullability
type Query {
someField: String!
someField2: String
someField3: String?
}
`);

const source = getIntrospectionQuery({
nullability: 'TRADITIONAL',
});

const result = graphqlSync({ schema, source });
// @ts-expect-error
const queryType = result.data?.__schema?.types.find(
(t) => t.name === 'Query',
);
const defaults = {
args: [],
deprecationReason: null,
description: null,
isDeprecated: false,
};
expect(queryType?.fields).to.deep.equal([
{
name: 'someField',
...defaults,
type: {
kind: 'NON_NULL',
name: null,
ofType: { kind: 'SCALAR', name: 'String', ofType: null },
},
},
{
name: 'someField2',
...defaults,
type: { kind: 'SCALAR', name: 'String', ofType: null },
},
{
name: 'someField3',
...defaults,
type: { kind: 'SCALAR', name: 'String', ofType: null },
},
]);
});

it('returns semantic-non-null types in full mode', () => {
const schema = buildSchema(`
@SemanticNullability
type Query {
someField: String!
someField2: String
someField3: String?
}
`);

const source = getIntrospectionQuery({
nullability: 'FULL',
});

const result = graphqlSync({ schema, source });
// @ts-expect-error
const queryType = result.data?.__schema?.types.find(
(t) => t.name === 'Query',
);
const defaults = {
args: [],
deprecationReason: null,
description: null,
isDeprecated: false,
};
expect(queryType?.fields).to.deep.equal([
{
name: 'someField',
...defaults,
type: {
kind: 'NON_NULL',
name: null,
ofType: { kind: 'SCALAR', name: 'String', ofType: null },
},
},
{
name: 'someField2',
...defaults,
type: {
kind: 'SEMANTIC_NON_NULL',
name: null,
ofType: { kind: 'SCALAR', name: 'String', ofType: null },
},
},
{
name: 'someField3',
...defaults,
type: { kind: 'SCALAR', name: 'String', ofType: null },
},
]);
});
});
});
11 changes: 11 additions & 0 deletions src/type/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,17 @@ export const GraphQLSkipDirective: GraphQLDirective = new GraphQLDirective({
},
});

/**
* Used to indicate that the nullability of the document will be parsed as semantic-non-null types.
*/
export const GraphQLSemanticNullabilityDirective: GraphQLDirective =
new GraphQLDirective({
name: 'SemanticNullability',
description:
'Indicates that the nullability of the document will be parsed as semantic-non-null types.',
locations: [DirectiveLocation.SCHEMA],
});

/**
* Constant string used for default reason for a deprecation.
*/
Expand Down
47 changes: 14 additions & 33 deletions src/type/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,36 +206,23 @@ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({
},
});

// TODO: rename enum and options
enum TypeNullability {
AUTO = 'AUTO',
TRADITIONAL = 'TRADITIONAL',
SEMANTIC = 'SEMANTIC',
FULL = 'FULL',
}

// TODO: rename
export const __TypeNullability: GraphQLEnumType = new GraphQLEnumType({
name: '__TypeNullability',
description: 'TODO',
description:
'This represents the type of nullability we want to return as part of the introspection.',
values: {
AUTO: {
value: TypeNullability.AUTO,
description:
'Determines nullability mode based on errorPropagation mode.',
},
TRADITIONAL: {
value: TypeNullability.TRADITIONAL,
description: 'Turn semantic-non-null types into nullable types.',
},
SEMANTIC: {
value: TypeNullability.SEMANTIC,
description: 'Turn non-null types into semantic-non-null types.',
},
FULL: {
value: TypeNullability.FULL,
description:
'Render the true nullability in the schema; be prepared for new types of nullability in future!',
description: 'Allow for returning semantic-non-null types.',
},
},
});
Expand Down Expand Up @@ -408,22 +395,11 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
args: {
nullability: {
type: new GraphQLNonNull(__TypeNullability),
defaultValue: TypeNullability.AUTO,
defaultValue: TypeNullability.TRADITIONAL,
},
},
resolve: (field, { nullability }, _context, info) => {
if (nullability === TypeNullability.FULL) {
return field.type;
}

const mode =
nullability === TypeNullability.AUTO
? info.errorPropagation
? TypeNullability.TRADITIONAL
: TypeNullability.SEMANTIC
: nullability;
return convertOutputTypeToNullabilityMode(field.type, mode);
},
resolve: (field, { nullability }, _context) =>
convertOutputTypeToNullabilityMode(field.type, nullability),
},
isDeprecated: {
type: new GraphQLNonNull(GraphQLBoolean),
Expand All @@ -436,10 +412,9 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
} as GraphQLFieldConfigMap<GraphQLField<unknown, unknown>, unknown>),
});

// TODO: move this elsewhere, rename, memoize
function convertOutputTypeToNullabilityMode(
type: GraphQLType,
mode: TypeNullability.TRADITIONAL | TypeNullability.SEMANTIC,
mode: TypeNullability,
): GraphQLType {
if (mode === TypeNullability.TRADITIONAL) {
if (isNonNullType(type)) {
Expand All @@ -455,7 +430,12 @@ function convertOutputTypeToNullabilityMode(
}
return type;
}
if (isNonNullType(type) || isSemanticNonNullType(type)) {

if (isNonNullType(type)) {
return new GraphQLNonNull(
convertOutputTypeToNullabilityMode(type.ofType, mode),
);
} else if (isSemanticNonNullType(type)) {
return new GraphQLSemanticNonNull(
convertOutputTypeToNullabilityMode(type.ofType, mode),
);
Expand All @@ -464,6 +444,7 @@ function convertOutputTypeToNullabilityMode(
convertOutputTypeToNullabilityMode(type.ofType, mode),
);
}

return type;
}

Expand Down
41 changes: 41 additions & 0 deletions src/utilities/__tests__/buildClientSchema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
assertEnumType,
GraphQLEnumType,
GraphQLObjectType,
GraphQLSemanticNonNull,
} from '../../type/definition';
import {
GraphQLBoolean,
Expand Down Expand Up @@ -983,4 +984,44 @@ describe('Type System: build schema from introspection', () => {
);
});
});

describe('SemanticNullability', () => {
it('should build a clinet schema with semantic-non-null types', () => {
const sdl = `
@SemanticNullability
type Query {
foo: String
bar: String?
}
`;
const schema = buildSchema(sdl, { assumeValid: true });
const introspection = introspectionFromSchema(schema, {
nullability: 'FULL',
});

const clientSchema = buildClientSchema(introspection);
// expect(printSchema(clientSchema)).to.equal(sdl);

const defaults = {
args: [],
astNode: undefined,
deprecationReason: null,
description: null,
extensions: {},
resolve: undefined,
subscribe: undefined,
};
expect(clientSchema.getType('Query')).to.deep.include({
name: 'Query',
_fields: {
foo: {
...defaults,
name: 'foo',
type: new GraphQLSemanticNonNull(GraphQLString),
},
bar: { ...defaults, name: 'bar', type: GraphQLString },
},
});
});
});
});
16 changes: 5 additions & 11 deletions src/utilities/__tests__/printSchema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,7 @@ describe('Type System Printer', () => {
name: String!
description: String
args(includeDeprecated: Boolean = false): [__InputValue!]!
type(nullability: __TypeNullability! = AUTO): __Type!
type(nullability: __TypeNullability! = TRADITIONAL): __Type!
isDeprecated: Boolean!
deprecationReason: String
}
Expand All @@ -803,20 +803,14 @@ describe('Type System Printer', () => {
deprecationReason: String
}
"""TODO"""
"""
This represents the type of nullability we want to return as part of the introspection.
"""
enum __TypeNullability {
"""Determines nullability mode based on errorPropagation mode."""
AUTO
"""Turn semantic-non-null types into nullable types."""
TRADITIONAL
"""Turn non-null types into semantic-non-null types."""
SEMANTIC
"""
Render the true nullability in the schema; be prepared for new types of nullability in future!
"""
"""Allow for returning semantic-non-null types."""
FULL
}
Expand Down
1 change: 1 addition & 0 deletions src/utilities/buildClientSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export function buildClientSchema(
const nullableType = getType(nullableRef);
return new GraphQLNonNull(assertNullableType(nullableType));
}

if (typeRef.kind === TypeKind.SEMANTIC_NON_NULL) {
const nullableRef = typeRef.ofType;
if (!nullableRef) {
Expand Down
Loading

0 comments on commit 4cf86ed

Please sign in to comment.