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

Custom parameter decorators #329

Merged
merged 4 commits into from May 6, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
@@ -1,7 +1,10 @@
# Changelog and release notes

<!-- ## Unreleased -->
## Unreleased
<!-- here goes all the unreleased changes descriptions -->
## Features
- add support for creating custom parameter decorators (#329)

## v0.17.3
### Features
- update packages `semver` to `^6.0.0` and `graphql-subscriptions` to `^1.1.0`
Expand Down
2 changes: 1 addition & 1 deletion dev.js
Expand Up @@ -5,7 +5,7 @@ require("ts-node/register/transpile-only");
// require("./examples/enums-and-unions/index.ts");
// require("./examples/generic-types/index.ts");
// require("./examples/interfaces-inheritance/index.ts");
// require("./examples/middlewares/index.ts");
// require("./examples/middlewares-custom-decorators/index.ts");
// require("./examples/query-complexity/index.ts");
// require("./examples/redis-subscriptions/index.ts");
// require("./examples/resolvers-inheritance/index.ts");
Expand Down
104 changes: 104 additions & 0 deletions docs/custom-decorators.md
@@ -0,0 +1,104 @@
---
title: Custom decorators
---

Custom decorators are a great way to reduce the boilerplate and reuse some common logic between different resolvers. TypeGraphQL supports two kinds of custom decorators - method and parameter.

## Method decorators

Using [middlewares](middlewares.md) allows to reuse some code between resolvers. To further reduce the boilerplate and have a nicer API, we can create our own custom method decorators.

They work in the same way as the [reusable middleware function](middlewares.md#reusable-middleware), however, in this case we need to call `createMethodDecorator` helper function with our middleware logic and return its value:

```typescript
export function ValidateArgs(schema: JoiSchema) {
return createMethodDecorator(async ({ args }, next) => {
// here place your middleware code that uses custom decorator arguments

// e.g. validation logic based on schema using joi
await joiValidate(schema, args);
return next();
});
}
```

The usage is then very simple, as we have a custom, descriptive decorator - we just place it above the resolver/field and pass the required arguments to it:

```typescript
@Resolver()
export class RecipeResolver {
@ValidateArgs(MyArgsSchema) // custom decorator
@UseMiddleware(ResolveTime) // explicit middleware
@Query()
randomValue(@Args() { scale }: MyArgs): number {
return Math.random() * scale;
}
}
```

## Parameter decorators

Parameter decorators are just like the custom method decorators or middlewares but with an ability to return some value that will be injected to the method as a parameter. Thanks to this, it reduces the pollution in `context` which was used as a workaround for the communication between reusable middlewares and resolvers.

They might be just a simple data extractor function, that makes our resolver more unit test friendly:

```typescript
function CurrentUser() {
return createParamDecorator<MyContextType>(({ context }) => context.currentUser);
}
```

Or might be a more advanced one that performs some calculations and encapsulates some logic. Compared to middlewares, they allows for a more granular control on executing the code, like calculating fields map based on GraphQL info only when it's really needed (requested by using the `@Fields()` decorator):

```typescript
function Fields(level = 1): ParameterDecorator {
return createParamDecorator(({ info }) => {
const fieldsMap: FieldsMap = {};
// calculate an object with info about requested fields
// based on GraphQL `info` parameter of the resolver and the level parameter
return fieldsMap;
}
}
```

Then we can use our custom param decorators in the resolvers just like the built-in decorators:

```typescript
@Resolver()
export class RecipeResolver {
constructor(private readonly recipesRepository: Repository<Recipe>) {}

@Authorized()
@Mutation(returns => Recipe)
async addRecipe(
@Args() recipeData: AddRecipeInput,
// here we place our custom decorator
// just like the built-in one
@CurrentUser() currentUser: User,
) {
const recipe: Recipe = {
...recipeData,
// and use the data returned from custom decorator in our resolver code
author: currentUser,
};
await this.recipesRepository.save(recipe);
return recipe;
}

@Query(returns => Recipe, { nullable: true })
async recipe(
@Arg("id") id: string,
// our custom decorator that parses the fields from graphql query info
@Fields() fields: FieldsMap,
) {
return await this.recipesRepository.find(id, {
// use the fields map as a select projection to optimize db queries
select: fields,
});
}
}
```

## Example

See how different kinds of custom decorators work in the [custom decorators and middlewares example](https://github.com/19majkel94/type-graphql/tree/master/examples/middlewares-custom-decorators).
2 changes: 1 addition & 1 deletion docs/examples.md
Expand Up @@ -27,7 +27,7 @@ All examples have an `examples.gql` file with sample queries/mutations/subscript
- [Types inheritance](https://github.com/19majkel94/type-graphql/tree/master/examples/interfaces-inheritance)
- [Resolvers inheritance](https://github.com/19majkel94/type-graphql/tree/master/examples/resolvers-inheritance)
- [Generic types](https://github.com/19majkel94/type-graphql/tree/master/examples/generic-types)
- [Middlewares](https://github.com/19majkel94/type-graphql/tree/master/examples/middlewares)
- [Middlewares and Custom Decorators](https://github.com/19majkel94/type-graphql/tree/master/examples/middlewares-custom-decorators)

## 3rd party libs integration

Expand Down
26 changes: 3 additions & 23 deletions docs/middlewares.md
Expand Up @@ -180,28 +180,8 @@ const schema = await buildSchema({

### Custom Decorators

If we want to have a more descriptive and declarative API, we can also create custom decorators. They work in the same way as the reusable middleware function, however, in this case we need to return the `UseMiddleware` decorator function:
If we want to use middlewares with a more descriptive and declarative API, we can also create a custom method decorators. See how to do this in [custom decorators docs](custom-decorators.md#method-decorators).

```typescript
export function ValidateArgs<T extends object>(schema: Schema<T>) {
return UseMiddleware(async ({ args }, next) => {
// here place your validation logic, e.g. based on schema using joi
await joiValidate(schema, args);
return next();
});
}
```

The usage is then very simple, as we have a custom, descriptive decorator - we just place it above the resolver/field and pass the required arguments to the id:
## Example

```typescript
@Resolver()
export class RecipeResolver {
@Query()
@ValidateArgs(MyArgsSchema) // custom decorator
@UseMiddleware(ResolveTime) // explicit middleware
randomValue(@Args() { scale }: MyArgs): number {
return Math.random() * scale;
}
}
```
See how different kinds of middlewares work in the [middlewares and custom decorators example](https://github.com/19majkel94/type-graphql/tree/master/examples/middlewares-custom-decorators).
2 changes: 1 addition & 1 deletion examples/README.md
Expand Up @@ -27,7 +27,7 @@ So if you are looking for examples that are compatible with the version you use,
- [Types inheritance](./interfaces-inheritance)
- [Resolvers inheritance](./resolvers-inheritance)
- [Generic types](./generic-types)
- [Middlewares](./middlewares)
- [Middlewares and custom decorators](./middlewares-custom-decorators)

## 3rd party libs integration

Expand Down
5 changes: 5 additions & 0 deletions examples/middlewares-custom-decorators/context.ts
@@ -0,0 +1,5 @@
import User from "./user";

export interface Context {
currentUser: User;
}
@@ -0,0 +1,6 @@
import { createParamDecorator } from "../../../src";
import { Context } from "../context";

export default function CurrentUser() {
return createParamDecorator<Context>(({ context }) => context.currentUser);
}
@@ -1,11 +1,11 @@
import { plainToClass } from "class-transformer";
import { validate } from "class-validator";
import { ClassType, ArgumentValidationError, UseMiddleware } from "../../../src";
import { ClassType, ArgumentValidationError, createMethodDecorator } from "../../../src";

// sample implementation of custom validation decorator
// this example use `class-validator` however you can plug-in `joi` or any other lib
export function ValidateArgs<T extends object>(type: ClassType<T>) {
return UseMiddleware(async ({ args }, next) => {
return createMethodDecorator(async ({ args }, next) => {
const instance = plainToClass(type, args);
const validationErrors = await validate(instance);
if (validationErrors.length > 0) {
Expand Down
Expand Up @@ -6,6 +6,7 @@ import { buildSchema } from "../../src";
import { RecipeResolver } from "./recipe/recipe.resolver";
import { ResolveTimeMiddleware } from "./middlewares/resolve-time";
import { ErrorLoggerMiddleware } from "./middlewares/error-logger";
import { Context } from "./context";

async function bootstrap() {
// build TypeGraphQL executable schema
Expand All @@ -16,7 +17,18 @@ async function bootstrap() {
});

// Create GraphQL server
const server = new ApolloServer({ schema });
const server = new ApolloServer({
schema,
context: (): Context => {
return {
// example user
currentUser: {
id: 123,
name: "Sample user",
},
};
},
});

// Start the server
const { url } = await server.listen(4000);
Expand Down
Expand Up @@ -2,7 +2,6 @@ import { Service } from "typedi";
import { MiddlewareInterface, NextFn, ResolverData, ArgumentValidationError } from "../../../src";

import { Context } from "../context";
import { Middleware } from "../../../src/interfaces/Middleware";
import { Logger } from "../logger";

@Service()
Expand All @@ -17,7 +16,7 @@ export class ErrorLoggerMiddleware implements MiddlewareInterface<Context> {
message: err.message,
operation: info.operation.operation,
fieldName: info.fieldName,
userName: context.username,
userName: context.currentUser.name,
});
if (!(err instanceof ArgumentValidationError)) {
// hide errors from db like printing sql query
Expand Down
Expand Up @@ -9,8 +9,9 @@ export class LogAccessMiddleware implements MiddlewareInterface<Context> {
constructor(private readonly logger: Logger) {}

async use({ context, info }: ResolverData<Context>, next: NextFn) {
const username: string = context.username || "guest";
this.logger.log(`Logging access: ${username} -> ${info.parentType.name}.${info.fieldName}`);
this.logger.log(
`Logging access: ${context.currentUser.name} -> ${info.parentType.name}.${info.fieldName}`,
);
return next();
}
}
@@ -1,9 +1,11 @@
import { Resolver, Query, Args, UseMiddleware } from "../../../src";
import { Resolver, Query, Args } from "../../../src";

import recipeSamples from "./recipe.samples";
import { Recipe } from "./recipe.type";
import { RecipesArgs } from "./recipe.args";
import { ValidateArgs } from "../decorators/validate-args";
import CurrentUser from "../decorators/current-user";
import User from "../user";

@Resolver(of => Recipe)
export class RecipeResolver {
Expand All @@ -14,7 +16,9 @@ export class RecipeResolver {
async recipes(
@Args({ validate: false }) // disable built-in validation here
options: RecipesArgs,
@CurrentUser() currentUser: User,
): Promise<Recipe[]> {
console.log(`User "${currentUser.name}" queried for recipes!`);
const start = options.skip;
const end = options.skip + options.take;
return await this.items.slice(start, end);
Expand Down
4 changes: 4 additions & 0 deletions examples/middlewares-custom-decorators/user.ts
@@ -0,0 +1,4 @@
export default interface User {
id: number;
name: string;
}
3 changes: 0 additions & 3 deletions examples/middlewares/context.ts

This file was deleted.

8 changes: 8 additions & 0 deletions src/decorators/createMethodDecorator.ts
@@ -0,0 +1,8 @@
import { UseMiddleware } from "./UseMiddleware";
import { MiddlewareFn } from "../interfaces/Middleware";

export function createMethodDecorator<TContextType = {}>(
resolver: MiddlewareFn<TContextType>,
): MethodDecorator {
return UseMiddleware(resolver);
}
20 changes: 20 additions & 0 deletions src/decorators/createParamDecorator.ts
@@ -0,0 +1,20 @@
import { ResolverData } from "../interfaces";
import { getMetadataStorage } from "../metadata/getMetadataStorage";
import { SymbolKeysNotSupportedError } from "../errors";

export function createParamDecorator<TContextType = {}>(
resolver: (resolverData: ResolverData<TContextType>) => any,
): ParameterDecorator {
return (prototype, propertyKey, parameterIndex) => {
if (typeof propertyKey === "symbol") {
throw new SymbolKeysNotSupportedError();
}
getMetadataStorage().collectHandlerParamMetadata({
kind: "custom",
target: prototype.constructor,
methodName: propertyKey,
index: parameterIndex,
resolver,
});
};
}
2 changes: 2 additions & 0 deletions src/decorators/index.ts
Expand Up @@ -2,6 +2,8 @@ export { Arg } from "./Arg";
export { Args } from "./Args";
export { ArgsType } from "./ArgsType";
export { Authorized } from "./Authorized";
export { createParamDecorator } from "./createParamDecorator";
export { createMethodDecorator } from "./createMethodDecorator";
export { Ctx } from "./Ctx";
export { registerEnumType } from "./enums";
export { Field } from "./Field";
Expand Down
6 changes: 6 additions & 0 deletions src/metadata/definitions/param-metadata.ts
@@ -1,6 +1,7 @@
import { ValidatorOptions } from "class-validator";

import { TypeValueThunk, TypeOptions } from "../../decorators/types";
import { ResolverData } from "../../interfaces";

export interface BasicParamMetadata {
target: Function;
Expand Down Expand Up @@ -36,6 +37,10 @@ export interface ArgParamMetadata extends CommonArgMetadata {
export interface ArgsParamMetadata extends CommonArgMetadata {
kind: "args";
}
export interface CustomParamMetadata extends BasicParamMetadata {
kind: "custom";
resolver: (resolverData: ResolverData<any>) => any;
}
// prettier-ignore
export type ParamMetadata =
| InfoParamMetadata
Expand All @@ -44,4 +49,5 @@ export type ParamMetadata =
| RootParamMetadata
| ArgParamMetadata
| ArgsParamMetadata
| CustomParamMetadata
;