UNPKG

@reflet/express

Version:

Well-defined and well-typed express decorators

1,026 lines (958 loc) 28.5 kB
import * as express from "express"; import * as core from "express-serve-static-core"; import { RequestHeader } from "@reflet/http"; /** * Main method to register routers into an express application. * @param app - express application. * @param routers - decorated classes or instances. * * @example * ```ts * class Foo { * @Get('/some') * get() {} * } * const app = express() * register(app, [Foo]) * app.listen(3000) * * // ------ * // or with instantiation: * register(app, [new Foo()]) * ``` * ------ * @public */ export declare function register(app: express.Application, routers: Registration[]): express.Application; /** * Defines a class type. Does the opposite of built-in `InstanceType`. * @public */ type ClassType = new (...args: any[]) => any; /** * @public */ type IsAny<T> = 0 extends 1 & T ? true : false; /** * Attaches an express Router to a class. * * The routes will be attached to the router at its root `path`. * * @param path - root path of the router. * @param options - specifies router behavior. * * @see https://expressjs.com/en/4x/api.html#router * @example * ```ts * @Router('/things') * class Foo { * @Get() * list(req: Request, res: Response, next: NextFunction) {} * * @Get('/:id') * getOne(req: Request, res: Response, next: NextFunction) {} * } * ``` * ------ * * @public */ export declare function Router(path: string | RegExp, options?: express.RouterOptions): Router.Decorator; export declare namespace Router { /** * Attaches children routers to a parent router, to have nested routers. * * @param register - function that should return an array of routers. * * @example * ```ts * @Router('/foo') * @Router.Children(() => [NestedRouter]) * class ParentRouter {} * * @Router('/bar') * class NestedRouter {} * ``` * ------ * @public */ function Children<T extends ClassType = any>(register: (...deps: IsAny<T> extends true ? unknown[] : ConstructorParameters<T>) => Registration[]): Router.Children.Decorator; namespace Children { /** * Equivalent to `ClassDecorator`. * @public */ type Decorator = ClassDecorator & { __expressRouterChildren?: never; }; } /** * Attaches an express Router to a class, without defining a root path. * The root path must be defined at registration. * * Useful for dynamic nested routers. * * @example * ```ts * @Router.Dynamic() * class Items { * @Get() * list(req: Request, res: Response, next: NextFunction) {} * } * * @Router('/foo') * class Foo { * constructor() { * register(this, [['/items', Items]]) * } * } * * @Router('/bar') * class Bar { * constructor() { * register(this, [['/elements', Items]]) * } * } * ``` * ------ * @public */ function Dynamic(options?: express.RouterOptions): Router.Decorator; /** * Equivalent to `ClassDecorator`. * @public */ type Decorator = ClassDecorator & { __expressRouter?: never; }; } /** * Middlewares applied to the router will be scoped to its own routes, independently of its path. * * @remarks * Express isolates routers by their path, so if two routers share the same path, they will share middlewares. * This decorator prevents this by applying middlewares directly to the routes instead of the router. * * @example * ```ts * @Router('/foo') * @ScopedMiddlewares * @Use(authenticate) * class FooSecret { * @Get() * getSecret(req: Request, res: Response, next: NextFunction) {} * } * * @Router('/foo') * class FooPublic { * @Get() * getPublic(req: Request, res: Response, next: NextFunction) {} * } * ``` * ------ * @public */ export declare function ScopedMiddlewares(): ScopedMiddlewares.Decorator; export declare function ScopedMiddlewares(...args: Parameters<ScopedMiddlewares.Decorator>): void; export declare namespace ScopedMiddlewares { /** * Remove `ScopedMiddlewares` behavior on a specific router, when applied globally to the app. */ function Dont(): ScopedMiddlewares.Dont.Decorator; function Dont(...args: Parameters<ScopedMiddlewares.Dont.Decorator>): void; namespace Dont { /** * Equivalent to `ClassDecorator`. * @public */ type Decorator = ClassDecorator & { __expressScopedMiddlewaresDont?: never; }; } /** * Equivalent to `ClassDecorator`. * @public */ type Decorator = ClassDecorator & { __expressScopedMiddlewares?: never; }; } /** * @public */ type PropertyOrMethodDecorator = (target: Object, propertyKey: string | symbol, descriptor?: TypedPropertyDescriptor<any>) => any; /** * Routes an HTTP request. * @param method - HTTP method of the request, in lowercase. * @param path - path for which the decorated class method is invoked. * @see https://expressjs.com/en/4x/api.html#app.METHOD * @public */ export declare function Route(method: Route.Method | Route.Method[], path: string | RegExp): Route.Decorator; export declare namespace Route { function Get(path?: string | RegExp): Route.Decorator; function Post(path?: string | RegExp): Route.Decorator; function Put(path?: string | RegExp): Route.Decorator; function Patch(path?: string | RegExp): Route.Decorator; function Delete(path?: string | RegExp): Route.Decorator; function Head(path?: string | RegExp): Route.Decorator; function Options(path?: string | RegExp): Route.Decorator; function Trace(path?: string | RegExp): Route.Decorator; function Notify(path?: string | RegExp): Route.Decorator; function Subscribe(path?: string | RegExp): Route.Decorator; function Unsubscribe(path?: string | RegExp): Route.Decorator; function Purge(path?: string | RegExp): Route.Decorator; function Checkout(path?: string | RegExp): Route.Decorator; function Move(path?: string | RegExp): Route.Decorator; function Copy(path?: string | RegExp): Route.Decorator; function Merge(path?: string | RegExp): Route.Decorator; function Report(path?: string | RegExp): Route.Decorator; function MSearch(path?: string | RegExp): Route.Decorator; function Mkactivity(path?: string | RegExp): Route.Decorator; function Mkcol(path?: string | RegExp): Route.Decorator; function Search(path?: string | RegExp): Route.Decorator; function Lock(path?: string | RegExp): Route.Decorator; function Unlock(path?: string | RegExp): Route.Decorator; function All(path?: string | RegExp): Route.Decorator; /** * @see http://expressjs.com/en/4x/api.html#routing-methods * @public */ type Method = 'checkout' | 'copy' | 'delete' | 'get' | 'head' | 'lock' | 'merge' | 'mkactivity' | 'mkcol' | 'move' | 'm-search' | 'notify' | 'options' | 'patch' | 'post' | 'purge' | 'put' | 'report' | 'search' | 'subscribe' | 'trace' | 'unlock' | 'unsubscribe' | 'all' | 'CHECKOUT' | 'COPY' | 'DELETE' | 'GET' | 'HEAD' | 'LOCK' | 'MERGE' | 'MKACTIVITY' | 'MKCOL' | 'MOVE' | 'M-SEARCH' | 'NOTIFY' | 'OPTIONS' | 'PATCH' | 'POST' | 'PURGE' | 'PUT' | 'REPORT' | 'SEARCH' | 'SUBSCRIBE' | 'TRACE' | 'UNLOCK' | 'UNSUBSCRIBE' | 'ALL'; /** * Equivalent to an union of `MethodDecorator` and `ProperyDecorator`. * @public */ type Decorator = PropertyOrMethodDecorator & { __expressRoute?: never; }; } /** * Routes HTTP `GET` requests. * @param path - path for which the decorated class method is invoked. * @see https://expressjs.com/en/4x/api.html#app.get.method * @example * ```ts * class Foo { * @Get('/things/:id') * get(req: Request, res: Response, next: NextFunction) {} * } * ``` * ------ * @public */ export declare function Get(path?: string | RegExp): Route.Decorator; /** * Routes HTTP `POST` requests. * @param path - path for which the decorated class method is invoked. * @see https://expressjs.com/en/4x/api.html#app.post.method * @example * ```ts * class Foo { * @Post('/things') * create(req: Request, res: Response, next: NextFunction) {} * } * ``` * ------ * @public */ export declare function Post(path?: string | RegExp): Route.Decorator; /** * Routes HTTP `PUT` requests. * @param path - path for which the decorated class method is invoked. * @see https://expressjs.com/en/4x/api.html#app.put.method * @example * ```ts * class Foo { * @Put('/things/:id') * replace(req: Request, res: Response, next: NextFunction) {} * } * ``` * ------ * @public */ export declare function Put(path?: string | RegExp): Route.Decorator; /** * Routes HTTP `PATCH` requests. * @param path - path for which the decorated class method is invoked. * @see https://expressjs.com/en/4x/api.html#app.METHOD * @example * ```ts * class Foo { * @Patch('/things/:id') * update(req: Request, res: Response, next: NextFunction) {} * } * ``` * ------ * @public */ export declare function Patch(path?: string | RegExp): Route.Decorator; /** * Routes HTTP `DELETE` requests. * @param path - path for which the decorated class method is invoked. * @see https://expressjs.com/en/4x/api.html#app.delete.method * @example * ```ts * class Foo { * @Delete('/things/:id') * remove(req: Request, res: Response, next: NextFunction) {} * } * ``` * ------ * @public */ export declare function Delete(path?: string | RegExp): Route.Decorator; /** * @public */ interface Handler<Req extends {} = {}> { (req: keyof Req extends undefined ? express.Request : express.Request<Req extends { params: infer P; } ? P : core.ParamsDictionary, any, Req extends { body: infer B; } ? B : any, Req extends { query: infer Q; } ? Q : core.Query> & Req, res: express.Response, next: express.NextFunction): any; } /** * @public */ type ClassOrMethodDecorator = <TFunction extends Function>(target: TFunction | Object, propertyKey?: string | symbol, descriptor?: TypedPropertyDescriptor<any>) => any; /** * Applies middlewares on a single route when applied to a method, or on multipe routes when applied to a class. * @see https://expressjs.com/en/4x/api.html#app.use * * @remarks * You can specify as much middlewares as you want inside a single `Use` decorator, * and apply as many `Use` decorators as you want. * Middlewares are applied on the routes in the order they are written. * * ------ * @example * ```ts * @Use(express.json(), express.urlencoded()) * class Foo { * @Use<{ bar?: number }>((req, res, next) => { * req.bar = 1 * next() * }) * @Post('/some') * create(req: Request & { bar: number }, res: Response, next: NextFunction) {} * } * ``` * ------ * @public */ export declare function Use<Req extends {}>(...middlewares: Handler<Req>[]): Use.Decorator; export declare namespace Use { /** * Equivalent to a union of `ClassDecorator` and `MethodDecorator`. * @public */ type Decorator = ClassOrMethodDecorator & { __expressUse?: never; }; } /** * Attaches an error handler on a single route when applied to a method, or on multipe routes when applied to a router class. * @see http://expressjs.com/en/guide/error-handling.html * * @remarks * You can apply as many `Catch` decorators as you want. * Error handlers are applied on the routes in the order they are written. * * @example * ```ts * @Catch(someDefaultErrorHandler) * class Foo { * @Catch((err, req, res, next) => { * res.status(400).send(err.message) * }) * @Get('/some') * get(req: Request, res: Response, next: NextFunction) { * throw Error('Nope') * } * } * ``` * ------ * @public */ export declare function Catch<T = any>(errorHandler: (err: T, req: express.Request, res: express.Response, next: express.NextFunction) => any): Catch.Decorator; export declare namespace Catch { /** * Used for `@Catch` decorator. * Equivalent to a union of `ClassDecorator` and `MethodDecorator`. * @public */ type Decorator = ClassOrMethodDecorator & { __expressCatch?: never; }; } /** * Injects Request object in the method's parameters. * @see https://expressjs.com/en/4x/api.html#req * @example * ```ts * // Without invokation: * @Get('/some') * get(@Req req: Req) {} * * // With invokation: * @Post('/some') * create(@Req() req: Req) {} * ``` * ------ * @public */ export declare function Req(): Req.Decorator; export declare function Req(...args: Parameters<Req.Decorator>): void; export type Req<P = core.ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = core.Query, Locals extends Record<string, any> = Record<string, any>> = express.Request<P, ResBody, ReqBody, ReqQuery, Locals>; export declare namespace Req { /** * Equivalent to `ParameterDecorator`. * @public */ type Decorator = ParameterDecorator & { __expressReq?: never; __expressHandlerParameter?: never; }; } /** * Inject Response object in the method's parameters. * @see https://expressjs.com/en/4x/api.html#res * @example * ```ts * // Without invokation: * @Get('/some') * get(@Res res: Res) {} * * // With invokation: * @Post('/some') * create(@Res() res: Res) {} * ``` * ------ * @public */ export declare function Res(): Res.Decorator; export declare function Res(...args: Parameters<Res.Decorator>): void; export type Res<ResBody = any, Locals extends Record<string, any> = Record<string, any>> = express.Response<ResBody, Locals>; export declare namespace Res { /** * Equivalent to `ParameterDecorator`. * @public */ type Decorator = ParameterDecorator & { __expressRes?: never; __expressHandlerParameter?: never; }; } /** * Injects `next` callback function in the method's parameters. * @see https://expressjs.com/en/guide/writing-middleware.html * @example * ```ts * // Without invokation: * @Get('/some') * get(@Next next: Next) {} * * // With invokation: * @Post('/some') * create(@Next() next: Next) {} * ``` * ------ * @public */ export declare function Next(): Next.Decorator; export declare function Next(...args: Parameters<Next.Decorator>): void; export type Next = express.NextFunction; export declare namespace Next { /** * Equivalent to `ParameterDecorator`. * @public */ type Decorator = ParameterDecorator & { __expressNext?: never; __expressHandlerParameter?: never; }; } /** * Injects request body in the method's parameters. * @param key - directly injects a body property. * @see https://expressjs.com/en/4x/api.html#req.body * @example * ```ts * // Whole body without invokation: * @Post('/some') * create(@Body body: object) {} * * // Whole body with invokation: * @Put('/some') * replace(@Body() body: object) {} * * // Sub property: * @Patch('/some') * update(@Body<User>('email') email: string) {} * ``` * ------ * @public */ export declare function Body<T extends object>(key?: keyof T): Body.Decorator; export declare function Body(...args: Parameters<Body.Decorator>): void; export declare namespace Body { /** * Equivalent to `ParameterDecorator`. * @public */ type Decorator = ParameterDecorator & { __expressBody?: never; __expressHandlerParameter?: never; }; } /** * Injects named route parameters in the method's parameters. * @param name - directly injects a single route parameter. * @see https://expressjs.com/en/4x/api.html#req.params * @example * ```ts * // Route parameters object without invokation: * @Get('/:col/:id') * get(@Params params: Params<'col' | 'id'>) {} * * // Route parameters object with invokation: * @Get('/:col/:id') * get(@Params() params: Params<'col' | 'id'>) {} * * // Single route parameter: * @Get('/:col/:id') * get(@Params('col') col: string, @Params('id') id: string) {} * ``` * ------ * @public */ export declare function Params(name?: string): Params.Decorator; export declare function Params(...args: Parameters<Params.Decorator>): void; export type Params<P extends string = string> = Record<P, string>; export declare namespace Params { /** * Equivalent to `ParameterDecorator`. * @public */ type Decorator = ParameterDecorator & { __expressParams?: never; __expressHandlerParameter?: never; }; } /** * Injects query string parameters in the method's parameters. * @param field - directly injects a value. * @see https://expressjs.com/en/4x/api.html#req.query * @example * ```ts * // Query string parameters object without invokation: * @Get('/search') * search(@Query query: Query) {} * * // Query string parameters object with invokation: * @Get('/search') * search(@Query() query: Query) {} * * // Single query string parameter: * @Get('/search') * search(@Query('name') name: string, @Query('sort') sort: string) {} * ``` * ------ * @public */ export declare function Query(field?: string): Query.Decorator; export declare function Query(...args: Parameters<Query.Decorator>): void; export type Query = { [key: string]: undefined | string | string[] | Query | Query[]; }; export declare namespace Query { /** * Equivalent to `ParameterDecorator`. * @public */ type Decorator = ParameterDecorator & { __expressQuery?: never; __expressHandlerParameter?: never; }; } /** * Injects request headers object in the method's parameters. * @param headerName - directly injects a specific header. * @see https://nodejs.org/api/http.html#http_message_headers * @example * ```ts * // Request headers object without invokation: * @Get('/some') * get(@Headers headers: Headers) {} * * // Request headers object with invokation: * @Get('/some') * get(@Headers() headers: Headers) {} * * // Single request header: * @Get('/some') * get(@Headers('user-agent') userAgent: string) {} * ``` * ------ * @public */ export declare function Headers(headerName?: RequestHeader): Headers.Decorator; export declare function Headers(...args: Parameters<Headers.Decorator>): void; export type Headers = RequestHeader.Record; export declare namespace Headers { /** * Equivalent to `ParameterDecorator`. * @public */ type Decorator = ParameterDecorator & { __expressHeaders?: never; __expressHandlerParameter?: never; }; } /** * @public */ declare type Middleware = express.RequestHandler | { handler: express.RequestHandler; dedupe?: boolean | 'by-name' | 'by-reference'; }; /** * Creates a parameter decorator, to inject anything we want in decorated routes. * * @param mapper - function that should return the thing we want to inject from the Request object. * @param use - adds middlewares to the route if the mapper needs them (_e.g. we need body-parser middlewares to retrieve `req.body`_). You can pass options to deduplicate middlewares based on the handler function reference or name (_e.g. if 'jsonParser' is already in use locally or globally, it won't be added again_). * * @remarks * We can create decorators with or without options. * Simple decorators without options are applied without being invoked. * * ------ * @example * ```ts * // Simple decorator: * const CurrentUser = createParamDecorator((req) => req.user) * class Foo { * @Get('/me') * get(@CurrentUser user: User) {} * } * ``` * ------ * @example * ```ts * // Advanced decorator (with option and middleware): * const BodyTrimmed = (key: string) => createParamDecorator( * (req) => req.body[key].trim(), * [{ handler: express.json(), dedupe: true }] * ) * class Foo { * @Post('/message') * create(@BodyTrimmed('text') text: string) {} * } * ``` * ------ * @public */ export declare function createParamDecorator<T = any>(mapper: (req: express.Request, res: express.Response) => T, use?: createParamDecorator.Middleware[]): createParamDecorator.Decorator; export declare namespace createParamDecorator { /** * @public */ type Options = { readonly mapper: (req: express.Request, res: express.Response, next?: express.NextFunction) => any; readonly use?: Middleware[]; }; /** * @public */ type Middleware = express.RequestHandler | { handler: express.RequestHandler; dedupe?: boolean | 'by-name' | 'by-reference'; }; /** * Equivalent to `ParameterDecorator`. * @public */ type Decorator = ParameterDecorator & { __expressHandlerParameter?: never; }; } /** * {@linkcode TypedPropertyDescriptor} * @public */ interface MethodDescriptorReturn<T> { value?: (...args: any[]) => T; } /** * @public */ type ClassOrTypedMethodDecorator<T> = <TFunction extends Function>(target: Object | TFunction, propertyKey?: string | symbol, descriptor?: MethodDescriptorReturn<T>) => TFunction extends Function ? any : MethodDescriptorReturn<T> | void; /** * Tells express to handle the method's return value and send it. * * @param options - Force json response type. * Return value will be sent with [`res.send`](https://expressjs.com/en/4x/api.html#res.send) by default. * Switch `json` option to `true` to send it with [`res.json`](https://expressjs.com/en/4x/api.html#res.json) * * ------ * @example * ```ts * class Foo { * @Send({ json: true }) * @Get('/some') * get() { * return 'bar' // content-type: application-json * } * } * ``` * ------ * @public */ export declare function Send(options?: Send.Options): Send.Decorator; /** * Handle the method's return value with a custom handler. * * ------ * @example * ```ts * class Foo { * @Send<string>((data, { res }) => { * res.json({ hello: data }) * }) * @Get('/some') * get() { * return 'world' * } * } * ``` * ------ * @public */ export declare function Send<T>(handler?: Send.Handler<T>): Send.Decorator<T>; export declare namespace Send { /** * Options for `@Send` decorator. * @public */ type Options = { /** Forces using express `res.json` method to send response */ json?: boolean; }; /** * @public */ type Handler<T> = (data: Awaited<T>, context: { req: express.Request; res: express.Response; next: express.NextFunction; }) => void | Promise<void>; /** * Equivalent to a union of `ClassDecorator` and `MethodDecorator`. * @public */ type Decorator<T = any> = ClassOrTypedMethodDecorator<T> & { __expressSend?: never; }; /** * Prevents a method from having its return value being sent, * if a `@Send` decorator is applied to its class. * * @example * ```ts * @Send() * class Foo { * @Get('/some') * get() { * return 'hi' * } * * @Send.Dont() * @Put('/some') * replace(req: Request, res: Response, next: NextFunction) { * res.send('hi') * } * } * ``` * ------ * @public */ function Dont(): Send.Dont.Decorator; function Dont(...args: Parameters<Send.Dont.Decorator>): void; namespace Dont { /** * Equivalent to a union of `ClassDecorator` and `MethodDecorator`. * @public */ type Decorator = ClassOrMethodDecorator & { __expressSendDont?: never; }; } } export interface Application extends express.Application { } /** * Express application class to extend, to apply the decorators with a global behavior. * * _Constructor simply returns an express application, augmented with `register` method._ * * @example * ```ts * @Send({ json: true }) * @Use(express.json()) * class App extends Application { * @Get('/healthcheck') * healthcheck() { * return { success: true } * } * } * * @Router('/foo') * class FooRouter { * @Get() * list() { * return db.collection('foo').find({}) * } * } * * const app = new App() * app.register([FooRouter]) * app.listen(3000) * ``` * ------ * @public */ export declare class Application { constructor(); register(routers?: Registration[]): this; } /** * @public */ declare type NotFunction = { bind?(): never; } | { call?(): never; } | { apply?(): never; }; /** * @public */ declare type NotArray = { push?(): never; } | { pop?(): never; } | { shift?(): never; } | { unshift?(): never; }; /** * @example * ```ts * const routers: Registration[] = [ * ['/foo', Foo], * ['/bar', Bar], * new Baz(), * ] * register(app, routers) * ``` * ------ * @public */ export type Registration = Registration.Class | Registration.Instance | Registration.Tuple; export declare namespace Registration { /** * @public */ type Class = new () => any; /** * @public */ type Instance = object & NotFunction & NotArray; /** * @public */ type Tuple = [path: string | RegExp, router: Registration.Class | Registration.Instance | express.IRouter]; } /** * Only readonly properties and methods from response. * @public */ type ResponseReadonly = Pick<express.Response, 'statusCode' | 'statusMessage' | 'locals' | 'charset' | 'headersSent' | 'getHeader' | 'getHeaders' | 'getHeaderNames' | 'hasHeader' | 'finished' | 'writableEnded' | 'writableFinished'>; /** * Final error handler to apply globally on the app, to be able to send errors as `json`. * * @example * ```ts * app.use(finalHandler({ * json: true, * expose: ['name', 'message'], * log: '5xx', * notFoundHandler: true, * })) * ``` * ------ * @public */ export declare function finalHandler(options: finalHandler.Options): express.ErrorRequestHandler; export declare namespace finalHandler { /** * @public */ interface Options { /** * Specifies behavior to use `res.json` to send errors: * * - `true`: always send as json. * - `false`: pass directly the error to `next`. * - `'from-response-type'`: looks for a json compatible `Content-Type` on the response (or else pass to `next`) * - `'from-response-type-or-request'`: if the response doesn't have a `Content-type` header, it looks for `X-Requested-With` or `Accept` headers on the request (or else pass to `next`) */ json: boolean | 'from-response-type' | 'from-response-type-or-request'; /** * Exposes error properties to client: * * - `true`: exposes all properties (stack included, beware of information leakage !). * - `false`: exposes nothing (empty object). * - `string[]`: whitelists specific error properties. * - `(status) => boolean | string[]`: flexible whitelisting. * * You can pass a function with the status code as parameter for more conditional whitelisting. */ expose: boolean | finalHandler.ErrorProps[] | finalHandler.Exposer; /** * log error: * * - `true`: every error * - `false`: none * - `'5xx'`: only server errors * - `(err, req, res) => void`: flexible logging */ log?: boolean | '5xx' | finalHandler.Logger; /** * Defines the handler when the route is not found: * * - switch to `true` to apply a basic handler throwing a `404` error. * - switch to a number to apply the same basic handler with a custom status code. * - or declare your own handler. */ notFoundHandler?: boolean | number | express.RequestHandler; } /** * @public */ type Exposer = (statusCode: number) => boolean | finalHandler.ErrorProps[]; /** * @public */ type Logger = (err: any, req: express.Request, readonlyRes: ResponseReadonly) => void; /** * @public */ type ErrorProps = 'name' | 'message' | 'stack' | (string & Record<never, never>); } export {}