@nestbolt/audit-log
Auditable Entities
Mark TypeORM entities for automatic change tracking using the @Auditable() decorator and AuditableMixin.
To enable automatic audit logging for a TypeORM entity, you need two things: the @Auditable() class decorator to mark the entity for tracking, and optionally the AuditableMixin to add convenience query methods directly on entity instances.
The @Auditable() Decorator
The @Auditable() decorator is a class decorator that stores metadata on the entity class. The TypeORM subscriber reads this metadata to determine whether and how to track changes on the entity.
Basic Usage
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import { Auditable, AuditableMixin } from "@nestbolt/audit-log";
@Entity("products")
@Auditable()
export class Product extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
name!: string;
@Column({ type: "decimal", precision: 10, scale: 2 })
price!: number;
@Column()
description!: string;
@Column({ default: true })
active!: boolean;
}With no options, @Auditable() tracks all fields (except the always-excluded ones: id, createdAt, updatedAt, created_at, updated_at) for all three actions (created, updated, deleted).
Decorator Options
@Auditable({
auditableType: "AppUser",
only: ["name", "email", "role"],
except: ["password", "token"],
events: ["created", "updated"],
})| Option | Type | Default | Description |
|---|---|---|---|
auditableType | string | Class name | Override the entity type name stored in audit logs |
only | string[] | All fields | Whitelist of fields to track (only these fields will appear in diffs) |
except | string[] | None | Blacklist of fields to exclude from tracking |
events | AuditAction[] | All actions | Limit which actions are tracked ("created", "updated", "deleted") |
auditableType
By default, the entity type stored in audit logs is the class name (e.g., "User", "Product"). Use auditableType to override this, which is useful when your class name differs from the logical entity name or when you have multiple entities that should share the same audit type.
@Entity("app_users")
@Auditable({ auditableType: "User" })
export class AppUserEntity extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() name!: string;
}Audit log entries for this entity will have entityType: "User" rather than entityType: "AppUserEntity".
only (Field Whitelist)
When only is set, only the listed fields are tracked. All other fields are ignored, even if they are not in an except list.
@Entity("users")
@Auditable({ only: ["name", "email", "role"] })
export class User extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() name!: string;
@Column() email!: string;
@Column() role!: string;
@Column() lastLoginAt!: Date; // Not tracked
@Column() loginCount!: number; // Not tracked
@Column() passwordHash!: string; // Not tracked
}This is useful when an entity has many columns but only a few are meaningful for auditing.
except (Field Blacklist)
When except is set, the listed fields are excluded from tracking. All other fields are still tracked.
@Entity("users")
@Auditable({ except: ["passwordHash", "refreshToken", "lastLoginAt"] })
export class User extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() name!: string; // Tracked
@Column() email!: string; // Tracked
@Column() role!: string; // Tracked
@Column() passwordHash!: string; // Excluded
@Column() refreshToken!: string; // Excluded
@Column() lastLoginAt!: Date; // Excluded
}The except list is combined with the module's globalExcludedFields and the always-excluded fields. If both only and except are set, only defines the initial set and except further removes fields from it.
events (Action Filter)
By default, all three actions are tracked: "created", "updated", and "deleted". Use events to limit tracking to specific actions.
// Only track creation and deletion, not updates
@Entity("sessions")
@Auditable({ events: ["created", "deleted"] })
export class Session extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() userId!: string;
@Column() expiresAt!: Date;
}// Only track updates (useful for entities created via seeding/migrations)
@Entity("settings")
@Auditable({ events: ["updated"] })
export class Setting extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() key!: string;
@Column() value!: string;
}The entity-level events filter is combined with the module-level disabledActions. An action must pass both filters to be logged.
The AuditableMixin
The AuditableMixin is a mixin function that extends your entity's base class with audit log query methods. It is optional -- you can use @Auditable() without it and query logs through AuditLogService instead.
Usage
import { BaseEntity } from "typeorm";
import { AuditableMixin } from "@nestbolt/audit-log";
@Entity("orders")
@Auditable()
export class Order extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() status!: string;
@Column({ type: "decimal" }) total!: number;
}The mixin works with any base class, not just BaseEntity:
import { AuditableMixin } from "@nestbolt/audit-log";
// Works with a custom base class
abstract class TimestampedEntity {
@CreateDateColumn() createdAt!: Date;
@UpdateDateColumn() updatedAt!: Date;
}
@Entity("invoices")
@Auditable()
export class Invoice extends AuditableMixin(TimestampedEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() amount!: number;
}Mixin Methods
| Method | Return Type | Description |
|---|---|---|
getAuditLogs(options?) | Promise<AuditLogEntity[]> | Get audit logs for this entity instance |
getLatestAuditLog() | Promise<AuditLogEntity | null> | Get the most recent audit log entry |
getAuditableType() | string | Get the entity type name (from decorator or class name) |
getAuditableId() | string | Get the entity's ID as a string |
getAuditLogs(options?)
Retrieves all audit log entries for this specific entity instance. Accepts the same query options as AuditLogService.getAuditLogs():
const order = await orderRepo.findOneByOrFail({ id: orderId });
// Get all audit logs
const allLogs = await order.getAuditLogs();
// Get only updates from the last week
const recentUpdates = await order.getAuditLogs({
action: "updated",
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
});
// Get the first 10 logs
const paginated = await order.getAuditLogs({ limit: 10, offset: 0 });The query options interface:
interface AuditLogQueryOptions {
action?: "created" | "updated" | "deleted";
from?: Date;
to?: Date;
limit?: number;
offset?: number;
}getLatestAuditLog()
Returns the most recent audit log entry for this entity instance, or null if no logs exist:
const order = await orderRepo.findOneByOrFail({ id: orderId });
const latest = await order.getLatestAuditLog();
if (latest) {
console.log(`Last change: ${latest.action} at ${latest.createdAt}`);
console.log(`Changed by: ${latest.actorType} ${latest.actorId}`);
}getAuditableType()
Returns the entity type name used in audit logs. This is the value from auditableType in the decorator options, or the class name if not specified:
@Entity("app_users")
@Auditable({ auditableType: "User" })
export class AppUser extends AuditableMixin(BaseEntity) { /* ... */ }
const user = new AppUser();
user.getAuditableType(); // "User"getAuditableId()
Returns the entity's primary key as a string:
const user = await userRepo.findOneByOrFail({ id: "abc-123" });
user.getAuditableId(); // "abc-123"Using @Auditable() Without the Mixin
The AuditableMixin is optional. If you prefer to query audit logs through AuditLogService directly, you can use @Auditable() alone:
@Entity("payments")
@Auditable({ except: ["cardNumber", "cvv"] })
export class Payment extends BaseEntity {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() amount!: number;
@Column() status!: string;
@Column() cardNumber!: string;
@Column() cvv!: string;
}Changes to this entity are still tracked automatically. You query the logs through the service:
const logs = await auditLogService.getAuditLogs("Payment", paymentId);Full Entity Examples
User Entity with Sensitive Field Exclusion
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import { Auditable, AuditableMixin } from "@nestbolt/audit-log";
@Entity("users")
@Auditable({
except: ["passwordHash", "refreshToken", "twoFactorSecret"],
})
export class User extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
name!: string;
@Column({ unique: true })
email!: string;
@Column({ default: "member" })
role!: string;
@Column()
passwordHash!: string;
@Column({ nullable: true })
refreshToken!: string | null;
@Column({ nullable: true })
twoFactorSecret!: string | null;
@Column({ default: true })
active!: boolean;
}Order Entity with Custom Type Name
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
BaseEntity,
} from "typeorm";
import { Auditable, AuditableMixin } from "@nestbolt/audit-log";
import { User } from "./user.entity";
@Entity("customer_orders")
@Auditable({
auditableType: "Order",
only: ["status", "total", "shippingAddress"],
})
export class CustomerOrder extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
status!: string;
@Column({ type: "decimal", precision: 10, scale: 2 })
total!: number;
@Column()
shippingAddress!: string;
@Column({ type: "jsonb" })
lineItems!: Record<string, any>[];
@ManyToOne(() => User)
user!: User;
@Column()
userId!: string;
@Column({ nullable: true })
internalNotes!: string;
}In this example, only status, total, and shippingAddress are tracked. Changes to lineItems, userId, or internalNotes are not recorded.
Configuration Entity (Updates Only)
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import { Auditable, AuditableMixin } from "@nestbolt/audit-log";
@Entity("app_settings")
@Auditable({
auditableType: "Setting",
events: ["updated"],
})
export class AppSetting extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column({ unique: true })
key!: string;
@Column({ type: "text" })
value!: string;
@Column({ nullable: true })
description!: string;
}This entity only logs updates. Creation and deletion events are ignored, which is useful for seed-managed configuration data.
Next Steps
- Manual Logging -- log operations that bypass the TypeORM subscriber.
- Querying -- learn the full query API for retrieving audit logs.