Aller au contenu

Hooks

type-graphql-query provides the following hooks that allow you to modify incoming requests.

  • @BeforeFindOne - invoked before any findById query.
  • @BeforeQueryMany - invoked before any queryMany query.
  • @BeforeCreateOne - invoked before any createOne mutation.
  • @BeforeCreateMany - invoked before any createMany mutation.
  • @BeforeUpdateOne - invoked before any updateOne mutation.
  • @BeforeUpdateMany - invoked before any updateMany mutation.
  • @BeforeDeleteOne - invoked before any deleteOne mutation.
  • @BeforeDeleteMany - invoked before any deleteMany 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 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;
}

The @BeforeCreateOne decorator can be used to modify incoming createOne mutations with information from the GraphQL context.

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;
}

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;
}

The @BeforeCreateMany decorator can be used to modify incoming createMany mutations with information from the GraphQL context.

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;
}

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;
}

The @BeforeUpdateOne decorator can be used to modify incoming updateOne mutations with information from the GraphQL context.

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;
}

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;
}

The @BeforeUpdateMany decorator can be used to modify incoming updateMany mutations with information from the GraphQL context.

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;
}

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;
}

The @BeforeDeleteOne decorator can be used to modify incoming deleteOne mutations with information from the GraphQL context.

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;
}
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;
}
}

The @BeforeDeleteMany decorator can be used to modify incoming deleteMany mutations with information from the GraphQL context.

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;
}
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;
}
}

The @BeforeFindOne decorator can be used to modify incoming findById queries with information from the GraphQL context.

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;
}

The @BeforeQueryMany decorator can be used to modify incoming queryMany queries with information from the GraphQL context.

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;
}

You can also use hooks in custom endpoints by using the HookArgs or MutationHookArgs decorators along with HookMiddleware.

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);
}
}

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.

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;
}
  1. Use Hook Classes for Reusability

    • Create reusable hook classes that can be shared across multiple DTOs
    • Leverage dependency injection for accessing services
  2. Keep Hooks Simple

    • Hooks should focus on modifying input data
    • Avoid complex business logic in hooks
  3. Type Safety

    • Use TypeScript types to ensure type safety
    • Define interfaces for hook input/output types
  4. Error Handling

    • Handle errors appropriately in hooks
    • Don’t swallow errors silently
  5. Performance

    • Keep hooks lightweight
    • Avoid expensive operations in hooks