Skip to content

Commit 405fe77

Browse files
xcfoxMichalLytek
andauthored
feat: Middleware Decorator to Resolver (#1297)
* ✨feat(Authorized): decorator to resolver * ✨feat(decorators/UseMiddleware): middleware decorator to Resolver * ✨feat(decorators): createMiddlewareDecorator * 🐞fix(tests/middlewares): add @resolver() to LocalResolver * 🐞fix(decorators): export createMiddlewareDecorator * Rename create decorator method * Add changelog * Add comments for cleanup in tests * Add missing shim for createClassMiddlewareDecorator * Update docs to reflect new class-defined auth and middlewares * Rename createResolverClassMiddlewareDecorator and update changelog * Update examples to use resolver class scoped middleware --------- Co-authored-by: Michał Lytek <[email protected]>
1 parent 9ae96c0 commit 405fe77

20 files changed

+409
-59
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
## Unreleased
44

5+
## Features
6+
7+
- support declaring middlewares on resolver class level (#620)
8+
- support declaring auth roles on resolver class level (#620)
9+
- make possible creating custom decorators on resolver class level - `createResolverClassMiddlewareDecorator`
10+
511
### Others
612

713
- **Breaking Change**: update `graphql-scalars` peer dependency to `^1.23.0`
14+
- **Breaking Change**: rename `createMethodDecorator` into `createMethodMiddlewareDecorator`
815

916
<!-- Here goes all the unreleased changes descriptions -->
1017

docs/authorization.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ In express.js (and other Node.js frameworks) we use middleware for this, like `p
88

99
That's why authorization is a first-class feature in `TypeGraphQL`!
1010

11-
## How to use
11+
## Declaration
1212

1313
First, we need to use the `@Authorized` decorator as a guard on a field, query or mutation.
1414
Example object type field guards:
@@ -68,7 +68,32 @@ class MyResolver {
6868

6969
Authorized users (regardless of their roles) will be able to read data from the `publicQuery` and the `authedQuery` queries, but will receive an error when trying to perform the `adminMutation` when their roles don't include `ADMIN` or `MODERATOR`.
7070

71-
Next, we need to create our auth checker function. Its implementation may depend on our business logic:
71+
However, declaring `@Authorized()` on all the resolver's class methods would be not only a tedious task but also an error-prone one, as it's easy to forget to put it on some newly added method, etc.
72+
Hence, TypeGraphQL support declaring `@Authorized()` or the resolver class level. This way you can declare it once per resolver's class but you can still overwrite the defaults and narrows the authorization rules:
73+
74+
```ts
75+
@Authorized()
76+
@Resolver()
77+
class MyResolver {
78+
// this will inherit the auth guard defined on the class level
79+
@Query()
80+
authedQuery(): string {
81+
return "Authorized users only!";
82+
}
83+
84+
// this one overwrites the resolver's one
85+
// and registers roles required for this mutation
86+
@Authorized("ADMIN", "MODERATOR")
87+
@Mutation()
88+
adminMutation(): string {
89+
return "You are an admin/moderator, you can safely drop the database ;)";
90+
}
91+
}
92+
```
93+
94+
## Runtime checks
95+
96+
Having all the metadata for authorization set, we need to create our auth checker function. Its implementation may depend on our business logic:
7297

7398
```ts
7499
export const customAuthChecker: AuthChecker<ContextType> = (

docs/custom-decorators.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
title: Custom decorators
33
---
44

5-
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.
5+
Custom decorators are a great way to reduce the boilerplate and reuse some common logic between different resolvers. TypeGraphQL supports three kinds of custom decorators - method, resolver class and parameter.
66

77
## Method decorators
88

99
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.
1010

11-
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:
11+
They work in the same way as the [reusable middleware function](./middlewares.md#reusable-middleware), however, in this case we need to call `createMethodMiddlewareDecorator` helper function with our middleware logic and return its value:
1212

1313
```ts
1414
export function ValidateArgs(schema: JoiSchema) {
15-
return createMethodDecorator(async ({ args }, next) => {
15+
return createMethodMiddlewareDecorator(async ({ args }, next) => {
1616
// Middleware code that uses custom decorator arguments
1717

1818
// e.g. Validation logic based on schema using 'joi'
@@ -36,6 +36,39 @@ export class RecipeResolver {
3636
}
3737
```
3838

39+
## Resolver class decorators
40+
41+
Similar to method decorators, we can create our own custom resolver class decorators.
42+
In this case we need to call `createResolverClassMiddlewareDecorator` helper function, just like we did for `createMethodMiddlewareDecorator`:
43+
44+
```ts
45+
export function ValidateArgs(schema: JoiSchema) {
46+
return createResolverClassMiddlewareDecorator(async ({ args }, next) => {
47+
// Middleware code that uses custom decorator arguments
48+
49+
// e.g. Validation logic based on schema using 'joi'
50+
await joiValidate(schema, args);
51+
return next();
52+
});
53+
}
54+
```
55+
56+
The usage is then analogue - we just place it above the resolver class and pass the required arguments to it:
57+
58+
```ts
59+
@ValidateArgs(MyArgsSchema) // Custom decorator
60+
@UseMiddleware(ResolveTime) // Explicit middleware
61+
@Resolver()
62+
export class RecipeResolver {
63+
@Query()
64+
randomValue(@Args() { scale }: MyArgs): number {
65+
return Math.random() * scale;
66+
}
67+
}
68+
```
69+
70+
This way, we just need to put it once in the code and our custom decorator will be applied to all the resolver's queries or mutations. As simple as that!
71+
3972
## Parameter decorators
4073

4174
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.

docs/middlewares.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export class LogAccess implements MiddlewareInterface<TContext> {
138138

139139
### Attaching Middleware
140140

141-
To attach middleware to a resolver, place the `@UseMiddleware()` decorator above the field or resolver declaration. It accepts an array of middleware that will be called in the provided order. We can also pass them without an array as it supports [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters):
141+
To attach middleware to a resolver method, place the `@UseMiddleware()` decorator above the method declaration. It accepts an array of middleware that will be called in the provided order. We can also pass them without an array as it supports [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters):
142142

143143
```ts
144144
@Resolver()
@@ -151,6 +151,26 @@ export class RecipeResolver {
151151
}
152152
```
153153

154+
If we want to apply the middlewares to all the resolver's class methods, we can put the decorator on top of the class declaration:
155+
156+
```ts
157+
@UseMiddleware(ResolveTime, LogAccess)
158+
@Resolver()
159+
export class RecipeResolver {
160+
@Query()
161+
randomValue(): number {
162+
return Math.random();
163+
}
164+
165+
@Query()
166+
constantValue(): number {
167+
return 21.37;
168+
}
169+
}
170+
```
171+
172+
> Be aware that resolver's class middlewares are executed first, before the method's ones.
173+
154174
We can also attach the middleware to the `ObjectType` fields, the same way as with the [`@Authorized()` decorator](./authorization.md).
155175

156176
```ts
@@ -167,9 +187,9 @@ export class Recipe {
167187

168188
### Global Middleware
169189

170-
However, for common middleware like measuring resolve time or catching errors, it might be annoying to place a `@UseMiddleware(ResolveTime)` decorator on every field/resolver.
190+
However, for common middlewares like measuring resolve time or catching errors, it might be annoying to place a `@UseMiddleware(ResolveTime)` decorator on every field, method or resolver class.
171191

172-
Hence, in TypeGraphQL we can also register a global middleware that will be called for each query, mutation, subscription and field resolver. For this, we use the `globalMiddlewares` property of the `buildSchema` configuration object:
192+
Hence, in TypeGraphQL we can also register a global middleware that will be called for each query, mutation, subscription and a field. For this, we use the `globalMiddlewares` property of the `buildSchema` configuration object:
173193

174194
```ts
175195
const schema = await buildSchema({

examples/middlewares-custom-decorators/decorators/validate-args.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { validate } from "class-validator";
2-
import { ArgumentValidationError, type ClassType, createMethodDecorator } from "type-graphql";
2+
import {
3+
ArgumentValidationError,
4+
type ClassType,
5+
createMethodMiddlewareDecorator,
6+
} from "type-graphql";
37

48
// Sample implementation of custom validation decorator
59
// This example use 'class-validator' however you can plug-in 'joi' or any other validation library
610
export function ValidateArgs<T extends object>(Type: ClassType<T>) {
7-
return createMethodDecorator(async ({ args }, next) => {
11+
return createMethodMiddlewareDecorator(async ({ args }, next) => {
812
const instance = Object.assign(new Type(), args);
913
const validationErrors = await validate(instance);
1014
if (validationErrors.length > 0) {

examples/middlewares-custom-decorators/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { startStandaloneServer } from "@apollo/server/standalone";
55
import { buildSchema } from "type-graphql";
66
import Container from "typedi";
77
import { type Context } from "./context.type";
8-
import { ErrorLoggerMiddleware, ResolveTimeMiddleware } from "./middlewares";
8+
import { ErrorLoggerMiddleware } from "./middlewares";
99
import { RecipeResolver } from "./recipe";
1010

1111
async function bootstrap() {
@@ -14,7 +14,7 @@ async function bootstrap() {
1414
// Array of resolvers
1515
resolvers: [RecipeResolver],
1616
// Array of global middlewares
17-
globalMiddlewares: [ErrorLoggerMiddleware, ResolveTimeMiddleware],
17+
globalMiddlewares: [ErrorLoggerMiddleware],
1818
// Create 'schema.graphql' file with schema definition in current directory
1919
emitSchemaFile: path.resolve(__dirname, "schema.graphql"),
2020
// Registry 3rd party IOC container

examples/middlewares-custom-decorators/recipe/recipe.resolver.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { Args, Query, Resolver } from "type-graphql";
1+
import { Args, Query, Resolver, UseMiddleware } from "type-graphql";
22
import { Service } from "typedi";
33
import { RecipesArgs } from "./recipe.args";
44
import { recipes as recipesData } from "./recipe.data";
55
import { Recipe } from "./recipe.type";
66
import { CurrentUser, ValidateArgs } from "../decorators";
7+
import { ResolveTimeMiddleware } from "../middlewares";
78
import { User } from "../user.type";
89

910
@Service()
11+
@UseMiddleware(ResolveTimeMiddleware)
1012
@Resolver(_of => Recipe)
1113
export class RecipeResolver {
1214
private readonly items: Recipe[] = recipesData;

src/decorators/Authorized.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
11
import { SymbolKeysNotSupportedError } from "@/errors";
22
import { getArrayFromOverloadedRest } from "@/helpers/decorators";
33
import { getMetadataStorage } from "@/metadata/getMetadataStorage";
4-
import { type MethodAndPropDecorator } from "./types";
4+
import { type MethodPropClassDecorator } from "./types";
55

6-
export function Authorized(): MethodAndPropDecorator;
7-
export function Authorized<RoleType = string>(roles: readonly RoleType[]): MethodAndPropDecorator;
6+
export function Authorized(): MethodPropClassDecorator;
7+
export function Authorized<RoleType = string>(roles: readonly RoleType[]): MethodPropClassDecorator;
88
export function Authorized<RoleType = string>(
99
...roles: readonly RoleType[]
10-
): MethodAndPropDecorator;
10+
): MethodPropClassDecorator;
1111
export function Authorized<RoleType = string>(
1212
...rolesOrRolesArray: Array<RoleType | readonly RoleType[]>
13-
): MethodDecorator | PropertyDecorator {
13+
): MethodPropClassDecorator {
1414
const roles = getArrayFromOverloadedRest(rolesOrRolesArray);
1515

16-
return (prototype, propertyKey, _descriptor) => {
16+
return (
17+
target: Function | Object,
18+
propertyKey?: string | symbol,
19+
_descriptor?: TypedPropertyDescriptor<any>,
20+
) => {
21+
if (propertyKey == null) {
22+
getMetadataStorage().collectAuthorizedResolverMetadata({
23+
target: target as Function,
24+
roles,
25+
});
26+
return;
27+
}
28+
1729
if (typeof propertyKey === "symbol") {
1830
throw new SymbolKeysNotSupportedError();
1931
}
2032

2133
getMetadataStorage().collectAuthorizedFieldMetadata({
22-
target: prototype.constructor,
34+
target: target.constructor,
2335
fieldName: propertyKey,
2436
roles,
2537
});

src/decorators/UseMiddleware.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,34 @@ import { SymbolKeysNotSupportedError } from "@/errors";
22
import { getArrayFromOverloadedRest } from "@/helpers/decorators";
33
import { getMetadataStorage } from "@/metadata/getMetadataStorage";
44
import { type Middleware } from "@/typings/middleware";
5-
import { type MethodAndPropDecorator } from "./types";
5+
import { type MethodPropClassDecorator } from "./types";
66

7-
export function UseMiddleware(middlewares: Array<Middleware<any>>): MethodAndPropDecorator;
8-
export function UseMiddleware(...middlewares: Array<Middleware<any>>): MethodAndPropDecorator;
7+
export function UseMiddleware(middlewares: Array<Middleware<any>>): MethodPropClassDecorator;
8+
export function UseMiddleware(...middlewares: Array<Middleware<any>>): MethodPropClassDecorator;
99
export function UseMiddleware(
1010
...middlewaresOrMiddlewareArray: Array<Middleware<any> | Array<Middleware<any>>>
11-
): MethodDecorator | PropertyDecorator {
11+
): MethodPropClassDecorator {
1212
const middlewares = getArrayFromOverloadedRest(middlewaresOrMiddlewareArray);
1313

14-
return (prototype, propertyKey, _descriptor) => {
14+
return (
15+
target: Function | Object,
16+
propertyKey?: string | symbol,
17+
_descriptor?: TypedPropertyDescriptor<any>,
18+
) => {
19+
if (propertyKey == null) {
20+
getMetadataStorage().collectResolverMiddlewareMetadata({
21+
target: target as Function,
22+
middlewares,
23+
});
24+
return;
25+
}
26+
1527
if (typeof propertyKey === "symbol") {
1628
throw new SymbolKeysNotSupportedError();
1729
}
1830

1931
getMetadataStorage().collectMiddlewareMetadata({
20-
target: prototype.constructor,
32+
target: target.constructor,
2133
fieldName: propertyKey,
2234
middlewares,
2335
});

src/decorators/createMethodDecorator.ts renamed to src/decorators/createMethodMiddlewareDecorator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type MiddlewareFn } from "@/typings/middleware";
22
import { UseMiddleware } from "./UseMiddleware";
33

4-
export function createMethodDecorator<TContextType extends object = object>(
4+
export function createMethodMiddlewareDecorator<TContextType extends object = object>(
55
resolver: MiddlewareFn<TContextType>,
66
): MethodDecorator {
77
return UseMiddleware(resolver);

0 commit comments

Comments
 (0)