NestboltNestbolt

@nestbolt/permissions

Events

Lifecycle events emitted by the permissions package -- role and permission creation, deletion, attachment, detachment, and cache flushes.

The package emits events throughout the lifecycle of roles and permissions. These events let you build audit trails, trigger side effects, synchronize external systems, or log access changes.

Prerequisites

Events require the @nestjs/event-emitter package to be installed and registered in your application:

npm install @nestjs/event-emitter
import { Module } from "@nestjs/common";
import { EventEmitterModule } from "@nestjs/event-emitter";
import { PermissionsModule } from "@nestbolt/permissions";

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    PermissionsModule.forRoot({
      userRepository: TypeOrmPermissionUserRepository,
    }),
  ],
})
export class AppModule {}

If @nestjs/event-emitter is not installed, the package functions normally but silently skips all event emissions. No errors are thrown.

Event Constants

All event names are available via the PERMISSION_EVENTS constant:

import { PERMISSION_EVENTS } from "@nestbolt/permissions";
const PERMISSION_EVENTS = {
  ROLE_ATTACHED: "permissions.role-attached",
  ROLE_DETACHED: "permissions.role-detached",
  PERMISSION_ATTACHED: "permissions.permission-attached",
  PERMISSION_DETACHED: "permissions.permission-detached",
  ROLE_CREATED: "permissions.role-created",
  ROLE_DELETED: "permissions.role-deleted",
  PERMISSION_CREATED: "permissions.permission-created",
  PERMISSION_DELETED: "permissions.permission-deleted",
  CACHE_FLUSHED: "permissions.cache-flushed",
} as const;

Event Payload Types

The package exports four payload interfaces:

import type {
  RoleEvent,
  PermissionEvent,
  RoleLifecycleEvent,
  PermissionLifecycleEvent,
} from "@nestbolt/permissions";

RoleEvent

Used for role attachment and detachment events.

interface RoleEvent {
  userId: string;
  roleIds: string[];
}
PropertyTypeDescription
userIdstringThe ID of the user whose roles changed.
roleIdsstring[]The IDs of the roles that were attached or detached.

PermissionEvent

Used for permission attachment and detachment events.

interface PermissionEvent {
  userId: string;
  permissionIds: string[];
}
PropertyTypeDescription
userIdstringThe ID of the user whose permissions changed.
permissionIdsstring[]The IDs of the permissions that were attached or detached.

RoleLifecycleEvent

Used for role creation and deletion events.

interface RoleLifecycleEvent {
  role: {
    id: string;
    name: string;
    guardName: string;
  };
}
PropertyTypeDescription
role.idstringThe UUID of the role.
role.namestringThe name of the role (e.g., "admin").
role.guardNamestringThe guard name (e.g., "default").

PermissionLifecycleEvent

Used for permission creation and deletion events.

interface PermissionLifecycleEvent {
  permission: {
    id: string;
    name: string;
    guardName: string;
  };
}
PropertyTypeDescription
permission.idstringThe UUID of the permission.
permission.namestringThe name of the permission (e.g., "posts.create").
permission.guardNamestringThe guard name (e.g., "default").

Event Reference

permissions.role-attached

Emitted when one or more roles are assigned to a user via PermissionRegistrarService.assignRole().

ConstantPERMISSION_EVENTS.ROLE_ATTACHED
PayloadRoleEvent
Triggered byPermissionRegistrarService.assignRole()
import { OnEvent } from "@nestjs/event-emitter";
import { Injectable } from "@nestjs/common";
import { PERMISSION_EVENTS } from "@nestbolt/permissions";
import type { RoleEvent } from "@nestbolt/permissions";

@Injectable()
export class RoleAttachedListener {
  @OnEvent(PERMISSION_EVENTS.ROLE_ATTACHED)
  handleRoleAttached(payload: RoleEvent) {
    console.log(`Roles ${payload.roleIds.join(", ")} assigned to user ${payload.userId}`);
  }
}

permissions.role-detached

Emitted when one or more roles are removed from a user via PermissionRegistrarService.removeRole().

ConstantPERMISSION_EVENTS.ROLE_DETACHED
PayloadRoleEvent
Triggered byPermissionRegistrarService.removeRole()
@OnEvent(PERMISSION_EVENTS.ROLE_DETACHED)
handleRoleDetached(payload: RoleEvent) {
  console.log(`Roles ${payload.roleIds.join(", ")} removed from user ${payload.userId}`);
}

permissions.permission-attached

Emitted when one or more direct permissions are granted to a user via PermissionRegistrarService.givePermissionTo().

ConstantPERMISSION_EVENTS.PERMISSION_ATTACHED
PayloadPermissionEvent
Triggered byPermissionRegistrarService.givePermissionTo()
@OnEvent(PERMISSION_EVENTS.PERMISSION_ATTACHED)
handlePermissionAttached(payload: PermissionEvent) {
  console.log(
    `Permissions ${payload.permissionIds.join(", ")} granted to user ${payload.userId}`,
  );
}

permissions.permission-detached

Emitted when one or more direct permissions are revoked from a user via PermissionRegistrarService.revokePermissionTo().

ConstantPERMISSION_EVENTS.PERMISSION_DETACHED
PayloadPermissionEvent
Triggered byPermissionRegistrarService.revokePermissionTo()
@OnEvent(PERMISSION_EVENTS.PERMISSION_DETACHED)
handlePermissionDetached(payload: PermissionEvent) {
  console.log(
    `Permissions ${payload.permissionIds.join(", ")} revoked from user ${payload.userId}`,
  );
}

permissions.role-created

Emitted when a new role is created via RoleService.create() or the creation path of RoleService.findOrCreate().

ConstantPERMISSION_EVENTS.ROLE_CREATED
PayloadRoleLifecycleEvent
Triggered byRoleService.create()
@OnEvent(PERMISSION_EVENTS.ROLE_CREATED)
handleRoleCreated(payload: RoleLifecycleEvent) {
  console.log(`Role "${payload.role.name}" created with ID ${payload.role.id}`);
}

permissions.role-deleted

Emitted when a role is deleted via RoleService.delete().

ConstantPERMISSION_EVENTS.ROLE_DELETED
PayloadRoleLifecycleEvent
Triggered byRoleService.delete()
@OnEvent(PERMISSION_EVENTS.ROLE_DELETED)
handleRoleDeleted(payload: RoleLifecycleEvent) {
  console.log(`Role "${payload.role.name}" deleted`);
}

permissions.permission-created

Emitted when a new permission is created via PermissionService.create() or the creation path of PermissionService.findOrCreate().

ConstantPERMISSION_EVENTS.PERMISSION_CREATED
PayloadPermissionLifecycleEvent
Triggered byPermissionService.create()
@OnEvent(PERMISSION_EVENTS.PERMISSION_CREATED)
handlePermissionCreated(payload: PermissionLifecycleEvent) {
  console.log(
    `Permission "${payload.permission.name}" created for guard "${payload.permission.guardName}"`,
  );
}

permissions.permission-deleted

Emitted when a permission is deleted via PermissionService.delete().

ConstantPERMISSION_EVENTS.PERMISSION_DELETED
PayloadPermissionLifecycleEvent
Triggered byPermissionService.delete()
@OnEvent(PERMISSION_EVENTS.PERMISSION_DELETED)
handlePermissionDeleted(payload: PermissionLifecycleEvent) {
  console.log(`Permission "${payload.permission.name}" deleted`);
}

permissions.cache-flushed

Emitted when the permission cache is manually flushed via PermissionRegistrarService.flushCache(). Note that internal cache flushes (triggered automatically when roles or permissions are modified) do not emit this event.

ConstantPERMISSION_EVENTS.CACHE_FLUSHED
Payload{} (empty object)
Triggered byPermissionRegistrarService.flushCache()
@OnEvent(PERMISSION_EVENTS.CACHE_FLUSHED)
handleCacheFlushed() {
  console.log("Permission cache was manually flushed");
}

Complete Audit Log Listener Example

Here is a full example of a listener service that logs all permission-related events for audit purposes:

import { Injectable, Logger } from "@nestjs/common";
import { OnEvent } from "@nestjs/event-emitter";
import { PERMISSION_EVENTS } from "@nestbolt/permissions";
import type {
  RoleEvent,
  PermissionEvent,
  RoleLifecycleEvent,
  PermissionLifecycleEvent,
} from "@nestbolt/permissions";

@Injectable()
export class PermissionAuditListener {
  private readonly logger = new Logger(PermissionAuditListener.name);

  @OnEvent(PERMISSION_EVENTS.ROLE_ATTACHED)
  onRoleAttached(payload: RoleEvent) {
    this.logger.log(
      `User ${payload.userId} was assigned roles: ${payload.roleIds.join(", ")}`,
    );
  }

  @OnEvent(PERMISSION_EVENTS.ROLE_DETACHED)
  onRoleDetached(payload: RoleEvent) {
    this.logger.log(
      `User ${payload.userId} had roles removed: ${payload.roleIds.join(", ")}`,
    );
  }

  @OnEvent(PERMISSION_EVENTS.PERMISSION_ATTACHED)
  onPermissionAttached(payload: PermissionEvent) {
    this.logger.log(
      `User ${payload.userId} was granted permissions: ${payload.permissionIds.join(", ")}`,
    );
  }

  @OnEvent(PERMISSION_EVENTS.PERMISSION_DETACHED)
  onPermissionDetached(payload: PermissionEvent) {
    this.logger.log(
      `User ${payload.userId} had permissions revoked: ${payload.permissionIds.join(", ")}`,
    );
  }

  @OnEvent(PERMISSION_EVENTS.ROLE_CREATED)
  onRoleCreated(payload: RoleLifecycleEvent) {
    this.logger.log(
      `Role created: "${payload.role.name}" (guard: ${payload.role.guardName}, id: ${payload.role.id})`,
    );
  }

  @OnEvent(PERMISSION_EVENTS.ROLE_DELETED)
  onRoleDeleted(payload: RoleLifecycleEvent) {
    this.logger.log(
      `Role deleted: "${payload.role.name}" (guard: ${payload.role.guardName})`,
    );
  }

  @OnEvent(PERMISSION_EVENTS.PERMISSION_CREATED)
  onPermissionCreated(payload: PermissionLifecycleEvent) {
    this.logger.log(
      `Permission created: "${payload.permission.name}" (guard: ${payload.permission.guardName}, id: ${payload.permission.id})`,
    );
  }

  @OnEvent(PERMISSION_EVENTS.PERMISSION_DELETED)
  onPermissionDeleted(payload: PermissionLifecycleEvent) {
    this.logger.log(
      `Permission deleted: "${payload.permission.name}" (guard: ${payload.permission.guardName})`,
    );
  }

  @OnEvent(PERMISSION_EVENTS.CACHE_FLUSHED)
  onCacheFlushed() {
    this.logger.log("Permission cache was manually flushed");
  }
}

Register the listener in a module:

import { Module } from "@nestjs/common";
import { PermissionAuditListener } from "./permission-audit.listener";

@Module({
  providers: [PermissionAuditListener],
})
export class AuditModule {}

Events Not Emitted by syncRoles() and syncPermissions()

The syncRoles() and syncPermissions() methods on PermissionRegistrarService perform bulk operations (detach all, then attach new). They call the PermissionUserRepository methods directly and flush the cache, but they do not emit ROLE_ATTACHED, ROLE_DETACHED, PERMISSION_ATTACHED, or PERMISSION_DETACHED events. If you need to track sync operations, consider wrapping these calls with your own event emission or using the CACHE_FLUSHED event as an indirect indicator that something changed.

Using Event Constants as String Literals

If you prefer to use string literals instead of the constant object, you can subscribe to events by their string names directly:

@OnEvent("permissions.role-attached")
handleRoleAttached(payload: RoleEvent) {
  // ...
}

However, using PERMISSION_EVENTS.ROLE_ATTACHED is recommended because it provides type safety and makes refactoring easier.