NestboltNestbolt

@nestbolt/audit-log

Quick Start

Get audit logging running in your NestJS application in under five minutes.

This guide walks you through setting up @nestbolt/audit-log from scratch: registering the module, marking an entity as auditable, and verifying that changes are tracked.

1. Register the Module

Import AuditLogModule in your root module and call forRoot():

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AuditLogModule } from "@nestbolt/audit-log";
import { User } from "./entities/user.entity";

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: "postgres",
      host: "localhost",
      port: 5432,
      database: "myapp",
      username: "postgres",
      password: "postgres",
      autoLoadEntities: true,
      synchronize: true,
    }),
    TypeOrmModule.forFeature([User]),
    AuditLogModule.forRoot({
      globalExcludedFields: ["password", "passwordHash", "token"],
    }),
  ],
})
export class AppModule {}

The module is registered globally. You do not need to import it again in feature modules.

2. Create an Auditable Entity

Decorate your entity with @Auditable() and extend AuditableMixin(BaseEntity):

import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
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()
  role!: string;

  @Column()
  passwordHash!: string;
}

The @Auditable() decorator marks this entity for automatic change tracking. The except option ensures the passwordHash field is never recorded in audit logs. The AuditableMixin adds convenience methods like getAuditLogs() directly on the entity instance.

3. Perform Operations and See Audit Logs

Now, every .save() and .remove() call on the User entity will create an audit log entry.

Create a User

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { AuditLogService } from "@nestbolt/audit-log";
import { User } from "./entities/user.entity";

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepo: Repository<User>,
    private readonly auditLogService: AuditLogService,
  ) {}

  async createUser(name: string, email: string): Promise<User> {
    const user = this.userRepo.create({
      name,
      email,
      role: "member",
      passwordHash: "hashed_value",
    });
    return this.userRepo.save(user);
    // Audit log created automatically:
    // action: "created"
    // entityType: "User"
    // entityId: "<generated-uuid>"
    // newValues: { name: "Alice", email: "alice@example.com", role: "member" }
    // (passwordHash is excluded)
  }

  async updateUser(userId: string, name: string): Promise<User> {
    const user = await this.userRepo.findOneByOrFail({ id: userId });
    user.name = name;
    return this.userRepo.save(user);
    // Audit log created automatically:
    // action: "updated"
    // oldValues: { name: "Alice" }
    // newValues: { name: "Bob" }
  }

  async deleteUser(userId: string): Promise<void> {
    const user = await this.userRepo.findOneByOrFail({ id: userId });
    await this.userRepo.remove(user);
    // Audit log created automatically:
    // action: "deleted"
    // oldValues: { name: "Bob", email: "alice@example.com", role: "member" }
  }

  async getUserHistory(userId: string) {
    return this.auditLogService.getAuditLogs("User", userId);
  }
}

Query Audit Logs via the Service

// Get all audit logs for a specific user
const logs = await this.auditLogService.getAuditLogs("User", userId);

// Get only update logs from the last 30 days
const updates = await this.auditLogService.getAuditLogs("User", userId, {
  action: "updated",
  from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
  limit: 50,
});

// Get the most recent audit log entry
const latest = await this.auditLogService.getLatestAuditLog("User", userId);

Query Audit Logs via the Entity

Because User extends AuditableMixin, you can query logs directly from an entity instance:

const user = await this.userRepo.findOneByOrFail({ id: userId });

// Get all audit logs for this user
const logs = await user.getAuditLogs();

// Get the latest audit log
const latest = await user.getLatestAuditLog();

// Get only the last 5 updates
const recentUpdates = await user.getAuditLogs({ action: "updated", limit: 5 });

4. Inspect the audit_logs Table

After performing the operations above, the audit_logs table contains entries like:

idactionentity_typeentity_idold_valuesnew_valuescreated_at
uuid-1createdUserabc-123null{"name":"Alice","email":"alice@example.com","role":"member"}2025-01-15 10:00:00
uuid-2updatedUserabc-123{"name":"Alice"}{"name":"Bob"}2025-01-15 10:05:00
uuid-3deletedUserabc-123{"name":"Bob","email":"alice@example.com","role":"member"}null2025-01-15 10:10:00

Note that passwordHash does not appear in any of the recorded values because it was listed in the except option.

What Triggers Audit Logging

OperationTriggers Audit Log
repository.save(entity)Yes
repository.remove(entity)Yes
entity.save() (ActiveRecord)Yes
entity.remove() (ActiveRecord)Yes
repository.update(id, data)No
repository.delete(id)No
queryBuilder.update().execute()No
queryBuilder.delete().execute()No

For operations that do not trigger automatic logging, use AuditLogService.log() to create entries manually. See Manual Logging.

Next Steps