@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
| Column | Type | Constraints | Description |
|---|---|---|---|
id | uuid | Primary key, auto-generated | Unique identifier |
name | varchar(255) | Unique with guard_name | Permission name (e.g., "posts.create") |
guard_name | varchar(255) | Default: "default" | Guard scope |
created_at | timestamp | Auto-set on insert | Creation timestamp |
updated_at | timestamp | Auto-set on update | Last update timestamp |
Relationships
- Many-to-many with
RoleEntityvia therole_has_permissionsjoin table. This is the inverse side of the relationship (the owning side is onRoleEntity).
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
| Column | Type | Constraints | Description |
|---|---|---|---|
id | uuid | Primary key, auto-generated | Unique identifier |
name | varchar(255) | Unique with guard_name | Role name (e.g., "admin") |
guard_name | varchar(255) | Default: "default" | Guard scope |
created_at | timestamp | Auto-set on insert | Creation timestamp |
updated_at | timestamp | Auto-set on update | Last update timestamp |
Relationships
- Many-to-many with
PermissionEntityvia therole_has_permissionsjoin table. This is the owning side of the relationship.
Join Table: role_has_permissions
| Column | Type | Description |
|---|---|---|
role_id | uuid | Foreign key to roles.id |
permission_id | uuid | Foreign 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
| Column | Type | Constraints | Description |
|---|---|---|---|
user_id | varchar | Composite primary key | Your application's user ID |
role_id | varchar | Composite primary key | Foreign 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
| Column | Type | Constraints | Description |
|---|---|---|---|
user_id | varchar | Composite primary key | Your application's user ID |
permission_id | varchar | Composite primary key | Foreign 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
| Method | Returns | Description |
|---|---|---|
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");