diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e8d7b4bce..533be8b2b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/source/generate-schema.md b/docs/source/generate-schema.md index 3818b42d19a..9c0feafb6bc 100644 --- a/docs/source/generate-schema.md +++ b/docs/source/generate-schema.md @@ -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 }); ``` @@ -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. diff --git a/docs/source/resolvers.md b/docs/source/resolvers.md index 206ef6eecf2..4df26f58d24 100644 --- a/docs/source/resolvers.md +++ b/docs/source/resolvers.md @@ -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 = { @@ -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`:

- addResolveFunctionsToSchema(schema, resolverMap) + addResolveFunctionsToSchema({ schema, resolvers, resolverValidationOptions?, inheritResolversFromInterfaces? })

-`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 " + @@ -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; +} ```

diff --git a/src/Interfaces.ts b/src/Interfaces.ts index c0614baba78..c016c26791c 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -26,6 +26,13 @@ export interface IResolverValidationOptions { allowResolversNotInSchema?: boolean; } +export interface IAddResolveFunctionsToSchemaOptions { + schema: GraphQLSchema; + resolvers: IResolvers; + resolverValidationOptions?: IResolverValidationOptions; + inheritResolversFromInterfaces?: boolean; +} + export interface IResolverOptions { resolve?: IFieldResolver; subscribe?: IFieldResolver; @@ -85,6 +92,7 @@ export interface IExecutableSchemaDefinition { directiveResolvers?: IDirectiveResolvers; schemaDirectives?: { [name: string]: typeof SchemaDirectiveVisitor }; parseOptions?: GraphQLParseOptions; + inheritResolversFromInterfaces?: boolean; } export type IFieldIteratorFn = ( diff --git a/src/schemaGenerator.ts b/src/schemaGenerator.ts index 30ff4d4e884..c27b0230048 100644 --- a/src/schemaGenerator.ts +++ b/src/schemaGenerator.ts @@ -42,6 +42,7 @@ import { IDirectiveResolvers, UnitOrList, GraphQLParseOptions, + IAddResolveFunctionsToSchemaOptions, } from './Interfaces'; import { SchemaDirectiveVisitor } from './schemaVisitor'; @@ -69,6 +70,7 @@ function _generateSchema( allowUndefinedInResolve: boolean, resolverValidationOptions: IResolverValidationOptions, parseOptions: GraphQLParseOptions, + inheritResolversFromInterfaces: boolean ) { if (typeof resolverValidationOptions !== 'object') { throw new SchemaError( @@ -92,7 +94,7 @@ function _generateSchema( const schema = buildSchemaFromTypeDefinitions(typeDefinitions, parseOptions); - addResolveFunctionsToSchema(schema, resolvers, resolverValidationOptions); + addResolveFunctionsToSchema({ schema, resolvers, resolverValidationOptions, inheritResolversFromInterfaces }); assertResolveFunctionsPresent(schema, resolverValidationOptions); @@ -117,6 +119,7 @@ function makeExecutableSchema({ directiveResolvers = null, schemaDirectives = null, parseOptions = {}, + inheritResolversFromInterfaces = false }: IExecutableSchemaDefinition) { const jsSchema = _generateSchema( typeDefs, @@ -125,6 +128,7 @@ function makeExecutableSchema({ allowUndefinedInResolve, resolverValidationOptions, parseOptions, + inheritResolversFromInterfaces ); if (typeof resolvers['__schema'] === 'function') { // TODO a bit of a hack now, better rewrite generateSchema to attach it there. @@ -385,16 +389,35 @@ function getFieldsForType(type: GraphQLType): GraphQLFieldMap { } 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 + }; + } + + 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) { @@ -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; } @@ -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) { @@ -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 }); @@ -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()) diff --git a/src/test/testSchemaGenerator.ts b/src/test/testSchemaGenerator.ts index d1c59517eb4..72ccfd5e8f3 100644 --- a/src/test/testSchemaGenerator.ts +++ b/src/test/testSchemaGenerator.ts @@ -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 {