Skip to content

Hooks & Context

Lifecycle hooks

Hooks let you run logic before and after each operation. They are injectable NestJS services and receive fully typed arguments.

@Injectable()
export class PostHooks extends RelayerHooks<PostEntity, EM, AppContext> {
private readonly logger = new Logger(PostHooks.name);
afterFind(entities: PostEntity[], ctx: AppContext): void {
this.logger.log(`Found ${entities.length} posts`);
}
beforeCreate(data: Partial<PostEntity>, ctx: AppContext) {
return { ...data, slug: slugify(data.title!) };
}
afterCreate(entity: PostEntity, ctx: AppContext): void {
this.logger.log(`Post created: ${entity.id}`);
}
}

Register in the resolver config and module providers:

@GqlResolver(PostEntity, { hooks: PostHooks })
export class PostsResolver extends RelayerResolver<PostEntity, EM, AppContext> { ... }
@Module({
providers: [PostsService, PostsResolver, PostHooks],
})
export class PostsModule {}

Available hooks

HookArgumentsCan modify result?
beforeCreate(data, ctx)Return modified data
afterCreate(entity, ctx)
beforeUpdate(data, where, ctx)Return modified data
afterUpdate(entity, ctx)
beforeDelete(where, ctx)
afterDelete(entity, ctx)
beforeFind(options, ctx)
afterFind(entities, ctx)Return modified list
beforeFindOne(options, ctx)
afterFindOne(entity, ctx)Return modified entity
beforeCount(options, ctx)
beforeAggregate(options, ctx)
afterAggregate(result, ctx)Return modified result
beforeRelation(op, name, ids, ctx)Return modified ids
afterRelation(op, name, ids, ctx)

Hooks that “can modify result” can return a new value to override the original. Return undefined to keep the original.

The hooks API is shared with @relayerjs/nestjs-crud. See the NestJS CRUD Hooks documentation for detailed examples.

Typed context

Override buildContext and buildQueryContext in the resolver to extract request data and pass it through the entire pipeline:

interface AppContext {
request: unknown;
currentUser: { id: number; role: 'admin' | 'user' };
}
interface AppQueryContext {
currentUserId: number;
isAdmin: boolean;
}
@GqlResolver(PostEntity, { name: 'Post' })
export class PostsResolver extends RelayerResolver<PostEntity, EM, AppContext, AppQueryContext> {
constructor(postsService: PostsService) {
super(postsService);
}
protected buildContext(req: unknown): AppContext {
const request = req as { headers?: Record<string, string> };
return {
request,
currentUser: {
id: Number(request?.headers?.['x-user-id'] ?? '0'),
role: (request?.headers?.['x-user-role'] ?? 'user') as 'admin' | 'user',
},
};
}
protected buildQueryContext(ctx: AppContext): AppQueryContext {
return {
currentUserId: ctx.currentUser.id,
isAdmin: ctx.currentUser.role === 'admin',
};
}
}

Row-level scoping

The query context is forwarded to the service, where getDefaultWhere can use it for row-level security:

@Injectable()
export class PostsService extends RelayerService<PostEntity, EM, AppQueryContext> {
constructor(@InjectRelayer() r: RelayerInstance<EM>) {
super(r, PostEntity);
}
protected getDefaultWhere(
upstream?: Where<PostEntity, EM>,
ctx?: AppQueryContext,
): Where<PostEntity, EM> | undefined {
if (!ctx || ctx.isAdmin) return upstream;
const scoped: Where<PostEntity, EM> = {
OR: [{ published: true }, { authorId: ctx.currentUserId }],
};
return upstream ? { AND: [upstream, scoped] } : scoped;
}
}

This scoping applies to every operation: reads, writes, counts, aggregates. A non-admin trying to update or delete someone else’s post gets a no-op because the scoped where filters it out before the SQL runs.

The service API is shared with @relayerjs/nestjs-crud. See the NestJS CRUD Query Service documentation for the full service reference.