Skip to content

Commit c470ffc

Browse files
reconbotbenjamn
authored andcommitted
Add inheritResolversFromInterfaces option (#720)
This adds a `inheritResolversFromInterfaces` option to `addResolveFunctionsToSchema` and `makeExecutableSchema`. Later on we'll want to support interfaces implementing other interfaces: graphql/graphql-spec#295
1 parent 7eb631d commit c470ffc

File tree

6 files changed

+203
-17
lines changed

6 files changed

+203
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* Fix typo in schema-directive.md deprecated example[PR #706](https://github.com/apollographql/graphql-tools/pull/706)
77
* Fix timezone bug in test for @date directive [PR #686](https://github.com/apollographql/graphql-tools/pull/686)
88
* Expose `defaultMergedResolver` from stitching [PR #685](https://github.com/apollographql/graphql-tools/pull/685)
9-
109
* Add `requireResolversForResolveType` to resolver validation options [PR #698](https://github.com/apollographql/graphql-tools/pull/698)
10+
* Add `inheritResolversFromInterfaces` to `makeExecutableSchema` and `addResolveFunctionsToSchema` [PR #720](https://github.com/apollographql/graphql-tools/pull/720)
1111

1212
### v2.23.0
1313

docs/source/generate-schema.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ const jsSchema = makeExecutableSchema({
334334
logger, // optional
335335
allowUndefinedInResolve = false, // optional
336336
resolverValidationOptions = {}, // optional
337+
directiveResolvers = null, // optional
338+
schemaDirectives = null, // optional
339+
parseOptions = {}, // optional
340+
inheritResolversFromInterfaces = false // optional
337341
});
338342
```
339343

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

359363
- `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.
364+
365+
- `inheritResolversFromInterfaces` GraphQL Objects that implement interfaces will inherit missing resolvers from their interface types defined in the `resolvers` object.

docs/source/resolvers.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Keep in mind that GraphQL resolvers can return [promises](https://developer.mozi
1212

1313
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".
1414

15-
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:
15+
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:
1616

1717
```js
1818
const resolverMap = {
@@ -143,15 +143,16 @@ const resolverMap = {
143143
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`:
144144

145145
<h3 id="addResolveFunctionsToSchema" title="addResolveFunctionsToSchema">
146-
addResolveFunctionsToSchema(schema, resolverMap)
146+
addResolveFunctionsToSchema({ schema, resolvers, resolverValidationOptions?, inheritResolversFromInterfaces? })
147147
</h3>
148148

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

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

154-
const resolverMap = {
155+
const resolvers = {
155156
RootQuery: {
156157
author(obj, { name }, context){
157158
console.log("RootQuery called with context " +
@@ -161,7 +162,17 @@ const resolverMap = {
161162
},
162163
};
163164

164-
addResolveFunctionsToSchema(schema, resolverMap);
165+
addResolveFunctionsToSchema({ schema, resolvers });
166+
```
167+
168+
The `IAddResolveFunctionsToSchemaOptions` object has 4 properties that are described in [`makeExecutableSchema`](/docs/graphql-tools/generate-schema.html#makeExecutableSchema).
169+
```ts
170+
export interface IAddResolveFunctionsToSchemaOptions {
171+
schema: GraphQLSchema;
172+
resolvers: IResolvers;
173+
resolverValidationOptions?: IResolverValidationOptions;
174+
inheritResolversFromInterfaces?: boolean;
175+
}
165176
```
166177

167178
<h3 id="addSchemaLevelResolveFunction" title="addSchemaLevelResolveFunction">

src/Interfaces.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ export interface IResolverValidationOptions {
2626
allowResolversNotInSchema?: boolean;
2727
}
2828

29+
export interface IAddResolveFunctionsToSchemaOptions {
30+
schema: GraphQLSchema;
31+
resolvers: IResolvers;
32+
resolverValidationOptions?: IResolverValidationOptions;
33+
inheritResolversFromInterfaces?: boolean;
34+
}
35+
2936
export interface IResolverOptions<TSource = any, TContext = any> {
3037
resolve?: IFieldResolver<TSource, TContext>;
3138
subscribe?: IFieldResolver<TSource, TContext>;
@@ -85,6 +92,7 @@ export interface IExecutableSchemaDefinition<TContext = any> {
8592
directiveResolvers?: IDirectiveResolvers<any, TContext>;
8693
schemaDirectives?: { [name: string]: typeof SchemaDirectiveVisitor };
8794
parseOptions?: GraphQLParseOptions;
95+
inheritResolversFromInterfaces?: boolean;
8896
}
8997

9098
export type IFieldIteratorFn = (

src/schemaGenerator.ts

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
IDirectiveResolvers,
4343
UnitOrList,
4444
GraphQLParseOptions,
45+
IAddResolveFunctionsToSchemaOptions,
4546
} from './Interfaces';
4647

4748
import { SchemaDirectiveVisitor } from './schemaVisitor';
@@ -69,6 +70,7 @@ function _generateSchema(
6970
allowUndefinedInResolve: boolean,
7071
resolverValidationOptions: IResolverValidationOptions,
7172
parseOptions: GraphQLParseOptions,
73+
inheritResolversFromInterfaces: boolean
7274
) {
7375
if (typeof resolverValidationOptions !== 'object') {
7476
throw new SchemaError(
@@ -92,7 +94,7 @@ function _generateSchema(
9294

9395
const schema = buildSchemaFromTypeDefinitions(typeDefinitions, parseOptions);
9496

95-
addResolveFunctionsToSchema(schema, resolvers, resolverValidationOptions);
97+
addResolveFunctionsToSchema({ schema, resolvers, resolverValidationOptions, inheritResolversFromInterfaces });
9698

9799
assertResolveFunctionsPresent(schema, resolverValidationOptions);
98100

@@ -117,6 +119,7 @@ function makeExecutableSchema<TContext = any>({
117119
directiveResolvers = null,
118120
schemaDirectives = null,
119121
parseOptions = {},
122+
inheritResolversFromInterfaces = false
120123
}: IExecutableSchemaDefinition<TContext>) {
121124
const jsSchema = _generateSchema(
122125
typeDefs,
@@ -125,6 +128,7 @@ function makeExecutableSchema<TContext = any>({
125128
allowUndefinedInResolve,
126129
resolverValidationOptions,
127130
parseOptions,
131+
inheritResolversFromInterfaces
128132
);
129133
if (typeof resolvers['__schema'] === 'function') {
130134
// TODO a bit of a hack now, better rewrite generateSchema to attach it there.
@@ -385,16 +389,35 @@ function getFieldsForType(type: GraphQLType): GraphQLFieldMap<any, any> {
385389
}
386390

387391
function addResolveFunctionsToSchema(
388-
schema: GraphQLSchema,
389-
resolveFunctions: IResolvers,
390-
resolverValidationOptions: IResolverValidationOptions = {},
391-
) {
392+
options: IAddResolveFunctionsToSchemaOptions|GraphQLSchema,
393+
legacyInputResolvers?: IResolvers,
394+
legacyInputValidationOptions?: IResolverValidationOptions) {
395+
if (options instanceof GraphQLSchema) {
396+
console.warn('addResolveFunctionsToSchema has a new api with more options see "IAddResolveFunctionsToSchemaOptions"');
397+
options = {
398+
schema: options,
399+
resolvers: legacyInputResolvers,
400+
resolverValidationOptions: legacyInputValidationOptions
401+
};
402+
}
403+
404+
const {
405+
schema,
406+
resolvers: inputResolvers,
407+
resolverValidationOptions = {},
408+
inheritResolversFromInterfaces = false
409+
} = options;
410+
392411
const {
393412
allowResolversNotInSchema = false,
394413
requireResolversForResolveType,
395414
} = resolverValidationOptions;
396415

397-
Object.keys(resolveFunctions).forEach(typeName => {
416+
const resolvers = inheritResolversFromInterfaces
417+
? extendResolversFromInterfaces(schema, inputResolvers)
418+
: inputResolvers;
419+
420+
Object.keys(resolvers).forEach(typeName => {
398421
const type = schema.getType(typeName);
399422
if (!type && typeName !== '__schema') {
400423
if (allowResolversNotInSchema) {
@@ -406,15 +429,15 @@ function addResolveFunctionsToSchema(
406429
);
407430
}
408431

409-
Object.keys(resolveFunctions[typeName]).forEach(fieldName => {
432+
Object.keys(resolvers[typeName]).forEach(fieldName => {
410433
if (fieldName.startsWith('__')) {
411434
// this is for isTypeOf and resolveType and all the other stuff.
412-
type[fieldName.substring(2)] = resolveFunctions[typeName][fieldName];
435+
type[fieldName.substring(2)] = resolvers[typeName][fieldName];
413436
return;
414437
}
415438

416439
if (type instanceof GraphQLScalarType) {
417-
type[fieldName] = resolveFunctions[typeName][fieldName];
440+
type[fieldName] = resolvers[typeName][fieldName];
418441
return;
419442
}
420443

@@ -426,10 +449,11 @@ function addResolveFunctionsToSchema(
426449
}
427450

428451
type.getValue(fieldName)['value'] =
429-
resolveFunctions[typeName][fieldName];
452+
resolvers[typeName][fieldName];
430453
return;
431454
}
432455

456+
// object type
433457
const fields = getFieldsForType(type);
434458
if (!fields) {
435459
if (allowResolversNotInSchema) {
@@ -451,7 +475,7 @@ function addResolveFunctionsToSchema(
451475
);
452476
}
453477
const field = fields[fieldName];
454-
const fieldResolve = resolveFunctions[typeName][fieldName];
478+
const fieldResolve = resolvers[typeName][fieldName];
455479
if (typeof fieldResolve === 'function') {
456480
// for convenience. Allows shorter syntax in resolver definition file
457481
setFieldProperties(field, { resolve: fieldResolve });
@@ -469,6 +493,29 @@ function addResolveFunctionsToSchema(
469493
checkForResolveTypeResolver(schema, requireResolversForResolveType);
470494
}
471495

496+
function extendResolversFromInterfaces(schema: GraphQLSchema, resolvers: IResolvers) {
497+
const typeNames = Object.keys({
498+
...schema.getTypeMap(),
499+
...resolvers
500+
});
501+
502+
const extendedResolvers: IResolvers = {};
503+
typeNames.forEach((typeName) => {
504+
const typeResolvers = resolvers[typeName];
505+
const type = schema.getType(typeName);
506+
if (type instanceof GraphQLObjectType) {
507+
const interfaceResolvers = type.getInterfaces().map((iFace) => resolvers[iFace.name]);
508+
extendedResolvers[typeName] = Object.assign({}, ...interfaceResolvers, typeResolvers);
509+
} else {
510+
if (typeResolvers) {
511+
extendedResolvers[typeName] = typeResolvers;
512+
}
513+
}
514+
});
515+
516+
return extendedResolvers;
517+
}
518+
472519
// If we have any union or interface types throw if no there is no resolveType or isTypeOf resolvers
473520
function checkForResolveTypeResolver(schema: GraphQLSchema, requireResolversForResolveType?: boolean) {
474521
Object.keys(schema.getTypeMap())

src/test/testSchemaGenerator.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2571,6 +2571,120 @@ describe('interfaces', () => {
25712571
});
25722572
});
25732573

2574+
describe('interface resolver inheritance', () => {
2575+
it('copies resolvers from the interfaces', async () => {
2576+
const testSchemaWithInterfaceResolvers = `
2577+
interface Node {
2578+
id: ID!
2579+
}
2580+
type User implements Node {
2581+
id: ID!
2582+
name: String!
2583+
}
2584+
type Query {
2585+
user: User!
2586+
}
2587+
schema {
2588+
query: Query
2589+
}
2590+
`;
2591+
const user = { id: 1, name: 'Ada', type: 'User' };
2592+
const resolvers = {
2593+
Node: {
2594+
__resolveType: ({ type }: { type: string }) => type,
2595+
id: ({ id }: { id: number }) => `Node:${id}`,
2596+
},
2597+
User: {
2598+
name: ({ name }: { name: string}) => `User:${name}`
2599+
},
2600+
Query: {
2601+
user: () => user
2602+
}
2603+
};
2604+
const schema = makeExecutableSchema({
2605+
typeDefs: testSchemaWithInterfaceResolvers,
2606+
resolvers,
2607+
inheritResolversFromInterfaces: true,
2608+
resolverValidationOptions: { requireResolversForAllFields: true, requireResolversForResolveType: true }
2609+
});
2610+
const query = `{ user { id name } }`;
2611+
const response = await graphql(schema, query);
2612+
assert.deepEqual(response, {
2613+
data: {
2614+
user: {
2615+
id: `Node:1`,
2616+
name: `User:Ada`
2617+
}
2618+
}
2619+
});
2620+
});
2621+
2622+
it('respects interface order and existing resolvers', async () => {
2623+
const testSchemaWithInterfaceResolvers = `
2624+
interface Node {
2625+
id: ID!
2626+
}
2627+
interface Person {
2628+
id: ID!
2629+
name: String!
2630+
}
2631+
type Replicant implements Node, Person {
2632+
id: ID!
2633+
name: String!
2634+
}
2635+
type Cyborg implements Person, Node {
2636+
id: ID!
2637+
name: String!
2638+
}
2639+
type Query {
2640+
cyborg: Cyborg!
2641+
replicant: Replicant!
2642+
}
2643+
schema {
2644+
query: Query
2645+
}
2646+
`;
2647+
const cyborg = { id: 1, name: 'Alex Murphy', type: 'Cyborg' };
2648+
const replicant = { id: 2, name: 'Rachael Tyrell', type: 'Replicant' };
2649+
const resolvers = {
2650+
Node: {
2651+
__resolveType: ({ type }: { type: string }) => type,
2652+
id: ({ id }: { id: number }) => `Node:${id}`,
2653+
},
2654+
Person: {
2655+
__resolveType: ({ type }: { type: string }) => type,
2656+
id: ({ id }: { id: number }) => `Person:${id}`,
2657+
name: ({ name }: { name: string}) => `Person:${name}`
2658+
},
2659+
Query: {
2660+
cyborg: () => cyborg,
2661+
replicant: () => replicant,
2662+
}
2663+
};
2664+
const schema = makeExecutableSchema({
2665+
parseOptions: { allowLegacySDLImplementsInterfaces: true },
2666+
typeDefs: testSchemaWithInterfaceResolvers,
2667+
resolvers,
2668+
inheritResolversFromInterfaces: true,
2669+
resolverValidationOptions: { requireResolversForAllFields: true, requireResolversForResolveType: true }
2670+
});
2671+
const query = `{ cyborg { id name } replicant { id name }}`;
2672+
const response = await graphql(schema, query);
2673+
assert.deepEqual(response, {
2674+
data: {
2675+
cyborg: {
2676+
id: `Node:1`,
2677+
name: `Person:Alex Murphy`
2678+
},
2679+
replicant: {
2680+
id: `Person:2`,
2681+
name: `Person:Rachael Tyrell`
2682+
}
2683+
}
2684+
});
2685+
});
2686+
});
2687+
25742688
describe('unions', () => {
25752689
const testSchemaWithUnions = `
25762690
type Post {

0 commit comments

Comments
 (0)