feat: Create an OperationRouterHandler

This allows us to route after an Operation has been parsed
This commit is contained in:
Joachim Van Herwegen
2022-09-29 16:46:46 +02:00
parent da99ff30f6
commit 3db1921633
14 changed files with 266 additions and 154 deletions

View File

@@ -0,0 +1,75 @@
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import type { AsyncHandlerInput, AsyncHandlerOutput } from '../../util/handlers/AsyncHandler';
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
import { trimTrailingSlashes } from '../../util/PathUtil';
export interface BaseRouterHandlerArgs<T extends AsyncHandler<any, any>> {
/**
* The base URL of the server.
* Not required if no value is provided for `allowedPathNames`.
*/
baseUrl?: string;
/**
* The handler to call if all checks pass.
*/
handler: T;
/**
* The allowed method(s). `*` can be used to indicate all methods are allowed.
* Default is `[ '*' ]`.
*/
allowedMethods?: string[];
/**
* Regular expression(s) used to match the target URL.
* The base URl without trailing slash will be stripped of before applying the regular expressions,
* so the input will always start with a `/`.
* Default is `[ '.*' ]`.
*/
allowedPathNames?: string[];
}
/**
* Checks if a given method and path are satisfied and allows its handler to be executed if so.
*
* Implementations of this class should call `canHandleInput` in their `canHandle` call with the correct parameters.
*
* `canHandleInput` expects a ResourceIdentifier to indicate it expects the target to have been validated already.
*/
export abstract class BaseRouterHandler<T extends AsyncHandler<any, any>>
extends AsyncHandler<AsyncHandlerInput<T>, AsyncHandlerOutput<T>> {
protected readonly baseUrlLength: number;
protected readonly handler: T;
protected readonly allowedMethods: string[];
protected readonly allMethods: boolean;
protected readonly allowedPathNamesRegEx: RegExp[];
protected constructor(args: BaseRouterHandlerArgs<T>) {
super();
if (typeof args.allowedPathNames !== 'undefined' && typeof args.baseUrl !== 'string') {
throw new Error('A value for allowedPathNames requires baseUrl to be defined.');
}
// Trimming trailing slash so regexes can start with `/`
this.baseUrlLength = trimTrailingSlashes(args.baseUrl ?? '').length;
this.handler = args.handler;
this.allowedMethods = args.allowedMethods ?? [ '*' ];
this.allMethods = this.allowedMethods.includes('*');
this.allowedPathNamesRegEx = (args.allowedPathNames ?? [ '.*' ]).map((pn): RegExp => new RegExp(pn, 'u'));
}
protected async canHandleInput(input: AsyncHandlerInput<T>, method: string, target: ResourceIdentifier):
Promise<void> {
if (!this.allMethods && !this.allowedMethods.includes(method)) {
throw new MethodNotAllowedHttpError([ method ], `${method} is not allowed.`);
}
const pathName = target.path.slice(this.baseUrlLength);
if (!this.allowedPathNamesRegEx.some((regex): boolean => regex.test(pathName))) {
throw new NotFoundHttpError(`Cannot handle route ${pathName}`);
}
await this.handler.canHandle(input);
}
public async handle(input: AsyncHandlerInput<T>): Promise<AsyncHandlerOutput<T>> {
return this.handler.handle(input);
}
}

View File

@@ -0,0 +1,16 @@
import type { OperationHttpHandlerInput, OperationHttpHandler } from '../OperationHttpHandler';
import type { BaseRouterHandlerArgs } from './BaseRouterHandler';
import { BaseRouterHandler } from './BaseRouterHandler';
/**
* A {@link BaseRouterHandler} for an {@link OperationHttpHandler}.
*/
export class OperationRouterHandler extends BaseRouterHandler<OperationHttpHandler> {
public constructor(args: BaseRouterHandlerArgs<OperationHttpHandler>) {
super(args);
}
public async canHandle(input: OperationHttpHandlerInput): Promise<void> {
await super.canHandleInput(input, input.operation.method, input.operation.target);
}
}

View File

@@ -1,41 +1,23 @@
import type { TargetExtractor } from '../../http/input/identifier/TargetExtractor';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { ensureTrailingSlash, getRelativeUrl } from '../../util/PathUtil';
import type { HttpHandlerInput } from '../HttpHandler';
import { HttpHandler } from '../HttpHandler';
import type { HttpHandlerInput, HttpHandler } from '../HttpHandler';
import { BaseRouterHandler } from './BaseRouterHandler';
import type { BaseRouterHandlerArgs } from './BaseRouterHandler';
export interface RouterHandlerArgs {
baseUrl: string;
export interface RouterHandlerArgs extends BaseRouterHandlerArgs<HttpHandler> {
targetExtractor: TargetExtractor;
handler: HttpHandler;
allowedMethods: string[];
allowedPathNames: string[];
}
/**
* An HttpHandler that checks if a given method and path are satisfied
* and allows its handler to be executed if so.
*
* If `allowedMethods` contains '*' it will match all methods.
* A {@link BaseRouterHandler} for an {@link HttpHandler}.
* Uses a {@link TargetExtractor} to generate the target identifier.
*/
export class RouterHandler extends HttpHandler {
private readonly baseUrl: string;
export class RouterHandler extends BaseRouterHandler<HttpHandler> {
private readonly targetExtractor: TargetExtractor;
private readonly handler: HttpHandler;
private readonly allowedMethods: string[];
private readonly allMethods: boolean;
private readonly allowedPathNamesRegEx: RegExp[];
public constructor(args: RouterHandlerArgs) {
super();
this.baseUrl = ensureTrailingSlash(args.baseUrl);
super(args);
this.targetExtractor = args.targetExtractor;
this.handler = args.handler;
this.allowedMethods = args.allowedMethods;
this.allMethods = args.allowedMethods.includes('*');
this.allowedPathNamesRegEx = args.allowedPathNames.map((pn): RegExp => new RegExp(pn, 'u'));
}
public async canHandle(input: HttpHandlerInput): Promise<void> {
@@ -43,20 +25,7 @@ export class RouterHandler extends HttpHandler {
if (!request.url) {
throw new BadRequestHttpError('Cannot handle request without a url');
}
if (!request.method) {
throw new BadRequestHttpError('Cannot handle request without a method');
}
if (!this.allMethods && !this.allowedMethods.includes(request.method)) {
throw new MethodNotAllowedHttpError([ request.method ], `${request.method} is not allowed.`);
}
const pathName = await getRelativeUrl(this.baseUrl, request, this.targetExtractor);
if (!this.allowedPathNamesRegEx.some((regex): boolean => regex.test(pathName))) {
throw new NotFoundHttpError(`Cannot handle route ${pathName}`);
}
await this.handler.canHandle(input);
}
public async handle(input: HttpHandlerInput): Promise<void> {
await this.handler.handle(input);
const target = await this.targetExtractor.handleSafe({ request });
await super.canHandleInput(input, request.method ?? 'UNKNOWN', target);
}
}