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/src/Interfaces.ts b/src/Interfaces.ts index c0614baba78..e742e6fa0ef 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -85,6 +85,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..052473ca0fa 100644 --- a/src/schemaGenerator.ts +++ b/src/schemaGenerator.ts @@ -69,6 +69,7 @@ function _generateSchema( allowUndefinedInResolve: boolean, resolverValidationOptions: IResolverValidationOptions, parseOptions: GraphQLParseOptions, + inheritResolversFromInterfaces: boolean ) { if (typeof resolverValidationOptions !== 'object') { throw new SchemaError( @@ -92,7 +93,7 @@ function _generateSchema( const schema = buildSchemaFromTypeDefinitions(typeDefinitions, parseOptions); - addResolveFunctionsToSchema(schema, resolvers, resolverValidationOptions); + addResolveFunctionsToSchema(schema, resolvers, resolverValidationOptions, inheritResolversFromInterfaces); assertResolveFunctionsPresent(schema, resolverValidationOptions); @@ -117,6 +118,7 @@ function makeExecutableSchema({ directiveResolvers = null, schemaDirectives = null, parseOptions = {}, + inheritResolversFromInterfaces = false }: IExecutableSchemaDefinition) { const jsSchema = _generateSchema( typeDefs, @@ -125,6 +127,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. @@ -386,14 +389,19 @@ function getFieldsForType(type: GraphQLType): GraphQLFieldMap { function addResolveFunctionsToSchema( schema: GraphQLSchema, - resolveFunctions: IResolvers, + inputResolveFunctions: IResolvers, resolverValidationOptions: IResolverValidationOptions = {}, + inheritResolversFromInterfaces: boolean = false ) { const { allowResolversNotInSchema = false, requireResolversForResolveType, } = resolverValidationOptions; + const resolveFunctions = inheritResolversFromInterfaces + ? extendResolversFromInterfaces(schema, inputResolveFunctions) + : inputResolveFunctions; + Object.keys(resolveFunctions).forEach(typeName => { const type = schema.getType(typeName); if (!type && typeName !== '__schema') { @@ -430,6 +438,7 @@ function addResolveFunctionsToSchema( return; } + // object type const fields = getFieldsForType(type); if (!fields) { if (allowResolversNotInSchema) { @@ -469,6 +478,30 @@ function addResolveFunctionsToSchema( checkForResolveTypeResolver(schema, requireResolversForResolveType); } +function extendResolversFromInterfaces(schema: GraphQLSchema, resolvers: IResolvers) { + const typeNames = new Set([ + ...Object.keys(schema.getTypeMap()), + ...Object.keys(resolvers) + ]); + + const extendedResolvers: IResolvers = {}; + typeNames.forEach((typeName) => { + const typeResolvers = resolvers[typeName]; + const type = schema.getType(typeName); + if (!(type instanceof GraphQLObjectType)) { + if (typeResolvers) { + extendedResolvers[typeName] = typeResolvers; + } + return; + } + + const interfaceResolvers = (type as GraphQLObjectType).getInterfaces().map((iFace) => resolvers[iFace.name]); + extendedResolvers[typeName] = Object.assign({}, ...interfaceResolvers, 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..545a2964773 100644 --- a/src/test/testSchemaGenerator.ts +++ b/src/test/testSchemaGenerator.ts @@ -2571,6 +2571,119 @@ 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({ + 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 {