Lifecycle Hooks
Hooks permitem executar lógica antes de inserções, atualizações e após o carregamento de entidades. São métodos da própria classe decorados com um dos três hooks disponíveis.
Hooks Disponíveis
Seção intitulada “Hooks Disponíveis”| Decorator | Quando dispara |
|---|---|
@BeforeInsert() | Antes do INSERT — após dirty check, antes dos timestamps |
@BeforeUpdate() | Antes do UPDATE — após dirty check, antes de @UpdatedAt |
@AfterLoad() | Após qualquer SELECT que hidrate a entidade |
Declaração
Seção intitulada “Declaração”Decore um método da entidade com o hook desejado. O método não recebe argumentos — use this para acessar a instância:
import { Entity, PrimaryColumn, Column, BeforeInsert, BeforeUpdate, AfterLoad } from 'mirror-orm';
@Entity('users')class User { @PrimaryColumn({ strategy: 'uuid_v7' }) id!: string;
@Column() email!: string;
@Column({ select: false }) passwordHash!: string;
@Column() emailNormalized!: string;
@BeforeInsert() normalizeEmail() { this.emailNormalized = this.email.toLowerCase().trim(); }
@BeforeUpdate() onUpdate() { this.emailNormalized = this.email.toLowerCase().trim(); }
@AfterLoad() onLoad() { // Chamado toda vez que esta entidade for carregada do banco console.log(`User ${this.id} carregado`); }}@BeforeInsert
Seção intitulada “@BeforeInsert”Roda antes de cada INSERT. Útil para normalização, geração de campos derivados ou validação de negócio.
Ordem de execução no INSERT:
1. @BeforeInsert() roda2. @CreatedAt → new Date()3. @UpdatedAt → new Date()4. INSERT executadoO hook não pode impedir a atribuição dos timestamps — eles sempre são definidos após o hook. Se você alterar campos de timestamp no hook, eles serão sobrescritos.
@BeforeInsert()prepareSlug() { this.slug = this.title .toLowerCase() .replace(/\s+/g, '-') .replace(/[^\w-]/g, '');}@BeforeUpdate
Seção intitulada “@BeforeUpdate”Roda antes de cada UPDATE. Chamado apenas quando save() detecta colunas alteradas pelo dirty check — se nada mudou, o hook não roda.
Ordem de execução no UPDATE:
1. @BeforeUpdate() roda2. @UpdatedAt → new Date()3. UPDATE executado (apenas colunas alteradas)@BeforeUpdate()incrementRevision() { this.revision = (this.revision ?? 0) + 1;}@AfterLoad
Seção intitulada “@AfterLoad”Roda após qualquer operação que hidrate a entidade: find(), findOne(), findById(), findAll(), e também ao carregar relações.
@AfterLoad()decryptSensitiveField() { if (this.encryptedData) { this.data = decrypt(this.encryptedData); }}
@AfterLoadnão roda em operações bulk comoupdate(data, where),delete(where)ouupsert()— apenas em operações que retornam entidades hidratadas.
Hooks Assíncronos
Seção intitulada “Hooks Assíncronos”Todos os hooks suportam async. O Mirror aguarda cada hook antes de prosseguir:
@BeforeInsert()async validateWithExternalService() { const isValid = await externalValidator.check(this.email);
if (!isValid) { throw new Error(`Email inválido: ${this.email}`); }}Hooks rodam em série — se uma entidade tiver múltiplos hooks do mesmo tipo, cada um é aguardado antes do próximo iniciar.
Lançar uma exceção dentro de um hook cancela a operação e, se houver uma transação ativa, dispara o rollback automaticamente:
@BeforeInsert()async checkUniqueness() { const exists = await conn.getRepository(User).exists({ email: this.email });
if (exists) { throw new Error('Email já cadastrado'); // → operação cancelada }}Hooks e Transações
Seção intitulada “Hooks e Transações”Hooks rodam dentro do contexto de transação ativo via AsyncLocalStorage. Queries feitas dentro de um hook participam da mesma transação:
@BeforeInsert()async createAuditEntry() { // Esta query participa da mesma transação que o INSERT da entidade await conn.getRepository(AuditLog).save( Object.assign(new AuditLog(), { action: 'user.created', targetId: this.id, }) );}Herança com @ChildEntity
Seção intitulada “Herança com @ChildEntity”Filhos herdam os hooks do pai. Se o filho declarar o mesmo tipo de hook, ele substitui o do pai — não há composição automática:
@Entity({ tableName: 'notifications', discriminatorColumn: 'type' })class Notification { @BeforeInsert() setDefaults() { this.readAt = null; }}
@ChildEntity('email')class EmailNotification extends Notification { @Column() toAddress!: string;
@BeforeInsert() validate() { // O setDefaults() do pai NÃO roda — este hook substitui if (!this.toAddress) throw new Error('toAddress obrigatório'); }}
@ChildEntity('push')class PushNotification extends Notification { // Sem @BeforeInsert declarado → herda setDefaults() do pai}Se precisar do comportamento do pai no filho, chame explicitamente:
@BeforeInsert()validate() { super.setDefaults(); // chama o hook do pai manualmente if (!this.toAddress) throw new Error('toAddress obrigatório');}O que os hooks não suportam
Seção intitulada “O que os hooks não suportam”@AfterInsert,@AfterUpdate,@BeforeRemove,@AfterRemove— não existem- Receber argumentos no método do hook
- Cancelar a operação via valor de retorno — lance uma exceção
- Rodar em operações bulk (
update(data, where),delete(where),upsert())
Ver também: Para herança de hooks entre subtipos, veja
@ChildEntity. Hooks que fazem queries participam automaticamente de transações ativas — veja Transactions.