diff --git a/CHANGELOG.md b/CHANGELOG.md index a2ef4a8c0..439ff215a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - **Breaking Change**: `AuthChecker` type is now "function or class" - update to `AuthCheckerFn` if the function form is needed in the code - support class-based auth checker, which allows for dependency injection +- allow defining directives for interface types and theirs fields, with inheritance for object types fields (#744) ## v1.1.1 ### Fixes diff --git a/docs/directives.md b/docs/directives.md index c6f41b4c5..0ce809a53 100644 --- a/docs/directives.md +++ b/docs/directives.md @@ -32,7 +32,7 @@ Basically, we declare the usage of directives just like in SDL, with the `@` syn @Directive('@deprecated(reason: "Use newField")') ``` -Currently, you can use the directives only on object types, input types and their fields or fields resolvers, as well as queries, mutations and subscriptions. +Currently, you can use the directives only on object types, input types, interface types and their fields or fields resolvers, as well as queries, mutations and subscriptions. Other locations like scalars, enums, unions or arguments are not yet supported. So the `@Directive` decorator can be placed over the class property/method or over the type class itself, depending on the needs and the placements supported by the implementation: diff --git a/src/schema/definition-node.ts b/src/schema/definition-node.ts index c76a51744..8db5aebc1 100644 --- a/src/schema/definition-node.ts +++ b/src/schema/definition-node.ts @@ -9,6 +9,7 @@ import { parseValue, DocumentNode, parse, + InterfaceTypeDefinitionNode, } from "graphql"; import { InvalidDirectiveError } from "../errors"; @@ -104,6 +105,25 @@ export function getInputValueDefinitionNode( }; } +export function getInterfaceTypeDefinitionNode( + name: string, + directiveMetadata?: DirectiveMetadata[], +): InterfaceTypeDefinitionNode | undefined { + if (!directiveMetadata || !directiveMetadata.length) { + return; + } + + return { + kind: "InterfaceTypeDefinition", + name: { + kind: "Name", + // FIXME: use proper AST representation + value: name, + }, + directives: directiveMetadata.map(getDirectiveNode), + }; +} + export function getDirectiveNode(directive: DirectiveMetadata): DirectiveNode { const { nameOrDefinition, args } = directive; diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index 89d1386eb..c3810f782 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -52,6 +52,7 @@ import { getFieldDefinitionNode, getInputObjectTypeDefinitionNode, getInputValueDefinitionNode, + getInterfaceTypeDefinitionNode, getObjectTypeDefinitionNode, } from "./definition-node"; import { ObjectClassMetadata } from "../metadata/definitions/object-class-metdata"; @@ -392,6 +393,7 @@ export abstract class SchemaGenerator { type: new GraphQLInterfaceType({ name: interfaceType.name, description: interfaceType.description, + astNode: getInterfaceTypeDefinitionNode(interfaceType.name, interfaceType.directives), interfaces: () => { let interfaces = (interfaceType.interfaceClasses || []).map( interfaceClass => @@ -431,19 +433,21 @@ export abstract class SchemaGenerator { (resolver.resolverClassMetadata === undefined || resolver.resolverClassMetadata.isAbstract === false), ); + const type = this.getGraphQLOutputType( + field.target, + field.name, + field.getType(), + field.typeOptions, + ); fieldsMap[field.schemaName] = { - type: this.getGraphQLOutputType( - field.target, - field.name, - field.getType(), - field.typeOptions, - ), + type, args: this.generateHandlerArgs(field.target, field.name, field.params!), resolve: fieldResolverMetadata ? createAdvancedFieldResolver(fieldResolverMetadata) : createBasicFieldResolver(field), description: field.description, deprecationReason: field.deprecationReason, + astNode: getFieldDefinitionNode(field.name, type, field.directives), extensions: { complexity: field.complexity, ...field.extensions, diff --git a/tests/functional/directives.ts b/tests/functional/directives.ts index 6191a4034..75ed4fbb5 100644 --- a/tests/functional/directives.ts +++ b/tests/functional/directives.ts @@ -1,7 +1,12 @@ // tslint:disable:member-ordering import "reflect-metadata"; - -import { GraphQLSchema, graphql, GraphQLInputObjectType, printSchema } from "graphql"; +import { + GraphQLSchema, + graphql, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLObjectType, +} from "graphql"; import { Field, InputType, @@ -14,6 +19,7 @@ import { Mutation, FieldResolver, Subscription, + InterfaceType, } from "../../src"; import { getMetadataStorage } from "../../src/metadata/getMetadataStorage"; import { SchemaDirectiveVisitor } from "graphql-tools"; @@ -108,6 +114,17 @@ describe("Directives", () => { } } + @InterfaceType() + @Directive("foo") + abstract class DirectiveOnInterface { + @Field() + @Directive("bar") + withDirective: string; + } + + @ObjectType({ implements: DirectiveOnInterface }) + class ObjectImplement extends DirectiveOnInterface {} + @Resolver() class SampleResolver { @Query(() => SampleObjectType) @@ -225,8 +242,21 @@ describe("Directives", () => { } } + @Resolver(() => ObjectImplement) + class ObjectImplementResolver { + @Query(() => ObjectImplement) + objectImplentingInterface(): ObjectImplement { + return new ObjectImplement(); + } + } + schema = await buildSchema({ - resolvers: [SampleResolver, SampleObjectTypeResolver, SubSampleResolver], + resolvers: [ + SampleResolver, + SampleObjectTypeResolver, + SubSampleResolver, + ObjectImplementResolver, + ], validate: false, }); @@ -483,6 +513,31 @@ describe("Directives", () => { }); }); }); + + describe("Interface", () => { + it("adds directive to interface", () => { + const interfaceType = schema.getType("DirectiveOnInterface") as GraphQLInterfaceType; + + expect(interfaceType).toHaveProperty("astNode"); + assertValidDirective(interfaceType.astNode, "foo"); + }); + + it("adds field directives to interface fields", async () => { + const fields = (schema.getType("DirectiveOnInterface") as GraphQLInterfaceType).getFields(); + + expect(fields).toHaveProperty("withDirective"); + expect(fields.withDirective).toHaveProperty("astNode"); + assertValidDirective(fields.withDirective.astNode, "bar"); + }); + + it("adds inherited field directives to object type fields while extending interface type class", async () => { + const fields = (schema.getType("ObjectImplement") as GraphQLObjectType).getFields(); + + expect(fields).toHaveProperty("withDirective"); + expect(fields.withDirective).toHaveProperty("astNode"); + assertValidDirective(fields.withDirective.astNode, "bar"); + }); + }); }); describe("errors", () => { diff --git a/tests/helpers/directives/assertValidDirective.ts b/tests/helpers/directives/assertValidDirective.ts index 0d75cfedf..49025debf 100644 --- a/tests/helpers/directives/assertValidDirective.ts +++ b/tests/helpers/directives/assertValidDirective.ts @@ -2,13 +2,19 @@ import { FieldDefinitionNode, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, parseValue, } from "graphql"; import { Maybe } from "../../../src/interfaces/Maybe"; export function assertValidDirective( - astNode: Maybe, + astNode: Maybe< + | FieldDefinitionNode + | InputObjectTypeDefinitionNode + | InputValueDefinitionNode + | InterfaceTypeDefinitionNode + >, name: string, args?: { [key: string]: string }, ): void {