Automatic entity change tracking and audit logging for NestJS with TypeORM.
This package provides an audit logging system for NestJS that automatically tracks insert, update, and delete operations on your entities with old/new value diffs, actor tracking, and configurable field filtering.
Once installed, using it is as simple as:
@Entity("users")
@Auditable({ except: ["password"] })
export class User extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() name!: string;
@Column() password!: string;
}
// Changes are tracked automatically via .save() and .remove()
const logs = await auditLogService.getAuditLogs("User", userId);- Installation
- Quick Start
- Module Configuration
- Using the @Auditable() Decorator
- Actor Resolution
- Manual Logging
- Query API
- Entity Mixin
- Events
- Using the Service Directly
- Configuration Options
- Audit Log Entity
- Standalone Usage
- Testing
- Changelog
- Contributing
- Security
- Credits
- License
Install the package via npm:
npm install @nestbolt/audit-logOr via yarn:
yarn add @nestbolt/audit-logOr via pnpm:
pnpm add @nestbolt/audit-logThis package requires the following peer dependencies, which you likely already have in a NestJS project:
@nestjs/common ^10.0.0 || ^11.0.0
@nestjs/core ^10.0.0 || ^11.0.0
@nestjs/typeorm ^10.0.0 || ^11.0.0
typeorm ^0.3.0
reflect-metadata ^0.1.13 || ^0.2.0
npm install @nestjs/event-emitter # For audit.logged eventsimport { AuditLogModule } from "@nestbolt/audit-log";
@Module({
imports: [
TypeOrmModule.forRoot({
/* ... */
}),
AuditLogModule.forRoot({
globalExcludedFields: ["password", "token"],
}),
],
})
export class AppModule {}import { Auditable, AuditableMixin } from "@nestbolt/audit-log";
@Entity("users")
@Auditable({ except: ["passwordHash"] })
export class User extends AuditableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() name!: string;
@Column() email!: string;
@Column() passwordHash!: string;
}// Insert — creates audit log with action "created"
const user = userRepo.create({ name: "Alice", email: "alice@example.com" });
await userRepo.save(user);
// Update — creates audit log with action "updated" and field diff
user.name = "Bob";
await userRepo.save(user);
// audit log: oldValues: { name: "Alice" }, newValues: { name: "Bob" }
// Delete — creates audit log with action "deleted"
await userRepo.remove(user);Important: Only
.save()and.remove()trigger TypeORM subscriber events.Repository.update()andQueryBuilder.update()do not trigger audit logging.
The module is registered globally — you only need to import it once.
AuditLogModule.forRoot({
defaultActor: { type: "System", id: "system" },
actorResolver: RequestActorResolver,
disabledActions: ["deleted"],
globalExcludedFields: ["password", "token", "secret"],
metadata: { app: "my-app", version: "1.0" },
});AuditLogModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
globalExcludedFields: config.get("audit.excludedFields"),
defaultActor: { type: "System", id: "system" },
}),
});The @Auditable() class decorator marks an entity for automatic change tracking:
@Auditable({
auditableType: "AppUser", // Override entity type name (defaults to class name)
only: ["name", "email", "role"], // Only track these fields (whitelist)
except: ["password", "token"], // Exclude these fields (blacklist)
events: ["created", "deleted"], // Only track specific actions (won't track updates)
})| Option | Type | Default | Description |
|---|---|---|---|
auditableType |
string |
Class name | Override the entity type name in audit logs |
only |
string[] |
— | Whitelist of fields to track (overrides except) |
except |
string[] |
— | Blacklist of fields to exclude from tracking |
events |
AuditAction[] |
All actions | Limit which actions are tracked |
Implement the ActorResolver interface to track who made changes:
import { ActorResolver, AuditActor } from "@nestbolt/audit-log";
import { ClsService } from "nestjs-cls";
@Injectable()
export class RequestActorResolver implements ActorResolver {
constructor(private readonly cls: ClsService) {}
resolve(): AuditActor | null {
const userId = this.cls.get("userId");
if (!userId) return null;
return { type: "User", id: userId };
}
}Register it in the module:
AuditLogModule.forRoot({
actorResolver: RequestActorResolver,
});The resolver is called automatically for both subscriber-based and manual logging (when no explicit actor is provided). If no resolver is configured, the defaultActor from options is used. If neither is set, actor fields are null.
Use AuditLogService.log() for cases not covered by the automatic subscriber:
await auditLogService.log({
action: "updated",
entityType: "User",
entityId: userId,
oldValues: { status: "active" },
newValues: { status: "banned" },
actor: { type: "Admin", id: adminId },
metadata: { reason: "Violation of TOS" },
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0",
});// By entity
const logs = await auditLogService.getAuditLogs("User", userId, {
action: "updated",
from: new Date("2024-01-01"),
to: new Date("2024-12-31"),
limit: 10,
offset: 0,
});
// By actor
const logs = await auditLogService.getAuditLogsByActor("Admin", adminId);
// Latest entry
const latest = await auditLogService.getLatestAuditLog("User", userId);The AuditableMixin adds convenience methods directly on your entity:
@Entity("users")
@Auditable()
export class User extends AuditableMixin(BaseEntity) {
// ...
}
// Usage
const logs = await user.getAuditLogs();
const logs = await user.getAuditLogs({ action: "updated", limit: 5 });
const latest = await user.getLatestAuditLog();| Method | Returns | Description |
|---|---|---|
getAuditLogs(options?) |
Promise<AuditLogEntity[]> |
Get audit logs for this entity |
getLatestAuditLog() |
Promise<AuditLogEntity | null> |
Get the most recent audit log |
getAuditableType() |
string |
Get the entity type name |
getAuditableId() |
string |
Get the entity ID |
When @nestjs/event-emitter is installed, the package emits:
| Event | Payload | When |
|---|---|---|
audit.logged |
{ auditLog: AuditLogEntity } |
After any audit log is created |
import { AUDIT_LOG_EVENTS, AuditLoggedEvent } from "@nestbolt/audit-log";
import { OnEvent } from "@nestjs/event-emitter";
@OnEvent(AUDIT_LOG_EVENTS.LOGGED)
handleAuditLog(event: AuditLoggedEvent) {
console.log(`${event.auditLog.action} on ${event.auditLog.entityType}`);
}Inject AuditLogService for manual logging and querying:
import { AuditLogService } from "@nestbolt/audit-log";
@Injectable()
export class MyService {
constructor(private readonly auditLogService: AuditLogService) {}
async banUser(userId: string, adminId: string) {
// ... ban logic ...
await this.auditLogService.log({
action: "updated",
entityType: "User",
entityId: userId,
oldValues: { status: "active" },
newValues: { status: "banned" },
actor: { type: "Admin", id: adminId },
});
}
}| Method | Returns | Description |
|---|---|---|
log(params) |
Promise<AuditLogEntity> |
Create a manual audit log entry |
getAuditLogs(entityType, entityId, options?) |
Promise<AuditLogEntity[]> |
Query logs by entity |
getAuditLogsByActor(actorType, actorId, options?) |
Promise<AuditLogEntity[]> |
Query logs by actor |
getLatestAuditLog(entityType, entityId) |
Promise<AuditLogEntity | null> |
Get most recent log for entity |
resolveActor() |
Promise<AuditActor | null> |
Resolve the current actor |
| Option | Type | Default | Description |
|---|---|---|---|
defaultActor |
{ type: string; id: string } |
— | Default actor when no resolver is configured |
actorResolver |
Type<ActorResolver> |
— | Class implementing ActorResolver interface |
disabledActions |
AuditAction[] |
— | Globally disable specific actions (created, updated, deleted) |
globalExcludedFields |
string[] |
— | Fields excluded from all audit logs |
metadata |
Record<string, any> |
— | Extra metadata added to all audit logs |
The audit_logs table stores:
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
action |
varchar(50) | created, updated, or deleted |
entity_type |
varchar(255) | Entity class name |
entity_id |
varchar(255) | Entity ID |
actor_type |
varchar(255) | Actor type (nullable) |
actor_id |
varchar(255) | Actor ID (nullable) |
old_values |
JSON | Previous field values |
new_values |
JSON | New field values |
metadata |
JSON | Extra context |
ip_address |
varchar(45) | Request IP (nullable) |
user_agent |
varchar(512) | User agent (nullable) |
created_at |
timestamp | When the change occurred |
Fields id, createdAt, updatedAt, created_at, and updated_at are always excluded from diffs automatically.
You can use the computeDiff utility and slugify function without the module:
import { computeDiff } from "@nestbolt/audit-log";
const diff = computeDiff(
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "alice@example.com" },
);
// diff = { oldValues: { name: "Alice" }, newValues: { name: "Bob" } }npm testRun tests in watch mode:
npm run test:watchGenerate coverage report:
npm run test:covPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.
The MIT License (MIT). Please see License File for more information.