Skip to content

Commit

Permalink
feat(router): allow connecting user router via mount route (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS authored Aug 28, 2023
1 parent cfd6b5b commit aded6ed
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 52 deletions.
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"typescript": "^4.5.0"
},
"dependencies": {
"@gravity-ui/nodekit": "^0.2.0",
"@gravity-ui/nodekit": "^0.6.0",
"body-parser": "^1.20.1",
"cookie-parser": "1.4.6",
"express": "^4.18.2",
Expand Down
4 changes: 2 additions & 2 deletions src/base-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Express} from 'express';
import type {Express} from 'express';
import {v4 as uuidv4} from 'uuid';
import {AppContext} from '@gravity-ui/nodekit';
import type {AppContext} from '@gravity-ui/nodekit';
import {DEFAULT_REQUEST_ID_HEADER} from './constants';

export function setupBaseMiddleware(ctx: AppContext, expressApp: Express) {
Expand Down
6 changes: 3 additions & 3 deletions src/expresskit.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import fs from 'fs';

import express, {Express} from 'express';
import {AppConfig, NodeKit} from '@gravity-ui/nodekit';
import {AppRoutes} from './types';
import {Server} from 'http';
import type {AppConfig, NodeKit} from '@gravity-ui/nodekit';
import type {AppRoutes} from './types';
import type {Server} from 'http';
import {setupRoutes} from './router';
import {setupBaseMiddleware} from './base-middleware';
import {setupParsers} from './parsers';
Expand Down
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
export {ExpressKit} from './expresskit';
export {
export type {
AppAuthHandler,
AppMiddleware,
AppMountDescription,
AppMountHandler,
AppRouteDescription,
AppRouteHandler,
AppRouteParams,
AppRoutes,
AuthPolicy,
Request,
Response,
AppErrorHandler,
NextFunction,
} from './types';

export {AuthPolicy} from './types';
6 changes: 3 additions & 3 deletions src/parsers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {AppContext} from '@gravity-ui/nodekit';
import {Express} from 'express';
import type {AppContext} from '@gravity-ui/nodekit';
import type {Express} from 'express';
import cookieParser from 'cookie-parser';
import bodyParser from 'body-parser';
import {AppErrorHandler} from './types';
import type {AppErrorHandler} from './types';

export function setupParsers(ctx: AppContext, expressApp: Express) {
expressApp.use(cookieParser(ctx.config.expressCookieSecret));
Expand Down
89 changes: 62 additions & 27 deletions src/router.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import {AppContext} from '@gravity-ui/nodekit';
import {Express} from 'express';
import {AppErrorHandler, AppMiddleware, AppRoutes, AuthPolicy, ExpressFinalError} from './types';

const ALLOWED_METHODS = ['get', 'head', 'options', 'post', 'put', 'patch', 'delete'] as const;
type HttpMethod = (typeof ALLOWED_METHODS)[number];
import type {AppContext} from '@gravity-ui/nodekit';
import {Express, Router} from 'express';
import {
AppErrorHandler,
AppMiddleware,
AppMountDescription,
AppRouteDescription,
AppRouteHandler,
AppRoutes,
AuthPolicy,
ExpressFinalError,
HttpMethod,
HTTP_METHODS,
} from './types';

function isAllowedMethod(method: string): method is HttpMethod | 'mount' {
return HTTP_METHODS.includes(method as any) || method === 'mount';

Check warning on line 17 in src/router.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected any. Specify a different type
}

function wrapMiddleware(fn: AppMiddleware, i?: number): AppMiddleware {
const result: AppMiddleware = async (req, res, next) => {
Expand Down Expand Up @@ -31,30 +43,44 @@ function wrapMiddleware(fn: AppMiddleware, i?: number): AppMiddleware {
return result;
}

export function setupRoutes(ctx: AppContext, expressApp: Express, routes: AppRoutes) {
Object.keys(routes).forEach((routeKey) => {
const rawRoute = routes[routeKey];
const route = typeof rawRoute === 'function' ? {handler: rawRoute} : rawRoute;
const controllerName = route.handler.name || 'unnamedController';
const UNNAMED_CONTROLLER = 'unnamedController';
function wrapRouteHandler(fn: AppRouteHandler, handlerName?: string) {
const handlerNameLocal = handlerName || fn.name || UNNAMED_CONTROLLER;

const handler: AppMiddleware = (req, res, next) => {
req.ctx = req.originalContext.create(handlerNameLocal);
if (req.routeInfo.handlerName !== handlerNameLocal) {
if (req.routeInfo.handlerName === UNNAMED_CONTROLLER) {
req.routeInfo.handlerName = handlerNameLocal;
} else {
req.routeInfo.handlerName = `${req.routeInfo.handlerName}(${handlerNameLocal})`;
}
}
Promise.resolve(fn(req, res))
.catch(next)
.finally(() => {
req.ctx.end();
req.ctx = req.originalContext;
});
};

Object.defineProperty(handler, 'name', {value: handlerNameLocal});

return handler;
}

export function setupRoutes(ctx: AppContext, expressApp: Express, routes: AppRoutes) {
Object.entries(routes).forEach(([routeKey, rawRoute]) => {
const routeKeyParts = routeKey.split(/\s+/);
const method: HttpMethod = routeKeyParts[0].toLowerCase() as HttpMethod;
const method = routeKeyParts[0].toLowerCase();
const routePath = routeKeyParts[1];

if (!ALLOWED_METHODS.includes(method)) {
if (!isAllowedMethod(method)) {
throw new Error(`Unknown http method "${method}" for route "${routePath}"`);
}

const handler: AppMiddleware = (req, res, next) => {
req.ctx = req.originalContext.create(controllerName);
Promise.resolve(route.handler(req, res))
.catch(next)
.finally(() => {
req.ctx.end();
req.ctx = req.originalContext;
});
};
Object.defineProperty(handler, 'name', {value: controllerName});
const route: AppMountDescription | AppRouteDescription =
typeof rawRoute === 'function' ? {handler: rawRoute} : rawRoute;

const {
authPolicy: routeAuthPolicy,
Expand All @@ -64,14 +90,15 @@ export function setupRoutes(ctx: AppContext, expressApp: Express, routes: AppRou
...restRouteInfo
} = route;
const authPolicy = routeAuthPolicy || ctx.config.appAuthPolicy || AuthPolicy.disabled;
const routeInfoMiddleware: AppMiddleware = (req, res, next) => {
Object.assign(req.routeInfo, restRouteInfo, {authPolicy});
const handlerName = restRouteInfo.handlerName || route.handler.name || UNNAMED_CONTROLLER;
const routeInfoMiddleware: AppMiddleware = function routeInfoMiddleware(req, res, next) {
Object.assign(req.routeInfo, restRouteInfo, {authPolicy, handlerName});

res.on('finish', () => {
if (req.ctx.config.appTelemetryChEnableSelfStats) {
req.ctx.stats({
service: 'self',
action: controllerName,
action: req.routeInfo.handlerName || UNNAMED_CONTROLLER,
responseStatus: res.statusCode,
requestId: req.id,
requestTime: req.originalContext.getTime(), //We have to use req.originalContext here to get full time
Expand Down Expand Up @@ -104,7 +131,15 @@ export function setupRoutes(ctx: AppContext, expressApp: Express, routes: AppRou

const wrappedMiddleware = routeMiddleware.map(wrapMiddleware);

expressApp[method](routePath, wrappedMiddleware, handler);
if (method === 'mount') {
// eslint-disable-next-line new-cap
const router = Router({mergeParams: true});
const targetApp = (route as AppMountDescription).handler({router, wrapRouteHandler});
expressApp.use(routePath, wrappedMiddleware, targetApp || router);
} else {
const handler = wrapRouteHandler((route as AppRouteDescription).handler, handlerName);
expressApp[method](routePath, wrappedMiddleware, handler);
}
});

if (ctx.config.appFinalErrorHandler) {
Expand Down
35 changes: 29 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import {AppContext} from '@gravity-ui/nodekit';
import bodyParser from 'body-parser';
import {ErrorRequestHandler, NextFunction, Request, Response, RequestHandler} from 'express';
import type {AppContext} from '@gravity-ui/nodekit';
import type bodyParser from 'body-parser';
import type {
ErrorRequestHandler,
NextFunction,
Request,
Response,
RequestHandler,
Router,
} from 'express';

declare global {
// eslint-disable-next-line
Expand Down Expand Up @@ -42,7 +49,7 @@ declare module '@gravity-ui/nodekit' {

appFinalErrorHandler?: ErrorRequestHandler;
appAuthHandler?: RequestHandler;
appAuthPolicy?: AuthPolicy;
appAuthPolicy?: `${AuthPolicy}`;

appBeforeAuthMiddleware?: RequestHandler[];
appAfterAuthMiddleware?: RequestHandler[];
Expand All @@ -63,7 +70,8 @@ export enum AuthPolicy {
}

export interface AppRouteParams {
authPolicy?: AuthPolicy;
authPolicy?: `${AuthPolicy}`;
handlerName?: string;
}

export interface AppRouteDescription extends AppRouteParams {
Expand All @@ -73,8 +81,23 @@ export interface AppRouteDescription extends AppRouteParams {
afterAuth?: AppMiddleware[];
}

export const HTTP_METHODS = ['get', 'head', 'options', 'post', 'put', 'patch', 'delete'] as const;
export type HttpMethod = (typeof HTTP_METHODS)[number];

export interface AppMountHandler {
(args: {
router: Router;
wrapRouteHandler: (fn: AppRouteHandler, handlerName?: string) => AppMiddleware;
}): void | Router;
}

export interface AppMountDescription extends Omit<AppRouteDescription, 'handler'> {
handler: AppMountHandler;
}

export interface AppRoutes {
[methodAndPath: string]: AppRouteHandler | AppRouteDescription;
[methodAndPath: `${Uppercase<HttpMethod>} ${string}`]: AppRouteHandler | AppRouteDescription;
[mountPath: `MOUNT ${string}`]: AppMountHandler | AppMountDescription;
}

interface ParamsDictionary {
Expand Down

0 comments on commit aded6ed

Please sign in to comment.