From a70ccf21c6a7f145ac7b6d7c58f32857e732a689 Mon Sep 17 00:00:00 2001 From: Anton Bessonov Date: Sat, 2 Apr 2022 23:50:33 +0200 Subject: [PATCH] add middleware placebo --- .eslintrc.js | 2 +- README.md | 107 ++++++++++++++++-- package.json | 17 +-- src/__tests__/router.test.ts | 16 ++- src/examples/micro.ts | 10 +- src/examples/node.ts | 4 +- src/index.ts | 10 +- src/matchers/AndMatcher.ts | 8 +- src/matchers/EndpointMatcher.ts | 12 +- src/matchers/ExactQueryMatcher.ts | 12 +- src/matchers/ExactUrlPathnameMatcher.ts | 12 +- src/matchers/Matcher.ts | 7 +- src/matchers/MethodMatcher.ts | 12 +- src/matchers/OrMatcher.ts | 10 +- src/matchers/RegExpUrlMatcher.ts | 12 +- src/matchers/__tests__/AndMatcher.test.ts | 4 +- .../ExactQueryMatcherMatcher.test.ts | 4 +- .../__tests__/ExactUrlPathnameMatcher.test.ts | 4 +- src/matchers/__tests__/MethodMatcher.test.ts | 4 +- src/matchers/__tests__/OrMatcher.test.ts | 4 +- .../__tests__/RegExpUrlMatcher.test.ts | 4 +- src/matchers/index.ts | 45 ++++++-- src/router.ts | 13 ++- 23 files changed, 264 insertions(+), 69 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index eb3ce7c..93fba05 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -32,7 +32,7 @@ module.exports = { 'error', { ImportDeclaration: { - minProperties: 2, + minProperties: 1, multiline: true, }, }, diff --git a/README.md b/README.md index 7bd5ca3..d398221 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,11 @@ pnpm add @bessonovs/node-http-router ## Documentation and examples -### Usage with native node http server +### Binding + +The router works with native http interfaces like `IncomingMessage` and `ServerResponse`. Therefore it should be possible to use it with most of existing servers. + +#### Usage with native node http server ```typescript const router = new Router((req, res) => { @@ -50,7 +54,7 @@ router.addRoute({ See [full example](src/examples/node.ts) and [native node http server](https://nodejs.org/api/http.html#http_class_http_server) documentation. -### Usage with micro +#### Usage with micro [micro](https://github.com/vercel/micro) is a very lightweight layer around the native node http server with some convenience methods. @@ -68,7 +72,11 @@ router.addRoute({ See [full example](src/examples/micro.ts). -### MethodMatcher +### Matchers + +In the core, matchers are responsible to decide if particular handler should be called or not. There is no magic: matchers are interated on every request and first positive "match" calls defined handler. + +#### MethodMatcher ([source](./src/matchers/MethodMatcher.ts)) Method matcher is the simplest matcher and matches any of the passed http methods: @@ -80,7 +88,7 @@ router.addRoute({ }) ``` -### ExactUrlPathnameMatcher +#### ExactUrlPathnameMatcher ([source](./src/matchers/ExactUrlPathnameMatcher.ts)) Matches given pathnames (but ignores query parameters): @@ -92,7 +100,7 @@ router.addRoute({ }) ``` -### ExactQueryMatcher +#### ExactQueryMatcher ([source](./src/matchers/ExactQueryMatcher.ts)) Defines expectations on query parameters: @@ -115,7 +123,7 @@ router.addRoute({ }) ``` -### RegExpUrlMatcher +#### RegExpUrlMatcher ([source](./src/matchers/RegExpUrlMatcher.ts)) Allows powerful expressions: @@ -127,7 +135,7 @@ router.addRoute({ ``` Ordinal parameters can be used too. Be aware that regular expression must match the whole base url (also with query parameters) and not only `pathname`. -### EndpointMatcher +#### EndpointMatcher ([source](./src/matchers/EndpointMatcher.ts)) EndpointMatcher is a combination of Method and RegExpUrl matcher for convenient usage: @@ -138,9 +146,92 @@ router.addRoute({ }) ``` +### Middleware + +Currently, there is no built-in API for middlewares. It seems like there is no aproach to provide centralized and typesafe way for middlewares. And it need some conceptual work, before it will be added. Open an issue, if you have a great idea! + +But well, handler can be wrapped like: + +```typescript +// example of a generic middleware, not a cors middleware! +function corsMiddleware(origin: string) { + return function corsWrapper( + wrappedHandler: Handler, + ): Handler { + return async function corsHandler(req, res, ...args) { + // -> executed before handler + // it's even possible to skip the handler at all + const result = await wrappedHandler(req, res, ...args) + // -> executed after handler, like: + res.setHeader('Access-Control-Allow-Origin', origin) + return result + } + } +} + +// create a configured instance of middleware +const cors = corsMiddleware('http://0.0.0.0:8080') + +router.addRoute({ + matcher: new MethodMatcher(['OPTIONS', 'POST']), + // use it + handler: cors((req, res, { method }) => `Method: ${method}`), +}) +``` + +Of course you can create a `middlewares` wrapper and put all middlewares inside it: +```typescript +type Middleware) => Handler> = Parameters[0]>[2] + +function middlewares( + handler: Handler + & Middleware + & Middleware>, +): Handler { + return function middlewaresHandler(...args) { + // @ts-expect-error + return cors(session(handler(...args))) + } +} + +router.addRoute({ + matcher, + // use it + handler: middlewares((req, res, { csrftoken }) => `Token: ${csrftoken}`), +}) +``` + +Apropos typesafety. You can modify types in middleware: + +```typescript +function valueMiddleware(myValue: string) { + return function valueWrapper( + handler: Handler & { + // add additional type + myValue: string + }>, + ): Handler { + return function valueHandler(req, res, match) { + return handler(req, res, { + ...match, + // add additional property + myValue, + }) + } + } +} + +const value = valueMiddleware('world') + +router.addRoute({ + matcher: new MethodMatcher(['GET']), + handler: value((req, res, { myValue }) => `Hello ${myValue}`), +}) +``` + ## License -The MIT License (MIT) +MIT License Copyright (c) 2019 - today, Anton Bessonov diff --git a/package.json b/package.json index bf43e6c..02a4df4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bessonovs/node-http-router", - "version": "0.0.8", + "version": "0.0.9", "description": "Extensible http router for node and micro", "keywords": [ "router", @@ -30,7 +30,8 @@ "build": "tsc", "example-node-start": "tsc && node dist/examples/node.js", "example-micro-start": "tsc && node dist/examples/micro.js", - "precommit": "$_ run test && $_ run lint && $_ run build" + "precommit": "$_ run test && $_ run lint && $_ run build", + "update": "pnpm update --interactive --recursive --latest" }, "dependencies": { "urlite": "3.0.0" @@ -40,19 +41,19 @@ "@types/express": "4.17.13", "@types/jest": "27.4.1", "@types/node": "16.11.7", - "@typescript-eslint/eslint-plugin": "5.13.0", - "@typescript-eslint/parser": "5.13.0", - "eslint": "8.10.0", + "@typescript-eslint/eslint-plugin": "5.17.0", + "@typescript-eslint/parser": "5.17.0", + "eslint": "8.12.0", "eslint-config-airbnb": "19.0.4", "eslint-plugin-import": "2.25.4", "eslint-plugin-jsx-a11y": "6.5.1", - "eslint-plugin-react": "7.29.3", + "eslint-plugin-react": "7.29.4", "jest": "27.5.1", "micro": "9.3.5-canary.3", "node-mocks-http": "1.11.0", "path-to-regexp": "6.2.0", - "ts-jest": "27.1.3", - "typescript": "4.6.2" + "ts-jest": "27.1.4", + "typescript": "4.6.3" }, "publishConfig": { "access": "public" diff --git a/src/__tests__/router.test.ts b/src/__tests__/router.test.ts index 832178b..ff11065 100644 --- a/src/__tests__/router.test.ts +++ b/src/__tests__/router.test.ts @@ -1,17 +1,23 @@ import { - IncomingMessage, ServerResponse, + IncomingMessage, + ServerResponse, } from 'http' import { - createRequest, createResponse, + createRequest, + createResponse, } from 'node-mocks-http' import { - compile, pathToRegexp, + compile, + pathToRegexp, } from 'path-to-regexp' import { - MatchedHandler, Router, + MatchedHandler, + Router, } from '../router' import { - AndMatcher, EndpointMatcher, ExactUrlPathnameMatcher, + AndMatcher, + EndpointMatcher, + ExactUrlPathnameMatcher, MethodMatcher, } from '../matchers' diff --git a/src/examples/micro.ts b/src/examples/micro.ts index ec6a874..29155ef 100644 --- a/src/examples/micro.ts +++ b/src/examples/micro.ts @@ -1,6 +1,10 @@ import http from 'http' -import micro, { send } from 'micro' -import { Router } from '../router' +import micro, { + send, +} from 'micro' +import { + Router, +} from '../router' import { EndpointMatcher, ExactUrlPathnameMatcher, @@ -30,7 +34,7 @@ server.once('listening', () => { router.addRoute({ // it's not necessary to type the matcher, but it give you a confidence - matcher: new EndpointMatcher<{name: string}>('GET', /^\/hello\/(?[^/]+)$/), + matcher: new EndpointMatcher<{ name: string }>('GET', /^\/hello\/(?[^/]+)$/), handler: (req, res, match) => { return `Hello ${match.match.groups.name}!` }, diff --git a/src/examples/node.ts b/src/examples/node.ts index f76c377..9a1f110 100644 --- a/src/examples/node.ts +++ b/src/examples/node.ts @@ -1,5 +1,7 @@ import http from 'http' -import { Router } from '../router' +import { + Router, +} from '../router' import { EndpointMatcher, ExactUrlPathnameMatcher, diff --git a/src/index.ts b/src/index.ts index a1c485b..d26e28c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,9 @@ export * from './matchers' -export type { Route, MatchedHandler } from './router' -export { Router } from './router' +export type { + Handler, + Route, + MatchedHandler, +} from './router' +export { + Router, +} from './router' diff --git a/src/matchers/AndMatcher.ts b/src/matchers/AndMatcher.ts index a4ec929..16ef79b 100644 --- a/src/matchers/AndMatcher.ts +++ b/src/matchers/AndMatcher.ts @@ -1,8 +1,12 @@ import { - IncomingMessage, ServerResponse, + IncomingMessage, + ServerResponse, } from 'http' import { - MatchResult, Matched, Matcher, isMatched, + MatchResult, + Matched, + Matcher, + isMatched, } from '.' export type AndMatcherResult = MatchResult<{ diff --git a/src/matchers/EndpointMatcher.ts b/src/matchers/EndpointMatcher.ts index 988834b..e34263e 100644 --- a/src/matchers/EndpointMatcher.ts +++ b/src/matchers/EndpointMatcher.ts @@ -2,14 +2,20 @@ import { IncomingMessage, ServerResponse, } from 'http' -import { Matcher } from './Matcher' -import { MatchResult } from './MatchResult' +import { + Matcher, +} from './Matcher' +import { + MatchResult, +} from './MatchResult' import { Method, MethodMatchResult, MethodMatcher, } from './MethodMatcher' -import { AndMatcher } from './AndMatcher' +import { + AndMatcher, +} from './AndMatcher' import { RegExpExecGroupArray, RegExpUrlMatchResult, diff --git a/src/matchers/ExactQueryMatcher.ts b/src/matchers/ExactQueryMatcher.ts index 70597ad..6660682 100644 --- a/src/matchers/ExactQueryMatcher.ts +++ b/src/matchers/ExactQueryMatcher.ts @@ -1,7 +1,13 @@ -import { IncomingMessage } from 'http' +import { + IncomingMessage, +} from 'http' import Url from 'urlite' -import { Matcher } from './Matcher' -import { MatchResult } from './MatchResult' +import { + Matcher, +} from './Matcher' +import { + MatchResult, +} from './MatchResult' type QueryMatch = {[key: string]: string | true | false | undefined} diff --git a/src/matchers/ExactUrlPathnameMatcher.ts b/src/matchers/ExactUrlPathnameMatcher.ts index fdbe847..43757d8 100644 --- a/src/matchers/ExactUrlPathnameMatcher.ts +++ b/src/matchers/ExactUrlPathnameMatcher.ts @@ -1,7 +1,13 @@ -import { IncomingMessage } from 'http' +import { + IncomingMessage, +} from 'http' import Url from 'urlite' -import { Matcher } from './Matcher' -import { MatchResult } from './MatchResult' +import { + Matcher, +} from './Matcher' +import { + MatchResult, +} from './MatchResult' export type ExactUrlPathnameMatchResult = MatchResult<{ pathname: U[number] diff --git a/src/matchers/Matcher.ts b/src/matchers/Matcher.ts index f70d049..d38bfa1 100644 --- a/src/matchers/Matcher.ts +++ b/src/matchers/Matcher.ts @@ -1,7 +1,10 @@ import { - IncomingMessage, ServerResponse, + IncomingMessage, + ServerResponse, } from 'http' -import { MatchResult } from './MatchResult' +import { + MatchResult, +} from './MatchResult' export type ExtractMatchResult = M extends Matcher ? MR : never diff --git a/src/matchers/MethodMatcher.ts b/src/matchers/MethodMatcher.ts index 8ab2f5e..1542740 100644 --- a/src/matchers/MethodMatcher.ts +++ b/src/matchers/MethodMatcher.ts @@ -1,6 +1,12 @@ -import { IncomingMessage } from 'http' -import { Matcher } from './Matcher' -import { MatchResult } from './MatchResult' +import { + IncomingMessage, +} from 'http' +import { + Matcher, +} from './Matcher' +import { + MatchResult, +} from './MatchResult' const validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as const diff --git a/src/matchers/OrMatcher.ts b/src/matchers/OrMatcher.ts index 6bd9339..0add990 100644 --- a/src/matchers/OrMatcher.ts +++ b/src/matchers/OrMatcher.ts @@ -1,10 +1,14 @@ import { - IncomingMessage, ServerResponse, + IncomingMessage, + ServerResponse, } from 'http' import { - MatchResult, isMatched, + MatchResult, + isMatched, } from './MatchResult' -import { Matcher } from './Matcher' +import { + Matcher, +} from './Matcher' export type OrMatcherResult = MatchResult<{ or: [MR1, MR2] diff --git a/src/matchers/RegExpUrlMatcher.ts b/src/matchers/RegExpUrlMatcher.ts index a026cdc..5acffb4 100644 --- a/src/matchers/RegExpUrlMatcher.ts +++ b/src/matchers/RegExpUrlMatcher.ts @@ -1,6 +1,12 @@ -import { IncomingMessage } from 'http' -import { Matcher } from './Matcher' -import { MatchResult } from './MatchResult' +import { + IncomingMessage, +} from 'http' +import { + Matcher, +} from './Matcher' +import { + MatchResult, +} from './MatchResult' export interface RegExpExecGroupArray< T extends object diff --git a/src/matchers/__tests__/AndMatcher.test.ts b/src/matchers/__tests__/AndMatcher.test.ts index ac4b075..34828df 100644 --- a/src/matchers/__tests__/AndMatcher.test.ts +++ b/src/matchers/__tests__/AndMatcher.test.ts @@ -1,6 +1,8 @@ import * as httpMocks from 'node-mocks-http' import { - AndMatcher, ExactUrlPathnameMatcher, MethodMatcher, + AndMatcher, + ExactUrlPathnameMatcher, + MethodMatcher, } from '..' it('none match', () => { diff --git a/src/matchers/__tests__/ExactQueryMatcherMatcher.test.ts b/src/matchers/__tests__/ExactQueryMatcherMatcher.test.ts index f637d4a..a642edf 100644 --- a/src/matchers/__tests__/ExactQueryMatcherMatcher.test.ts +++ b/src/matchers/__tests__/ExactQueryMatcherMatcher.test.ts @@ -1,5 +1,7 @@ import * as httpMocks from 'node-mocks-http' -import { ExactQueryMatcher } from '..' +import { + ExactQueryMatcher, +} from '..' const matcher = new ExactQueryMatcher({ mustPresent: true, diff --git a/src/matchers/__tests__/ExactUrlPathnameMatcher.test.ts b/src/matchers/__tests__/ExactUrlPathnameMatcher.test.ts index f5925e8..fd107af 100644 --- a/src/matchers/__tests__/ExactUrlPathnameMatcher.test.ts +++ b/src/matchers/__tests__/ExactUrlPathnameMatcher.test.ts @@ -1,5 +1,7 @@ import * as httpMocks from 'node-mocks-http' -import { ExactUrlPathnameMatcher } from '..' +import { + ExactUrlPathnameMatcher, +} from '..' it('not match empty', () => { const result = new ExactUrlPathnameMatcher(['/test']) diff --git a/src/matchers/__tests__/MethodMatcher.test.ts b/src/matchers/__tests__/MethodMatcher.test.ts index c17cb4c..d0f3c32 100644 --- a/src/matchers/__tests__/MethodMatcher.test.ts +++ b/src/matchers/__tests__/MethodMatcher.test.ts @@ -1,5 +1,7 @@ import * as httpMocks from 'node-mocks-http' -import { MethodMatcher } from '..' +import { + MethodMatcher, +} from '..' it('not match', () => { const result = new MethodMatcher(['POST']) diff --git a/src/matchers/__tests__/OrMatcher.test.ts b/src/matchers/__tests__/OrMatcher.test.ts index b1922d7..0f709fb 100644 --- a/src/matchers/__tests__/OrMatcher.test.ts +++ b/src/matchers/__tests__/OrMatcher.test.ts @@ -1,6 +1,8 @@ import * as httpMocks from 'node-mocks-http' import { - ExactUrlPathnameMatcher, MethodMatcher, OrMatcher, + ExactUrlPathnameMatcher, + MethodMatcher, + OrMatcher, } from '..' it('none match', () => { diff --git a/src/matchers/__tests__/RegExpUrlMatcher.test.ts b/src/matchers/__tests__/RegExpUrlMatcher.test.ts index a3ae318..a5760ac 100644 --- a/src/matchers/__tests__/RegExpUrlMatcher.test.ts +++ b/src/matchers/__tests__/RegExpUrlMatcher.test.ts @@ -1,5 +1,7 @@ import * as httpMocks from 'node-mocks-http' -import { RegExpUrlMatcher } from '..' +import { + RegExpUrlMatcher, +} from '..' it('not match', () => { const result = new RegExpUrlMatcher([/^\/test$/]) diff --git a/src/matchers/index.ts b/src/matchers/index.ts index 8cb7adf..89e7c9e 100644 --- a/src/matchers/index.ts +++ b/src/matchers/index.ts @@ -1,9 +1,36 @@ -export { AndMatcher, AndMatcherResult } from './AndMatcher' -export { EndpointMatcher } from './EndpointMatcher' -export { ExactQueryMatcher } from './ExactQueryMatcher' -export { ExactUrlPathnameMatcher, ExactUrlPathnameMatchResult } from './ExactUrlPathnameMatcher' -export type { ExtractMatchResult, Matcher } from './Matcher' -export { isMatched, Matched, MatchResult } from './MatchResult' -export { Method, MethodMatcher, MethodMatchResult } from './MethodMatcher' -export { OrMatcher, OrMatcherResult } from './OrMatcher' -export { RegExpUrlMatcher, RegExpUrlMatchResult } from './RegExpUrlMatcher' +export { + AndMatcher, + AndMatcherResult, +} from './AndMatcher' +export { + EndpointMatcher, +} from './EndpointMatcher' +export { + ExactQueryMatcher, +} from './ExactQueryMatcher' +export { + ExactUrlPathnameMatcher, + ExactUrlPathnameMatchResult, +} from './ExactUrlPathnameMatcher' +export type { + ExtractMatchResult, + Matcher, +} from './Matcher' +export { + isMatched, + Matched, + MatchResult, +} from './MatchResult' +export { + Method, + MethodMatcher, + MethodMatchResult, +} from './MethodMatcher' +export { + OrMatcher, + OrMatcherResult, +} from './OrMatcher' +export { + RegExpUrlMatcher, + RegExpUrlMatchResult, +} from './RegExpUrlMatcher' diff --git a/src/router.ts b/src/router.ts index 808fc69..28b436c 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,14 +1,19 @@ import { - IncomingMessage, ServerResponse, + IncomingMessage, + ServerResponse, } from 'http' import { - MatchResult, Matched, Matcher, isMatched, + MatchResult, + Matched, + Matcher, + isMatched, } from './matchers' -export type Handler = ( +export type Handler> = ( req: IncomingMessage, res: ServerResponse, - matchResult: Matched) => any // eslint-disable-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: D) => any export type MatchedHandler = Handler>