Hook

Hooks are the place that you can add your own code to the default behavior or DryerJS. Below is a example of a hook that will be called before a new Tag is created. If the tag already exists, it will throw an error.

@Definition()
export class Tag {
  @Id()
  id: ObjectId;
 
  @Property()
  name: string;
}
 
@Injectable()
class TagHook {
  constructor(@InjectBaseService(Tag) public tagService: BaseService<Tag, Context>) {}
 
  @BeforeCreateHook(() => Tag)
  async throwErrorIfNameAlreadyExists({ input }: BeforeCreateHookInput<Tag, Context>) {
    const existingTag = await this.tagService.model.findOne({ name: input.name });
    if (existingTag) {
      throw new Error(`Tag with name ${input.name} already exists`);
    }
  }
}
 
@Module({
  imports: [
    // other imports
    DryerModule.register({ definitions: [Tag], hooks: [TagHook] }),
  ],
})
export class AppModule {}

List of hooks

Below is the list of hooks that you can use.

BeforeCreateHook
AfterCreateHook
BeforeUpdateHook
AfterUpdateHook
BeforeRemoveHook
AfterRemoveHook
BeforeFindOneHook
AfterFindOneHook
BeforeFindManyHook
AfterFindManyHook
BeforeReadFilterHook
BeforeWriteFilterHook

Below is the types of hooks input.

type AfterRemoveHookInput<T = any, Context = any> = {
  ctx: Context;
  removed: T;
  definition: Definition;
  options: RemoveOptions;
};
 
type BeforeRemoveHookInput<T = any, Context = any> = {
  ctx: Context;
  beforeRemoved: T;
  definition: Definition;
  options: RemoveOptions;
};
 
type AfterFindOneHookInput<T = any, Context = any> = {
  ctx: Context;
  filter: FilterQuery<T>;
  result: T;
  definition: Definition;
};
 
type BeforeFindOneHookInput<T = any, Context = any> = {
  ctx: Context;
  filter: FilterQuery<T>;
  definition: Definition;
};
 
type AfterUpdateHookInput<T = any, Context = any> = {
  ctx: Context;
  input: Partial<T>;
  updated: T;
  beforeUpdated: T;
  definition: Definition;
};
 
type BeforeUpdateHookInput<T = any, Context = any> = {
  ctx: Context;
  input: Partial<T>;
  beforeUpdated: T;
  definition: Definition;
};
 
type AfterCreateHookInput<T = any, Context = any> = {
  ctx: Context;
  input: Partial<T>;
  created: T;
  definition: Definition;
};
 
type BeforeCreateHookInput<T = any, Context = any> = {
  ctx: Context;
  input: Partial<T>;
  definition: Definition;
};
 
type BeforeFindManyHookInput<T = any, Context = any> = {
  ctx: Context;
  filter: FilterQuery<T>;
  sort: object;
  limit?: number;
  page?: number;
  definition: Definition;
};
 
type AfterFindManyHookInput<T = any, Context = any> = {
  ctx: Context;
  filter: FilterQuery<T>;
  sort: object;
  items: T[];
  limit?: number;
  page?: number;
  definition: Definition;
};
 
type BeforeReadFilterHookInput<T = any, Context = any> = {
  ctx: Context;
  filter: FilterQuery<T>;
  definition: Definition;
};
 
type BeforeWriteFilterHookInput<T = any, Context = any> = {
  ctx: Context;
  filter: FilterQuery<T>;
  definition: Definition;
};
💡

API update and remove use findOne under the hood so you might feel like beforeFindOne and afterFindOne are applied for update and remove too. If your context cannot findOne a document, you will not be able to update and remove it.

Special hooks

beforeReadFilter

beforeReadFilter will be called before findMany and findOne, paginate. This hook is called before beforeFindOne and beforeFindMany.

beforeWriteFilter

beforeWriteFilter will be called before update, remove. This hook is called before beforeUpdate, beforeRemove.

beforeFindMany

beforeFindMany will be called before findMany and paginate and after beforeReadFilter.

Customize Context

There is a ctx in every hook. By default, the value is an object which looks like below.

{ req: express.Request }

How NestJS passing context

You can skip this sub-section if you are familiar with NestJS

In NestJS world, it's common practice to attach properties to the request. In side your Guard, you can add user to req, and then create a custom parameter to use it in controllers or resolvers:

@Injectable()
export class MustBeUser implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const { req } = GqlExecutionContext.create(context).getContext();
    // getUserFromReq is a function that will get user from req
    // commonly it will parse a JWT token from header
    const user = await getUserFromReq(req);
    req.user = user;
  }
}
 
export const Ctx = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
  return GqlExecutionContext.create(ctx).getContext().req.user || null;
});
 
@Resolver(() => Tag)
export class TagResolver {
  @Query(() => Tag)
  async allTags(@Ctx() ctx: User) {
    return this.tagService.findMany({ userId: ctx.id });
  }
}

Usage on DryerJS

You can set the parameter decorator that returns the context object when calling DryerModule.register.

export const Ctx = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
  return GqlExecutionContext.create(ctx).getContext().req.user || null;
});
 
// app.module.ts
@Module({
  imports: [
    // other imports
    DryerModule.register({
      definitions: [Tag, User],
      contextDecorator: Ctx,
    }),
  ],
})
export class AppModule {}
 

Possible use cases

beforeCreate & beforeUpdate

  • Add custom input validation (e.g. check if email is used so we can throw a better error)
  • Modify the input based on context (e.g. add userId from context to input so that the user does not need to input it)

beforeFindOne & beforeFindMany

  • Validate filter (e.g. check if the user has permission to see the data)
  • Modify filter based on context (e.g. add userId from context to filter so that the user can only see their own data)

afterFindOne & afterFindMany

  • Validate the returned data (e.g. check if the user has permission to see the data)
  • Modify the returned data based on context (e.g. remove some fields that the user does not have permission to see)

afterCreate & afterUpdate & afterRemove

  • Emit events (e.g. send emails to users after they register)

beforeReadFilter

  • Same as beforeFindOne & beforeFindMany but will be called before beforeFindOne & beforeFindMany. This is helpful when you do not want to duplicate your code in beforeFindOne & beforeFindMany.

beforeWriteFilter

  • Modify filter based on context on update and remove (e.g. add userId from context to filter so that the user can only update or remove their own data)

Priority

If multiple hooks are defined for the same method, they will be sorted by priority. If you do not specify the priority, it will be set to 100.

@Injectable()
export class ProductHook {
  @BeforeCreateHook(() => Product, { priority: 1 })
  async assignUserIdIfNotMentioned({
    input,
    ctx,
  }: BeforeCreateHookInput<Product, Context>) {
    // assign userId if not provided
    if (_.isNil(input.userId)) {
      input.userId = ctx.user.id;
    }
  }
 
  @BeforeCreateHook(() => Product, { priority: 2 })
  async ensureNormalUserNotCreateProductForOtherUsers({
    input,
    ctx,
  }: BeforeCreateHookInput<Product, Context>) {
    if (ctx.user.role === UserRole.ADMIN) return;
    // only admin can create product for other user
    if (ctx.user.id.toString() === input.userId.toString()) return;
    throw new UnauthorizedException('USER_ID_NOT_MATCH');
  }
}

On the above example, assignUserIdIfNotMentioned will be called before ensureNormalUserNotCreateProductForOtherUsers.

General Hook

You can also create a general hook that will be called for all models.

class GeneralHook implements Hook {
  @BeforeCreateHook(() => AllDefinitions)
  async logOnCreate({ input }: BeforeCreateHookInput) {
    console.log(`Before create ${definition.name}`);
  }
}
💡

AllDefinitions is a special value that you can use to indicate that the hook will be called for all models which can be imported from 'dryerjs'.

Default hook

DryerJS has a default hook that will be called for all models. It is mostly used for checking relations before write operations. It also removes related documents when remove mode is CleanUpRelationsAfterRemoved. See Remove Mode for more details.

You can skip the default hook by setting skipDefaultHookMethods in @Definition decorator.

@Definition({
  skipDefaultHookMethods: [
    'beforeCreate',
    'beforeUpdate',
    'beforeRemove',
  ],
})
class Tag {}
💡

If you want to ignore default hook completely, you can set { skipDefaultHookMethods: ['all'] }