Skip to content

nestbolt/audit-log

Repository files navigation

@nestbolt/audit-log

Automatic entity change tracking and audit logging for NestJS with TypeORM.

npm version npm downloads tests license


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

Table of Contents

Installation

Install the package via npm:

npm install @nestbolt/audit-log

Or via yarn:

yarn add @nestbolt/audit-log

Or via pnpm:

pnpm add @nestbolt/audit-log

Peer Dependencies

This 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

Optional

npm install @nestjs/event-emitter   # For audit.logged events

Quick Start

1. Register the module in your AppModule

import { AuditLogModule } from "@nestbolt/audit-log";

@Module({
  imports: [
    TypeOrmModule.forRoot({
      /* ... */
    }),
    AuditLogModule.forRoot({
      globalExcludedFields: ["password", "token"],
    }),
  ],
})
export class AppModule {}

2. Mark entities as auditable

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

3. Changes are tracked automatically

// 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() and QueryBuilder.update() do not trigger audit logging.

Module Configuration

The module is registered globally — you only need to import it once.

Static Configuration (forRoot)

AuditLogModule.forRoot({
  defaultActor: { type: "System", id: "system" },
  actorResolver: RequestActorResolver,
  disabledActions: ["deleted"],
  globalExcludedFields: ["password", "token", "secret"],
  metadata: { app: "my-app", version: "1.0" },
});

Async Configuration (forRootAsync)

AuditLogModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    globalExcludedFields: config.get("audit.excludedFields"),
    defaultActor: { type: "System", id: "system" },
  }),
});

Using the @Auditable() Decorator

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

Actor Resolution

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.

Manual Logging

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

Query API

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

Entity Mixin

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

Events

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

Using the Service Directly

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

Configuration Options

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

Audit Log Entity

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.

Standalone Usage

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" } }

Testing

npm test

Run tests in watch mode:

npm run test:watch

Generate coverage report:

npm run test:cov

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.

License

The MIT License (MIT). Please see License File for more information.

About

Automatic entity change tracking and audit logging for NestJS with TypeORM.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors