npm package: @feedmepos/hrm-actionguard
Description: NestJS Action Guard for HRM permission system
Latest version: 1.0.0

# @feedmepos/hrm-actionguard

NestJS Guard + Decorator for declarative permission control, built on CASL and `@feedmepos/hrm-permission`.

## Installation

```bash
pnpm add @feedmepos/hrm-actionguard @feedmepos/hrm-permission
```

## Usage

### 1. Import PermissionModule

```typescript
import { PermissionModule } from '@feedmepos/hrm-permission/nestjs';

@Module({
	imports: [PermissionModule.forRoot({ mongoUrl: process.env.MONGODB_URL, dbName: 'companyDB' })],
})
export class AppModule {}
```

### 2. Declare Permissions at Controller / Method Level

```typescript
import { ActionGuard, Action } from '@feedmepos/hrm-actionguard/nestjs';
import { Permission } from '@feedmepos/hrm-permission/nestjs';

// Controller level — all methods inherit
@Action({
  level: Permission.Level.business,
  subject: Permission.Subject.Business.permission,
  action: Permission.Action.manage,
})
@UseGuards(AuthGuard('firebase'), ActionGuard)
@Controller('portal/businesses/:businessId/users')
export class UserController {
  @Get()
  async getUsers() {}
}

// Method level override
@Action({ level: Permission.Level.business, subject: 'role', action: 'read' })
@UseGuards(AuthGuard('firebase'), ActionGuard)
@Get()
async getRoles() {}
```

### 3. Access Ability in Controller

After the guard passes, ability is injected into `request.user.ability`:

```typescript
@Get()
async getData(@Req() req: Request) {
  const ability = req.user?.ability[Permission.Level.business];
  const canDelete = ability?.can('delete', { subjectType: 'role' });
}
```

## API

### `@Action(metadata)`

| Param             | Type                                            | Description                                                           |
| ----------------- | ----------------------------------------------- | --------------------------------------------------------------------- |
| `level`           | `0 \| 1 \| 2`                                   | Permission level: 0=FeedMe, 1=Business, 2=Restaurant                  |
| `subject`         | `string`                                        | Permission subject, e.g. `"permission"`, `"role"`, `"menu"`           |
| `action`          | `string`                                        | CASL action: `"manage"`, `"read"`, `"create"`, `"update"`, `"delete"` |
| `field?`          | `string`                                        | Optional field-level permission                                       |
| `skipValidation?` | `boolean`                                       | Skip validation (non-production only)                                 |
| `operationLabel?` | `string \| ((ctx: OperationContext) => string)` | Human-readable label surfaced in audit log metadata                   |

### `ActionGuard`

1. Must be applied **after** `AuthGuard` (requires `request.user`)
2. Requires `PermissionService` (provided by `PermissionModule`)
3. Route must include relevant params (`:businessId`, `:restaurantId`)

**Execution flow:**

```
AuthGuard → ActionGuard
  1. Read @Action metadata (method takes precedence over class)
  2. Call PermissionService.constructAbility()
  3. ability.can(action, { subjectType: subject, businessId, restaurantId }, field)
  4. Inject request.user.ability = { [level]: Ability }
  5. Return true (allow) or false (deny, 403)
```

## Operation Labels

The optional `operationLabel` field provides a human-readable description of the operation. The resolved string is stored in `metadata.operationLabel` in the ClickHouse audit log.

### Static Label

```typescript
@Action({
  level: Permission.Level.business,
  subject: Permission.Subject.Business.inventory,
  action: Permission.Action.update,
  operationLabel: 'Update inventory stock',
})
```

### Dynamic Label

Pass a function to build the label from request context. The function receives an `OperationContext`:

| Field    | Type                                              | Description                                           |
| -------- | ------------------------------------------------- | ----------------------------------------------------- |
| `params` | `Record<string, string>`                          | Route params, e.g. `{ businessId, restaurantId }`     |
| `body`   | `Record<string, unknown> \| undefined`            | Parsed request body                                   |
| `query`  | `Record<string, string \| string[]> \| undefined` | Query string params                                   |
| `path`   | `string`                                          | Full request path, e.g. `"/api/businesses/123/users"` |
| `method` | `string`                                          | HTTP method, e.g. `"GET"`, `"PUT"`                    |
| `user`   | `{ uid: string; role?: string } \| undefined`     | Authenticated user (may be `undefined`)               |

```typescript
@Action({
  level: Permission.Level.restaurant,
  subject: Permission.Subject.Restaurant.inventory_stock,
  action: Permission.Action.update,
  operationLabel: ({ params }) => `Update stock for outlet ${params.restaurantId}`,
})

@Action({
  level: Permission.Level.business,
  subject: Permission.Subject.Business.hrm_employee,
  action: Permission.Action.update,
  operationLabel: ({ body, params }) =>
    `Update employee ${body?.name ?? params.employeeId}`,
})
```

> **Note**: The label function runs before the user check. `ctx.user` may be `undefined` on unauthenticated requests — always guard with optional chaining.

## Build Notes

This package is built with Vite (esbuild). Two critical configurations:

- **`minify: false`** — esbuild minifies class names to empty strings, causing NestJS to silently skip the guard. Minification must be disabled to preserve `ActionGuard.name`.
- **`@Inject()` decorators** — esbuild does not support `emitDecoratorMetadata`, so `design:paramtypes` is not emitted. Constructor params must use explicit `@Inject(Reflector)` / `@Inject(PermissionService)`.

## PermissionService Requirement

Abilities returned by `PermissionService.constructAbility()` must include the `subjectName` option to support the object-style subjects used by ActionGuard:

```typescript
new Ability(rules, {
	subjectName(subject) {
		if (typeof subject !== 'string' && subject.subjectType) return subject.subjectType;
		return subject;
	},
});
```

## Exports

```typescript
// Types
import type { Action } from '@feedmepos/hrm-actionguard';

// NestJS Guard & Decorator
import { ActionGuard, Action } from '@feedmepos/hrm-actionguard/nestjs';
```

## Related Packages

- [@feedmepos/hrm-permission](../permission) — Permission service and CASL integration
- [@casl/ability](https://casl.js.org/) — Authorization library