Reputation: 1075
I need to write middleware to process requests, but some of paths should be excluded. I don't want manually hardcode all of them, so I have an idea:
Create special decorator, which will tag methods to exclude, something like this:
import { ReflectMetadata } from '@nestjs/common';
export const Exclude = () =>
ReflectMetadata('exclude', 'true');
Is there a way after creating NestJS application somehow recursively get all methods, annotated with this decorator, to automatically add their paths to exclude in my middleware?
Upvotes: 4
Views: 8103
Reputation: 21187
I've published a reusable module for discovering metadata on your handlers or Injectable classes specifically to support this pattern. You can grab @nestjs-plus/common
from NPM and then use the DiscoveryService
to automatically retrieve all matching handlers or classes based on a MetaData token that you provide. Source code is available on Github. I'll be continuing to update the docs in the short term but there are several example usages of it included in the repository already.
Under the hood it uses the MetaDataScanner but wraps things up in a nice easy to use API. Looking at the snippet you've posted this could help cut down a significant amount of boilerplate for your particular use case. You can see more advanced usages in the @nestjs-plus/rabbitmq
module (from the same repository) of how you can use this to glue together advanced functionality.
EDIT:
I've updated the library to support scenarios for discovering controllers and controller methods to support your scenario. There's a complete test suite that mimics your setup with the @Roles
decorator you can check out.. After including the DiscoveryModule
in your imports, and injecting the DiscoverService
you can find all controller methods using the simplified methodsAndControllerMethodsWithMeta
API.
// Inject the service
constructor(private readonly discover: DiscoveryService) { }
// Discover all controller methods decorated with guest roles or
// belonging to controllers with guest roles
const allMethods = this.discover.methodsAndControllerMethodsWithMeta<string[]>(
rolesMetaKey,
x => x.includes('guest')
);
After you've discovered all the methods you're after you can do whatever you want with them, in your case building up a collection of their RequestMethod
and path
.
const fullPaths = allGuestMethods.map(x => {
const controllerPath = Reflect.getMetadata(
PATH_METADATA,
x.component.metatype
);
const methodPath = Reflect.getMetadata(PATH_METADATA, x.handler);
const methodHttpVerb = Reflect.getMetadata(
METHOD_METADATA,
x.handler
);
return {
verb: methodHttpVerb,
path: `${controllerPath}/${methodPath}`
}
});
Which would give you back something like this (taken from the linked test suite).
expect(fullPaths).toContainEqual({verb: RequestMethod.GET, path: 'guest/route-path-one'});
expect(fullPaths).toContainEqual({verb: RequestMethod.GET, path: 'super/route-path-two'});
expect(fullPaths).toContainEqual({verb: RequestMethod.POST, path: 'admin/route-path-three'});
Feel free to provide feedback on the approach/API.
Upvotes: 9
Reputation: 1075
So... help yourself.
After digging into NestJS sources I found a way, here is direction for those who interested:
import * as pathToRegexp from 'path-to-regexp';
import { INestApplication, RequestMethod } from '@nestjs/common';
import { NestContainer } from '@nestjs/core/injector/container';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { PATH_METADATA, MODULE_PATH, METHOD_METADATA } from '@nestjs/common/constants';
const trimSlashes = (str: string) => {
if (str != null && str.length) {
while (str.length && str[str.length - 1] === '/') {
str = str.slice(0, str.length - 1);
}
}
return str || '';
};
const joinPath = (...p: string[]) =>
'/' + trimSlashes(p.map(trimSlashes).filter(x => x).join('/'));
// ---------------8<----------------
const app = await NestFactory.create(AppModule);
// ---------------8<----------------
const excludes = Object.create(null);
const container: NestContainer = (app as any).container; // this is "protected" field, so a bit hacky here
const modules = container.getModules();
const scanner = new MetadataScanner();
modules.forEach(({ routes, metatype }, moduleName) => {
let modulePath = metatype ? Reflect.getMetadata(MODULE_PATH, metatype) : undefined;
modulePath = modulePath ? modulePath + globalPrefix : globalPrefix;
routes.forEach(({ instance, metatype }, controllerName) => {
const controllerPath = Reflect.getMetadata(PATH_METADATA, metatype);
const isExcludeController = Reflect.getMetadata('exclude', metatype) === 'true';
const instancePrototype = Object.getPrototypeOf(instance);
scanner.scanFromPrototype(instance, instancePrototype, method => {
const targetCallback = instancePrototype[method];
const isExcludeMethod = Reflect.getMetadata('exclude', targetCallback) === 'true';
if (isExcludeController || isExcludeMethod) {
const requestMethod: RequestMethod = Reflect.getMetadata(METHOD_METADATA, targetCallback);
const routePath = Reflect.getMetadata(PATH_METADATA, targetCallback);
// add request method to map, if doesn't exist already
if (!excludes[RequestMethod[requestMethod]]) {
excludes[RequestMethod[requestMethod]] = [];
}
// add path to excludes
excludes[RequestMethod[requestMethod]].push(
// transform path to regexp to match it later in middleware
pathToRegexp(joinPath(modulePath, controllerPath, routePath)),
);
}
});
});
});
// now you can use `excludes` map in middleware
Upvotes: 2