Skip to content

Commit

Permalink
feat(directives): add support for directives on inline @Arg
Browse files Browse the repository at this point in the history
  • Loading branch information
MichalLytek committed Sep 20, 2023
1 parent 818ac67 commit 582a9cb
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- **Breaking Change**: expose shim as a package entry point `type-graphql/shim` (and `/node_modules/type-graphql/build/typings/shim.ts`)
- support defining directives on `@Field` of `@Args`
- support defining directives on inline `@Arg`

## v2.0.0-beta.3

Expand Down
36 changes: 34 additions & 2 deletions docs/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,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, interface types and their fields or fields resolvers, args type fields, as well as queries, mutations and subscriptions. Other locations like scalars, enums, unions or inline arguments are not yet supported.
Currently, you can use the directives only on object types, input types, interface types and their fields or fields resolvers, args type fields, as well as queries, mutations and subscriptions and the inline arguments. Other locations like scalars, enums or unions 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 All @@ -49,11 +49,18 @@ class Bar {
field: string;
}

@ArgsType()
class FooBarArgs {
@Directive('@deprecated(reason: "Not used anymore")')
@Field({ nullable: true })
baz?: string;
}

@Resolver(of => Foo)
class FooBarResolver {
@Directive("@auth(requires: ANY)")
@Query()
foobar(@Arg("baz") baz: string): string {
foobar(@Args() { baz }: FooBarArgs): string {
return "foobar";
}

Expand All @@ -65,6 +72,31 @@ class FooBarResolver {
}
```

In case of inline args using `@Arg` decorator, directives can be placed over the parameter of the class method:

```ts
@Resolver(of => Foo)
class FooBarResolver {
@Query()
foo(
@Directive('@deprecated(reason: "Not used anymore")')
@Arg("foobar", { defaultValue: "foobar" })
foobar: string,
) {
return "foo";
}

@FieldResolver()
bar(
@Directive('@deprecated(reason: "Not used anymore")')
@Arg("foobar", { defaultValue: "foobar" })
foobar: string,
) {
return "bar";
}
}
```

> Note that even as directives are a purely SDL thing, they won't appear in the generated schema definition file. Current implementation of directives in TypeGraphQL is using some crazy workarounds because [`graphql-js` doesn't support setting them by code](https://github.com/graphql/graphql-js/issues/1343) and the built-in `printSchema` utility omits the directives while printing. See [emit schema with custom directives](./emit-schema.md#emit-schema-with-custom-directives) for more info.
Also please note that `@Directive` can only contain a single GraphQL directive name or declaration. If you need to have multiple directives declared, just place multiple decorators:
Expand Down
31 changes: 23 additions & 8 deletions src/decorators/Directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,37 @@ import { SymbolKeysNotSupportedError } from "@/errors";
import { getMetadataStorage } from "@/metadata/getMetadataStorage";
import { type MethodAndPropDecorator } from "./types";

export function Directive(sdl: string): MethodAndPropDecorator & ClassDecorator;
export function Directive(
sdl: string,
): MethodAndPropDecorator & ClassDecorator & ParameterDecorator;
export function Directive(
nameOrDefinition: string,
): MethodDecorator | PropertyDecorator | ClassDecorator {
return (targetOrPrototype, propertyKey, _descriptor) => {
): MethodDecorator | PropertyDecorator | ClassDecorator | ParameterDecorator {
return (
targetOrPrototype: Object,
propertyKey: string | symbol | undefined,
parameterIndexOrDescriptor: number | TypedPropertyDescriptor<Object>,
) => {
const directive = { nameOrDefinition, args: {} };

if (typeof propertyKey === "symbol") {
throw new SymbolKeysNotSupportedError();
}
if (propertyKey) {
getMetadataStorage().collectDirectiveFieldMetadata({
target: targetOrPrototype.constructor,
fieldName: propertyKey,
directive,
});
if (typeof parameterIndexOrDescriptor === "number") {
getMetadataStorage().collectDirectiveArgumentMetadata({
target: targetOrPrototype.constructor,
fieldName: propertyKey,
parameterIndex: parameterIndexOrDescriptor,
directive,
});
} else {
getMetadataStorage().collectDirectiveFieldMetadata({
target: targetOrPrototype.constructor,
fieldName: propertyKey,
directive,
});
}
} else {
getMetadataStorage().collectDirectiveClassMetadata({
target: targetOrPrototype as Function,
Expand Down
7 changes: 7 additions & 0 deletions src/metadata/definitions/directive-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ export interface DirectiveFieldMetadata {
fieldName: string;
directive: DirectiveMetadata;
}

export interface DirectiveArgumentMetadata {
target: Function;
fieldName: string;
parameterIndex: number;
directive: DirectiveMetadata;
}
9 changes: 9 additions & 0 deletions src/metadata/metadata-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type UnionMetadataWithSymbol,
} from "./definitions";
import {
type DirectiveArgumentMetadata,
type DirectiveClassMetadata,
type DirectiveFieldMetadata,
} from "./definitions/directive-metadata";
Expand Down Expand Up @@ -62,6 +63,8 @@ export class MetadataStorage {

fieldDirectives: DirectiveFieldMetadata[] = [];

argumentDirectives: DirectiveArgumentMetadata[] = [];

classExtensions: ExtensionsClassMetadata[] = [];

fieldExtensions: ExtensionsFieldMetadata[] = [];
Expand Down Expand Up @@ -149,6 +152,10 @@ export class MetadataStorage {
this.fieldDirectives.push(definition);
}

collectDirectiveArgumentMetadata(definition: DirectiveArgumentMetadata) {
this.argumentDirectives.push(definition);
}

collectExtensionsClassMetadata(definition: ExtensionsClassMetadata) {
this.classExtensions.push(definition);
}
Expand All @@ -160,6 +167,7 @@ export class MetadataStorage {
build(options: SchemaGeneratorOptions) {
this.classDirectives.reverse();
this.fieldDirectives.reverse();
this.argumentDirectives.reverse();
this.classExtensions.reverse();
this.fieldExtensions.reverse();

Expand Down Expand Up @@ -192,6 +200,7 @@ export class MetadataStorage {
this.middlewares = [];
this.classDirectives = [];
this.fieldDirectives = [];
this.argumentDirectives = [];
this.classExtensions = [];
this.fieldExtensions = [];

Expand Down
26 changes: 18 additions & 8 deletions src/schema/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,19 +686,29 @@ export abstract class SchemaGenerator {
): GraphQLFieldConfigArgumentMap {
return params!.reduce<GraphQLFieldConfigArgumentMap>((args, param) => {
if (param.kind === "arg") {
const type = this.getGraphQLInputType(
target,
propertyName,
param.getType(),
param.typeOptions,
param.index,
param.name,
);
const argDirectives = getMetadataStorage()
.argumentDirectives.filter(
it =>
it.target === target &&
it.fieldName === propertyName &&
it.parameterIndex === param.index,
)
.map(it => it.directive);
// eslint-disable-next-line no-param-reassign
args[param.name] = {
description: param.description,
type: this.getGraphQLInputType(
target,
propertyName,
param.getType(),
param.typeOptions,
param.index,
param.name,
),
type,
defaultValue: param.typeOptions.defaultValue,
deprecationReason: param.deprecationReason,
astNode: getInputValueDefinitionNode(param.name, type, argDirectives),
};
} else if (param.kind === "args") {
const argumentType = getMetadataStorage().argumentTypes.find(
Expand Down
44 changes: 43 additions & 1 deletion tests/functional/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ describe("Directives", () => {
});
});

describe("on Query field argument", () => {
describe("on Query field argument using @Args", () => {
let schema: GraphQLSchema;
beforeAll(async () => {
@ArgsType()
Expand Down Expand Up @@ -445,6 +445,48 @@ describe("Directives", () => {
});
});

describe("on Query field argument using @Arg", () => {
let schema: GraphQLSchema;
beforeAll(async () => {
@Resolver()
class SampleResolver {
@Query()
sampleQuery(
@Arg("sampleArgument")
@Directive("@test")
sampleArgument: string,
): string {
return sampleArgument;
}
}

schema = await buildSchema({
resolvers: [SampleResolver],
directives: [testDirective],
validate: false,
});
schema = testDirectiveTransformer(schema);
});

it("should properly emit directive in AST", () => {
const sampleQueryArgTypeInfo = (schema.getType("Query") as GraphQLObjectType).getFields()
.sampleQuery.args[0];

expect(() => {
assertValidDirective(sampleQueryArgTypeInfo.astNode, "test");
}).not.toThrow();
});

it("should properly apply directive mapper", async () => {
const sampleQueryArgTypeInfo = (schema.getType("Query") as GraphQLObjectType).getFields()
.sampleQuery.args[0];

expect(sampleQueryArgTypeInfo.extensions).toMatchObject({
TypeGraphQL: { isMappedByDirective: true },
});
});
});

describe("on Mutation", () => {
let schema: GraphQLSchema;
beforeAll(async () => {
Expand Down

0 comments on commit 582a9cb

Please sign in to comment.