Skip to content

Commit

Permalink
feat(directives): add support for directives on interface types (#744)
Browse files Browse the repository at this point in the history
Co-authored-by: Michał Lytek <michal.wojciech.lytek@gmail.com>
  • Loading branch information
Superd22 and MichalLytek committed Jan 4, 2021
1 parent 8d8bbae commit d508613
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
20 changes: 20 additions & 0 deletions src/schema/definition-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
parseValue,
DocumentNode,
parse,
InterfaceTypeDefinitionNode,
} from "graphql";

import { InvalidDirectiveError } from "../errors";
Expand Down Expand Up @@ -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;

Expand Down
16 changes: 10 additions & 6 deletions src/schema/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
getFieldDefinitionNode,
getInputObjectTypeDefinitionNode,
getInputValueDefinitionNode,
getInterfaceTypeDefinitionNode,
getObjectTypeDefinitionNode,
} from "./definition-node";
import { ObjectClassMetadata } from "../metadata/definitions/object-class-metdata";
Expand Down Expand Up @@ -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<GraphQLInterfaceType>(
interfaceClass =>
Expand Down Expand Up @@ -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,
Expand Down
61 changes: 58 additions & 3 deletions tests/functional/directives.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -14,6 +19,7 @@ import {
Mutation,
FieldResolver,
Subscription,
InterfaceType,
} from "../../src";
import { getMetadataStorage } from "../../src/metadata/getMetadataStorage";
import { SchemaDirectiveVisitor } from "graphql-tools";
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
});

Expand Down Expand Up @@ -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", () => {
Expand Down
8 changes: 7 additions & 1 deletion tests/helpers/directives/assertValidDirective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import {
FieldDefinitionNode,
InputObjectTypeDefinitionNode,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
parseValue,
} from "graphql";

import { Maybe } from "../../../src/interfaces/Maybe";

export function assertValidDirective(
astNode: Maybe<FieldDefinitionNode | InputObjectTypeDefinitionNode | InputValueDefinitionNode>,
astNode: Maybe<
| FieldDefinitionNode
| InputObjectTypeDefinitionNode
| InputValueDefinitionNode
| InterfaceTypeDefinitionNode
>,
name: string,
args?: { [key: string]: string },
): void {
Expand Down

0 comments on commit d508613

Please sign in to comment.