Hooks
nestjs-query
provides the following hooks that allow you to modify incoming requests.
@BeforeFindOne
- invoked before anyfindOne
query.@BeforeQueryMany
- invoked before anyqueryMany
query.@BeforeCreateOne
- invoked before anycreateOne
mutation.@BeforeCreateMany
- invoked before anycreateMany
mutation.@BeforeUpdateOne
- invoked before anyupdateOne
mutation.@BeforeUpdateMany
- invoked before anyupdateMany
mutation.@BeforeDeleteOne
- invoked before anydeleteOne
mutation.@BeforeDeleteMany
- invoked before anydeleteMany
mutation.
In order to use a hook you only need to decorate your DTO with the corresponding decorator.
Each hook decorator can be provided one of the following:
- A hook function
- A class that extends
Hook
, when using a class you can use DI to access other services just likeguards
,interceptors
orpipes
.
The graphql context by default only contains the incoming request!
If you provide a custom create
or update
DTO you can also decorate those classes with corresponding decorators.
All of the examples reference a GqlContext. This was defined for the sake of the example. It is recommended that you define a custom type that represents the information in the context based on the guards and interceptors used in your application.
We have defined our GqlContext
as
export type GqlContext = { req: { headers: Record<string, string> } };
@BeforeCreateOne
The @BeforeCreateOne
decorator can be used to modify incoming createOne
mutations with information from the graphql
context.
Hook Function
In this example we set the createdBy field based on the context.
import { FilterableField, BeforeCreateOne, CreateOneInputType } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
import { GqlContext } from '../../interfaces';
import { getUserName } from '../../helpers';
@ObjectType('TodoItem')
@BeforeCreateOne((input: CreateOneInputType<TodoItemDTO>, context: GqlContext) => {
input.input.createdBy = getUserName(context);
return input;
})
export class TodoItemDTO {
/**
Other fields
**/
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
Hook Class
You can also provide a custom Hook
class that can leverage nestjs
dependency injection.
In this example we create a simple Hook that works with any type that has a createdBy
property.
import { Injectable } from '@nestjs/common';
import { BeforeCreateOneHook, CreateOneInputType,} from '@ptc-org/nestjs-query-graphql';
import { GqlContext } from './auth/auth.guard';
import { AuthService } from './auth/auth.service';
interface CreatedBy {
createdBy: string;
}
@Injectable()
export class CreatedByHook<T extends CreatedBy> implements BeforeCreateOneHook<T, GqlContext> {
constructor(readonly authService: AuthService) {}
async run(instance: CreateOneInputType<T>, context: GqlContext): Promise<CreateOneInputType<T>> {
const createdBy = await this.authService.getUserEmail(context.userId);
instance.input.createdBy = createdBy;
return instance;
}
}
Now we just provide the hook to the BeforeCreateOne
decorator.
import { FilterableField, BeforeCreateOne } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
import { CreatedByHook } from '../../hooks';
@ObjectType('TodoItem')
@BeforeCreateOne(CreatedByHook)
export class TodoItemDTO {
/**
Other fields
**/
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
@BeforeCreateMany
The @BeforeCreateMany
decorator can be used to modify incoming createMany
mutations with information from the
graphql context.
Hook Function
In this example we set the createdBy field on each record based on the context.
import { FilterableField, BeforeCreateMany, CreateManyInputType } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
import { GqlContext } from '../../interfaces';
import { getUserName } from '../../helpers';
@ObjectType('TodoItem')
@BeforeCreateMany((input: CreateManyInputType<TodoItemDTO>, context: GqlContext) => {
const createdBy = getUserName(context);
input.input = input.input.map((c) => ({ ...c, createdBy }));
return input;
})
export class TodoItemDTO {
/**
Other fields
**/
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
Hook Class
You can also provide a custom Hook
class that can leverage nestjs
dependency injection.
In this example we create a simple Hook that works with any type that has a createdBy
property.
import { Injectable } from '@nestjs/common';
import { BeforeCreateManyHook, CreateManyInputType,} from '@ptc-org/nestjs-query-graphql';
import { GqlContext } from './auth/auth.guard';
import { AuthService } from './auth/auth.service';
interface CreatedBy {
createdBy: string;
}
@Injectable()
export class CreatedByHook<T extends CreatedBy> implements BeforeCreateManyHook<T, GqlContext> {
constructor(readonly authService: AuthService) {}
async run(instance: CreateManyInputType<T>, context: GqlContext): Promise<CreateManyInputType<T>> {
const createdBy = await this.authService.getUserEmail(context.userId);
instance.input = instance.input.map((c) => ({ ...c, createdBy }));
return instance;
}
}
Now we just provide the hook to the BeforeCreateMany
decorator.
import { FilterableField, BeforeCreateMany } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
import { CreatedByHook } from '../../hooks';
@ObjectType('TodoItem')
@BeforeCreateMany(CreatedByHook)
export class TodoItemDTO {
/**
Other fields
**/
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
@BeforeUpdateOne
The @BeforeUpdateOne
decorator can be used to modify incoming updateOne
mutations with information from the graphql
context.
Hook Fnction
In this example we set the updatedBy field in the update.
import { FilterableField, BeforeUpdateOne, UpdateOneInputType } from '@ptc-org/nestjs-query-graphql';
import { ObjectType } from '@nestjs/graphql';
import { GqlContext } from '../../interfaces';
import { getUserName } from '../../helpers';
@ObjectType('TodoItem')
@BeforeUpdateOne((input: UpdateOneInputType<TodoItemDTO>, context: GqlContext) => {
input.update.updatedBy = getUserName(context);
return input;
})
export class TodoItemDTO {
/**
Other fields
**/
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
Hook Class
You can also provide a custom Hook
class that can leverage nestjs
dependency injection.
In this example we create a simple Hook that works with any type that has a updatedBy
property.
import { BeforeUpdateOneHook, UpdateOneInputType } from '@ptc-org/nestjs-query-graphql';
import { Injectable } from '@nestjs/common';
import { GqlContext } from './auth/auth.guard';
import { AuthService } from './auth/auth.service';
interface UpdatedBy {
updatedBy: string;
}
@Injectable()
export class UpdatedByHook<T extends UpdatedBy> implements BeforeUpdateOneHook<T, GqlContext> {
constructor(readonly authService: AuthService) {}
async run(instance: UpdateOneInputType<T>, context: GqlContext): Promise<UpdateOneInputType<T>> {
// eslint-disable-next-line no-param-reassign
instance.update.updatedBy = await this.authService.getUserEmail(context.userId);
return instance;
}
}
Now we just provide the hook to the BeforeUpdateOne
decorator.
import { FilterableField, BeforeUpdateOne } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
import { CreatedByHook } from '../../hooks';
@ObjectType('TodoItem')
@BeforeUpdateOne(UpdatedByHook)
export class TodoItemDTO {
/**
Other fields
**/
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
@BeforeUpdateMany
The @BeforeUpdateMany
decorator can be used to modify incoming updateMany
mutations with information from the
graphql context.
Hook Function
In this example we set the updatedBy field in the update.
import { FilterableField, BeforeUpdateMany, UpdateManyInputType } from '@ptc-org/nestjs-query-graphql';
import { ObjectType } from '@nestjs/graphql';
import { GqlContext } from '../../interfaces';
import { getUserName } from '../../helpers';
@ObjectType('TodoItem')
@BeforeUpdateMany((input: UpdateManyInputType<TodoItemDTO, TodoItemDTO>, context: GqlContext) => {
input.update.updatedBy = getUserName(context);
return input;
})
export class TodoItemDTO {
/**
Other fields
**/
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
Hook Class
You can also provide a custom Hook
class that can leverage nestjs
dependency injection.
In this example we create a simple Hook that works with any type that has a updatedBy
property.
import { BeforeUpdateManyHook, UpdateManyInputType } from '@ptc-org/nestjs-query-graphql';
import { Injectable } from '@nestjs/common';
import { GqlContext } from './auth/auth.guard';
import { AuthService } from './auth/auth.service';
interface UpdatedBy {
updatedBy: string;
}
@Injectable()
export class UpdatedByHook<T extends UpdatedBy> implements BeforeUpdateManyHook<T, GqlContext> {
constructor(readonly authService: AuthService) {}
async run(instance: UpdateManyInputType<T, T>, context: GqlContext): Promise<UpdateManyInputType<T, T>> {
// eslint-disable-next-line no-param-reassign
instance.update.updatedBy = await this.authService.getUserEmail(context.userId);
return instance;
}
}
Now we just provide the hook to the BeforeUpdateMany
decorator.
import { FilterableField, BeforeUpdateMany } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
import { CreatedByHook } from '../../hooks';
@ObjectType('TodoItem')
@BeforeUpdateMany(UpdatedByHook)
export class TodoItemDTO {
/**
Other fields
**/
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
Using Hooks In Custom Endpoints
You can also use hooks in custom endpoints by using the HookInterceptor
along with
HookArgs
- Used to apply hooks to any query endpoint.MutationHookArgs
- Used to apply hooks to anymutation
that usesMutationArgsType
Example
In this example we'll create an endpoint that marks all todo items that are currently not completed as completed.
To start we'll create our input types.
There are two types that are created
- The
UpdateManyTodoItemsInput
which extends theUpdateManyInputType
this exposes anupdate
andfilter
field just like theupdateMany
endpoints that are auto generated. - The
UpdateManyTodoItemsArgs
which extendsMutationArgsType
, this provides a uniform interface for all mutations ensuring that the argument provided to the mutation is namedinput
.
import { MutationArgsType, UpdateManyInputType } from '@ptc-org/nestjs-query-graphql';
import { ArgsType, InputType } from '@nestjs/graphql';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemUpdateDTO } from './dto/todo-item-update.dto';
// create the base input type
@InputType()
export class MarkTodoItemsAsCompletedInput extends UpdateManyInputType(TodoItemDTO, TodoItemUpdateDTO) {}
// Wrap the input in the MutationArgsType to provide a uniform format for all mutations
// The `MutationArgsType` is a thin wrapper that names the args as input
@ArgsType()
export class MarkTodoItemsAsCompletedArgs extends MutationArgsType(UpdateManyTodoItemsInput) {}
Now we can use our new types in the resolver.
import { InjectQueryService, mergeFilter, QueryService, UpdateManyResponse } from '@ptc-org/nestjs-query-core';
import { HookTypes, HookInterceptor, MutationHookArgs, UpdateManyResponseType } from '@ptc-org/nestjs-query-graphql';
import { UseInterceptors } from '@nestjs/common';
import { Mutation, Resolver } from '@nestjs/graphql';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoItemEntity } from './todo-item.entity';
import { MarkTodoItemsAsCompletedArgs } from './types';
import { TodoItemUpdateDTO } from './dto/todo-item-update.dto';
@Resolver(() => TodoItemDTO)
export class TodoItemResolver {
constructor(@InjectQueryService(TodoItemEntity) readonly service: QueryService<TodoItemDTO>) {}
// Set the return type to the TodoItemConnection
@Mutation(() => UpdateManyResponseType())
@UseInterceptors(HookInterceptor(HookTypes.BEFORE_UPDATE_MANY, TodoItemUpdateDTO))
markTodoItemsAsCompleted(@MutationHookArgs() { input }: MarkTodoItemsAsCompletedArgs): Promise<UpdateManyResponse> {
return this.service.updateMany(
{ ...input.update, completed: true },
mergeFilter(input.filter, { completed: { is: false } }),
);
}
}
The first thing to notice is the
@UseInterceptors(HookInterceptor(HookTypes.BEFORE_UPDATE_MANY, TodoItemUpdateDTO))
This interceptor adds the correct hook to the context
to be used by the param decorator.
There are a few things to take note of:
- The
HookTypes.BEFORE_UPDATE_MANY
lets the interceptor know we are wanting the BeforeUpdateMany hook to be used for this mutation. - We use the
TodoItemUpdateDTO
, that is because the@BeforeUpdateMany
decorator was put on theTodoItemUpdateDTO
not theTodoItemDTO
.
When using the HookInterceptor you must use the DTO that you added the hook decorator to.
In this example we bind the BEFORE_UPDATE_MANY
hook, you can use any of the hooks available to bind to the correct
one when creating
, updating
, or deleting
records.
The next piece is the
@MutationHookArgs() { input }: UpdateManyTodoItemsArgs
By using the MutationHookArgs
decorator we ensure that the hook is applied to the arguments adding any additional
fields to the update.
Finally we invoke the service updateMany
with a filter that ensures we only update TodoItems
that are completed,
and add an setting completed
to true to the update
return this.service.updateMany(
{ ...input.update, completed: false },
mergeFilter(input.filter, { completed: { is: false } }),
);