Lifecycle Hooks
Hooks allow you to execute logic before insertions, updates and after loading entities. They are methods of the class itself decorated with one of the three available hooks.
Hooks Available
Section titled “Hooks Available”| Decorator | When it fires |
|---|---|
@BeforeInsert() | Before INSERT — after dirty check, before timestamps |
@BeforeUpdate() | Before UPDATE — after dirty check, before @UpdatedAt |
@AfterLoad() | After any SELECT that hydrates the entity |
Declaration
Section titled “Declaration”Decorate an entity method with the desired hook. The method takes no arguments — use this to access the instance:
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
Section titled “@BeforeInsert”Runs before every INSERT. Useful for normalization, generation of derived fields or business validation.
Execution order in INSERT:
1. @BeforeInsert() roda2. @CreatedAt → new Date()3. @UpdatedAt → new Date()4. INSERT executadoThe hook cannot prevent the assignment of timestamps — they are always set after the hook. If you change timestamp fields in the hook, they will be overwritten.
@BeforeInsert()prepareSlug() { this.slug = this.title .toLowerCase() .replace(/\s+/g, '-') .replace(/[^\w-]/g, '');}@BeforeUpdate
Section titled “@BeforeUpdate”Runs before every UPDATE. Called only when save() detects columns changed by dirty check — if nothing has changed, the hook does not run.
Execution order in UPDATE:
1. @BeforeUpdate() roda2. @UpdatedAt → new Date()3. UPDATE executado (apenas colunas alteradas)@BeforeUpdate()incrementRevision() { this.revision = (this.revision ?? 0) + 1;}@AfterLoad
Section titled “@AfterLoad”Runs after any operation that hydrates the entity: find(), findOne(), findById(), findAll(), and also when loading relations.
@AfterLoad()decryptSensitiveField() { if (this.encryptedData) { this.data = decrypt(this.encryptedData); }}
@AfterLoaddoes not run in bulk operations such asupdate(data, where),delete(where)orupsert()— only in operations that return hydrated entities.
Asynchronous Hooks
Section titled “Asynchronous Hooks”All hooks support async. Mirror waits for each hook before proceeding:
@BeforeInsert()async validateWithExternalService() { const isValid = await externalValidator.check(this.email);
if (!isValid) { throw new Error(`Email inválido: ${this.email}`); }}Hooks run in series — if an entity has multiple hooks of the same type, each one is waited before the next one starts.
Throwing an exception within a hook cancels the operation and, if there is an active transaction, triggers the rollback automatically:
@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 and Transactions
Section titled “Hooks and Transactions”Hooks run within the active transaction context via AsyncLocalStorage. Queries made within a hook participate in the same transaction:
@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, }) );}Inheritance with @ChildEntity
Section titled “Inheritance with @ChildEntity”Children inherit their father’s hooks. If the child declares the same hook type, it overrides the parent’s — there is no automatic composition:
@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}If you need the parent’s behavior in the child, explicitly call:
@BeforeInsert()validate() { super.setDefaults(); // chama o hook do pai manualmente if (!this.toAddress) throw new Error('toAddress obrigatório');}What hooks don’t support
Section titled “What hooks don’t support”@AfterInsert,@AfterUpdate,@BeforeRemove,@AfterRemove— do not exist- Receive arguments in the hook method
- Cancel operation via return value — throw an exception
- Run in bulk operations (
update(data, where),delete(where),upsert())
See also:
@ChildEntityhook inheritance is covered in Inheritance & Composition. Hooks that run queries automatically participate in active transactions — see Transactions.