Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add directives support to Interfaces #744

Merged
merged 3 commits into from
Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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