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

Add resolver validation options for Interface and Union types #698

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* Fix timezone bug in test for @date directive [PR #686](https://github.com/apollographql/graphql-tools/pull/686)
* Expose `defaultMergedResolver` from stitching [PR #685](https://github.com/apollographql/graphql-tools/pull/685)

* Add `requireResolversForResolveType` to resolver validation options [PR #698](https://github.com/apollographql/graphql-tools/pull/698)

### v2.23.0

* The `SchemaDirectiveVisitor` abstraction for implementing reusable schema `@directive`s has landed. Read our [blog post](https://dev-blog.apollodata.com/reusable-graphql-schema-directives-131fb3a177d1) about this new functionality, and/or check out the [documentation](https://www.apollographql.com/docs/graphql-tools/schema-directives.html) for even more examples. [PR #640](https://github.com/apollographql/graphql-tools/pull/640)
Expand Down
13 changes: 7 additions & 6 deletions docs/source/generate-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,12 +347,13 @@ const jsSchema = makeExecutableSchema({

- `allowUndefinedInResolve` is an optional argument, which is `true` by default. When set to `false`, causes your resolve functions to throw errors if they return undefined, which can help make debugging easier.

- `resolverValidationOptions` is an optional argument which accepts an object of the following shape: `{ requireResolversForArgs, requireResolversForNonScalar, requireResolversForAllFields, allowResolversNotInSchema }`.
- `resolverValidationOptions` is an optional argument which accepts an `ResolverValidationOptions` object which has the following boolean properties:
- `requireResolversForArgs` will cause `makeExecutableSchema` to throw an error if no resolve function is defined for a field that has arguments.

- `requireResolversForArgs` will cause `makeExecutableSchema` to throw an error if no resolve function is defined for a field that has arguments.
- `requireResolversForNonScalar` will cause `makeExecutableSchema` to throw an error if a non-scalar field has no resolver defined. By default, both of these are true, which can help catch errors faster.

- `requireResolversForNonScalar` will cause `makeExecutableSchema` to throw an error if a non-scalar field has no resolver defined. By default, both of these are true, which can help catch errors faster. To get the normal behavior of GraphQL, set both of them to `false`.

- `requireResolversForAllFields` asserts that *all* fields have a valid resolve function.
- `requireResolversForAllFields` asserts that *all* fields have a valid resolve function.

- `allowResolversNotInSchema` turns off the functionality which throws errors when resolvers are found which are not present in the schema. Defaults to `false`, to help catch common errors.
- `requireResolversForResolveType` will require a `resolveType()` method for Interface and Union types. This can be passed in with the field resolvers as `__resolveType()`. False to disable the warning.

- `allowResolversNotInSchema` turns off the functionality which throws errors when resolvers are found which are not present in the schema. Defaults to `false`, to help catch common errors.
1 change: 1 addition & 0 deletions src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface IResolverValidationOptions {
requireResolversForArgs?: boolean;
requireResolversForNonScalar?: boolean;
requireResolversForAllFields?: boolean;
requireResolversForResolveType?: boolean;
allowResolversNotInSchema?: boolean;
}

Expand Down
30 changes: 28 additions & 2 deletions src/schemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
GraphQLType,
GraphQLInterfaceType,
GraphQLFieldMap,
GraphQLUnionType,
} from 'graphql';

import {
Expand Down Expand Up @@ -388,7 +389,10 @@ function addResolveFunctionsToSchema(
resolveFunctions: IResolvers,
resolverValidationOptions: IResolverValidationOptions = {},
) {
const { allowResolversNotInSchema = false } = resolverValidationOptions;
const {
allowResolversNotInSchema = false,
requireResolversForResolveType,
} = resolverValidationOptions;

Object.keys(resolveFunctions).forEach(typeName => {
const type = schema.getType(typeName);
Expand All @@ -405,7 +409,6 @@ function addResolveFunctionsToSchema(
Object.keys(resolveFunctions[typeName]).forEach(fieldName => {
if (fieldName.startsWith('__')) {
// this is for isTypeOf and resolveType and all the other stuff.
// TODO require resolveType for unions and interfaces.
type[fieldName.substring(2)] = resolveFunctions[typeName][fieldName];
return;
}
Expand Down Expand Up @@ -462,6 +465,29 @@ function addResolveFunctionsToSchema(
}
});
});

checkForResolveTypeResolver(schema, requireResolversForResolveType);
}

// If we have any union or interface types throw if no there is no resolveType or isTypeOf resolvers
function checkForResolveTypeResolver(schema: GraphQLSchema, requireResolversForResolveType?: boolean) {
Object.keys(schema.getTypeMap())
.map(typeName => schema.getType(typeName))
.forEach((type: GraphQLUnionType | GraphQLInterfaceType) => {
if (!(type instanceof GraphQLUnionType || type instanceof GraphQLInterfaceType)) {
return;
}
if (!type.resolveType) {
if (requireResolversForResolveType === false) {
return;
}
if (requireResolversForResolveType === true) {
throw new SchemaError(`Type "${type.name}" is missing a "resolveType" resolver`);
}
// tslint:disable-next-line:max-line-length
console.warn(`Type "${type.name}" is missing a "resolveType" resolver. Pass false into "resolverValidationOptions.requireResolversForResolveType" to disable this warning.`);
}
});
}

function setFieldProperties(
Expand Down
157 changes: 157 additions & 0 deletions src/test/testSchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2495,3 +2495,160 @@ describe('can specify lexical parser options', () => {
});
}
});

describe('interfaces', () => {
const testSchemaWithInterfaces = `
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
type Query {
node: Node!
user: User!
}
schema {
query: Query
}
`;
const user = { id: 1, type: 'User', name: 'Kim' };
const queryResolver = {
node: () => user,
user: () => user,
};
const query = `query {
node { id __typename }
user { id name }
}`;

if (process.env.GRAPHQL_VERSION !== '^0.11') {
it('throws if there is no interface resolveType resolver', async () => {
const resolvers = {
Query: queryResolver,
};
try {
makeExecutableSchema({
typeDefs: testSchemaWithInterfaces,
resolvers,
resolverValidationOptions: { requireResolversForResolveType: true },
});
} catch (error) {
assert.equal(
error.message,
'Type "Node" is missing a "resolveType" resolver',
);
return;
}
throw new Error('Should have had an error.');
});
}
it('does not throw if there is an interface resolveType resolver', async () => {
const resolvers = {
Query: queryResolver,
Node: {
__resolveType: ({ type }: { type: String }) => type,
},
};
const schema = makeExecutableSchema({
typeDefs: testSchemaWithInterfaces,
resolvers,
resolverValidationOptions: { requireResolversForResolveType: true },
});
const response = await graphql(schema, query);
assert.isUndefined(response.errors);
});
it('does not warn if requireResolversForResolveType is disabled and there are missing resolvers', async () => {
const resolvers = {
Query: queryResolver,
};
makeExecutableSchema({
typeDefs: testSchemaWithInterfaces,
resolvers,
resolverValidationOptions: { requireResolversForResolveType: false },
});
});
});

describe('unions', () => {
const testSchemaWithUnions = `
type Post {
title: String!
}
type Page {
title: String!
}
union Displayable = Page | Post
type Query {
page: Page!
post: Post!
displayable: [Displayable!]!
}
schema {
query: Query
}
`;
const post = { title: 'I am a post', type: 'Post' };
const page = { title: 'I am a page', type: 'Page' };
const queryResolver = {
page: () => page,
post: () => post,
displayable: () => [post, page],
};
const query = `query {
post { title }
page { title }
displayable {
... on Post { title }
... on Page { title }
}
}`;

if (process.env.GRAPHQL_VERSION !== '^0.11') {
it('throws if there is no union resolveType resolver', async () => {
const resolvers = {
Query: queryResolver,
};
try {
makeExecutableSchema({
typeDefs: testSchemaWithUnions,
resolvers,
resolverValidationOptions: { requireResolversForResolveType: true },
});
} catch (error) {
assert.equal(
error.message,
'Type "Displayable" is missing a "resolveType" resolver',
);
return;
}
throw new Error('Should have had an error.');
});
}
it('does not throw if there is a resolveType resolver', async () => {
const resolvers = {
Query: queryResolver,
Displayable: {
__resolveType: ({ type }: { type: String }) => type,
},
};
const schema = makeExecutableSchema({
typeDefs: testSchemaWithUnions,
resolvers,
resolverValidationOptions: { requireResolversForResolveType: true },
});
const response = await graphql(schema, query);
assert.isUndefined(response.errors);
});
it('does not warn if requireResolversForResolveType is disabled', async () => {
const resolvers = {
Query: queryResolver,
};
makeExecutableSchema({
typeDefs: testSchemaWithUnions,
resolvers,
resolverValidationOptions: { requireResolversForResolveType: false },
});
});
});