Files
nest/packages/core/middleware/builder.ts
2026-02-15 13:07:08 +01:00

136 lines
3.9 KiB
TypeScript

import { iterate } from 'iterare';
import { RouteInfoPathExtractor } from './route-info-path-extractor.js';
import { RoutesMapper } from './routes-mapper.js';
import { filterMiddleware } from './utils.js';
import type { HttpServer, MiddlewareConsumer, Type } from '@nestjs/common';
import {
type MiddlewareConfigProxy,
type MiddlewareConfiguration,
type RouteInfo,
stripEndSlash,
} from '@nestjs/common/internal';
export class MiddlewareBuilder implements MiddlewareConsumer {
private readonly middlewareCollection = new Set<MiddlewareConfiguration>();
constructor(
private readonly routesMapper: RoutesMapper,
private readonly httpAdapter: HttpServer,
private readonly routeInfoPathExtractor: RouteInfoPathExtractor,
) {}
public apply(
...middleware: Array<Type<any> | Function | Array<Type<any> | Function>>
): MiddlewareConfigProxy {
return new MiddlewareBuilder.ConfigProxy(
this,
middleware.flat(),
this.routeInfoPathExtractor,
);
}
public build(): MiddlewareConfiguration[] {
return [...this.middlewareCollection];
}
public getHttpAdapter(): HttpServer {
return this.httpAdapter;
}
private static readonly ConfigProxy = class implements MiddlewareConfigProxy {
private excludedRoutes: RouteInfo[] = [];
constructor(
private readonly builder: MiddlewareBuilder,
private readonly middleware: Array<Type<any> | Function>,
private routeInfoPathExtractor: RouteInfoPathExtractor,
) {}
public getExcludedRoutes(): RouteInfo[] {
return this.excludedRoutes;
}
public exclude(
...routes: Array<string | RouteInfo>
): MiddlewareConfigProxy {
this.excludedRoutes = [
...this.excludedRoutes,
...this.getRoutesFlatList(routes).reduce((excludedRoutes, route) => {
for (const routePath of this.routeInfoPathExtractor.extractPathFrom(
route,
)) {
excludedRoutes.push({
...route,
path: routePath,
});
}
return excludedRoutes;
}, [] as RouteInfo[]),
];
return this;
}
public forRoutes(
...routes: Array<string | Type<any> | RouteInfo>
): MiddlewareConsumer {
const { middlewareCollection } = this.builder;
const flattedRoutes = this.getRoutesFlatList(routes);
const forRoutes = this.removeOverlappedRoutes(flattedRoutes);
const configuration = {
middleware: filterMiddleware(
this.middleware,
this.excludedRoutes,
this.builder.getHttpAdapter(),
),
forRoutes,
};
middlewareCollection.add(configuration);
return this.builder;
}
private getRoutesFlatList(
routes: Array<string | Type<any> | RouteInfo>,
): RouteInfo[] {
const { routesMapper } = this.builder;
return iterate(routes)
.map(route => routesMapper.mapRouteToRouteInfo(route))
.flatten()
.toArray();
}
private removeOverlappedRoutes(routes: RouteInfo[]) {
const regexMatchParams = /(:[^/]*)/g;
const wildcard = '([^/]*)';
const routesWithRegex = routes
.filter(route => route.path.includes(':'))
.map(route => ({
method: route.method,
path: route.path,
regex: new RegExp(
'^(' + route.path.replace(regexMatchParams, wildcard) + ')$',
'g',
),
}));
return routes.filter(route => {
const isOverlapped = (item: { regex: RegExp } & RouteInfo): boolean => {
if (route.method !== item.method) {
return false;
}
const normalizedRoutePath = stripEndSlash(route.path);
return (
normalizedRoutePath !== item.path &&
item.regex.test(normalizedRoutePath)
);
};
const routeMatch = routesWithRegex.find(isOverlapped);
return routeMatch === undefined;
});
}
};
}