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 ebe2ee5
Show file tree
Hide file tree
Showing 18 changed files with 696 additions and 99 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
116 changes: 105 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,108 @@ 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(
// @ts-expect-error
(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(
// @ts-expect-error
(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
62 changes: 62 additions & 0 deletions src/utilities/__tests__/TypeInfo-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,4 +457,66 @@ describe('visitWithTypeInfo', () => {
['leave', 'SelectionSet', null, 'Human', 'Human'],
]);
});

it('supports traversals of semantic non-null types', () => {
const schema = buildSchema(`
@SemanticNullability
type Query {
id: String!
name: String
something: String?
}
`);

const typeInfo = new TypeInfo(schema);

const visited: Array<any> = [];
const ast = parse('{ id name something }');

visit(
ast,
visitWithTypeInfo(typeInfo, {
enter(node) {
const type = typeInfo.getType();
visited.push([
'enter',
node.kind,
node.kind === 'Name' ? node.value : null,
String(type),
]);
},
leave(node) {
const type = typeInfo.getType();
visited.push([
'leave',
node.kind,
node.kind === 'Name' ? node.value : null,
// TODO: inspect currently returns "String" for a nullable type
String(type),
]);
},
}),
);

expect(visited).to.deep.equal([
['enter', 'Document', null, 'undefined'],
['enter', 'OperationDefinition', null, 'Query'],
['enter', 'SelectionSet', null, 'Query'],
['enter', 'Field', null, 'String!'],
['enter', 'Name', 'id', 'String!'],
['leave', 'Name', 'id', 'String!'],
['leave', 'Field', null, 'String!'],
['enter', 'Field', null, 'String'],
['enter', 'Name', 'name', 'String'],
['leave', 'Name', 'name', 'String'],
['leave', 'Field', null, 'String'],
['enter', 'Field', null, 'String'],
['enter', 'Name', 'something', 'String'],
['leave', 'Name', 'something', 'String'],
['leave', 'Field', null, 'String'],
['leave', 'SelectionSet', null, 'Query'],
['leave', 'OperationDefinition', null, 'Query'],
['leave', 'Document', null, 'undefined'],
]);
});
});
Loading

0 comments on commit ebe2ee5

Please sign in to comment.