How Metadata and Guards Work

Metadata, Reflector and a Guard Chain

Example

SetMetadata stores a value on the method, Reflector reads it, and guards run one after another; the first one that fails stops the request. A runnable version with legacy method decorators (a Map plays the role of reflect-metadata):

const metadata = new Map<Function, Map<string, unknown>>();

function SetMetadata(key: string, value: unknown): MethodDecorator {
  return (_target, _name, descriptor) => {
    const fn = descriptor.value as unknown as Function;
    const map = metadata.get(fn) ?? new Map<string, unknown>();
    map.set(key, value);
    metadata.set(fn, map);
  };
}
const Roles = (...roles: string[]) => SetMetadata('roles', roles);
const reflector = { get: <T>(key: string, fn: Function) => metadata.get(fn)?.get(key) as T | undefined };

class ProductsController {
  findAll() { return 'list'; }
  @Roles('admin') remove() { return 'deleted'; }
}

type User = { name: string; roles: string[] } | null;
type Ctx = { user: User; handler: Function };
type Guard = (ctx: Ctx) => true | string;

const authGuard: Guard = (ctx) => (ctx.user ? true : '401 Unauthorized');
const rolesGuard: Guard = (ctx) => {
  const required = reflector.get<string[]>('roles', ctx.handler);
  if (!required) return true;
  return required.some((r) => ctx.user!.roles.includes(r)) ? true : '403 Forbidden resource';
};

function dispatch(user: User, method: 'findAll' | 'remove') {
  const controller = new ProductsController();
  const ctx: Ctx = { user, handler: ProductsController.prototype[method] };
  for (const guard of [authGuard, rolesGuard]) {
    const result = guard(ctx);
    if (result !== true) return result;
  }
  return '200 ' + controller[method]();
}

const priya = { name: 'Priya', roles: ['customer'] };
const admin = { name: 'Arjun', roles: ['admin'] };
console.log('guest  findAll:', dispatch(null, 'findAll'));
console.log('priya  findAll:', dispatch(priya, 'findAll'));
console.log('priya  remove :', dispatch(priya, 'remove'));
console.log('admin  remove :', dispatch(admin, 'remove'));

Output

guest  findAll: 401 Unauthorized
priya  findAll: 200 list
priya  remove : 403 Forbidden resource
admin  remove : 200 deleted