Data-Mapper ORM for TypeScript and ES2021+ supporting MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.
Comprehensive entity lifecycle event system with hooks and subscribers for implementing cross-cutting concerns like auditing, validation, and caching. TypeORM provides both decorator-based entity listeners and class-based subscribers.
Decorators for defining entity lifecycle event handlers directly on entity methods.
/**
* Executed before entity is inserted into database
*/
function BeforeInsert(): MethodDecorator;
/**
* Executed after entity is inserted into database
*/
function AfterInsert(): MethodDecorator;
/**
* Executed before entity is updated in database
*/
function BeforeUpdate(): MethodDecorator;
/**
* Executed after entity is updated in database
*/
function AfterUpdate(): MethodDecorator;
/**
* Executed before entity is removed from database
*/
function BeforeRemove(): MethodDecorator;
/**
* Executed after entity is removed from database
*/
function AfterRemove(): MethodDecorator;
/**
* Executed before entity is soft removed (DeleteDateColumn set)
*/
function BeforeSoftRemove(): MethodDecorator;
/**
* Executed after entity is soft removed
*/
function AfterSoftRemove(): MethodDecorator;
/**
* Executed before soft-deleted entity is recovered
*/
function BeforeRecover(): MethodDecorator;
/**
* Executed after soft-deleted entity is recovered
*/
function AfterRecover(): MethodDecorator;
/**
* Executed after entity is loaded from database
*/
function AfterLoad(): MethodDecorator;Entity Lifecycle Examples:
import {
Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn,
BeforeInsert, AfterInsert, BeforeUpdate, AfterUpdate, AfterLoad
} from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
passwordHash: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@Column({ nullable: true })
lastLoginAt: Date;
// Computed property
fullName: string;
@BeforeInsert()
beforeInsert() {
console.log("About to insert user:", this.email);
// Hash password, validate data, etc.
this.email = this.email.toLowerCase();
}
@AfterInsert()
afterInsert() {
console.log("User inserted with ID:", this.id);
// Send welcome email, create audit log, etc.
}
@BeforeUpdate()
beforeUpdate() {
console.log("About to update user:", this.id);
// Validate changes, update modified fields, etc.
this.email = this.email.toLowerCase();
}
@AfterUpdate()
afterUpdate() {
console.log("User updated:", this.id);
// Clear cache, send notifications, etc.
}
@AfterLoad()
afterLoad() {
// Compute derived properties after loading from database
this.fullName = `${this.firstName} ${this.lastName}`;
}
}Interface for creating global event subscribers that can listen to events across all entities or specific entities.
/**
* Interface for entity event subscribers
* @template Entity - Optional entity type to listen to
*/
interface EntitySubscriberInterface<Entity = any> {
/**
* Specifies which entity this subscriber listens to
* @returns Entity constructor, schema, or string name
*/
listenTo?(): ObjectType<Entity> | EntitySchema<Entity> | string;
/**
* Called after entity is loaded from database
* @param entity - Loaded entity
* @param event - Load event details
*/
afterLoad?(entity: Entity, event?: LoadEvent<Entity>): Promise<any> | void;
/**
* Called before entity is inserted
* @param event - Insert event details
*/
beforeInsert?(event: InsertEvent<Entity>): Promise<any> | void;
/**
* Called after entity is inserted
* @param event - Insert event details
*/
afterInsert?(event: InsertEvent<Entity>): Promise<any> | void;
/**
* Called before entity is updated
* @param event - Update event details
*/
beforeUpdate?(event: UpdateEvent<Entity>): Promise<any> | void;
/**
* Called after entity is updated
* @param event - Update event details
*/
afterUpdate?(event: UpdateEvent<Entity>): Promise<any> | void;
/**
* Called before entity is removed
* @param event - Remove event details
*/
beforeRemove?(event: RemoveEvent<Entity>): Promise<any> | void;
/**
* Called after entity is removed
* @param event - Remove event details
*/
afterRemove?(event: RemoveEvent<Entity>): Promise<any> | void;
/**
* Called before entity is soft removed
* @param event - Soft remove event details
*/
beforeSoftRemove?(event: SoftRemoveEvent<Entity>): Promise<any> | void;
/**
* Called after entity is soft removed
* @param event - Soft remove event details
*/
afterSoftRemove?(event: SoftRemoveEvent<Entity>): Promise<any> | void;
/**
* Called before soft-deleted entity is recovered
* @param event - Recover event details
*/
beforeRecover?(event: RecoverEvent<Entity>): Promise<any> | void;
/**
* Called after soft-deleted entity is recovered
* @param event - Recover event details
*/
afterRecover?(event: RecoverEvent<Entity>): Promise<any> | void;
/**
* Called before transaction starts
* @param event - Transaction start event details
*/
beforeTransactionStart?(event: TransactionStartEvent): Promise<any> | void;
/**
* Called after transaction starts
* @param event - Transaction start event details
*/
afterTransactionStart?(event: TransactionStartEvent): Promise<any> | void;
/**
* Called before transaction commits
* @param event - Transaction commit event details
*/
beforeTransactionCommit?(event: TransactionCommitEvent): Promise<any> | void;
/**
* Called after transaction commits
* @param event - Transaction commit event details
*/
afterTransactionCommit?(event: TransactionCommitEvent): Promise<any> | void;
/**
* Called before transaction rolls back
* @param event - Transaction rollback event details
*/
beforeTransactionRollback?(event: TransactionRollbackEvent): Promise<any> | void;
/**
* Called after transaction rolls back
* @param event - Transaction rollback event details
*/
afterTransactionRollback?(event: TransactionRollbackEvent): Promise<any> | void;
}
/**
* Marks a class as an entity subscriber
*/
function EventSubscriber(): ClassDecorator;Detailed interfaces for different types of entity events.
/**
* Event fired when entity is loaded from database
*/
interface LoadEvent<Entity> {
/** Connection where event occurred */
connection: DataSource;
/** Query runner executing the operation */
queryRunner?: QueryRunner;
/** Entity manager */
manager: EntityManager;
/** Loaded entity */
entity: Entity;
}
/**
* Event fired during entity insert operations
*/
interface InsertEvent<Entity> {
/** Connection where event occurred */
connection: DataSource;
/** Query runner executing the operation */
queryRunner?: QueryRunner;
/** Entity manager */
manager: EntityManager;
/** Entity being inserted */
entity: Entity;
}
/**
* Event fired during entity update operations
*/
interface UpdateEvent<Entity> {
/** Connection where event occurred */
connection: DataSource;
/** Query runner executing the operation */
queryRunner?: QueryRunner;
/** Entity manager */
manager: EntityManager;
/** Entity being updated */
entity: Entity;
/** Database entity state (before update) */
databaseEntity: Entity;
/** Changed columns */
updatedColumns: ColumnMetadata[];
/** Changed relations */
updatedRelations: RelationMetadata[];
}
/**
* Event fired during entity remove operations
*/
interface RemoveEvent<Entity> {
/** Connection where event occurred */
connection: DataSource;
/** Query runner executing the operation */
queryRunner?: QueryRunner;
/** Entity manager */
manager: EntityManager;
/** Entity being removed */
entity: Entity;
/** Entity ID being removed */
entityId: any;
/** Database entity state */
databaseEntity: Entity;
}
/**
* Event fired during soft remove operations
*/
interface SoftRemoveEvent<Entity> {
/** Connection where event occurred */
connection: DataSource;
/** Query runner executing the operation */
queryRunner?: QueryRunner;
/** Entity manager */
manager: EntityManager;
/** Entity being soft removed */
entity: Entity;
/** Database entity state */
databaseEntity: Entity;
}
/**
* Event fired during entity recovery operations
*/
interface RecoverEvent<Entity> {
/** Connection where event occurred */
connection: DataSource;
/** Query runner executing the operation */
queryRunner?: QueryRunner;
/** Entity manager */
manager: EntityManager;
/** Entity being recovered */
entity: Entity;
/** Database entity state */
databaseEntity: Entity;
}
/**
* Event fired when database transaction starts
*/
interface TransactionStartEvent {
/** Connection where event occurred */
connection: DataSource;
/** Query runner executing the transaction */
queryRunner: QueryRunner;
}
/**
* Event fired when database transaction commits
*/
interface TransactionCommitEvent {
/** Connection where event occurred */
connection: DataSource;
/** Query runner executing the transaction */
queryRunner: QueryRunner;
}
/**
* Event fired when database transaction rolls back
*/
interface TransactionRollbackEvent {
/** Connection where event occurred */
connection: DataSource;
/** Query runner executing the transaction */
queryRunner: QueryRunner;
}Entity Subscriber Examples:
import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from "typeorm";
// Global subscriber listening to all entities
@EventSubscriber()
export class GlobalAuditSubscriber implements EntitySubscriberInterface {
beforeInsert(event: InsertEvent<any>) {
console.log(`Inserting entity: ${event.entity.constructor.name}`);
// Log to audit system
}
beforeUpdate(event: UpdateEvent<any>) {
console.log(`Updating entity: ${event.entity.constructor.name}`);
// Compare changes and log
console.log("Changed columns:", event.updatedColumns.map(col => col.propertyName));
}
}
// Subscriber for specific entity
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
beforeInsert(event: InsertEvent<User>) {
// Hash password before insert
if (event.entity.password) {
event.entity.passwordHash = hashPassword(event.entity.password);
delete event.entity.password;
}
}
afterInsert(event: InsertEvent<User>) {
// Send welcome email
sendWelcomeEmail(event.entity.email);
// Create user profile
const profile = new Profile();
profile.user = event.entity;
return event.manager.save(Profile, profile);
}
beforeUpdate(event: UpdateEvent<User>) {
// Check if email changed
if (event.entity.email !== event.databaseEntity.email) {
event.entity.emailVerified = false;
}
}
afterUpdate(event: UpdateEvent<User>) {
// Clear user cache
clearUserCache(event.entity.id);
// Send notification if important field changed
const importantFields = ['email', 'role'];
const changedImportantFields = event.updatedColumns
.map(col => col.propertyName)
.filter(field => importantFields.includes(field));
if (changedImportantFields.length > 0) {
sendAccountChangeNotification(event.entity.email, changedImportantFields);
}
}
beforeRemove(event: RemoveEvent<User>) {
// Archive user data before deletion
archiveUserData(event.entity);
}
}
// Transaction subscriber
@EventSubscriber()
export class TransactionSubscriber implements EntitySubscriberInterface {
afterTransactionCommit(event: TransactionCommitEvent) {
// Clear cache, send events, etc.
console.log("Transaction committed successfully");
}
afterTransactionRollback(event: TransactionRollbackEvent) {
// Log error, send alerts, etc.
console.log("Transaction rolled back");
}
}Complex event handling patterns for enterprise applications.
// Event aggregation subscriber
@EventSubscriber()
export class EventAggregatorSubscriber implements EntitySubscriberInterface {
private events: any[] = [];
afterInsert(event: InsertEvent<any>) {
this.events.push({
type: 'INSERT',
entity: event.entity.constructor.name,
id: event.entity.id,
timestamp: new Date()
});
}
afterUpdate(event: UpdateEvent<any>) {
this.events.push({
type: 'UPDATE',
entity: event.entity.constructor.name,
id: event.entity.id,
changes: event.updatedColumns.map(col => col.propertyName),
timestamp: new Date()
});
}
afterTransactionCommit(event: TransactionCommitEvent) {
// Batch send all events after successful transaction
if (this.events.length > 0) {
sendEventBatch(this.events);
this.events = [];
}
}
afterTransactionRollback(event: TransactionRollbackEvent) {
// Clear events on rollback
this.events = [];
}
}
// Validation subscriber
@EventSubscriber()
export class ValidationSubscriber implements EntitySubscriberInterface {
beforeInsert(event: InsertEvent<any>) {
return this.validateEntity(event.entity, event.manager);
}
beforeUpdate(event: UpdateEvent<any>) {
return this.validateEntity(event.entity, event.manager);
}
private async validateEntity(entity: any, manager: EntityManager) {
// Custom validation logic
const errors = [];
// Example: Check unique constraints
if (entity.email) {
const existingUser = await manager.findOneBy(entity.constructor, {
email: entity.email,
id: Not(entity.id || 0)
});
if (existingUser) {
errors.push('Email already exists');
}
}
if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(', ')}`);
}
}
}
// Caching subscriber
@EventSubscriber()
export class CacheSubscriber implements EntitySubscriberInterface {
afterInsert(event: InsertEvent<any>) {
this.invalidateCache(event.entity.constructor.name);
}
afterUpdate(event: UpdateEvent<any>) {
this.invalidateCache(event.entity.constructor.name);
this.invalidateEntityCache(event.entity.constructor.name, event.entity.id);
}
afterRemove(event: RemoveEvent<any>) {
this.invalidateCache(event.entity.constructor.name);
this.invalidateEntityCache(event.entity.constructor.name, event.entityId);
}
private invalidateCache(entityName: string) {
// Clear list caches
cache.del(`${entityName}:list:*`);
}
private invalidateEntityCache(entityName: string, entityId: any) {
// Clear specific entity cache
cache.del(`${entityName}:${entityId}`);
}
}Configure event subscribers in your DataSource:
import { DataSource } from "typeorm";
import { UserSubscriber, GlobalAuditSubscriber } from "./subscribers";
const dataSource = new DataSource({
// ... other options
subscribers: [
UserSubscriber,
GlobalAuditSubscriber,
// ... other subscribers
],
});
// Or add subscribers programmatically
dataSource.subscribers.push(new UserSubscriber());The event system is ideal for:
@EventSubscriber()
export class PerformantSubscriber implements EntitySubscriberInterface {
// Use async operations carefully
async afterInsert(event: InsertEvent<any>) {
// Don't block the main transaction
setImmediate(() => {
this.sendNotificationAsync(event.entity);
});
}
private async sendNotificationAsync(entity: any) {
// Async operation that doesn't affect main transaction
}
// Batch operations when possible
private pendingOperations: any[] = [];
afterUpdate(event: UpdateEvent<any>) {
this.pendingOperations.push(event);
}
afterTransactionCommit(event: TransactionCommitEvent) {
if (this.pendingOperations.length > 0) {
this.processBatch(this.pendingOperations);
this.pendingOperations = [];
}
}
}Install with Tessl CLI
npx tessl i tessl/npm-typeorm