NestboltNestbolt

@nestbolt/audit-log

Querying

Retrieve and filter audit log entries by entity, actor, date range, and action type.

The AuditLogService provides three query methods for retrieving audit log entries. You can also query logs directly from entity instances when using AuditableMixin.

AuditLogService Query Methods

getAuditLogs(entityType, entityId, options?)

Retrieves audit log entries for a specific entity, ordered by createdAt descending (most recent first).

import { Injectable } from "@nestjs/common";
import { AuditLogService } from "@nestbolt/audit-log";

@Injectable()
export class AuditService {
  constructor(private readonly auditLogService: AuditLogService) {}

  async getHistory(entityType: string, entityId: string) {
    // Get all audit logs for an entity
    const logs = await this.auditLogService.getAuditLogs("User", userId);

    // With filtering options
    const filtered = await this.auditLogService.getAuditLogs("User", userId, {
      action: "updated",
      from: new Date("2025-01-01"),
      to: new Date("2025-12-31"),
      limit: 20,
      offset: 0,
    });

    return filtered;
  }
}

Parameters:

ParameterTypeDescription
entityTypestringThe entity type name (as set by @Auditable() or the class name)
entityIdstringThe entity's unique identifier
optionsAuditLogQueryOptionsOptional filtering and pagination

Returns: Promise<AuditLogEntity[]>

getAuditLogsByActor(actorType, actorId, options?)

Retrieves all audit log entries created by a specific actor, ordered by createdAt descending. This is useful for building user activity feeds or admin action reports.

// Get all actions performed by a specific admin
const adminActions = await this.auditLogService.getAuditLogsByActor("Admin", adminId);

// Get only the last 50 actions by a user in the past month
const recentActions = await this.auditLogService.getAuditLogsByActor("User", userId, {
  from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
  limit: 50,
});

// Get only delete actions by a specific actor
const deletions = await this.auditLogService.getAuditLogsByActor("Admin", adminId, {
  action: "deleted",
});

Parameters:

ParameterTypeDescription
actorTypestringThe actor type (e.g., "User", "Admin", "System")
actorIdstringThe actor's unique identifier
optionsAuditLogQueryOptionsOptional filtering and pagination

Returns: Promise<AuditLogEntity[]>

getLatestAuditLog(entityType, entityId)

Returns the single most recent audit log entry for a specific entity, or null if no entries exist.

const latest = await this.auditLogService.getLatestAuditLog("User", userId);

if (latest) {
  console.log(`Last modified: ${latest.createdAt}`);
  console.log(`Action: ${latest.action}`);
  console.log(`By: ${latest.actorType} ${latest.actorId}`);
}

Parameters:

ParameterTypeDescription
entityTypestringThe entity type name
entityIdstringThe entity's unique identifier

Returns: Promise<AuditLogEntity | null>

Query Options

All query methods that accept options use the AuditLogQueryOptions interface:

interface AuditLogQueryOptions {
  action?: "created" | "updated" | "deleted";
  from?: Date;
  to?: Date;
  limit?: number;
  offset?: number;
}
OptionTypeDescription
action"created" | "updated" | "deleted"Filter by action type
fromDateInclude only logs created at or after this date
toDateInclude only logs created at or before this date
limitnumberMaximum number of entries to return
offsetnumberNumber of entries to skip (for pagination)

All options are optional. When no options are provided, all matching entries are returned ordered by createdAt descending.

Pagination Example

async getAuditLogPage(
  entityType: string,
  entityId: string,
  page: number,
  pageSize: number,
) {
  return this.auditLogService.getAuditLogs(entityType, entityId, {
    limit: pageSize,
    offset: (page - 1) * pageSize,
  });
}

// Usage
const page1 = await this.getAuditLogPage("User", userId, 1, 20); // First 20 entries
const page2 = await this.getAuditLogPage("User", userId, 2, 20); // Entries 21-40

Date Range Example

// Get all changes made today
const today = new Date();
today.setHours(0, 0, 0, 0);

const todayLogs = await this.auditLogService.getAuditLogs("Order", orderId, {
  from: today,
  to: new Date(),
});

// Get all changes made in Q1 2025
const q1Logs = await this.auditLogService.getAuditLogs("Order", orderId, {
  from: new Date("2025-01-01"),
  to: new Date("2025-03-31T23:59:59.999Z"),
});

Querying via Entity Instances

When your entity extends AuditableMixin, you can query audit logs directly from entity instances without needing to inject AuditLogService:

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

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

// Get filtered logs
const updates = await user.getAuditLogs({
  action: "updated",
  limit: 10,
});

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

The mixin methods internally use AuditLogService, so the module must be initialized. If you call these methods before AuditLogModule is registered, an AuditLogNotInitializedException is thrown.

The AuditLogEntity

Each audit log entry is stored as an AuditLogEntity with the following properties:

class AuditLogEntity {
  id: string;                           // UUID primary key
  action: string;                       // "created", "updated", or "deleted"
  entityType: string;                   // Entity type name
  entityId: string;                     // Entity ID
  actorType: string | null;             // Actor type (e.g., "User", "Admin", "System")
  actorId: string | null;               // Actor ID
  oldValues: Record<string, any> | null; // Previous field values (null for "created")
  newValues: Record<string, any> | null; // New field values (null for "deleted")
  metadata: Record<string, any> | null;  // Extra context
  ipAddress: string | null;              // Request IP address
  userAgent: string | null;              // Request user agent
  createdAt: Date;                       // When the audit entry was created
}

Database Columns

The entity maps to the audit_logs table with the following column definitions:

ColumnTypeNullableDescription
idUUIDNoAuto-generated primary key
actionVARCHAR(50)NoThe action type
entity_typeVARCHAR(255)NoEntity type name
entity_idVARCHAR(255)NoEntity identifier
actor_typeVARCHAR(255)YesActor type
actor_idVARCHAR(255)YesActor identifier
old_valuesJSONYesPrevious field values as JSON
new_valuesJSONYesNew field values as JSON
metadataJSONYesExtra context as JSON
ip_addressVARCHAR(45)YesIPv4 or IPv6 address
user_agentVARCHAR(512)YesBrowser/client user agent
created_atTIMESTAMPNoAuto-generated creation timestamp

Database Indexes

The table includes indexes for efficient querying:

IndexColumnsPurpose
Primary keyidUnique entry lookup
Entity indexentity_type, entity_idFast lookup by entity
Actor indexactor_type, actor_idFast lookup by actor
Action indexactionFilter by action type
Created at indexcreated_atDate range queries and ordering

Building an Audit Log API Endpoint

Here is a complete example of a controller that exposes audit log queries via REST:

import { Controller, Get, Param, Query } from "@nestjs/common";
import { AuditLogService, AuditLogEntity } from "@nestbolt/audit-log";

@Controller("audit-logs")
export class AuditLogController {
  constructor(private readonly auditLogService: AuditLogService) {}

  @Get("entity/:entityType/:entityId")
  async getEntityLogs(
    @Param("entityType") entityType: string,
    @Param("entityId") entityId: string,
    @Query("action") action?: "created" | "updated" | "deleted",
    @Query("from") from?: string,
    @Query("to") to?: string,
    @Query("limit") limit?: string,
    @Query("offset") offset?: string,
  ): Promise<AuditLogEntity[]> {
    return this.auditLogService.getAuditLogs(entityType, entityId, {
      action,
      from: from ? new Date(from) : undefined,
      to: to ? new Date(to) : undefined,
      limit: limit ? parseInt(limit, 10) : undefined,
      offset: offset ? parseInt(offset, 10) : undefined,
    });
  }

  @Get("actor/:actorType/:actorId")
  async getActorLogs(
    @Param("actorType") actorType: string,
    @Param("actorId") actorId: string,
    @Query("action") action?: "created" | "updated" | "deleted",
    @Query("limit") limit?: string,
    @Query("offset") offset?: string,
  ): Promise<AuditLogEntity[]> {
    return this.auditLogService.getAuditLogsByActor(actorType, actorId, {
      action,
      limit: limit ? parseInt(limit, 10) : undefined,
      offset: offset ? parseInt(offset, 10) : undefined,
    });
  }

  @Get("entity/:entityType/:entityId/latest")
  async getLatest(
    @Param("entityType") entityType: string,
    @Param("entityId") entityId: string,
  ): Promise<AuditLogEntity | null> {
    return this.auditLogService.getLatestAuditLog(entityType, entityId);
  }
}

Next Steps

  • Actor Resolution -- learn how to track which user or system made each change.
  • Events -- react to audit log creation with event listeners.