NestJS Logo

Extensions

Warning This chapter applies only to the code first approach.

Extensions is an advanced, low-level feature that lets you define arbitrary data in the types configuration. Attaching custom metadata to certain fields allows you to create more sophisticated, generic solutions. For example, with extensions, you can define field-level roles required to access particular fields. Such roles can be reflected at runtime to determine whether the caller has sufficient permissions to retrieve a specific field.

Adding custom metadata#

To attach custom metadata for a field, use the @Extensions() decorator exported from the @nestjs/graphql package.


@Field()
@Extensions({ role: Role.ADMIN })
password: string;

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.

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

Using custom metadata#

The logic that leverages the custom metatada 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).

Let's define a FieldRolesGuard that implements a basic version of such a field-level permissions system.


import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { GraphQLNonNull, GraphQLObjectType, GraphQLResolveInfo } from 'graphql';
import * as graphqlFields from 'graphql-fields';

@Injectable()
export class FieldRolesGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const info = GqlExecutionContext.create(context).getInfo<
      GraphQLResolveInfo
    >();
    const returnType = (info.returnType instanceof GraphQLNonNull
      ? info.returnType.ofType
      : info.returnType) as GraphQLObjectType;

    const fields = returnType.getFields();
    const requestedFields = graphqlFields(info);

    Object.entries(fields)
      .filter(([key]) => key in requestedFields)
      .map(([_, field]) => field)
      .filter((field) => field.extensions && field.extensions.role)
      .forEach((field) => {
        // match user and field roles here
        console.log(field.extensions.role);
      });

    return true;
  }
}
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).

In the example above, we've used the 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.

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.

Support us

Nest is an MIT-licensed open source project. It can grow thanks to the support by these awesome people. If you'd like to join them, please read more here.

Principal Sponsors

Sanofi LogoValor Logo

Sponsors / Partners

Become a sponsor