Pular para o conteúdo

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.


DecoratorQuando 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

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

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() roda
2. @CreatedAt → new Date()
3. @UpdatedAt → new Date()
4. INSERT executado

O 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, '');
}

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() roda
2. @UpdatedAt → new Date()
3. UPDATE executado (apenas colunas alteradas)
@BeforeUpdate()
incrementRevision() {
this.revision = (this.revision ?? 0) + 1;
}

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

@AfterLoad não roda em operações bulk como update(data, where), delete(where) ou upsert() — apenas em operações que retornam entidades hidratadas.


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

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

  • @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.