Skip to content

NestJS

@vperms/nest integrates with NestJS through a global guard, decorators and a dynamic module. Decorators only store metadata; the guard hydrates the request context once and evaluates the metadata.

import { Module } from "@nestjs/common";
import { VPermsModule } from "@vperms/nest";
import { VeguiPermsMemoryAdapter } from "vperms";
@Module({
imports: [
VPermsModule.forRoot({
adapter: new VeguiPermsMemoryAdapter(),
workspace: "workspace",
resolver: (req) => req.user?.id ?? null,
}),
],
})
export class AppModule {}
interface VPermsModuleOptions {
adapter: VeguiPermsAdapter;
resolver: SubjectResolver;
workspace: string;
defaultParents?: DefaultParents;
permissionsExport?: { path: string };
}
type SubjectResolverResult = SubjectId | Principal | null;
type SubjectResolver =
(req: Request) => SubjectResolverResult | Promise<SubjectResolverResult>;

forRoot is global. It registers the service, the VPermsGuard as an APP_GUARD and — when permissionsExport is set — a controller that serves the export endpoint.

import {
Ability,
AnyPermission,
Kind,
Permission,
Subject,
} from "@vperms/nest";
@Controller("posts")
export class PostsController {
@Get(":id")
@Permission("posts.read")
read(@Ability() ability: RequestAbility) {
return { canWrite: ability.can("posts.write") };
}
@Delete(":id")
@AnyPermission("admin", "posts.delete")
remove(@Subject() subject: Subject, @Kind() kind: SubjectType) {}
}
  • @Permission(...) requires all permissions; @AnyPermission(...) at least one. Both can be applied to methods or whole controllers, and can be combined with handler/class metadata resolution.
  • Parameter decorators @Ability(), @Subject() and @Kind() read the hydrated state. @Ability() throws if the context is missing.
  • Permissions may be functions of the request:
import type { PermissionBuilder } from "@vperms/nest";
const ownsPost: PermissionBuilder = (req) => `posts.${req.params.id}.write`;
@Put(":id")
@Permission(ownsPost)
update() {}

VPermsGuard never denies on its own. For each request it hydrates the context idempotently (guarded by a symbol on the request), then:

  1. If @Permission metadata exists, every permission must pass, otherwise canActivate returns false (Nest responds 403).
  2. Else if @AnyPermission metadata exists, at least one must pass.
  3. Routes without metadata are untouched.

Extend AbilityGuard for imperatively defined rules:

import { AbilityGuard } from "@vperms/nest";
@Injectable()
export class PostOwnerGuard extends AbilityGuard {
protected async check(ability, context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
return ability.can(`posts.${request.params.id}.write`);
}
}

check(ability, context) is abstract; getAbility, getSubject and getKind are available to subclasses.

interface VpermsRequest extends Request {
ability: Ability;
subject?: Subject;
kind?: SubjectType;
}

VPermsModule, VPermsGuard, AbilityGuard, Permission, AnyPermission, Ability, Subject, Kind, PERMISSION_METADATA, ANY_PERMISSION_METADATA, VPERMS_OPTIONS, VPERMS_SERVICE, PermissionBuilder, PermissionInput, VPermsModuleOptions, VpermsRequest, SubjectResolver, SubjectResolverResult, PermissionsExportOptions.