NestboltNestbolt

@nestbolt/permissions

Entities

TypeORM entities shipped with the package -- PermissionEntity, RoleEntity, UserHasRolesEntity, UserHasPermissionsEntity -- and the PermissionUserRepository interface.

The package ships with four TypeORM entities that define the database schema for the RBAC system. These entities are automatically registered with TypeORM when you call PermissionsModule.forRoot() or PermissionsModule.forRootAsync(), so you do not need to add them to your own TypeOrmModule.forFeature() imports.

PermissionEntity

Maps to the permissions table. Each row represents a named permission scoped to a guard.

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToMany,
  Unique,
} from "typeorm";
import { RoleEntity } from "./role.entity";

@Entity("permissions")
@Unique(["name", "guardName"])
export class PermissionEntity {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column({ type: "varchar", length: 255 })
  name!: string;

  @Column({ name: "guard_name", type: "varchar", length: 255, default: "default" })
  guardName!: string;

  @ManyToMany(() => RoleEntity, (role) => role.permissions)
  roles!: RoleEntity[];

  @CreateDateColumn({ name: "created_at" })
  createdAt!: Date;

  @UpdateDateColumn({ name: "updated_at" })
  updatedAt!: Date;
}

Table: permissions

ColumnTypeConstraintsDescription
iduuidPrimary key, auto-generatedUnique identifier
namevarchar(255)Unique with guard_namePermission name (e.g., "posts.create")
guard_namevarchar(255)Default: "default"Guard scope
created_attimestampAuto-set on insertCreation timestamp
updated_attimestampAuto-set on updateLast update timestamp

Relationships

  • Many-to-many with RoleEntity via the role_has_permissions join table. This is the inverse side of the relationship (the owning side is on RoleEntity).

RoleEntity

Maps to the roles table. Each row represents a named role scoped to a guard.

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToMany,
  JoinTable,
  Unique,
} from "typeorm";
import { PermissionEntity } from "./permission.entity";

@Entity("roles")
@Unique(["name", "guardName"])
export class RoleEntity {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column({ type: "varchar", length: 255 })
  name!: string;

  @Column({ name: "guard_name", type: "varchar", length: 255, default: "default" })
  guardName!: string;

  @ManyToMany(() => PermissionEntity, (permission) => permission.roles, { eager: false })
  @JoinTable({
    name: "role_has_permissions",
    joinColumn: { name: "role_id", referencedColumnName: "id" },
    inverseJoinColumn: { name: "permission_id", referencedColumnName: "id" },
  })
  permissions!: PermissionEntity[];

  @CreateDateColumn({ name: "created_at" })
  createdAt!: Date;

  @UpdateDateColumn({ name: "updated_at" })
  updatedAt!: Date;
}

Table: roles

ColumnTypeConstraintsDescription
iduuidPrimary key, auto-generatedUnique identifier
namevarchar(255)Unique with guard_nameRole name (e.g., "admin")
guard_namevarchar(255)Default: "default"Guard scope
created_attimestampAuto-set on insertCreation timestamp
updated_attimestampAuto-set on updateLast update timestamp

Relationships

  • Many-to-many with PermissionEntity via the role_has_permissions join table. This is the owning side of the relationship.

Join Table: role_has_permissions

ColumnTypeDescription
role_iduuidForeign key to roles.id
permission_iduuidForeign key to permissions.id

This table is managed automatically by TypeORM through the @JoinTable decorator on RoleEntity. The permissions relation is not eager -- it is loaded explicitly when needed (e.g., in RoleService.getRolePermissions()).

UserHasRolesEntity

Maps to the user_has_roles table. This is a simple join table that associates users with roles using a composite primary key.

import { Entity, PrimaryColumn } from "typeorm";

@Entity("user_has_roles")
export class UserHasRolesEntity {
  @PrimaryColumn({ name: "user_id", type: "varchar" })
  userId!: string;

  @PrimaryColumn({ name: "role_id", type: "varchar" })
  roleId!: string;
}

Table: user_has_roles

ColumnTypeConstraintsDescription
user_idvarcharComposite primary keyYour application's user ID
role_idvarcharComposite primary keyForeign key to roles.id

The user_id column uses varchar type so it can accommodate any ID format your application uses (UUIDs, numeric IDs stored as strings, etc.).

UserHasPermissionsEntity

Maps to the user_has_permissions table. This join table associates users with direct permissions.

import { Entity, PrimaryColumn } from "typeorm";

@Entity("user_has_permissions")
export class UserHasPermissionsEntity {
  @PrimaryColumn({ name: "user_id", type: "varchar" })
  userId!: string;

  @PrimaryColumn({ name: "permission_id", type: "varchar" })
  permissionId!: string;
}

Table: user_has_permissions

ColumnTypeConstraintsDescription
user_idvarcharComposite primary keyYour application's user ID
permission_idvarcharComposite primary keyForeign key to permissions.id

Database Schema Diagram

The complete schema relationships:

users (your table)
  |
  |--< user_has_roles >-- roles --< role_has_permissions >-- permissions
  |
  |--< user_has_permissions >-- permissions
  • A user can have many roles (via user_has_roles).
  • A user can have many direct permissions (via user_has_permissions).
  • A role can have many permissions (via role_has_permissions).
  • A permission can belong to many roles (via role_has_permissions).

PermissionUserRepository Interface

You must implement this interface to bridge the package with your application's user model. The implementation tells the package how to read and write user-role and user-permission associations.

export interface PermissionUserRepository {
  getDirectPermissionIds(userId: string): Promise<string[]>;
  getRoleIds(userId: string): Promise<string[]>;
  attachRoles(userId: string, roleIds: string[]): Promise<void>;
  detachRoles(userId: string, roleIds: string[]): Promise<void>;
  detachAllRoles(userId: string): Promise<void>;
  attachPermissions(userId: string, permissionIds: string[]): Promise<void>;
  detachPermissions(userId: string, permissionIds: string[]): Promise<void>;
  detachAllPermissions(userId: string): Promise<void>;
  userExists(userId: string): Promise<boolean>;
}

Method Reference

MethodReturnsDescription
getDirectPermissionIds(userId)Promise<string[]>Return the IDs of all permissions directly assigned to the user.
getRoleIds(userId)Promise<string[]>Return the IDs of all roles assigned to the user.
attachRoles(userId, roleIds)Promise<void>Insert user-role associations for the given role IDs.
detachRoles(userId, roleIds)Promise<void>Remove user-role associations for the given role IDs.
detachAllRoles(userId)Promise<void>Remove all roles from the user.
attachPermissions(userId, permissionIds)Promise<void>Insert user-permission associations for the given permission IDs.
detachPermissions(userId, permissionIds)Promise<void>Remove user-permission associations for the given permission IDs.
detachAllPermissions(userId)Promise<void>Remove all direct permissions from the user.
userExists(userId)Promise<boolean>Return whether a user with the given ID exists.

Full Implementation Example

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import {
  PermissionUserRepository,
  UserHasRolesEntity,
  UserHasPermissionsEntity,
} from "@nestbolt/permissions";
import { User } from "../entities/user.entity";

@Injectable()
export class TypeOrmPermissionUserRepository
  implements PermissionUserRepository
{
  constructor(
    @InjectRepository(UserHasRolesEntity)
    private readonly userRolesRepo: Repository<UserHasRolesEntity>,
    @InjectRepository(UserHasPermissionsEntity)
    private readonly userPermissionsRepo: Repository<UserHasPermissionsEntity>,
    @InjectRepository(User)
    private readonly userRepo: Repository<User>,
  ) {}

  async getRoleIds(userId: string): Promise<string[]> {
    const rows = await this.userRolesRepo.find({ where: { userId } });
    return rows.map((r) => r.roleId);
  }

  async getDirectPermissionIds(userId: string): Promise<string[]> {
    const rows = await this.userPermissionsRepo.find({ where: { userId } });
    return rows.map((r) => r.permissionId);
  }

  async attachRoles(userId: string, roleIds: string[]): Promise<void> {
    const entities = roleIds.map((roleId) =>
      this.userRolesRepo.create({ userId, roleId }),
    );
    await this.userRolesRepo.save(entities);
  }

  async detachRoles(userId: string, roleIds: string[]): Promise<void> {
    for (const roleId of roleIds) {
      await this.userRolesRepo.delete({ userId, roleId });
    }
  }

  async detachAllRoles(userId: string): Promise<void> {
    await this.userRolesRepo.delete({ userId });
  }

  async attachPermissions(
    userId: string,
    permissionIds: string[],
  ): Promise<void> {
    const entities = permissionIds.map((permissionId) =>
      this.userPermissionsRepo.create({ userId, permissionId }),
    );
    await this.userPermissionsRepo.save(entities);
  }

  async detachPermissions(
    userId: string,
    permissionIds: string[],
  ): Promise<void> {
    for (const permissionId of permissionIds) {
      await this.userPermissionsRepo.delete({ userId, permissionId });
    }
  }

  async detachAllPermissions(userId: string): Promise<void> {
    await this.userPermissionsRepo.delete({ userId });
  }

  async userExists(userId: string): Promise<boolean> {
    const count = await this.userRepo.count({ where: { id: userId } });
    return count > 0;
  }
}

Registering the Implementation

Pass your implementation class (not an instance) to the module configuration:

PermissionsModule.forRoot({
  userRepository: TypeOrmPermissionUserRepository,
});

The module will instantiate the class via NestJS dependency injection. For forRoot(), it is provided with useClass. For forRootAsync(), it is created via ModuleRef.create() after the options factory resolves.

Using Your Own User Entity

The user_has_roles and user_has_permissions tables reference users by their ID as a varchar. Your User entity can use any ID type (UUID, auto-increment integer, etc.) as long as it can be represented as a string. The package never directly queries your User table -- all user-related operations go through the PermissionUserRepository you provide.

If your user ID is numeric, convert it to a string when interacting with the permissions API:

await this.registrar.assignRole(String(user.id), "admin");