Hooks
type-graphql-query provides the following hooks that allow you to modify incoming requests.
@BeforeFindOne- invoked before anyfindByIdquery.@BeforeQueryMany- invoked before anyqueryManyquery.@BeforeCreateOne- invoked before anycreateOnemutation.@BeforeCreateMany- invoked before anycreateManymutation.@BeforeUpdateOne- invoked before anyupdateOnemutation.@BeforeUpdateMany- invoked before anyupdateManymutation.@BeforeDeleteOne- invoked before anydeleteOnemutation.@BeforeDeleteMany- invoked before anydeleteManymutation.
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 implements
Hook, when using a class you can use dependency injection to access other services.
The GraphQL context by default only contains the incoming request! Make sure your context includes the necessary information (like userId, locale, etc.) through your authentication/authorization setup.
For the examples, we’ll use an ExecutionContext type that includes user information:
export interface ExecutionContext { userId: string; locale: string; platformRole?: string;}@BeforeCreateOne
Section intitulée « @BeforeCreateOne »The @BeforeCreateOne decorator can be used to modify incoming createOne mutations with information from the GraphQL context.
Hook Function
Section intitulée « Hook Function »In this example we set the createdBy field based on the context.
import { BeforeCreateOne, CreateOneInputType } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { ExecutionContext } from '../interfaces';import { ProductInputDTO } from './product-input.type';
@ObjectType('Product')@BeforeCreateOne((input: CreateOneInputType<ProductInputDTO>, context: ExecutionContext) => { input.input.createdBy = context.userId; return input;})export class ProductDTO { @Field() id!: string;
@Field() name!: string;
@Field({ nullable: true }) createdBy?: string;
@Field({ nullable: true }) updatedBy?: string;}Hook Class
Section intitulée « Hook Class »You can also provide a custom Hook class that can leverage dependency injection.
In this example we create a simple Hook that works with any type that has a createdBy property.
import { Hook } from '@lib/type-graphql-query';import { CreateOneInputType } from '@lib/type-graphql-query';import { ExecutionContext } from '../interfaces';import { AuthService } from './auth.service';
interface CreatedBy { createdBy: string;}
export class CreatedByHook<T extends CreatedBy> implements Hook< CreateOneInputType<T>, ExecutionContext> { constructor(readonly authService: AuthService) {}
async run( instance: CreateOneInputType<T>, context: ExecutionContext, ): 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 { BeforeCreateOne } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { CreatedByHook } from '../hooks';
@ObjectType('Product')@BeforeCreateOne(CreatedByHook)export class ProductDTO { @Field() id!: string;
@Field() name!: string;
@Field({ nullable: true }) createdBy?: string;}@BeforeCreateMany
Section intitulée « @BeforeCreateMany »The @BeforeCreateMany decorator can be used to modify incoming createMany mutations with information from the GraphQL context.
Hook Function
Section intitulée « Hook Function »In this example we set the createdBy field on each record based on the context.
import { BeforeCreateMany, CreateManyInputType } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { ExecutionContext } from '../interfaces';import { ProductInputDTO } from './product-input.type';
@ObjectType('Product')@BeforeCreateMany((input: CreateManyInputType<ProductInputDTO>, context: ExecutionContext) => { const createdBy = context.userId; input.input = input.input.map((c) => ({ ...c, createdBy })); return input;})export class ProductDTO { @Field() id!: string;
@Field() name!: string;
@Field({ nullable: true }) createdBy?: string;
@Field({ nullable: true }) updatedBy?: string;}Hook Class
Section intitulée « Hook Class »You can also provide a custom Hook class that can leverage dependency injection.
import { Hook } from '@lib/type-graphql-query';import { CreateManyInputType } from '@lib/type-graphql-query';import { ExecutionContext } from '../interfaces';import { AuthService } from './auth.service';
interface CreatedBy { createdBy: string;}
export class CreatedByHook<T extends CreatedBy> implements Hook< CreateManyInputType<T>, ExecutionContext> { constructor(readonly authService: AuthService) {}
async run( instance: CreateManyInputType<T>, context: ExecutionContext, ): 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 { BeforeCreateMany } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { CreatedByHook } from '../hooks';
@ObjectType('Product')@BeforeCreateMany(CreatedByHook)export class ProductDTO { @Field() id!: string;
@Field() name!: string;
@Field({ nullable: true }) createdBy?: string;}@BeforeUpdateOne
Section intitulée « @BeforeUpdateOne »The @BeforeUpdateOne decorator can be used to modify incoming updateOne mutations with information from the GraphQL context.
Hook Function
Section intitulée « Hook Function »In this example we set the updatedBy field in the update.
import { BeforeUpdateOne, UpdateOneInputType } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { ExecutionContext } from '../interfaces';import { ProductUpdateDTO } from './product-update.type';
@ObjectType('Product')@BeforeUpdateOne((input: UpdateOneInputType<ProductUpdateDTO>, context: ExecutionContext) => { input.update.updatedBy = context.userId; return input;})export class ProductDTO { @Field() id!: string;
@Field() name!: string;
@Field({ nullable: true }) createdBy?: string;
@Field({ nullable: true }) updatedBy?: string;}Hook Class
Section intitulée « Hook Class »You can also provide a custom Hook class that can leverage dependency injection.
import { Hook } from '@lib/type-graphql-query';import { UpdateOneInputType } from '@lib/type-graphql-query';import { ExecutionContext } from '../interfaces';import { AuthService } from './auth.service';
interface UpdatedBy { updatedBy: string;}
export class UpdatedByHook<T extends UpdatedBy> implements Hook< UpdateOneInputType<T>, ExecutionContext> { constructor(readonly authService: AuthService) {}
async run( instance: UpdateOneInputType<T>, context: ExecutionContext, ): Promise<UpdateOneInputType<T>> { instance.update.updatedBy = await this.authService.getUserEmail(context.userId); return instance; }}Now we just provide the hook to the @BeforeUpdateOne decorator.
import { BeforeUpdateOne } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { UpdatedByHook } from '../hooks';
@ObjectType('Product')@BeforeUpdateOne(UpdatedByHook)export class ProductDTO { @Field() id!: string;
@Field() name!: string;
@Field({ nullable: true }) updatedBy?: string;}@BeforeUpdateMany
Section intitulée « @BeforeUpdateMany »The @BeforeUpdateMany decorator can be used to modify incoming updateMany mutations with information from the GraphQL context.
Hook Function
Section intitulée « Hook Function »In this example we set the updatedBy field in the update.
import { BeforeUpdateMany, UpdateManyInputType } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { ExecutionContext } from '../interfaces';import { ProductUpdateDTO } from './product-update.type';import { ProductDTO } from './product.type';
@ObjectType('Product')@BeforeUpdateMany( (input: UpdateManyInputType<ProductDTO, ProductUpdateDTO>, context: ExecutionContext) => { input.update.updatedBy = context.userId; return input; },)export class ProductDTO { @Field() id!: string;
@Field() name!: string;
@Field({ nullable: true }) createdBy?: string;
@Field({ nullable: true }) updatedBy?: string;}Hook Class
Section intitulée « Hook Class »You can also provide a custom Hook class that can leverage dependency injection.
import { Hook } from '@lib/type-graphql-query';import { UpdateManyInputType } from '@lib/type-graphql-query';import { ExecutionContext } from '../interfaces';import { AuthService } from './auth.service';
interface UpdatedBy { updatedBy: string;}
export class UpdatedByHook<DTO extends object, U extends UpdatedBy> implements Hook< UpdateManyInputType<DTO, U>, ExecutionContext> { constructor(readonly authService: AuthService) {}
async run( instance: UpdateManyInputType<DTO, U>, context: ExecutionContext, ): Promise<UpdateManyInputType<DTO, U>> { instance.update.updatedBy = await this.authService.getUserEmail(context.userId); return instance; }}Now we just provide the hook to the @BeforeUpdateMany decorator.
import { BeforeUpdateMany } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { UpdatedByHook } from '../hooks';
@ObjectType('Product')@BeforeUpdateMany(UpdatedByHook)export class ProductDTO { @Field() id!: string;
@Field() name!: string;
@Field({ nullable: true }) updatedBy?: string;}@BeforeDeleteOne
Section intitulée « @BeforeDeleteOne »The @BeforeDeleteOne decorator can be used to modify incoming deleteOne mutations with information from the GraphQL context.
Hook Function
Section intitulée « Hook Function »import { BeforeDeleteOne, DeleteOneInputType } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { ExecutionContext } from '../interfaces';
@ObjectType('Product')@BeforeDeleteOne((input: DeleteOneInputType, context: ExecutionContext) => { // Log deletion or perform additional checks console.log(`Deleting product ${input.id} by user ${context.userId}`); return input;})export class ProductDTO { @Field() id!: string;
@Field() name!: string;}Hook Class
Section intitulée « Hook Class »import { Hook } from '@lib/type-graphql-query';import { DeleteOneInputType } from '@lib/type-graphql-query';import { ExecutionContext } from '../interfaces';import { AuditService } from './audit.service';
export class DeleteAuditHook implements Hook<DeleteOneInputType, ExecutionContext> { constructor(readonly auditService: AuditService) {}
async run(instance: DeleteOneInputType, context: ExecutionContext): Promise<DeleteOneInputType> { await this.auditService.logDeletion(instance.id, context.userId); return instance; }}@BeforeDeleteMany
Section intitulée « @BeforeDeleteMany »The @BeforeDeleteMany decorator can be used to modify incoming deleteMany mutations with information from the GraphQL context.
Hook Function
Section intitulée « Hook Function »import { BeforeDeleteMany, DeleteManyInputType } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { ExecutionContext } from '../interfaces';import { ProductDTO } from './product.type';
@ObjectType('Product')@BeforeDeleteMany((input: DeleteManyInputType<ProductDTO>, context: ExecutionContext) => { // Log deletion or perform additional checks console.log(`Deleting products matching filter by user ${context.userId}`); return input;})export class ProductDTO { @Field() id!: string;
@Field() name!: string;}Hook Class
Section intitulée « Hook Class »import { Hook } from '@lib/type-graphql-query';import { DeleteManyInputType } from '@lib/type-graphql-query';import { ExecutionContext } from '../interfaces';import { AuditService } from './audit.service';
export class DeleteManyAuditHook<DTO extends object> implements Hook< DeleteManyInputType<DTO>, ExecutionContext> { constructor(readonly auditService: AuditService) {}
async run( instance: DeleteManyInputType<DTO>, context: ExecutionContext, ): Promise<DeleteManyInputType<DTO>> { await this.auditService.logBulkDeletion(instance.filter, context.userId); return instance; }}@BeforeFindOne
Section intitulée « @BeforeFindOne »The @BeforeFindOne decorator can be used to modify incoming findById queries with information from the GraphQL context.
Hook Function
Section intitulée « Hook Function »import { BeforeFindOne, FindOneArgsType } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { ExecutionContext } from '../interfaces';
@ObjectType('Product')@BeforeFindOne((input: FindOneArgsType, context: ExecutionContext) => { // Add additional query options or logging console.log(`Finding product ${input.id} for user ${context.userId}`); return input;})export class ProductDTO { @Field() id!: string;
@Field() name!: string;}@BeforeQueryMany
Section intitulée « @BeforeQueryMany »The @BeforeQueryMany decorator can be used to modify incoming queryMany queries with information from the GraphQL context.
Hook Function
Section intitulée « Hook Function »import { BeforeQueryMany } from '@lib/type-graphql-query';import { ObjectType, Field } from '@lib/type-graphql';import { ExecutionContext } from '../interfaces';import { Query } from '@lib/type-graphql-query';import { ProductDTO } from './product.type';
@ObjectType('Product')@BeforeQueryMany((query: Query<ProductDTO>, context: ExecutionContext) => { // Add default filters or modify query if (!query.filter) { query.filter = {}; } // Add user-specific filtering return query;})export class ProductDTO { @Field() id!: string;
@Field() name!: string;}Using Hooks In Custom Endpoints
Section intitulée « Using Hooks In Custom Endpoints »You can also use hooks in custom endpoints by using the HookArgs or MutationHookArgs decorators along with HookMiddleware.
HookArgs
Section intitulée « HookArgs »Used to apply hooks to any query endpoint.
import { Resolver, Query, Args } from '@lib/type-graphql';import { HookArgs, HookMiddleware, HookTypes } from '@lib/type-graphql-query';import { UseMiddleware } from '@lib/type-graphql';import { ProductDTO } from './product.type';import { FindOneArgsType } from '@lib/type-graphql-query';
@Resolver(() => ProductDTO)export class ProductResolver { @Query(() => ProductDTO, { nullable: true }) @UseMiddleware(HookMiddleware(HookTypes.BEFORE_FIND_ONE, ProductDTO)) async findProduct( @HookArgs<FindOneArgsType>() args: FindOneArgsType, ): Promise<ProductDTO | null> { // Hooks from @BeforeFindOne on ProductDTO will be applied return this.service.findById(args.id); }}MutationHookArgs
Section intitulée « MutationHookArgs »Used to apply hooks to any mutation that uses MutationArgsType.
import { Resolver, Mutation, Args } from '@lib/type-graphql';import { MutationHookArgs, HookMiddleware, HookTypes, UpdateManyInputType, MutationArgsType,} from '@lib/type-graphql-query';import { UseMiddleware } from '@lib/type-graphql';import { ProductDTO } from './product.type';import { ProductUpdateDTO } from './product-update.type';
@ArgsType()class UpdateManyProductsArgs extends MutationArgsType( UpdateManyInputType(ProductDTO, ProductUpdateDTO),) {}
@Resolver(() => ProductDTO)export class ProductResolver { @Mutation(() => UpdateManyResponseType()) @UseMiddleware(HookMiddleware(HookTypes.BEFORE_UPDATE_MANY, ProductUpdateDTO)) async markProductsAsCompleted( @MutationHookArgs() { input }: UpdateManyProductsArgs, ): Promise<UpdateManyResponse> { // Hooks from @BeforeUpdateMany on ProductUpdateDTO will be applied return this.service.updateMany( { ...input.update, completed: true, }, input.filter, ); }}When using HookMiddleware you must use the DTO that you added the hook decorator to. For example, if you added @BeforeUpdateMany to ProductUpdateDTO, use ProductUpdateDTO in the middleware, not ProductDTO.
Complete Example
Section intitulée « Complete Example »Here’s a complete example showing how to use hooks:
import { ObjectType, Field } from '@lib/type-graphql';import { BeforeCreateOne, BeforeUpdateOne, CreateOneInputType, UpdateOneInputType,} from '@lib/type-graphql-query';import { ExecutionContext } from '../interfaces';import { ProductInputDTO } from './product-input.type';import { ProductUpdateDTO } from './product-update.type';
@ObjectType('Product')@BeforeCreateOne((input: CreateOneInputType<ProductInputDTO>, context: ExecutionContext) => { input.input.createdBy = context.userId; input.input.createdAt = new Date(); return input;})@BeforeUpdateOne((input: UpdateOneInputType<ProductUpdateDTO>, context: ExecutionContext) => { input.update.updatedBy = context.userId; input.update.updatedAt = new Date(); return input;})export class ProductDTO { @Field() id!: string;
@Field() name!: string;
@Field({ nullable: true }) createdBy?: string;
@Field({ nullable: true }) updatedBy?: string;
@Field({ nullable: true }) createdAt?: Date;
@Field({ nullable: true }) updatedAt?: Date;}Best Practices
Section intitulée « Best Practices »-
Use Hook Classes for Reusability
- Create reusable hook classes that can be shared across multiple DTOs
- Leverage dependency injection for accessing services
-
Keep Hooks Simple
- Hooks should focus on modifying input data
- Avoid complex business logic in hooks
-
Type Safety
- Use TypeScript types to ensure type safety
- Define interfaces for hook input/output types
-
Error Handling
- Handle errors appropriately in hooks
- Don’t swallow errors silently
-
Performance
- Keep hooks lightweight
- Avoid expensive operations in hooks
See Also
Section intitulée « See Also »- Mutations - Learn about mutation endpoints
- Queries - Learn about query endpoints
- Resolvers - Learn about resolver implementation
- Authorization - Learn about authorization