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 inheritResolversFromInterfaces option #720

Merged
merged 4 commits into from
Apr 10, 2018
Merged
Show file tree
Hide file tree
Changes from 3 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: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* Fix typo in schema-directive.md deprecated example[PR #706](https://github.com/apollographql/graphql-tools/pull/706)
* 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)
* Add `inheritResolversFromInterfaces` to `makeExecutableSchema` and `addResolveFunctionsToSchema` [PR #720](https://github.com/apollographql/graphql-tools/pull/720)

### v2.23.0

Expand Down
6 changes: 6 additions & 0 deletions docs/source/generate-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ const jsSchema = makeExecutableSchema({
logger, // optional
allowUndefinedInResolve = false, // optional
resolverValidationOptions = {}, // optional
directiveResolvers = null, // optional
schemaDirectives = null, // optional
parseOptions = {}, // optional
inheritResolversFromInterfaces = false // optional
});
```

Expand All @@ -357,3 +361,5 @@ const jsSchema = makeExecutableSchema({
- `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.

- `inheritResolversFromInterfaces` GraphQL Objects that implement interfaces will inherit missing resolvers from their interface types defined in the `resolvers` object.
21 changes: 16 additions & 5 deletions docs/source/resolvers.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Keep in mind that GraphQL resolvers can return [promises](https://developer.mozi

In order to respond to queries, a schema needs to have resolve functions for all fields. Resolve functions cannot be included in the GraphQL schema language, so they must be added separately. This collection of functions is called the "resolver map".

The `resolverMap` object should have a map of resolvers for each relevant GraphQL Object Type. The following is an example of a valid `resolverMap` object:
The `resolverMap` object (`IResolvers`) should have a map of resolvers for each relevant GraphQL Object Type. The following is an example of a valid `resolverMap` object:

```js
const resolverMap = {
Expand Down Expand Up @@ -143,15 +143,16 @@ const resolverMap = {
In addition to using a resolver map with `makeExecutableSchema`, you can use it with any GraphQL.js schema by importing the following function from `graphql-tools`:

<h3 id="addResolveFunctionsToSchema" title="addResolveFunctionsToSchema">
addResolveFunctionsToSchema(schema, resolverMap)
addResolveFunctionsToSchema({ schema, resolvers, resolverValidationOptions?, inheritResolversFromInterfaces? })
</h3>

`addResolveFunctionsToSchema` takes two arguments, a GraphQLSchema and a resolver map, and modifies the schema in place by attaching the resolvers to the relevant types.
`addResolveFunctionsToSchema` takes an options object of `IAddResolveFunctionsToSchemaOptions` and modifies the schema in place by attaching the resolvers to the relevant types.


```js
import { addResolveFunctionsToSchema } from 'graphql-tools';

const resolverMap = {
const resolvers = {
RootQuery: {
author(obj, { name }, context){
console.log("RootQuery called with context " +
Expand All @@ -161,7 +162,17 @@ const resolverMap = {
},
};

addResolveFunctionsToSchema(schema, resolverMap);
addResolveFunctionsToSchema({ schema, resolvers });
```

The `IAddResolveFunctionsToSchemaOptions` object has 4 properties that are described in [`makeExecutableSchema`](/docs/graphql-tools/generate-schema.html#makeExecutableSchema).
```ts
export interface IAddResolveFunctionsToSchemaOptions {
schema: GraphQLSchema;
resolvers: IResolvers;
resolverValidationOptions?: IResolverValidationOptions;
inheritResolversFromInterfaces?: boolean;
}
```

<h3 id="addSchemaLevelResolveFunction" title="addSchemaLevelResolveFunction">
Expand Down
8 changes: 8 additions & 0 deletions src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export interface IResolverValidationOptions {
allowResolversNotInSchema?: boolean;
}

export interface IAddResolveFunctionsToSchemaOptions {
schema: GraphQLSchema;
resolvers: IResolvers;
resolverValidationOptions?: IResolverValidationOptions;
inheritResolversFromInterfaces?: boolean;
}

export interface IResolverOptions<TSource = any, TContext = any> {
resolve?: IFieldResolver<TSource, TContext>;
subscribe?: IFieldResolver<TSource, TContext>;
Expand Down Expand Up @@ -85,6 +92,7 @@ export interface IExecutableSchemaDefinition<TContext = any> {
directiveResolvers?: IDirectiveResolvers<any, TContext>;
schemaDirectives?: { [name: string]: typeof SchemaDirectiveVisitor };
parseOptions?: GraphQLParseOptions;
inheritResolversFromInterfaces?: boolean;
}

export type IFieldIteratorFn = (
Expand Down
69 changes: 58 additions & 11 deletions src/schemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
IDirectiveResolvers,
UnitOrList,
GraphQLParseOptions,
IAddResolveFunctionsToSchemaOptions,
} from './Interfaces';

import { SchemaDirectiveVisitor } from './schemaVisitor';
Expand Down Expand Up @@ -69,6 +70,7 @@ function _generateSchema(
allowUndefinedInResolve: boolean,
resolverValidationOptions: IResolverValidationOptions,
parseOptions: GraphQLParseOptions,
inheritResolversFromInterfaces: boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This boolean argument is fine because _generateSchema is private to this module (and not exported like addResolveFunctionsToSchema).

) {
if (typeof resolverValidationOptions !== 'object') {
throw new SchemaError(
Expand All @@ -92,7 +94,7 @@ function _generateSchema(

const schema = buildSchemaFromTypeDefinitions(typeDefinitions, parseOptions);

addResolveFunctionsToSchema(schema, resolvers, resolverValidationOptions);
addResolveFunctionsToSchema({ schema, resolvers, resolverValidationOptions, inheritResolversFromInterfaces });

assertResolveFunctionsPresent(schema, resolverValidationOptions);

Expand All @@ -117,6 +119,7 @@ function makeExecutableSchema<TContext = any>({
directiveResolvers = null,
schemaDirectives = null,
parseOptions = {},
inheritResolversFromInterfaces = false
}: IExecutableSchemaDefinition<TContext>) {
const jsSchema = _generateSchema(
typeDefs,
Expand All @@ -125,6 +128,7 @@ function makeExecutableSchema<TContext = any>({
allowUndefinedInResolve,
resolverValidationOptions,
parseOptions,
inheritResolversFromInterfaces
);
if (typeof resolvers['__schema'] === 'function') {
// TODO a bit of a hack now, better rewrite generateSchema to attach it there.
Expand Down Expand Up @@ -385,16 +389,35 @@ function getFieldsForType(type: GraphQLType): GraphQLFieldMap<any, any> {
}

function addResolveFunctionsToSchema(
schema: GraphQLSchema,
resolveFunctions: IResolvers,
resolverValidationOptions: IResolverValidationOptions = {},
) {
options: IAddResolveFunctionsToSchemaOptions|GraphQLSchema,
legacyInputResolvers?: IResolvers,
legacyInputValidationOptions?: IResolverValidationOptions) {
if (options instanceof GraphQLSchema) {
console.warn('addResolveFunctionsToSchema has a new api with more options see "IAddResolveFunctionsToSchemaOptions"');
options = {
schema: options,
resolvers: legacyInputResolvers,
resolverValidationOptions: legacyInputValidationOptions
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

}

const {
schema,
resolvers: inputResolvers,
resolverValidationOptions = {},
inheritResolversFromInterfaces = false
} = options;

const {
allowResolversNotInSchema = false,
requireResolversForResolveType,
} = resolverValidationOptions;

Object.keys(resolveFunctions).forEach(typeName => {
const resolvers = inheritResolversFromInterfaces
? extendResolversFromInterfaces(schema, inputResolvers)
: inputResolvers;

Object.keys(resolvers).forEach(typeName => {
const type = schema.getType(typeName);
if (!type && typeName !== '__schema') {
if (allowResolversNotInSchema) {
Expand All @@ -406,15 +429,15 @@ function addResolveFunctionsToSchema(
);
}

Object.keys(resolveFunctions[typeName]).forEach(fieldName => {
Object.keys(resolvers[typeName]).forEach(fieldName => {
if (fieldName.startsWith('__')) {
// this is for isTypeOf and resolveType and all the other stuff.
type[fieldName.substring(2)] = resolveFunctions[typeName][fieldName];
type[fieldName.substring(2)] = resolvers[typeName][fieldName];
return;
}

if (type instanceof GraphQLScalarType) {
type[fieldName] = resolveFunctions[typeName][fieldName];
type[fieldName] = resolvers[typeName][fieldName];
return;
}

Expand All @@ -426,10 +449,11 @@ function addResolveFunctionsToSchema(
}

type.getValue(fieldName)['value'] =
resolveFunctions[typeName][fieldName];
resolvers[typeName][fieldName];
return;
}

// object type
const fields = getFieldsForType(type);
if (!fields) {
if (allowResolversNotInSchema) {
Expand All @@ -451,7 +475,7 @@ function addResolveFunctionsToSchema(
);
}
const field = fields[fieldName];
const fieldResolve = resolveFunctions[typeName][fieldName];
const fieldResolve = resolvers[typeName][fieldName];
if (typeof fieldResolve === 'function') {
// for convenience. Allows shorter syntax in resolver definition file
setFieldProperties(field, { resolve: fieldResolve });
Expand All @@ -469,6 +493,29 @@ function addResolveFunctionsToSchema(
checkForResolveTypeResolver(schema, requireResolversForResolveType);
}

function extendResolversFromInterfaces(schema: GraphQLSchema, resolvers: IResolvers) {
const typeNames = Object.keys({
...schema.getTypeMap(),
...resolvers
});

const extendedResolvers: IResolvers = {};
typeNames.forEach((typeName) => {
const typeResolvers = resolvers[typeName];
const type = schema.getType(typeName);
if (type instanceof GraphQLObjectType) {
const interfaceResolvers = type.getInterfaces().map((iFace) => resolvers[iFace.name]);
extendedResolvers[typeName] = Object.assign({}, ...interfaceResolvers, typeResolvers);
} else {
if (typeResolvers) {
extendedResolvers[typeName] = typeResolvers;
}
}
});

return extendedResolvers;
}

// 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())
Expand Down
114 changes: 114 additions & 0 deletions src/test/testSchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2571,6 +2571,120 @@ describe('interfaces', () => {
});
});

describe('interface resolver inheritance', () => {
it('copies resolvers from the interfaces', async () => {
const testSchemaWithInterfaceResolvers = `
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
type Query {
user: User!
}
schema {
query: Query
}
`;
const user = { id: 1, name: 'Ada', type: 'User' };
const resolvers = {
Node: {
__resolveType: ({ type }: { type: string }) => type,
id: ({ id }: { id: number }) => `Node:${id}`,
},
User: {
name: ({ name }: { name: string}) => `User:${name}`
},
Query: {
user: () => user
}
};
const schema = makeExecutableSchema({
typeDefs: testSchemaWithInterfaceResolvers,
resolvers,
inheritResolversFromInterfaces: true,
resolverValidationOptions: { requireResolversForAllFields: true, requireResolversForResolveType: true }
});
const query = `{ user { id name } }`;
const response = await graphql(schema, query);
assert.deepEqual(response, {
data: {
user: {
id: `Node:1`,
name: `User:Ada`
}
}
});
});

it('respects interface order and existing resolvers', async () => {
const testSchemaWithInterfaceResolvers = `
interface Node {
id: ID!
}
interface Person {
id: ID!
name: String!
}
type Replicant implements Node, Person {
id: ID!
name: String!
}
type Cyborg implements Person, Node {
id: ID!
name: String!
}
type Query {
cyborg: Cyborg!
replicant: Replicant!
}
schema {
query: Query
}
`;
const cyborg = { id: 1, name: 'Alex Murphy', type: 'Cyborg' };
const replicant = { id: 2, name: 'Rachael Tyrell', type: 'Replicant' };
const resolvers = {
Node: {
__resolveType: ({ type }: { type: string }) => type,
id: ({ id }: { id: number }) => `Node:${id}`,
},
Person: {
__resolveType: ({ type }: { type: string }) => type,
id: ({ id }: { id: number }) => `Person:${id}`,
name: ({ name }: { name: string}) => `Person:${name}`
},
Query: {
cyborg: () => cyborg,
replicant: () => replicant,
}
};
const schema = makeExecutableSchema({
parseOptions: { allowLegacySDLImplementsInterfaces: true },
typeDefs: testSchemaWithInterfaceResolvers,
resolvers,
inheritResolversFromInterfaces: true,
resolverValidationOptions: { requireResolversForAllFields: true, requireResolversForResolveType: true }
});
const query = `{ cyborg { id name } replicant { id name }}`;
const response = await graphql(schema, query);
assert.deepEqual(response, {
data: {
cyborg: {
id: `Node:1`,
name: `Person:Alex Murphy`
},
replicant: {
id: `Person:2`,
name: `Person:Rachael Tyrell`
}
}
});
});
});

describe('unions', () => {
const testSchemaWithUnions = `
type Post {
Expand Down