Skip to content

Commit 20419ee

Browse files
docs(graphql): add chapter about field middleware feature
1 parent d6456e2 commit 20419ee

File tree

9 files changed

+264
-180
lines changed

9 files changed

+264
-180
lines changed

content/graphql/enums.md

Lines changed: 0 additions & 123 deletions
This file was deleted.

content/graphql/extensions.md

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,51 +14,43 @@ To attach custom metadata for a field, use the `@Extensions()` decorator exporte
1414
password: string;
1515
```
1616

17-
In the example above, we assigned the `role` metadata property the value of `Role.ADMIN`. `Role` is a simple TypeScript enum that groups all the user roles available in our system.
17+
In the example above, we assigned the `role` metadata property the value of `Role.ADMIN`. `Role` is a simple TypeScript enum that groups all the user roles available in our system.
1818

1919
Note, in addition to setting metadata on fields, you can use the `@Extensions()` decorator at the class level and method level (e.g., on the query handler).
2020

2121
#### Using custom metadata
2222

23-
The logic that leverages the custom metadata can be as complex as needed. For example, you can create a simple interceptor that stores/logs events per method invocation, or create a sophisticated guard that **analyzes requested fields**, iterates through the `GraphQLObjectType` definition, and matches the roles required to retrieve specific fields with the caller permissions (field-level permissions system).
23+
Logic that leverages the custom metadata can be as complex as needed. For example, you can create a simple interceptor that stores/logs events per method invocation, or a [field middleware](/graphql/field-middleware) that matches roles required to retrieve a field with the caller permissions (field-level permissions system).
2424

25-
Let's define a `FieldRolesGuard` that implements a basic version of such a field-level permissions system.
25+
For illustration purposes, let's define a `checkRoleMiddleware` that compares a user's role (hardcoded here) with a role required to access a target field:
2626

2727
```typescript
28-
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
29-
import { GqlExecutionContext } from '@nestjs/graphql';
30-
import { GraphQLNonNull, GraphQLObjectType, GraphQLResolveInfo } from 'graphql';
31-
import * as graphqlFields from 'graphql-fields';
32-
33-
@Injectable()
34-
export class FieldRolesGuard implements CanActivate {
35-
canActivate(context: ExecutionContext): boolean {
36-
const info = GqlExecutionContext.create(context).getInfo<
37-
GraphQLResolveInfo
38-
>();
39-
const returnType = (info.returnType instanceof GraphQLNonNull
40-
? info.returnType.ofType
41-
: info.returnType) as GraphQLObjectType;
42-
43-
const fields = returnType.getFields();
44-
const requestedFields = graphqlFields(info);
45-
46-
Object.entries(fields)
47-
.filter(([key]) => key in requestedFields)
48-
.map(([_, field]) => field)
49-
.filter((field) => field.extensions && field.extensions.role)
50-
.forEach((field) => {
51-
// match user and field roles here
52-
console.log(field.extensions.role);
53-
});
54-
55-
return true;
28+
export const checkRoleMiddleware: FieldMiddleware = async (
29+
ctx: MiddlewareContext,
30+
next: NextFn,
31+
) => {
32+
const { info } = ctx;
33+
const { extensions } = info.parentType.getFields()[info.fieldName];
34+
35+
/**
36+
* In a real-world application, the "userRole" variable
37+
* should represent the caller's (user) role (for example, "ctx.user.role").
38+
*/
39+
const userRole = Role.USER;
40+
if (userRole === extensions.role) {
41+
// or just "return null" to ignore
42+
throw new ForbiddenException(
43+
`User does not have sufficient permissions to access "${info.fieldName}" field.`,
44+
);
5645
}
57-
}
46+
return next();
47+
};
5848
```
5949

60-
> warning **Warning** For illustration purposes, we assumed that **every** resolver returns either the `GraphQLObjectType` or `GraphQLNonNull` that wraps the object type. In a real-world application, you should cover other cases (scalars, etc.). Note that using this particular implementation can lead to unexpected errors (e.g., missing `getFields()` method).
61-
62-
In the example above, we've used the [graphql-fields](https://github.com/robrichard/graphql-fields) package that turns the `GraphQLResolveInfo` object into an object that consists of the requested fields. We used this specific library to make the presented example somewhat simpler.
50+
With this in place, we can register a middleware for the `password` field, as follows:
6351

64-
With this guard in place, if the return type of any resolver contains a field annotated with the `@Extensions({{ '{' }} role: Role.ADMIN {{ '}' }}})` decorator, this `role` (`Role.ADMIN`) will be logged in the console **if requested** in the GraphQL query.
52+
```typescript
53+
@Field({ middleware: [checkRoleMiddleware] })
54+
@Extensions({ role: Role.ADMIN })
55+
password: string;
56+
```

content/graphql/field-middleware.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
### Field middleware
2+
3+
> warning **Warning** This chapter applies only to the code first approach.
4+
5+
Field Middleware lets you run arbitrary code **before or after** a field is resolved. A field middleware can be used to convert the result of a field, validate the arguments of a field, or even check field-level roles (for example, required to access a target field for which a middleware function is executed).
6+
7+
You can connect multiple middleware functions to a field. In this case, they will be called sequentially along the chain where the previous middleware decides to call the next one. The order of the middleware functions in the `middleware` array is important. The first resolver is the "most-outer" layer, so it gets executed first and last (similarly to the `graphql-middleware` package). The second resolver is the "second-outer" layer, so it gets executed second and second to last.
8+
9+
#### Getting started
10+
11+
Let's start off by creating a simple middleware that will log a field value before it's sent back to the client:
12+
13+
```typescript
14+
import { FieldMiddleware, MiddlewareContext, NextFn } from '@nestjs/graphql';
15+
16+
const loggerMiddleware: FieldMiddleware = async (
17+
ctx: MiddlewareContext,
18+
next: NextFn,
19+
) => {
20+
const value = await next();
21+
console.log(value);
22+
return value;
23+
};
24+
```
25+
26+
> info **Hint** The `MiddlewareContext` is an object that consist of the same arguments that are normally received by the GraphQL resolver function (`{{ '{' }} source, args, context, info {{ '}' }}`), while `NextFn` is a function that let you execute the next middleware in the stack (bound to this field) or the actual field resolver.
27+
28+
> warning **Warning** Field middleware functions cannot inject dependencies nor access Nest's DI container as they are designed to be very lightweight and shouldn't perform any potentially time-consuming operations (like retrieving data from the database). If you need to call external services/query data from the data source, you should do it in a guard/interceptor bounded to a root query/mutation handler and assing it to `context` object which you can access from within the field middleware (specifically, from the `MiddlewareContext` object).
29+
30+
Note that field middleware must match the `FieldMiddleware` interface. In the example above, we first run the `next()` function (which executes the actual field resolver and returns a field value) and then, we log this value to our terminal. Also, the value returned from the middleware function completely overrides the previous value and since we don't want to perform any changes, we simply return the original value.
31+
32+
With this in place, we can register our middleware directly in the `@Field()` decorator, as follows:
33+
34+
```typescript
35+
@ObjectType()
36+
export class Recipe {
37+
@Field({ middleware: [loggerMiddleware] })
38+
title: string;
39+
}
40+
```
41+
42+
Now whenever we request the `title` field of `Recipe` object type, the original field's value will be logged to the console.
43+
44+
> info **Hint** To learn how you can implement a field-level permissions system with the use of [extensions](/graphql/extensions) feature, check out this [section](/graphql/extensions#using-custom-metadata).
45+
46+
Also, as mentioned above, we can control the field's value from within the middleware function. For demonstration purposes, let's capitalise a recipe's title (if present):
47+
48+
```typescript
49+
const value = await next();
50+
return value?.toUpperCase();
51+
```
52+
53+
In this case, every title will be automatically uppercased, when requested.
54+
55+
Likewise, you can bind a field middleware to a custom field resolver (a method annotated with the `@ResolveField()` decorator), as follows:
56+
57+
```typescript
58+
@ResolveField(() => String, { middleware: [loggerMiddleware] })
59+
title() {
60+
return 'Placeholder';
61+
}
62+
```
63+
64+
> warning **Warning** In case enhancers are enabled at the field resolver level ([read more](/graphl//other-features#execute-enhancers-at-the-field-resolver-level)), field middleware functions will run before any interceptors, guards, etc., **bounded to the method** (but after the root-level enhancers registered for query or mutation handlers).
65+
66+
#### Global field middleware
67+
68+
In addition to binding a middleware directly to a specific field, you can also register one or multiple middleware functions globally. In this case, they will be automatically connected to all fields of your object types.
69+
70+
```typescript
71+
GraphQLModule.forRoot({
72+
autoSchemaFile: 'schema.gql',
73+
buildSchemaOptions: {
74+
fieldMiddleware: [loggerMiddleware],
75+
},
76+
}),
77+
```
78+
79+
> info **Hint** Globally registered field middleware functions will be executed **before** locally registered ones (those bound directly to specific fields).

0 commit comments

Comments
 (0)